├── _config.yml ├── .gitignore ├── IoTWatch ├── monkey.jungle ├── resources │ ├── strings │ │ └── strings.xml │ ├── drawables │ │ ├── D3.240.png │ │ ├── D3.40.png │ │ └── drawables.xml │ └── layouts │ │ └── layout.xml ├── .project ├── source │ ├── IoTWatchApp.mc │ └── IoTWatchView.mc └── manifest.xml ├── IoTWatch2 ├── monkey.jungle ├── resources │ ├── strings │ │ └── strings.xml │ ├── drawables │ │ ├── D3.40.png │ │ ├── D3.240.png │ │ └── drawables.xml │ └── layouts │ │ └── layout.xml ├── .project ├── source │ ├── IoTWatchApp.mc │ └── IoTWatchView.mc └── manifest.xml ├── IoTWatchEH ├── monkey.jungle ├── bin │ ├── IoTWatchEH.prg │ ├── IoTWatchEH-settings.json │ └── IoTWatchEH.prg.debug.xml ├── resources │ ├── drawables │ │ ├── D3.240.png │ │ ├── D3.36.png │ │ ├── D3.40.png │ │ └── drawables.xml │ ├── strings │ │ └── strings.xml │ ├── layouts │ │ └── layout.xml │ └── properties.xml ├── .project ├── source │ ├── IoTWatchApp.mc │ └── IoTWatchView.mc └── manifest.xml ├── images ├── 15.SAS.png ├── 10.code.png ├── 18.code.png ├── 13.Firewall.png ├── 20.NewInput.png ├── 21.Outputs.png ├── 4.ParseJSON.png ├── MLADBLaunch.png ├── MLSplitData.png ├── 1.NewLogicApp.png ├── 11.container.png ├── 14.createHub.png ├── 2.HTTPRequest.png ├── 23.dashboard.png ├── 6.NewDataset.png ├── MLImportData.png ├── environment1.png ├── environment2.png ├── mlExperiment.png ├── 19.StreamInput.png ├── 22.PowerBIOutput.png ├── MLArchitecture.png ├── MLNEWADBCluster.png ├── MLNEWADBCluster2.png ├── MLNewDatabricks.png ├── MLNewNotebook1.png ├── MLNewNotebook2.png ├── MLNewNotebook3.png ├── MLSelectColumns.png ├── 12.CreateEventHub.png ├── 16.tokenGenerator.png ├── 17.StreamAnalytics.png ├── 7.AddRowsToDataset.png ├── MLADBAttachCluster.png ├── 8.CustomStreamingData.png └── 5.CreateStreamingDataset.png ├── README.md ├── MLModelApplication.md ├── MLModelTraining.md └── IoTWatchInstructions.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | IoTWatchEH/source/IoTWatchView.mc 3 | -------------------------------------------------------------------------------- /IoTWatch/monkey.jungle: -------------------------------------------------------------------------------- 1 | project.manifest = manifest.xml 2 | -------------------------------------------------------------------------------- /IoTWatch2/monkey.jungle: -------------------------------------------------------------------------------- 1 | project.manifest = manifest.xml 2 | -------------------------------------------------------------------------------- /IoTWatchEH/monkey.jungle: -------------------------------------------------------------------------------- 1 | project.manifest = manifest.xml 2 | -------------------------------------------------------------------------------- /images/15.SAS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/15.SAS.png -------------------------------------------------------------------------------- /images/10.code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/10.code.png -------------------------------------------------------------------------------- /images/18.code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/18.code.png -------------------------------------------------------------------------------- /images/13.Firewall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/13.Firewall.png -------------------------------------------------------------------------------- /images/20.NewInput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/20.NewInput.png -------------------------------------------------------------------------------- /images/21.Outputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/21.Outputs.png -------------------------------------------------------------------------------- /images/4.ParseJSON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/4.ParseJSON.png -------------------------------------------------------------------------------- /images/MLADBLaunch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLADBLaunch.png -------------------------------------------------------------------------------- /images/MLSplitData.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLSplitData.png -------------------------------------------------------------------------------- /IoTWatch/resources/strings/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | IoTWatch 3 | 4 | -------------------------------------------------------------------------------- /IoTWatch2/resources/strings/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | IoTWatch2 3 | 4 | -------------------------------------------------------------------------------- /images/1.NewLogicApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/1.NewLogicApp.png -------------------------------------------------------------------------------- /images/11.container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/11.container.png -------------------------------------------------------------------------------- /images/14.createHub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/14.createHub.png -------------------------------------------------------------------------------- /images/2.HTTPRequest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/2.HTTPRequest.png -------------------------------------------------------------------------------- /images/23.dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/23.dashboard.png -------------------------------------------------------------------------------- /images/6.NewDataset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/6.NewDataset.png -------------------------------------------------------------------------------- /images/MLImportData.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLImportData.png -------------------------------------------------------------------------------- /images/environment1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/environment1.png -------------------------------------------------------------------------------- /images/environment2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/environment2.png -------------------------------------------------------------------------------- /images/mlExperiment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/mlExperiment.png -------------------------------------------------------------------------------- /images/19.StreamInput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/19.StreamInput.png -------------------------------------------------------------------------------- /images/22.PowerBIOutput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/22.PowerBIOutput.png -------------------------------------------------------------------------------- /images/MLArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLArchitecture.png -------------------------------------------------------------------------------- /images/MLNEWADBCluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLNEWADBCluster.png -------------------------------------------------------------------------------- /images/MLNEWADBCluster2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLNEWADBCluster2.png -------------------------------------------------------------------------------- /images/MLNewDatabricks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLNewDatabricks.png -------------------------------------------------------------------------------- /images/MLNewNotebook1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLNewNotebook1.png -------------------------------------------------------------------------------- /images/MLNewNotebook2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLNewNotebook2.png -------------------------------------------------------------------------------- /images/MLNewNotebook3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLNewNotebook3.png -------------------------------------------------------------------------------- /images/MLSelectColumns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLSelectColumns.png -------------------------------------------------------------------------------- /IoTWatchEH/bin/IoTWatchEH.prg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/IoTWatchEH/bin/IoTWatchEH.prg -------------------------------------------------------------------------------- /images/12.CreateEventHub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/12.CreateEventHub.png -------------------------------------------------------------------------------- /images/16.tokenGenerator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/16.tokenGenerator.png -------------------------------------------------------------------------------- /images/17.StreamAnalytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/17.StreamAnalytics.png -------------------------------------------------------------------------------- /images/7.AddRowsToDataset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/7.AddRowsToDataset.png -------------------------------------------------------------------------------- /images/MLADBAttachCluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/MLADBAttachCluster.png -------------------------------------------------------------------------------- /images/8.CustomStreamingData.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/8.CustomStreamingData.png -------------------------------------------------------------------------------- /images/5.CreateStreamingDataset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/images/5.CreateStreamingDataset.png -------------------------------------------------------------------------------- /IoTWatch/resources/drawables/D3.240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/IoTWatch/resources/drawables/D3.240.png -------------------------------------------------------------------------------- /IoTWatch/resources/drawables/D3.40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/IoTWatch/resources/drawables/D3.40.png -------------------------------------------------------------------------------- /IoTWatch2/resources/drawables/D3.40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/IoTWatch2/resources/drawables/D3.40.png -------------------------------------------------------------------------------- /IoTWatch2/resources/drawables/D3.240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/IoTWatch2/resources/drawables/D3.240.png -------------------------------------------------------------------------------- /IoTWatchEH/resources/drawables/D3.240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/IoTWatchEH/resources/drawables/D3.240.png -------------------------------------------------------------------------------- /IoTWatchEH/resources/drawables/D3.36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/IoTWatchEH/resources/drawables/D3.36.png -------------------------------------------------------------------------------- /IoTWatchEH/resources/drawables/D3.40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedoesdemos/ConnectIQ-Watch-IoT/HEAD/IoTWatchEH/resources/drawables/D3.40.png -------------------------------------------------------------------------------- /IoTWatch/resources/layouts/layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /IoTWatch2/resources/layouts/layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /IoTWatch/resources/drawables/drawables.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /IoTWatch2/resources/drawables/drawables.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /IoTWatchEH/resources/strings/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | IoTWatchEH 3 | Status 4 | xAccel 5 | yAccel 6 | HR 7 | 8 | -------------------------------------------------------------------------------- /IoTWatch/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | IoTWatch 4 | 5 | 6 | 7 | 8 | 9 | connectiq.builder 10 | 11 | 12 | 13 | 14 | 15 | connectiq.projectNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /IoTWatch2/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | IoTWatch2 4 | 5 | 6 | 7 | 8 | 9 | connectiq.builder 10 | 11 | 12 | 13 | 14 | 15 | connectiq.projectNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /IoTWatchEH/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | IoTWatchEH 4 | 5 | 6 | 7 | 8 | 9 | connectiq.builder 10 | 11 | 12 | 13 | 14 | 15 | connectiq.projectNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /IoTWatch/source/IoTWatchApp.mc: -------------------------------------------------------------------------------- 1 | using Toybox.Application; 2 | using Toybox.WatchUi; 3 | 4 | class IoTWatchApp extends Application.AppBase { 5 | 6 | function initialize() { 7 | AppBase.initialize(); 8 | } 9 | 10 | // onStart() is called on application start up 11 | function onStart(state) { 12 | } 13 | 14 | // onStop() is called when your application is exiting 15 | function onStop(state) { 16 | } 17 | 18 | // Return the initial view of your application here 19 | function getInitialView() { 20 | return [ new IoTWatchView()]; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /IoTWatch2/source/IoTWatchApp.mc: -------------------------------------------------------------------------------- 1 | using Toybox.Application; 2 | using Toybox.WatchUi; 3 | 4 | class IoTWatchApp extends Application.AppBase { 5 | 6 | function initialize() { 7 | AppBase.initialize(); 8 | } 9 | 10 | // onStart() is called on application start up 11 | function onStart(state) { 12 | } 13 | 14 | // onStop() is called when your application is exiting 15 | function onStop(state) { 16 | } 17 | 18 | // Return the initial view of your application here 19 | function getInitialView() { 20 | return [ new IoTWatchView()]; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /IoTWatchEH/resources/drawables/drawables.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConnectIQ-Watch-IoT 2 | This is a Connect IQ app for Garmin devices. It will send data in realtime to Azure via the Connect Mobile app. 3 | 4 | # IoTWatch 5 | This version uses a callback on the HR sensor and submits data whenever it changes. Only submits HR. 6 | 7 | # IoTWatch2 8 | This version uses a timer and submits all sensor data as a JSON each time the timer triggers. 9 | 10 | # Instructions 11 | Set up a Logic App with HTTP Response trigger as a POST method. Save the Logic App and copy the URL into the code before building and running. You may need to look at [developer.garmin.com](https://developer.garmin.com) to see how to build Connect IQ apps, as well as how to set up the required tooling such as Eclipse and the SDK. -------------------------------------------------------------------------------- /IoTWatchEH/resources/layouts/layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /IoTWatch/manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | eng 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /IoTWatch2/manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | eng 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /IoTWatchEH/source/IoTWatchApp.mc: -------------------------------------------------------------------------------- 1 | using Toybox.Application; 2 | using Toybox.WatchUi; 3 | 4 | class IoTWatchApp extends Application.AppBase { 5 | 6 | var IoTView; 7 | 8 | function initialize() { 9 | AppBase.initialize(); 10 | } 11 | 12 | // onStart() is called on application start up 13 | function onStart(state) { 14 | Position.enableLocationEvents(Position.LOCATION_CONTINUOUS, method(:onPosition)); 15 | } 16 | 17 | // onStop() is called when your application is exiting 18 | function onStop(state) { 19 | Position.enableLocationEvents(Position.LOCATION_DISABLE, method(:onPosition)); 20 | } 21 | 22 | function onPosition(info) { 23 | IoTView.setPosition(info); 24 | } 25 | 26 | // Return the initial view of your application here 27 | function getInitialView() { 28 | IoTView = new IoTWatchView(); 29 | return [ IoTView ]; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /MLModelApplication.md: -------------------------------------------------------------------------------- 1 | # Heart Rate data anomoly detection Part 2 2 | **produced by Dave Lusty** 3 | 4 | # Introduction 5 | This guide details how to use the machine learning model we trained in the last part to test new readings coming from the watch. In this demo we'll set up the web service in Stream Analytics 6 | 7 | You can find videos of this demo and the rest in the series on Youtube at the following locations: 8 | [Part 1 - Intro](https://youtu.be/_39eKRNK3UU) 9 | [Part 2 - Initial Platform Build](https://youtu.be/9llyGjfKiLo) 10 | 11 | The architecture for this demo is shown below. 12 | ![MLArchitecture.png](images/MLArchitecture.png) 13 | 14 | ## Prerequisites 15 | You'll need to have completed the previous demo so you can generate some useful data to train the model with. You can find this at [Watch demo infrastructure](https://github.com/davedoesdemos/ConnectIQ-Watch-IoT/blob/master/IoTWatchInstructions.md) but if you don't have a device you can skip over to YouTube and see the demo videos instead. 16 | 17 | 18 | ## Stream Analytics 19 | 20 | ```SQL 21 | WITH bob AS ( 22 | SELECT EventProcessedUtcTime, EventEnqueuedUtcTime, heartRate, yAccel, xAccel, altitude, cadence, heading, xMag, yMag, zMag, power, pressure, speed, temp, latitude, longitude, test(heartRate, yAccel, xAccel, altitude, cadence, heading, xMag, yMag, zMag, power, pressure, speed, temp, latitude, longitude) as result 23 | FROM connectIQ 24 | ) 25 | 26 | SELECT EventProcessedUtcTime, EventEnqueuedUtcTime, heartRate, yAccel, xAccel, altitude, cadence, heading, xMag, yMag, zMag, power, pressure, speed, temp, latitude, longitude, cast(result.[Scored Labels] as int) AS scoredLabels 27 | INTO connectIQOut2 28 | FROM bob 29 | ``` -------------------------------------------------------------------------------- /IoTWatchEH/manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | eng 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /IoTWatch/source/IoTWatchView.mc: -------------------------------------------------------------------------------- 1 | using Toybox.WatchUi; 2 | using Toybox.Communications as Comm; 3 | using Toybox.Sensor; 4 | using Toybox.Graphics; 5 | using Toybox.System; 6 | using Toybox.Lang; 7 | using Toybox.Time.Gregorian; 8 | using Toybox.Sensor; 9 | using Toybox.Application; 10 | using Toybox.Position; 11 | 12 | class IoTWatchView extends WatchUi.View { 13 | var string_HR; 14 | 15 | function initialize() { 16 | View.initialize(); 17 | Sensor.setEnabledSensors( [Sensor.SENSOR_HEARTRATE,Sensor.SENSOR_TEMPERATURE] ); 18 | Sensor.enableSensorEvents( method(:onSensor) ); 19 | } 20 | 21 | function onSensor(sensor_info) { 22 | var HR = sensor_info.heartRate; 23 | //Fill in this variable with your REST API endpoint 24 | var url = ""; 25 | if( sensor_info.heartRate != null ) 26 | { 27 | string_HR = HR.toString() + "bpm"; 28 | var params = { 29 | "heartRate" => HR 30 | }; 31 | var headers = { 32 | "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON 33 | }; 34 | var options = { 35 | :headers => headers, 36 | :method => Communications.HTTP_REQUEST_METHOD_POST, 37 | :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON 38 | }; 39 | Communications.makeWebRequest(url, params, options, method(:onReceive)); 40 | } 41 | else 42 | { 43 | string_HR = "---bpm"; 44 | } 45 | } 46 | 47 | function onReceive(responseCode, data) { 48 | } 49 | 50 | // Load your resources here 51 | function onLayout(dc) { 52 | setLayout(Rez.Layouts.MainLayout(dc)); 53 | } 54 | 55 | function onShow() { 56 | } 57 | 58 | function onUpdate(dc) { 59 | View.onUpdate(dc); 60 | 61 | } 62 | 63 | function onHide() { 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /IoTWatchEH/resources/properties.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | yournamespace 5 | YourEventHub 6 | Your SAS Key Name 7 | Your SAS Key 8 | Your User ID 9 | 1000 10 | 11 | 12 | 13 | Your Namespace 14 | Your Event Hub 15 | SAS Key Name 16 | SAS Key 17 | User ID 18 | Timer Delay 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /IoTWatchEH/bin/IoTWatchEH-settings.json: -------------------------------------------------------------------------------- 1 | {"settings":[{"key":"eHNamespace","valueType":"string","defaultValue":"yournamespace","configTitle":"eHNamespace_title","configPrompt":null,"configHelpUrl":null,"configError":null,"configType":"alphaNumeric","configReadonly":false,"configRequired":false,"configOptions":null,"configMin":null,"configMax":null,"configMaxLength":null},{"key":"eHEventHub","valueType":"string","defaultValue":"YourEventHub","configTitle":"eHEventHub_title","configPrompt":null,"configHelpUrl":null,"configError":null,"configType":"alphaNumeric","configReadonly":false,"configRequired":false,"configOptions":null,"configMin":null,"configMax":null,"configMaxLength":null},{"key":"eHSASKeyName","valueType":"string","defaultValue":"Your SAS Key Name","configTitle":"eHSASKeyName_title","configPrompt":null,"configHelpUrl":null,"configError":null,"configType":"alphaNumeric","configReadonly":false,"configRequired":false,"configOptions":null,"configMin":null,"configMax":null,"configMaxLength":null},{"key":"eHSASKey","valueType":"string","defaultValue":"Your SAS Key","configTitle":"eHSASKey_title","configPrompt":null,"configHelpUrl":null,"configError":null,"configType":"alphaNumeric","configReadonly":false,"configRequired":false,"configOptions":null,"configMin":null,"configMax":null,"configMaxLength":null},{"key":"userID","valueType":"string","defaultValue":"Your User ID","configTitle":"userID_title","configPrompt":null,"configHelpUrl":null,"configError":null,"configType":"alphaNumeric","configReadonly":false,"configRequired":false,"configOptions":null,"configMin":null,"configMax":null,"configMaxLength":null},{"key":"timer","valueType":"number","defaultValue":1000,"configTitle":"timer_title","configPrompt":null,"configHelpUrl":null,"configError":null,"configType":"numeric","configReadonly":false,"configRequired":false,"configOptions":null,"configMin":null,"configMax":null,"configMaxLength":null}],"languages":{"valyrian":{"userID_title":"User ID","field1":"Status","eHNamespace_title":"Your Namespace","eHSASKeyName_title":"SAS Key Name","eHEventHub_title":"Your Event Hub","field3":"yAccel","field2":"xAccel","eHSASKey_title":"SAS Key","timer_title":"Timer Delay","AppName":"IoTWatchEH","field4":"HR"}}} -------------------------------------------------------------------------------- /IoTWatch2/source/IoTWatchView.mc: -------------------------------------------------------------------------------- 1 | using Toybox.WatchUi; 2 | using Toybox.Communications as Comm; 3 | using Toybox.Sensor; 4 | using Toybox.Graphics; 5 | using Toybox.System; 6 | using Toybox.Lang; 7 | using Toybox.Time.Gregorian; 8 | using Toybox.Sensor; 9 | using Toybox.Application; 10 | using Toybox.Position; 11 | using Toybox.Timer; 12 | 13 | class IoTWatchView extends WatchUi.View { 14 | var dataTimer = new Timer.Timer(); 15 | var string_HR; 16 | //Fill in this variable with your REST API endpoint 17 | var url = ""; 18 | //Fill in this variable with the time interval in ms that you want to use for submitting data. Lower values will cause more calls so will cost more! 19 | var timer = 5000; 20 | 21 | function initialize() { 22 | View.initialize(); 23 | // Set up a timer 24 | dataTimer.start(method(:timerCallback), timer, true); 25 | } 26 | 27 | function timerCallback() { 28 | var sensorInfo = Sensor.getInfo(); 29 | var positionInfo = Position.getInfo(); 30 | var xAccel = 0; 31 | var yAccel = 0; 32 | var hR = 0; 33 | var altitude = 0; 34 | var cadence = 0; 35 | var heading = 0; 36 | var xMag = 0; 37 | var yMag = 0; 38 | var zMag = 0; 39 | var power = 0; 40 | var pressure = 0; 41 | var speed = 0; 42 | var temp = 0; 43 | var latitude = 0; 44 | var longitude = 0; 45 | 46 | //Collect Data 47 | //Accelerometer 48 | if (sensorInfo has :accel && sensorInfo.accel != null) { 49 | var accel = sensorInfo.accel; 50 | xAccel = accel[0]; 51 | yAccel = accel[1]; 52 | } 53 | else { 54 | xAccel = 0; 55 | yAccel = 0; 56 | } 57 | //Heartrate 58 | if (sensorInfo has :heartRate && sensorInfo.heartRate != null) { 59 | hR = sensorInfo.heartRate; 60 | } 61 | else { 62 | hR = 0; 63 | } 64 | //Altitude 65 | if (sensorInfo has :altitude && sensorInfo.altitude != null) { 66 | altitude = sensorInfo.altitude; 67 | } 68 | else { 69 | altitude = 0; 70 | } 71 | //Cadence 72 | if (sensorInfo has :cadence && sensorInfo.cadence != null) { 73 | cadence = sensorInfo.cadence; 74 | } 75 | else { 76 | cadence = 0; 77 | } 78 | //heading 79 | if (sensorInfo has :heading && sensorInfo.heading != null) { 80 | heading = sensorInfo.heading; 81 | } 82 | else { 83 | heading = 0; 84 | } 85 | //magnetometer 86 | if (sensorInfo has :mag && sensorInfo.mag != null) { 87 | var mag = sensorInfo.mag; 88 | xMag = mag[0]; 89 | yMag = mag[1]; 90 | zMag = mag[2]; 91 | } 92 | else { 93 | xMag = 0; 94 | yMag = 1; 95 | zMag = 2; 96 | } 97 | //Power 98 | if (sensorInfo has :power && sensorInfo.power != null) { 99 | power = sensorInfo.power; 100 | } 101 | else { 102 | power = 0; 103 | } 104 | //Pressure 105 | if (sensorInfo has :pressure && sensorInfo.pressure != null) { 106 | pressure = sensorInfo.pressure; 107 | } 108 | else { 109 | pressure = 0; 110 | } 111 | //Speed 112 | if (sensorInfo has :speed && sensorInfo.speed != null) { 113 | speed = sensorInfo.speed; 114 | } 115 | else { 116 | speed = 0; 117 | } 118 | //Temperature 119 | if (sensorInfo has :temp && sensorInfo.temp != null) { 120 | temp = sensorInfo.temp; 121 | } 122 | else { 123 | temp = 0; 124 | } 125 | //Position 126 | if (positionInfo has :position && positionInfo.position != null) { 127 | var location = positionInfo.position.toDegrees(); 128 | latitude = location[0]; 129 | longitude = location[1]; 130 | } 131 | else { 132 | latitude = 0; 133 | longitude = 0; 134 | 135 | } 136 | 137 | //Send the data to the REST API 138 | 139 | var params = { 140 | "heartRate" => hR.toNumber(), 141 | "xAccel" => xAccel.toNumber(), 142 | "yAccel" => yAccel.toNumber(), 143 | "altitude" => altitude.toFloat(), 144 | "cadence" => cadence.toNumber(), 145 | "heading" => heading.toFloat(), 146 | "xMag" => xMag.toNumber(), 147 | "yMag" => yMag.toNumber(), 148 | "zMag" => zMag.toNumber(), 149 | "power" => power.toNumber(), 150 | "pressure" => pressure.toFloat(), 151 | "speed" => speed.toFloat(), 152 | "temp" => temp.toNumber(), 153 | "latitude" => latitude.toFloat(), 154 | "longitude" => longitude.toFloat() 155 | }; 156 | var headers = { 157 | "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON 158 | }; 159 | var options = { 160 | :headers => headers, 161 | :method => Communications.HTTP_REQUEST_METHOD_POST, 162 | :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON 163 | }; 164 | Communications.makeWebRequest(url, params, options, method(:onReceive)); 165 | } 166 | 167 | function onReceive(responseCode, data) { 168 | } 169 | 170 | // Load your resources here 171 | function onLayout(dc) { 172 | setLayout(Rez.Layouts.MainLayout(dc)); 173 | } 174 | 175 | function onShow() { 176 | } 177 | 178 | // Update the view 179 | function onUpdate(dc) { 180 | View.onUpdate(dc); 181 | 182 | } 183 | 184 | function onHide() { 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /MLModelTraining.md: -------------------------------------------------------------------------------- 1 | # Heart Rate data anomoly detection 2 | **produced by Dave Lusty** 3 | 4 | # Introduction 5 | This guide details how to use the data generated by the Garmin watch application to train a machine learning model. In this demo we'll generate some resting HR data, then transform it with Data Factory and Azure Databricks. Finally we'll train a model using a "one class SVM" algorithm for anomoly detection before publishing that model as a web service. The one class SVM uses statistical analysis to detect outlier data, so when we generate new data with unusual values (such as during exercise) it will detect an anomoly. 6 | 7 | You can find videos of this demo and the rest in the series on Youtube at the following locations: 8 | [Part 1 - Intro](https://youtu.be/_39eKRNK3UU) 9 | [Part 2 - Initial Platform Build](https://youtu.be/9llyGjfKiLo) 10 | 11 | The architecture for this demo is shown below. 12 | ![MLArchitecture.png](images/MLArchitecture.png) 13 | 14 | ## Prerequisites 15 | You'll need to have completed the previous demo so you can generate some useful data to train the model with. You can find this at [Watch demo infrastructure](https://github.com/davedoesdemos/ConnectIQ-Watch-IoT/blob/master/IoTWatchInstructions.md) but if you don't have a device you can skip over to YouTube and see the demo videos instead. 16 | 17 | # Generate data 18 | If you have previous test data in your storage account, open that account and delete the entire container and data. For this demo to work, we need some stable resting heart rate data so the previous data might not be useful. Please bear in mind we're doing this so the demo works - in real life we don't ordinarily cheat like this with our data sets for machine learning. Once deleted, recreate the storage container so that new data will have somewhere to go. 19 | Next, sit in a chair and relax for a few minutes. Lying down will also work. Once relaxed, start the demo app on your watch to start recording "resting" data. Continue to relax for a few minutes to gather a reasonable amount of training data. Stop the app on your watch and check that you have some data in your storage account. 20 | 21 | # Data Transformation 22 | 23 | For the transformation, we'll be using Azure Databricks with a transformation script written in Scala. There are many, many ways to achieve this transformation and I'll cover some of those in later demos. We'll be triggering the script from Azure Data Factory here. Although not strictly necessary, a normal next step would be to "industrialise" the process of training the model so that we can later re-train based on new data. This would involve a recurring job in Data Factory to copy new data for training. 24 | 25 | ## Azure Databricks 26 | 27 | First, create a new Azure Databricks workspace called "connectiq" and place it into your demo resource group with the othe components for the watch demo. Select the standard pricing tier and choose not to place the workspace on your network. 28 | 29 | ![MLNewDatabricks.png](images/MLNewDatabricks.png) 30 | 31 | Once the workspace is deployed, open it in the portal and choose Launch Workspace. 32 | 33 | ![MLADBLaunch.png](images/MLADBLaunch.png) 34 | 35 | Once in the workspace, click New Notebook to create a notebook. This is similar to a script, and allows you to place code into a document and see output from that code within the same document. 36 | 37 | ![MLNewNotebook1.png](images/MLNewNotebook1.png) 38 | 39 | Give the notebook a name such as ConvertAvroToCSV, and choose Scala as the language. Everything here can also be achived in Python so when writing your own scripts choose 40 | 41 | ![MLNewNotebook2.png](images/MLNewNotebook2.png) 42 | 43 | ![MLNewNotebook3.png](images/MLNewNotebook3.png) 44 | 45 | ```scala 46 | import org.apache.spark.sql.functions._ 47 | import org.apache.spark.sql.types._ 48 | 49 | //Set up Blob storage keys 50 | spark.conf.set( 51 | "fs.azure.sas.YOURWATCHDATACONTAINER.YOURSTORAGEACCOUNT.blob.core.windows.net", 52 | "https://YOURSTORAGEACCOUNT.blob.core.windows.net/?sv=2018-03-28&ss=REST OF YOUR SAS TOKEN") 53 | spark.conf.set( 54 | "fs.azure.sas.YOURTARGETDATACONTAINER.YOURSTORAGEACCOUNT.blob.core.windows.net", 55 | "https://YOURSTORAGEACCOUNT.blob.core.windows.net/?sv=2018-03-28&ss=REST OF YOUR SAS TOKEN") 56 | 57 | //Create the schema based on what the watch app sends 58 | val watchSchema = (new StructType ) 59 | .add("heartRate",IntegerType) 60 | .add("yAccel",IntegerType) 61 | .add("xAccel",IntegerType) 62 | .add("altitude",FloatType) 63 | .add("cadence",IntegerType) 64 | .add("heading",FloatType) 65 | .add("xMag",IntegerType) 66 | .add("yMag",IntegerType) 67 | .add("zMag",IntegerType) 68 | .add("power",IntegerType) 69 | .add("pressure",FloatType) 70 | .add("speed",FloatType) 71 | .add("temp",IntegerType) 72 | .add("latitude",FloatType) 73 | .add("longitude",FloatType) 74 | 75 | //Import the data in AVRO format from Blob and extract the JSON payload using the above schema 76 | //Use wildcards in the path to select less data if needed 77 | val data = spark.read.format("avro").load("wasbs://YOURWATCHDATACONTAINER@YOURSTORAGEACCOUNT.blob.core.windows.net/YOURSTORAGEACCOUNT/watchdata/0/2019/*/*/*/*/*").selectExpr("cast (body as string) as json").select(from_json($"json", schema=watchSchema).as("readings")) 78 | //Select the data we need, in this case just heart rate data 79 | val data2 = data.select($"readings.heartRate") 80 | //Output the data to Blob in CSV Format 81 | data2.write.csv("wasbs://YOURTARGETDATACONTAINER@YOURSTORAGEACCOUNT.blob.core.windows.net/csv") 82 | ``` 83 | 84 | ![MLNEWADBCluster.png](images/MLNEWADBCluster.png) 85 | 86 | ![MLNEWADBCluster2.png](images/MLNEWADBCluster2.png) 87 | 88 | ![MLADBAttachCluster.png](images/MLADBAttachCluster.png) 89 | 90 | 91 | 92 | 93 | ## Data Factory 94 | 95 | # Machine Learning 96 | 97 | ## ML Studio 98 | 99 | ![mlExperiment.png](images/mlExperiment.png) 100 | 101 | ![MLImportData.png](images/MLImportData.png) 102 | 103 | ![MLSelectColumns.png](images/MLSelectColumns.png) 104 | 105 | ![MLSplitData.png](images/MLSplitData.png) 106 | 107 | 108 | # Testing -------------------------------------------------------------------------------- /IoTWatchEH/source/IoTWatchView.mc: -------------------------------------------------------------------------------- 1 | using Toybox.WatchUi; 2 | using Toybox.Communications as Comm; 3 | using Toybox.Sensor; 4 | using Toybox.Graphics; 5 | using Toybox.System; 6 | using Toybox.Lang; 7 | using Toybox.Time.Gregorian; 8 | using Toybox.Sensor; 9 | using Toybox.Application; 10 | using Toybox.Position; 11 | using Toybox.Timer; 12 | using Toybox.Cryptography; 13 | 14 | class IoTWatchView extends WatchUi.View { 15 | //set up display variables 16 | var status = "Waiting"; 17 | var field2 = "0"; 18 | var field3 = "0"; 19 | var field4 = "0"; 20 | var positionInfo = null; 21 | //set up the text labels 22 | hidden var dispField1; 23 | hidden var dispField2; 24 | hidden var dispField3; 25 | hidden var dispField4; 26 | 27 | //Set up the timer 28 | var dataTimer = new Timer.Timer(); 29 | //Fill in this variable with the time interval in ms that you want to use for submitting data. Lower values will cause more calls so will cost more! 30 | var timer = 1000; 31 | 32 | //Set up the variables for the API call. These will be overwritten by the app settings but I filled in here to show an example of correct strings 33 | var url = "https://yournamespace.servicebus.windows.net/yourhub/publishers/uniquestring/messages"; 34 | var sas = "SharedAccessSignature sr=https%3a%2f%2fyournamespace.servicebus.windows.net%2fyourhub%2fpublishers%2funiquestring%2fmessages&sig=signature%3d&se=61572283137&skn=KeyName"; 35 | 36 | function initialize() { 37 | View.initialize(); 38 | 39 | //get unique identifier to use as publisher string 40 | var mySettings = System.getDeviceSettings(); 41 | var publisher = mySettings.uniqueIdentifier; 42 | 43 | //set up the URL we will call 44 | url = "https://" + Application.Properties.getValue("eHNamespace") + ".servicebus.windows.net/" + Application.Properties.getValue("eHEventHub") + "/publishers/" + publisher + "/messages"; 45 | 46 | //Set up the SAS key for HMAC encryption 47 | var mySASKey = Application.Properties.getValue("eHSASKey"); 48 | var keyConvertOptions = { 49 | :fromRepresentation => StringUtil.REPRESENTATION_STRING_PLAIN_TEXT, 50 | :toRepresentation => StringUtil.REPRESENTATION_BYTE_ARRAY, 51 | :encoding => StringUtil.CHAR_ENCODING_UTF8 52 | }; 53 | //Convert SAS Key to Byte Array 54 | var mySASKeyByteArray = StringUtil.convertEncodedString(mySASKey, keyConvertOptions); 55 | 56 | //Set up string to convert 57 | //current time 58 | var UTCNow = Time.now().value(); 59 | //duration for key to last 60 | var duration = 2678400; //month 61 | var keyExpiry = (UTCNow + duration).toString(); 62 | var stringToConvert = Comm.encodeURL(url) + "\n" + keyExpiry; 63 | var bytesToConvert = StringUtil.convertEncodedString(stringToConvert, keyConvertOptions); 64 | 65 | //Set up HMAC (HashBasedMessageAuthenticationCode) 66 | var HMACOptions = { 67 | :algorithm => Cryptography.HASH_SHA256, 68 | :key => mySASKeyByteArray 69 | }; 70 | 71 | var HMAC = new Cryptography.HashBasedMessageAuthenticationCode(HMACOptions); 72 | //convert the string 73 | HMAC.update(bytesToConvert); 74 | var encryptedBytes = HMAC.digest(); 75 | 76 | HMAC.initialize(HMACOptions); 77 | //convert the string 78 | HMAC.update(bytesToConvert); 79 | encryptedBytes = HMAC.digest(); 80 | 81 | var keyConvertOptions2 = { 82 | :fromRepresentation => StringUtil.REPRESENTATION_BYTE_ARRAY, 83 | :toRepresentation => StringUtil.REPRESENTATION_STRING_BASE64, 84 | :encoding => StringUtil.CHAR_ENCODING_UTF8 85 | }; 86 | 87 | var myoutput = StringUtil.convertEncodedString(encryptedBytes, keyConvertOptions2); 88 | 89 | sas = "SharedAccessSignature sr=" + Comm.encodeURL(url) + "&sig=" + Comm.encodeURL(myoutput) + "&se=" + keyExpiry + "&skn=" + Application.Properties.getValue("eHSASKeyName"); 90 | 91 | // Set up a timer 92 | timer = Application.Properties.getValue("timer"); 93 | dataTimer.start(method(:timerCallback), timer, true); 94 | } 95 | 96 | function timerCallback() { 97 | var sensorInfo = Sensor.getInfo(); 98 | //var positionInfo = Position.getInfo(); 99 | var xAccel = 0; 100 | var yAccel = 0; 101 | var hR = 0; 102 | var altitude = 0; 103 | var cadence = 0; 104 | var heading = 0; 105 | var xMag = 0; 106 | var yMag = 0; 107 | var zMag = 0; 108 | var power = 0; 109 | var pressure = 0; 110 | var speed = 0; 111 | var temp = 0; 112 | var latitude = 0; 113 | var longitude = 0; 114 | var userID = Application.Properties.getValue("userID"); 115 | 116 | 117 | //Collect Data 118 | //Accelerometer 119 | if (sensorInfo has :accel && sensorInfo.accel != null) { 120 | var accel = sensorInfo.accel; 121 | xAccel = accel[0]; 122 | yAccel = accel[1]; 123 | } 124 | else { 125 | xAccel = 0; 126 | yAccel = 0; 127 | } 128 | //Heartrate 129 | if (sensorInfo has :heartRate && sensorInfo.heartRate != null) { 130 | hR = sensorInfo.heartRate; 131 | } 132 | else { 133 | hR = 0; 134 | } 135 | //Altitude 136 | if (sensorInfo has :altitude && sensorInfo.altitude != null) { 137 | altitude = sensorInfo.altitude; 138 | } 139 | else { 140 | altitude = 0; 141 | } 142 | //Cadence 143 | if (sensorInfo has :cadence && sensorInfo.cadence != null) { 144 | cadence = sensorInfo.cadence; 145 | } 146 | else { 147 | cadence = 0; 148 | } 149 | //heading 150 | if (sensorInfo has :heading && sensorInfo.heading != null) { 151 | heading = sensorInfo.heading; 152 | } 153 | else { 154 | heading = 0; 155 | } 156 | //magnetometer 157 | if (sensorInfo has :mag && sensorInfo.mag != null) { 158 | var mag = sensorInfo.mag; 159 | xMag = mag[0]; 160 | yMag = mag[1]; 161 | zMag = mag[2]; 162 | } 163 | else { 164 | xMag = 0; 165 | yMag = 1; 166 | zMag = 2; 167 | } 168 | //Power 169 | if (sensorInfo has :power && sensorInfo.power != null) { 170 | power = sensorInfo.power; 171 | } 172 | else { 173 | power = 0; 174 | } 175 | //Pressure 176 | if (sensorInfo has :pressure && sensorInfo.pressure != null) { 177 | pressure = sensorInfo.pressure; 178 | } 179 | else { 180 | pressure = 0; 181 | } 182 | //Speed 183 | if (sensorInfo has :speed && sensorInfo.speed != null) { 184 | speed = sensorInfo.speed; 185 | } 186 | else { 187 | speed = 0; 188 | } 189 | //Temperature 190 | if (sensorInfo has :temp && sensorInfo.temp != null) { 191 | temp = sensorInfo.temp; 192 | } 193 | else { 194 | temp = 0; 195 | } 196 | //Position 197 | if( positionInfo != null ) { 198 | if (positionInfo has :position && positionInfo.position != null) { 199 | var location = positionInfo.position.toDegrees(); 200 | latitude = location[0]; 201 | longitude = location[1]; 202 | } 203 | } 204 | else { 205 | latitude = 0; 206 | longitude = 0; 207 | } 208 | 209 | //Send the data to the REST API 210 | 211 | var params = { 212 | "userID" => userID, 213 | "heartRate" => hR.toNumber(), 214 | "xAccel" => xAccel.toNumber(), 215 | "yAccel" => yAccel.toNumber(), 216 | "altitude" => altitude.toFloat(), 217 | "cadence" => cadence.toNumber(), 218 | "heading" => heading.toFloat(), 219 | "xMag" => xMag.toNumber(), 220 | "yMag" => yMag.toNumber(), 221 | "zMag" => zMag.toNumber(), 222 | "power" => power.toNumber(), 223 | "pressure" => pressure.toFloat(), 224 | "speed" => speed.toFloat(), 225 | "temp" => temp.toNumber(), 226 | "latitude" => latitude.toFloat(), 227 | "longitude" => longitude.toFloat() 228 | }; 229 | 230 | //set the display field variables 231 | field2 = xAccel.toString(); 232 | field3 = yAccel.toString(); 233 | field4 = hR.toString(); 234 | 235 | var headers = { 236 | "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON, 237 | "Authorization" => sas 238 | }; 239 | var options = { 240 | :headers => headers, 241 | :method => Communications.HTTP_REQUEST_METHOD_POST, 242 | :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON 243 | }; 244 | Communications.makeWebRequest(url, params, options, method(:onReceive)); 245 | 246 | //Troubleshooting 247 | //System.println("URL: " + url); 248 | //System.println("SAS Token: " + sas); 249 | } 250 | 251 | function onReceive(responseCode, data) { 252 | //Uncomment for debug 253 | //System.println("Response code: " + responseCode); 254 | //System.println("Data: " + data); 255 | 256 | switch (responseCode.toString()) { 257 | case "-400": 258 | status = "OK"; 259 | break; 260 | case "201": 261 | status = "OK"; 262 | break; 263 | case "-101": 264 | status = "BLE Queue full"; 265 | break; 266 | case "-104": 267 | status = "No Connection"; 268 | break; 269 | case "--2": 270 | status = "Host Timeout"; 271 | break; 272 | default: 273 | status = responseCode.toString(); 274 | break; 275 | } 276 | 277 | WatchUi.requestUpdate(); 278 | } 279 | 280 | // Load your resources here 281 | function onLayout(dc) { 282 | setLayout(Rez.Layouts.MainLayout(dc)); 283 | } 284 | 285 | function onShow() { 286 | 287 | } 288 | 289 | // Update the view 290 | function onUpdate(dc) { 291 | View.onUpdate(dc); 292 | dc.clear(); 293 | // Call the parent onUpdate function to redraw the layout 294 | dispField1 = new WatchUi.Text({ 295 | :text=>status, 296 | :color=>Graphics.COLOR_BLACK, 297 | :font=>Graphics.FONT_LARGE, 298 | :locX =>WatchUi.LAYOUT_HALIGN_CENTER, 299 | :locY=>30, 300 | :justification=>Graphics.TEXT_JUSTIFY_CENTER 301 | }); 302 | dispField2 = new WatchUi.Text({ 303 | :text=>field2, 304 | :color=>Graphics.COLOR_BLACK, 305 | :font=>Graphics.FONT_LARGE, 306 | :locX =>60, 307 | :locY=>110, 308 | :justification=>Graphics.TEXT_JUSTIFY_CENTER 309 | }); 310 | dispField3 = new WatchUi.Text({ 311 | :text=>field3, 312 | :color=>Graphics.COLOR_BLACK, 313 | :font=>Graphics.FONT_LARGE, 314 | :locX =>180, 315 | :locY=>110, 316 | :justification=>Graphics.TEXT_JUSTIFY_CENTER 317 | }); 318 | dispField4 = new WatchUi.Text({ 319 | :text=>field4, 320 | :color=>Graphics.COLOR_BLACK, 321 | :font=>Graphics.FONT_LARGE, 322 | :locX =>WatchUi.LAYOUT_HALIGN_CENTER, 323 | :locY=>190, 324 | :justification=>Graphics.TEXT_JUSTIFY_CENTER 325 | }); 326 | dispField1.draw(dc); 327 | dispField2.draw(dc); 328 | dispField3.draw(dc); 329 | dispField4.draw(dc); 330 | 331 | } 332 | 333 | 334 | function setPosition(info) { 335 | positionInfo = info; 336 | WatchUi.requestUpdate(); 337 | } 338 | 339 | function onHide() { 340 | } 341 | 342 | } 343 | -------------------------------------------------------------------------------- /IoTWatchInstructions.md: -------------------------------------------------------------------------------- 1 | # Garmin Device Connect IQ IoT App 2 | **produced by Dave Lusty** 3 | 4 | ## Introduction 5 | This guide details how to configure the IoT watch app with Azure Services. 6 | You can find videos of this demo on Youtube at the following locations: 7 | [Part 1 - Intro](https://youtu.be/_39eKRNK3UU) 8 | [Part 2 - Initial Platform Build](https://youtu.be/9llyGjfKiLo) 9 | 10 | You can also find instructions on adding machine learning to the solution at [Watch Demo HR check with machine learning](https://github.com/davedoesdemos/ConnectIQ-Watch-IoT/blob/master/MLModelTraining.md) 11 | 12 | ## Prerequisites 13 | 14 | Before getting started with this app you'll need to have a few things. Firstly you'll need a compatible (Garmin device)[https://developer.garmin.com/connect-iq/compatible-devices/]. Different devices have different capabilities so not all of them will be able to send all metrics. 15 | 16 | Next, you'll need the Connect IQ SDK, the Eclipse IDE and the Java JRE installed. Instructions for setting these up can be found in the (Garmin getting started guide)[https://developer.garmin.com/connect-iq/programmers-guide/getting-started/]. 17 | 18 | # Environment 1 19 | 20 | ![environment 1 image](images/environment1.png) 21 | 22 | Of the two methods shown here, environment 1 is the simplest. This architecture uses a Logic App with HTTP endpoint to ingest data and push this to the Power BI streaming data service. This is simple to configure but not as scalable as the Event Hub version below. 23 | 24 | ## Power BI 25 | 26 | Log in to [Powerbi.microsoft.com](https://powerbi.microsoft.com/en-us/) and create a new app workspace called ConnectIQ Demo. Once created, add a streaming dataset. Choose API and click Next. 27 | 28 | ![5.CreateStreamingDataset.png](images/5.CreateStreamingDataset.png) 29 | 30 | Add in the various fields we'll be collecting. All of these are numbers except the timestamp which will be datetime. Enable historical data analysis so we can use the data later as well as in real time. 31 | 32 | ![6.NewDataset.png](images/6.NewDataset.png) 33 | 34 | Now create a dashboard and call it ConnectIQ Data. Add a tile and choose custom streaming data. Select your ConnectIQ dataset and then choose Card as the visualisation type. Select Heartrate as the field and click next. Add a title if you want one, and cilck apply to finish. Repeat this process to add a graph with timestamp as the axis and Heartrate as the value. Heartrate changes often so is a good metric to test with, but feel free to experiment. 35 | 36 | ![8.CustomStreamingData.png](images/8.CustomStreamingData.png) 37 | 38 | ## Logic App 39 | 40 | Create a Logic App in your subscription, call it ConnectIQAPI and choose a suitable location. 41 | 42 | ![1.NewLogicApp.png](images/1.NewLogicApp.png) 43 | 44 | Once created, open the Logic App and add a trigger for HTTP request received. Set the method to POST and save your Logic App. This will fill in the URL which you'll need to copy into the source code of the ConnectIQ App. 45 | 46 | ![2.HTTPRequest.png](images/2.HTTPRequest.png) 47 | 48 | Next, add a Parse JSON task and add in the body of the request as the content. In order to fill in the schema you may like to set up the app and receive a sample payload, for instance if you've modified the app in some way. For now, fill in the schema with the below: 49 | 50 | ```JSON 51 | { 52 | "properties": { 53 | "altitude": { 54 | "type": "number" 55 | }, 56 | "cadence": { 57 | "type": "integer" 58 | }, 59 | "heading": { 60 | "type": "number" 61 | }, 62 | "heartRate": { 63 | "type": "integer" 64 | }, 65 | "latitude": { 66 | "type": "number" 67 | }, 68 | "longitude": { 69 | "type": "number" 70 | }, 71 | "power": { 72 | "type": "integer" 73 | }, 74 | "pressure": { 75 | "type": "number" 76 | }, 77 | "speed": { 78 | "type": "number" 79 | }, 80 | "temp": { 81 | "type": "integer" 82 | }, 83 | "xAccel": { 84 | "type": "integer" 85 | }, 86 | "xMag": { 87 | "type": "integer" 88 | }, 89 | "yAccel": { 90 | "type": "integer" 91 | }, 92 | "yMag": { 93 | "type": "integer" 94 | }, 95 | "zMag": { 96 | "type": "integer" 97 | } 98 | }, 99 | "type": "object" 100 | } 101 | ``` 102 | 103 | ![4.ParseJSON.png](images/4.ParseJSON.png) 104 | 105 | Finally, add a Power BI task to add rows to dataset. You'll need to authenticate to your Power BI account here, and then choose your app workspace and dataset. Add in the columns from the dataset and then use the variables from the Parse JSON task to fill them. Use utcnow() to add the timestamp. 106 | 107 | ![7.AddRowsToDataset.png](images/7.AddRowsToDataset.png) 108 | 109 | ## Create the App 110 | 111 | Clone the repository to your local machine and open the IoTWatch2 project. Paste the URL from your Logic App HTTP trigger into the variable in the IoTWatchView.mc file. This includes the security token so is all you need to do. You can optionally change the timer variable from 5000 (5 seconds) to some other value. This is a balance of cost and battery life, remember your Logic App wil charge for each run. 112 | 113 | ![10.code.png](images/10.code.png) 114 | 115 | Run the app to test and you should see data in your Logic App runs as well as being passed through to Power BI in near real time. 116 | 117 | You can now side load the app to your device using the USB cable. Copy the file from the bin directory in your copy of the repository into the Apps directory on your device. You may need to restart the device to see the app. Start the app and ensure you have Bluetooth connectivity to the Connect mobile app and Internet access on your phone. You'll now see live data from the device in your Power BI session. 118 | 119 | # Environment 2 120 | 121 | ![environment 2 image](images/environment2.png) 122 | 123 | This environment uses an Azure Event Hub to ingest messages. As such this would be much more scalable and allows for copying data to a data lake for later analytics. Stream Analytics then takes these events and pushes them on to Power BI for the demo environment. 124 | 125 | ## Storage Account 126 | 127 | Create a storage account in a new resource group for the demo. Add a container in Blob called Watchdata and set it to Private. This container will be a sink for the incoming data from Event Hubs and will store a permanent copy of the events which can later be processed by HDInsight, SQL Data Warehouse, or Databricks. 128 | 129 | ![11.container.png](images/11.container.png) 130 | 131 | ## Event Hubs 132 | 133 | Create a new Event Hubs namespace and give it a unique name (for the demo use ConnectIQ and put your name in there). Choose standard for the tier. Choose 1 in the throughput units. This is caable of ingesting 1MB of events, or 1000 events per second whichever comes first. As you can imagine, this is able to handle many devices so we are unlikely to hit these limits with this app. Click Create to create the namespace. 134 | 135 | ![12.CreateEventHub.png](images/12.CreateEventHub.png) 136 | 137 | Once the namespace is created, open it in the console and click Firewalls and virtual networks then set the access to all networks. This is an IoT solution on which messages will be delivered from arbitary endpoints on the Internet so the firewall is not applicable. That's not to say the app is not secure. Connectivity is secured with encryption as well as shared access keys, so it's impossible to submit messages without this authentication and authorisation. 138 | 139 | ![13.Firewall.png](images/13.Firewall.png) 140 | 141 | Click on Overview and add event hub. Name the event hub watchdata and set capture to on. Select the checkbox to avoid empty files being created when there is no data. Select your watchdata container in your storage account and then click Create. 142 | 143 | ![14.createHub.png](images/14.createHub.png) 144 | 145 | Click the watchdata event hub and then click Shared access policies. Click Add and create a policy with the name ConnectIQApp and Send permissions. This SAS policy will be used by all devices, but each device will have a specific token and publisher. 146 | 147 | ![15.SAS.png](images/15.SAS.png) 148 | 149 | Once created, click on the policy to access the keys. Copy the primary key and paste it into the [Event Hubs Signature Generator](https://github.com/sandrinodimattia/RedDog/releases/tag/0.2.0.1) or use the instructions [from Microsoft](https://docs.microsoft.com/en-us/azure/event-hubs/event-hubs-authentication-and-security-model-overview) to create a SAS token for the device. Note that I've used the device serial number as publisher - you can use anything you like here but a serial number ensures each device is authenticated separately and also that the data is published to a dedicated location from each device. You may also choose to just enter "watch" or the model name here. 150 | 151 | ![16.tokenGenerator.png](images/16.tokenGenerator.png) 152 | 153 | This will give us a message URI of `https//connectiqlusty.servicebus.windows.net/watchdata/publishers/5mt000987/messages` which we will enter into the source code of the app. Ensure you replace this with the one you generate, which will be `https//.servicebus.windows.net//publishers//messages`. 154 | 155 | ## Create the App 156 | 157 | Clone the repository to your local machine and open the IoTWatchEH project. Paste the URL from your Event Hub as above into the variable in the IoTWatchView.mc file. Also copy the shared access signature you generated with the tool and paste this in to the code. You can optionally change the timer variable from 5000 (5 seconds) to some other value. This is a balance of cost and battery life. 158 | 159 | ![18.code.png](images/18.code.png) 160 | 161 | Run the app to test and you should see messages appear in your Event Hub monitor. 162 | 163 | You can now side load the app to your device using the USB cable. Copy the file from the bin directory in your copy of the repository into the Apps directory on your device. You may need to restart the device to see the app. Start the app and ensure you have Bluetooth connectivity to the Connect mobile app and Internet access on your phone. You'll now see live data from the device arrive into your Event Hub. 164 | 165 | ## Power BI 166 | 167 | Log in to [Powerbi.microsoft.com](https://powerbi.microsoft.com/en-us/) and create a new app workspace called ConnectIQ Demo. Stream Analytics will create the data set so for now this is all we need to do. 168 | 169 | ## Stream Analytics 170 | 171 | Create a Stream Analytics job and name is ConnectIQ. Choose Cloud as the location and click Create. 172 | 173 | ![17.StreamAnalytics.png](images/17.StreamAnalytics.png) 174 | 175 | Click on Inputs and select Add Stream Input. Choose Event Hubs as the source. 176 | 177 | ![19.StreamInput.png](images/19.StreamInput.png) 178 | 179 | Fill in your event hub details and give the input a name. 180 | 181 | ![20.NewInput.png](images/20.NewInput.png) 182 | 183 | Next, click Outputs and select Add, then Power BI. 184 | 185 | ![21.Outputs.png](images/21.Outputs.png) 186 | 187 | You'll need to authorise the app to connect to the Power BI service. This will then allow you to choose your app workspace and create a dataset in the service. 188 | 189 | ![22.PowerBIOutput.png](images/22.PowerBIOutput.png) 190 | 191 | ## Power BI Dashboard 192 | 193 | Now create a dashboard and call it ConnectIQ Data. Add a tile and choose custom streaming data. Select your ConnectIQ dataset and then choose Card as the visualisation type. Select Heartrate as the field and click next. Add a title if you want one, and cilck apply to finish. Repeat this process to add a graph with timestamp as the axis and Heartrate as the value. Heartrate changes often so is a good metric to test with, but feel free to experiment. 194 | 195 | ![8.CustomStreamingData.png](images/8.CustomStreamingData.png) 196 | 197 | If you now start the app you will see real time data on this dashboard. 198 | 199 | ![23.dashboard.png](images/23.dashboard.png) 200 | 201 | Note that at this point you will also see files begin to be created in your Blob storage, which we will later use for machine learning. -------------------------------------------------------------------------------- /IoTWatchEH/bin/IoTWatchEH.prg.debug.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | --------------------------------------------------------------------------------