├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── acme-bots-buildspec.yml ├── acme-bots.template ├── backend ├── acme-bots-config.sh ├── config │ └── config.yml ├── package.json ├── serverless.yml └── src │ ├── bot │ ├── ecsDeleteBot.js │ └── ecsProvisionBot.js │ ├── cleanup │ ├── clear-ecr-repo.js │ ├── clear-ecs-cluster.js │ ├── clear-s3-bucket.js │ └── detachPrincipals.js │ ├── cwEvents │ ├── constants.js │ ├── processAcmeBotsConnectivityEvents.js │ ├── processAcmeBotsStatusEvents.js │ └── searchLowBatteryBots.js │ ├── dashboards │ └── bots-operations.js │ ├── iot │ ├── attachCertToThing.js │ ├── attachPolicyToCert.js │ ├── checkIfThingExists.js │ ├── checkProvisioning.js │ ├── checkThingAttachments.js │ ├── createKeysAndCert.js │ ├── createPolicy.js │ ├── createThing.js │ ├── deleteCert.js │ ├── deleteMetadata.js │ ├── deletePolicy.js │ ├── deleteThing.js │ ├── detachCertFromThing.js │ ├── detachPolicyFromCert.js │ ├── disableCert.js │ ├── policy.doc │ └── utils.js │ ├── iotRules │ ├── constants.js │ ├── handleTelemetryData.js │ └── trackConnectivity.js │ └── step-functions │ ├── provisionThing.js │ └── removeThing.js ├── bots ├── Dockerfile ├── buildspec.yml ├── example.env ├── example │ └── Main.js ├── package.json ├── run_thing.sh ├── scripts │ ├── clear-ecr-repo.js │ ├── clear-ecs-cluster.js │ ├── env-setup.js │ └── setup.js └── src │ ├── bot │ ├── aws-configs.js │ ├── constants.js │ ├── core.js │ ├── index.js │ ├── iot.js │ ├── logger.js │ ├── msgHandler.js │ ├── telemetryCache.js │ └── verisign-root-ca.pem │ └── config │ └── index.js ├── docs ├── about.md ├── bootstrapping.md ├── bots-core.md ├── cleanup.md ├── cmd-ctrl.md ├── imgs │ ├── bootstrap-bot-diagram.png │ ├── bot-flow-chart.png │ ├── cmd-ctrl-diagram.png │ ├── cmd-ctrl-screen-shot.png │ ├── installation-workflow.png │ ├── prov-state-machine.png │ ├── provisioning-diagram.png │ ├── provisioning-screen-shot.png │ ├── remove-state-machine.png │ ├── telemetry-diagram.png │ └── telemetry-screen-shot.png ├── installing.md ├── provisioning.md └── telemetry.md └── frontend ├── package.json ├── public ├── favicon.ico ├── images │ ├── acmebots-architecture.png │ ├── aws-iot.png │ ├── browser-icon.png │ ├── terminal-icon.png │ └── thing.svg ├── index.html └── manifest.json ├── scripts ├── detachPrincipals.js └── setup.js └── src ├── App.css ├── App.js ├── components ├── Home.js ├── Menu.js ├── Telemetry.js └── Things.js ├── config └── index.js ├── index.css ├── index.js └── registerServiceWorker.js /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode/* 3 | **/node_modules/ 4 | **/.serverless/ 5 | frontend/src/config/config.json 6 | bots/src/config/config.json 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/aws-iot-core-acmebots-monitoring/issues), or [recently closed](https://github.com/aws-samples/aws-iot-core-acmebots-monitoring/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/aws-iot-core-acmebots-monitoring/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/aws-iot-core-acmebots-monitoring/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS Iot Core Acmebots Monitoring 2 | 3 | A sample IoT application used to show real-world monitoring scenarios. 4 | 5 | Services used: CloudWatch Logs, Events, Dashboards, Metrics, Lambda, StepFunctions, IoT, DynamoDB and S3. 6 | 7 | Follow the links below for details of the application and how to install and use it. 8 | 9 | 1. [About Acme Bots](./docs/about.md) 10 | 2. [Installing](./docs/installing.md) 11 | 3. [Provisioning](./docs/provisioning.md) 12 | 4. [Bootstraping](./docs/bootstrapping.md) 13 | 5. [Viewing Bot's Telemetry](./docs/telemetry.md) 14 | 6. [Sending Commands to the Bots](./docs/cmd-ctrl.md) 15 | 7. [Clean Up / Delete](./docs/cleanup.md) 16 | 17 | ## License Summary 18 | 19 | This sample code is made available under a modified MIT license. See the LICENSE file. -------------------------------------------------------------------------------- /acme-bots-buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | commands: 6 | - echo Installing Node Pre-requisites and updating Serverless Config... 7 | - npm install -g yarn serverless 8 | - ls 9 | - cd backend 10 | - chmod 755 acme-bots-config.sh 11 | - ./acme-bots-config.sh 12 | - echo Configuration set as follows 13 | - cat config/config.yml 14 | - npm install 15 | - cd ../frontend 16 | - npm install 17 | - yarn install 18 | - echo Uploading lambda code to S3 19 | - cd .. 20 | - mkdir lambda-code 21 | - cd lambda-code 22 | - zip -j clear-ecr-repo.zip ../backend/src/cleanup/clear-ecr-repo.js 23 | - zip -j clear-ecs-cluster.zip ../backend/src/cleanup/clear-ecs-cluster.js 24 | - zip -j clear-s3-bucket.zip ../backend/src/cleanup/clear-s3-bucket.js 25 | - zip -j detachPrincipals.zip ../backend/src/cleanup/detachPrincipals.js 26 | - aws s3 sync . "s3://$ArtifactS3Bucket" 27 | build: 28 | commands: 29 | - echo Serverless deploy started on `date` 30 | - cd ../backend 31 | - serverless deploy | tee deploy.out 32 | - echo Front end deploy started on `date` 33 | - cd ../frontend 34 | - node scripts/setup.js 35 | - npm run build 36 | post_build: 37 | commands: 38 | - echo Build completed on `date` 39 | - echo Pushing static website to s3 40 | - aws s3 sync --delete build/ "s3://$S3BUCKET" 41 | 42 | -------------------------------------------------------------------------------- /acme-bots.template: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: > 3 | Acme-bots deployment CloudFormation template. This template uses CodePipeline 4 | to deploy a serverless IoT application from GitHub. 5 | 6 | Parameters: 7 | 8 | UserName: 9 | Type: String 10 | Description: Admin user name to create for service access. 11 | Default: acmebotsadmin 12 | 13 | Email: 14 | Type: String 15 | Description: Valid email to sent admin user password. 16 | #AllowedPattern: '/[^\s@]+@[^\s@]+\.[^\s@]+/' 17 | #ConstraintDescription: Enter a valid email 18 | 19 | Stage: 20 | Type: String 21 | Description: Stage for deployment (dev, test, prod) 22 | Default: dev 23 | 24 | GitHubOAuthToken: 25 | Description: Enter GitHub Personal Access Token from https://github.com/settings/tokens 26 | Type: String 27 | 28 | GitHubUser: 29 | Description: Enter your GitHub username 30 | Type: String 31 | 32 | GitHubRepository: 33 | Description: Enter the repository name that should be monitored for changes 34 | Type: String 35 | 36 | GitHubBranch: 37 | Description: Enter the GitHub branch to monitored 38 | Type: String 39 | Default: master 40 | 41 | Metadata: 42 | AWS::CloudFormation::Interface: 43 | ParameterGroups: 44 | - 45 | Label: 46 | default: Application Configuration 47 | Parameters: 48 | - UserName 49 | - Email 50 | - Stage 51 | - 52 | Label: 53 | default: GitHub Configuration 54 | Parameters: 55 | - GitHubOAuthToken 56 | - GitHubUser 57 | - GitHubRepository 58 | - GitHubBranch 59 | 60 | ParameterLabels: 61 | UserName: 62 | default: Admin User Name 63 | Email: 64 | default: E-mail address 65 | Stage: 66 | default: Stage 67 | GitHubUser: 68 | default: GitHub User 69 | GitHubRepository: 70 | default: Repository Name 71 | GitHubBranch: 72 | default: Repository Branch 73 | GitHubOAuthToken: 74 | default: OAuth2 Token 75 | 76 | Resources: 77 | LambdaExecutionS3CleanupRole: 78 | Type: AWS::IAM::Role 79 | Properties: 80 | AssumeRolePolicyDocument: 81 | Statement: 82 | - Effect: Allow 83 | Principal: 84 | Service: [lambda.amazonaws.com] 85 | Action: ['sts:AssumeRole'] 86 | Path: / 87 | Policies: 88 | - PolicyName: lambda-execute 89 | PolicyDocument: 90 | Statement: 91 | - Effect: Allow 92 | Action: 93 | - logs:* 94 | Resource: 'arn:aws:logs:*:*:*' 95 | - Effect: Allow 96 | Action: 97 | - s3:GetObject 98 | - s3:PutObject 99 | Resource: 'arn:aws:s3:::*' 100 | - PolicyName: lambda-basic-execution 101 | PolicyDocument: 102 | Statement: 103 | - Effect: Allow 104 | Action: 105 | - logs:CreateLogGroup 106 | - logs:CreateLogStream 107 | - logs:PutLogEvents 108 | Resource: '*' 109 | - PolicyName: s3-object-delete 110 | PolicyDocument: 111 | Statement: 112 | - Effect: Allow 113 | Action: 114 | - s3:GetObject 115 | - s3:ListBucket 116 | - s3:DeleteObject 117 | Resource: '*' 118 | 119 | CleanBucketFunction: 120 | Type: "AWS::Lambda::Function" 121 | DependsOn: LambdaExecutionS3CleanupRole 122 | Properties: 123 | Handler: "index.clearS3Bucket" 124 | Role: !GetAtt LambdaExecutionS3CleanupRole.Arn 125 | Runtime: "nodejs10.x" 126 | Timeout: 25 127 | Code: 128 | ZipFile: > 129 | 'use strict'; 130 | 131 | var AWS = require('aws-sdk'); 132 | var s3 = new AWS.S3(); 133 | 134 | module.exports = { 135 | clearS3Bucket: function (event, context, cb) { 136 | console.log("Event=", event); 137 | console.log("Context=", context); 138 | if (event.RequestType === 'Delete') { 139 | var bucketName = event.ResourceProperties.BucketName; 140 | 141 | console.log("Delete bucket requested for", bucketName); 142 | 143 | var objects = listObjects(s3, bucketName); 144 | 145 | objects.then(function(result) { 146 | var keysToDeleteArray = []; 147 | console.log("Found "+ result.Contents.length + " objects to delete."); 148 | if (result.Contents.length === 0) { 149 | sendResponse(event, context, "SUCCESS"); 150 | } else { 151 | for (var i = 0, len = result.Contents.length; i < len; i++) { 152 | var item = new Object(); 153 | item = {}; 154 | item = { Key: result.Contents[i].Key }; 155 | keysToDeleteArray.push(item); 156 | } 157 | 158 | var delete_params = { 159 | Bucket: bucketName, 160 | Delete: { 161 | Objects: keysToDeleteArray, 162 | Quiet: false 163 | } 164 | }; 165 | 166 | var deletedObjects = deleteObjects(s3, delete_params); 167 | 168 | deletedObjects.then(function(result) { 169 | console.log("deleteObjects API returned ", result); 170 | sendResponse(event, context, "SUCCESS"); 171 | }, function(err) { 172 | console.log("ERROR: deleteObjects API Call failed!"); 173 | console.log(err); 174 | sendResponse(event, context, "FAILED"); 175 | }); 176 | } 177 | }, function(err) { 178 | console.log("ERROR: listObjects API Call failed!"); 179 | console.log(err); 180 | sendResponse(event, context, "FAILED"); 181 | }); 182 | 183 | } else { 184 | console.log("Delete not requested."); 185 | sendResponse(event, context, "SUCCESS"); 186 | } 187 | 188 | } 189 | }; 190 | 191 | function listObjects(client, bucketName) { 192 | return new Promise(function (resolve, reject){ 193 | client.listObjectsV2({Bucket: bucketName}, function (err, res){ 194 | if (err) reject(err); 195 | else resolve(res); 196 | }); 197 | }); 198 | } 199 | 200 | function deleteObjects(client, params) { 201 | return new Promise(function (resolve, reject){ 202 | client.deleteObjects(params, function (err, res){ 203 | if (err) reject(err); 204 | else resolve(res); 205 | }); 206 | }); 207 | } 208 | 209 | function sendResponse(event, context, responseStatus, responseData, physicalResourceId, noEcho) { 210 | var responseBody = JSON.stringify({ 211 | Status: responseStatus, 212 | Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName, 213 | PhysicalResourceId: physicalResourceId || context.logStreamName, 214 | StackId: event.StackId, 215 | RequestId: event.RequestId, 216 | LogicalResourceId: event.LogicalResourceId, 217 | NoEcho: noEcho || false, 218 | Data: responseData 219 | }); 220 | 221 | console.log("Response body:\n", responseBody); 222 | 223 | var https = require("https"); 224 | var url = require("url"); 225 | 226 | var parsedUrl = url.parse(event.ResponseURL); 227 | var options = { 228 | hostname: parsedUrl.hostname, 229 | port: 443, 230 | path: parsedUrl.path, 231 | method: "PUT", 232 | headers: { 233 | "content-type": "", 234 | "content-length": responseBody.length 235 | } 236 | }; 237 | 238 | var request = https.request(options, function(response) { 239 | console.log("Status code: " + response.statusCode); 240 | console.log("Status message: " + response.statusMessage); 241 | context.done(); 242 | }); 243 | 244 | request.on("error", function(error) { 245 | console.log("send(..) failed executing https.request(..): " + error); 246 | context.done(); 247 | }); 248 | 249 | request.write(responseBody); 250 | request.end(); 251 | } 252 | 253 | cleanupServerlessS3Bucket: 254 | Type: Custom::cleanupServerlessS3Bucket 255 | DependsOn: 256 | - CleanBucketFunction 257 | - ServerlessS3Bucket 258 | Properties: 259 | ServiceToken: !GetAtt CleanBucketFunction.Arn 260 | BucketName: !Sub ${AWS::StackName}-${Stage}-serverlessdeploy-${AWS::AccountId}-${AWS::Region} 261 | 262 | cleanupArtifactS3Bucket: 263 | Type: Custom::cleanupArtifactS3Bucket 264 | DependsOn: 265 | - CleanBucketFunction 266 | - ArtifactS3Bucket 267 | Properties: 268 | ServiceToken: !GetAtt CleanBucketFunction.Arn 269 | BucketName: !Sub ${AWS::StackName}-${Stage}-artifacts-${AWS::AccountId}-${AWS::Region} 270 | 271 | cleanupStaticWebsiteBucket: 272 | Type: Custom::cleanupStaticWebsiteBucket 273 | DependsOn: 274 | - CleanBucketFunction 275 | - StaticWebsiteBucket 276 | Properties: 277 | ServiceToken: !GetAtt CleanBucketFunction.Arn 278 | BucketName: !Sub ${AWS::StackName}-${Stage}-website-${AWS::AccountId}-${AWS::Region} 279 | 280 | ServerlessS3Bucket: 281 | Type: AWS::S3::Bucket 282 | Properties: 283 | BucketName: !Sub ${AWS::StackName}-${Stage}-serverlessdeploy-${AWS::AccountId}-${AWS::Region} 284 | # This must match the cleanup BucketName 285 | DeletionPolicy: Delete 286 | 287 | ArtifactS3Bucket: 288 | Type: AWS::S3::Bucket 289 | Properties: 290 | BucketName: !Sub ${AWS::StackName}-${Stage}-artifacts-${AWS::AccountId}-${AWS::Region} 291 | # This must match the cleanup BucketName 292 | DeletionPolicy: Delete 293 | #DependsOn: cleanupArtifactS3Bucket 294 | 295 | StaticWebsiteBucket: 296 | Type: AWS::S3::Bucket 297 | Properties: 298 | AccessControl: PublicRead 299 | BucketName: !Sub ${AWS::StackName}-${Stage}-website-${AWS::AccountId}-${AWS::Region} 300 | # This must match the cleanup BucketName 301 | WebsiteConfiguration: 302 | IndexDocument: index.html 303 | ErrorDocument: index.html 304 | DeletionPolicy: Delete 305 | #DependsOn: cleanupStaticWebsiteBucket 306 | 307 | BucketPolicy: 308 | Type: AWS::S3::BucketPolicy 309 | Properties: 310 | PolicyDocument: 311 | Id: MyPolicy 312 | Version: 2012-10-17 313 | Statement: 314 | - Sid: PublicReadForGetBucketObjects 315 | Effect: Allow 316 | Principal: '*' 317 | Action: 's3:GetObject' 318 | Resource: !Join 319 | - '' 320 | - - 'arn:aws:s3:::' 321 | - !Ref StaticWebsiteBucket 322 | - /* 323 | Bucket: !Ref StaticWebsiteBucket 324 | 325 | CodePipelineRole: 326 | Type: "AWS::IAM::Role" 327 | Properties: 328 | #RoleName: !Sub CodePipelineRole-${AWS::StackName}-${Stage} 329 | AssumeRolePolicyDocument: 330 | Statement: 331 | - Action: sts:AssumeRole 332 | Effect: "Allow" 333 | Principal: 334 | Service: codepipeline.amazonaws.com 335 | Version: "2012-10-17" 336 | Path: / 337 | Policies: 338 | - PolicyName: CodePipelineAccess 339 | PolicyDocument: 340 | Version: "2012-10-17" 341 | Statement: 342 | - 343 | Effect: "Allow" 344 | Action: 345 | - "s3:DeleteObject" 346 | - "s3:GetObject" 347 | - "s3:GetObjectVersion" 348 | - "s3:ListBucket" 349 | - "s3:PutObject" 350 | - "s3:GetBucketPolicy" 351 | Resource: 352 | - !GetAtt ArtifactS3Bucket.Arn 353 | - !Join 354 | - '' 355 | - - !GetAtt ArtifactS3Bucket.Arn 356 | - "/*" 357 | - 358 | Effect: "Allow" 359 | Action: 360 | - "cloudformation:CreateChangeSet" 361 | - "cloudformation:CreateStack" 362 | - "cloudformation:CreateUploadBucket" 363 | - "cloudformation:DeleteStack" 364 | - "cloudformation:Describe*" 365 | - "cloudformation:List*" 366 | - "cloudformation:UpdateStack" 367 | - "cloudformation:ValidateTemplate" 368 | - "cloudformation:ExecuteChangeSet" 369 | - "cloudformation:DeleteChangeSet" 370 | - "cloudformation:SetStackPolicy" 371 | Resource: "*" 372 | - 373 | Effect: "Allow" 374 | Action: 375 | - "codebuild:StartBuild" 376 | - "codebuild:BatchGetBuilds" 377 | Resource: "*" 378 | 379 | CodePipeline: 380 | Type: AWS::CodePipeline::Pipeline 381 | DependsOn: CodePipelineRole 382 | Properties: 383 | Name: !Sub "${AWS::StackName}-${Stage}" 384 | RoleArn: !GetAtt CodePipelineRole.Arn 385 | ArtifactStore: 386 | Type: S3 387 | Location: !Ref ArtifactS3Bucket 388 | Stages: 389 | - 390 | Name: Source 391 | Actions: 392 | - 393 | Name: GitHub 394 | ActionTypeId: 395 | Category: Source 396 | Owner: ThirdParty 397 | Version: 1 398 | Provider: GitHub 399 | OutputArtifacts: 400 | - Name: SourceOutput 401 | Configuration: 402 | Owner: !Ref GitHubUser 403 | Repo: !Ref GitHubRepository 404 | Branch: !Ref GitHubBranch 405 | OAuthToken: !Ref GitHubOAuthToken 406 | - 407 | Name: Build 408 | Actions: 409 | - 410 | Name: CodeBuild 411 | InputArtifacts: 412 | - Name: SourceOutput 413 | ActionTypeId: 414 | Category: Build 415 | Owner: AWS 416 | Version: 1 417 | Provider: CodeBuild 418 | OutputArtifacts: 419 | - Name: Built 420 | Configuration: 421 | ProjectName: !Ref CodeBuildProject 422 | 423 | CodeBuildProject: 424 | Type: AWS::CodeBuild::Project 425 | Properties: 426 | Name: !Sub "${AWS::StackName}-${Stage}" 427 | ServiceRole: !Ref CodeBuildRole 428 | Artifacts: 429 | Type: CODEPIPELINE 430 | Environment: 431 | Type: LINUX_CONTAINER 432 | ComputeType: BUILD_GENERAL1_SMALL 433 | Image: aws/codebuild/nodejs:8.11.0 434 | EnvironmentVariables: 435 | - Name: AWS_DEFAULT_REGION 436 | Type: PLAINTEXT 437 | Value: !Ref "AWS::Region" 438 | - Name: AWS_ACCOUNT_ID 439 | Value: !Ref "AWS::AccountId" 440 | - Name: USERNAME 441 | Value: !Ref UserName 442 | - Name: EMAIL 443 | Value: !Ref Email 444 | - Name: STAGE 445 | Value: !Ref Stage 446 | - Name: S3BUCKET 447 | Value: !Ref StaticWebsiteBucket 448 | - Name: ArtifactS3Bucket 449 | Value: !Ref ArtifactS3Bucket 450 | - Name: ServerlessS3Bucket 451 | Value: !Ref ServerlessS3Bucket 452 | - Name: GitHubUser 453 | Value: !Ref GitHubUser 454 | - Name: GitHubRepository 455 | Value: !Ref GitHubRepository 456 | - Name: GitHubBranch 457 | Value: !Ref GitHubBranch 458 | - Name: GitHubOAuthToken 459 | Value: !Ref GitHubOAuthToken 460 | Source: 461 | Type: CODEPIPELINE 462 | BuildSpec: "acme-bots-buildspec.yml" 463 | TimeoutInMinutes: 15 464 | 465 | CodeBuildRole: 466 | Type: AWS::IAM::Role 467 | Properties: 468 | AssumeRolePolicyDocument: 469 | Statement: 470 | - Action: sts:AssumeRole 471 | Effect: Allow 472 | Principal: 473 | Service: codebuild.amazonaws.com 474 | Version: '2012-10-17' 475 | Path: / 476 | Policies: 477 | - PolicyName: CodeBuildAccess 478 | PolicyDocument: 479 | Version: '2012-10-17' 480 | Statement: 481 | - Resource: "*" 482 | Effect: Allow 483 | # Does CodeBuild need all of this access? 484 | Action: 485 | - cloudwatch:* 486 | - dynamodb:* 487 | - events:* 488 | - iot:* 489 | - s3:* 490 | - xray:* 491 | - logs:* 492 | - ecs:* 493 | - ecr:* 494 | - states:* 495 | - sns:* 496 | - cognito-idp:* 497 | - cognito-identity:* 498 | - codecommit:* 499 | - lambda:* 500 | - codebuild:* 501 | - codepipeline:* 502 | - Resource: "*" 503 | Effect: Allow 504 | Action: 505 | - iam:* 506 | - Resource: "*" 507 | Effect: "Allow" 508 | Action: 509 | - cloudformation:Get* 510 | - cloudformation:Describe* 511 | - cloudformation:List* 512 | - cloudformation:Create* 513 | - cloudformation:Update* 514 | - cloudformation:ValidateTemplate 515 | - Resource: "*" 516 | Effect: Allow 517 | Action: 518 | - ec2:* 519 | - elasticloadbalancing:* 520 | 521 | Outputs: 522 | ArtifactS3Bucket: 523 | Description: 'S3 bucket that stores CodePipeline Artifacts.' 524 | Value: !Ref ArtifactS3Bucket 525 | 526 | StaticWebsiteBucket: 527 | Description: 'S3 bucket that hosts the front-end static website.' 528 | Value: !Ref StaticWebsiteBucket 529 | 530 | WebsiteURL: 531 | Description: 'URL for the front-end website hosted on S3.' 532 | Value: !GetAtt StaticWebsiteBucket.WebsiteURL 533 | 534 | CodePipelineURL: 535 | Description: 'The URL for the created pipeline' 536 | Value: !Sub https://${AWS::Region}.console.aws.amazon.com/codepipeline/home?region=${AWS::Region}#/view/${AWS::StackName}-${Stage} 537 | 538 | CodeBuildProject: 539 | Description: 'The project used to run serverless deploy' 540 | Value: !Ref CodeBuildProject 541 | 542 | 543 | -------------------------------------------------------------------------------- /backend/acme-bots-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Simple script to update the config yaml with variables 3 | sed -i "s/username: .*/username: $USERNAME/" config/config.yml 4 | sed -i "s/email: .*/email: $EMAIL/" config/config.yml 5 | sed -i "s/region: .*/region: $AWS_DEFAULT_REGION/" serverless.yml 6 | sed -i "s/stage: .*/stage: $STAGE/" serverless.yml 7 | sed -i "s/deploymentBucket: .*/deploymentBucket: $ServerlessS3Bucket/" serverless.yml 8 | sed -i "s/GitHubOAuthToken: .*/GitHubOAuthToken: $GitHubOAuthToken/" config/config.yml 9 | sed -i "s/GitHubUser: .*/GitHubUser: $GitHubUser/" config/config.yml 10 | sed -i "s/GitHubRepository: .*/GitHubRepository: $GitHubRepository/" config/config.yml 11 | sed -i "s/GitHubBranch: .*/GitHubBranch: $GitHubBranch/" config/config.yml 12 | sed -i "s/LambdaS3Bucket: .*/LambdaS3Bucket: $ArtifactS3Bucket/" config/config.yml 13 | -------------------------------------------------------------------------------- /backend/config/config.yml: -------------------------------------------------------------------------------- 1 | username: acmebotsadmin 2 | email: myemail@example.com 3 | GitHubUser: mygithubuser 4 | GitHubRepository: myreponame 5 | GitHubBranch: master 6 | GitHubOAuthToken: myprivatetoken 7 | 8 | # Do not change anything bellow this comment, 9 | # unless you REALLY know what you are doing 10 | 11 | LambdaS3Bucket: myS3BucketContainingLambdaCode 12 | 13 | iotThingsTableName: ${self:service}-${self:provider.stage}-iotThingsTable 14 | 15 | # Monitoring 16 | metricNameSpace: AcmeBots 17 | lowBatteryBotsCountMetric: lowBatteryCount 18 | eventsDelayMetric: eventsDelay 19 | lowBatteryThreshold: 10.0 20 | cwEventsDelayThreshold: 10000.00 21 | lowBatteryBotsCountAlarm: acme-bots-LowBatteryBotsCountAlarm 22 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iot-demo", 3 | "description": "", 4 | "version": "0.1.0", 5 | "dependencies": { 6 | "aws-xray-sdk": "^3.1.0", 7 | "json-size": "^1.0.0", 8 | "serverless-finch": "^2.6.0", 9 | "serverless-hooks-plugin": "^1.1.0", 10 | "serverless-pseudo-parameters": "^2.5.0" 11 | }, 12 | "devDependencies": { 13 | "serverless-plugin-tracing": "^2.0.0", 14 | "serverless-step-functions": "^2.21.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/bot/ecsDeleteBot.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var ecs = new AWS.ECS(); 3 | module.exports = { 4 | ecsDeleteBot: function (event, context, callback) { 5 | 6 | console.log(event); 7 | 8 | var blankArray = new Object(); 9 | blankArray = []; 10 | var tasks = listTasks(ecs, process.env.CLUSTER, event.thingName); 11 | 12 | tasks.then(function(result) { 13 | if (JSON.stringify(result.taskArns) === JSON.stringify(blankArray)) { 14 | console.log("No tasks exist for", event.thingName); 15 | console.log("listTasks API returned", result); 16 | } else { 17 | console.log("Task(s) found for ", event.thingName, ". DELETING"); 18 | console.log(result); 19 | 20 | var stop = []; 21 | for (var i = 0, len = result.taskArns.length; i < len; i++) { 22 | stop.push(stopTask(ecs, process.env.CLUSTER, result.taskArns[i])); 23 | } 24 | 25 | Promise.all(stop) 26 | .then(function(result) { 27 | for (var i = 0, len = result.length; i < len; i++) { 28 | console.log("Stopped Task", result[i].task.taskArn); 29 | } 30 | }) 31 | .catch(function(err) { 32 | console.log("ERROR: stopTask API call failed!"); 33 | console.log(err); 34 | }); 35 | } 36 | }, function(err) { 37 | console.log("ERROR: listTasks API call failed!"); 38 | console.log(err); 39 | }); 40 | 41 | 42 | var taskDefinitions = listActiveTaskDefinitions(ecs, event.thingName); 43 | 44 | taskDefinitions.then(function(result) { 45 | if (JSON.stringify(result.taskDefinitionArns) === JSON.stringify(blankArray)) { 46 | console.log("No task definitions exist for", event.thingName); 47 | console.log("listTaskDefinitions API returned", result); 48 | } else { 49 | console.log("Task Definition(s) found for ", event.thingName, ". DELETING"); 50 | console.log(result); 51 | 52 | var deregister = []; 53 | for (var i = 0, len = result.taskDefinitionArns.length; i < len; i++) { 54 | deregister.push(deregisterTaskDefinition(ecs, result.taskDefinitionArns[i])); 55 | } 56 | 57 | Promise.all(deregister) 58 | .then(function(result) { 59 | for (var i = 0, len = result.length; i < len; i++) { 60 | console.log("Deregistered Task Definition", result[i].taskDefinition.taskDefinitionArn); 61 | } 62 | }) 63 | .catch(function(err) { 64 | console.log("ERROR: deregisterTaskDefinition API call failed!"); 65 | console.log(err); 66 | }); 67 | 68 | } 69 | }, function(err) { 70 | console.log("ERROR: listTaskDefinitions API call failed!"); 71 | console.log(err); 72 | }); 73 | 74 | } 75 | }; 76 | 77 | function listTasks(client, taskCluster, taskFamily) { 78 | return new Promise(function (resolve, reject){ 79 | client.listTasks({cluster: taskCluster, family: taskFamily}, function (err, res){ 80 | if (err) reject(err); 81 | else resolve(res); 82 | }); 83 | }); 84 | } 85 | 86 | function stopTask(client, taskCluster, taskArn) { 87 | return new Promise(function (resolve, reject){ 88 | console.log("stopTask called for", taskArn); 89 | client.stopTask({task: taskArn, cluster: taskCluster}, function (err, res){ 90 | if (err) reject(err); 91 | else resolve(res); 92 | }); 93 | }); 94 | } 95 | 96 | function listActiveTaskDefinitions(client, taskFamily) { 97 | return new Promise(function (resolve, reject){ 98 | client.listTaskDefinitions({status: "ACTIVE", familyPrefix: taskFamily}, function (err, res){ 99 | if (err) reject(err); 100 | else resolve(res); 101 | }); 102 | }); 103 | } 104 | 105 | function deregisterTaskDefinition(client, taskDefArn) { 106 | return new Promise(function (resolve, reject){ 107 | console.log("deregisterTaskDefinition called for", taskDefArn); 108 | client.deregisterTaskDefinition({taskDefinition: taskDefArn}, function (err, res){ 109 | if (err) reject(err); 110 | else resolve(res); 111 | }); 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /backend/src/bot/ecsProvisionBot.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var ecs = new AWS.ECS(); 3 | module.exports = { 4 | ecsProvisionBot: function (event, context, callback) { 5 | 6 | console.log(event); 7 | 8 | var awsLogsGroup = "/ecs/" + process.env.SERVICE; 9 | var params = { 10 | containerDefinitions: [ 11 | { 12 | name: event.thingName, 13 | essential: true, 14 | image: process.env.DOCKER_IMAGE, 15 | environment: [ 16 | {name: 'REGION', value: process.env.AWS_REGION}, 17 | {name: 'SERVICE', value: process.env.SERVICE}, 18 | {name: 'STAGE', value: process.env.STAGE}, 19 | {name: 'THING_NAME', value: event.thingName} 20 | ], 21 | logConfiguration: { 22 | "logDriver": "awslogs", 23 | "options": { 24 | "awslogs-group": awsLogsGroup, 25 | "awslogs-region": process.env.AWS_REGION, 26 | "awslogs-stream-prefix": event.thingName, 27 | "awslogs-create-group": "true" 28 | } 29 | } 30 | }], 31 | family: event.thingName, 32 | executionRoleArn: process.env.TASK_EXEC_ROLE_ARN, 33 | taskRoleArn: process.env.TASK_ROLE_ARN, 34 | networkMode: "awsvpc", 35 | requiresCompatibilities: [ "FARGATE" ], 36 | cpu: "256", 37 | memory: "512" 38 | }; 39 | 40 | // Params to run latest task definition 41 | var runtask_params = { 42 | taskDefinition: event.thingName, 43 | cluster: process.env.CLUSTER, 44 | count: 1, 45 | group: process.env.SERVICE, 46 | launchType: 'FARGATE', 47 | networkConfiguration: { 48 | awsvpcConfiguration: { 49 | subnets: process.env.SUBNETS.split(','), 50 | assignPublicIp: 'DISABLED', 51 | securityGroups: process.env.TASK_SECGROUP.split(',') 52 | } 53 | }, 54 | }; 55 | 56 | var taskDef = registerTaskDefinition(ecs, params); 57 | taskDef.then(function(result) { 58 | console.log("Success - Registered Task Definition"); // successful response 59 | console.log(result); 60 | 61 | // Run Task 62 | var taskRun = runTask(ecs, runtask_params); 63 | taskRun.then(function(result) { 64 | console.log("Success - Initiated Task Run"); // successful response 65 | console.log(result); 66 | }) 67 | .catch(function(err) { 68 | console.log("ERROR: Running Task Failed"); // an error occurred 69 | console.log(err); 70 | }); 71 | 72 | }) 73 | .catch(function(err) { 74 | console.log("ERROR: Task Definition Failed"); // an error occurred 75 | console.log(err); 76 | }); 77 | 78 | } 79 | }; 80 | 81 | function registerTaskDefinition(client, params) { 82 | return new Promise(function (resolve, reject){ 83 | client.registerTaskDefinition(params, function(err, res) { 84 | if (err) reject(err); 85 | else resolve(res); 86 | }); 87 | }); 88 | } 89 | 90 | function runTask(client, params) { 91 | return new Promise(function (resolve, reject){ 92 | client.runTask(params, function(err, res) { 93 | if (err) reject(err); 94 | else resolve(res); 95 | }); 96 | }); 97 | } 98 | 99 | -------------------------------------------------------------------------------- /backend/src/cleanup/clear-ecr-repo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AWS = require('aws-sdk'); 4 | var ecr = new AWS.ECR(); 5 | 6 | module.exports = { 7 | clearEcrRepo: function (event, context, cb) { 8 | console.log("Event=", event); 9 | console.log("Context=", context); 10 | if (event.RequestType === 'Delete') { 11 | var images = null; 12 | var repoName = event.ResourceProperties.RepoName; 13 | 14 | console.log("Looking for images in", repoName); 15 | 16 | var params = { 17 | repositoryName: repoName 18 | }; 19 | ecr.listImages(params, function(err, data) { 20 | if (err) { 21 | console.log("ERROR: listImages API Call failed!"); 22 | console.log(err); // an error occurred 23 | sendResponse(event, context, "FAILED"); 24 | } else { 25 | console.log("Images listed: ", data); // successful response 26 | if (JSON.stringify(data.imageIds) === '[]') { 27 | console.log("No images found"); 28 | sendResponse(event, context, "SUCCESS"); 29 | } else { 30 | images = { 31 | repositoryName: repoName, 32 | imageIds: data.imageIds 33 | }; 34 | console.log("Deleting Images..."); 35 | // Delete images 36 | ecr.batchDeleteImage(images, function(err, data) { 37 | if (err) { 38 | console.log("ERROR: batchDeleteImage API Call failed!"); 39 | console.log(err, err.stack); // an error occurred 40 | sendResponse(event, context, "FAILED"); 41 | } else { 42 | console.log("bacthDeleteImage API returned ", data); // successful response 43 | sendResponse(event, context, "SUCCESS"); 44 | } 45 | }); 46 | } 47 | } 48 | }); 49 | 50 | } else { 51 | console.log("Delete not requested."); 52 | sendResponse(event, context, "SUCCESS"); 53 | } 54 | 55 | } 56 | }; 57 | 58 | function sendResponse(event, context, responseStatus, responseData, physicalResourceId, noEcho) { 59 | var responseBody = JSON.stringify({ 60 | Status: responseStatus, 61 | Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName, 62 | PhysicalResourceId: physicalResourceId || context.logStreamName, 63 | StackId: event.StackId, 64 | RequestId: event.RequestId, 65 | LogicalResourceId: event.LogicalResourceId, 66 | NoEcho: noEcho || false, 67 | Data: responseData 68 | }); 69 | 70 | console.log("Response body:\n", responseBody); 71 | 72 | var https = require("https"); 73 | var url = require("url"); 74 | 75 | var parsedUrl = url.parse(event.ResponseURL); 76 | var options = { 77 | hostname: parsedUrl.hostname, 78 | port: 443, 79 | path: parsedUrl.path, 80 | method: "PUT", 81 | headers: { 82 | "content-type": "", 83 | "content-length": responseBody.length 84 | } 85 | }; 86 | 87 | var request = https.request(options, function(response) { 88 | console.log("Status code: " + response.statusCode); 89 | console.log("Status message: " + response.statusMessage); 90 | context.done(); 91 | }); 92 | 93 | request.on("error", function(error) { 94 | console.log("send(..) failed executing https.request(..): " + error); 95 | context.done(); 96 | }); 97 | 98 | request.write(responseBody); 99 | request.end(); 100 | } 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /backend/src/cleanup/clear-ecs-cluster.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AWS = require('aws-sdk'); 4 | var ecs = new AWS.ECS(); 5 | 6 | module.exports = { 7 | clearEcsCluster: function (event, context, cb) { 8 | console.log("Event=", event); 9 | console.log("Context=", context); 10 | if (event.RequestType === 'Delete') { 11 | var tasks = null; 12 | var taskDefinitions = null; 13 | var failure = 0; 14 | 15 | var blankArray = new Object(); 16 | blankArray = []; 17 | 18 | var clusterName = event.ResourceProperties.ECSClusterName; 19 | var taskRoleArn = event.ResourceProperties.ECSTaskRoleArn; 20 | 21 | console.log("Found cluster name: ", clusterName); 22 | 23 | tasks = listTasks(ecs, clusterName); 24 | tasks.then(function(result) { 25 | if (JSON.stringify(result.taskArns) === JSON.stringify(blankArray)) { 26 | console.log("No tasks exist in cluster", clusterName); 27 | console.log("listTasks API returned", result); 28 | } else { 29 | console.log("Task(s) found for cluster ", clusterName, ". DELETING"); 30 | console.log(result); 31 | 32 | var stop = []; 33 | for (var i = 0, len = result.taskArns.length; i < len; i++) { 34 | stop.push(stopTask(ecs, clusterName, result.taskArns[i])); 35 | } 36 | 37 | Promise.all(stop) 38 | .then(function(result) { 39 | for (var i = 0, len = result.length; i < len; i++) { 40 | console.log("Stopped Task", result[i].task.taskArn); 41 | } 42 | }) 43 | .catch(function(err) { 44 | console.log("ERROR: stopTask API call failed!"); 45 | console.log(err); 46 | sendResponse(event, context, "FAILED"); 47 | failure = 1; 48 | }); 49 | } 50 | }, function(err) { 51 | console.log("ERROR: listTasks API call failed!"); 52 | console.log(err); 53 | sendResponse(event, context, "FAILED"); 54 | failure = 1; 55 | }); 56 | 57 | console.log("Found task role arn: ", taskRoleArn); 58 | 59 | taskDefinitions = listActiveTaskDefinitions(ecs); 60 | 61 | taskDefinitions.then(function(result) { 62 | if (JSON.stringify(result.taskDefinitionArns) === JSON.stringify(blankArray)) { 63 | console.log("No task definitions found with Role Arn ", taskRoleArn); 64 | console.log("listTaskDefinitions API returned", result); 65 | } else { 66 | console.log("Task Definition(s) found. DELETING"); 67 | console.log(result); 68 | 69 | var descTaskDef = []; 70 | for (var i = 0, len = result.taskDefinitionArns.length; i < len; i++) { 71 | descTaskDef.push(describeTaskDefinition(ecs, result.taskDefinitionArns[i])); 72 | } 73 | 74 | Promise.all(descTaskDef) 75 | .then(function(result) { 76 | var deregister = []; 77 | 78 | for (var i = 0, len = result.length; i < len; i++) { 79 | console.log(result[i].taskDefinition.taskRoleArn, " = ", taskRoleArn); 80 | 81 | if ( result[i].taskDefinition.taskRoleArn === taskRoleArn ) { 82 | deregister.push(deregisterTaskDefinition(ecs, result[i].taskDefinition.taskDefinitionArn)); 83 | } 84 | 85 | Promise.all(deregister) 86 | .then(function(result) { 87 | for (var i = 0, len = result.length; i < len; i++) { 88 | console.log("Deregistered Task Definition", result[i].taskDefinition.taskDefinitionArn); 89 | } 90 | }) 91 | .catch(function(err) { 92 | console.log("ERROR: deregisterTaskDefinition API call failed!"); 93 | console.log(err); 94 | sendResponse(event, context, "FAILED"); 95 | failure = 1; 96 | }); 97 | 98 | } 99 | }) 100 | .catch(function(err) { 101 | console.log("ERROR: describeTaskDefinition API call failed!"); 102 | console.log(err); 103 | sendResponse(event, context, "FAILED"); 104 | failure = 1; 105 | }); 106 | } 107 | }, function(err) { 108 | console.log("ERROR: listTaskDefinitions API call failed!"); 109 | console.log(err); 110 | sendResponse(event, context, "FAILED"); 111 | failure = 1; 112 | }); 113 | 114 | if ( failure === 0 ) { 115 | sendResponse(event, context, "SUCCESS"); 116 | } 117 | } else { 118 | console.log("Delete not requested."); 119 | sendResponse(event, context, "SUCCESS"); 120 | } 121 | } 122 | }; 123 | 124 | function listTasks(client, taskCluster) { 125 | return new Promise(function (resolve, reject){ 126 | client.listTasks({cluster: taskCluster}, function (err, res){ 127 | if (err) reject(err); 128 | else resolve(res); 129 | }); 130 | }); 131 | } 132 | 133 | function stopTask(client, taskCluster, taskArn) { 134 | return new Promise(function (resolve, reject){ 135 | console.log("stopTask called for", taskArn); 136 | client.stopTask({task: taskArn, cluster: taskCluster}, function (err, res){ 137 | if (err) reject(err); 138 | else resolve(res); 139 | }); 140 | }); 141 | } 142 | 143 | function listActiveTaskDefinitions(client) { 144 | return new Promise(function (resolve, reject){ 145 | client.listTaskDefinitions({status: "ACTIVE"}, function (err, res){ 146 | if (err) reject(err); 147 | else resolve(res); 148 | }); 149 | }); 150 | } 151 | 152 | function describeTaskDefinition(client, taskDefinition) { 153 | return new Promise(function (resolve, reject){ 154 | client.describeTaskDefinition({taskDefinition: taskDefinition}, function (err, res){ 155 | if (err) reject(err); 156 | else resolve(res); 157 | }); 158 | }); 159 | } 160 | 161 | function deregisterTaskDefinition(client, taskDefArn) { 162 | return new Promise(function (resolve, reject){ 163 | console.log("deregisterTaskDefinition called for", taskDefArn); 164 | client.deregisterTaskDefinition({taskDefinition: taskDefArn}, function (err, res){ 165 | if (err) reject(err); 166 | else resolve(res); 167 | }); 168 | }); 169 | } 170 | 171 | function sendResponse(event, context, responseStatus, responseData, physicalResourceId, noEcho) { 172 | var responseBody = JSON.stringify({ 173 | Status: responseStatus, 174 | Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName, 175 | PhysicalResourceId: physicalResourceId || context.logStreamName, 176 | StackId: event.StackId, 177 | RequestId: event.RequestId, 178 | LogicalResourceId: event.LogicalResourceId, 179 | NoEcho: noEcho || false, 180 | Data: responseData 181 | }); 182 | 183 | console.log("Response body:\n", responseBody); 184 | 185 | var https = require("https"); 186 | var url = require("url"); 187 | 188 | var parsedUrl = url.parse(event.ResponseURL); 189 | var options = { 190 | hostname: parsedUrl.hostname, 191 | port: 443, 192 | path: parsedUrl.path, 193 | method: "PUT", 194 | headers: { 195 | "content-type": "", 196 | "content-length": responseBody.length 197 | } 198 | }; 199 | 200 | var request = https.request(options, function(response) { 201 | console.log("Status code: " + response.statusCode); 202 | console.log("Status message: " + response.statusMessage); 203 | context.done(); 204 | }); 205 | 206 | request.on("error", function(error) { 207 | console.log("send(..) failed executing https.request(..): " + error); 208 | context.done(); 209 | }); 210 | 211 | request.write(responseBody); 212 | request.end(); 213 | } 214 | -------------------------------------------------------------------------------- /backend/src/cleanup/clear-s3-bucket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AWS = require('aws-sdk'); 4 | var s3 = new AWS.S3(); 5 | 6 | module.exports = { 7 | clearS3Bucket: function (event, context, cb) { 8 | console.log("Event=", event); 9 | console.log("Context=", context); 10 | if (event.RequestType === 'Delete') { 11 | var bucketName = event.ResourceProperties.BucketName; 12 | 13 | console.log("Delete bucket requested for", bucketName); 14 | 15 | var objects = listObjects(s3, bucketName); 16 | 17 | objects.then(function(result) { 18 | var keysToDeleteArray = []; 19 | console.log("Found "+ result.Contents.length + " objects to delete."); 20 | if (result.Contents.length === 0) { 21 | sendResponse(event, context, "SUCCESS"); 22 | } else { 23 | for (var i = 0, len = result.Contents.length; i < len; i++) { 24 | var item = new Object(); 25 | item = {}; 26 | item = { Key: result.Contents[i].Key }; 27 | keysToDeleteArray.push(item); 28 | } 29 | 30 | var delete_params = { 31 | Bucket: bucketName, 32 | Delete: { 33 | Objects: keysToDeleteArray, 34 | Quiet: false 35 | } 36 | }; 37 | 38 | var deletedObjects = deleteObjects(s3, delete_params); 39 | 40 | deletedObjects.then(function(result) { 41 | console.log("deleteObjects API returned ", result); 42 | sendResponse(event, context, "SUCCESS"); 43 | }, function(err) { 44 | console.log("ERROR: deleteObjects API Call failed!"); 45 | console.log(err); 46 | sendResponse(event, context, "FAILED"); 47 | }); 48 | } 49 | }, function(err) { 50 | console.log("ERROR: listObjects API Call failed!"); 51 | console.log(err); 52 | sendResponse(event, context, "FAILED"); 53 | }); 54 | 55 | } else { 56 | console.log("Delete not requested."); 57 | sendResponse(event, context, "SUCCESS"); 58 | } 59 | 60 | } 61 | }; 62 | 63 | function listObjects(client, bucketName) { 64 | return new Promise(function (resolve, reject){ 65 | client.listObjectsV2({Bucket: bucketName}, function (err, res){ 66 | if (err) reject(err); 67 | else resolve(res); 68 | }); 69 | }); 70 | } 71 | 72 | function deleteObjects(client, params) { 73 | return new Promise(function (resolve, reject){ 74 | client.deleteObjects(params, function (err, res){ 75 | if (err) reject(err); 76 | else resolve(res); 77 | }); 78 | }); 79 | } 80 | 81 | function sendResponse(event, context, responseStatus, responseData, physicalResourceId, noEcho) { 82 | var responseBody = JSON.stringify({ 83 | Status: responseStatus, 84 | Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName, 85 | PhysicalResourceId: physicalResourceId || context.logStreamName, 86 | StackId: event.StackId, 87 | RequestId: event.RequestId, 88 | LogicalResourceId: event.LogicalResourceId, 89 | NoEcho: noEcho || false, 90 | Data: responseData 91 | }); 92 | 93 | console.log("Response body:\n", responseBody); 94 | 95 | var https = require("https"); 96 | var url = require("url"); 97 | 98 | var parsedUrl = url.parse(event.ResponseURL); 99 | var options = { 100 | hostname: parsedUrl.hostname, 101 | port: 443, 102 | path: parsedUrl.path, 103 | method: "PUT", 104 | headers: { 105 | "content-type": "", 106 | "content-length": responseBody.length 107 | } 108 | }; 109 | 110 | var request = https.request(options, function(response) { 111 | console.log("Status code: " + response.statusCode); 112 | console.log("Status message: " + response.statusMessage); 113 | context.done(); 114 | }); 115 | 116 | request.on("error", function(error) { 117 | console.log("send(..) failed executing https.request(..): " + error); 118 | context.done(); 119 | }); 120 | 121 | request.write(responseBody); 122 | request.end(); 123 | } 124 | -------------------------------------------------------------------------------- /backend/src/cleanup/detachPrincipals.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var Iot = require('aws-sdk/clients/iot'); 5 | var iot = new Iot(); 6 | 7 | module.exports = { 8 | clearPrincipals: function (event, context, cb) { 9 | console.log("Event=", event); 10 | console.log("Context=", context); 11 | if (event.RequestType === 'Delete') { 12 | var policyName = event.ResourceProperties.PolicyName; 13 | var failure = 0; 14 | 15 | var params = { 16 | policyName: policyName 17 | }; 18 | iot.listPolicyPrincipals(params, function(err, data) { 19 | if (err) { 20 | console.log("ERROR: listPolicyPrincipals API Call failed!"); 21 | console.log(err, err.stack); 22 | sendResponse(event, context, "FAILED"); 23 | failure = 1; 24 | } else { 25 | data.principals.forEach(function(principal){ 26 | var detachParams = { 27 | policyName: policyName, 28 | principal: principal 29 | }; 30 | iot.detachPrincipalPolicy(detachParams, function(err2, data2){ 31 | if (err2) { 32 | console.log("ERROR: detachPrincipalPolicy API Call failed!"); 33 | console.log(err2, err2.stack); 34 | sendResponse(event, context, "FAILED"); 35 | failure = 1; 36 | } else { 37 | console.log(`Successfully detached ${principal} from ${policyName}`); 38 | } 39 | }); 40 | }); 41 | } 42 | }); 43 | 44 | if ( failure === 0 ) { 45 | sendResponse(event, context, "SUCCESS"); 46 | } 47 | } else { 48 | console.log("Detach not requested."); 49 | sendResponse(event, context, "SUCCESS"); 50 | } 51 | 52 | } 53 | }; 54 | 55 | function sendResponse(event, context, responseStatus, responseData, physicalResourceId, noEcho) { 56 | var responseBody = JSON.stringify({ 57 | Status: responseStatus, 58 | Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName, 59 | PhysicalResourceId: physicalResourceId || context.logStreamName, 60 | StackId: event.StackId, 61 | RequestId: event.RequestId, 62 | LogicalResourceId: event.LogicalResourceId, 63 | NoEcho: noEcho || false, 64 | Data: responseData 65 | }); 66 | 67 | console.log("Response body:\n", responseBody); 68 | 69 | var https = require("https"); 70 | var url = require("url"); 71 | 72 | var parsedUrl = url.parse(event.ResponseURL); 73 | var options = { 74 | hostname: parsedUrl.hostname, 75 | port: 443, 76 | path: parsedUrl.path, 77 | method: "PUT", 78 | headers: { 79 | "content-type": "", 80 | "content-length": responseBody.length 81 | } 82 | }; 83 | 84 | var request = https.request(options, function(response) { 85 | console.log("Status code: " + response.statusCode); 86 | console.log("Status message: " + response.statusMessage); 87 | context.done(); 88 | }); 89 | 90 | request.on("error", function(error) { 91 | console.log("send(..) failed executing https.request(..): " + error); 92 | context.done(); 93 | }); 94 | 95 | request.write(responseBody); 96 | request.end(); 97 | } 98 | 99 | -------------------------------------------------------------------------------- /backend/src/cwEvents/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | METRICS_NAMESPACE : 'AcmeBots', 3 | EVENTS_DELAY_METRIC : 'eventsDelay' 4 | } -------------------------------------------------------------------------------- /backend/src/cwEvents/processAcmeBotsConnectivityEvents.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var CloudWatch = require('aws-sdk/clients/cloudwatch'); 6 | var DynamoDB = require('aws-sdk/clients/dynamodb'); 7 | var Constants = require(`${__dirname}/constants`); 8 | 9 | AWS.config.region = process.env.AWS_REGION; 10 | var cw = AWSXRay.captureAWSClient(new CloudWatch()); 11 | var ddb = AWSXRay.captureAWSClient(new DynamoDB()); 12 | 13 | module.exports = { 14 | handler: function(event, context, cb) { 15 | var now = new Date(); 16 | var delay = now - event.detail.timestamp; 17 | 18 | // Update the dynamoDB item 19 | var ddbParams = { 20 | TableName: process.env.THINGS_TABLE, 21 | Key: { 22 | "thingName": { 23 | S: event.detail.clientId 24 | } 25 | }, 26 | UpdateExpression: "SET #C = :c, #L = :l", 27 | ExpressionAttributeNames: { 28 | "#C": "connected", 29 | "#L": "lastSeenAt" 30 | }, 31 | ExpressionAttributeValues: { 32 | ":c": { 33 | BOOL: event.detail.eventType === 'connected' 34 | }, 35 | ":l": { 36 | N: `${now.getTime()}` 37 | }, 38 | ":tn": { 39 | S: event.detail.clientId 40 | } 41 | }, 42 | ConditionExpression: "thingName = :tn", 43 | }; 44 | ddb.updateItem(ddbParams, function(err, data) { 45 | if (err) { 46 | if( err.code == 'ConditionalCheckFailedException') { 47 | data = {thingName: event.clientId, exists: false} 48 | cb(null, data) 49 | } else { 50 | cb(err, null); 51 | } 52 | } else { 53 | cb(err, data); 54 | } 55 | }); 56 | 57 | // Send cw custom metric 58 | var params = { 59 | MetricData: [ 60 | { 61 | MetricName: Constants.EVENTS_DELAY_METRIC, 62 | StorageResolution: 1, 63 | Timestamp: now, 64 | Unit: 'Milliseconds', 65 | Value: delay 66 | } 67 | ], 68 | Namespace: Constants.METRICS_NAMESPACE 69 | }; 70 | cw.putMetricData(params, function(err, data) { 71 | if (err) console.log(err, err.stack); 72 | }); 73 | 74 | } 75 | } -------------------------------------------------------------------------------- /backend/src/cwEvents/processAcmeBotsStatusEvents.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var CloudWatch = require('aws-sdk/clients/cloudwatch'); 6 | var DynamoDB = require('aws-sdk/clients/dynamodb'); 7 | var Constants = require(`${__dirname}/constants`); 8 | 9 | AWS.config.region = process.env.AWS_REGION; 10 | var cw = AWSXRay.captureAWSClient(new CloudWatch()); 11 | var ddb = AWSXRay.captureAWSClient(new DynamoDB()); 12 | 13 | module.exports = { 14 | handler: function(event, context, cb) { 15 | var now = new Date(); 16 | var delay = now - new Date(event.detail.recorded_at); 17 | 18 | // Update the dynamoDB item 19 | var ddbParams = { 20 | TableName: process.env.THINGS_TABLE, 21 | Key: { 22 | "thingName": { 23 | S: event.detail.botId 24 | } 25 | }, 26 | UpdateExpression: "SET #S = :s, #L = :l", 27 | ExpressionAttributeNames: { 28 | "#S": "status", 29 | "#L": "lastSeenAt" 30 | }, 31 | ExpressionAttributeValues: { 32 | ":s": { 33 | S: event.detail.status 34 | }, 35 | ":l": { 36 | N: `${now.getTime()}` 37 | }, 38 | ":tn": { 39 | S: event.detail.botId 40 | } 41 | }, 42 | ConditionExpression: "thingName = :tn", 43 | }; 44 | ddb.updateItem(ddbParams, function(err, data) { 45 | if (err) { 46 | if( err.code == 'ConditionalCheckFailedException') { 47 | data = {thingName: event.botId, exists: false} 48 | cb(null, data) 49 | } else { 50 | cb(err, null); 51 | } 52 | } else { 53 | cb(err, data); 54 | } 55 | }); 56 | 57 | // Send cw custom metric 58 | var params = { 59 | MetricData: [ 60 | { 61 | MetricName: Constants.EVENTS_DELAY_METRIC, 62 | StorageResolution: 1, 63 | Timestamp: now, 64 | Unit: 'Milliseconds', 65 | Value: delay 66 | } 67 | ], 68 | Namespace: Constants.METRICS_NAMESPACE 69 | }; 70 | cw.putMetricData(params, function(err, data) { 71 | if (err) console.log(err, err.stack); 72 | }); 73 | 74 | } 75 | } -------------------------------------------------------------------------------- /backend/src/cwEvents/searchLowBatteryBots.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var CloudWatch = require('aws-sdk/clients/cloudwatch'); 6 | var DynamoDB = require('aws-sdk/clients/dynamodb'); 7 | var Iot = require('aws-sdk/clients/iot'); 8 | var IotData = require('aws-sdk/clients/iotdata'); 9 | 10 | const Util = require('util'); 11 | 12 | AWS.config.region = process.env.AWS_REGION; 13 | var cw = AWSXRay.captureAWSClient(new CloudWatch()); 14 | var ddb = AWSXRay.captureAWSClient(new DynamoDB()); 15 | var iot = AWSXRay.captureAWSClient(new Iot()); 16 | var iotData = null; 17 | 18 | // Idempotent function to fetch the aws iot endpoint ONCE. 19 | function getIotEndpoint(cb) { 20 | if (iotData === null) { 21 | console.log('Fetching iot endpoint ...') 22 | iot.describeEndpoint({}, function(err, data) { 23 | var iotEndpoint = data.endpointAddress; 24 | iotData = AWSXRay.captureAWSClient(new IotData({endpoint: iotEndpoint})); 25 | console.log(`iot endpoint set to ${iotEndpoint}`); 26 | cb(err, data); 27 | }); 28 | } else { 29 | cb(null, {}); 30 | } 31 | } 32 | 33 | function queryLowBateryBots(cb) { 34 | var params = { 35 | "TableName": process.env.TABLE_NAME, 36 | "IndexName": process.env.INDEX_NAME, 37 | "KeyConditionExpression": "lowBatteryDetected = :v", 38 | "ExpressionAttributeValues": { 39 | ":v": {"S": "true"} 40 | }, 41 | "ProjectionExpression": "thingName, batteryLife", 42 | "ScanIndexForward": false 43 | }; 44 | 45 | ddb.query(params, function(err, data) { 46 | cb(err, data); 47 | }); 48 | } 49 | 50 | function putMetricData(value) { 51 | var params = { 52 | MetricData: [ 53 | { 54 | MetricName: process.env.METRIC_NAME, 55 | StorageResolution: 60, 56 | Timestamp: new Date(), 57 | Unit: process.env.METRIC_UNIT, 58 | Value: value 59 | } 60 | ], 61 | Namespace: process.env.METRIC_NAMESPACE 62 | }; 63 | cw.putMetricData(params, function(err, data) { 64 | if (err) console.log(err, err.stack); 65 | }); 66 | } 67 | 68 | function handleLowBatteryBots(bots) { 69 | 70 | // Self-healing action 71 | if(bots.length > 0) { 72 | getIotEndpoint(function(e, d) { 73 | var botsArr = []; 74 | bots.forEach(function(bot) { 75 | var thingName = bot.thingName.S; 76 | var batteryLife = bot.batteryLife.N; 77 | var topic = `myThings/${thingName}/cmds`; 78 | var params = { 79 | topic: topic, 80 | payload: JSON.stringify({cmd: "startCharging"}) 81 | } 82 | iotData.publish(params, function(err, data) { 83 | }); 84 | botsArr.push({ thingName: thingName, batteryLife: batteryLife}); 85 | }); 86 | 87 | // Log to CloudWatch logs 88 | console.log(`Low battery devices: ${Util.inspect(botsArr, false, null)}`); 89 | }); 90 | } 91 | 92 | // put metric data 93 | putMetricData(bots.length); 94 | } 95 | 96 | module.exports = { 97 | handler: function(event, context, cb) { 98 | queryLowBateryBots( function(err, data) { 99 | if(err) console.log(err, err.stack); 100 | if(data) { 101 | handleLowBatteryBots(data.Items); 102 | } 103 | }); 104 | } 105 | } -------------------------------------------------------------------------------- /backend/src/dashboards/bots-operations.js: -------------------------------------------------------------------------------- 1 | var dashboard = { 2 | "widgets": [ 3 | { 4 | "type": "metric", 5 | "x": 12, 6 | "y": 1, 7 | "width": 12, 8 | "height": 6, 9 | "properties": { 10 | "metrics": [ 11 | [ "${self:custom.backend.metricNameSpace}", "batteryLife", "bot", "bot1" ], 12 | [ "...", "bot2" ], 13 | [ "...", "bot3" ] 14 | ], 15 | "view": "timeSeries", 16 | "stacked": false, 17 | "region": "#{AWS::Region}", 18 | "stat": "Average", 19 | "period": 1, 20 | "yAxis": { 21 | "left": { 22 | "min": 0, 23 | "max": 100 24 | } 25 | }, 26 | "annotations": { 27 | "horizontal": [ 28 | { 29 | "label": "Threshold", 30 | "value": 15, 31 | "fill": "below" 32 | } 33 | ] 34 | }, 35 | "title": "BatteryLife" 36 | } 37 | }, 38 | { 39 | "type": "metric", 40 | "x": 0, 41 | "y": 1, 42 | "width": 12, 43 | "height": 6, 44 | "properties": { 45 | "title": "LowBatteryBotsCountAlarm", 46 | "annotations": { 47 | "alarms": [ 48 | "arn:aws:cloudwatch:#{AWS::Region}:#{AWS::AccountId}:alarm:${self:custom.backend.lowBatteryBotsCountAlarm}" 49 | ] 50 | }, 51 | "view": "timeSeries", 52 | "stacked": true 53 | } 54 | }, 55 | { 56 | "type": "text", 57 | "x": 0, 58 | "y": 0, 59 | "width": 24, 60 | "height": 1, 61 | "properties": { 62 | "markdown": "\n# Bots Operations\n" 63 | } 64 | }, 65 | { 66 | "type": "text", 67 | "x": 0, 68 | "y": 13, 69 | "width": 24, 70 | "height": 1, 71 | "properties": { 72 | "markdown": "\n# Custom Backend Operations\n" 73 | } 74 | }, 75 | { 76 | "type": "metric", 77 | "x": 0, 78 | "y": 21, 79 | "width": 12, 80 | "height": 6, 81 | "properties": { 82 | "metrics": [ 83 | [ "${self:custom.backend.metricNameSpace}", "eventsDelay" ] 84 | ], 85 | "view": "timeSeries", 86 | "stacked": false, 87 | "region": "#{AWS::Region}", 88 | "stat": "Maximum", 89 | "period": 300 90 | } 91 | }, 92 | { 93 | "type": "metric", 94 | "x": 12, 95 | "y": 14, 96 | "width": 12, 97 | "height": 6, 98 | "properties": { 99 | "metrics": [ 100 | [ "${self:custom.backend.metricNameSpace}", "telemetryDelay", "bot", "bot1" ], 101 | [ "...", "bot2" ], 102 | [ "...", "bot3" ] 103 | ], 104 | "view": "timeSeries", 105 | "stacked": false, 106 | "region": "#{AWS::Region}", 107 | "period": 300, 108 | "stat": "Maximum" 109 | } 110 | }, 111 | { 112 | "type": "metric", 113 | "x": 0, 114 | "y": 14, 115 | "width": 12, 116 | "height": 6, 117 | "properties": { 118 | "metrics": [ 119 | [ "${self:custom.backend.metricNameSpace}", "telemetryPacketSize", "bot", "bot1" ], 120 | [ "...", "bot2" ], 121 | [ "...", "bot3" ] 122 | ], 123 | "view": "timeSeries", 124 | "stacked": false, 125 | "region": "#{AWS::Region}", 126 | "period": 30, 127 | "stat": "Maximum" 128 | } 129 | }, 130 | { 131 | "type": "text", 132 | "x": 0, 133 | "y": 20, 134 | "width": 24, 135 | "height": 1, 136 | "properties": { 137 | "markdown": "\n# CloudWatch Events Benchmarking\n" 138 | } 139 | }, 140 | { 141 | "type": "text", 142 | "x": 0, 143 | "y": 27, 144 | "width": 24, 145 | "height": 1, 146 | "properties": { 147 | "markdown": "\n# AWS Services Metrics\n" 148 | } 149 | }, 150 | { 151 | "type": "metric", 152 | "x": 0, 153 | "y": 7, 154 | "width": 12, 155 | "height": 6, 156 | "properties": { 157 | "metrics": [ 158 | [ "AWS/Events", "Invocations", "RuleName", "acme-bots-search-low-battery-bots" ], 159 | [ ".", "TriggeredRules", ".", "." ], 160 | [ "AWS/Lambda", "Invocations", "FunctionName", "acme-bots-dev-searchLowBatteryBots" ] 161 | ], 162 | "view": "timeSeries", 163 | "stacked": true, 164 | "region": "#{AWS::Region}", 165 | "period": 60, 166 | "title": "Search Low Battery Bots Scheduled Events", 167 | "stat": "Average" 168 | } 169 | } 170 | ] 171 | }; 172 | 173 | module.exports.dashboard = () => { 174 | return JSON.stringify(dashboard); 175 | } 176 | -------------------------------------------------------------------------------- /backend/src/iot/attachCertToThing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var iot = AWSXRay.captureAWSClient(new Iot()); 9 | 10 | 11 | module.exports = { 12 | attachCertToThing: function (event, context, cb) { 13 | var params = { principal: event.certificateArn, thingName: event.thingName }; 14 | iot.attachThingPrincipal(params, function (err, data) { 15 | if (err) { 16 | cb(err, data); 17 | } else { 18 | cb(null, event); 19 | } 20 | }); 21 | } 22 | } -------------------------------------------------------------------------------- /backend/src/iot/attachPolicyToCert.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var iot = AWSXRay.captureAWSClient(new Iot()); 9 | 10 | module.exports = { 11 | attachPolicyToCert: function(event, context, cb) { 12 | var params = { policyName: event.policyName, target: event.certificateArn }; 13 | iot.attachPolicy(params, function (err, data) { 14 | if (err) { 15 | cb(err, null); 16 | } else { 17 | cb(null, event); 18 | } 19 | }); 20 | } 21 | } -------------------------------------------------------------------------------- /backend/src/iot/checkIfThingExists.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var iot = AWSXRay.captureAWSClient(new Iot()); 9 | 10 | module.exports = { 11 | checkIfThingExists: function (event, context, cb) { 12 | var params = { thingName: event.thingName }; 13 | iot.describeThing(params, function (err, data) { 14 | if (err) { 15 | if( err.code == 'ResourceNotFoundException') { 16 | data = {thingName: event.thingName, exists: false} 17 | cb(null, data) 18 | } else { 19 | cb(err, null); 20 | } 21 | } else if (err === null && data.thingName === event.thingName) { 22 | data = {thingName: event.thingName, exists: true} 23 | cb(null, data) 24 | } 25 | }); 26 | } 27 | } -------------------------------------------------------------------------------- /backend/src/iot/checkProvisioning.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var DynamoDB = require('aws-sdk/clients/dynamodb'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var ddb = AWSXRay.captureAWSClient(new DynamoDB()); 9 | 10 | module.exports = { 11 | checkProvisioning: function(event, context, cb) { 12 | var data = { 13 | thingName: event[0].thingName, 14 | certificateId: event[1].certificateId, 15 | certificateArn: event[1].certificateArn, 16 | policyName: event[2].policyName 17 | }; 18 | 19 | // Write to dynamoDB table 20 | var ddbParams = { 21 | TableName: process.env.THINGS_TABLE, 22 | Item: { 23 | 'thingName': {S: data.thingName}, 24 | 'certificateId': {S: data.certificateId}, 25 | 'certificateArn': {S: data.certificateArn}, 26 | 'policyName': {S: data.policyName} 27 | } 28 | }; 29 | ddb.putItem(ddbParams, function(err, resp1) { 30 | cb(err, data) 31 | }); 32 | } 33 | } -------------------------------------------------------------------------------- /backend/src/iot/checkThingAttachments.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var Iot = require('aws-sdk/clients/iot'); 5 | 6 | AWS.config.region = process.env.AWS_REGION; 7 | var iot = new Iot(); 8 | 9 | module.exports = { 10 | checkThingAttachments: function (event, context, cb) { 11 | cb(null, event[0]); 12 | } 13 | } -------------------------------------------------------------------------------- /backend/src/iot/createKeysAndCert.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | var S3 = require('aws-sdk/clients/s3'); 7 | 8 | AWS.config.region = process.env.AWS_REGION; 9 | var iot = AWSXRay.captureAWSClient(new Iot()); 10 | var s3 = AWSXRay.captureAWSClient(new S3()); 11 | 12 | module.exports = { 13 | createKeysAndCert: function (event, context, cb) { 14 | var params = { setAsActive: true }; 15 | iot.createKeysAndCertificate(params, function (err, data) { 16 | if (err) { 17 | cb(err, null); 18 | } else { 19 | var respdata = { 20 | certificateId: data.certificateId, 21 | certificateArn: data.certificateArn 22 | } 23 | 24 | // write to S3 25 | var bucketName = process.env.S3_BUCKET; 26 | var s3_params = { 27 | Body:JSON.stringify(data, null, 4), 28 | Bucket: bucketName, 29 | Key: data.certificateId 30 | }; 31 | s3.putObject(s3_params, function(err, data2) { 32 | cb(err, respdata); 33 | }); 34 | } 35 | }); 36 | } 37 | } -------------------------------------------------------------------------------- /backend/src/iot/createPolicy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | var myUtils = require('./utils'); 7 | 8 | var region = process.env.AWS_REGION; 9 | AWS.config.region = region; 10 | var iot = AWSXRay.captureAWSClient(new Iot()); 11 | 12 | const POLICY_DOCUMENT = myUtils.readFile(`${__dirname}/policy.doc`); 13 | 14 | module.exports = { 15 | createPolicy: function (event, context, cb) { 16 | var awsAccountId = context.invokedFunctionArn.match(/\d{3,}/)[0]; 17 | var policyDoc = POLICY_DOCUMENT 18 | .replace(//g, region) 19 | .replace(//g, awsAccountId) 20 | .replace(//g, event.thingName); 21 | var params = { policyName: event.thingName, policyDocument: policyDoc }; 22 | iot.createPolicy(params, function (err, data) { 23 | if (err) { 24 | cb(err, null); 25 | } else { 26 | cb(null, {policyName: data.policyName}); 27 | } 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /backend/src/iot/createThing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var iot = AWSXRay.captureAWSClient(new Iot()); 9 | 10 | module.exports = { 11 | createThing: function (event, context, cb) { 12 | var params = { thingName: event.thingName }; 13 | iot.createThing(params, function (err, data) { 14 | if (err) { 15 | cb(err, null); 16 | } else { 17 | cb(null, {thingName: data.thingName} ); 18 | } 19 | }); 20 | } 21 | } -------------------------------------------------------------------------------- /backend/src/iot/deleteCert.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | var S3 = require('aws-sdk/clients/s3'); 7 | 8 | AWS.config.region = process.env.AWS_REGION; 9 | var iot = AWSXRay.captureAWSClient(new Iot()); 10 | var s3 = AWSXRay.captureAWSClient(new S3()); 11 | 12 | module.exports = { 13 | deleteCert: function (event, context, cb) { 14 | var params = { certificateId: event.certificateId, forceDelete: true }; 15 | iot.deleteCertificate(params, function (err, data) { 16 | if(err) { 17 | cb(err, null) 18 | } else { 19 | var s3_params = { 20 | Key: event.certificateId, 21 | Bucket: process.env.S3_BUCKET 22 | }; 23 | s3.deleteObject(s3_params, function(err, s3Data) { 24 | console.log(`Deleting ${event.certificateId} from S3`); 25 | cb(err, s3Data); 26 | }); 27 | } 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /backend/src/iot/deleteMetadata.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var DynamoDB = require('aws-sdk/clients/dynamodb'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var ddb = AWSXRay.captureAWSClient(new DynamoDB()); 9 | 10 | module.exports = { 11 | deleteMetadata: function (event, context, cb) { 12 | var ddbParams = { 13 | Key: { "thingName": { S: event.thingName} }, 14 | TableName: process.env.THINGS_TABLE 15 | }; 16 | console.log(`Deleting ${event.thingName} item from DDB.`); 17 | ddb.deleteItem(ddbParams, function(err, data) { 18 | cb(err, data); 19 | }); 20 | } 21 | } -------------------------------------------------------------------------------- /backend/src/iot/deletePolicy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var iot = AWSXRay.captureAWSClient(new Iot()); 9 | 10 | module.exports = { 11 | deletePolicy: function (event, context, cb) { 12 | var params = { policyName: event.policyName }; 13 | iot.deletePolicy(params, function (err, data) { 14 | cb(err, data); 15 | }); 16 | } 17 | } -------------------------------------------------------------------------------- /backend/src/iot/deleteThing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var iot = AWSXRay.captureAWSClient(new Iot()); 9 | 10 | module.exports = { 11 | deleteThing: function (event, context, cb) { 12 | var params = { thingName: event.thingName }; 13 | iot.deleteThing(params, function (err, data) { 14 | if (err) { 15 | cb(err, null) 16 | } else { 17 | cb(null, {"thingName": event.thingName}); 18 | } 19 | }); 20 | } 21 | } -------------------------------------------------------------------------------- /backend/src/iot/detachCertFromThing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var iot = AWSXRay.captureAWSClient(new Iot()); 9 | 10 | module.exports = { 11 | detachCertFromThing: function (event, context, cb) { 12 | var params = { principal: event.certificateArn, thingName: event.thingName }; 13 | iot.detachThingPrincipal(params, function (err, data) { 14 | cb(err, event); 15 | }); 16 | } 17 | } -------------------------------------------------------------------------------- /backend/src/iot/detachPolicyFromCert.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var iot = AWSXRay.captureAWSClient(new Iot()); 9 | 10 | module.exports = { 11 | detachPolicyFromCert: function (event, context, cb) { 12 | var params = { policyName: event.policyName, target: event.certificateArn }; 13 | iot.detachPolicy(params, function (err, data) { 14 | cb(err, event); 15 | }); 16 | } 17 | } -------------------------------------------------------------------------------- /backend/src/iot/disableCert.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var Iot = require('aws-sdk/clients/iot'); 6 | var DynamoDB = require('aws-sdk/clients/dynamodb'); 7 | 8 | AWS.config.region = process.env.AWS_REGION; 9 | var iot = AWSXRay.captureAWSClient(new Iot()); 10 | var ddb = AWSXRay.captureAWSClient(new DynamoDB()); 11 | 12 | const INACTIVE_STATUS = 'INACTIVE'; 13 | 14 | module.exports = { 15 | disableCert: function (event, context, cb) { 16 | var ddbParams = { 17 | Key: { 18 | "thingName": { S: event.thingName } 19 | }, 20 | TableName: process.env.THINGS_TABLE 21 | }; 22 | ddb.getItem(ddbParams, function(err, ddbData){ 23 | if(err) { 24 | cb(err, null) 25 | } else { 26 | var outputParams = { 27 | thingName: ddbData.Item.thingName.S, 28 | certificateArn: ddbData.Item.certificateArn.S, 29 | certificateId: ddbData.Item.certificateId.S, 30 | policyName: ddbData.Item.policyName.S 31 | }; 32 | 33 | var params = { 34 | certificateId: outputParams.certificateId, 35 | newStatus: INACTIVE_STATUS 36 | }; 37 | iot.updateCertificate(params, function (err, data) { 38 | cb(err, outputParams); 39 | }); 40 | } 41 | }); 42 | } 43 | } -------------------------------------------------------------------------------- /backend/src/iot/policy.doc: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [{ 4 | "Effect": "Allow", 5 | "Action":["iot:Connect"], 6 | "Resource": ["*"] 7 | },{ 8 | "Effect": "Allow", 9 | "Action":[ 10 | "iot:Publish" 11 | ], 12 | "Resource": [ 13 | "arn:aws:iot:::topic/myThings//telemetry", 14 | "arn:aws:iot:::topic/myThings//cmds/ack" 15 | ] 16 | },{ 17 | "Effect": "Allow", 18 | "Action":[ 19 | "iot:Subscribe" 20 | ], 21 | "Resource": [ 22 | "arn:aws:iot:::topicfilter/myThings//cmds" 23 | ] 24 | },{ 25 | "Effect": "Allow", 26 | "Action":[ 27 | "iot:Receive" 28 | ], 29 | "Resource": [ 30 | "arn:aws:iot:::topic/myThings//cmds" 31 | ] 32 | }] 33 | } -------------------------------------------------------------------------------- /backend/src/iot/utils.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | class MyUtils { 4 | 5 | readFile(path) { 6 | return fs.readFileSync(path, 'utf8'); 7 | } 8 | 9 | readJsonFile(path) { 10 | var str = this.readFile(path); 11 | return JSON.parse(str); 12 | } 13 | 14 | } 15 | module.exports = new (MyUtils) -------------------------------------------------------------------------------- /backend/src/iotRules/constants.js: -------------------------------------------------------------------------------- 1 | const EVENTS_PREFIX = 'acmebots'; 2 | 3 | module.exports = { 4 | EVENTS_PREFIX : EVENTS_PREFIX, 5 | CONNECTIVITY_EVENT_SOURCE: `${EVENTS_PREFIX}.connectivity`, 6 | STATUS_EVENT_SOURCE : `${EVENTS_PREFIX}.status`, 7 | 8 | METRICS_NAMESPACE : 'AcmeBots', 9 | TELEMETRY_DELAY_METRIC : 'telemetryDelay', 10 | TELEMETRY_PACKAGE_SIZE_METRIC: 'telemetryPacketSize', 11 | BATTERY_LIFE_METRIC : 'batteryLife', 12 | } -------------------------------------------------------------------------------- /backend/src/iotRules/handleTelemetryData.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var CloudWatch = require('aws-sdk/clients/cloudwatch'); 6 | var CloudWatchEvents = require('aws-sdk/clients/cloudwatchevents'); 7 | var DynamoDB = require('aws-sdk/clients/dynamodb'); 8 | var jsonSize = require('json-size'); 9 | var Constants = require(`${__dirname}/constants`); 10 | 11 | AWS.config.region = process.env.AWS_REGION; 12 | var cw = AWSXRay.captureAWSClient(new CloudWatch()); 13 | var cwe = AWSXRay.captureAWSClient(new CloudWatchEvents()); 14 | var ddb = AWSXRay.captureAWSClient(new DynamoDB()); 15 | 16 | module.exports = { 17 | handleTelemetryData: function(event, context, cb) { 18 | var now = new Date(); 19 | var last_recorded_at = null; 20 | var last_status = null; 21 | var version = null; 22 | var last_datapoint = null; 23 | var telemetry = event.telemetry || []; 24 | var params = { 25 | MetricData: [], 26 | Namespace: Constants.METRICS_NAMESPACE 27 | }; 28 | telemetry.forEach(function(datapoint){ 29 | last_recorded_at = new Date(datapoint.recorded_at); 30 | last_status = datapoint.status; 31 | version = datapoint.version; 32 | last_datapoint = datapoint; 33 | var metricData = { 34 | MetricName: Constants.BATTERY_LIFE_METRIC, 35 | Dimensions: [ 36 | { 37 | Name: 'bot', 38 | Value: event.dimension 39 | } 40 | ], 41 | StorageResolution: 1, 42 | Timestamp: last_recorded_at, 43 | Unit: 'Percent', 44 | Value: datapoint.batteryLife 45 | } 46 | params.MetricData.push(metricData); 47 | }); 48 | 49 | params.MetricData.push({ 50 | MetricName: Constants.TELEMETRY_DELAY_METRIC, 51 | Dimensions: [ 52 | { 53 | Name: 'bot', 54 | Value: event.dimension 55 | } 56 | ], 57 | StorageResolution: 1, 58 | Timestamp: now, 59 | Unit: 'Milliseconds', 60 | Value: now - last_recorded_at 61 | }); 62 | 63 | params.MetricData.push({ 64 | MetricName: Constants.TELEMETRY_PACKAGE_SIZE_METRIC, 65 | Dimensions: [ 66 | { 67 | Name: 'bot', 68 | Value: event.dimension 69 | } 70 | ], 71 | StorageResolution: 1, 72 | Timestamp: now, 73 | Unit: 'Bytes', 74 | Value: jsonSize(event.telemetry) 75 | }); 76 | 77 | cw.putMetricData(params, function(err, data) { 78 | if (err) console.log(err, err.stack); 79 | }); 80 | 81 | // Update the dynamoDB item 82 | var expressionAttributeValues = { 83 | ":s": { 84 | S: last_status 85 | }, 86 | ":l": { 87 | N: `${last_recorded_at.getTime()}` 88 | }, 89 | ":b": { 90 | N: `${last_datapoint.batteryLife}` 91 | }, 92 | ":v": { 93 | S: version 94 | }, 95 | ":tn": { 96 | S: event.dimension 97 | } 98 | } 99 | var updateExpression = "SET #S = :s, #L = :l, #B = :b, #V = :v"; 100 | var expressionAttributeNames= { 101 | "#S": "status", 102 | "#L": "lastSeenAt", 103 | "#B": "batteryLife", 104 | "#V": "version" 105 | } 106 | if(last_datapoint.batteryLife < 10.0) { 107 | expressionAttributeValues[":lbd"] = { S: "true" }; 108 | updateExpression = updateExpression.concat(", #LBD = :lbd"); 109 | expressionAttributeNames["#LBD"] = "lowBatteryDetected"; 110 | } else { 111 | updateExpression = updateExpression.concat(" REMOVE lowBatteryDetected"); 112 | } 113 | var ddbParams = { 114 | TableName: process.env.THINGS_TABLE, 115 | Key: { 116 | "thingName": { 117 | S: event.dimension 118 | } 119 | }, 120 | UpdateExpression: updateExpression, 121 | ExpressionAttributeNames: expressionAttributeNames, 122 | ExpressionAttributeValues: expressionAttributeValues, 123 | ConditionExpression: "thingName = :tn", 124 | }; 125 | ddb.updateItem(ddbParams, function(err, data) { 126 | if (err) { 127 | if( err.code == 'ConditionalCheckFailedException') { 128 | data = {thingName: event.clientId, exists: false} 129 | cb(null, data) 130 | } else { 131 | cb(err, null); 132 | } 133 | } else { 134 | cb(err, data); 135 | } 136 | }); 137 | 138 | // raise cw status event 139 | last_datapoint.botId = event.dimension; 140 | var cweParams = { 141 | Entries: [ 142 | { 143 | Detail: JSON.stringify(last_datapoint), 144 | DetailType: 'AcmeBot Bot Status', 145 | Resources: [], 146 | Source: Constants.STATUS_EVENT_SOURCE, 147 | Time: last_recorded_at 148 | } 149 | ] 150 | }; 151 | cwe.putEvents(cweParams, function(err, data) { 152 | if (err) console.log(err, err.stack); 153 | }); 154 | 155 | } 156 | } -------------------------------------------------------------------------------- /backend/src/iotRules/trackConnectivity.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var CloudWatchEvents = require('aws-sdk/clients/cloudwatchevents'); 6 | var Constants = require(`${__dirname}/constants`); 7 | 8 | AWS.config.region = process.env.AWS_REGION; 9 | var cwe = AWSXRay.captureAWSClient(new CloudWatchEvents()); 10 | 11 | module.exports = { 12 | trackConnectivity: function(event, context, cb) { 13 | 14 | var params = { 15 | Entries: [ 16 | { 17 | Detail: JSON.stringify(event), 18 | DetailType: 'AcmeBot Bot Connectivity Status', 19 | Resources: [], 20 | Source: Constants.CONNECTIVITY_EVENT_SOURCE, 21 | Time: new Date(event.timestamp) 22 | } 23 | ] 24 | }; 25 | cwe.putEvents(params, function(err, data) { 26 | if (err) console.log(err, err.stack); 27 | }); 28 | } 29 | } -------------------------------------------------------------------------------- /backend/src/step-functions/provisionThing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var StepFunctions = require('aws-sdk/clients/stepfunctions'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var stepfunctions = AWSXRay.captureAWSClient(new StepFunctions()); 9 | 10 | module.exports = { 11 | provisionThing: function (event, context, cb) { 12 | var params = { 13 | stateMachineArn: process.env.STATE_MACHINE_ARN, 14 | input: JSON.stringify({thingName: event.thingName}) 15 | }; 16 | stepfunctions.startExecution(params, function (err, data) { 17 | if (err) { 18 | cb(err, null); 19 | } else { 20 | cb(null, data); 21 | } 22 | }); 23 | } 24 | } -------------------------------------------------------------------------------- /backend/src/step-functions/removeThing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk/global'); 4 | var AWSXRay = require('aws-xray-sdk'); 5 | var StepFunctions = require('aws-sdk/clients/stepfunctions'); 6 | 7 | AWS.config.region = process.env.AWS_REGION; 8 | var stepfunctions = AWSXRay.captureAWSClient(new StepFunctions()); 9 | 10 | module.exports = { 11 | removeThing: function (event, context, cb) { 12 | var params = { 13 | stateMachineArn: process.env.STATE_MACHINE_ARN, 14 | input: JSON.stringify({thingName: event.thingName}) 15 | }; 16 | stepfunctions.startExecution(params, function (err, data) { 17 | if (err) { 18 | cb(err, null); 19 | } else { 20 | cb(null, data); 21 | } 22 | }); 23 | } 24 | } -------------------------------------------------------------------------------- /bots/Dockerfile: -------------------------------------------------------------------------------- 1 | # Create image based on the official Node 8 image from docker hub 2 | FROM node:8 3 | 4 | # Create a directory where our app will be placed 5 | RUN mkdir -p /usr/src/app 6 | 7 | # Change directory so that our commands run inside this new directory 8 | WORKDIR /usr/src/app 9 | 10 | # Copy source code 11 | COPY . /usr/src/app 12 | 13 | # Install dependecies 14 | RUN npm install 15 | 16 | # Run Setup / Config 17 | # RUN node scripts/setup.js 18 | 19 | # Make run_thing.sh executable 20 | RUN chmod 755 run_thing.sh 21 | 22 | # Expose the MQTT port 23 | EXPOSE 8883 24 | 25 | # Run the bot (name passed from environment variable $THING_NAME) 26 | #CMD ["node", "example/Main.js", "$THING_NAME"] 27 | CMD ["./run_thing.sh"] 28 | -------------------------------------------------------------------------------- /bots/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | pre_build: 5 | commands: 6 | - echo Logging in to Amazon ECR... 7 | - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION) 8 | build: 9 | commands: 10 | - echo Build started on `date` 11 | - echo Building the Docker image... 12 | - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG bots/ 13 | - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG 14 | post_build: 15 | commands: 16 | - echo Build completed on `date` 17 | - echo Pushing the Docker image... 18 | - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG 19 | -------------------------------------------------------------------------------- /bots/example.env: -------------------------------------------------------------------------------- 1 | # Example Variables for docker file 2 | REGION=us-east-1 3 | SERVICE=acme-bots 4 | STAGE=dev 5 | THING_NAME=Thing1 6 | -------------------------------------------------------------------------------- /bots/example/Main.js: -------------------------------------------------------------------------------- 1 | var Bot = require('../src/bot'); 2 | 3 | DEFAULT_THING_NAME = 'Thing1'; 4 | DEFAULT_VERSION = '2.0'; 5 | 6 | var thingName = DEFAULT_THING_NAME; 7 | if (process.argv.length > 2) { 8 | thingName = process.argv[2]; 9 | } 10 | 11 | var version = DEFAULT_VERSION; 12 | if (process.argv.length > 3) { 13 | version = process.argv[3]; 14 | } 15 | 16 | params = {thingName: thingName, version: version}; 17 | var bot = new Bot(params); -------------------------------------------------------------------------------- /bots/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iot-demo-devices", 3 | "description": "", 4 | "version": "0.1.0", 5 | "dependencies": { 6 | "aws-iot-device-sdk": "^2.2.6", 7 | "aws-sdk": "^2.265.1", 8 | "js-yaml": "^3.13.1", 9 | "winston": "^3.1.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bots/run_thing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Ensure Variables are set 4 | if [ -z $REGION ]; then echo "REGION is not set"; exit 1; fi 5 | if [ -z $SERVICE ]; then echo "SERVICE is not set"; exit 1; fi 6 | if [ -z $STAGE ]; then echo "STAGE is not set"; exit 1; fi 7 | if [ -z $THING_NAME ]; then echo "THING_NAME is not set"; exit 1; fi 8 | 9 | # Create config file 10 | node scripts/env-setup.js 11 | 12 | # Run the thing 13 | node example/Main.js $THING_NAME 14 | -------------------------------------------------------------------------------- /bots/scripts/clear-ecr-repo.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var yaml = require('js-yaml'); 5 | var AWS = require('aws-sdk'); 6 | 7 | const SERVERLESS_FILE_PATH = path.normalize(`${path.resolve(__dirname)}/../../backend/serverless.yml`); 8 | var backend_config_yaml_doc = null; 9 | var region = null; 10 | var images = null; 11 | 12 | AWS.config.region = getBackEndRegion(); 13 | const repoName = getRepoName(); 14 | 15 | console.log("Looking for images in", repoName); 16 | 17 | var ecr = new AWS.ECR(); 18 | 19 | var params = { 20 | repositoryName: repoName 21 | }; 22 | ecr.listImages(params, function(err, data) { 23 | if (err) console.log(err, err.stack); // an error occurred 24 | else { 25 | console.log("Images listed: ", data); // successful response 26 | if (JSON.stringify(data.imageIds) === '[]') { 27 | console.log("No images found"); 28 | } else { 29 | images = { 30 | repositoryName: repoName, 31 | imageIds: data.imageIds 32 | }; 33 | console.log("Deleting Images..."); 34 | // Delete images 35 | ecr.batchDeleteImage(images, function(err, data) { 36 | if (err) console.log(err, err.stack); // an error occurred 37 | else console.log(data); // successful response 38 | }); 39 | } 40 | } 41 | }); 42 | 43 | 44 | function getRepoName() { 45 | if (backend_config_yaml_doc === null) { 46 | var contents = fs.readFileSync(SERVERLESS_FILE_PATH,'utf8'); 47 | backend_config_yaml_doc = yaml.safeLoad(contents); 48 | } 49 | var service = backend_config_yaml_doc['service']; 50 | var stage = backend_config_yaml_doc['provider']['stage'] || 'dev'; 51 | return `${service}-${stage}`; 52 | } 53 | 54 | function getBackEndRegion() { 55 | if (region === null) { 56 | if (backend_config_yaml_doc === null) { 57 | var contents = fs.readFileSync(SERVERLESS_FILE_PATH,'utf8'); 58 | backend_config_yaml_doc = yaml.safeLoad(contents); 59 | } 60 | region = backend_config_yaml_doc['provider']['region'] || 'us-east-1'; 61 | } 62 | return region; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /bots/scripts/clear-ecs-cluster.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var yaml = require('js-yaml'); 5 | var AWS = require('aws-sdk'); 6 | 7 | const SERVERLESS_FILE_PATH = path.normalize(`${path.resolve(__dirname)}/../../backend/serverless.yml`); 8 | var backend_config_yaml_doc = null; 9 | var region = null; 10 | var tasks = null; 11 | var taskDefinitions = null; 12 | 13 | var blankArray = new Object(); 14 | blankArray = []; 15 | 16 | var backend_config_yaml_doc = null; 17 | 18 | console.log('Reading backend configuration file ...'); 19 | 20 | AWS.config.region = getBackEndRegion(); 21 | 22 | var cfn = new AWS.CloudFormation(); 23 | var stackName = getBackEndStackName(); 24 | 25 | var clusterNamePromise = getClusterName(cfn, stackName); 26 | 27 | var taskRoleArnPromise = getTaskRoleArn(cfn, stackName); 28 | 29 | var ecs = new AWS.ECS(); 30 | 31 | clusterNamePromise.then(function(result) { 32 | 33 | clusterName = find(result.Stacks[0].Outputs, "ECSClusterName"); 34 | console.log("Found cluster name: ", clusterName); 35 | 36 | var tasks = listTasks(ecs, clusterName); 37 | 38 | tasks.then(function(result) { 39 | if (JSON.stringify(result.taskArns) === JSON.stringify(blankArray)) { 40 | console.log("No tasks exist in cluster", clusterName); 41 | console.log("listTasks API returned", result); 42 | } else { 43 | console.log("Task(s) found for cluster ", clusterName, ". DELETING"); 44 | console.log(result); 45 | 46 | var stop = []; 47 | for (var i = 0, len = result.taskArns.length; i < len; i++) { 48 | stop.push(stopTask(ecs, clusterName, result.taskArns[i])); 49 | } 50 | 51 | Promise.all(stop) 52 | .then(function(result) { 53 | for (var i = 0, len = result.length; i < len; i++) { 54 | console.log("Stopped Task", result[i].task.taskArn); 55 | } 56 | }) 57 | .catch(function(err) { 58 | console.log("ERROR: stopTask API call failed!"); 59 | console.log(err); 60 | }); 61 | } 62 | }, function(err) { 63 | console.log("ERROR: listTasks API call failed!"); 64 | console.log(err); 65 | }); 66 | }) 67 | .catch(function(err) { 68 | console.log("ERROR: getClusterName API call failed!"); 69 | console.log(err); 70 | }); 71 | 72 | taskRoleArnPromise.then(function(result) { 73 | taskRoleArn = find(result.Stacks[0].Outputs, "ECSTaskRoleArn"); 74 | console.log("Found task role arn: ", taskRoleArn) 75 | 76 | var taskDefinitions = listActiveTaskDefinitions(ecs); 77 | 78 | taskDefinitions.then(function(result) { 79 | if (JSON.stringify(result.taskDefinitionArns) === JSON.stringify(blankArray)) { 80 | console.log("No task definitions found with Role Arn ", taskRoleArn); 81 | console.log("listTaskDefinitions API returned", result); 82 | } else { 83 | console.log("Task Definition(s) found. DELETING"); 84 | console.log(result); 85 | 86 | var descTaskDef = []; 87 | for (var i = 0, len = result.taskDefinitionArns.length; i < len; i++) { 88 | descTaskDef.push(describeTaskDefinition(ecs, result.taskDefinitionArns[i])); 89 | } 90 | 91 | Promise.all(descTaskDef) 92 | .then(function(result) { 93 | var deregister = []; 94 | 95 | for (var i = 0, len = result.length; i < len; i++) { 96 | console.log(result[i].taskDefinition.taskRoleArn, " = ", taskRoleArn); 97 | 98 | if ( result[i].taskDefinition.taskRoleArn === taskRoleArn ) { 99 | deregister.push(deregisterTaskDefinition(ecs, result[i].taskDefinition.taskDefinitionArn)); 100 | }; 101 | 102 | Promise.all(deregister) 103 | .then(function(result) { 104 | for (var i = 0, len = result.length; i < len; i++) { 105 | console.log("Deregistered Task Definition", result[i].taskDefinition.taskDefinitionArn); 106 | } 107 | }) 108 | .catch(function(err) { 109 | console.log("ERROR: deregisterTaskDefinition API call failed!"); 110 | console.log(err); 111 | }); 112 | 113 | } 114 | }) 115 | .catch(function(err) { 116 | console.log("ERROR: describeTaskDefinition API call failed!"); 117 | console.log(err); 118 | }); 119 | 120 | } 121 | }, function(err) { 122 | console.log("ERROR: listTaskDefinitions API call failed!"); 123 | console.log(err); 124 | }); 125 | 126 | }) 127 | .catch(function(err) { 128 | console.log("ERROR: getTaskRoleArn API call failed!"); 129 | console.log(err); 130 | }); 131 | 132 | function listTasks(client, taskCluster) { 133 | return new Promise(function (resolve, reject){ 134 | client.listTasks({cluster: taskCluster}, function (err, res){ 135 | if (err) reject(err); 136 | else resolve(res); 137 | }); 138 | }); 139 | } 140 | 141 | function stopTask(client, taskCluster, taskArn) { 142 | return new Promise(function (resolve, reject){ 143 | console.log("stopTask called for", taskArn); 144 | client.stopTask({task: taskArn, cluster: taskCluster}, function (err, res){ 145 | if (err) reject(err); 146 | else resolve(res); 147 | }); 148 | }); 149 | } 150 | 151 | function listActiveTaskDefinitions(client) { 152 | return new Promise(function (resolve, reject){ 153 | client.listTaskDefinitions({status: "ACTIVE"}, function (err, res){ 154 | if (err) reject(err); 155 | else resolve(res); 156 | }); 157 | }); 158 | } 159 | 160 | function describeTaskDefinition(client, taskDefinition) { 161 | return new Promise(function (resolve, reject){ 162 | client.describeTaskDefinition({taskDefinition: taskDefinition}, function (err, res){ 163 | if (err) reject(err); 164 | else resolve(res); 165 | }); 166 | }); 167 | } 168 | 169 | function deregisterTaskDefinition(client, taskDefArn) { 170 | return new Promise(function (resolve, reject){ 171 | console.log("deregisterTaskDefinition called for", taskDefArn); 172 | client.deregisterTaskDefinition({taskDefinition: taskDefArn}, function (err, res){ 173 | if (err) reject(err); 174 | else resolve(res); 175 | }); 176 | }); 177 | } 178 | 179 | function getClusterName(cfn, stackName) { 180 | return new Promise(function (resolve, reject){ 181 | console.log(`Parsing ECSClusterName from CloudFormation stack: ${stackName} ...`); 182 | cfn.describeStacks({StackName: stackName}, function(err, res) { 183 | if (err) reject(err); 184 | else resolve(res); 185 | }); 186 | }); 187 | //var outputs = data.Stacks[0].Outputs; 188 | //return find(outputs, "ECSClusterName"); 189 | } 190 | 191 | function getTaskRoleArn(cfn, stackName) { 192 | return new Promise(function (resolve, reject){ 193 | console.log(`Parsing ECSTaskRoleArn from CloudFormation stack: ${stackName} ...`); 194 | cfn.describeStacks({StackName: stackName}, function(err, res) { 195 | if (err) reject(err); 196 | else resolve(res); 197 | }); 198 | }); 199 | } 200 | 201 | function getBackEndStackName() { 202 | if (backend_config_yaml_doc === null) { 203 | var contents = fs.readFileSync(SERVERLESS_FILE_PATH,'utf8'); 204 | backend_config_yaml_doc = yaml.safeLoad(contents); 205 | } 206 | var service = backend_config_yaml_doc['service']; 207 | var stage = backend_config_yaml_doc['provider']['stage'] || 'dev'; 208 | return `${service}-${stage}`; 209 | } 210 | 211 | function getBackEndRegion() { 212 | if (region === null) { 213 | if (backend_config_yaml_doc === null) { 214 | var contents = fs.readFileSync(SERVERLESS_FILE_PATH,'utf8'); 215 | backend_config_yaml_doc = yaml.safeLoad(contents); 216 | } 217 | region = backend_config_yaml_doc['provider']['region'] || 'us-east-1'; 218 | } 219 | return region; 220 | } 221 | 222 | function find(arr, key) { 223 | var found = arr.find(function(element) { 224 | return element['OutputKey'] === key; 225 | }); 226 | return found['OutputValue']; 227 | } 228 | 229 | -------------------------------------------------------------------------------- /bots/scripts/env-setup.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var yaml = require('js-yaml'); 5 | var AWS = require('aws-sdk/global'); 6 | var Cloudformation = require('aws-sdk/clients/cloudformation'); 7 | var Iot = require('aws-sdk/clients/iot'); 8 | 9 | const KEYS = ['iotThingsTable', 10 | 'iotKeysAndCertsBucket' 11 | ]; 12 | 13 | //const SERVERLESS_FILE_PATH = path.normalize(`${path.resolve(__dirname)}/../../backend/serverless.yml`); 14 | const BACKEND_CONFIG_FILE_PATH = path.normalize(`${path.resolve(__dirname)}/../src/config/config.json`); 15 | 16 | var backend_config_yaml_doc = null; 17 | var region = null; 18 | 19 | function getBackEndStackName() { 20 | //if (backend_config_yaml_doc === null) { 21 | // var contents = fs.readFileSync(SERVERLESS_FILE_PATH,'utf8'); 22 | // backend_config_yaml_doc = yaml.safeLoad(contents); 23 | // } 24 | //var service = backend_config_yaml_doc['service']; 25 | //var stage = backend_config_yaml_doc['provider']['stage'] || 'dev'; 26 | var service = process.env.SERVICE; 27 | var stage = process.env.STAGE; 28 | return `${service}-${stage}`; 29 | } 30 | 31 | function getBackEndRegion() { 32 | //if (region === null) { 33 | // if (backend_config_yaml_doc === null) { 34 | // var contents = fs.readFileSync(SERVERLESS_FILE_PATH,'utf8'); 35 | // backend_config_yaml_doc = yaml.safeLoad(contents); 36 | // } 37 | // region = backend_config_yaml_doc['provider']['region'] || 'us-east-1'; 38 | //} 39 | region = process.env.REGION || 'us-east-1'; 40 | return region; 41 | } 42 | 43 | function find(arr, key) { 44 | var found = arr.find(function(element) { 45 | return element['OutputKey'] === key; 46 | }); 47 | return found['OutputValue']; 48 | } 49 | 50 | function fetchConfig(keys, outputs) { 51 | console.log('Reading backend configuration file ...'); 52 | AWS.config.region = getBackEndRegion(); 53 | const cfn = new Cloudformation(); 54 | var stackName = getBackEndStackName() 55 | var params = { 56 | StackName: stackName 57 | }; 58 | 59 | console.log(`Parsing configuration params from CloudFormation stack ${stackName} ...`); 60 | cfn.describeStacks(params, function(err, data) { 61 | if (err) console.log(err, err.stack); 62 | else { 63 | var config = {region: region}; 64 | var outputs = data.Stacks[0].Outputs; 65 | KEYS.forEach(function(key) { 66 | config[key] = find(outputs, key); 67 | }); 68 | var iot = new Iot(); 69 | iot.describeEndpoint({}, function(err, data){ 70 | if (err) console.log(err, err.stack); 71 | else { 72 | config['iotEndpointAddress'] = data.endpointAddress; 73 | var content = JSON.stringify(config, null, 4); 74 | fs.writeFileSync(BACKEND_CONFIG_FILE_PATH, content); 75 | console.log(`Backend configuration saved to ${BACKEND_CONFIG_FILE_PATH}`); 76 | } 77 | }); 78 | } 79 | }); 80 | } 81 | 82 | fetchConfig(); 83 | -------------------------------------------------------------------------------- /bots/scripts/setup.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var yaml = require('js-yaml'); 5 | var AWS = require('aws-sdk/global'); 6 | var Cloudformation = require('aws-sdk/clients/cloudformation'); 7 | var Iot = require('aws-sdk/clients/iot'); 8 | 9 | const KEYS = ['iotThingsTable', 10 | 'iotKeysAndCertsBucket' 11 | ]; 12 | 13 | const SERVERLESS_FILE_PATH = path.normalize(`${path.resolve(__dirname)}/../../backend/serverless.yml`); 14 | const BACKEND_CONFIG_FILE_PATH = path.normalize(`${path.resolve(__dirname)}/../src/config/config.json`); 15 | 16 | var backend_config_yaml_doc = null; 17 | var region = null; 18 | 19 | function getBackEndStackName() { 20 | if (backend_config_yaml_doc === null) { 21 | var contents = fs.readFileSync(SERVERLESS_FILE_PATH,'utf8'); 22 | backend_config_yaml_doc = yaml.safeLoad(contents); 23 | } 24 | var service = backend_config_yaml_doc['service']; 25 | var stage = backend_config_yaml_doc['provider']['stage'] || 'dev'; 26 | return `${service}-${stage}`; 27 | } 28 | 29 | function getBackEndRegion() { 30 | if (region === null) { 31 | if (backend_config_yaml_doc === null) { 32 | var contents = fs.readFileSync(SERVERLESS_FILE_PATH,'utf8'); 33 | backend_config_yaml_doc = yaml.safeLoad(contents); 34 | } 35 | region = backend_config_yaml_doc['provider']['region'] || 'us-east-1'; 36 | } 37 | return region; 38 | } 39 | 40 | function find(arr, key) { 41 | var found = arr.find(function(element) { 42 | return element['OutputKey'] === key; 43 | }); 44 | return found['OutputValue']; 45 | } 46 | 47 | function fetchConfig(keys, outputs) { 48 | console.log('Reading backend configuration file ...'); 49 | AWS.config.region = getBackEndRegion(); 50 | const cfn = new Cloudformation(); 51 | var stackName = getBackEndStackName() 52 | var params = { 53 | StackName: stackName 54 | }; 55 | 56 | console.log(`Parsing configuration params from CloudFormation stack ${stackName} ...`); 57 | cfn.describeStacks(params, function(err, data) { 58 | if (err) console.log(err, err.stack); 59 | else { 60 | var config = {region: region}; 61 | var outputs = data.Stacks[0].Outputs; 62 | KEYS.forEach(function(key) { 63 | config[key] = find(outputs, key); 64 | }); 65 | var iot = new Iot(); 66 | iot.describeEndpoint({}, function(err, data){ 67 | if (err) console.log(err, err.stack); 68 | else { 69 | config['iotEndpointAddress'] = data.endpointAddress; 70 | var content = JSON.stringify(config, null, 4); 71 | fs.writeFileSync(BACKEND_CONFIG_FILE_PATH, content); 72 | console.log(`Backend configuration saved to ${BACKEND_CONFIG_FILE_PATH}`); 73 | } 74 | }); 75 | } 76 | }); 77 | } 78 | 79 | fetchConfig(); 80 | -------------------------------------------------------------------------------- /bots/src/bot/aws-configs.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | "region": "us-east-1", 4 | "iotThingsTable": "iotThingsTable-369233778488-us-east-1", 5 | "iotKeysAndCertsBucket": "acme-bots-keys-and-certs-369233778488-us-east-1", 6 | "iotEndpointAddress": "a1rewi10dv0qmn.iot.us-east-1.amazonaws.com" 7 | } 8 | -------------------------------------------------------------------------------- /bots/src/bot/constants.js: -------------------------------------------------------------------------------- 1 | var Config = require(`${__dirname}/../config`); 2 | 3 | const DEVICE_ID_PLACEHOLDER = ''; 4 | 5 | module.exports = { 6 | STANDBY_STATUS : 'standby', 7 | OFF_STATUS : 'off', 8 | CHARGING_STATUS: 'charging', 9 | WORKING_STATUS : 'working', 10 | 11 | OTA_UPDATE_CMD : 'otaUpdate', 12 | START_WORK_CMD : 'startWork', 13 | STAND_BY_CMD : 'standby', 14 | ENABLE_AUTO_CHARGING_CMD : 'enableAutoCharging', 15 | DISABLE_AUTO_CHARGING_CMD : 'disableAutoCharging', 16 | START_CHARGING : 'startCharging', 17 | 18 | REGION: Config.region, 19 | S3_BUCKET: Config.iotKeysAndCertsBucket, 20 | IOT_THINGS_TABLE: Config.iotThingsTable, 21 | IOT_HOST: Config.iotEndpointAddress, 22 | 23 | DEVICE_ID_PLACEHOLDER : DEVICE_ID_PLACEHOLDER, 24 | TELEMETRY_TOPIC_TEMPLATE: `myThings/${DEVICE_ID_PLACEHOLDER}/telemetry`, 25 | CMD_TOPIC_TEMPLATE : `myThings/${DEVICE_ID_PLACEHOLDER}/cmds`, 26 | ACK_SUFFIX : 'ack', 27 | } 28 | -------------------------------------------------------------------------------- /bots/src/bot/core.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk/global'); 2 | var DynamoDB = require('aws-sdk/clients/dynamodb'); 3 | var S3 = require('aws-sdk/clients/s3'); 4 | 5 | var Constants = require(`${__dirname}/constants`); 6 | var cache = require(`${__dirname}/telemetryCache`); 7 | 8 | AWS.config.region = Constants.REGION; 9 | 10 | const OLD_VERSION = '1.0' 11 | const OLD_VERSION_BATTERY_LIFE = (30); // 60 secs 12 | const NEW_VERSION_BATTERY_LIFE = (60); // 120 secs 13 | const OLD_VERSION_CHARGING_TIME = 60; // 60 secs 14 | const NEW_VERSION_CHARGING_TIME = 30; // 30 secs 15 | 16 | const STANDBY_BATTERY_LIFE_MULTIPLIER = 0.5; 17 | 18 | const MAX_BATTERY_LIVE = 100.00; // in percentage 19 | const MIN_BATTERY_LIFE = 0.00; // in percentage 20 | 21 | const DEFAULT_CHARGING_THRESHOLD = 15.00; // in percentage 22 | 23 | function getDelta(version, status, secs_elapsed=1.0) { 24 | if (status === Constants.OFF_STATUS) { 25 | return 0.0; 26 | } 27 | else if (status === Constants.CHARGING_STATUS || status == Constants.STANDBY_STATUS) { 28 | var inc = Number((MAX_BATTERY_LIVE / OLD_VERSION_CHARGING_TIME).toFixed(6)); 29 | if (version !== OLD_VERSION) { 30 | inc = Number((MAX_BATTERY_LIVE / NEW_VERSION_CHARGING_TIME).toFixed(6)); 31 | } 32 | return Number((inc * secs_elapsed).toFixed(6)); 33 | } else if (status === Constants.WORKING_STATUS) { 34 | var dec = Number((MAX_BATTERY_LIVE / OLD_VERSION_BATTERY_LIFE).toFixed(6)); 35 | if (version !== OLD_VERSION) { 36 | dec = Number((MAX_BATTERY_LIVE / NEW_VERSION_BATTERY_LIFE).toFixed(6)); 37 | } 38 | return Number((-1.0 * dec * secs_elapsed).toFixed(6)); 39 | } 40 | } 41 | 42 | 43 | function getThing(thingName, cb) { 44 | const ddb = new DynamoDB(); 45 | var params = { 46 | TableName: Constants.IOT_THINGS_TABLE, 47 | Key: { 48 | "thingName": { 49 | S: thingName 50 | } 51 | }, 52 | } 53 | ddb.getItem(params, function(err, data) { 54 | if(cb) cb(err, data); 55 | }); 56 | } 57 | 58 | function getKeysAndCert(certificateId, cb) { 59 | const s3 = new S3(); 60 | 61 | var params = { 62 | Bucket: Constants.S3_BUCKET, 63 | Key: certificateId 64 | } 65 | s3.getObject(params, function(err, data) { 66 | if(cb) cb(err, data); 67 | }); 68 | } 69 | 70 | module.exports = { 71 | bootstrap: function(thingName, cb) { 72 | getThing(thingName, function(err, ddb_data) { 73 | if (err) cb(err, null); 74 | else { 75 | if(ddb_data.Item) { 76 | const certificateId = ddb_data.Item.certificateId.S; 77 | getKeysAndCert(certificateId, function(err, s3_data) { 78 | if(err) cb(err, null); 79 | else { 80 | const certs = JSON.parse(s3_data.Body); 81 | cb(null, certs); 82 | } 83 | }); 84 | } else { 85 | err = new Error(`Thing with thingName='${thingName}' does not exist.`); 86 | cb(err, null); 87 | } 88 | } 89 | }); 90 | }, 91 | updateState: function (props, logger) { 92 | const version = props.version; 93 | const status = props.status; 94 | const batteryLife = props.batteryLife 95 | 96 | // Check if there is a status change needed 97 | if(status === Constants.OFF_STATUS) return ; 98 | if(props.startWorkRequested) { 99 | logger.info(`bot-status-change from ${status} to ${Constants.WORKING_STATUS}`); 100 | props.status = Constants.WORKING_STATUS; 101 | props.startWorkRequested = false; 102 | } 103 | if(props.standByRequested) { 104 | logger.info(`bot-status-change from ${status} to ${Constants.STANDBY_STATUS}`); 105 | props.status = Constants.STANDBY_STATUS; 106 | props.standByRequested = false; 107 | } 108 | else if (status === Constants.WORKING_STATUS && batteryLife <= DEFAULT_CHARGING_THRESHOLD && props.autoChargingEnabled === true) { 109 | props.status = Constants.CHARGING_STATUS; 110 | logger.info(`bot-status-change from ${status} to ${Constants.CHARGING_STATUS}`); 111 | return; 112 | } else if(status === Constants.CHARGING_STATUS && batteryLife >= MAX_BATTERY_LIVE) { 113 | props.status = Constants.WORKING_STATUS; 114 | logger.info(`bot-status-change from ${status} to ${Constants.WORKING_STATUS}`); 115 | return; 116 | } 117 | 118 | // Update batteryLife value 119 | var delta = getDelta(version, status, 1); 120 | var newValue = props.batteryLife + delta; 121 | if (newValue > MAX_BATTERY_LIVE) { 122 | newValue = MAX_BATTERY_LIVE; 123 | } else if (newValue < MIN_BATTERY_LIFE) { 124 | newValue = MIN_BATTERY_LIFE; 125 | } 126 | props.batteryLife = Number(newValue.toFixed(6)); 127 | telemetryData = { 128 | recorded_at: Date.now(), 129 | version: version, 130 | status: status, 131 | batteryLife: batteryLife 132 | 133 | } 134 | if (version === OLD_VERSION) { // just send 1 sec of data 135 | cache.recordTelemetry(telemetryData, 1); 136 | } else { // send up to 2 mins of data 137 | cache.recordTelemetry(telemetryData); 138 | } 139 | 140 | }, 141 | getTelemetryData: function() { 142 | return cache.flushTelemetry(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /bots/src/bot/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | var BotCore = require(`${__dirname}/core`); 7 | const BotLogger = require(`${__dirname}/logger`); 8 | var Constants = require(`${__dirname}/constants`); 9 | var Iot = require(`${__dirname}/iot`); 10 | 11 | const DEFAULT_TELEMETRY_INTERVAL = 15000 // in milliseconds 12 | const DEFAULT_VERSION = '1.0' 13 | const DEFAULT_BATTERY_LIFE = 50; 14 | const DEFAULT_STATUS = Constants.CHARGING_STATUS; 15 | 16 | function initProps(obj, opts) { 17 | obj.props = opts; 18 | const placeholder = Constants.DEVICE_ID_PLACEHOLDER; 19 | obj.props.telemetryTopic = opts.telemetryTopic || Constants.TELEMETRY_TOPIC_TEMPLATE.replace(placeholder, obj.props.thingName); 20 | obj.props.cmdsTopic = opts.cmdsTopic || Constants.CMD_TOPIC_TEMPLATE.replace(placeholder, obj.props.thingName);; 21 | obj.props.cmdAckTopic = `${obj.props.cmdsTopic}/${Constants.ACK_SUFFIX}`; 22 | obj.props.certsProps = null; 23 | obj.props.telemetry = { 24 | status: opts.status || DEFAULT_STATUS, 25 | batteryLife: opts.batteryLife || DEFAULT_BATTERY_LIFE, 26 | version: opts.version || DEFAULT_VERSION, 27 | startWorkRequested: false, 28 | standByRequested: false, 29 | autoChargingEnabled: true 30 | } 31 | obj.props.logger = BotLogger.getLogger(obj.props.thingName); 32 | } 33 | 34 | module.exports = class Bot { 35 | 36 | constructor(opts={}) { 37 | initProps(this, opts); 38 | 39 | // 40 | // Load keys and certs and then start the bot. 41 | // 42 | var _this = this; 43 | BotCore.bootstrap(this.props.thingName, function(err, data) { 44 | if(err) { 45 | console.log(err, err.stack); 46 | return; 47 | } else { 48 | _this.props.certsProps = data; 49 | _this.start(); 50 | } 51 | }); 52 | } 53 | 54 | start() { 55 | Iot.bootstrap(this); 56 | var _this = this; 57 | setInterval( 58 | function() { 59 | BotCore.updateState(_this.props.telemetry, _this.props.logger); 60 | }, 61 | 1000 62 | ); 63 | _this.props.logger.info(`Will send telemetry data every ${DEFAULT_TELEMETRY_INTERVAL} seconds to ${_this.props.telemetryTopic}`); 64 | setInterval( 65 | function() { 66 | var arr = BotCore.getTelemetryData(); 67 | var data = { telemetry: arr}; 68 | Iot.sendTelemetry(_this.props.telemetryTopic, JSON.stringify(data) ); 69 | }, 70 | DEFAULT_TELEMETRY_INTERVAL 71 | ); 72 | } 73 | 74 | setState(props) { 75 | var state = this.props.telemetry; 76 | for (var key in props) { 77 | if (state.hasOwnProperty(key)) { 78 | state[key] = props[key]; 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /bots/src/bot/iot.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var iot = require('aws-iot-device-sdk'); 6 | var Constants = require(`${__dirname}/constants`); 7 | var MessageHandler = require(`${__dirname}/msgHandler`); 8 | 9 | 10 | const VERISIGN_ROOT_CA_FILENAME = 'verisign-root-ca.pem' 11 | const CA_CERT = fs.readFileSync( 12 | `${path.resolve(__dirname)}/${VERISIGN_ROOT_CA_FILENAME}`,'utf8'); 13 | 14 | var aws_iot_device = null; 15 | var bot = null; 16 | 17 | module.exports = { 18 | bootstrap: function(caller) { 19 | bot = caller; 20 | var opts = { 21 | privateKey: Buffer.from(bot.props.certsProps.keyPair.PrivateKey, 'utf8'), 22 | clientCert: Buffer.from(bot.props.certsProps.certificatePem, 'utf8'), 23 | caCert: Buffer.from(CA_CERT, 'utf8'), 24 | clientId: bot.props.thingName, 25 | host: Constants.IOT_HOST, 26 | } 27 | aws_iot_device = iot.device(opts); 28 | 29 | aws_iot_device.on('connect', function() { 30 | bot.props.logger.info(`Subscribing to ${bot.props.cmdsTopic}`); 31 | aws_iot_device.subscribe(bot.props.cmdsTopic); 32 | aws_iot_device.on('message', function(t, payload) { 33 | const resp = MessageHandler.handle(payload.toString(), bot); 34 | aws_iot_device.publish(bot.props.cmdAckTopic, resp); 35 | }); 36 | }); 37 | 38 | aws_iot_device.on('connect', function() { 39 | bot.props.logger.info('aws-iot-event connect'); 40 | }); 41 | aws_iot_device.on('reconnect', function() { 42 | bot.props.logger.info('aws-iot-event reconnect'); 43 | }); 44 | aws_iot_device.on('close', function() { 45 | bot.props.logger.info('aws-iot-event close'); 46 | }); 47 | aws_iot_device.on('offline', function() { 48 | bot.props.logger.info('aws-iot-event offline') 49 | }); 50 | aws_iot_device.on('error', function() { 51 | bot.props.logger.info('aws-iot-event error'); 52 | }); 53 | aws_iot_device.on('end', function() { 54 | bot.props.logger.info('aws-iot-event end'); 55 | }); 56 | 57 | }, 58 | sendTelemetry: function (topic, data) { 59 | aws_iot_device.publish(topic, data); 60 | } 61 | } -------------------------------------------------------------------------------- /bots/src/bot/logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston'); 2 | const { combine, timestamp, label, printf } = format; 3 | 4 | const myFormat = printf(info => { 5 | return `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`; 6 | }); 7 | 8 | module.exports = { 9 | getLogger: function (botName) { 10 | return createLogger({ 11 | transports: [ 12 | new transports.Console() 13 | ], 14 | format: combine( 15 | label({ label: botName }), 16 | timestamp(), 17 | myFormat 18 | ) 19 | }); 20 | } 21 | } -------------------------------------------------------------------------------- /bots/src/bot/msgHandler.js: -------------------------------------------------------------------------------- 1 | var Constants = require(`${__dirname}/constants`); 2 | 3 | function handleOtaUpdate(msg, bot) { 4 | var state = bot.props.telemetry; 5 | if (msg.version === undefined) { 6 | throw new Error('Missing required param: version'); 7 | } else if (state.version === msg.version) { 8 | throw new Error(`I am already running version ${msg.version}`); 9 | } else { 10 | bot.setState({version: msg.version}); 11 | return {status: 'ok'}; 12 | } 13 | } 14 | 15 | function handleStartWork(msg, bot) { 16 | var state = bot.props.telemetry; 17 | if (state.status === Constants.WORKING_STATUS) { 18 | throw new Error('I am already working'); 19 | } else { 20 | bot.setState({startWorkRequested: true}); 21 | return {status: 'ok'}; 22 | } 23 | } 24 | 25 | function handleStandBy(msg, bot) { 26 | var state = bot.props.telemetry; 27 | if (state.status === Constants.STANDBY_STATUS) { 28 | throw new Error('I am already on standby'); 29 | } else { 30 | bot.setState({standByRequested: true}); 31 | return {status: 'ok'}; 32 | } 33 | } 34 | 35 | function handleEnableAutoCharging(bot) { 36 | var state = bot.props.telemetry 37 | if (state.autoChargingEnabled == true) { 38 | throw new Error('Auto charging is already enabled.'); 39 | } else { 40 | bot.setState({autoChargingEnabled: true}); 41 | return {status: 'ok'}; 42 | } 43 | } 44 | 45 | function handleDisableAutoCharging(bot) { 46 | var state = bot.props.telemetry 47 | if (state.autoChargingEnabled == false) { 48 | throw new Error('Auto charging is already disabled.'); 49 | } else { 50 | bot.setState({autoChargingEnabled: false}); 51 | return {status: 'ok'}; 52 | } 53 | } 54 | 55 | function handleStartCharging(bot) { 56 | var state = bot.props.telemetry 57 | if (state.status == Constants.CHARGING_STATUS) { 58 | throw new Error('I am already charging.'); 59 | } else { 60 | bot.setState({status: Constants.CHARGING_STATUS}); 61 | return {status: 'ok'}; 62 | } 63 | } 64 | 65 | function processCmd(msg, bot) { 66 | if (msg.cmd === Constants.OTA_UPDATE_CMD) { 67 | return handleOtaUpdate(msg, bot); 68 | } else if (msg.cmd === Constants.START_WORK_CMD) { 69 | return handleStartWork(msg, bot); 70 | } else if (msg.cmd === Constants.STAND_BY_CMD) { 71 | return handleStandBy(msg, bot); 72 | } else if (msg.cmd === Constants.ENABLE_AUTO_CHARGING_CMD) { 73 | return handleEnableAutoCharging(bot); 74 | } else if (msg.cmd === Constants.DISABLE_AUTO_CHARGING_CMD) { 75 | return handleDisableAutoCharging(bot); 76 | } else if (msg.cmd === Constants.START_CHARGING) { 77 | return handleStartCharging(bot); 78 | } else { 79 | throw new Error(`Unsupported cmd: ${msg.cmd}`); 80 | } 81 | } 82 | 83 | module.exports = class MessageHandler { 84 | 85 | static handle(stringMsg, bot) { 86 | var msg, error, resp; 87 | try { 88 | var msg = JSON.parse(stringMsg); 89 | resp = processCmd(msg, bot); 90 | } catch(err) { 91 | resp = { error: err.message }; 92 | } 93 | return JSON.stringify(resp); 94 | } 95 | } -------------------------------------------------------------------------------- /bots/src/bot/telemetryCache.js: -------------------------------------------------------------------------------- 1 | const MAX_SIZE = 120; // 2 mins 2 | var store_arr = []; 3 | 4 | module.exports = { 5 | recordTelemetry: function(data, maxSize=MAX_SIZE) { 6 | store_arr.push(data); 7 | while(store_arr.length > maxSize) { 8 | store_arr.shift(); 9 | } 10 | }, 11 | flushTelemetry: function() { 12 | var shallowCopy = store_arr.slice(); 13 | store_arr = []; 14 | return shallowCopy; 15 | } 16 | } -------------------------------------------------------------------------------- /bots/src/bot/verisign-root-ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB 3 | yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL 4 | ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp 5 | U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW 6 | ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 7 | aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL 8 | MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW 9 | ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln 10 | biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp 11 | U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y 12 | aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 13 | nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex 14 | t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz 15 | SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG 16 | BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ 17 | rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ 18 | NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E 19 | BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH 20 | BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy 21 | aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv 22 | MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE 23 | p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y 24 | 5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK 25 | WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ 26 | 4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N 27 | hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq 28 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /bots/src/config/index.js: -------------------------------------------------------------------------------- 1 | var config = require('./config.json'); 2 | 3 | module.exports = { 4 | region: config.region, 5 | iotThingsTable: config.iotThingsTable, 6 | iotKeysAndCertsBucket: config.iotKeysAndCertsBucket, 7 | iotEndpointAddress: config.iotEndpointAddress, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # Acme Bots 2 | 3 | Acme Bots offers robotics services for customers in several industries, such as (but not limited to) entertainment, security, oil & gas, building, retail and manufacturing. 4 | 5 | Customers contract bots from a vast array of options, such as sub-sea, terrestrial and drone bots. For example, oil & gas customers uses terrestrial bots to automate their warehouse and supply-chain operations. Sub-sea bots are used to inspect operations that take lace on deep water well sites and drone are used to inspect miles of mid-stream pipelines. 6 | 7 | Every single Acme Bots contract has availability SLAs. In order to make sure there are no customer disruptions and that customers are operating the bots according to their requirements, every single bot sends a lot of telemetry data to AWS IoT and can also accept remote commands. This also enables AcmeBots to have historical data about every single device in order to make software and hardware improvements and also optimize preventive maintenance. -------------------------------------------------------------------------------- /docs/bootstrapping.md: -------------------------------------------------------------------------------- 1 | ## Bootstrapping Bots 2 | After a bot is provisioned, you can start/stop it as a container. Note that this is all managed by step functions so you do not need to perform these actions yourself. 3 | 4 | ![Bootstraping bots](imgs/bootstrap-bot-diagram.png) 5 | -------------------------------------------------------------------------------- /docs/bots-core.md: -------------------------------------------------------------------------------- 1 | # Bots Functionality 2 | 3 | A bot can have 3 statuses: 4 | 5 | 1. **standby:** Fully charged, on the dock waiting to work. 6 | 2. **charging:** Battery life is less than 100%, on the dock, charging. 7 | 3. **working:** Means that the bot is doing something. The bots are programmed to only work if their battery life is bigger than 15%. Whenever its battery life reaches 15%, a bot will automatically go to its charging dock to charge. 8 | 9 | ![bots-logic](imgs/bot-flow-chart.png) 10 | 11 | A bot will always go to the charging dock to charge whenever it reaches 15% of battery life. Unless explicitly requested, the bot will not go back to work while charging, until its battery is 100% charged. 12 | 13 | Once charged, the bot goes automatically back to work, unless explicitly requested to standby. In that case, it will be on stand by mode on the charging dock, without draining battery. 14 | 15 | Whenever a bot is working, it will start draining its battery, until it reaches 15%. At this point, the bot will automatically go back to the charging dock for charging. 16 | 17 | Currently we have two bots versions, and the table bellow illustrates the difference between both: 18 | 19 | |Functionality | Version | Description | 20 | |--------------|---------|-------------------------------------| 21 | |Battery Life |1.0 | 5 mins | 22 | | |2.0 |10 mins | 23 | |Charging Time |1.0 |60 seconds | 24 | | |2.0 |30 seconds | 25 | |Telemetry |1.0 |Sends 1 datapoint every 15 seconds. | 26 | | |2.0 |Sends 15 datapoints every 15 seconds.| -------------------------------------------------------------------------------- /docs/cleanup.md: -------------------------------------------------------------------------------- 1 | ## Cleanup 2 | 3 | CloudFormation custom resources are used for cleaning up components before deleting, including Fargate tasks, Fargate Task Defintions, ECR Image Repository, IoT principals, and S3 buckets. To clean up, you should only need to delete stacks one at a time. Note that bots are not cleaned up, so do that in the web UI as a first step. 4 | 5 | * In the AcmeBots Web UI, select each thing you provisioned and delete it 6 | * In the AWS Console, got to *CloudFormation* 7 | * Select the top stack (named acme-bots-dev in this example), and click *Actions* -> *Delete Stack* 8 | * Wait for this to complete 9 | * Select the first stack (acmebots in the example above), and click *Actions* -> *Delete Stack* 10 | 11 | ### Manual Cleanup 12 | 13 | If for some reason the stack delete does not run as planned, there are a number of places to look at components deployed: 14 | 15 | * ECS - Clusters 16 | * Acme-bots-dev ECS Cluster - Go to Tasks - Stop any running Tasks 17 | * Delete the ECS Cluster 18 | * ECS - Task Definitions - Deregister each task definition 19 | * ECS - Amazon ECR Repositories - Delete the acme-bots-dev repository 20 | * IOT Core - Policies - Delete any policies for your bots. Delete AcmeBotsGui policy 21 | * IOT Core - Manage Things - Delete any bots you’ve provisioned. 22 | * IOT Core - Secure - Certificates - Delete any certs related to these 23 | * Lambda Functions - Search for ‘acme’ and delete any functions. Do NOT delete functions that have ‘Clean’ in their name as they are used in CloudFormation cleanup. They should be removed by the CloudFormation template 24 | * Step Functions - Delete the iotCreateThing and iotDeleteThing step functions 25 | * VPC - Delete all VPC components that were created 26 | * S3 - Delete any ‘acme’ buckets 27 | 28 | -------------------------------------------------------------------------------- /docs/cmd-ctrl.md: -------------------------------------------------------------------------------- 1 | ## Controlling Bots 2 | Bots can be controlled remotely. They listen for commands on a specific MQTT topic and send the response to those commands on another MQTT topic. The table bellow describes it in more details: 3 | 4 | |Entity |Topic |Description | 5 | |-------|-----------------------|---------------------| 6 | |WebApp |`myThings//cmds`|Publishes commands. | 7 | | |`myThings//ack `|Listen for responses.| 8 | |Bot |`myThings//cmds`|Listen for commands. | 9 | | |`myThings//ack `|Publishes responses. | 10 | 11 | ![Controlling Bots](imgs/cmd-ctrl-diagram.png) 12 | 13 | 14 | You can send the following commands to the bots: 15 | 16 | 1. **otaUpdate:** Update the bot software to version 1.0: Updates the software to version 1.0. 17 | 2. **otaUpdate:** Update the bot software to version 2.0: Updates the software to version 2.0. 18 | 3. **Start Work:** Request the bot to go to work immediately. 19 | 4. **Stand by:** Request the bot to go to stand by mode immediately. 20 | 21 | 22 | The picture bellows illustrates how you can send those commands to a specific bot within the web front-end. 23 | 24 | ![Controlling Bots](imgs/cmd-ctrl-screen-shot.png) 25 | -------------------------------------------------------------------------------- /docs/imgs/bootstrap-bot-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/bootstrap-bot-diagram.png -------------------------------------------------------------------------------- /docs/imgs/bot-flow-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/bot-flow-chart.png -------------------------------------------------------------------------------- /docs/imgs/cmd-ctrl-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/cmd-ctrl-diagram.png -------------------------------------------------------------------------------- /docs/imgs/cmd-ctrl-screen-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/cmd-ctrl-screen-shot.png -------------------------------------------------------------------------------- /docs/imgs/installation-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/installation-workflow.png -------------------------------------------------------------------------------- /docs/imgs/prov-state-machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/prov-state-machine.png -------------------------------------------------------------------------------- /docs/imgs/provisioning-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/provisioning-diagram.png -------------------------------------------------------------------------------- /docs/imgs/provisioning-screen-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/provisioning-screen-shot.png -------------------------------------------------------------------------------- /docs/imgs/remove-state-machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/remove-state-machine.png -------------------------------------------------------------------------------- /docs/imgs/telemetry-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/telemetry-diagram.png -------------------------------------------------------------------------------- /docs/imgs/telemetry-screen-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/docs/imgs/telemetry-screen-shot.png -------------------------------------------------------------------------------- /docs/installing.md: -------------------------------------------------------------------------------- 1 | # Installing Acme Bots 2 | 3 | Acme bots was built using the serverless application framework using Node.js and React. The serverless framework was retained in the overall deployment methodology. CloudFormation and CodePipeline are included to simplify provisioning for the end user. Below is a diagram showing the installation workflow: 4 | 5 | ![installation-workflow](imgs/installation-workflow.png) 6 | 7 | Services Used: 8 | 9 | * [AWS Lambda](https://aws.amazon.com/lambda/) provides logic for Acme Bots without requiring servers 10 | * [AWS Step Functions](https://aws.amazon.com/step-functions/) provides serverless workflows to tie together multiple Lambda functions to meet the needs of Acme Bots 11 | * [AWS IoT Core](https://aws.amazon.com/iot-core/) allows Acme bots to securely connect and track IoT bots while also making it easy for other services to gather and act on data generated from the devices 12 | * [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) provides a serverless database used for tracking metadata about IoT Bots 13 | * [Amazon Cognito](https://aws.amazon.com/cognito/) provides sign in and access control for the Acme Bots Front end Web Application 14 | * [Amazon S3](https://aws.amazon.com/s3/) hosts the Acme Bots Static front end website as well as build artifacts 15 | * [AWS Fargate](https://aws.amazon.com/fargate/) hosts our IoT bots in lieu of having physical bots 16 | * [AWS CodePipeline](https://aws.amazon.com/codepipeline/) provides a pipeline for managing our build from code. 17 | * [AWS CodeBuild](https://aws.amazon.com/codebuild/) executes serverless deploy and bot container builds 18 | * [AWS CloudFormation](https://aws.amazon.com/cloudformation/) allows deploying infrastructure (Infrastructure-as-Code) 19 | 20 | [Amazon CloudWatch](https://aws.amazon.com/cloudwatch/) is used for monitoring our application as follows: 21 | 22 | * Metrics track bot low battery, event delays, and battery threshold 23 | * Alarms send notifications based on bot low battery or telemetry delays 24 | * Events are used for publishing bot status 25 | * Logs are used to gain insight into execution 26 | 27 | ## Installation 28 | 29 | Installation includes provisioning a VPC and all related components for this solution. To install Acme bots you must have a GitHub account with a personal access token. 30 | 31 | * Sign up for a [GitHub](https://github.com) account if you do not have one 32 | * Setup a personal access token as described [here](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) 33 | * For scopes, check *repo* and it's child scopes. Also check *read:repo_hook*. 34 | * Click *Generate Token*. Record the token as you'll need it for deployment. 35 | * Fork this GitHub repo into your own account 36 | * Right click on the *acme-bots.template*, *Save link as* and save to your computer 37 | * In the AWS Console, go to *CloudFormation* and *Create Stack*. 38 | * Select upload a template to S3, click *Browse*, and select your copy of acme-bots.template. Click *Next* 39 | * Fill in the form (example below) 40 | * Stack Name: acmebots 41 | * User Name: acmebotsadmin 42 | * Email: my@example.com (This must be a valid email as it sends you the login password) 43 | * Stage: dev 44 | * GitHub OAUTH Token: ... The personal access token above 45 | * GitHub User: mygithubuser The name of your user in GitHub 46 | * GitHub repo: aws-iot-core-acmebots-monitoring The name of your forked repo 47 | * GitHub branch: master 48 | * Click *Next* 49 | * On the *Options* page, click *Next* again 50 | * Check *I acknowledge that AWS CloudFormation might create IAM resources with custom names* and click *Create* 51 | 52 | Deployment takes around 25 minutes including both CodePipeline runs 53 | 54 | ## Verifying Deployment 55 | * In the AWS Console, in CloudFormation, you should see 2 stacks in CREATE_COMPLETE 56 | ** First, the stackname you used above (acmebots). 57 | ** Second, a stack called acme-bots- (acme-bots-dev) 58 | * In the AWS Console, go to CodePipeline. You should see 2 pipelines (acmebots-dev and acme-bots-botpipeline-dev). Confirm each of these has successfully completed 59 | ** acmebots-dev (-) - Runs our serverless template to create lambda functinos, IoT resources, S3, CodeBuild, etc. This is what creates the acme-bots- CloudFormation stack 60 | ** acme-bots-botpipeline-dev - Runs the build of our bot code and pushes the docker image to our Elastic Container Registry (ECR) 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/provisioning.md: -------------------------------------------------------------------------------- 1 | ## Provisioning Bots 2 | Before you use a bot, it needs to be properly configured on the AWS IoT platform. Our app has a screen just for searching, creating and removing things. 3 | 4 | * To provision a bot, you need to connect to the web front-end in your browser. You can find the URL in the Outputs tab (WebsiteURL) for the stack you created (acmebots if you used the default stack name in these directions). 5 | * The user name and password to connect to this site will be in an email sent to the address you provided during stack creation. Authenticate to the web site (Cognito) using this user/password. You will have to change your password and enter a verification code from email to continue. 6 | * The provisioning frontend GUI looks like this: 7 | ![provisioning screen shot](imgs/provisioning-screen-shot.png) 8 | * Go to the 'Things' tab. Type in a bot name and click 'Create Thing'. In the demo we created bot1, bot2, and bot3. 9 | * Clear the thing name from the box and click 'Search'. You should see the bots you provisioned. 10 | 11 | The process of provisioning is shown below: 12 | ![provisioning diagram](imgs/provisioning-diagram.png) 13 | 14 | This consists of: 15 | 16 | 1. Provision the necessary entities on AWS IoT 17 | 1. Create a thing 18 | 2. Create a certificate. 19 | 3. Create an IoT policy. 20 | 2. Group the entities together on AWS IoT: 21 | 1. Attach the thing to the cert. 22 | 2. Attach the cert to the policy. 23 | 3. Save the meta data on DynamoDB so we can query it. We use DynamoDB here because the AWS IoT API has a limit on how many API calls you can do per second. 24 | 4. Save the keys and certs to S3, since AWS IoT just give us that information once, right after we create the certificate. The certs would be needed later on for the bots to connect to AWS IoT. 25 | 5. Provision the bot on AWS Fargate. 26 | 27 | The picture bellow shows the AWS StepFunctions state machine for the provisioning process: 28 | 29 | ![provisioning state machine](imgs/prov-state-machine.png) 30 | 31 | The removing process consists of: 32 | 1. Disable the certificate use by the thing being removed. 33 | 2. Detach the entities: 34 | 1. Detach the thing from the certificate. 35 | 2. Detach the certificate from the policy. 36 | 3. Delete entities from AWS IoT: 37 | 1. Delete the thing. 38 | 2. Delete the certificate. 39 | 3. Delete the policy. 40 | 4. Delete the corresponding data: 41 | 1. Delete the meta data from the DynamoDB table. 42 | 2. Delete the certs and keys from the S3 bucket. 43 | 44 | The picture bellow shows the AWS StepFunctions state machine for the removing process: 45 | 46 | ![remove state machine](imgs/remove-state-machine.png) 47 | -------------------------------------------------------------------------------- /docs/telemetry.md: -------------------------------------------------------------------------------- 1 | ## Viewing Bot's Telemetry 2 | Once a bot is provisioned and operational, we can use the web front-end to see its telemetry in real-time. 3 | 4 | ![Viewing Telemetry](imgs/telemetry-diagram.png) 5 | 6 | This is how the frontend GUI looks like: 7 | 8 | ![Viewing Telemetry Screen Shot](imgs/telemetry-screen-shot.png) 9 | 10 | A bot sends the following telemetry information, every 15 seconds: 11 | 12 | 1. **Version:** The software version it is currently running. 13 | 2. **Status:** The bot status. 14 | 3. **Battery Life:** The current battery life, in percentage. 15 | 16 | **Note:** The app does not fetch any historical data. All it does is to subscribe to the respective thing topic and wait for telemetry being sent by the bot every 15 seconds. So, whenever you subscribe to a bot, it can take up to 15 seconds to receive the first telemetry data. If you keep subscribed, the data will be automatically updated every 15 seconds. 17 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "aws-amplify": "^3.0.17", 7 | "aws-amplify-react": "^4.1.16", 8 | "aws-sdk": "^2.693.0", 9 | "js-yaml": "^3.14.0", 10 | "moment": "^2.26.0", 11 | "react": "^16.13.1", 12 | "react-dom": "^16.13.1", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "^3.4.3", 15 | "recharts": "^1.8.5", 16 | "semantic-ui-css": "^2.4.1", 17 | "semantic-ui-react": "^0.88.2", 18 | "uuid": "^8.1.0" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test --env=jsdom", 24 | "eject": "react-scripts eject" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/images/acmebots-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/frontend/public/images/acmebots-architecture.png -------------------------------------------------------------------------------- /frontend/public/images/aws-iot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/frontend/public/images/aws-iot.png -------------------------------------------------------------------------------- /frontend/public/images/browser-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/frontend/public/images/browser-icon.png -------------------------------------------------------------------------------- /frontend/public/images/terminal-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-acmebots-monitoring/7503c17933d4cd334ca2c9d9eb4a2aa0d3ec8608/frontend/public/images/terminal-icon.png -------------------------------------------------------------------------------- /frontend/public/images/thing.svg: -------------------------------------------------------------------------------- 1 | InternetOfThings -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Acme Bots 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Acme Bots", 3 | "name": "Acme Bots Application", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/scripts/detachPrincipals.js: -------------------------------------------------------------------------------- 1 | 2 | var AWS = require('aws-sdk/global'); 3 | var Iot = require('aws-sdk/clients/iot'); 4 | var Config = require('../src/config/config.json'); 5 | 6 | AWS.config.region = Config.region; 7 | 8 | var iot = new Iot(); 9 | 10 | const policyName = Config.AcmeBotsGuiIotPolicy; 11 | var params = { 12 | policyName: policyName 13 | }; 14 | iot.listPolicyPrincipals(params, function(err, data) { 15 | if (err) console.log(err, err.stack); 16 | else { 17 | data.principals.forEach(function(principal){ 18 | var detachParams = { 19 | policyName: policyName, 20 | principal: principal 21 | }; 22 | iot.detachPrincipalPolicy(detachParams, function(err2, data2){ 23 | if (err2) console.log(err2, err2.stack); 24 | else { 25 | console.log(`Successfully detached ${principal} from ${policyName}`) 26 | } 27 | }); 28 | }) 29 | } 30 | }); 31 | 32 | // var params = { 33 | // policyName: Config.AcmeBotsGuiIotPolicy, 34 | // principal: ?, 35 | // }; 36 | // iot.detachPrincipalPolicy(params, function(err, data) { 37 | // if (err) console.log(err, err.stack); // an error occurred 38 | // else console.log(data); // successful response 39 | // }); -------------------------------------------------------------------------------- /frontend/scripts/setup.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var yaml = require('js-yaml'); 5 | var AWS = require('aws-sdk/global'); 6 | var Cloudformation = require('aws-sdk/clients/cloudformation'); 7 | var Iot = require('aws-sdk/clients/iot'); 8 | 9 | const KEYS = [ 10 | 'IdentityPoolId', 11 | 'UserPoolId', 12 | 'ReactAppClientId', 13 | 'iotThingsTable', 14 | 'IotProvisionThingLambdaFunctionQualifiedArn', 15 | 'IotRemoveThingLambdaFunctionQualifiedArn', 16 | 'AcmeBotsGuiIotPolicy', 17 | ]; 18 | 19 | const SERVERLESS_FILE_PATH = path.normalize(`${path.resolve(__dirname)}/../../backend/serverless.yml`); 20 | const BACKEND_CONFIG_FILE_PATH = path.normalize(`${path.resolve(__dirname)}/../src/config/config.json`); 21 | 22 | var backend_config_yaml_doc = null; 23 | var region = null; 24 | 25 | function getBackEndStackName() { 26 | if (backend_config_yaml_doc === null) { 27 | var contents = fs.readFileSync(SERVERLESS_FILE_PATH,'utf8'); 28 | backend_config_yaml_doc = yaml.safeLoad(contents); 29 | } 30 | var service = backend_config_yaml_doc['service']; 31 | var stage = backend_config_yaml_doc['provider']['stage'] || 'dev'; 32 | return `${service}-${stage}`; 33 | } 34 | 35 | function getBackEndRegion() { 36 | if (region === null) { 37 | if (backend_config_yaml_doc === null) { 38 | var contents = fs.readFileSync(SERVERLESS_FILE_PATH,'utf8'); 39 | backend_config_yaml_doc = yaml.safeLoad(contents); 40 | } 41 | region = backend_config_yaml_doc['provider']['region'] || 'us-east-1'; 42 | } 43 | return region; 44 | } 45 | 46 | function find(arr, key) { 47 | var found = arr.find(function(element) { 48 | return element['OutputKey'] === key; 49 | }); 50 | return found['OutputValue']; 51 | } 52 | 53 | function fetchConfig(keys, outputs) { 54 | console.log('Reading backend configuration file ...'); 55 | AWS.config.region = getBackEndRegion(); 56 | const cfn = new Cloudformation(); 57 | var stackName = getBackEndStackName() 58 | var params = { 59 | StackName: stackName 60 | }; 61 | 62 | console.log(`Parsing configuration params from CloudFormation stack ${stackName} ...`); 63 | cfn.describeStacks(params, function(err, data) { 64 | if (err) console.log(err, err.stack); 65 | else { 66 | var config = {region: region}; 67 | var outputs = data.Stacks[0].Outputs; 68 | KEYS.forEach(function(key) { 69 | config[key] = find(outputs, key); 70 | }); 71 | var iot = new Iot(); 72 | iot.describeEndpoint({endpointType: 'iot:Data-ATS'}, function(err, data){ 73 | if (err) console.log(err, err.stack); 74 | else { 75 | config['iotEndpointAddress'] = data.endpointAddress 76 | var content = JSON.stringify(config, null, 4); 77 | fs.writeFileSync(BACKEND_CONFIG_FILE_PATH, content); 78 | console.log(`Backend configuration saved to ${BACKEND_CONFIG_FILE_PATH}`); 79 | } 80 | }); 81 | } 82 | }); 83 | } 84 | 85 | fetchConfig(); 86 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | import { withAuthenticator } from 'aws-amplify-react'; 4 | 5 | import './App.css'; 6 | import IotTutorialMenu from './components/Menu'; 7 | import HomePage from './components/Home'; 8 | import Things from './components/Things'; 9 | import Telemetry from './components/Telemetry'; 10 | 11 | import Amplify from 'aws-amplify'; 12 | 13 | import Config from './config'; 14 | 15 | Amplify.configure({ 16 | Auth: { 17 | identityPoolId: Config.IdentityPoolId, 18 | region: Config.region, 19 | userPoolId: Config.UserPoolId, 20 | userPoolWebClientId: Config.ReactAppClientId, 21 | mandatorySignIn: true, 22 | } 23 | }); 24 | 25 | class App extends Component { 26 | render() { 27 | return ( 28 | 29 |
30 | 31 |
32 | 33 | 34 | 35 | 36 | window.location = 'https://aws.amazon.com/cli/'}/> 37 |
38 |
39 |
40 | 41 | ); 42 | } 43 | } 44 | 45 | // export default App; 46 | export default withAuthenticator(App, { 47 | includeGreetings: true, 48 | }); -------------------------------------------------------------------------------- /frontend/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Container, Header, Image, Item } from 'semantic-ui-react'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | export default class HomePage extends Component { 6 | 7 | render() { 8 | 9 | return ( 10 |
11 |
12 | 13 | Acme-Bots IoT Application 14 |
15 | 16 | Acme Bots offers robotics services for customers in several 17 | industries, such as (but not limited to) entertainment, 18 | security, oil & gas, building, retail and manufacturing. 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Menu } from 'semantic-ui-react'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | export default class IotTutorialMenu extends Component { 6 | state = { activeItem: 'home' }; 7 | 8 | handleItemClick = (e, { name }) => this.setState({ activeItem: name }); 9 | 10 | render() { 11 | const { activeItem } = this.state; 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | } -------------------------------------------------------------------------------- /frontend/src/components/Telemetry.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Divider, Dropdown, Form, Icon, Label, Message, Popup, Statistic } from 'semantic-ui-react'; 3 | import { Area, AreaChart, Legend, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; 4 | import moment from 'moment'; 5 | import { Auth } from 'aws-amplify'; 6 | import Amplify, { PubSub } from 'aws-amplify'; 7 | // import { AWSIoTProvider } from 'aws-amplify/lib/PubSub/Providers'; 8 | import { AWSIoTProvider } from '@aws-amplify/pubsub/lib/Providers'; 9 | import Config from '../config'; 10 | import DynamoDB from 'aws-sdk/clients/dynamodb'; 11 | import IoT from 'aws-sdk/clients/iot'; 12 | import uuidv4 from 'uuid/v4'; 13 | 14 | var mqttSubscription = null; 15 | 16 | Amplify.addPluggable(new AWSIoTProvider({ 17 | aws_pubsub_region: Config.region, 18 | aws_pubsub_endpoint: `wss://${Config.iotEndpointAddress}/mqtt`, 19 | })); 20 | 21 | function fetchThings(thingName, cb) { 22 | Auth.currentCredentials() 23 | .then(credentials => { 24 | const ddb = new DynamoDB({ 25 | apiVersion: '2012-08-10', 26 | credentials: Auth.essentialCredentials(credentials), 27 | region: Config.region, 28 | }); 29 | var params = { 30 | TableName: Config.iotThingsTable, 31 | } 32 | if(thingName !== '') { 33 | params['FilterExpression'] = "begins_with(thingName, :tn)" 34 | params['ExpressionAttributeValues'] = {":tn": {"S":thingName}} 35 | } 36 | ddb.scan(params, function(err, data) { 37 | if (err) cb(err, null); 38 | else { 39 | if(cb) cb(null, data); 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | function attachIotPolicy() { 46 | Auth.currentCredentials(). 47 | then((credentials) => { 48 | const iot = new IoT({ 49 | apiVersion: '2015-05-28', 50 | credentials: Auth.essentialCredentials(credentials), 51 | region: Config.region, 52 | }); 53 | var params = { 54 | policyName: Config.IotPolicyName, 55 | principal: credentials._identityId 56 | }; 57 | iot.attachPrincipalPolicy(params, function(err, data) { 58 | if (err) console.log(err, err.stack); 59 | }); 60 | }); 61 | } 62 | 63 | class CmdCtrlPopup extends React.Component { 64 | 65 | constructor(props) { 66 | super(props); 67 | 68 | this.state = { 69 | connected: false, 70 | subscription: null, 71 | cmd: '', 72 | resp: '', 73 | sentAt: '', 74 | receivedAt: '' 75 | }; 76 | } 77 | 78 | componentWillUnmount() { 79 | var sub = this.state.subscription; 80 | if (sub !== null) { 81 | sub.unsubscribe(); 82 | } 83 | } 84 | 85 | handleChange = (e, { value }) => { 86 | this.setState({cmd: value}); 87 | } 88 | 89 | handleNewCmdResp(data) { 90 | var now = new Date(); 91 | this.setState({ 92 | resp: JSON.stringify(data.value), 93 | receivedAt: now 94 | }); 95 | } 96 | 97 | handleCmdsPopupOpen = () => { 98 | var topic = `myThings/${this.props.thing}/cmds/ack`; 99 | const cliendId = uuidv4(); 100 | var opts = {clientId: cliendId}; 101 | this.setState({ 102 | subscription: PubSub.subscribe(topic, opts).subscribe({ 103 | next: data => this.handleNewCmdResp(data), 104 | error: err => console.err(err), 105 | close: () => console.log('Done'), 106 | }) 107 | }); 108 | 109 | this.props.onCmdsPopupOpen(); 110 | this.setState({ 111 | connected: true, 112 | positiveMessage: `MQTT topic: myThings/${this.state.selected}/telemetry` 113 | }); 114 | 115 | } 116 | 117 | handleCmdsPopupClose = () => { 118 | const sub = this.state.subscription; 119 | if (sub !== null) { 120 | sub.unsubscribe(); 121 | } 122 | 123 | this.props.onCmdsPopupClose(); 124 | this.setState({ 125 | connected: false, 126 | cmd: '', 127 | resp: '', 128 | sentAt: '', 129 | receivedAt: '' 130 | }); 131 | } 132 | 133 | handleSendCmd = () => { 134 | var now = new Date(); 135 | PubSub.publish(`myThings/${this.props.thing}/cmds`, JSON.parse(this.state.cmd)) 136 | .then(() => { 137 | this.setState({ 138 | sentAt: now, 139 | }); 140 | }); 141 | } 142 | 143 | render() { 144 | 145 | const options = [ 146 | { 147 | text: 'otaUpdate 1.0', 148 | value: JSON.stringify({cmd: "otaUpdate", version: "1.0"}), 149 | }, 150 | { 151 | text: 'otaUpdate 2.0', 152 | value: JSON.stringify({cmd: "otaUpdate", version: "2.0"}), 153 | }, 154 | { 155 | text: 'startWork', 156 | value: JSON.stringify({cmd: "startWork"}), 157 | }, 158 | { 159 | text: 'standBy', 160 | value: JSON.stringify({cmd: "standby"}), 161 | }, 162 | { 163 | text: 'enableAutoCharging', 164 | value: JSON.stringify({cmd: "enableAutoCharging"}), 165 | }, 166 | { 167 | text: 'disableAutoCharging', 168 | value: JSON.stringify({cmd: "disableAutoCharging"}), 169 | }, 170 | ]; 171 | let cmdContent; 172 | var cmd = this.state.cmd; 173 | if (this.state.sentAt) { 174 | cmdContent = 175 |
176 | {cmd} 177 | 178 | Last sent at: 179 |
; 180 | } else { 181 | cmdContent = 182 |

183 | {cmd} 184 |

; 185 | } 186 | let respContent; 187 | var resp = this.state.resp; 188 | if (this.state.receivedAt) { 189 | respContent = 190 |
191 | {resp} 192 | 193 | Last received at: 194 | Latency: ms. 195 |
; 196 | } else { 197 | respContent = 198 |

199 | {resp} 200 |

; 201 | } 202 | return ( 203 | } 206 | content={ 207 |
208 | 215 | 216 | 217 | myThings/{this.props.thing}/cmds 218 | 219 | {cmdContent} 220 | 221 | 222 | myThings/{this.props.thing}/cmds/ack 223 | 224 | {respContent} 225 | 226 |
234 | } 235 | on='click' 236 | open={this.props.open} 237 | onOpen={this.handleCmdsPopupOpen} 238 | position='top right' 239 | /> 240 | ) 241 | } 242 | } 243 | class ThingsForm extends React.Component { 244 | 245 | constructor(props) { 246 | super(props); 247 | 248 | this.state = { 249 | cmdsPopupOpen: false, 250 | }; 251 | 252 | this.handleChange = this.handleChange.bind(this); 253 | this.handleSubscribeRequested = this.handleSubscribeRequested.bind(this); 254 | this.handleDisconnectRequested = this.handleDisconnectRequested.bind(this); 255 | } 256 | 257 | handleChange(e, { value }) { 258 | this.props.onChange(value); 259 | } 260 | 261 | handleSubscribeRequested() { 262 | this.props.onSubscribeRequested(); 263 | } 264 | 265 | handleDisconnectRequested() { 266 | this.props.onDisconnectRequested(); 267 | } 268 | 269 | handleCmdsPopupOpen = () => { 270 | this.setState({ cmdsPopupOpen: true }); 271 | } 272 | 273 | handleCmdsPopupClose = () => { 274 | this.setState({ cmdsPopupOpen: false }); 275 | } 276 | 277 | render() { 278 | const options = [] 279 | this.props.things.map(thing => { 280 | var thingName = thing.thingName.S 281 | options.push( { key: thingName, text: thingName, value: thingName } ); 282 | }); 283 | 284 | const positiveMessage = this.props.positiveMessage; 285 | var headerMessage = `Successfully Subscribed to ${this.props.selected} Telemetry` 286 | 287 | let positiveMessageComponent; 288 | 289 | if (positiveMessage) { 290 | positiveMessageComponent = 291 |
292 | 293 | 294 | 295 | {headerMessage} 296 | {positiveMessage} 297 | 298 | 299 |
301 | } 302 | 303 | 304 | return( 305 |
306 |
307 |
308 | 309 | 310 | 311 | 320 | 321 | 322 | 329 | 336 | 337 | 338 | 344 | 345 | 346 |
347 |
348 |
349 | {positiveMessageComponent} 350 |
351 |
352 | ) 353 | } 354 | } 355 | 356 | class ThingStatus extends Component { 357 | render() { 358 | var versionLabel = (this.props.version === '') ? '?' : this.props.version 359 | 360 | var statusLabel = '?'; 361 | if (this.props.status === 'standby') { 362 | statusLabel = 363 |
364 | {this.props.status} 365 |
; 366 | } else if (this.props.status === 'charging') { 367 | statusLabel = 368 |
369 | {this.props.status} 370 |
; 371 | 372 | } else if (this.props.status === 'working') { 373 | statusLabel = 374 |
375 | {this.props.status} 376 |
; 377 | } 378 | 379 | var lastReceivedAtLabel = '?'; 380 | if (this.props.lastTelemetryReceivedAt) { 381 | var d = new Date(this.props.lastTelemetryReceivedAt); 382 | lastReceivedAtLabel = 383 |
384 | {moment(d).format('HH:mm:ss')} 385 |
; 386 | } 387 | 388 | return ( 389 |
390 | 391 | 392 | {versionLabel} 393 | Version 394 | 395 | 396 | 397 | {statusLabel} 398 | 399 | Status 400 | 401 | 402 | {lastReceivedAtLabel} 403 | Last data received at 404 | 405 | 406 |
407 | ) 408 | } 409 | } 410 | 411 | class BatteryLifeChart extends Component { 412 | 413 | render() { 414 | return ( 415 |
416 | 417 | 418 | moment(unixTime).format('HH:mm::ss')} 421 | /> 422 | 423 | moment(unixTime).format('HH:mm::ss')} 425 | formatter={(value) => `${value} %`} 426 | /> 427 | 428 | 429 | 430 | 431 | 432 |
433 | ) 434 | } 435 | } 436 | export default class Telemetry extends Component { 437 | 438 | constructor(props) { 439 | super(props); 440 | 441 | this.state = { 442 | things: [], 443 | botData: [], 444 | botStatus: '', 445 | botVersion: '', 446 | lastTelemetryReceivedAt: '', 447 | selected: '', 448 | negativeMessage: '', 449 | searching: false, 450 | subscribing: false, 451 | unsibscribing: false, 452 | connected: false, 453 | }; 454 | 455 | this.handleChange = this.handleChange.bind(this); 456 | this.handleSubscribeRequested = this.handleSubscribeRequested.bind(this); 457 | this.handleDisconnectRequested = this.handleDisconnectRequested.bind(this); 458 | } 459 | 460 | componentDidMount() { 461 | var component = this; 462 | component.setState({searching: true}); 463 | fetchThings('', function(err, data){ 464 | component.setState({searching: false}); 465 | if(err) { 466 | component.setState({ 467 | negativeMessage: `${err.message}\n${err.stack}` 468 | }); 469 | } else { 470 | component.setState({ 471 | things: data.Items 472 | }); 473 | } 474 | }); 475 | attachIotPolicy(); 476 | } 477 | 478 | 479 | 480 | componentWillUnmount() { 481 | if (mqttSubscription !== null) { 482 | mqttSubscription.unsubscribe(); 483 | } 484 | } 485 | 486 | handleChange(value) { 487 | this.setState({selected: value}); 488 | } 489 | 490 | handleNewTelemetryData(data) { 491 | var new_arr = this.state.botData.concat(data.value.telemetry); 492 | var index = data.value.telemetry.length-1; 493 | var last_item = data.value.telemetry[index]; 494 | 495 | this.setState({ 496 | botData: new_arr, 497 | botStatus: last_item.status, 498 | botVersion: last_item.version, 499 | lastTelemetryReceivedAt: Date.now() 500 | }); 501 | } 502 | 503 | handleSubscribeRequested() { 504 | var topic = `myThings/${this.state.selected}/telemetry`; 505 | const cliendId = uuidv4(); 506 | var opts = {clientId: cliendId}; 507 | mqttSubscription = PubSub.subscribe(topic, opts).subscribe({ 508 | next: data => this.handleNewTelemetryData(data), 509 | error: err => console.err(err), 510 | close: () => console.log('Done'), 511 | }); 512 | 513 | this.setState({ 514 | connected: true, 515 | positiveMessage: `MQTT topic: myThings/${this.state.selected}/telemetry` 516 | }); 517 | } 518 | 519 | handleDisconnectRequested() { 520 | if (mqttSubscription !== null) { 521 | mqttSubscription.unsubscribe(); 522 | } 523 | this.setState({ 524 | connected: false, 525 | positiveMessage: '', 526 | botData: [], 527 | botStatus: '', 528 | botVersion: '', 529 | lastTelemetryReceivedAt: '' 530 | }); 531 | } 532 | 533 | render() { 534 | let chartComponent; 535 | let thingStatusComponent; 536 | 537 | if (this.state.connected) { 538 | chartComponent = 539 | ; 540 | thingStatusComponent = 541 |
542 | 545 | 546 |
; 547 | } 548 | return ( 549 |
550 | 561 | {thingStatusComponent} 562 | {chartComponent} 563 |
564 | ) 565 | } 566 | } -------------------------------------------------------------------------------- /frontend/src/components/Things.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Form, Header, Icon, Image, Input, Message, Table} from 'semantic-ui-react'; 3 | import DynamoDB from 'aws-sdk/clients/dynamodb'; 4 | import Lambda from 'aws-sdk/clients/lambda'; 5 | import { Auth } from 'aws-amplify'; 6 | import Config from '../config'; 7 | 8 | function fetchThings(thingName, cb) { 9 | Auth.currentCredentials() 10 | .then(credentials => { 11 | const ddb = new DynamoDB({ 12 | apiVersion: '2012-08-10', 13 | credentials: Auth.essentialCredentials(credentials), 14 | region: Config.region, 15 | }); 16 | var params = { 17 | TableName: Config.iotThingsTable, 18 | } 19 | if(thingName !== '') { 20 | params['FilterExpression'] = "begins_with(thingName, :tn)" 21 | params['ExpressionAttributeValues'] = {":tn": {"S":thingName}} 22 | } 23 | ddb.scan(params, function(err, data) { 24 | if (err) cb(err, null); 25 | else { 26 | if(cb) cb(null, data); 27 | } 28 | }); 29 | }); 30 | } 31 | 32 | function provisionThing(thingName, cb) { 33 | Auth.currentCredentials() 34 | .then(credentials => { 35 | const lambda = new Lambda({ 36 | apiVersion: '2015-03-31', 37 | credentials: Auth.essentialCredentials(credentials), 38 | region: Config.region, 39 | }); 40 | var params = { 41 | ClientContext: "MyApp", 42 | FunctionName: Config.IotProvisionThingLambdaFunctionQualifiedArn, 43 | InvocationType: "Event", 44 | Payload: JSON.stringify({thingName: thingName}) 45 | }; 46 | lambda.invoke(params, function(err, data) { 47 | if (err) console.log(err, err.stack); 48 | else { 49 | if(cb) cb(null, data) 50 | } 51 | }); 52 | }) 53 | } 54 | 55 | function removeThing(thingName, cb){ 56 | Auth.currentCredentials() 57 | .then(credentials => { 58 | const lambda = new Lambda({ 59 | apiVersion: '2015-03-31', 60 | credentials: Auth.essentialCredentials(credentials), 61 | region: Config.region, 62 | }); 63 | var params = { 64 | ClientContext: "MyApp", 65 | FunctionName: Config.IotRemoveThingLambdaFunctionQualifiedArn, 66 | InvocationType: "Event", 67 | Payload: JSON.stringify({thingName: thingName}) 68 | }; 69 | lambda.invoke(params, function(err, data) { 70 | if (err) console.log(err, err.stack); 71 | else { 72 | if(cb) cb(null, data) 73 | } 74 | }); 75 | }) 76 | } 77 | 78 | class ThingsForm extends React.Component { 79 | 80 | constructor(props) { 81 | super(props); 82 | this.handleFilterTextChange = this.handleFilterTextChange.bind(this); 83 | this.handleCreateRequest = this.handleCreateRequest.bind(this); 84 | this.handleSearchRequested = this.handleSearchRequested.bind(this); 85 | } 86 | 87 | handleFilterTextChange(e) { 88 | this.props.onFilterTextChange(e.target.value); 89 | } 90 | 91 | handleSearchRequested() { 92 | this.props.onSearchRequested(); 93 | } 94 | 95 | handleCreateRequest() { 96 | this.props.onCreateRequested(); 97 | } 98 | 99 | render() { 100 | 101 | const positiveMessage = this.props.positiveMessage; 102 | const negativeMessage = this.props.negativeMessage; 103 | 104 | let positiveMessageComponent; 105 | let negativeMessageComponent; 106 | 107 | if (positiveMessage) { 108 | positiveMessageComponent = 109 | 113 | 114 | } 115 | 116 | if (negativeMessage) { 117 | negativeMessageComponent = 118 | 122 | 123 | } 124 | 125 | return( 126 |
127 | {positiveMessageComponent} 128 | {negativeMessageComponent} 129 |
130 | 131 | 132 | 133 | 134 | 135 | 136 | 139 | 140 | 141 | 142 | 143 | 144 |
145 |
146 | ) 147 | } 148 | } 149 | 150 | class TableFooter extends React.Component { 151 | render() { 152 | return ( 153 | 154 | 155 | Returned Items: {this.props.returnedItems} 156 | Scanned Items: {this.props.scannedCount} 157 | 158 | 159 | ) 160 | } 161 | } 162 | 163 | class ThingsTableleRow extends React.Component { 164 | 165 | constructor(props) { 166 | super(props); 167 | 168 | this.state = { 169 | deleteRequested: false 170 | }; 171 | this.handleRemoveThing = this.handleRemoveThing.bind(this); 172 | } 173 | 174 | handleRemoveThing() { 175 | const thingName = this.props.thing.thingName.S; 176 | const component = this; 177 | removeThing(thingName, function(err, data){ 178 | if(err) { 179 | console.log(err, err.stack); 180 | } else { 181 | component.setState({ 182 | deleteRequested: true 183 | }); 184 | } 185 | }); 186 | this.setState({deleteRequested: true}); 187 | } 188 | 189 | render() { 190 | var thing = this.props.thing; 191 | var component = this; 192 | var deleteRequested = this.state.deleteRequested; 193 | 194 | return( 195 | 198 | 199 |
200 | 201 | 202 | {thing.thingName.S} 203 | 204 |
205 |
206 | 207 | 212 | 213 |
214 | ) 215 | } 216 | } 217 | 218 | class ThingsTable extends React.Component {g 219 | 220 | render() { 221 | return ( 222 | 223 | 224 | 225 | Name 226 | Actions 227 | 228 | 229 | 230 | {this.props.things.map(thing => { 231 | return ( 232 | 236 | ) 237 | })} 238 | 239 | 240 |
241 | ) 242 | } 243 | } 244 | class ThingsPanel extends React.Component { 245 | 246 | constructor(props) { 247 | super(props); 248 | this.state = {filterText: '', 249 | things: [], 250 | scannedCount: 0, 251 | removeRequests: [], 252 | createRequests: [], 253 | positiveMessage: '', 254 | negativeMessage: '', 255 | searching: false, 256 | }; 257 | 258 | this.handleFilterTextChange = this.handleFilterTextChange.bind(this); 259 | this.handleCreateRequested = this.handleCreateRequested.bind(this); 260 | this.handleSearchRequested = this.handleSearchRequested.bind(this); 261 | } 262 | 263 | componentDidMount() { 264 | var component = this; 265 | component.setState({searching: true}); 266 | fetchThings('', function(err, data){ 267 | component.setState({searching: false}); 268 | if(err) { 269 | component.setState({ 270 | negativeMessage: `${err.message}\n${err.stack}` 271 | }); 272 | } else { 273 | component.setState({ 274 | things: data.Items, 275 | scannedCount: data.ScannedCount 276 | }); 277 | } 278 | }); 279 | } 280 | 281 | handleFilterTextChange(filterText) { 282 | this.setState({filterText: filterText}); 283 | } 284 | 285 | handleSearchRequested() { 286 | var component = this; 287 | component.setState({searching: true}); 288 | fetchThings(this.state.filterText, function(err, data){ 289 | component.setState({searching: false}); 290 | if(err) { 291 | this.setState({ 292 | negativeMessage: `${err.message}\n${err.stack}` 293 | }); 294 | } else { 295 | component.setState({ 296 | things: data.Items, 297 | scannedCount: data.ScannedCount, 298 | positiveMessage: '' 299 | }); 300 | } 301 | }); 302 | } 303 | 304 | handleCreateRequested() { 305 | var component = this; 306 | const thingName = this.state.filterText; 307 | provisionThing(thingName, function(err, data){ 308 | if(err) { 309 | this.setState({ 310 | negativeMessage: `${err.message}\n${err.stack}` 311 | }); 312 | } else { 313 | var arr = component.state.createRequests; 314 | arr.push(thingName); 315 | component.setState({ 316 | createRequests: arr, 317 | positiveMessage: `Successfully sent a create request for ${component.state.filterText}.` 318 | }); 319 | } 320 | }); 321 | } 322 | 323 | render() { 324 | 325 | let searchingMessage; 326 | let thingsTable; 327 | if(this.state.searching) { 328 | searchingMessage = 329 | 330 | 331 | 332 | Just one second 333 | We are fetching that content for you. 334 | 335 | 336 | } else { 337 | thingsTable = 338 | 342 | } 343 | return ( 344 |
345 | 353 | {searchingMessage} 354 | {thingsTable} 355 |
356 | ); 357 | } 358 | } 359 | 360 | class ThingsPage extends React.Component { 361 | 362 | render() { 363 | return ( 364 |
365 | 366 |
367 | ); 368 | } 369 | } 370 | 371 | export default ThingsPage -------------------------------------------------------------------------------- /frontend/src/config/index.js: -------------------------------------------------------------------------------- 1 | import Config from './config.json'; 2 | 3 | export default { 4 | region: Config.region, 5 | IdentityPoolId: Config.IdentityPoolId, 6 | UserPoolId: Config.UserPoolId, 7 | ReactAppClientId: Config.ReactAppClientId, 8 | iotThingsTable: Config.iotThingsTable, 9 | IotProvisionThingLambdaFunctionQualifiedArn: Config.IotProvisionThingLambdaFunctionQualifiedArn, 10 | IotRemoveThingLambdaFunctionQualifiedArn: Config.IotRemoveThingLambdaFunctionQualifiedArn, 11 | iotEndpointAddress: Config.iotEndpointAddress, 12 | IotPolicyName: Config.AcmeBotsGuiIotPolicy, 13 | }; -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | .pie-row .pie-wrap > div { 8 | background: red; 9 | margin: 0 auto; 10 | } -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | import "semantic-ui-css/semantic.min.css"; 4 | import ReactDOM from 'react-dom'; 5 | import './index.css'; 6 | import App from './App'; 7 | import registerServiceWorker from './registerServiceWorker'; 8 | 9 | ReactDOM.render(, document.getElementById('root')); 10 | registerServiceWorker(); 11 | -------------------------------------------------------------------------------- /frontend/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------