diff --git a/packages/movesense_flutter/.gitignore b/packages/movesense_flutter/.gitignore new file mode 100644 index 000000000..dd5eb9895 --- /dev/null +++ b/packages/movesense_flutter/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +/build/ +/coverage/ diff --git a/packages/movesense_flutter/.metadata b/packages/movesense_flutter/.metadata new file mode 100644 index 000000000..e0ef2019b --- /dev/null +++ b/packages/movesense_flutter/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407" + channel: "stable" + +project_type: package diff --git a/packages/movesense_flutter/CHANGELOG.md b/packages/movesense_flutter/CHANGELOG.md new file mode 100644 index 000000000..bbd403c48 --- /dev/null +++ b/packages/movesense_flutter/CHANGELOG.md @@ -0,0 +1,13 @@ +## 1.0.0 + +Initial release supporting: + +* scan for Movensense devices (and stop scanning again) +* connect and disconnect to a device +* get device and state information +* get a stream of the following data from a device: + * heart rate + * ECG + * IMU + * temperature + * state events (movement, battery, connectors, double tap, tap, free fall) diff --git a/packages/movesense_flutter/LICENSE b/packages/movesense_flutter/LICENSE new file mode 100644 index 000000000..efc75bc14 --- /dev/null +++ b/packages/movesense_flutter/LICENSE @@ -0,0 +1,9 @@ +MIT License. + +Copyright 2025 the Technical University of Denmark (DTU). + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ”Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED ”AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/movesense_flutter/README.md b/packages/movesense_flutter/README.md new file mode 100644 index 000000000..29cbfd3e0 --- /dev/null +++ b/packages/movesense_flutter/README.md @@ -0,0 +1,172 @@ +A Flutter plugin for accessing the [Movesense](https://www.movesense.com/) family of ECG devices. This is a developer-friendly wrapper of the [mdsflutter](https://pub.dev/packages/mdsflutter) plugin. + +## Features + +This plugin supports the following features, which is the most commonly used sub-set of the total [Movensene API](https://www.movesense.com/docs/esw/api_reference/): + +* scan for Movensense devices (and stop scanning again) +* connect and disconnect to a device +* get device and state information +* get a stream of the following data from a device: + * heart rate + * ECG + * IMU + * temperature + * state events (movement, battery, connectors, double tap, tap, free fall) + +## Getting started + +Similar to the [mdsflutter](https://pub.dev/packages/mdsflutter) plugin, the Movesense library needs to be installed in your app. + +### iOS + +Install the Movesense iOS library using CocoaPods with adding this line to your app's Podfile: + +```shell +pod 'Movesense', :git => 'ssh://git@altssh.bitbucket.org:443/movesense/movesense-mobile-lib.git' +``` + +Then set up your `ios/Podfile` as follows: + +```pod +target 'Runner' do + use_modular_headers! + use_frameworks! :linkage => :static + + pod 'Movesense', :git => 'https://bitbucket.org/movesense/movesense-mobile-lib.git' + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end +``` + +This will ensure that the MDS library is linked correctly. If you need to use frameworks that demand dynamic linking, [follow the instructions here](https://bitbucket.org/movesense/movesense-mobile-lib/issues/119/cannot-use-health-and-mdsflutter-depending). + +### Android + +Download `mdslib-x.x.x-release.aar` from the [movesense-mobile-lib](https://bitbucket.org/movesense/movesense-mobile-lib/src/master/) repository and put it somewhere under 'android' folder of your app. Preferably create a new folder named `android/libs` and put it there. + +In the `build.gradle` of your android project, add the following lines (assuming `.aar` file is in `android/libs`): + +```graddle +allprojects { + repositories { + ... + flatDir{ + dirs "$rootDir/libs" + } + } +} +``` + +## Usage + +### Scanning for devices + +The singleton `Movesense` can be used for scanning devices: + +```dart +/// Listen for discovered devices +Movesense().devices.listen((device) { + debugPrint('Discovered device: ${device.name} [${device.address}]'); +}); +/// Start scanning. +Movesense().scan(); + +/// Stop scanning at some later point. +Movesense().stopScan(); +``` + +### Connecting to a device + +Scanning return a `MovesenseDevice` which can be used to connect to the device, like: + +```dart +if (!device.isConnected) { + device.connect(); +} +``` + +A `MovesenseDevice` can either be obtained using the scan method above, or can be create if the address of the device is know: + +```dart +final MovesenseDevice device = MovesenseDevice(address: '0C:8C:DC:1B:23:BF'); + +device.connect(); +``` + +Connection status is available in the `status` can is emitted in the `statusEvents` stream. + +```dart +print(device.status); + +device.statusEvents.listen((status) { + print('Device connection status changed: ${status.name}'); +}); +``` + +### Accessing device information and battery status + +The following device information and status is available as getter methods: + +* `getDeviceInfo` - get the full [device info](https://www.movesense.com/docs/esw/api_reference/#info) of this Movesense device. +* `getBatteryStatus` - get the [battery level](https://www.movesense.com/docs/esw/api_reference/#systemstates) ("OK" or "LOW") from the device. +* `getState` - get the [system state](https://www.movesense.com/docs/esw/api_reference/#systemstates) from the device (movement, connectors, doubleTap, tap, freeFall). + +For example, getting device info and battery level: + +```dart +var info = await device.getDeviceInfo(); +print('Product name: ${info?.productName}'); + +var battery = await device.getBatteryStatus(); +print('Battery level: ${battery.name}'); +``` + +### Streaming sensor data + +The following sensor data is available as streams: + +* `hr` - Heart rate as an `int` at 1 Hz. +* `ecg` - Electrocardiography (ECG) as a sample of reading at 125 Hz. +* `imu` - 9-axis Inertial Movement Unit (IMU) at 13 Hz. +* `temperature` - Surface temperature of the device in Kelvin. + +For example, you can listen to the heart rate stream like this: + +```dart +// Start listening to the stream of heart rate readings +var hrSubscription = device.hr.listen((hr) { + print('Heart Rate: $hr'); +}); + +// Stop listening. +hrSubscription?.cancel(); +``` + +The example app shows streaming heart rate data, once connected to a device. + +### Streaming state change events + +You can also listen to a stream of [system state](https://www.movesense.com/docs/esw/api_reference/#systemstates) from the device (movement, connectors, doubleTap, tap, freeFall). For example, listening for 'tap' events like this: + +```dart +stateSubscription = device + .getStateEvents(SystemStateComponent.tap) + .listen((state) { + print('State: $state'); + }); +``` + +Note, however, that listening to system state events on a Movensense device comes with a lot of limitations. First of all, you can only listen to one type of state events at a time. Second, not all Movesense devices seems to support subscription of all types of state events. For example, it seems like only the 'connectors' and 'tap' states are supported on the Movesense MD and HR2 devices. + +## Example App + +The included example app is very simple. It shows how to connect to a device with a known `address` and once connected, it listens to the heart rate (`hr`) stream and shows it in the app. Use the floating button to (i) connect, (ii) start streaming heart rate data, and (iii) stop streaming again. + +## Features and bugs + +Please read about existing issues and file new feature requests and bug reports at the issue tracker. + +## License + +This software is copyright (c) the [Technical University of Denmark](https://dtu.dk/) (DTU) and is part of the [Copenhagen Research Platform](https://carp.dk/). This software is available 'as-is' under a [MIT license](LICENSE). diff --git a/packages/movesense_flutter/analysis_options.yaml b/packages/movesense_flutter/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/packages/movesense_flutter/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/movesense_flutter/example/.gitignore b/packages/movesense_flutter/example/.gitignore new file mode 100644 index 000000000..3820a95c6 --- /dev/null +++ b/packages/movesense_flutter/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/movesense_flutter/example/.metadata b/packages/movesense_flutter/example/.metadata new file mode 100644 index 000000000..a046c411a --- /dev/null +++ b/packages/movesense_flutter/example/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + - platform: android + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + - platform: ios + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/movesense_flutter/example/README.md b/packages/movesense_flutter/example/README.md new file mode 100644 index 000000000..202279b44 --- /dev/null +++ b/packages/movesense_flutter/example/README.md @@ -0,0 +1,3 @@ +# movesense_flutter_example + +A new Flutter project. diff --git a/packages/movesense_flutter/example/analysis_options.yaml b/packages/movesense_flutter/example/analysis_options.yaml new file mode 100644 index 000000000..f9b303465 --- /dev/null +++ b/packages/movesense_flutter/example/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/packages/movesense_flutter/example/android/.gitignore b/packages/movesense_flutter/example/android/.gitignore new file mode 100644 index 000000000..be3943c96 --- /dev/null +++ b/packages/movesense_flutter/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packages/movesense_flutter/example/android/app/build.gradle.kts b/packages/movesense_flutter/example/android/app/build.gradle.kts new file mode 100644 index 000000000..ae680e245 --- /dev/null +++ b/packages/movesense_flutter/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "dk.carp.movesense_flutter_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "dk.carp.movesense_flutter_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/movesense_flutter/example/android/app/src/debug/AndroidManifest.xml b/packages/movesense_flutter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..399f6981d --- /dev/null +++ b/packages/movesense_flutter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/movesense_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/movesense_flutter/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..867d930d8 --- /dev/null +++ b/packages/movesense_flutter/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/movesense_flutter/example/android/app/src/main/kotlin/dk/carp/movesense_flutter_example/MainActivity.kt b/packages/movesense_flutter/example/android/app/src/main/kotlin/dk/carp/movesense_flutter_example/MainActivity.kt new file mode 100644 index 000000000..6004ac712 --- /dev/null +++ b/packages/movesense_flutter/example/android/app/src/main/kotlin/dk/carp/movesense_flutter_example/MainActivity.kt @@ -0,0 +1,5 @@ +package dk.carp.movesense_flutter_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/movesense_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/movesense_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000..f74085f3f --- /dev/null +++ b/packages/movesense_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/movesense_flutter/example/android/app/src/main/res/drawable/launch_background.xml b/packages/movesense_flutter/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000..304732f88 --- /dev/null +++ b/packages/movesense_flutter/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/movesense_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/movesense_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..db77bb4b7 Binary files /dev/null and b/packages/movesense_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/movesense_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/movesense_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..17987b79b Binary files /dev/null and b/packages/movesense_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/movesense_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/movesense_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..09d439148 Binary files /dev/null and b/packages/movesense_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/movesense_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/movesense_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d5f1c8d34 Binary files /dev/null and b/packages/movesense_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/movesense_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/movesense_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/movesense_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/movesense_flutter/example/android/app/src/main/res/values-night/styles.xml b/packages/movesense_flutter/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000..06952be74 --- /dev/null +++ b/packages/movesense_flutter/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/movesense_flutter/example/android/app/src/main/res/values/styles.xml b/packages/movesense_flutter/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..cb1ef8805 --- /dev/null +++ b/packages/movesense_flutter/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/movesense_flutter/example/android/app/src/profile/AndroidManifest.xml b/packages/movesense_flutter/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000..399f6981d --- /dev/null +++ b/packages/movesense_flutter/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/movesense_flutter/example/android/build.gradle.kts b/packages/movesense_flutter/example/android/build.gradle.kts new file mode 100644 index 000000000..317aa589d --- /dev/null +++ b/packages/movesense_flutter/example/android/build.gradle.kts @@ -0,0 +1,27 @@ +allprojects { + repositories { + google() + mavenCentral() + flatDir{ + dirs("$rootDir/libs") + } + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/movesense_flutter/example/android/gradle.properties b/packages/movesense_flutter/example/android/gradle.properties new file mode 100644 index 000000000..fbee1d8cd --- /dev/null +++ b/packages/movesense_flutter/example/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/packages/movesense_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/movesense_flutter/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e4ef43fb9 --- /dev/null +++ b/packages/movesense_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/packages/movesense_flutter/example/android/libs/mdslib-3.15.0(1)-release.aar b/packages/movesense_flutter/example/android/libs/mdslib-3.15.0(1)-release.aar new file mode 100644 index 000000000..5e70b0665 Binary files /dev/null and b/packages/movesense_flutter/example/android/libs/mdslib-3.15.0(1)-release.aar differ diff --git a/packages/movesense_flutter/example/android/settings.gradle.kts b/packages/movesense_flutter/example/android/settings.gradle.kts new file mode 100644 index 000000000..ca7fe065c --- /dev/null +++ b/packages/movesense_flutter/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/packages/movesense_flutter/example/ios/.gitignore b/packages/movesense_flutter/example/ios/.gitignore new file mode 100644 index 000000000..7a7f9873a --- /dev/null +++ b/packages/movesense_flutter/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/movesense_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/movesense_flutter/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000..1dc6cf765 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/packages/movesense_flutter/example/ios/Flutter/Debug.xcconfig b/packages/movesense_flutter/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000..ec97fc6f3 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/movesense_flutter/example/ios/Flutter/Release.xcconfig b/packages/movesense_flutter/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000..c4855bfe2 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/movesense_flutter/example/ios/Podfile b/packages/movesense_flutter/example/ios/Podfile new file mode 100644 index 000000000..620e46eba --- /dev/null +++ b/packages/movesense_flutter/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..4ad176a12 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,619 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 59TCTNUBMQ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dk.carp.movesenseFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dk.carp.movesenseFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dk.carp.movesenseFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dk.carp.movesenseFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 59TCTNUBMQ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dk.carp.movesenseFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 59TCTNUBMQ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dk.carp.movesenseFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/movesense_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/movesense_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..e3773d42e --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/movesense_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/movesense_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..1d526a16e --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/movesense_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/movesense_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/movesense_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/movesense_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/movesense_flutter/example/ios/Runner/AppDelegate.swift b/packages/movesense_flutter/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000..626664468 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..d36b1fab2 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000..dc9ada472 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000..7353c41ec Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000..797d452e4 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000..6ed2d933e Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000..4cd7b0099 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000..fe730945a Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000..321773cd8 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000..797d452e4 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000..502f463a9 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000..0ec303439 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000..0ec303439 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000..e9f5fea27 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000..84ac32ae7 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000..8953cba09 Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000..0467bf12a Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000..0bedcf2fd --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000..89c2725b7 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/movesense_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/movesense_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..f2e259c7c --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/movesense_flutter/example/ios/Runner/Base.lproj/Main.storyboard b/packages/movesense_flutter/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000..f3c28516f --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/movesense_flutter/example/ios/Runner/Info.plist b/packages/movesense_flutter/example/ios/Runner/Info.plist new file mode 100644 index 000000000..9cc1b59d3 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner/Info.plist @@ -0,0 +1,59 @@ + + + + + + NSBluetoothAlwaysUsageDescription + Used to connect to Movesense devices + NSBluetoothPeripheralUsageDescription + Used to connect to Movesense devices + UIBackgroundModes + + bluetooth-central + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Movesense Flutter Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + movesense_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/movesense_flutter/example/ios/Runner/Runner-Bridging-Header.h b/packages/movesense_flutter/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000..308a2a560 --- /dev/null +++ b/packages/movesense_flutter/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/movesense_flutter/example/ios/RunnerTests/RunnerTests.swift b/packages/movesense_flutter/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..86a7c3b1b --- /dev/null +++ b/packages/movesense_flutter/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/movesense_flutter/example/lib/main.dart b/packages/movesense_flutter/example/lib/main.dart new file mode 100644 index 000000000..4c88563fa --- /dev/null +++ b/packages/movesense_flutter/example/lib/main.dart @@ -0,0 +1,121 @@ +// ignore_for_file: depend_on_referenced_packages + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:movesense_flutter/movesense_flutter.dart'; +import 'package:permission_handler/permission_handler.dart'; + +void main() => runApp(const MovesenseApp()); + +class MovesenseApp extends StatelessWidget { + const MovesenseApp({super.key}); + + @override + Widget build(BuildContext context) => + const MaterialApp(home: MovesenseHomePage()); +} + +class MovesenseHomePage extends StatefulWidget { + const MovesenseHomePage({super.key}); + + @override + State createState() => MovesenseHomePageState(); +} + +class MovesenseHomePageState extends State { + // My device address: 0C:8C:DC:1B:23:BF, serial 233830000816 + // Replace with your Movesense device address. + final MovesenseDevice device = MovesenseDevice(address: '0C:8C:DC:1B:23:BF'); + bool isSampling = false; + StreamSubscription? hrSubscription; + StreamSubscription? stateSubscription; + + @override + void initState() { + super.initState(); + requestPermissions(); + } + + Future requestPermissions() async { + if (!mounted) return; + + await [ + Permission.bluetooth, + Permission.bluetoothScan, + Permission.bluetoothConnect, + ].request(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Movensense HR Monitor')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamBuilder( + stream: device.statusEvents, + builder: (context, snapshot) => + Text('Movesense [${device.address}] - ${device.status.name}'), + ), + const Text('Your heart rate is:'), + StreamBuilder( + stream: device.hr, + builder: (context, snapshot) => Text( + snapshot.hasData ? '${snapshot.data}' : '...', + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => onButtonPressed(), + child: (!(device.isConnected)) + ? const Icon(Icons.refresh) + : (!isSampling) + ? const Icon(Icons.play_arrow) + : const Icon(Icons.stop), + ), + ); + } + + /// Handle button press to connect to device and start/stop sampling. + void onButtonPressed() { + setState(() { + if (!device.isConnected) { + // if not connected, connect to the device + device.connect(); + } else { + // when connected, first get device info and battery status + device.getDeviceInfo().then( + (info) => debugPrint('>> Product name: ${info?.productName}'), + ); + + device.getBatteryStatus().then( + (battery) => debugPrint('>> Battery level: ${battery.name}'), + ); + // then start/stop sampling + if (!isSampling) { + // Example of subscribing to heart rate data + hrSubscription = device.hr.listen((hr) { + debugPrint('>> Heart Rate: $hr'); + }); + + // Example of subscribing to tap state changes + stateSubscription = device + .getStateEvents(SystemStateComponent.tap) + .listen((state) { + debugPrint('>> State: ${state.toString()}'); + }); + isSampling = true; + } else { + hrSubscription?.cancel(); + stateSubscription?.cancel(); + isSampling = false; + } + } + }); + } +} diff --git a/packages/movesense_flutter/example/pubspec.yaml b/packages/movesense_flutter/example/pubspec.yaml new file mode 100644 index 000000000..2706e5d71 --- /dev/null +++ b/packages/movesense_flutter/example/pubspec.yaml @@ -0,0 +1,30 @@ +name: movesense_flutter_example +description: "An example of how to use the movesense_flutter plugin." +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: ">=3.8.0 <4.0.0" + flutter: ">=3.19.0" + +dependencies: + flutter: + sdk: flutter + + movesense_flutter: + path: ../../movesense_flutter/ + + cupertino_icons: ^1.0.2 + permission_handler: ^11.0.0 + polar: ^7.0.0 + sembast: ^3.5.0 + path_provider: ^2.1.0 + mdsflutter: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: any + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/packages/movesense_flutter/lib/device_info.json b/packages/movesense_flutter/lib/device_info.json new file mode 100644 index 000000000..b3d520bae --- /dev/null +++ b/packages/movesense_flutter/lib/device_info.json @@ -0,0 +1,26 @@ +{ + "Content": { + "manufacturerName": "Movesense Ltd", + "brandName": null, + "productName": "Movesense", + "variant": "Avocado", + "design": "black", + "hwCompatibilityId": "C", + "serial": "233830000816", + "pcbaSerial": "80413336500828200000100022666H4233", + "sw": "2.1.5", + "hw": "H4", + "additionalVersionInfo": null, + "addressInfo": [ + { + "name": "BLE", + "address": "0C-8C-DC-1B-23-BF" + }, + { + "name": "DFU-BLE", + "address": "E9-AC-4C-25-9A-65" + } + ], + "apiLevel": "1" + } +} \ No newline at end of file diff --git a/packages/movesense_flutter/lib/movesense_data.dart b/packages/movesense_flutter/lib/movesense_data.dart new file mode 100644 index 000000000..cf7b01eed --- /dev/null +++ b/packages/movesense_flutter/lib/movesense_data.dart @@ -0,0 +1,349 @@ +part of 'movesense_flutter.dart'; + +/// Information about used device and the platform which is running on it. +/// Contains knowledge about the hardware version, serial number, app name or +/// modules state. +/// +/// See https://www.movesense.com/docs/esw/api_reference/#info +class MovesenseDeviceInfo { + final String manufacturerName; + final String? brandName; + final String productName; + final String variant; + final String design; + final String hwCompatibilityId; + final String serial; + final String pcbaSerial; + final String sw; + final String hw; + final String? additionalVersionInfo; + final List addressInfo; + final String apiLevel; + + const MovesenseDeviceInfo( + this.manufacturerName, + this.brandName, + this.productName, + this.variant, + this.design, + this.hwCompatibilityId, + this.serial, + this.pcbaSerial, + this.sw, + this.hw, + this.additionalVersionInfo, + this.addressInfo, + this.apiLevel, + ); + + /// Build from the raw JSON string returned by the device. + factory MovesenseDeviceInfo.fromMovesenseData(String data) { + final dynamic decoded = json.decode(data); + if (decoded is! Map) { + throw const FormatException('Device info must be a JSON object'); + } + final dynamic content = decoded['Content']; + if (content is! Map) { + throw const FormatException('Device info JSON missing Content object'); + } + return MovesenseDeviceInfo.fromMap(content); + } + + /// Build from a JSON map. + factory MovesenseDeviceInfo.fromMap(Map map) { + final dynamic addresses = map['addressInfo']; + if (addresses is! List) { + throw const FormatException('Device info missing addressInfo list'); + } + + return MovesenseDeviceInfo( + map['manufacturerName'] as String? ?? '', + map['brandName'] as String?, + map['productName'] as String? ?? '', + map['variant'] as String? ?? '', + map['design'] as String? ?? '', + map['hwCompatibilityId'] as String? ?? '', + map['serial'] as String? ?? '', + map['pcbaSerial'] as String? ?? '', + map['sw'] as String? ?? '', + map['hw'] as String? ?? '', + map['additionalVersionInfo'] as String?, + addresses + .map( + (entry) => MovesenseAddressInfo.fromMap( + entry is Map ? entry : {}, + ), + ) + .toList(growable: false), + map['apiLevel'] as String? ?? '', + ); + } + + Map toMap() => { + 'manufacturerName': manufacturerName, + 'brandName': brandName, + 'productName': productName, + 'variant': variant, + 'design': design, + 'hwCompatibilityId': hwCompatibilityId, + 'serial': serial, + 'pcbaSerial': pcbaSerial, + 'sw': sw, + 'hw': hw, + 'additionalVersionInfo': additionalVersionInfo, + 'addressInfo': addressInfo.map((a) => a.toMap()).toList(growable: false), + 'apiLevel': apiLevel, + }; + + /// Serialize to JSON string with the original Content wrapper. + String toJsonString() => json.encode({'Content': toMap()}); +} + +/// Address info entry in the Movesense device info. +class MovesenseAddressInfo { + final String name; + final String address; + + const MovesenseAddressInfo(this.name, this.address); + + factory MovesenseAddressInfo.fromMap(Map map) { + return MovesenseAddressInfo( + map['name'] as String? ?? '', + map['address'] as String? ?? '', + ); + } + + Map toMap() => { + 'name': name, + 'address': address, + }; +} + +/// Base class for Movesense data samples. +abstract class MovesenseData { + /// The device's internal timestamp of this data sample. + /// According to the Movesense documentation, this is in microseconds + /// since 00:00 1st Jan 1970 UTC. + /// See https://www.movesense.com/docs/esw/api_reference/#time + int timestamp; + MovesenseData(this.timestamp); +} + +/// Different states of the Movensense device. +/// +/// See https://www.movesense.com/docs/esw/api_reference/#systemstates for an +/// overview. +enum MovesenseDeviceState { + /// Unknown state. + unknown, + + /// Device is moving. + moving, + + /// Device is not moving. + notMoving, + + /// Device connected to gear (e.g., strap). + connected, + + /// Device disconnected to gear. + disconnected, + + /// Device tapped once. + tap, + + /// Device double tapped. + doubleTap, + + /// Device is under acceleration. + acceleration, + + /// Device is in free fall (no gravity). + freeFall, +} + +/// State of a Movesense system component. +class MovesenseState extends MovesenseData { + /// The state of the component. + final MovesenseDeviceState state; + + MovesenseState(super.timestamp, this.state); + + /// Get state object from the data returned directly from the device. + /// + /// Handles both GET and SUBSCRIBE data formats. + /// + /// GET response is in a String format `{"Content": }`. + /// SUBSCRIBE response is in a JSON map format `Body: {Timestamp: 32591214, StateId: 0, NewState: 1}`. + /// + /// Note that we do not get the timestamp in the GET response, so we set it to + /// the current time (in microseconds) on the phone. But note, that this may + /// be quite different from the device's internal timestamp. + /// + /// See https://www.movesense.com/docs/esw/api_reference/#systemstates for details. + /// Note, however, that the json listed on the official Movesense API is wrong! + factory MovesenseState.fromMovesenseData( + SystemStateComponent component, + dynamic data, + ) { + MovesenseDeviceState state = MovesenseDeviceState.unknown; + num newState = 0; + num timestamp = DateTime.now().microsecondsSinceEpoch; + + if (data is String) { + data = json.decode(data); + newState = data["Content"] as num; + } else if (data is Map) { + timestamp = data["Body"]["Timestamp"] as num; + newState = data["Body"]["NewState"] as num; + } + + switch (component) { + case SystemStateComponent.movement: // movement + state = (newState == 0) + ? MovesenseDeviceState.notMoving + : MovesenseDeviceState.moving; + break; + case SystemStateComponent.connectors: // connectors + state = (newState == 0) + ? MovesenseDeviceState.disconnected + : MovesenseDeviceState.connected; + break; + case SystemStateComponent.doubleTap: // double-tap + if (newState == 1) { + state = MovesenseDeviceState.doubleTap; + } + break; + case SystemStateComponent.tap: // tap + if (newState == 1) { + state = MovesenseDeviceState.tap; + } + break; + case SystemStateComponent.freeFall: // free-fall + state = (newState == 0) + ? MovesenseDeviceState.acceleration + : MovesenseDeviceState.freeFall; + break; + + default: + } + + return MovesenseState(timestamp.toInt(), state); + } + + @override + String toString() => state.name; +} + +/// Electrocardiogram (ECG) reading. +/// +/// See https://www.movesense.com/docs/esw/api_reference/#measecg +class MovesenseECG extends MovesenseData { + /// The ECG samples. + final List samples; + + MovesenseECG(super.timestamp, this.samples); + + factory MovesenseECG.fromMovesenseData(dynamic data) { + List samples = (data["Body"]["Samples"] as List) + .map((e) => e as int) + .toList(); + num timestamp = data["Body"]["Timestamp"] as num; + return MovesenseECG(timestamp.toInt(), samples); + } +} + +/// The device's internal temperature. Returned values are in units +/// of Kelvins. +/// +/// See https://www.movesense.com/docs/esw/api_reference/#meastemperature +class MovesenseTemperature extends MovesenseData { + /// The device's internal temperature in units of Kelvins (K). + final int measurement; + + MovesenseTemperature(super.timestamp, this.measurement); + + factory MovesenseTemperature.fromMovesenseData(dynamic data) { + num timestamp = data["Body"]["Timestamp"] as num; + num measurement = data["Body"]["Measurement"] as num; + + return MovesenseTemperature(timestamp.toInt(), measurement.toInt()); + } +} + +/// Provides a synchronized access to combined accelerometer, gyroscope and +/// magnetometer data samples for easier processing e.g. for AHRS algorithms. +/// +/// See https://www.movesense.com/docs/esw/api_reference/#measimu +class MovesenseIMU extends MovesenseData { + final List accelerometer; + final List gyroscope; + final List magnetometer; + + MovesenseIMU( + super.timestamp, + this.accelerometer, + this.gyroscope, + this.magnetometer, + ); + + factory MovesenseIMU.fromMovesenseData(dynamic data) { + num timestamp = data["Body"]["Timestamp"] as num; + + List acc = + (data["Body"]["ArrayAcc"] as List) + .map( + (sample) => MovesenseAccelerometerSample( + sample['x'] as num, + sample['y'] as num, + sample['z'] as num, + ), + ) + .toList(); + + List gyro = + (data["Body"]["ArrayAcc"] as List) + .map( + (sample) => MovesenseGyroscopeSample( + sample['x'] as num, + sample['y'] as num, + sample['z'] as num, + ), + ) + .toList(); + + List mag = + (data["Body"]["ArrayAcc"] as List) + .map( + (sample) => MovesenseMagnetometerSample( + sample['x'] as num, + sample['y'] as num, + sample['z'] as num, + ), + ) + .toList(); + + return MovesenseIMU(timestamp.toInt(), acc, gyro, mag); + } +} + +/// Movesense accelerometer sample. +/// X,Y,Z value in milli-G (including gravity). +class MovesenseAccelerometerSample { + final num x, y, z; + MovesenseAccelerometerSample(this.x, this.y, this.z); +} + +/// Movesense gyroscope sample. +/// X, Y, Z axis value in deg/sec. +class MovesenseGyroscopeSample { + final num x, y, z; + MovesenseGyroscopeSample(this.x, this.y, this.z); +} + +/// Movesense magnetometer sample. +/// X, Y, Z axis value in Gauss. +class MovesenseMagnetometerSample { + final num x, y, z; + MovesenseMagnetometerSample(this.x, this.y, this.z); +} diff --git a/packages/movesense_flutter/lib/movesense_device.dart b/packages/movesense_flutter/lib/movesense_device.dart new file mode 100644 index 000000000..c417eb733 --- /dev/null +++ b/packages/movesense_flutter/lib/movesense_device.dart @@ -0,0 +1,285 @@ +part of 'movesense_flutter.dart'; + +/// A class representing a Movesense device. +class MovesenseDevice { + MovesenseDeviceInfo? _deviceInfo; + DeviceBatteryLevel _batteryLevel = DeviceBatteryLevel.unknown; + + /// The Movesense device address. + /// + /// Address is Bluetooth MAC address for Android devices and UUID for iOS devices. + String? address; + + /// The Movesense device serial number. + String? serial; + + /// The Movesense device name. + String? name; + + /// The type of Movesense device, if known. + MovesenseDeviceType deviceType; + + /// The current connection status of the device. + DeviceConnectionStatus get status => _status; + DeviceConnectionStatus _status = DeviceConnectionStatus.disconnected; + set status(DeviceConnectionStatus newStatus) { + if (_status != newStatus) { + debugPrint("$runtimeType - Setting device '$address' as $newStatus"); + _status = newStatus; + _statusController.add(_status); + } + } + + /// A stream of connection status events of the device. + Stream get statusEvents => + _statusController.stream.asBroadcastStream(); + final _statusController = + StreamController.broadcast(); + + MovesenseDevice({ + this.address, + this.serial, + this.name, + this.deviceType = MovesenseDeviceType.unknown, + }); + + /// Returns true if the device has a valid address and can be connected to. + bool canConnect() => address != null; + + /// Returns true if the device is currently connected. + bool get isConnected => serial != null; + // bool get isConnected => status == DeviceConnectionStatus.connected; + + /// The device info for the connected Movesense device. + /// Only available after device is connected. + /// See https://www.movesense.com/docs/esw/api_reference/#info + MovesenseDeviceInfo? get deviceInfo => _deviceInfo; + + /// The battery level of the device, if known. + /// Battery level is updated every 10 minutes when connected. + DeviceBatteryLevel? get batteryLevel => _batteryLevel; + + /// Connect to the Movesense device using the [address] specified. + /// If the address is not set, an exception is thrown. + Future connect() async { + if (!canConnect()) { + throw Exception('Cannot connect to device - address is not set.'); + } + + status = DeviceConnectionStatus.connecting; + + Mds.connect( + address!, + // onConnected + (serial) async { + this.serial = serial; + await getDeviceInfo(); // fetch device info upon connection + await getBatteryStatus(); // fetch battery status upon connection + + status = DeviceConnectionStatus.connected; + }, + // onDisconnected + () => status = DeviceConnectionStatus.disconnected, + // onError + (error) => status = DeviceConnectionStatus.error, + // onBleConnected + // - for now we ignore this callback + (_) {}, + ); + } + + /// Disconnect from the Movesense device. + Future disconnect() async { + if (!isConnected) return; + Mds.disconnect(address!); + } + + /// Get the detailed info about this Movesense device. + /// See https://www.movesense.com/docs/esw/api_reference/#info + /// Returns [MovesenseDeviceInfo] on success, + /// or null if the device is not connected. + Future getDeviceInfo() async { + // fast out if not connected + if (!isConnected) return null; + + var completer = Completer(); + + Mds.get( + Mds.createRequestUri(serial!, "/Info"), + "{}", + // onSuccess + ((info, statusCode) { + _deviceInfo = MovesenseDeviceInfo.fromMovesenseData(info); + + // Try to figure out the type of device based on the "hw" property + // H3 is "HR+", H4 is "HR2", A1 is "MD" + deviceType = switch (_deviceInfo?.hw.toUpperCase()) { + 'A1' => MovesenseDeviceType.MD, + 'H3' => MovesenseDeviceType.HR_PLUS, + 'H4' => MovesenseDeviceType.HR2, + _ => MovesenseDeviceType.unknown, + }; + completer.complete(_deviceInfo); + }), + // onError + (error, statusCode) => + completer.completeError('$runtimeType - error: $error'), + ); + + return completer.future; + } + + /// Get battery status from the device. + Future getBatteryStatus() { + // fast out if not connected + if (!isConnected) return Future.value(DeviceBatteryLevel.unknown); + + var completer = Completer(); + _batteryLevel = DeviceBatteryLevel.unknown; + + Mds.get( + Mds.createRequestUri(serial!, "/System/States/1"), + "{}", + ((data, statusCode) { + final dataContent = json.decode(data); + num batteryState = dataContent["Content"] as num; + // Movesense reports "OK" (0) or "LOW" (1) battery state + _batteryLevel = batteryState == 0 + ? DeviceBatteryLevel.ok + : DeviceBatteryLevel.low; + completer.complete(_batteryLevel); + }), + (error, statusCode) { + _batteryLevel = DeviceBatteryLevel.unknown; + completer.complete(_batteryLevel); + }, + ); + return completer.future; + } + + /// Get the state of the device. + /// See https://www.movesense.com/docs/esw/api_reference/#systemstates + /// + /// Returns [MovesenseState] on success, + /// or null if the device is not connected. + Future getState(SystemStateComponent state) { + // fast out if not connected + if (!isConnected) return Future.value(null); + + var completer = Completer(); + + Mds.get( + Mds.createRequestUri(serial!, "/System/States/${state.index}"), + "{}", + ((data, _) => + completer.complete(MovesenseState.fromMovesenseData(state, data))), + (error, _) => completer.complete(null), + ); + return completer.future; + } + + /// A stream of heart rate (HR) measurements from the Movesense device. + /// Only available when the device is connected. + Stream get hr => !isConnected + ? Stream.empty() + : MdsAsync.subscribe(Mds.createSubscriptionUri(serial!, "/Meas/HR"), "{}") + .map((data) => (data["Body"]["average"] as num).toInt()) + .asBroadcastStream(); + + /// A stream of ECG measurements from the Movesense device collected at 125 Hz. + /// Only available when the device is connected. + Stream get ecg => !isConnected + ? Stream.empty() + : MdsAsync.subscribe( + Mds.createSubscriptionUri(serial!, "/Meas/ECG/125"), + "{}", + ) + .map((data) => MovesenseECG.fromMovesenseData(data)) + .asBroadcastStream(); + + /// A stream of IMU measurements from the Movesense device collected at 13 Hz (lowest). + /// Only available when the device is connected. + Stream get imu => !isConnected + ? Stream.empty() + : MdsAsync.subscribe( + Mds.createSubscriptionUri(serial!, "/Meas/IMU9/13"), + "{}", + ) + .map((data) => MovesenseIMU.fromMovesenseData(data)) + .asBroadcastStream(); + + /// A stream of temperature measurements from the Movesense device. + /// Only available when the device is connected. + Stream get temperature => !isConnected + ? Stream.empty() + : MdsAsync.subscribe( + Mds.createSubscriptionUri(serial!, "/Meas/Temp"), + "{}", + ) + .map((data) => MovesenseTemperature.fromMovesenseData(data)) + .asBroadcastStream(); + + /// Get a stream of state events for a specific [component] from the Movesense device. + /// The types of state changes available are listed in [SystemStateComponent]. + /// See https://www.movesense.com/docs/esw/api_reference/#systemstates + /// + /// **NOTE**, that currently there is a limitation to the Movesense API and you + /// can only subscribe to a single type of event at a time. + /// See issue [#15](https://github.com/petri-lipponen-movesense/mdsflutter/issues/15). + /// + /// Also note that not all state changes are supported on all types of devices. + /// For example, it seems like only the 'connectors' and 'tap' states are supported + /// on the Movesense MD and HR2 devices. + /// + /// The returned stream emits [MovesenseState] objects representing + /// the state change events. + /// + /// Only available when the device is connected. + Stream getStateEvents(SystemStateComponent component) => + !isConnected + ? Stream.empty() + : MdsAsync.subscribe( + Mds.createSubscriptionUri( + serial!, + "/System/States/${component.index}", + ), + "{}", + ) + .map((data) => MovesenseState.fromMovesenseData(component, data)) + .asBroadcastStream(); +} + +/// Enumeration of supported Movesense devices. +enum MovesenseDeviceType { + /// Unknown Movesense type + unknown, + + /// Movesense Medical sensor + MD, + + /// Movesense ACTIVE HR+ + HR_PLUS, + + /// Movesense ACTIVE HR2 sensor + HR2, + + /// Movesense FLASH sensor + FLASH, +} + +/// Enumeration of the connection status of the Movesense device. +enum DeviceConnectionStatus { disconnected, connecting, connected, error } + +/// Enumeration of the battery level of the Movesense device. +enum DeviceBatteryLevel { low, ok, unknown } + +/// Enumeration of the type of system state components available on the Movesense device. +/// See https://www.movesense.com/docs/esw/api_reference/#systemstates +enum SystemStateComponent { + movement, + battery, + connectors, + doubleTap, + tap, + freeFall, +} diff --git a/packages/movesense_flutter/lib/movesense_flutter.dart b/packages/movesense_flutter/lib/movesense_flutter.dart new file mode 100644 index 000000000..d960abb4c --- /dev/null +++ b/packages/movesense_flutter/lib/movesense_flutter.dart @@ -0,0 +1,36 @@ +library; + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:mdsflutter/Mds.dart'; + +part 'movesense_device.dart'; +part 'movesense_data.dart'; + +class Movesense { + static final Movesense _instance = Movesense._(); + Movesense._(); + final StreamController _scanController = + StreamController.broadcast(); + + /// Returns the singleton instance of [Movesense]. + factory Movesense() => _instance; + + /// A stream of discovered Movesense devices during scanning. + Stream get devices => + _scanController.stream.asBroadcastStream(); + + /// Scan for available Movesense devices. + /// Found devices are emitted on the [devices] stream. + /// Call [stopScan] to stop scanning. + void scan() { + Mds.startScan( + (name, address) => + _scanController.add(MovesenseDevice(address: address, name: name)), + ); + } + + /// Stop scanning for Movesense devices. + void stopScan() => Mds.stopScan(); +} diff --git a/packages/movesense_flutter/pubspec.yaml b/packages/movesense_flutter/pubspec.yaml new file mode 100644 index 000000000..eabe3454d --- /dev/null +++ b/packages/movesense_flutter/pubspec.yaml @@ -0,0 +1,26 @@ +name: movesense_flutter +description: "A Flutter package for Movesense sensor integration. Wraps the MDS plugin and provides easy access to Movesense device features and data streams." +version: 1.0.0 +homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/movesense_flutter + +environment: + sdk: ">=3.8.0 <4.0.0" + flutter: ">=3.19.0" + +# This package only supports iOS and Android +platforms: + android: + ios: + +dependencies: + flutter: + sdk: flutter + mdsflutter: ^2.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: any + + +flutter: diff --git a/packages/movesense_flutter/test/movesense_flutter_test.dart b/packages/movesense_flutter/test/movesense_flutter_test.dart new file mode 100644 index 000000000..aa4b0fb31 --- /dev/null +++ b/packages/movesense_flutter/test/movesense_flutter_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:movesense_flutter/movesense_flutter.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +}