diff --git a/examples/Chronometer/Chronometer/Chronometer.cs b/examples/Chronometer/Chronometer/Chronometer.cs new file mode 100644 index 0000000..51db4bb --- /dev/null +++ b/examples/Chronometer/Chronometer/Chronometer.cs @@ -0,0 +1,232 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +using System.ComponentModel; +using System.Diagnostics; + +namespace WatchModels +{ + public interface ILapRecorder + { + void Mark(int hours, int minutes, int seconds, int milliseconds); + } + + public class Chronometer : INotifyPropertyChanged + { + public double Hours + { + get => hours; + private set => SetProperty(ref hours, value, nameof(Hours)); + } + public double Minutes + { + get => minutes; + private set => SetProperty(ref minutes, value, nameof(Minutes)); + } + public double Seconds + { + get => seconds; + private set => SetProperty(ref seconds, value, nameof(Seconds)); + } + + public int Day + { + get => day; + private set => SetProperty(ref day, value, nameof(Day)); + } + + public double ElapsedHours + { + get => elapsedHours; + private set => SetProperty(ref elapsedHours, value, nameof(ElapsedHours)); + } + + public double ElapsedMinutes + { + get => elapsedMinutes; + private set => SetProperty(ref elapsedMinutes, value, nameof(ElapsedMinutes)); + } + + public double ElapsedSeconds + { + get => elapsedSeconds; + private set => SetProperty(ref elapsedSeconds, value, nameof(ElapsedSeconds)); + } + + public double ElapsedMilliseconds + { + get => elapsedMilliseconds; + private set => SetProperty(ref elapsedMilliseconds, value, nameof(ElapsedMilliseconds)); + } + + public bool Started + { + get => Stopwatch.IsRunning; + private set + { + if (value == Stopwatch.IsRunning) + return; + if (value) + Stopwatch.Start(); + else + Stopwatch.Stop(); + NotifyPropertyChanged(nameof(Started)); + } + } + + public bool AdjustDayMode + { + get => adjustDayMode; + set + { + if (value == adjustDayMode) + return; + adjustDayMode = value; + if (adjustDayMode) { + if (adjustTimeMode) { + adjustTimeMode = false; + NotifyPropertyChanged(nameof(AdjustTimeMode)); + } + Started = false; + Reset(); + Time.Stop(); + baseTime = baseTime.Add(Time.Elapsed); + Time.Reset(); + } else if (!adjustTimeMode) { + Time.Start(); + } + NotifyPropertyChanged(nameof(AdjustDayMode)); + } + } + + public bool AdjustTimeMode + { + get => adjustTimeMode; + set + { + if (value == adjustTimeMode) + return; + adjustTimeMode = value; + if (adjustTimeMode) { + if (adjustDayMode) { + adjustDayMode = false; + NotifyPropertyChanged(nameof(AdjustDayMode)); + } + Started = false; + Reset(); + Time.Stop(); + baseTime = baseTime.Add(Time.Elapsed); + Time.Reset(); + } else if (!adjustDayMode) { + Time.Start(); + } + NotifyPropertyChanged(nameof(AdjustTimeMode)); + } + } + + public ILapRecorder LapRecorder { get; set; } + + public Chronometer() + { + Mechanism = new Task(async () => await MechanismLoopAsync(), MechanismLoop.Token); + Mechanism.Start(); + baseTime = DateTime.Now; + Time.Start(); + } + + public void StartStop() + { + if (AdjustTimeMode || AdjustDayMode) + return; + Started = !Started; + } + + public void Reset() + { + ElapsedHours = ElapsedMinutes = ElapsedSeconds = ElapsedMilliseconds = 0; + if (!Stopwatch.IsRunning) { + Stopwatch.Reset(); + } else { + LapRecorder?.Mark( + Stopwatch.Elapsed.Hours, + Stopwatch.Elapsed.Minutes, + Stopwatch.Elapsed.Seconds, + Stopwatch.Elapsed.Milliseconds); + Stopwatch.Restart(); + } + } + + public void Adjust(int delta) + { + if (AdjustDayMode) + baseTime = baseTime.AddDays(delta); + else if (AdjustTimeMode) + baseTime = baseTime.AddSeconds(delta * 60); + Refresh(); + } + + public event PropertyChangedEventHandler PropertyChanged; + private void NotifyPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private void Refresh() + { + DateTime now = baseTime.Add(Time.Elapsed); + Day = now.Day; + + TimeSpan time = now.TimeOfDay; + Hours = time.TotalHours; + Minutes = time.TotalMinutes + - (time.Hours * 60); + Seconds = time.TotalSeconds + - (time.Hours * 3600) - (time.Minutes * 60); + + TimeSpan elapsed = Stopwatch.Elapsed; + ElapsedHours = elapsed.TotalHours; + ElapsedMinutes = elapsed.TotalMinutes + - (elapsed.Hours * 60); + ElapsedSeconds = elapsed.TotalSeconds + - (elapsed.Hours * 3600) - (elapsed.Minutes * 60); + ElapsedMilliseconds = elapsed.TotalMilliseconds + - (elapsed.Hours * 3600000) - (elapsed.Minutes * 60000) - (elapsed.Seconds * 1000); + } + + private async Task MechanismLoopAsync() + { + while (!MechanismLoop.IsCancellationRequested) { + await Task.Delay(5); + Refresh(); + } + } + + private void SetProperty(ref T currentValue, T newValue, string name) + { + if (newValue.Equals(currentValue)) + return; + currentValue = newValue; + NotifyPropertyChanged(name); + } + + private double hours; + private double minutes; + private double seconds; + private int day; + private double elapsedHours; + private double elapsedMinutes; + private double elapsedSeconds; + private double elapsedMilliseconds; + private bool adjustDayMode; + private bool adjustTimeMode; + + private DateTime baseTime; + private Stopwatch Time { get; } = new(); + private Stopwatch Stopwatch { get; } = new(); + + private CancellationTokenSource MechanismLoop { get; } = new(); + private Task Mechanism { get; } + } +} diff --git a/examples/Chronometer/Chronometer/ChronometerModel.csproj b/examples/Chronometer/Chronometer/ChronometerModel.csproj new file mode 100644 index 0000000..141e38f --- /dev/null +++ b/examples/Chronometer/Chronometer/ChronometerModel.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + disable + + + diff --git a/examples/Chronometer/QmlChronometer/OutDir.props b/examples/Chronometer/QmlChronometer/OutDir.props new file mode 100644 index 0000000..c7a9463 --- /dev/null +++ b/examples/Chronometer/QmlChronometer/OutDir.props @@ -0,0 +1,11 @@ + + + + + + bin\$(VisualStudioVersion)\$(Platform)\$(Configuration)\ + obj\$(VisualStudioVersion)\$(Platform)\$(Configuration)\ + + + + \ No newline at end of file diff --git a/examples/Chronometer/QmlChronometer/QChronometer/AdjustmentWheel.qml b/examples/Chronometer/QmlChronometer/QChronometer/AdjustmentWheel.qml new file mode 100644 index 0000000..0c5d904 --- /dev/null +++ b/examples/Chronometer/QmlChronometer/QChronometer/AdjustmentWheel.qml @@ -0,0 +1,125 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes + +////////////////////////////////////////////////////////////////////// +// Adjustment wheel +Tumbler { + id: adjustmentWheel + model: 100 + property int startX + property int startY + + x: startX + ((chrono.adjustDayMode || chrono.adjustTimeMode) ? 8 : 0) + Behavior on x { + SpringAnimation { spring: 3; damping: 0.5 } + } + y: startY; width: 25; height: 70 + + enabled: chrono.adjustDayMode || chrono.adjustTimeMode + onEnabledChanged: { + if (enabled) { + chrono.reset(); + if (!chrono.started) { + laps.reset(); + showLap = currentLap; + } + } + } + + property int lastIndex: 0 + property var lastTime: Date.UTC(0) + property double turnSpeed: 0.0 + onCurrentIndexChanged: { + if (currentIndex != lastIndex) { + var i1 = currentIndex; + var i0 = lastIndex; + if (Math.abs(i1 - i0) > 50) { + if (i1 < i0) + i1 += 100; + else + i0 += 100; + } + var deltaX = i1 - i0; + chrono.adjust(deltaX); + lastIndex = currentIndex; + + var deltaT = Date.now() - lastTime; + lastTime += deltaT; + turnSpeed = Math.abs((deltaX * 1000) / deltaT); + } + } + + MouseArea { + anchors.fill: parent + onWheel: function(wheel) { + turn(wheel.angleDelta.y > 0 ? 1 : -1); + } + } + + function turn(delta) { + if (enabled) { + adjustmentWheel.currentIndex = (100 + adjustmentWheel.currentIndex + (delta)) % 100; + } + } + + ////////////////////////////////////////////////////////////////////// + // Wheel surface + background: Rectangle { + anchors.fill: adjustmentWheel + color: gray6 + border.color: gray8 + border.width: 2 + radius: 2 + } + + ////////////////////////////////////////////////////////////////////// + // Notches + delegate: Component { + Item { + Rectangle { + x: 4; y: 0; width: Tumbler.tumbler.width - 8; height: 2 + color: gray3 + } + } + } + + ////////////////////////////////////////////////////////////////////// + // Wheel shadow + Rectangle { + anchors.centerIn: parent + width: parent.width; height: parent.height + gradient: Gradient { + GradientStop { position: 0.0; color: gray3 } + GradientStop { position: 0.3; color: "transparent" } + GradientStop { position: 0.5; color: "transparent" } + GradientStop { position: 0.7; color: "transparent" } + GradientStop { position: 1.0; color: gray3 } + } + } + + ////////////////////////////////////////////////////////////////////// + // Wheel axis + Rectangle { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: parent.startX - parent.x + 2 + width: 20; height: 20 + z: -1 + gradient: Gradient { + GradientStop { position: 0.0; color: gray3 } + GradientStop { position: 0.3; color: gray6 } + GradientStop { position: 0.5; color: gray6 } + GradientStop { position: 0.7; color: gray6 } + GradientStop { position: 1.0; color: gray3 } + } + border.color: "transparent" + } +} diff --git a/examples/Chronometer/QmlChronometer/QChronometer/InsetDial.qml b/examples/Chronometer/QmlChronometer/QChronometer/InsetDial.qml new file mode 100644 index 0000000..7c07106 --- /dev/null +++ b/examples/Chronometer/QmlChronometer/QChronometer/InsetDial.qml @@ -0,0 +1,65 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes + +////////////////////////////////////////////////////////////////// +// Inset dial +Item { + id: insetDial + property string handSource + property string pinSource + property int centerX + property int centerY + property double rotationAngle + ////////////////////////////////////////////////////////////////// + // Hand + Image { + source: insetDial.handSource + transform: Rotation { + origin.x: insetDial.centerX; origin.y: insetDial.centerY + Behavior on angle { + SpringAnimation { spring: 3; damping: 0.5; modulus: 360 } + } + angle: insetDial.rotationAngle + } + } + ////////////////////////////////////////////////////////////////// + // Highlight + Shape { + id: insetDialHighlight + anchors.fill: insetDial + opacity: 0.5 + property var centerX: insetDial.centerX + property var centerY: insetDial.centerY + property var color: + showLap == lastLap ? blue86 : showLap == bestLap ? green86 : "transparent" + ShapePath { + startX: insetDialHighlight.centerX; startY: insetDialHighlight.centerY + strokeColor: "transparent" + PathAngleArc { + centerX: insetDialHighlight.centerX; centerY: insetDialHighlight.centerY + radiusX: 55; radiusY: 55; startAngle: 0; sweepAngle: 360 + } + fillGradient: RadialGradient { + centerX: insetDialHighlight.centerX; centerY: insetDialHighlight.centerY + centerRadius: 55; + focalX: centerX; focalY: centerY + GradientStop { position: 0; color: "transparent" } + GradientStop { position: 0.6; color: "transparent" } + GradientStop { position: 1; color: insetDialHighlight.color } + } + } + } + ////////////////////////////////////////////////////////////////// + // Center pin + Image { + source: insetDial.pinSource + } +} diff --git a/examples/Chronometer/QmlChronometer/QChronometer/WatchButton.qml b/examples/Chronometer/QmlChronometer/QChronometer/WatchButton.qml new file mode 100644 index 0000000..6894c69 --- /dev/null +++ b/examples/Chronometer/QmlChronometer/QChronometer/WatchButton.qml @@ -0,0 +1,31 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes + +RoundButton { + id: watchButton + property string buttonText + property bool split + property string color + width: 70; height: 70; radius: 25 + palette.button: color + Text { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + font.bold: true + text: watchButton.buttonText + } + Rectangle { + visible: watchButton.split + color: "black" + width: 55; height: 1 + anchors.centerIn: parent + } +} diff --git a/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj b/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj new file mode 100644 index 0000000..8fb3def --- /dev/null +++ b/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj @@ -0,0 +1,158 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {5A1E5424-CDB1-4776-A9CC-01B2CD57F516} + QtVS_v304 + 10.0.19041.0 + 10.0.19041.0 + $(MSBuildProjectDirectory)\QtMsBuild + + + + Application + v143 + + + Application + v143 + + + + + + + $(DefaultQtVersion) + quick;core + debug + + + $(DefaultQtVersion) + quick;core + release + + + + + + + + + + + + + + + + + + + ..\..\..\include;$(IncludePath) + bin\$(VisualStudioVersion)\$(Platform)\$(Configuration)\ + obj\$(VisualStudioVersion)\$(Platform)\$(Configuration)\ + + + ..\..\..\include;$(IncludePath) + bin\$(VisualStudioVersion)\$(Platform)\$(Configuration)\ + obj\$(VisualStudioVersion)\$(Platform)\$(Configuration)\ + + + + true + true + ProgramDatabase + Disabled + MultiThreadedDebugDLL + + + Windows + true + + + + + true + true + None + MaxSpeed + MultiThreadedDLL + + + Windows + false + + + + + + + + + + + + true + Document + true + + + + + + + + + {d3d5ed0e-fd65-4f54-b2d8-d3a380f247a2} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj.filters b/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj.filters new file mode 100644 index 0000000..30ca20d --- /dev/null +++ b/examples/Chronometer/QmlChronometer/QmlChronometer.vcxproj.filters @@ -0,0 +1,156 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {99349809-55BA-4b9d-BF79-8FDBB0286EB3} + ui + + + {639EADAA-A684-42e4-A9AD-28FC9BCB8F7C} + ts + + + {a17873af-389e-48bb-9b3d-0d6fcd150a7f} + + + {1f892826-c7e1-49be-b4eb-0007d1308298} + + + {9f5c583a-a421-4bcf-9f22-59ef59da1b60} + .qml + + + + + Source Files + + + Resource Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + + + + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + Header Files\qtdotnet + + + + + Resource Files\png + + + Resource Files\png + + + Resource Files\png + + + Resource Files\png + + + Resource Files\png + + + Resource Files\png + + + Resource Files\png + + + Resource Files\png + + + Resource Files\png + + + Resource Files\png + + + Resource Files\png + + + + + Source Files\qml + + + Source Files\qml + + + Source Files\qml + + + Source Files + + + \ No newline at end of file diff --git a/examples/Chronometer/QmlChronometer/content/center.png b/examples/Chronometer/QmlChronometer/content/center.png new file mode 100644 index 0000000..a6c610f Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/center.png differ diff --git a/examples/Chronometer/QmlChronometer/content/chrono_1_center.png b/examples/Chronometer/QmlChronometer/content/chrono_1_center.png new file mode 100644 index 0000000..eb57973 Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/chrono_1_center.png differ diff --git a/examples/Chronometer/QmlChronometer/content/chrono_1_hand.png b/examples/Chronometer/QmlChronometer/content/chrono_1_hand.png new file mode 100644 index 0000000..d507b5e Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/chrono_1_hand.png differ diff --git a/examples/Chronometer/QmlChronometer/content/chrono_2_center.png b/examples/Chronometer/QmlChronometer/content/chrono_2_center.png new file mode 100644 index 0000000..1552403 Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/chrono_2_center.png differ diff --git a/examples/Chronometer/QmlChronometer/content/chrono_2_hand.png b/examples/Chronometer/QmlChronometer/content/chrono_2_hand.png new file mode 100644 index 0000000..390b0e4 Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/chrono_2_hand.png differ diff --git a/examples/Chronometer/QmlChronometer/content/chrono_3_center.png b/examples/Chronometer/QmlChronometer/content/chrono_3_center.png new file mode 100644 index 0000000..6dbfb64 Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/chrono_3_center.png differ diff --git a/examples/Chronometer/QmlChronometer/content/chrono_3_needle.png b/examples/Chronometer/QmlChronometer/content/chrono_3_needle.png new file mode 100644 index 0000000..6499d0a Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/chrono_3_needle.png differ diff --git a/examples/Chronometer/QmlChronometer/content/hour_hand.png b/examples/Chronometer/QmlChronometer/content/hour_hand.png new file mode 100644 index 0000000..2bd7ad7 Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/hour_hand.png differ diff --git a/examples/Chronometer/QmlChronometer/content/minute_hand.png b/examples/Chronometer/QmlChronometer/content/minute_hand.png new file mode 100644 index 0000000..ee352fe Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/minute_hand.png differ diff --git a/examples/Chronometer/QmlChronometer/content/second_hand.png b/examples/Chronometer/QmlChronometer/content/second_hand.png new file mode 100644 index 0000000..33f4504 Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/second_hand.png differ diff --git a/examples/Chronometer/QmlChronometer/content/watchface.png b/examples/Chronometer/QmlChronometer/content/watchface.png new file mode 100644 index 0000000..e336d5e Binary files /dev/null and b/examples/Chronometer/QmlChronometer/content/watchface.png differ diff --git a/examples/Chronometer/QmlChronometer/main.cpp b/examples/Chronometer/QmlChronometer/main.cpp new file mode 100644 index 0000000..94f8f45 --- /dev/null +++ b/examples/Chronometer/QmlChronometer/main.cpp @@ -0,0 +1,29 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#include "qchronometer.h" +#include "qlaprecorder.h" + +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + QQmlApplicationEngine engine; + + QLapRecorder lapRecorder; + engine.rootContext()->setContextProperty("laps", &lapRecorder); + + QChronometer chrono(lapRecorder); + engine.rootContext()->setContextProperty("chrono", &chrono); + + engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); + if (engine.rootObjects().isEmpty()) + return -1; + + return app.exec(); +} diff --git a/examples/Chronometer/QmlChronometer/main.qml b/examples/Chronometer/QmlChronometer/main.qml new file mode 100644 index 0000000..059c8ae --- /dev/null +++ b/examples/Chronometer/QmlChronometer/main.qml @@ -0,0 +1,453 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes + +import "QChronometer" + +Window { + visible: true + property int windowWidth: 544 + (showHelp.checked ? 265 : 0) + Behavior on windowWidth { SmoothedAnimation { duration: 750 } } + width: windowWidth; minimumWidth: windowWidth; maximumWidth: windowWidth + height: 500; minimumHeight: height; maximumHeight: height + title: "QML Chronometer" + + ////////////////////////////////////////////////////////////////// + // Colors + readonly property var gray1: "#111111" + readonly property var gray3: "#333333" + readonly property var gray4: "#444444" + readonly property var gray6: "#666666" + readonly property var redF6: "#FF6666" + readonly property var green86: "#668866" + readonly property var blue86: "#666688" + readonly property var gray8: "#888888" + + ////////////////////////////////////////////////////////////////// + // Window background color + color: gray4 + + ////////////////////////////////////////////////////////////////// + // Stopwatch mode + property int showLap: currentLap + // 0: showing current lap, or stopwatch not running + readonly property int currentLap: 0 + // 1: showing last recorded lap + readonly property int lastLap: 1 + // 2: showing best recorded lap + readonly property int bestLap: 2 + + ////////////////////////////////////////////////////////////////// + // Watch + Image { + id: watch + source: "watchface.png" + } + + ////////////////////////////////////////////////////////////////// + // Rim + Rectangle { + color: "transparent" + border { color: gray8; width: 3 } + anchors.centerIn: watch + width: watch.width; height: watch.height; radius: watch.width / 2 + } + + ////////////////////////////////////////////////////////////////// + // Calendar + Text { + enabled: false + x: 345; y: 295; width: 32; height: 22 + transform: Rotation { origin.x: 0; origin.y: 0; angle: 30 } + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font { bold: true; pointSize: 11 } + text: chrono.day + } + + ////////////////////////////////////////////////////////////////// + // Inset dial #1 (above-left of center; 30x minutes) + InsetDial { + id: insetDial1 + handSource: "/chrono_1_hand.png" + pinSource: "/chrono_1_center.png" + centerX: 176; centerY: 208 + rotationAngle: + (showLap == lastLap) ? ( + ///////////////////////////////////////////////////////////// + // Show minutes of previous lap + (laps.lastMinutes % 30) * 12 + ) : (showLap == bestLap) ? ( + ///////////////////////////////////////////////////////////// + // Show minutes of best lap + (laps.bestMinutes % 30) * 12 + ) : ( + ///////////////////////////////////////////////////////////// + // Show minutes of current lap + (chrono.elapsedMinutes % 30) * 12 + ) + } + + //////////////////////////////////////////////////////////////////////////////////// + // Inset chrono counter #2 (above-right of center; 10x 1/10 second or 10x hours) + InsetDial { + id: insetDial2 + handSource: "/chrono_2_hand.png" + pinSource: "/chrono_2_center.png" + centerX: 325; centerY: 208 + rotationAngle: + (showLap == lastLap) ? ( + ///////////////////////////////////////////////////////////// + // Show previous lap + (laps.lastHours == 0 && laps.lastMinutes < 30) ? ( + ///////////////////////////////////////////////////////////// + // 1/10 seconds + laps.lastMilliseconds * 360 / 1000 + ) : ( + ///////////////////////////////////////////////////////////// + // hours + ((laps.lastHours % 10) + (laps.lastMinutes / 60)) * 360 / 10 + ) + ) : (showLap == bestLap) ? ( + ///////////////////////////////////////////////////////////// + // Show best lap + (laps.bestHours == 0 && laps.bestMinutes < 30) ? ( + ///////////////////////////////////////////////////////////// + // 1/10 seconds + laps.bestMilliseconds * 360 / 1000 + ) : ( + ///////////////////////////////////////////////////////////// + // hours + ((laps.bestHours % 10) + (laps.bestMinutes / 60)) * 360 / 10 + ) + ) : ( + ///////////////////////////////////////////////////////////// + // Show current lap + (chrono.elapsedHours < 1 && chrono.elapsedMinutes < 30) ? ( + ///////////////////////////////////////////////////////////// + // 1/10 seconds + chrono.elapsedMilliseconds * 360 / 1000 + ) : ( + ///////////////////////////////////////////////////////////// + // hours + (chrono.elapsedHours % 10) * 360 / 10 + ) + ) + } + + ////////////////////////////////////////////////////////////////////// + // Inset chrono counter #3 (below center; 60x seconds) + InsetDial { + id: insetDial3 + handSource: "/chrono_3_needle.png" + pinSource: "/chrono_3_center.png" + centerX: 250; centerY: 336 + rotationAngle: 150 + ( + (showLap == lastLap) ? ( + ///////////////////////////////////////////////////////////// + // Show seconds of previous lap + laps.lastSeconds * 6 + ) : (showLap == bestLap) ? ( + ///////////////////////////////////////////////////////////// + // Show seconds of best lap + laps.bestSeconds * 6 + ) : ( + ///////////////////////////////////////////////////////////// + // Show seconds of current (wall-clock) time + chrono.seconds * 6 + )) + } + + ////////////////////////////////////////////////////////////////////// + // Hours hand for current (wall-clock) time + Image { + id: hoursHand; + source: "hour_hand.png" + transform: Rotation { + origin.x: 249; origin.y: 251 + angle: 110 + (chrono.hours % 12) * 30 + Behavior on angle { + enabled: adjustmentWheel.turnSpeed < 75 + SpringAnimation { spring: 3; damping: 0.5; modulus: 360 } + } + } + } + + ////////////////////////////////////////////////////////////////////// + // Minutes hand for current (wall-clock) time + Image { + id: minutesHand; + source: "minute_hand.png" + transform: Rotation { + origin.x: 249; origin.y: 251 + angle: -108 + chrono.minutes * 6 + Behavior on angle { + enabled: adjustmentWheel.turnSpeed < 75 + SpringAnimation { spring: 3; damping: 0.5; modulus: 360 } + } + } + } + Image { + source: "center.png" + } + + ////////////////////////////////////////////////////////////////////// + // Stopwatch seconds hand + Image { + id: secondsHand; + source: "second_hand.png" + transform: Rotation { + origin.x: 250; origin.y: 250 + angle: chrono.elapsedSeconds * 6 + Behavior on angle { + SpringAnimation { spring: 3; damping: 0.5; modulus: 360 } + } + } + } + + ////////////////////////////////////////////////////////////////////// + // Adjustment wheel + AdjustmentWheel { + id: adjustmentWheel + startX: 498; startY: 215 + } + + ////////////////////////////////////////////////////////////////////// + // Adjust date + Switch { + id: adjustDay + x: 500; y: 290; padding: 0; spacing: -35 + palette.button: gray8; font { bold: true; pointSize: 9 } + text: "Date" + checked: chrono.adjustDayMode + onToggled: chrono.adjustDayMode = (position == 1) + } + + ////////////////////////////////////////////////////////////////////// + // Adjust time + Switch { + id: adjustTime + x: 500; y: 310; padding: 0; spacing: -35 + palette.button: gray8; font { bold: true; pointSize: 9 } + text: "Time" + checked: chrono.adjustTimeMode + onToggled: chrono.adjustTimeMode = (position == 1) + } + + ////////////////////////////////////////////////////////////////////// + // Stopwatch start/stop button + WatchButton { + id: buttonStartStop + x: 425; y: 5 + buttonText: "Start\n\nStop"; split: true + color: chrono.started ? redF6 : !enabled ? gray3 : gray6 + enabled: !chrono.adjustDayMode && !chrono.adjustTimeMode + onClicked: chrono.startStop() + } + + ////////////////////////////////////////////////////////////////////// + // Stopwatch lap/reset button + WatchButton { + id: buttonLapReset + x: 425; y: 425 + buttonText: "Lap\n\nReset"; split: true + color: !enabled ? gray3 : gray6 + enabled: chrono.started + || chrono.elapsedHours > 0 + || chrono.elapsedMinutes > 0 + || chrono.elapsedSeconds > 0 + || chrono.elapsedMilliseconds > 0 + || laps.lapCount > 0 + onClicked: { + chrono.reset(); + if (!chrono.started) { + laps.reset(); + showLap = currentLap; + } + } + } + + ////////////////////////////////////////////////////////////////////// + // Stopwatch last lap button + WatchButton { + id: buttonLastLap + x: 5; y: 425 + buttonText: "Last\nLap"; split: false + color: (showLap == lastLap) ? blue86 : !enabled ? gray3 : gray6 + enabled: laps.lapCount > 0 + onClicked: { + showLapTimer.stop(); + if (laps.lapCount > 0) { + showLap = (showLap != lastLap) ? lastLap : currentLap; + } + } + } + + ////////////////////////////////////////////////////////////////////// + // Stopwatch best lap button + WatchButton { + id: buttonBestLap + x: 5; y: 5 + buttonText: "Best\nLap"; split: false + color: (showLap == bestLap) ? green86 : !enabled ? gray3 : gray6 + enabled: laps.lapCount > 1 + onClicked: { + showLapTimer.stop(); + if (laps.lapCount > 1) { + showLap = (showLap != bestLap) ? bestLap : currentLap; + } + } + } + + ////////////////////////////////////////////////////////////////////// + // Timer to show last/best lap for 5 secs. after mark + Timer { + id: showLapTimer + interval: 5000 + running: false + repeat: false + onTriggered: showLap = currentLap + } + + ////////////////////////////////////////////////////////////////////// + // Lap events + Connections { + target: laps + + ////////////////////////////////////////////////////////////////////// + // Lap counter changed: new lap recorded, or lap counter reset + function onLapCountChanged() { + if (laps.lapCount > 0) { + showLap = lastLap; + showLapTimer.restart() + } + } + + ////////////////////////////////////////////////////////////////////// + // New best lap recorded + function onNewBestLap() { + if (laps.lapCount > 1) { + showLap = bestLap; + showLapTimer.restart() + } + } + } + + ////////////////////////////////////////////////////////////////////// + // Keyboard events + Shortcut { + sequence: " " + onActivated: buttonStartStop.clicked() + } + Shortcut { + sequences: [ "Return", "Enter" ] + onActivated: { + if (chrono.started) + buttonLapReset.clicked(); + } + } + Shortcut { + sequence: "Escape" + onActivated: { + if (chrono.adjustDayMode || chrono.adjustTimeMode) + chrono.adjustDayMode = chrono.adjustTimeMode = false; + else if (!chrono.started) + buttonLapReset.clicked() + else + showLap = currentLap; + } + } + Shortcut { + sequence: "Tab" + onActivated: buttonLastLap.clicked() + } + Shortcut { + sequence: "Shift+Tab" + onActivated: buttonBestLap.clicked() + } + Shortcut { + sequence: "Ctrl+D" + onActivated: { + adjustDay.toggle(); + adjustDay.onToggled(); + } + } + Shortcut { + sequence: "Ctrl+T" + onActivated: { + adjustTime.toggle(); + adjustTime.onToggled(); + } + } + Shortcut { + sequence: "Up" + onActivated: adjustmentWheel.turn(1) + } + Shortcut { + sequence: "Down" + onActivated: adjustmentWheel.turn(-1) + } + Shortcut { + sequence: "F1" + onActivated: showHelp.toggle() + } + + ////////////////////////////////////////////////////////////////////// + // Usage instructions + RoundButton { + id: showHelp + checkable: true + x: 524; y: 0; width: 20; height: 40 + palette.button: gray6 + radius: 0 + contentItem: Text { + font.bold: true + font.pointSize: 11 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: parent.checked ? "<" : "?" + } + } + Rectangle { + x: 544; y:0 + width: 265 + height: 500 + color: gray6 + border.width: 0 + Text { + anchors.fill: parent + anchors.topMargin: 10 + anchors.leftMargin: 5 + anchors.rightMargin: 5 + textFormat: Text.MarkdownText + wrapMode: Text.WordWrap + color: gray1 + text: "### Usage instructions + +The **hours and minutes hands** show the current (wall-clock) time. The **seconds hand** shows the +stopwatch elapsed seconds. **Inset dial #1** (above-left of center) shows elapsed minutes of +current/last/best lap. **Inset dial #2** (above-right of center) shows a 1/10th-second counter of +current/last/best lap. **Inset dial #3** (below center) shows seconds of current time, or elapsed +seconds of last/best lap. + +Press **Start|Stop** (shortcut: **[Space]**) to begin timing. While the stopwatch is running, press +**Lap|Reset** (shortcut: **[Enter]**) to record a lap. The stopwatch can memorize the last and the +best lap. Press **Last** **Lap** (shortcut: **[Tab]**) or **Best** **Lap** (shortcut: +**[Shift+Tab]**) to view the recorded time of the last or best lap. Press **Start|Stop** (shortcut: +**[Space]**) again to stop timing. Press **Lap|Reset** (shortcut: **[Esc]**) to reset stopwatch +counters and clear lap memory. + +Press the **Date** switch (shortcut: **[Ctrl+D]**) or **Time** switch (shortcut: **[Ctrl+T]**) to +enter adjustment mode. Turn the **adjustment wheel** (shortcut: **mouse wheel** or **[Up]** / +**[Down]**) to set the desired date or time. Press the active adjustment switch (or **[Esc]**) to +leave adjustment mode. Note: entering adjustment mode resets the stopwatch." + } + } +} diff --git a/examples/Chronometer/QmlChronometer/qchronometer.cpp b/examples/Chronometer/QmlChronometer/qchronometer.cpp new file mode 100644 index 0000000..16951a0 --- /dev/null +++ b/examples/Chronometer/QmlChronometer/qchronometer.cpp @@ -0,0 +1,163 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#include "qchronometer.h" +#include "qlaprecorder.h" + +#include + +struct QChronometerPrivate : QDotNetObject::IEventHandler +{ + QChronometerPrivate(QChronometer *q) + :q(q) + {} + + void handleEvent(const QString &eventName, QDotNetObject &sender, QDotNetObject &args) override + { + if (eventName != "PropertyChanged") + return; + + if (args.type().fullName() != QDotNetPropertyEvent::FullyQualifiedTypeName) + return; + + const auto propertyChangedEvent = args.cast(); + if (propertyChangedEvent.propertyName() == "Hours") + emit q->hoursChanged(); + else if (propertyChangedEvent.propertyName() == "Minutes") + emit q->minutesChanged(); + else if (propertyChangedEvent.propertyName() == "Seconds") + emit q->secondsChanged(); + else if (propertyChangedEvent.propertyName() == "Day") + emit q->dayChanged(); + else if (propertyChangedEvent.propertyName() == "Started") + emit q->startedChanged(); + else if (propertyChangedEvent.propertyName() == "ElapsedHours") + emit q->elapsedHoursChanged(); + else if (propertyChangedEvent.propertyName() == "ElapsedMinutes") + emit q->elapsedMinutesChanged(); + else if (propertyChangedEvent.propertyName() == "ElapsedSeconds") + emit q->elapsedSecondsChanged(); + else if (propertyChangedEvent.propertyName() == "ElapsedMilliseconds") + emit q->elapsedMillisecondsChanged(); + else if (propertyChangedEvent.propertyName() == "AdjustDayMode") + emit q->adjustDayModeChanged(); + else if (propertyChangedEvent.propertyName() == "AdjustTimeMode") + emit q->adjustTimeModeChanged(); + } + + QChronometer *q = nullptr; + + QDotNetFunction hours= nullptr; + QDotNetFunction minutes = nullptr; + QDotNetFunction seconds = nullptr; + QDotNetFunction day = nullptr; + QDotNetFunction elapsedHours = nullptr; + QDotNetFunction elapsedMinutes = nullptr; + QDotNetFunction elapsedSeconds = nullptr; + QDotNetFunction elapsedMilliseconds = nullptr; + QDotNetFunction started, adjustDayMode, adjustTimeMode = nullptr; + QDotNetFunction startStop, reset, breakWatch = nullptr; + QDotNetFunction setAdjustDayMode, setAdjustTimeMode = nullptr; + QDotNetFunction adjust = nullptr; +}; + + +Q_DOTNET_OBJECT_IMPL(QChronometer, Q_DOTNET_OBJECT_INIT(d(new QChronometerPrivate(this)))); + + +QChronometer::QChronometer(const ILapRecorder &lapRecorder) + : d(new QChronometerPrivate(this)) +{ + *this = constructor().invoke(nullptr); + method("set_LapRecorder").invoke(*this, lapRecorder); + subscribeEvent("PropertyChanged", d); +} + +QChronometer::~QChronometer() +{ + if (isValid()) + unsubscribeEvent("PropertyChanged", d); + delete d; +} + +double QChronometer::hours() const +{ + return method("get_Hours", d->hours).invoke(*this); +} + +double QChronometer::minutes() const +{ + return method("get_Minutes", d->minutes).invoke(*this); +} + +double QChronometer::seconds() const +{ + return method("get_Seconds", d->seconds).invoke(*this); +} + +int QChronometer::day() const +{ + return method("get_Day", d->day).invoke(*this); +} + +bool QChronometer::started() const +{ + return method("get_Started", d->started).invoke(*this); +} + +double QChronometer::elapsedHours() const +{ + return method("get_ElapsedHours", d->elapsedHours).invoke(*this); +} + +double QChronometer::elapsedMinutes() const +{ + return method("get_ElapsedMinutes", d->elapsedMinutes).invoke(*this); +} + +double QChronometer::elapsedSeconds() const +{ + return method("get_ElapsedSeconds", d->elapsedSeconds).invoke(*this); +} + +double QChronometer::elapsedMilliseconds() const +{ + return method("get_ElapsedMilliseconds", d->elapsedMilliseconds).invoke(*this); +} + +bool QChronometer::adjustDayMode() const +{ + return method("get_AdjustDayMode", d->adjustDayMode).invoke(*this); +} + +void QChronometer::setAdjustDayMode(bool value) +{ + method("set_AdjustDayMode", d->setAdjustDayMode).invoke(*this, value); +} + +bool QChronometer::adjustTimeMode() const +{ + return method("get_AdjustTimeMode", d->adjustTimeMode).invoke(*this); +} + +void QChronometer::setAdjustTimeMode(bool value) +{ + method("set_AdjustTimeMode", d->setAdjustTimeMode).invoke(*this, value); +} + +void QChronometer::adjust(int delta) +{ + method("Adjust", d->adjust).invoke(*this, delta); +} + +void QChronometer::startStop() +{ + method("StartStop", d->startStop).invoke(*this); +} + +void QChronometer::reset() +{ + method("Reset", d->reset).invoke(*this); +} diff --git a/examples/Chronometer/QmlChronometer/qchronometer.h b/examples/Chronometer/QmlChronometer/qchronometer.h new file mode 100644 index 0000000..b90c0e5 --- /dev/null +++ b/examples/Chronometer/QmlChronometer/qchronometer.h @@ -0,0 +1,84 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#pragma once + +#include +#include + +#ifdef __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wconversion" +#endif +#include +#include +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif + +struct ILapRecorder; +struct QChronometerPrivate; + +class QChronometer : public QObject, public QDotNetObject +{ + Q_OBJECT + Q_PROPERTY(double hours READ hours NOTIFY hoursChanged) + Q_PROPERTY(double minutes READ minutes NOTIFY minutesChanged) + Q_PROPERTY(double seconds READ seconds NOTIFY secondsChanged) + Q_PROPERTY(int day READ day NOTIFY dayChanged) + Q_PROPERTY(bool started READ started NOTIFY startedChanged) + Q_PROPERTY(double elapsedHours READ elapsedHours NOTIFY elapsedHoursChanged) + Q_PROPERTY(double elapsedMinutes READ elapsedMinutes NOTIFY elapsedMinutesChanged) + Q_PROPERTY(double elapsedSeconds READ elapsedSeconds NOTIFY elapsedSecondsChanged) + Q_PROPERTY(double elapsedMilliseconds + READ elapsedMilliseconds NOTIFY elapsedMillisecondsChanged) + Q_PROPERTY(bool adjustDayMode + READ adjustDayMode WRITE setAdjustDayMode NOTIFY adjustDayModeChanged) + Q_PROPERTY(bool adjustTimeMode + READ adjustTimeMode WRITE setAdjustTimeMode NOTIFY adjustTimeModeChanged) + +public: + Q_DOTNET_OBJECT(QChronometer, "WatchModels.Chronometer, ChronometerModel"); + + QChronometer(const ILapRecorder &lapRecorder); + ~QChronometer() override; + + double hours() const; + double minutes() const; + double seconds() const; + int day() const; + bool started() const; + double elapsedHours() const; + double elapsedMinutes() const; + double elapsedSeconds() const; + double elapsedMilliseconds() const; + bool adjustDayMode() const; + bool adjustTimeMode() const; + +public slots: + void startStop(); + void reset(); + void setAdjustDayMode(bool value); + void setAdjustTimeMode(bool value); + void adjust(int delta); + + +signals: + void hoursChanged(); + void minutesChanged(); + void secondsChanged(); + void dayChanged(); + void startedChanged(); + void elapsedHoursChanged(); + void elapsedMinutesChanged(); + void elapsedSecondsChanged(); + void elapsedMillisecondsChanged(); + void adjustDayModeChanged(); + void adjustTimeModeChanged(); + void lap(int hours, int minutes, int seconds, int milliseconds); + +private: + QChronometerPrivate *d = nullptr; +}; diff --git a/examples/Chronometer/QmlChronometer/qlaprecorder.cpp b/examples/Chronometer/QmlChronometer/qlaprecorder.cpp new file mode 100644 index 0000000..20956c0 --- /dev/null +++ b/examples/Chronometer/QmlChronometer/qlaprecorder.cpp @@ -0,0 +1,128 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#include "qlaprecorder.h" + +struct QLapRecorderPrivate +{ + QLapRecorderPrivate() = default; + + int lapCount = 0; + int lastHours = 0; + int lastMinutes = 0; + int lastSeconds = 0; + int lastMilliseconds = 0; + int bestHours = 0; + int bestMinutes = 0; + int bestSeconds = 0; + int bestMilliseconds = 0; +}; + + +ILapRecorder::ILapRecorder() : QDotNetInterface(FullyQualifiedTypeName) +{ + setCallback( + "Mark", [this](int hours, int minutes, int seconds, int milliseconds) + { + mark(hours, minutes, seconds, milliseconds); + }); +} + + +QLapRecorder::QLapRecorder(QObject *parent) + : QObject(parent), d(new QLapRecorderPrivate()) +{} + +QLapRecorder::~QLapRecorder() +{ + delete d; +} + +int QLapRecorder::lapCount() const { return d->lapCount; } +int QLapRecorder::lastHours() const { return d->lastHours; } +int QLapRecorder::lastMinutes() const { return d->lastMinutes; } +int QLapRecorder::lastSeconds() const { return d->lastSeconds; } +int QLapRecorder::lastMilliseconds() const { return d->lastMilliseconds; } +int QLapRecorder::bestHours() const { return d->bestHours; } +int QLapRecorder::bestMinutes() const { return d->bestMinutes; } +int QLapRecorder::bestSeconds() const { return d->bestSeconds; } +int QLapRecorder::bestMilliseconds() const { return d->bestMilliseconds; } + +void QLapRecorder::mark(int hours, int minutes, int seconds, int milliseconds) +{ + d->lastHours = hours; + emit lastHoursChanged(); + + d->lastMinutes = minutes; + emit lastMinutesChanged(); + + d->lastSeconds = seconds; + emit lastSecondsChanged(); + + d->lastMilliseconds = milliseconds; + emit lastMillisecondsChanged(); + + d->lapCount++; + emit lapCountChanged(); + + if (d->lapCount > 1 + && (d->lastHours > d->bestHours + || (d->lastHours == d->bestHours + && d->lastMinutes > d->bestMinutes) + || (d->lastHours == d->bestHours + && d->lastMinutes == d->bestMinutes + && d->lastSeconds > d->bestSeconds) + || (d->lastHours == d->bestHours + && d->lastMinutes == d->bestMinutes + && d->lastSeconds == d->bestSeconds + && d->lastMilliseconds > d->bestMilliseconds))) { + return; + } + + d->bestHours = hours; + emit bestHoursChanged(); + + d->bestMinutes = minutes; + emit bestMinutesChanged(); + + d->bestSeconds = seconds; + emit bestSecondsChanged(); + + d->bestMilliseconds = milliseconds; + emit bestMillisecondsChanged(); + + if (d->lapCount > 1) + emit newBestLap(); +} + +void QLapRecorder::reset() +{ + d->lastHours = 0; + emit lastHoursChanged(); + + d->lastMinutes = 0; + emit lastMinutesChanged(); + + d->lastSeconds = 0; + emit lastSecondsChanged(); + + d->lastMilliseconds = 0; + emit lastMillisecondsChanged(); + + d->lapCount = 0; + emit lapCountChanged(); + + d->bestHours = 0; + emit bestHoursChanged(); + + d->bestMinutes = 0; + emit bestMinutesChanged(); + + d->bestSeconds = 0; + emit bestSecondsChanged(); + + d->bestMilliseconds = 0; + emit bestMillisecondsChanged(); +} diff --git a/examples/Chronometer/QmlChronometer/qlaprecorder.h b/examples/Chronometer/QmlChronometer/qlaprecorder.h new file mode 100644 index 0000000..0456288 --- /dev/null +++ b/examples/Chronometer/QmlChronometer/qlaprecorder.h @@ -0,0 +1,69 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#pragma once + +#include "qchronometer.h" + +#include + +struct QLapRecorderPrivate; + +struct ILapRecorder : QDotNetInterface +{ + static inline const QString& FullyQualifiedTypeName = + QStringLiteral("WatchModels.ILapRecorder, ChronometerModel"); + ILapRecorder(); + + virtual void mark(int hours, int minutes, int seconds, int milliseconds) = 0; + virtual void reset() = 0; +}; + +class QLapRecorder : public QObject, public ILapRecorder +{ + Q_OBJECT + Q_PROPERTY(int lastHours READ lastHours NOTIFY lastHoursChanged) + Q_PROPERTY(int lastMinutes READ lastMinutes NOTIFY lastMinutesChanged) + Q_PROPERTY(int lastSeconds READ lastSeconds NOTIFY lastSecondsChanged) + Q_PROPERTY(int lastMilliseconds READ lastMilliseconds NOTIFY lastMillisecondsChanged) + Q_PROPERTY(int bestHours READ bestHours NOTIFY bestHoursChanged) + Q_PROPERTY(int bestMinutes READ bestMinutes NOTIFY bestMinutesChanged) + Q_PROPERTY(int bestSeconds READ bestSeconds NOTIFY bestSecondsChanged) + Q_PROPERTY(int bestMilliseconds READ bestMilliseconds NOTIFY bestMillisecondsChanged) + Q_PROPERTY(int lapCount READ lapCount NOTIFY lapCountChanged) + +public: + QLapRecorder(QObject *parent = nullptr); + ~QLapRecorder() override; + + [[nodiscard]] int lapCount() const; + [[nodiscard]] int lastHours() const; + [[nodiscard]] int lastMinutes() const; + [[nodiscard]] int lastSeconds() const; + [[nodiscard]] int lastMilliseconds() const; + [[nodiscard]] int bestHours() const; + [[nodiscard]] int bestMinutes() const; + [[nodiscard]] int bestSeconds() const; + [[nodiscard]] int bestMilliseconds() const; + +public slots: + void mark(int hours, int minutes, int seconds, int milliseconds) override; + void reset() override; + +signals: + void lapCountChanged(); + void lastHoursChanged(); + void lastMinutesChanged(); + void lastSecondsChanged(); + void lastMillisecondsChanged(); + void bestHoursChanged(); + void bestMinutesChanged(); + void bestSecondsChanged(); + void bestMillisecondsChanged(); + void newBestLap(); + +private: + QLapRecorderPrivate *d = nullptr; +}; diff --git a/examples/Chronometer/QmlChronometer/qml.qrc b/examples/Chronometer/QmlChronometer/qml.qrc new file mode 100644 index 0000000..15d835d --- /dev/null +++ b/examples/Chronometer/QmlChronometer/qml.qrc @@ -0,0 +1,19 @@ + + + main.qml + content/watchface.png + content/chrono_1_hand.png + content/chrono_2_hand.png + content/chrono_3_needle.png + content/hour_hand.png + content/minute_hand.png + content/second_hand.png + content/center.png + content/chrono_1_center.png + content/chrono_2_center.png + content/chrono_3_center.png + QChronometer/AdjustmentWheel.qml + QChronometer/InsetDial.qml + QChronometer/WatchButton.qml + + diff --git a/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj b/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj new file mode 100644 index 0000000..2544420 --- /dev/null +++ b/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj @@ -0,0 +1,111 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {8C7C4962-6AAA-4A90-927A-BC88C782CAAA} + QtVS_v304 + 10.0.19041.0 + 10.0.19041.0 + $(MSBuildProjectDirectory)\QtMsBuild + + + + Application + v143 + + + Application + v143 + + + + + + + $(DefaultQtVersion) + quick;core + debug + + + $(DefaultQtVersion) + quick;core + release + + + + + + + + + + + + + + + + + ../../../include/;$(IncludePath) + + + ../../../include/;$(IncludePath) + + + + true + true + ProgramDatabase + Disabled + + + Windows + true + + + + + true + true + None + MaxSpeed + + + Windows + false + + + + + + + + + + + + + + ..\..\..\bin\Qt.DotNet.Adapter.dll + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj.filters b/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj.filters new file mode 100644 index 0000000..d958351 --- /dev/null +++ b/examples/EmbeddedWindow/QmlApp/QmlApp.vcxproj.filters @@ -0,0 +1,57 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {99349809-55BA-4b9d-BF79-8FDBB0286EB3} + ui + + + {639EADAA-A684-42e4-A9AD-28FC9BCB8F7C} + ts + + + + + Resource Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + + + Resource Files + + + \ No newline at end of file diff --git a/examples/EmbeddedWindow/QmlApp/embeddedwindow.cpp b/examples/EmbeddedWindow/QmlApp/embeddedwindow.cpp new file mode 100644 index 0000000..3b7b17d --- /dev/null +++ b/examples/EmbeddedWindow/QmlApp/embeddedwindow.cpp @@ -0,0 +1,39 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#include "embeddedwindow.h" + +#include +#include +#include +#include + +#include "mainwindow.h" + +EmbeddedWindow::EmbeddedWindow(QQmlEngine *qmlEngine, MainWindow *mainWindow) + : qmlEngine(qmlEngine), mainWindow(mainWindow) +{ + connect(mainWindow, &MainWindow::contentRendered, this, &EmbeddedWindow::show); + connect(mainWindow, &MainWindow::closed, this, &EmbeddedWindow::close); +} + +EmbeddedWindow::~EmbeddedWindow() +{ + delete quickView; +} + +void EmbeddedWindow::show() +{ + embeddedWindow = QWindow::fromWinId((WId)mainWindow->hostHandle()); + quickView = new QQuickView(qmlEngine, embeddedWindow); + qmlEngine->rootContext()->setContextProperty("window", quickView); + quickView->setSource(QUrl(QStringLiteral("qrc:/main.qml"))); + quickView->show(); +} + +void EmbeddedWindow::close() +{ + embeddedWindow->close(); +} diff --git a/examples/EmbeddedWindow/QmlApp/embeddedwindow.h b/examples/EmbeddedWindow/QmlApp/embeddedwindow.h new file mode 100644 index 0000000..c84a370 --- /dev/null +++ b/examples/EmbeddedWindow/QmlApp/embeddedwindow.h @@ -0,0 +1,31 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#pragma once + +#include + +class QQmlEngine; +class QQuickView; +class QWindow; +class MainWindow; + +class EmbeddedWindow : public QObject +{ + Q_OBJECT +public: + EmbeddedWindow(QQmlEngine *qmlEngine, MainWindow *mainWindow); + ~EmbeddedWindow(); + +public slots: + void show(); + void close(); + +private: + QQmlEngine *qmlEngine = nullptr; + QQuickView *quickView = nullptr; + MainWindow *mainWindow = nullptr; + QWindow *embeddedWindow = nullptr; +}; diff --git a/examples/EmbeddedWindow/QmlApp/main.cpp b/examples/EmbeddedWindow/QmlApp/main.cpp new file mode 100644 index 0000000..957c749 --- /dev/null +++ b/examples/EmbeddedWindow/QmlApp/main.cpp @@ -0,0 +1,65 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#include +#include +#include +#include +#include + +#include + +#include "mainwindow.h" +#include "embeddedwindow.h" + +int main(int argc, char *argv[]) +{ +#if defined(Q_OS_WIN) + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +#endif + QGuiApplication app(argc, argv); + + MainWindow mainWindow; + QObject::connect(&mainWindow, &MainWindow::closed, &app, &QCoreApplication::quit); + + QQmlApplicationEngine engine; + engine.rootContext()->setContextProperty("mainWindow", &mainWindow); + + EmbeddedWindow embeddedWindow(&engine, &mainWindow); + + QThread *wpfThread = QThread::create([&app, &mainWindow] { + + if (FAILED(CoInitialize(nullptr))) { + app.quit(); + return; + } + + QString runtimeConfig = R"[json]( +{ + "runtimeOptions": { + "tfm": "net6.0-windows", + "rollForward": "LatestMinor", + "framework": { + "name": "Microsoft.WindowsDesktop.App", + "version": "6.0.0" + } + } +} +)[json]"; + QFile wpfAppRuntimeConfig(QGuiApplication::applicationDirPath() + "/WpfApp.runtimeconfig.json"); + if (wpfAppRuntimeConfig.open(QFile::ReadOnly | QFile::Text)) + runtimeConfig = QString(wpfAppRuntimeConfig.readAll()); + + QDotNetHost host; + if (!host.load(runtimeConfig)) { + app.quit(); + return; + } + QDotNetAdapter::init(&host); + mainWindow.init(); + }); + wpfThread->start(); + return app.exec(); +} diff --git a/examples/EmbeddedWindow/QmlApp/main.qml b/examples/EmbeddedWindow/QmlApp/main.qml new file mode 100644 index 0000000..04b018d --- /dev/null +++ b/examples/EmbeddedWindow/QmlApp/main.qml @@ -0,0 +1,135 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +import QtQuick +import QtQuick3D + +Rectangle { + width: mainWindow.hostWidth + height: mainWindow.hostHeight + gradient: Gradient { + GradientStop { position: 0.0; color: mainWindow.backgroundColor } + GradientStop { position: 0.40; color: "#E6ECED" } + GradientStop { position: 0.50; color: "#CCD9DB" } + GradientStop { position: 0.60; color: "#B3C6C9" } + GradientStop { position: 0.70; color: "#99B3B7" } + GradientStop { position: 0.75; color: "#80A0A5" } + GradientStop { position: 0.80; color: "#668D92" } + GradientStop { position: 0.85; color: "#4D7A80" } + GradientStop { position: 0.90; color: "#33676E" } + GradientStop { position: 0.95; color: "#19545C" } + GradientStop { position: 1.0; color: "#00414A" } + } + Text { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.bottomMargin: 5 + anchors.rightMargin: 10 + font.pointSize: 20 + font.weight: Font.Bold + color: "#001012" + text: "QML" + } + + View3D { + id: view + anchors.fill: parent + + PerspectiveCamera { + position: Qt.vector3d( + mainWindow.cameraPositionX, + mainWindow.cameraPositionY + 200, + mainWindow.cameraPositionZ + 300) + eulerRotation.x: (mainWindow.cameraRotationX - 30) % 360 + eulerRotation.y: mainWindow.cameraRotationY + eulerRotation.z: mainWindow.cameraRotationZ + } + + DirectionalLight { + eulerRotation.x: (mainWindow.cameraRotationX - 30) % 360 + eulerRotation.y: mainWindow.cameraRotationY + eulerRotation.z: mainWindow.cameraRotationZ + } + + Model { + id: cube + source: "#Cube" + materials: DefaultMaterial { + diffuseMap: Texture { + sourceItem: Item { + id: qt_logo + width: 230 + height: 230 + visible: false + layer.enabled: true + Rectangle { + anchors.fill: parent + color: "black" + Image { + anchors.fill: parent + source: "qt_logo.png" + } + Text { + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + color: "white" + font.pixelSize: 17 + text: "The Future is Written with Qt" + } + Text { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + color: "white" + font.pixelSize: 17 + text: "The Future is Written with Qt" + } + } + } + } + } + property var rotation: Qt.vector3d(0, 90, 0) + + eulerRotation.x: rotation.x % 360 + eulerRotation.y: rotation.y % 360 + eulerRotation.z: rotation.z % 360 + + Vector3dAnimation on rotation { + property var delta: Qt.vector3d(0, 0, 0) + id: cubeAnimation + loops: Animation.Infinite + duration: mainWindow.animationDuration + from: Qt.vector3d(0, 0, 0).plus(delta) + to: Qt.vector3d(360, 0, 360).plus(delta) + onDurationChanged: { + delta = cube.eulerRotation; + restart(); + } + } + } + } + + property var t0: 0 + property var n: 0 + + Component.onCompleted: { + window.afterFrameEnd.connect( + function() { + var t = Date.now(); + if (t0 == 0) { + t0 = t; + n = 1; + } else { + var dt = t - t0; + if (dt >= 1000) { + mainWindow.framesPerSecond = (1000 * n) / dt; + n = 0; + t0 = t; + } else { + n++; + } + } + }); + } +} diff --git a/examples/EmbeddedWindow/QmlApp/mainwindow.cpp b/examples/EmbeddedWindow/QmlApp/mainwindow.cpp new file mode 100644 index 0000000..2829f89 --- /dev/null +++ b/examples/EmbeddedWindow/QmlApp/mainwindow.cpp @@ -0,0 +1,170 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#include "mainwindow.h" + +class HwndHost : public QDotNetObject +{ +public: + Q_DOTNET_OBJECT_INLINE(HwndHost, "System.Windows.Interop.HwndHost, PresentationFramework"); + void *handle() { return method("get_Handle", fnGetHandle).invoke(*this); } + double width() { return method("get_Width", fnGetWidth).invoke(*this); } + double height() { return method("get_Height", fnGetHeight).invoke(*this); } +private: + QDotNetFunction fnGetHandle = nullptr; + QDotNetFunction fnGetWidth = nullptr; + QDotNetFunction fnGetHeight = nullptr; +}; + +class MouseEventArgs : public QDotNetObject +{ +public: + Q_DOTNET_OBJECT_INLINE(MouseEventArgs, "System.Windows.Input.MouseEventArgs, PresentationCore"); +}; + +class MainWindowPrivate : public QDotNetObject::IEventHandler +{ +public: + MainWindowPrivate(MainWindow *q) : q(q) + {} + void handleEvent(const QString &evName, QDotNetObject &evSource, QDotNetObject &evArgs) override + { + if (evName == "ContentRendered") { + emit q->contentRendered(); + } else if (evName == "SizeChanged") { + double width = evArgs.object("NewSize").call("get_Width"); + double height = evArgs.object("NewSize").call("get_Height"); + if (width != hostWidth) { + hostWidth = width; + emit q->hostWidthChanged(); + } + if (height != hostHeight) { + hostHeight = height; + emit q->hostHeightChanged(); + } + } else if (evName == "Closed") { + emit q->closed(); + } else if (evName == "PropertyChanged") { + QString propertyName = evArgs.call("get_PropertyName"); + if (propertyName == "CameraPositionX") + emit q->cameraPositionXChanged(); + else if (propertyName == "CameraPositionY") + emit q->cameraPositionYChanged(); + else if (propertyName == "CameraPositionZ") + emit q->cameraPositionZChanged(); + else if (propertyName == "CameraRotationX") + emit q->cameraRotationXChanged(); + else if (propertyName == "CameraRotationY") + emit q->cameraRotationYChanged(); + else if (propertyName == "CameraRotationZ") + emit q->cameraRotationZChanged(); + else if (propertyName == "AnimationDuration") + emit q->animationDurationChanged(); + } + }; + + HwndHost hwndHost = nullptr; + double hostWidth = 0.0, hostHeight = 0.0; + QDotNetFunction fnSetEmbeddedFps = nullptr; + QDotNetFunction fnGetCameraPositionX = nullptr; + QDotNetFunction fnGetCameraPositionY = nullptr; + QDotNetFunction fnGetCameraPositionZ = nullptr; + QDotNetFunction fnGetCameraRotationX = nullptr; + QDotNetFunction fnGetCameraRotationY = nullptr; + QDotNetFunction fnGetCameraRotationZ = nullptr; + QDotNetFunction fnGetAnimationDuration = nullptr; + QDotNetFunction fnGetFramesPerSecond = nullptr; + QDotNetFunction fnGetBackgroundColor = nullptr; + +private: + MainWindow *q; +}; + +Q_DOTNET_OBJECT_IMPL(MainWindow, Q_DOTNET_OBJECT_INIT(d(new MainWindowPrivate(this)))); + +MainWindow::MainWindow() : QDotNetObject(nullptr), d(new MainWindowPrivate(this)) +{} + +MainWindow::~MainWindow() +{} + +void MainWindow::init() +{ + *this = constructor().invoke(nullptr); + d->hwndHost = method("get_HwndHost").invoke(*this); + subscribeEvent("ContentRendered", d); + subscribeEvent("Closed", d); + subscribeEvent("PropertyChanged", d); + d->hwndHost.subscribeEvent("SizeChanged", d); + + QtDotNet::call("WpfApp.Program, WpfApp", "set_MainWindow", *this); + QtDotNet::call("WpfApp.Program, WpfApp", "Main"); +} + +void *MainWindow::hostHandle() +{ + return d->hwndHost.handle(); +} + +int MainWindow::hostWidth() +{ + return d->hostWidth; +} + +int MainWindow::hostHeight() +{ + return d->hostHeight; +} + + +double MainWindow::cameraPositionX() +{ + return method("get_CameraPositionX", d->fnGetCameraPositionX).invoke(*this); +} + +double MainWindow::cameraPositionY() +{ + return method("get_CameraPositionY", d->fnGetCameraPositionY).invoke(*this); +} + +double MainWindow::cameraPositionZ() +{ + return method("get_CameraPositionZ", d->fnGetCameraPositionZ).invoke(*this); +} + +double MainWindow::cameraRotationX() +{ + return method("get_CameraRotationX", d->fnGetCameraRotationX).invoke(*this); +} + +double MainWindow::cameraRotationY() +{ + return method("get_CameraRotationY", d->fnGetCameraRotationY).invoke(*this); +} + +double MainWindow::cameraRotationZ() +{ + return method("get_CameraRotationZ", d->fnGetCameraRotationZ).invoke(*this); +} + +double MainWindow::animationDuration() +{ + return method("get_AnimationDuration", d->fnGetAnimationDuration).invoke(*this); +} + +double MainWindow::framesPerSecond() +{ + return method("get_FramesPerSecond", d->fnGetFramesPerSecond).invoke(*this); +} + +QString MainWindow::backgroundColor() +{ + return method("get_BackgroundColor", d->fnGetBackgroundColor).invoke(*this); +} + +void MainWindow::setFramesPerSecond(double fps) +{ + method("set_FramesPerSecond", d->fnSetEmbeddedFps).invoke(*this, fps); +} diff --git a/examples/EmbeddedWindow/QmlApp/mainwindow.h b/examples/EmbeddedWindow/QmlApp/mainwindow.h new file mode 100644 index 0000000..4d81ed0 --- /dev/null +++ b/examples/EmbeddedWindow/QmlApp/mainwindow.h @@ -0,0 +1,61 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#pragma once + +#include +#include + +class MainWindowPrivate; + +class MainWindow : public QObject, public QDotNetObject +{ + Q_OBJECT + Q_PROPERTY(double hostWidth READ hostWidth NOTIFY hostWidthChanged) + Q_PROPERTY(double hostHeight READ hostHeight NOTIFY hostHeightChanged) + Q_PROPERTY(double cameraPositionX READ cameraPositionX NOTIFY cameraPositionXChanged) + Q_PROPERTY(double cameraPositionY READ cameraPositionY NOTIFY cameraPositionYChanged) + Q_PROPERTY(double cameraPositionZ READ cameraPositionZ NOTIFY cameraPositionZChanged) + Q_PROPERTY(double cameraRotationX READ cameraRotationX NOTIFY cameraRotationXChanged) + Q_PROPERTY(double cameraRotationY READ cameraRotationY NOTIFY cameraRotationYChanged) + Q_PROPERTY(double cameraRotationZ READ cameraRotationZ NOTIFY cameraRotationZChanged) + Q_PROPERTY(double animationDuration READ animationDuration NOTIFY animationDurationChanged) + Q_PROPERTY(QString backgroundColor READ backgroundColor NOTIFY backgroundColorChanged) + Q_PROPERTY(double framesPerSecond READ framesPerSecond WRITE setFramesPerSecond) +public: + Q_DOTNET_OBJECT(MainWindow, "WpfApp.MainWindow, WpfApp"); + MainWindow(); + ~MainWindow(); + void init(); + void *hostHandle(); + int hostWidth(); + int hostHeight(); + double cameraPositionX(); + double cameraPositionY(); + double cameraPositionZ(); + double cameraRotationX(); + double cameraRotationY(); + double cameraRotationZ(); + double animationDuration(); + double framesPerSecond(); + QString backgroundColor(); +signals: + void contentRendered(); + void hostWidthChanged(); + void hostHeightChanged(); + void cameraPositionXChanged(); + void cameraPositionYChanged(); + void cameraPositionZChanged(); + void cameraRotationXChanged(); + void cameraRotationYChanged(); + void cameraRotationZChanged(); + void animationDurationChanged(); + void backgroundColorChanged(); + void closed(); +public slots: + void setFramesPerSecond(double fps); +private: + MainWindowPrivate *d; +}; diff --git a/examples/EmbeddedWindow/QmlApp/qml.qrc b/examples/EmbeddedWindow/QmlApp/qml.qrc new file mode 100644 index 0000000..040dc48 --- /dev/null +++ b/examples/EmbeddedWindow/QmlApp/qml.qrc @@ -0,0 +1,6 @@ + + + main.qml + qt_logo.png + + diff --git a/examples/EmbeddedWindow/QmlApp/qt_logo.png b/examples/EmbeddedWindow/QmlApp/qt_logo.png new file mode 100644 index 0000000..30c621c Binary files /dev/null and b/examples/EmbeddedWindow/QmlApp/qt_logo.png differ diff --git a/examples/EmbeddedWindow/WpfApp/AssemblyInfo.cs b/examples/EmbeddedWindow/WpfApp/AssemblyInfo.cs new file mode 100644 index 0000000..8b5504e --- /dev/null +++ b/examples/EmbeddedWindow/WpfApp/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/examples/EmbeddedWindow/WpfApp/MainWindow.xaml b/examples/EmbeddedWindow/WpfApp/MainWindow.xaml new file mode 100644 index 0000000..2695ba9 --- /dev/null +++ b/examples/EmbeddedWindow/WpfApp/MainWindow.xaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + diff --git a/examples/EmbeddedWindow/WpfApp/MainWindow.xaml.cs b/examples/EmbeddedWindow/WpfApp/MainWindow.xaml.cs new file mode 100644 index 0000000..77f1bf8 --- /dev/null +++ b/examples/EmbeddedWindow/WpfApp/MainWindow.xaml.cs @@ -0,0 +1,109 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; + +namespace WpfApp +{ + public partial class MainWindow : Window, INotifyPropertyChanged + { + public MainWindow() + { + InitializeComponent(); + } + + public HwndHost HwndHost => EmbeddedAppHost; + + public double CameraPositionX => WpfThread(() => SliderCameraPositionX.Value); + public double CameraPositionY => WpfThread(() => SliderCameraPositionY.Value); + public double CameraPositionZ => WpfThread(() => SliderCameraPositionZ.Value); + public double CameraRotationX => WpfThread(() => SliderCameraRotationX.Value); + public double CameraRotationY => WpfThread(() => SliderCameraRotationY.Value); + public double CameraRotationZ => WpfThread(() => SliderCameraRotationZ.Value); + + public static double MaxRpm => 100; + public double Rpm => MaxRpm * Math.Max(1 / MaxRpm, SliderAnimationDuration.Value); + public double AnimationDuration => WpfThread(() => 60000 / Rpm); + + public double FramesPerSecond + { + get => WpfThread(() => FpsValue.Value); + set + { + WpfThread(() => + { + if (value <= FpsValue.Maximum) + FpsValue.Value = value; + FpsLabel.Text = $"{value:0.0} fps"; + }); + } + } + + public string BackgroundColor { get; set; } = "#FFFFFF"; + + public event PropertyChangedEventHandler PropertyChanged; + private void NotifyPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) + { + if (sender is not Slider slider) + return; + NotifyPropertyChanged(slider.Name switch + { + nameof(SliderCameraPositionX) => nameof(CameraPositionX), + nameof(SliderCameraPositionY) => nameof(CameraPositionY), + nameof(SliderCameraPositionZ) => nameof(CameraPositionZ), + nameof(SliderCameraRotationX) => nameof(CameraRotationX), + nameof(SliderCameraRotationY) => nameof(CameraRotationY), + nameof(SliderCameraRotationZ) => nameof(CameraRotationZ), + nameof(SliderAnimationDuration) => nameof(AnimationDuration), + _ => throw new NotSupportedException() + }); + if (slider.Name is nameof(SliderAnimationDuration)) + NotifyPropertyChanged(nameof(Rpm)); + } + + private void Slider_MouseDoubleClick(object sender, MouseButtonEventArgs e) + { + if (sender is not Slider slider) + return; + if (slider.Name is nameof(SliderAnimationDuration)) + slider.Value = (slider.Maximum + slider.Minimum) / 10; + else + slider.Value = (slider.Maximum + slider.Minimum) / 2; + } + + protected override void OnRender(DrawingContext drawingContext) + { + base.OnRender(drawingContext); + if (StackPanel.Background is not SolidColorBrush panelBrush) + return; + if (BackgroundColor != (BackgroundColor = panelBrush.Color.ToString())) + NotifyPropertyChanged(nameof(BackgroundColor)); + } + + private static void WpfThread(Action action) + { + if (Application.Current?.Dispatcher is { } dispatcher) + dispatcher.Invoke(action); + } + + private static T WpfThread(Func func) + { + if (Application.Current?.Dispatcher is not { } dispatcher) + return default; + return dispatcher.Invoke(func); + } + } +} diff --git a/examples/EmbeddedWindow/WpfApp/Properties/launchSettings.json b/examples/EmbeddedWindow/WpfApp/Properties/launchSettings.json new file mode 100644 index 0000000..70d4330 --- /dev/null +++ b/examples/EmbeddedWindow/WpfApp/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "WpfApp": { + "commandName": "Project", + "environmentVariables": { + "PATH": "C:/lib/Qt/6.2.4/msvc2019_64/bin;%PATH%" + }, + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/examples/EmbeddedWindow/WpfApp/WpfApp.cs b/examples/EmbeddedWindow/WpfApp/WpfApp.cs new file mode 100644 index 0000000..b80d9df --- /dev/null +++ b/examples/EmbeddedWindow/WpfApp/WpfApp.cs @@ -0,0 +1,23 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +using System; +using System.Windows; + +namespace WpfApp +{ + public static class Program + { + [STAThread] + public static int Main() + { + var application = new Application(); + (MainWindow ??= new MainWindow()).InitializeComponent(); + MainWindow.Show(); + return application.Run(); + } + public static MainWindow MainWindow { get; set; } + } +} diff --git a/examples/EmbeddedWindow/WpfApp/WpfApp.csproj b/examples/EmbeddedWindow/WpfApp/WpfApp.csproj new file mode 100644 index 0000000..8046823 --- /dev/null +++ b/examples/EmbeddedWindow/WpfApp/WpfApp.csproj @@ -0,0 +1,33 @@ + + + + WinExe + net$(BundledNETCoreAppTargetFrameworkVersion)-windows + disable + true + WpfApp.Program + + + + + + + + + ..\..\..\bin\Qt.DotNet.Adapter.dll + + + $(ProgramFiles)\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\$(BundledNETCoreAppPackageVersion)\ref\net$(BundledNETCoreAppTargetFrameworkVersion)\System.Drawing.Common.dll + + + $(ProgramFiles)\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\$(BundledNETCoreAppPackageVersion)\ref\net$(BundledNETCoreAppTargetFrameworkVersion)\System.Windows.Forms.dll + + + $(ProgramFiles)\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\$(BundledNETCoreAppPackageVersion)\ref\net$(BundledNETCoreAppTargetFrameworkVersion)\System.Windows.Forms.Primitives.dll + + + $(ProgramFiles)\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\$(BundledNETCoreAppPackageVersion)\ref\net$(BundledNETCoreAppTargetFrameworkVersion)\WindowsFormsIntegration.dll + + + + diff --git a/examples/QtAzureIoT/QtAzureIoT.sln b/examples/QtAzureIoT/QtAzureIoT.sln new file mode 100644 index 0000000..4f5a653 --- /dev/null +++ b/examples/QtAzureIoT/QtAzureIoT.sln @@ -0,0 +1,88 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.33130.402 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "device", "device", "{BD2258EF-0E47-4DAA-94DF-D08CA1B09A93}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SensorData", "device\SensorData\SensorData.csproj", "{D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeviceToBackoffice", "device\DeviceToBackoffice\DeviceToBackoffice.csproj", "{4F2A52A9-9DAE-4250-A881-F0A013A589F4}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "deviceapp", "device\deviceapp\deviceapp.vcxproj", "{2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{AC26C5B9-CA58-434C-B607-DC94BFE2A665}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "app", "app", "{60F1722D-12B9-4671-B9E3-EDE5C41F1086}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CardReader", "device\CardReader\CardReader.csproj", "{66A69341-3B00-4812-AA77-EC5C2E9EA23A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{34513197-4269-4943-9F6D-CE4D89CB4DD2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Utils", "common\Utils.csproj", "{DEF7470A-3D27-4D71-9E48-A96C9129FA42}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Debug|x64.ActiveCfg = Debug|Any CPU + {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Debug|x64.Build.0 = Debug|Any CPU + {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Release|Any CPU.Build.0 = Release|Any CPU + {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Release|x64.ActiveCfg = Release|Any CPU + {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0}.Release|x64.Build.0 = Release|Any CPU + {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Debug|x64.Build.0 = Debug|Any CPU + {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Release|Any CPU.Build.0 = Release|Any CPU + {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Release|x64.ActiveCfg = Release|Any CPU + {4F2A52A9-9DAE-4250-A881-F0A013A589F4}.Release|x64.Build.0 = Release|Any CPU + {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Debug|Any CPU.ActiveCfg = Debug|x64 + {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Debug|Any CPU.Build.0 = Debug|x64 + {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Debug|x64.ActiveCfg = Debug|x64 + {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Debug|x64.Build.0 = Debug|x64 + {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Release|Any CPU.ActiveCfg = Release|x64 + {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Release|Any CPU.Build.0 = Release|x64 + {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Release|x64.ActiveCfg = Release|x64 + {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF}.Release|x64.Build.0 = Release|x64 + {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Debug|x64.ActiveCfg = Debug|Any CPU + {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Debug|x64.Build.0 = Debug|Any CPU + {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Release|Any CPU.Build.0 = Release|Any CPU + {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Release|x64.ActiveCfg = Release|Any CPU + {66A69341-3B00-4812-AA77-EC5C2E9EA23A}.Release|x64.Build.0 = Release|Any CPU + {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Debug|x64.ActiveCfg = Debug|Any CPU + {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Debug|x64.Build.0 = Debug|Any CPU + {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Release|Any CPU.Build.0 = Release|Any CPU + {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Release|x64.ActiveCfg = Release|Any CPU + {DEF7470A-3D27-4D71-9E48-A96C9129FA42}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D8CD7E8D-7ECA-46A7-AA39-E471F99C94F0} = {AC26C5B9-CA58-434C-B607-DC94BFE2A665} + {4F2A52A9-9DAE-4250-A881-F0A013A589F4} = {AC26C5B9-CA58-434C-B607-DC94BFE2A665} + {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF} = {60F1722D-12B9-4671-B9E3-EDE5C41F1086} + {AC26C5B9-CA58-434C-B607-DC94BFE2A665} = {BD2258EF-0E47-4DAA-94DF-D08CA1B09A93} + {60F1722D-12B9-4671-B9E3-EDE5C41F1086} = {BD2258EF-0E47-4DAA-94DF-D08CA1B09A93} + {66A69341-3B00-4812-AA77-EC5C2E9EA23A} = {AC26C5B9-CA58-434C-B607-DC94BFE2A665} + {DEF7470A-3D27-4D71-9E48-A96C9129FA42} = {34513197-4269-4943-9F6D-CE4D89CB4DD2} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9EC3D98D-2164-4E07-A5D2-334E9C93C570} + EndGlobalSection +EndGlobal diff --git a/examples/QtAzureIoT/common/PropertySet.cs b/examples/QtAzureIoT/common/PropertySet.cs new file mode 100644 index 0000000..2744a5c --- /dev/null +++ b/examples/QtAzureIoT/common/PropertySet.cs @@ -0,0 +1,27 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +using System.ComponentModel; + +namespace QtAzureIoT.Utils +{ + public class PropertySet : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + protected void NotifyPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected void SetProperty(ref T currentValue, T newValue, string name) + { + if (newValue.Equals(currentValue)) + return; + currentValue = newValue; + NotifyPropertyChanged(name); + } + } +} diff --git a/examples/QtAzureIoT/common/Utils.csproj b/examples/QtAzureIoT/common/Utils.csproj new file mode 100644 index 0000000..141e38f --- /dev/null +++ b/examples/QtAzureIoT/common/Utils.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + disable + + + diff --git a/examples/QtAzureIoT/device/CardReader/CardReader.cs b/examples/QtAzureIoT/device/CardReader/CardReader.cs new file mode 100644 index 0000000..e97de9e --- /dev/null +++ b/examples/QtAzureIoT/device/CardReader/CardReader.cs @@ -0,0 +1,98 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +using System.Device.I2c; +using System.Diagnostics; +using Iot.Device.Pn532; +using Iot.Device.Pn532.ListPassive; +using Iot.Device.Pn532.RfConfiguration; +using QtAzureIoT.Utils; + +namespace QtAzureIoT.Device +{ + public class CardReader : PropertySet, IDisposable + { + public CardReader() + { } + + public bool CardInReader + { + get => propertyCardInReader; + private set => SetProperty(ref propertyCardInReader, value, nameof(CardInReader)); + } + + public void StartPolling() + { + if (Nfc != null) + return; + BusConnectionSettings = new I2cConnectionSettings(1, 0x24); + BusDevice = I2cDevice.Create(BusConnectionSettings); + Nfc = new Pn532(BusDevice); + + PollingLoop = new CancellationTokenSource(); + Polling = new Task(async () => await PollingLoopAsync(), PollingLoop.Token); + Polling.Start(); + } + + public void StopPolling() + { + if (Nfc == null) + return; + PollingLoop.Cancel(); + Polling.Wait(); + Nfc.Dispose(); + Nfc = null; + BusDevice.Dispose(); + BusDevice = null; + BusConnectionSettings = null; + PollingLoop.Dispose(); + PollingLoop = null; + Polling.Dispose(); + Polling = null; + } + + #region private + private I2cConnectionSettings BusConnectionSettings { get; set; } + private I2cDevice BusDevice { get; set; } + private Pn532 Nfc { get; set; } + private CancellationTokenSource PollingLoop { get; set; } + private Task Polling { get; set; } + + + private async Task PollingLoopAsync() + { + TargetBaudRate cardType = TargetBaudRate.B106kbpsTypeA; + while (!PollingLoop.IsCancellationRequested) { + try { + if (Nfc.ListPassiveTarget(MaxTarget.One, cardType) is object) { + CardInReader = true; + var timeSinceDetected = Stopwatch.StartNew(); + while (timeSinceDetected.ElapsedMilliseconds < 3000) { + if (Nfc.ListPassiveTarget(MaxTarget.One, cardType) is object) + timeSinceDetected.Restart(); + await Task.Delay(200); + } + CardInReader = false; + } else { + Nfc.SetRfField(RfFieldMode.None); + await Task.Delay(1000); + Nfc.SetRfField(RfFieldMode.RF); + } + } catch (Exception e) { + Debug.WriteLine($"Exception: {e.GetType().Name}: {e.Message}"); + Nfc.SetRfField(RfFieldMode.None); + } + } + } + + public void Dispose() + { + StopPolling(); + } + + private bool propertyCardInReader = false; + #endregion + } +} diff --git a/examples/QtAzureIoT/device/CardReader/CardReader.csproj b/examples/QtAzureIoT/device/CardReader/CardReader.csproj new file mode 100644 index 0000000..c694f5b --- /dev/null +++ b/examples/QtAzureIoT/device/CardReader/CardReader.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + disable + + + + + + + + + + + diff --git a/examples/QtAzureIoT/device/DeviceToBackoffice/Backoffice.cs b/examples/QtAzureIoT/device/DeviceToBackoffice/Backoffice.cs new file mode 100644 index 0000000..5fe3cb9 --- /dev/null +++ b/examples/QtAzureIoT/device/DeviceToBackoffice/Backoffice.cs @@ -0,0 +1,70 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Devices.Client; +using Microsoft.Azure.Devices.Client.Samples; +using Microsoft.Azure.Devices.Provisioning.Client; +using Microsoft.Azure.Devices.Provisioning.Client.PlugAndPlay; +using Microsoft.Azure.Devices.Provisioning.Client.Transport; +using Microsoft.Azure.Devices.Shared; +using Microsoft.Extensions.Logging; + +namespace QtAzureIoT.Device +{ + public class Backoffice + { + public Backoffice() + { + DeviceClient = DeviceClient.CreateFromConnectionString( + "HostName=QtDotNetDemo-Hub.azure-devices.net;DeviceId=QtDotNetDemoDevice;SharedAccessKey=YkZmsSOZf8lvQb5HDthosRHP4XV1hYSuDEoExe/2Fj8=", + TransportType.Mqtt, + new ClientOptions + { + ModelId = "dtmi:com:example:TemperatureController;2" + }); + BackofficeInterface = new TemperatureControllerSample(DeviceClient); + } + + public void SetTelemetry(string name, double value) + { + BackofficeInterface.SetTelemetry(name, value); + } + + public void SetTelemetry(string name, bool value) + { + BackofficeInterface.SetTelemetry(name, value); + } + + public void StartPolling() + { + PollingLoop = new CancellationTokenSource(); + Polling = new Task(async () => await PollingLoopAsync(), PollingLoop.Token); + Polling.Start(); + } + + public void StopPolling() + { + PollingLoop.Cancel(); + } + + #region private + private CancellationTokenSource PollingLoop { get; set; } + private Task Polling { get; set; } + private DeviceClient DeviceClient { get; } + private TemperatureControllerSample BackofficeInterface { get; } + + private async Task PollingLoopAsync() + { + await BackofficeInterface.InitOperationsAsync(PollingLoop.Token); + while (!PollingLoop.IsCancellationRequested) { + await BackofficeInterface.PerformOperationsAsync(PollingLoop.Token); + } + } + #endregion + } +} diff --git a/examples/QtAzureIoT/device/DeviceToBackoffice/DeviceToBackoffice.csproj b/examples/QtAzureIoT/device/DeviceToBackoffice/DeviceToBackoffice.csproj new file mode 100644 index 0000000..a53efc9 --- /dev/null +++ b/examples/QtAzureIoT/device/DeviceToBackoffice/DeviceToBackoffice.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + disable + + + + + + + + + + + diff --git a/examples/QtAzureIoT/device/DeviceToBackoffice/PnpConvention.cs b/examples/QtAzureIoT/device/DeviceToBackoffice/PnpConvention.cs new file mode 100644 index 0000000..16bef2b --- /dev/null +++ b/examples/QtAzureIoT/device/DeviceToBackoffice/PnpConvention.cs @@ -0,0 +1,423 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Azure.Devices.Client; +using Microsoft.Azure.Devices.Shared; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Text; + +namespace PnpHelpers +{ + public class PnpConvention + { + /// + /// The content type for a plug and play compatible telemetry message. + /// + public const string ContentApplicationJson = "application/json"; + + /// + /// The key for a component identifier within a property update patch. Corresponding value is . + /// + public const string PropertyComponentIdentifierKey = "__t"; + + /// + /// The value for a component identifier within a property update patch. Corresponding key is . + /// + public const string PropertyComponentIdentifierValue = "c"; + + /// + /// Create a plug and play compatible telemetry message. + /// + /// The name of the telemetry, as defined in the DTDL interface. Must be 64 characters or less. For more details see + /// . + /// The unserialized telemetry payload, in the format defined in the DTDL interface. + /// The name of the component in which the telemetry is defined. Can be null for telemetry defined under the root interface. + /// The character encoding to be used when encoding the message body to bytes. This defaults to utf-8. + /// A plug and play compatible telemetry message, which can be sent to IoT Hub. The caller must dispose this object when finished. + public static Message CreateMessage(string telemetryName, object telemetryValue, string componentName = default, Encoding encoding = default) + { + if (string.IsNullOrWhiteSpace(telemetryName)) + { + throw new ArgumentNullException(nameof(telemetryName)); + } + if (telemetryValue == null) + { + throw new ArgumentNullException(nameof(telemetryValue)); + } + + return CreateMessage(new Dictionary { { telemetryName, telemetryValue } }, componentName, encoding); + } + + /// + /// Create a plug and play compatible telemetry message. + /// + /// The name of the component in which the telemetry is defined. Can be null for telemetry defined under the root interface. + /// The unserialized name and value telemetry pairs, as defined in the DTDL interface. Names must be 64 characters or less. For more details see + /// . + /// The character encoding to be used when encoding the message body to bytes. This defaults to utf-8. + /// A plug and play compatible telemetry message, which can be sent to IoT Hub. The caller must dispose this object when finished. + public static Message CreateMessage(IDictionary telemetryPairs, string componentName = default, Encoding encoding = default) + { + if (telemetryPairs == null) + { + throw new ArgumentNullException(nameof(telemetryPairs)); + } + + Encoding messageEncoding = encoding ?? Encoding.UTF8; + string payload = JsonConvert.SerializeObject(telemetryPairs); + var message = new Message(messageEncoding.GetBytes(payload)) + { + ContentEncoding = messageEncoding.WebName, + ContentType = ContentApplicationJson, + }; + + if (!string.IsNullOrWhiteSpace(componentName)) + { + message.ComponentName = componentName; + } + + return message; + } + + /// + /// Creates a batch property update payload for the specified property key/value pairs. + /// + /// The name of the twin property. + /// The unserialized value of the twin property. + /// A compact payload of the properties to update. + /// + /// This creates a property patch for both read-only and read-write properties, both of which are named from a service perspective. + /// All properties are read-write from a device's perspective. + /// For a root-level property update, the patch is in the format: { "samplePropertyName": 20 } + /// + public static TwinCollection CreatePropertyPatch(string propertyName, object propertyValue) + { + return CreatePropertyPatch(new Dictionary { { propertyName, propertyValue } }); + } + + /// + /// Creates a batch property update payload for the specified property key/value pairs + /// + /// + /// This creates a property patch for both read-only and read-write properties, both of which are named from a service perspective. + /// All properties are read-write from a device's perspective. + /// For a root-level property update, the patch is in the format: { "samplePropertyName": 20 } + /// + /// The twin properties and values to update. + /// A compact payload of the properties to update. + public static TwinCollection CreatePropertyPatch(IDictionary propertyPairs) + { + return new TwinCollection(JsonConvert.SerializeObject(propertyPairs)); + } + + /// + /// Create a key/value property patch for updating digital twin properties. + /// + /// + /// This creates a property patch for both read-only and read-write properties, both of which are named from a service perspective. + /// All properties are read-write from a device's perspective. + /// For a component-level property update, the patch is in the format: + /// + /// { + /// "sampleComponentName": { + /// "__t": "c", + /// "samplePropertyName"": 20 + /// } + /// } + /// + /// + /// The name of the component in which the property is defined. Can be null for property defined under the root interface. + /// The name of the twin property. + /// The unserialized value of the twin property. + /// The property patch for read-only and read-write property updates. + public static TwinCollection CreateComponentPropertyPatch(string componentName, string propertyName, object propertyValue) + { + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentNullException(nameof(propertyName)); + } + if (propertyValue == null) + { + throw new ArgumentNullException(nameof(propertyValue)); + } + + return CreateComponentPropertyPatch(componentName, new Dictionary { { propertyName, propertyValue } }); + } + + /// + /// Create a key/value property patch for updating digital twin properties. + /// + /// + /// This creates a property patch for both read-only and read-write properties, both of which are named from a service perspective. + /// All properties are read-write from a device's perspective. + /// For a component-level property update, the patch is in the format: + /// + /// { + /// "sampleComponentName": { + /// "__t": "c", + /// "samplePropertyName": 20 + /// } + /// } + /// + /// + /// The name of the component in which the property is defined. Can be null for property defined under the root interface. + /// The property name and an unserialized value, as defined in the DTDL interface. + /// The property patch for read-only and read-write property updates. + public static TwinCollection CreateComponentPropertyPatch(string componentName, IDictionary propertyPairs) + { + if (string.IsNullOrWhiteSpace(componentName)) + { + throw new ArgumentNullException(nameof(componentName)); + } + if (propertyPairs == null) + { + throw new ArgumentNullException(nameof(propertyPairs)); + } + + var propertyPatch = new StringBuilder(); + propertyPatch.Append('{'); + propertyPatch.Append($"\"{componentName}\":"); + propertyPatch.Append('{'); + propertyPatch.Append($"\"{PropertyComponentIdentifierKey}\":\"{PropertyComponentIdentifierValue}\","); + foreach (var kvp in propertyPairs) + { + propertyPatch.Append($"\"{kvp.Key}\":{JsonConvert.SerializeObject(kvp.Value)},"); + } + + // remove the extra comma + propertyPatch.Remove(propertyPatch.Length - 1, 1); + + propertyPatch.Append("}}"); + + return new TwinCollection(propertyPatch.ToString()); + } + + /// + /// Creates a response to a write request on a device property. + /// + /// + /// This creates a property patch for both read-only and read-write properties, both of which are named from a service perspective. + /// All properties are read-write from a device's perspective. + /// For a component-level property update, the patch is in the format: + /// + /// { + /// "sampleComponentName": { + /// "__t": "c", + /// "samplePropertyName": 20 + /// } + /// } + /// + /// + /// The name of the property to report. + /// The unserialized property value. + /// The acknowledgment code, usually an HTTP Status Code e.g. 200, 400. + /// The acknowledgment version, as supplied in the property update request. + /// The acknowledgment description, an optional, human-readable message about the result of the property update. + /// A serialized json string response. + public static TwinCollection CreateWritablePropertyResponse( + string propertyName, + object propertyValue, + int ackCode, + long ackVersion, + string ackDescription = null) + { + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + return CreateWritablePropertyResponse( + new Dictionary { { propertyName, propertyValue } }, + ackCode, + ackVersion, + ackDescription); + } + + /// + /// Creates a response to a write request on a device property. + /// + /// The name and unserialized value of the property to report. + /// The acknowledgment code, usually an HTTP Status Code e.g. 200, 400. + /// The acknowledgment version, as supplied in the property update request. + /// The acknowledgment description, an optional, human-readable message about the result of the property update. + /// A serialized json string response. + public static TwinCollection CreateWritablePropertyResponse( + IDictionary propertyPairs, + int ackCode, + long ackVersion, + string ackDescription = null) + { + if (propertyPairs == null) + { + throw new ArgumentNullException(nameof(propertyPairs)); + } + + var response = new Dictionary(propertyPairs.Count); + foreach (var kvp in propertyPairs) + { + if (string.IsNullOrWhiteSpace(kvp.Key)) + { + throw new ArgumentNullException(nameof(kvp.Key), $"One of the propertyPairs keys was null, empty, or white space."); + } + response.Add(kvp.Key, new WritablePropertyResponse(kvp.Value, ackCode, ackVersion, ackDescription)); + } + + return new TwinCollection(JsonConvert.SerializeObject(response)); + } + + /// + /// Creates a response to a write request on a device property. + /// + /// + /// For a component-level property update, the patch is in the format: + /// + /// "sampleComponentName": { + /// "__t": "c", + /// "samplePropertyName": { + /// "value": 20, + /// "ac": 200, + /// "av": 5, + /// "ad": "The update was successful." + /// } + /// } + /// } + /// + /// + /// The component to which the property belongs. + /// The name of the property to report. + /// The unserialized property value. + /// The acknowledgment code, usually an HTTP Status Code e.g. 200, 400. + /// The acknowledgment version, as supplied in the property update request. + /// The acknowledgment description, an optional, human-readable message about the result of the property update. + /// A serialized json string response. + public static TwinCollection CreateComponentWritablePropertyResponse( + string componentName, + string propertyName, + object propertyValue, + int ackCode, + long ackVersion, + string ackDescription = null) + { + if (string.IsNullOrWhiteSpace(componentName)) + { + throw new ArgumentNullException(nameof(componentName)); + } + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + return CreateComponentWritablePropertyResponse( + componentName, + new Dictionary { { propertyName, propertyValue } }, + ackCode, + ackVersion, + ackDescription); + } + + /// + /// Creates a response to a write request on a device property. + /// + /// + /// For a component-level property update, the patch is in the format: + /// + /// "sampleComponentName": { + /// "__t": "c", + /// "samplePropertyName": { + /// "value": 20, + /// "ac": 200, + /// "av": 5, + /// "ad": "The update was successful." + /// } + /// } + /// } + /// + /// + /// The component to which the property belongs. + /// The name and unserialized value of the property to report. + /// The acknowledgment code, usually an HTTP Status Code e.g. 200, 400. + /// The acknowledgment version, as supplied in the property update request. + /// The acknowledgment description, an optional, human-readable message about the result of the property update. + /// A serialized json string response. + public static TwinCollection CreateComponentWritablePropertyResponse( + string componentName, + IDictionary propertyPairs, + int ackCode, + long ackVersion, + string ackDescription = null) + { + if (string.IsNullOrWhiteSpace(componentName)) + { + throw new ArgumentNullException(nameof(componentName)); + } + if (propertyPairs == null) + { + throw new ArgumentNullException(nameof(propertyPairs)); + } + + var propertyPatch = new Dictionary + { + { PropertyComponentIdentifierKey, PropertyComponentIdentifierValue }, + }; + foreach (var kvp in propertyPairs) + { + if (string.IsNullOrWhiteSpace(kvp.Key)) + { + throw new ArgumentNullException(nameof(kvp.Key), $"One of the propertyPairs keys was null, empty, or white space."); + } + propertyPatch.Add(kvp.Key, new WritablePropertyResponse(kvp.Value, ackCode, ackVersion, ackDescription)); + } + + var response = new Dictionary + { + { componentName, propertyPatch }, + }; + + return new TwinCollection(JsonConvert.SerializeObject(response)); + } + + /// + /// Helper to retrieve the property value from the property update patch which was received as a result of service-initiated update. + /// + /// The data type of the property, as defined in the DTDL interface. + /// The property update patch received as a result of service-initiated update. + /// The property name, as defined in the DTDL interface. + /// The corresponding property value. + /// The name of the component in which the property is defined. Can be null for property defined under the root interface. + /// A boolean indicating if the property update patch received contains the property update. + public static bool TryGetPropertyFromTwin(TwinCollection collection, string propertyName, out T propertyValue, string componentName = null) + { + if (collection == null) + { + throw new ArgumentNullException(nameof(collection)); + } + + // If the desired property update is for a root component or nested component, verify that property patch received contains the desired property update. + propertyValue = default; + + if (string.IsNullOrWhiteSpace(componentName)) + { + if (collection.Contains(propertyName)) + { + propertyValue = (T)collection[propertyName]; + return true; + } + } + + if (collection.Contains(componentName)) + { + JObject componentProperty = collection[componentName]; + if (componentProperty.ContainsKey(propertyName)) + { + propertyValue = componentProperty.Value(propertyName); + return true; + } + } + + return false; + } + } +} diff --git a/examples/QtAzureIoT/device/DeviceToBackoffice/TemperatureControllerSample.cs b/examples/QtAzureIoT/device/DeviceToBackoffice/TemperatureControllerSample.cs new file mode 100644 index 0000000..58e7885 --- /dev/null +++ b/examples/QtAzureIoT/device/DeviceToBackoffice/TemperatureControllerSample.cs @@ -0,0 +1,483 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Devices.Shared; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using PnpHelpers; + +namespace Microsoft.Azure.Devices.Client.Samples +{ + internal enum StatusCode + { + Completed = 200, + InProgress = 202, + ReportDeviceInitialProperty = 203, + BadRequest = 400, + NotFound = 404 + } + + + public class TemperatureControllerSample + { + public ConcurrentDictionary Telemetry = new(); + public void SetTelemetry(string name, double value) + { + Telemetry.AddOrUpdate(name, value, (k, v) => value); + } + public void SetTelemetry(string name, bool value) + { + Telemetry.AddOrUpdate(name, value, (k, v) => value); + } + + // The default reported "value" and "av" for each "Thermostat" component on the client initial startup. + // See https://docs.microsoft.com/azure/iot-develop/concepts-convention#writable-properties for more details in acknowledgment responses. + private const double DefaultPropertyValue = 0d; + + private const long DefaultAckVersion = 0L; + + private const string TargetTemperatureProperty = "targetTemperature"; + + private const string Thermostat1 = "thermostat1"; + private const string Thermostat2 = "thermostat2"; + private const string SerialNumber = "SR-123456"; + + private static readonly Random s_random = new Random(); + + private readonly DeviceClient _deviceClient; + //private readonly ILogger _logger; + + // Dictionary to hold the temperature updates sent over each "Thermostat" component. + // NOTE: Memory constrained devices should leverage storage capabilities of an external service to store this + // information and perform computation. + // See https://docs.microsoft.com/en-us/azure/event-grid/compare-messaging-services for more details. + private readonly Dictionary> _temperatureReadingsDateTimeOffset = + new Dictionary>(); + + // A dictionary to hold all desired property change callbacks that this pnp device should be able to handle. + // The key for this dictionary is the componentName. + private readonly IDictionary _desiredPropertyUpdateCallbacks = + new Dictionary(); + + // Dictionary to hold the current temperature for each "Thermostat" component. + private readonly Dictionary _temperature = new Dictionary(); + + // Dictionary to hold the max temperature since last reboot, for each "Thermostat" component. + private readonly Dictionary _maxTemp = new Dictionary(); + + // A safe initial value for caching the writable properties version is 1, so the client + // will process all previous property change requests and initialize the device application + // after which this version will be updated to that, so we have a high water mark of which version number + // has been processed. + private static long s_localWritablePropertiesVersion = 1; + + public TemperatureControllerSample(DeviceClient deviceClient) + { + _deviceClient = deviceClient ?? throw new ArgumentNullException(nameof(deviceClient)); + //_logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task InitOperationsAsync(CancellationToken cancellationToken) + { + _deviceClient.SetConnectionStatusChangesHandler(async (status, reason) => + { + //_logger.LogDebug($"Connection status change registered - status={status}, reason={reason}."); + + // Call GetWritablePropertiesAndHandleChangesAsync() to get writable properties from the server once the connection status changes into Connected. + // This can get back "lost" property updates in a device reconnection from status Disconnected_Retrying or Disconnected. + if (status == ConnectionStatus.Connected) { + await GetWritablePropertiesAndHandleChangesAsync(); + } + }); + + //_logger.LogDebug("Set handler for 'reboot' command."); + await _deviceClient.SetMethodHandlerAsync("reboot", HandleRebootCommandAsync, _deviceClient, cancellationToken); + + // For a component-level command, the command name is in the format "*". + //_logger.LogDebug($"Set handler for \"getMaxMinReport\" command."); + await _deviceClient.SetMethodHandlerAsync("thermostat1*getMaxMinReport", HandleMaxMinReportCommand, Thermostat1, cancellationToken); + await _deviceClient.SetMethodHandlerAsync("thermostat2*getMaxMinReport", HandleMaxMinReportCommand, Thermostat2, cancellationToken); + + //_logger.LogDebug("Set handler to receive 'targetTemperature' updates."); + await _deviceClient.SetDesiredPropertyUpdateCallbackAsync(SetDesiredPropertyUpdateCallback, null, cancellationToken); + _desiredPropertyUpdateCallbacks.Add(Thermostat1, TargetTemperatureUpdateCallbackAsync); + _desiredPropertyUpdateCallbacks.Add(Thermostat2, TargetTemperatureUpdateCallbackAsync); + + //_logger.LogDebug("For each component, check if the device properties are empty on the initial startup."); + await CheckEmptyPropertiesAsync(Thermostat1, cancellationToken); + await CheckEmptyPropertiesAsync(Thermostat2, cancellationToken); + + await UpdateDeviceInformationAsync(cancellationToken); + await SendDeviceSerialNumberAsync(cancellationToken); + + _maxTemp[Thermostat1] = 0d; + _maxTemp[Thermostat2] = 0d; + } + + public async Task PerformOperationsAsync(CancellationToken cancellationToken) + { + // This sample follows the following workflow: + // -> Set handler to receive and respond to connection status changes. + // -> Set handler to receive "reboot" command - root interface. + // -> Set handler to receive "getMaxMinReport" command - on "Thermostat" components. + // -> Set handler to receive "targetTemperature" property updates from service - on "Thermostat" components. + // -> Check if the properties are empty on the initial startup - for each "Thermostat" component. If so, report the default values with ACK to the hub. + // -> Update device information on "deviceInformation" component. + // -> Send initial device info - "workingSet" over telemetry, "serialNumber" over reported property update - root interface. + // -> Periodically send "temperature" over telemetry - on "Thermostat" components. + // -> Send "maxTempSinceLastReboot" over property update, when a new max temperature is set - on "Thermostat" components. + + + + //while (!cancellationToken.IsCancellationRequested) + { + //if (temperatureReset) + //{ + // // Generate a random value between 5.0°C and 45.0°C for the current temperature reading for each "Thermostat" component. + // _temperature[Thermostat1] = Math.Round(s_random.NextDouble() * 40.0 + 5.0, 1); + // _temperature[Thermostat2] = Math.Round(s_random.NextDouble() * 40.0 + 5.0, 1); + //} + + await SendTemperatureAsync(Thermostat1, cancellationToken); + //await SendTemperatureAsync(Thermostat2, cancellationToken); + //await SendDeviceMemoryAsync(cancellationToken); + + //temperatureReset = _temperature[Thermostat1] == 0 && _temperature[Thermostat2] == 0; + await Task.Delay(5 * 1000, cancellationToken); + } + } + + private async Task GetWritablePropertiesAndHandleChangesAsync() + { + Twin twin = await _deviceClient.GetTwinAsync(); + //_logger.LogInformation($"Device retrieving twin values on CONNECT: {twin.ToJson()}"); + + TwinCollection twinCollection = twin.Properties.Desired; + long serverWritablePropertiesVersion = twinCollection.Version; + + // Check if the writable property version is outdated on the local side. + // For the purpose of this sample, we'll only check the writable property versions between local and server + // side without comparing the property values. + if (serverWritablePropertiesVersion > s_localWritablePropertiesVersion) + { + //_logger.LogInformation($"The writable property version cached on local is changing " + + //$"from {s_localWritablePropertiesVersion} to {serverWritablePropertiesVersion}."); + + foreach (KeyValuePair propertyUpdate in twinCollection) + { + string componentName = propertyUpdate.Key; + switch (componentName) + { + case Thermostat1: + case Thermostat2: + // This will be called when a device client gets initialized and the _temperature dictionary is still empty. + if (!_temperature.TryGetValue(componentName, out double value)) + { + _temperature[componentName] = 21d; // The default temperature value is 21°C. + } + await TargetTemperatureUpdateCallbackAsync(twinCollection, componentName); + break; + + default: + //_logger.LogWarning($"Property: Received an unrecognized property update from service:" + + //$"\n[ {propertyUpdate.Key}: {propertyUpdate.Value} ]."); + break; + } + } + + //_logger.LogInformation($"The writable property version on local is currently {s_localWritablePropertiesVersion}."); + } + } + + // The callback to handle "reboot" command. This method will send a temperature update (of 0°C) over telemetry for both associated components. + private async Task HandleRebootCommandAsync(MethodRequest request, object userContext) + { + try + { + int delay = JsonConvert.DeserializeObject(request.DataAsJson); + + //_logger.LogDebug($"Command: Received - Rebooting thermostat (resetting temperature reading to 0°C after {delay} seconds)."); + await Task.Delay(delay * 1000); + + //_logger.LogDebug("\tRebooting..."); + + _temperature[Thermostat1] = _maxTemp[Thermostat1] = 0; + _temperature[Thermostat2] = _maxTemp[Thermostat2] = 0; + + _temperatureReadingsDateTimeOffset.Clear(); + + //_logger.LogDebug("\tRestored."); + } catch (JsonReaderException /*ex*/) + { + //_logger.LogDebug($"Command input is invalid: {ex.Message}."); + return new MethodResponse((int)StatusCode.BadRequest); + } + + return new MethodResponse((int)StatusCode.Completed); + } + + // The callback to handle "getMaxMinReport" command. This method will returns the max, min and average temperature from the + // specified time to the current time. + private Task HandleMaxMinReportCommand(MethodRequest request, object userContext) + { + try + { + string componentName = (string)userContext; + DateTime sinceInUtc = JsonConvert.DeserializeObject(request.DataAsJson); + var sinceInDateTimeOffset = new DateTimeOffset(sinceInUtc); + + if (_temperatureReadingsDateTimeOffset.ContainsKey(componentName)) + { + //_logger.LogDebug($"Command: Received - component=\"{componentName}\", generating max, min and avg temperature " + + //$"report since {sinceInDateTimeOffset.LocalDateTime}."); + + Dictionary allReadings = _temperatureReadingsDateTimeOffset[componentName]; + Dictionary filteredReadings = allReadings.Where(i => i.Key > sinceInDateTimeOffset) + .ToDictionary(i => i.Key, i => i.Value); + + if (filteredReadings != null && filteredReadings.Any()) + { + var report = new + { + maxTemp = filteredReadings.Values.Max(), + minTemp = filteredReadings.Values.Min(), + avgTemp = filteredReadings.Values.Average(), + startTime = filteredReadings.Keys.Min(), + endTime = filteredReadings.Keys.Max(), + }; + + //_logger.LogDebug($"Command: component=\"{componentName}\", MaxMinReport since {sinceInDateTimeOffset.LocalDateTime}:" + + // $" maxTemp={report.maxTemp}, minTemp={report.minTemp}, avgTemp={report.avgTemp}, startTime={report.startTime.LocalDateTime}, " + + // $"endTime={report.endTime.LocalDateTime}"); + + byte[] responsePayload = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(report)); + return Task.FromResult(new MethodResponse(responsePayload, (int)StatusCode.Completed)); + } + + //_logger.LogDebug($"Command: component=\"{componentName}\", no relevant readings found since {sinceInDateTimeOffset.LocalDateTime}, " + + // $"cannot generate any report."); + return Task.FromResult(new MethodResponse((int)StatusCode.NotFound)); + } + + //_logger.LogDebug($"Command: component=\"{componentName}\", no temperature readings sent yet, cannot generate any report."); + return Task.FromResult(new MethodResponse((int)StatusCode.NotFound)); + } + catch (JsonReaderException /*ex*/) + { + //_logger.LogDebug($"Command input is invalid: {ex.Message}."); + return Task.FromResult(new MethodResponse((int)StatusCode.BadRequest)); + } + } + + private Task SetDesiredPropertyUpdateCallback(TwinCollection desiredProperties, object userContext) + { + bool callbackNotInvoked = true; + + foreach (KeyValuePair propertyUpdate in desiredProperties) + { + string componentName = propertyUpdate.Key; + if (_desiredPropertyUpdateCallbacks.ContainsKey(componentName)) + { + _desiredPropertyUpdateCallbacks[componentName]?.Invoke(desiredProperties, componentName); + callbackNotInvoked = false; + } + } + + if (callbackNotInvoked) + { + //_logger.LogDebug($"Property: Received a property update that is not implemented by any associated component."); + } + + return Task.CompletedTask; + } + + // The desired property update callback, which receives the target temperature as a desired property update, + // and updates the current temperature value over telemetry and property update. + private async Task TargetTemperatureUpdateCallbackAsync(TwinCollection desiredProperties, object userContext) + { + string componentName = (string)userContext; + + bool targetTempUpdateReceived = PnpConvention.TryGetPropertyFromTwin( + desiredProperties, + TargetTemperatureProperty, + out double targetTemperature, + componentName); + if (!targetTempUpdateReceived) + { + //_logger.LogDebug($"Property: Update - component=\"{componentName}\", received an update which is not associated with a valid property.\n{desiredProperties.ToJson()}"); + return; + } + + //_logger.LogDebug($"Property: Received - component=\"{componentName}\", {{ \"{TargetTemperatureProperty}\": {targetTemperature}°C }}."); + + s_localWritablePropertiesVersion = desiredProperties.Version; + + TwinCollection pendingReportedProperty = PnpConvention.CreateComponentWritablePropertyResponse( + componentName, + TargetTemperatureProperty, + targetTemperature, + (int)StatusCode.InProgress, + desiredProperties.Version, + "In progress - reporting current temperature"); + + await _deviceClient.UpdateReportedPropertiesAsync(pendingReportedProperty); + //_logger.LogDebug($"Property: Update - component=\"{componentName}\", {{\"{TargetTemperatureProperty}\": {targetTemperature} }} in °C is {StatusCode.InProgress}."); + + // Update Temperature in 2 steps + double step = (targetTemperature - _temperature[componentName]) / 2d; + for (int i = 1; i <= 2; i++) + { + _temperature[componentName] = Math.Round(_temperature[componentName] + step, 1); + await Task.Delay(6 * 1000); + } + + TwinCollection completedReportedProperty = PnpConvention.CreateComponentWritablePropertyResponse( + componentName, + TargetTemperatureProperty, + _temperature[componentName], + (int)StatusCode.Completed, + desiredProperties.Version, + "Successfully updated target temperature"); + + await _deviceClient.UpdateReportedPropertiesAsync(completedReportedProperty); + //_logger.LogDebug($"Property: Update - component=\"{componentName}\", {{\"{TargetTemperatureProperty}\": {_temperature[componentName]} }} in °C is {StatusCode.Completed}"); + } + + // Report the property updates on "deviceInformation" component. + private async Task UpdateDeviceInformationAsync(CancellationToken cancellationToken) + { + const string componentName = "deviceInformation"; + + TwinCollection deviceInfoTc = PnpConvention.CreateComponentPropertyPatch( + componentName, + new Dictionary + { + { "manufacturer", "element15" }, + { "model", "ModelIDxcdvmk" }, + { "swVersion", "1.0.0" }, + { "osName", "Windows 10" }, + { "processorArchitecture", "64-bit" }, + { "processorManufacturer", "Intel" }, + { "totalStorage", 256 }, + { "totalMemory", 1024 }, + }); + + await _deviceClient.UpdateReportedPropertiesAsync(deviceInfoTc, cancellationToken); + //_logger.LogDebug($"Property: Update - component = '{componentName}', properties update is complete."); + } + + // Send working set of device memory over telemetry. + private async Task SendDeviceMemoryAsync(CancellationToken cancellationToken) + { + const string workingSetName = "workingSet"; + + long workingSet = Process.GetCurrentProcess().PrivateMemorySize64 / 1024; + + var telemetry = new Dictionary + { + { workingSetName, workingSet }, + }; + + using Message msg = PnpConvention.CreateMessage(telemetry); + + await _deviceClient.SendEventAsync(msg, cancellationToken); + //_logger.LogDebug($"Telemetry: Sent - {JsonConvert.SerializeObject(telemetry)} in KB."); + } + + // Send device serial number over property update. + private async Task SendDeviceSerialNumberAsync(CancellationToken cancellationToken) + { + const string propertyName = "serialNumber"; + TwinCollection reportedProperties = PnpConvention.CreatePropertyPatch(propertyName, SerialNumber); + + await _deviceClient.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken); + //var oBrace = '{'; + //var cBrace = '}'; + //_logger.LogDebug($"Property: Update - {oBrace} \"{propertyName}\": \"{SerialNumber}\" {cBrace} is complete."); + } + + private async Task SendTemperatureAsync(string componentName, CancellationToken cancellationToken) + { + await SendTemperatureTelemetryAsync(componentName, cancellationToken); + + double maxTemp = _temperatureReadingsDateTimeOffset[componentName].Values.Max(); + if (maxTemp > _maxTemp[componentName]) + { + _maxTemp[componentName] = maxTemp; + await UpdateMaxTemperatureSinceLastRebootAsync(componentName, cancellationToken); + } + } + + private async Task SendTemperatureTelemetryAsync(string componentName, CancellationToken cancellationToken) + { + //const string telemetryName = "temperature"; + double currentTemperature = _temperature[componentName]; + using Message msg = PnpConvention.CreateMessage(Telemetry, /*telemetryName, currentTemperature, */componentName); + await _deviceClient.SendEventAsync(msg, cancellationToken); + + //_logger.LogDebug($"Telemetry: Sent - component=\"{componentName}\", {{ \"{telemetryName}\": {currentTemperature} }} in °C."); + + if (_temperatureReadingsDateTimeOffset.ContainsKey(componentName)) + { + _temperatureReadingsDateTimeOffset[componentName].TryAdd(DateTimeOffset.UtcNow, currentTemperature); + } + else + { + _temperatureReadingsDateTimeOffset.TryAdd( + componentName, + new Dictionary + { + { DateTimeOffset.UtcNow, currentTemperature }, + }); + } + } + + private async Task UpdateMaxTemperatureSinceLastRebootAsync(string componentName, CancellationToken cancellationToken) + { + const string propertyName = "maxTempSinceLastReboot"; + double maxTemp = _maxTemp[componentName]; + TwinCollection reportedProperties = PnpConvention.CreateComponentPropertyPatch(componentName, propertyName, maxTemp); + + await _deviceClient.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken); + //_logger.LogDebug($"Property: Update - component=\"{componentName}\", {{ \"{propertyName}\": {maxTemp} }} in °C is complete."); + } + + private async Task CheckEmptyPropertiesAsync(string componentName, CancellationToken cancellationToken) + { + Twin twin = await _deviceClient.GetTwinAsync(cancellationToken); + TwinCollection writableProperty = twin.Properties.Desired; + TwinCollection reportedProperty = twin.Properties.Reported; + + // Check if the device properties (both writable and reported) for the current component are empty. + if (!writableProperty.Contains(componentName) && !reportedProperty.Contains(componentName)) + { + await ReportInitialPropertyAsync(componentName, TargetTemperatureProperty, cancellationToken); + } + } + + private async Task ReportInitialPropertyAsync(string componentName, string propertyName, CancellationToken cancellationToken) + { + // If the device properties are empty, report the default value with ACK(ac=203, av=0) as part of the PnP convention. + // "DefaultPropertyValue" is set from the device when the desired property is not set via the hub. + TwinCollection reportedProperties = PnpConvention.CreateComponentWritablePropertyResponse( + componentName, + propertyName, + DefaultPropertyValue, + (int)StatusCode.ReportDeviceInitialProperty, + DefaultAckVersion, + "Initialized with default value"); + + await _deviceClient.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken); + + //_logger.LogDebug($"Report the default values for \"{componentName}\".\nProperty: Update - {reportedProperties.ToJson()} is complete."); + } + } +} diff --git a/examples/QtAzureIoT/device/DeviceToBackoffice/WritablePropertyResponse.cs b/examples/QtAzureIoT/device/DeviceToBackoffice/WritablePropertyResponse.cs new file mode 100644 index 0000000..3cbfb64 --- /dev/null +++ b/examples/QtAzureIoT/device/DeviceToBackoffice/WritablePropertyResponse.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; + +namespace PnpHelpers +{ + /// + /// The payload for a property update response. + /// + public class WritablePropertyResponse + { + /// + /// Empty constructor. + /// + public WritablePropertyResponse() { } + + /// + /// Convenience constructor for specifying the properties. + /// + /// The unserialized property value. + /// The acknowledgment code, usually an HTTP Status Code e.g. 200, 400. + /// The acknowledgment version, as supplied in the property update request. + /// The acknowledgment description, an optional, human-readable message about the result of the property update. + public WritablePropertyResponse(object propertyValue, int ackCode, long ackVersion, string ackDescription = null) + { + PropertyValue = propertyValue; + AckCode = ackCode; + AckVersion = ackVersion; + AckDescription = ackDescription; + } + + /// + /// The unserialized property value. + /// + [JsonProperty("value")] + public object PropertyValue { get; set; } + + /// + /// The acknowledgment code, usually an HTTP Status Code e.g. 200, 400. + /// + [JsonProperty("ac")] + public int AckCode { get; set; } + + /// + /// The acknowledgment version, as supplied in the property update request. + /// + [JsonProperty("av")] + public long AckVersion { get; set; } + + /// + /// The acknowledgment description, an optional, human-readable message about the result of the property update. + /// + [JsonProperty("ad", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string AckDescription { get; set; } + } +} diff --git a/examples/QtAzureIoT/device/SensorData/SensorData.cs b/examples/QtAzureIoT/device/SensorData/SensorData.cs new file mode 100644 index 0000000..c58c5c1 --- /dev/null +++ b/examples/QtAzureIoT/device/SensorData/SensorData.cs @@ -0,0 +1,107 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +using System.Diagnostics; +using System.Device.I2c; +using Iot.Device.Bmxx80; +using Iot.Device.Bmxx80.PowerMode; +using QtAzureIoT.Utils; + +namespace QtAzureIoT.Device +{ + public class SensorData : PropertySet, IDisposable + { + public SensorData() + { } + + public double Temperature + { + get => propertyTemperature; + private set => SetProperty(ref propertyTemperature, value, nameof(Temperature)); + } + + public double Pressure + { + get => propertyPressure; + private set => SetProperty(ref propertyPressure, value, nameof(Pressure)); + } + + public double Humidity + { + get => propertyHumidity; + private set => SetProperty(ref propertyHumidity, value, nameof(Humidity)); + } + + public void StartPolling() + { + if (Sensor != null) + return; + BusConnectionSettings = new I2cConnectionSettings(1, Bme280.DefaultI2cAddress); + BusDevice = I2cDevice.Create(BusConnectionSettings); + Sensor = new Bme280(BusDevice); + MesasuramentDelay = Sensor.GetMeasurementDuration(); + + PollingLoop = new CancellationTokenSource(); + Polling = new Task(async () => await PollingLoopAsync(), PollingLoop.Token); + Polling.Start(); + } + + public void StopPolling() + { + if (Sensor == null) + return; + PollingLoop.Cancel(); + Polling.Wait(); + Sensor.Dispose(); + Sensor = null; + BusDevice.Dispose(); + BusDevice = null; + BusConnectionSettings = null; + PollingLoop.Dispose(); + PollingLoop = null; + Polling.Dispose(); + Polling = null; + } + + #region private + private I2cConnectionSettings BusConnectionSettings { get; set; } + private I2cDevice BusDevice { get; set; } + private Bme280 Sensor { get; set; } + int MesasuramentDelay { get; set; } + private CancellationTokenSource PollingLoop { get; set; } + private Task Polling { get; set; } + + + private async Task PollingLoopAsync() + { + while (!PollingLoop.IsCancellationRequested) { + try { + Sensor.SetPowerMode(Bmx280PowerMode.Forced); + await Task.Delay(MesasuramentDelay); + + if (Sensor.TryReadTemperature(out var tempValue)) + Temperature = tempValue.DegreesCelsius; + if (Sensor.TryReadPressure(out var preValue)) + Pressure = preValue.Hectopascals; + if (Sensor.TryReadHumidity(out var humValue)) + Humidity = humValue.Percent; + } catch (Exception e) { + Debug.WriteLine($"Exception: {e.GetType().Name}: {e.Message}"); + } + await Task.Delay(1000); + } + } + + public void Dispose() + { + StopPolling(); + } + + double propertyTemperature; + double propertyPressure; + double propertyHumidity; + #endregion + } +} diff --git a/examples/QtAzureIoT/device/SensorData/SensorData.csproj b/examples/QtAzureIoT/device/SensorData/SensorData.csproj new file mode 100644 index 0000000..c694f5b --- /dev/null +++ b/examples/QtAzureIoT/device/SensorData/SensorData.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + disable + + + + + + + + + + + diff --git a/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj b/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj new file mode 100644 index 0000000..fb25e64 --- /dev/null +++ b/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj @@ -0,0 +1,126 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {2B3D1059-93F6-42F8-9709-AE06BEDE1EEF} + QtVS_v304 + 10.0.19041.0 + 10.0.19041.0 + $(MSBuildProjectDirectory)\QtMsBuild + + + + Application + v143 + + + Application + v143 + + + + + + + 6.2.7_msvc2019_64 + core;quick + debug + + + 6.2.7_msvc2019_64 + core;quick + release + + + + + + + + + + + + + + + + + C:\dev\source\qt-labs\qtdotnet\include;$(IncludePath) + bin\$(Platform)\$(Configuration)\ + obj\$(Platform)\$(Configuration)\ + true + + + C:\dev\source\qt-labs\qtdotnet\include;$(IncludePath) + bin\$(Platform)\$(Configuration)\ + obj\$(Platform)\$(Configuration)\ + true + + + + true + true + ProgramDatabase + Disabled + + + Windows + true + + + + + true + true + None + MaxSpeed + + + Windows + false + + + + + input + %(Filename).moc + input + %(Filename).moc + + + + Document + true + true + + + + + + {66a69341-3b00-4812-aa77-ec5c2e9ea23a} + true + false + + + {d8cd7e8d-7eca-46a7-aa39-e471f99c94f0} + true + false + + + + + + + + + \ No newline at end of file diff --git a/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj.filters b/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj.filters new file mode 100644 index 0000000..4965748 --- /dev/null +++ b/examples/QtAzureIoT/device/deviceapp/deviceapp.vcxproj.filters @@ -0,0 +1,41 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {99349809-55BA-4b9d-BF79-8FDBB0286EB3} + ui + + + {639EADAA-A684-42e4-A9AD-28FC9BCB8F7C} + ts + + + + + Resource Files + + + Source Files + + + + + + + + Source Files + + + \ No newline at end of file diff --git a/examples/QtAzureIoT/device/deviceapp/main.cpp b/examples/QtAzureIoT/device/deviceapp/main.cpp new file mode 100644 index 0000000..ba049d1 --- /dev/null +++ b/examples/QtAzureIoT/device/deviceapp/main.cpp @@ -0,0 +1,183 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +#include +#include +#include + +#include +#include + +class Backoffice : public QDotNetObject +{ +public: + Q_DOTNET_OBJECT_INLINE(Backoffice, "QtAzureIoT.Device.Backoffice, DeviceToBackoffice"); + Backoffice() : QDotNetObject(getConstructor().invoke(nullptr)) + {} + void setTelemetry(QString name, double value) + { + getMethod("SetTelemetry", fnSetTelemetryDouble).invoke(*this, name, value); + } + void setTelemetry(QString name, bool value) + { + getMethod("SetTelemetry", fnSetTelemetryBool).invoke(*this, name, value); + } +public: + void startPolling() { + getMethod("StartPolling", fnStartPolling).invoke(*this); + } + void stopPolling() { + getMethod("StopPolling", fnStopPolling).invoke(*this); + } +private: + mutable QDotNetFunction fnSetTelemetryDouble; + mutable QDotNetFunction fnSetTelemetryBool; + mutable QDotNetFunction fnStartPolling; + mutable QDotNetFunction fnStopPolling; +}; + +class SensorData : public QObject, public QDotNetObject, public QDotNetObject::IEventHandler +{ + Q_OBJECT + Q_PROPERTY(double temperature READ temperature NOTIFY temperatureChanged) + Q_PROPERTY(double pressure READ pressure NOTIFY pressureChanged) + Q_PROPERTY(double humidity READ humidity NOTIFY humidityChanged) +public: + Q_DOTNET_OBJECT_INLINE(SensorData, "QtAzureIoT.Device.SensorData, SensorData"); + SensorData() : QDotNetObject(getConstructor().invoke(nullptr)) + { + subscribeEvent("PropertyChanged", this); + } + double temperature() const + { + return getMethod("get_Temperature", fnGet_Temperature).invoke(*this); + } + double pressure() const + { + return getMethod("get_Pressure", fnGet_Pressure).invoke(*this); + } + double humidity() const + { + return getMethod("get_Humidity", fnGet_Humidity).invoke(*this); + } +public slots: + void startPolling() { + getMethod("StartPolling", fnStartPolling).invoke(*this); + } + void stopPolling() { + getMethod("StopPolling", fnStopPolling).invoke(*this); + } +signals: + void temperatureChanged(); + void pressureChanged(); + void humidityChanged(); +private: + void handleEvent(const QString& evName, QDotNetObject& evSrc, QDotNetObject& evArgs) override + { + if (evName == "PropertyChanged") { + if (evArgs.type().fullName() == QDotNetPropertyEvent::FullyQualifiedTypeName) { + auto propertyChangedEvent = evArgs.cast(); + if (propertyChangedEvent.propertyName() == "Temperature") + emit temperatureChanged(); + else if (propertyChangedEvent.propertyName() == "Pressure") + emit pressureChanged(); + else if (propertyChangedEvent.propertyName() == "Humidity") + emit humidityChanged(); + } + } + } + mutable QDotNetFunction fnGet_Temperature; + mutable QDotNetFunction fnGet_Pressure; + mutable QDotNetFunction fnGet_Humidity; + mutable QDotNetFunction fnStartPolling; + mutable QDotNetFunction fnStopPolling; +}; + +class CardReader : public QObject, public QDotNetObject, public QDotNetObject::IEventHandler +{ + Q_OBJECT + Q_PROPERTY(bool cardInReader READ cardInReader NOTIFY cardInReaderChanged) +public: + Q_DOTNET_OBJECT_INLINE(CardReader, "QtAzureIoT.Device.CardReader, CardReader"); + CardReader() : QDotNetObject(getConstructor().invoke(nullptr)) + { + subscribeEvent("PropertyChanged", this); + } + bool cardInReader() const + { + return getMethod("get_CardInReader", fnGet_CardInReader).invoke(*this); + } +public slots: + void startPolling() { + getMethod("StartPolling", fnStartPolling).invoke(*this); + } + void stopPolling() { + getMethod("StopPolling", fnStopPolling).invoke(*this); + } +signals: + void cardInReaderChanged(); +private: + void handleEvent(const QString& evName, QDotNetObject& evSrc, QDotNetObject& evArgs) override + { + if (evName == "PropertyChanged") { + if (evArgs.type().fullName() == QDotNetPropertyEvent::FullyQualifiedTypeName) { + auto propertyChangedEvent = evArgs.cast(); + if (propertyChangedEvent.propertyName() == "CardInReader") + emit cardInReaderChanged(); + } + } + } + mutable QDotNetFunction fnGet_CardInReader; + mutable QDotNetFunction fnStartPolling; + mutable QDotNetFunction fnStopPolling; +}; + +int main(int argc, char* argv[]) +{ + QGuiApplication app(argc, argv); + QQmlApplicationEngine engine; + + CardReader card; + card.startPolling(); + engine.rootContext()->setContextProperty("card", &card); + + SensorData sensor; + sensor.startPolling(); + engine.rootContext()->setContextProperty("sensor", &sensor); + + Backoffice backoffice; + QObject::connect(&card, &CardReader::cardInReaderChanged, + [&backoffice, &card]() + { + backoffice.setTelemetry("card", card.cardInReader()); + }); + + QObject::connect(&sensor, &SensorData::temperatureChanged, + [&backoffice, &sensor]() + { + backoffice.setTelemetry("temperature", sensor.temperature()); + }); + + QObject::connect(&sensor, &SensorData::pressureChanged, + [&backoffice, &sensor]() + { + backoffice.setTelemetry("pressure", sensor.pressure()); + }); + + QObject::connect(&sensor, &SensorData::humidityChanged, + [&backoffice, &sensor]() + { + backoffice.setTelemetry("humidity", sensor.humidity()); + }); + backoffice.startPolling(); + + engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); + if (engine.rootObjects().isEmpty()) + return -1; + + return app.exec(); +} + +#include "main.moc" diff --git a/examples/QtAzureIoT/device/deviceapp/main.qml b/examples/QtAzureIoT/device/deviceapp/main.qml new file mode 100644 index 0000000..368ce0c --- /dev/null +++ b/examples/QtAzureIoT/device/deviceapp/main.qml @@ -0,0 +1,56 @@ +/*************************************************************************************************** + Copyright (C) 2023 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +***************************************************************************************************/ + +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes + +Window { + visible: true + width: 800 + height: 480 + flags: Qt.FramelessWindowHint + color: "black" + GridLayout { + anchors.fill: parent + columns: 2 + Text { + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter + font { bold: true; pointSize: 32 } + color: "white" + text: sensor.temperature.toFixed(2) + " deg.C." + } + Text { + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter + font { bold: true; pointSize: 32 } + color: "white" + text: card.cardInReader ? "CARD DETECTED" : "No card"; + } + Text { + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter + font { bold: true; pointSize: 32 } + color: "white" + text: sensor.pressure.toFixed(2) + " hPa." + } + Text { + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter + font { bold: true; pointSize: 32 } + color: "white" + text: sensor.humidity.toFixed(2) + " %" + } + } + Button { + text: "EXIT" + onClicked: Qt.exit(0) + anchors.right: parent.right + anchors.bottom: parent.bottom + } +} diff --git a/examples/QtAzureIoT/device/deviceapp/qml.qrc b/examples/QtAzureIoT/device/deviceapp/qml.qrc new file mode 100644 index 0000000..5f6483a --- /dev/null +++ b/examples/QtAzureIoT/device/deviceapp/qml.qrc @@ -0,0 +1,5 @@ + + + main.qml + +