├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── general_question.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE.txt ├── README.md ├── architecture.png ├── deployment ├── build-s3-dist.sh └── run-unit-tests.sh ├── solution-manifest.yaml └── source ├── console ├── package-lock.json ├── package.json ├── public │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── components │ │ ├── DeviceTypeCreate │ │ │ ├── AttributeFields.tsx │ │ │ └── ModalForm.tsx │ │ ├── Shared │ │ │ ├── DeleteConfirmation.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ ├── Interfaces.ts │ │ │ └── PageTitleBar.tsx │ │ ├── SimulationCreate │ │ │ └── DeviceFields.tsx │ │ └── Simulations │ │ │ └── TableData.tsx │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── setupTests.ts │ ├── util │ │ ├── Utils.ts │ │ └── lang │ │ │ └── en.json │ └── views │ │ ├── DeviceTypeCreate.tsx │ │ ├── DeviceTypes.tsx │ │ ├── PageNotFound.tsx │ │ ├── SimulationCreate.tsx │ │ ├── SimulationDetails.tsx │ │ └── Simulations.tsx └── tsconfig.json ├── custom-resource ├── index.ts ├── interfaces.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── test │ └── index.test.ts └── tsconfig.json ├── infrastructure ├── .gitignore ├── .npmignore ├── bin │ └── iot-device-simulator.ts ├── cdk.json ├── jest.config.js ├── lib │ ├── api.ts │ ├── application-resource.ts │ ├── common-resources.ts │ ├── console.ts │ ├── custom-resource.ts │ ├── iot-device-simulator-stack.ts │ ├── simulator.ts │ └── storage.ts ├── package-lock.json ├── package.json ├── test │ ├── api.test.ts │ ├── common-resources.test.ts │ ├── console.test.ts │ ├── custom-resource.test.ts │ ├── iot-device-simulator.stack.test.ts │ ├── simulator.test.ts │ └── storage.test.ts ├── tsconfig.json └── utils │ └── utils.ts ├── microservices ├── index.js ├── lib │ ├── deviceTypeManager.js │ ├── responseManager.js │ ├── responseManager.spec.js │ └── simulationManager.js ├── metrics │ └── index.js ├── package-lock.json └── package.json ├── resources └── routes │ ├── manifest.json │ ├── route-a.json │ ├── route-b.json │ ├── route-c.json │ ├── route-d.json │ ├── route-e.json │ ├── route-f.json │ ├── route-g.json │ ├── route-h.json │ ├── route-i.json │ ├── route-j.json │ ├── route-k.json │ ├── route-l.json │ ├── route-m.json │ ├── route-n.json │ ├── route-o.json │ ├── route-p.json │ └── route-q.json └── simulator ├── index.js ├── index.spec.js ├── lib ├── device │ ├── generators │ │ ├── random │ │ │ ├── generator.js │ │ │ └── generator.spec.js │ │ └── vehicle │ │ │ ├── dynamics │ │ │ ├── acceleration-calc.js │ │ │ ├── data-calc.js │ │ │ ├── dynamics-model.js │ │ │ ├── engine-speed-calc.js │ │ │ ├── fuel-consumed-calc.js │ │ │ ├── fuel-level-calc.js │ │ │ ├── gear-calc.js │ │ │ ├── gear-int-calc.js │ │ │ ├── heading-calc.js │ │ │ ├── lat-calc.js │ │ │ ├── lon-calc.js │ │ │ ├── odometer-calc.js │ │ │ ├── oil-temp-calc.js │ │ │ ├── route-calc.js │ │ │ ├── speed-calc.js │ │ │ └── torque-calc.js │ │ │ ├── generator.js │ │ │ └── generator.spec.js │ ├── index.js │ └── index.spec.js └── engine │ ├── index.js │ └── index.spec.js ├── package-lock.json └── package.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 15 | 16 | **Expected behavior** 17 | 18 | 19 | **Please complete the following information about the solution:** 20 | - [ ] Version: [e.g. v1.0.0] 21 | - [ ] Region: [e.g. us-east-1] 22 | - [ ] Was the solution modified from the version published on this repository? 23 | - [ ] If the answer to the previous question was yes, are the changes available on GitHub? 24 | - [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the services this solution uses? 25 | - [ ] Were there any errors in the CloudWatch Logs? 26 | 27 | **Screenshots** 28 | 29 | 30 | **Additional context** 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this solution 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the feature you'd like** 14 | 15 | 16 | **Additional context** 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General question 3 | about: Ask a general question 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What is your question?** 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Issue #, if available:** 2 | 3 | 4 | 5 | 6 | **Description of changes:** 7 | 8 | 9 | 10 | **Checklist** 11 | - [ ] :wave: I have run the unit tests, and all unit tests have passed. 12 | - [ ] :warning: This pull request might incur a breaking change. 13 | 14 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | **/dist 3 | **/build 4 | **/open-source 5 | **/.zip 6 | **/tmp 7 | **/out-tsc 8 | **/global-s3-assets 9 | **/regional-s3-assets 10 | 11 | # dependencies 12 | **/node_modules 13 | 14 | # e2e 15 | **/e2e/*.js 16 | **/e2e/*.map 17 | 18 | # misc 19 | **/npm-debug.log 20 | **/testem.log 21 | **/.vscode/settings.json 22 | **/yarn.lock 23 | **/aws_config.js 24 | **/coverage 25 | lcov.info 26 | clover.xml 27 | coverage-final.json 28 | 29 | # System Files 30 | **/.DS_Store 31 | **/.vscode 32 | 33 | # CDK asset staging directory 34 | .cdk.staging 35 | cdk.out -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [3.0.9] - 2024-10-29 8 | ### Security 9 | - Addressed security vulnerabilities found in npm packages 10 | 11 | ## [3.0.8] - 2024-08-19 12 | ### Security 13 | - Addressed security vulnerabilities found in npm packages 14 | 15 | ## [3.0.7] - 2024-07-16 16 | ### Security 17 | - Addressed security vulnerabilities found in npm packages 18 | 19 | ## [3.0.6] - 2024-04-03 20 | ### Security 21 | - Addressed security vulnerabilities found in npm packages 22 | 23 | ## [3.0.5] - 2024-02-05 24 | ### Fixed 25 | - Pinned the cdk-nag package version to 2.27.104 26 | 27 | ## [3.0.4] - 2023-10-20 28 | ### Security 29 | - Addressed security vulnerabilities found in npm packages 30 | 31 | ## [3.0.3] - 2023-08-17 32 | ### Changed 33 | - Updated AWS Amplify 4 to 5 and UI login page 34 | - Migrated AWS SDK for JavaScript V2 to V3 35 | - Updated outdated libraries 36 | - Improvement to service API metrics collection 37 | 38 | ### Fixed 39 | - Simulations map not working 40 | - Fix Device Type and simulation deletion UI issue (GitHub [issue #29](https://github.com/aws-solutions/iot-device-simulator/issues/29)) 41 | 42 | ### Removed 43 | - CDK Bootstrap requirement for provisioning the CloudFormation stack 44 | 45 | ### Security 46 | - Addressed security vulnerabilities found in npm packages 47 | 48 | ## [3.0.2] - 2023-06-12 49 | ### Changed 50 | - Added deployment details in README.md 51 | 52 | ### Fixed 53 | - fast-xml-parser vulnerability 54 | 55 | ## [3.0.1] - 2023-04-27 56 | ### Changed 57 | - Upgraded to Node 18 58 | - Added App registry integration 59 | - Upgraded to CDK 2, React Scripts 5 60 | - Upgraded to use ES2022 61 | 62 | ## [3.0.0] - 2021-11-22 63 | ⚠️ BREAKING CHANGES 64 | 65 | v3.0.0 does not support to upgrade from the previous version due to 66 | - Design change 67 | - Using AWS Step Functions instead of AWS Fargate to run a simulator 68 | - Merged `devices` and `widgets` into `simulations` 69 | - Merged `device-widgets`, `sim-settings`, and `sim-metrics` Amazon DynamoDB tables into `Simulator` DynamoDB table 70 | - Merged all microservices AWS Lambda function into a single `microservices` AWS Lambda function 71 | - AWS CDK to generate AWS CloudFormation template 72 | 73 | ### Added 74 | - Support import/Export of device types 75 | - Multiple vehicles displayed on map 76 | - Ability to define a character set (an alphabet from which the ID will be created) and the length of the ID attribute 77 | 78 | ### Changed 79 | - Replace Mapbox map to Amazon Location Service map for the automotive demo 80 | - UI framework migration: Angular JS to React 81 | - Change `helper` AWS Lambda function to TypeScript 82 | - Abstracted devices - devices are no longer created and stored, they will be generated in the simulator lambda using the specified device type and amount specified for each device type 83 | - Simplified `boolean` attribute: no `max` and `min` to generate `boolean` data 84 | - Automotive demo attributes aggregated into a single message 85 | 86 | ### Fixed 87 | - Unix timestamp to show the epoch time correctly 88 | 89 | ### Removed 90 | - Data attributes: `uuid`, `shortid` 91 | 92 | ## [2.1.1] - 2019-12-20 93 | ### Added 94 | - Lambda runtime updated to Node.js 12 95 | - CloudFront distribution access log enabled 96 | - S3 access log enabled 97 | - Public access and public ACLs denied on S3 buckets 98 | - Encryption at rest enabled for S3 and SQS using AWS default keys 99 | - Docker build image updated to Node.js 12 on ECR 100 | 101 | ## [2.1.0] - 2019-03-23 102 | ### Added 103 | - Removed unauthenticated role from the Amazon Cognito Identity Pool 104 | 105 | ## [2.0.0] - 2019-02-27 106 | ### Added 107 | - Added new data generation algorithms: Sinusoidal and Decay 108 | - Added new attribute type for nested (JSON) objects (up to 3 deep) for device type payloads 109 | - Added ability to share Device Templates between users of the same installation 110 | - Update user interface paging size for widget and fleet listings to 100 111 | - Update launch and start limits for widgets and fleets to 100 112 | - Added ability for users to stop simulations (single or in bulk of 100) for widgets and vehicles. 113 | - Add search ability for widgets (or vehicles) by device id, device type and status 114 | - DynamoDB tables switched to be created with PAY_PER_REQUEST billing mode vice PROVISIONED capacity 115 | - Added ability for device attributes to be included in the topic as variables 116 | - Resolved availability zone offset issue with Fargate cluster VPC 117 | 118 | ## [1.1.0] - 2018-11-05 119 | ### Added 120 | - Change manifest generator to use writeFileSync to eliminate callback deprecation error-* Added development template [iot-device-simualtor.yaml] to deployment folder 121 | - Added Amazon CloudFront distribution to simulator console 122 | - Set Amazon CloudFront OAI restrictive permissions for simulator console Amazon S3 bucket 123 | - Updated signin URL for user invitation emails to Amazon CloudFront distribution domain 124 | - Added new device type attribute `DEVICE ID` which provides a static unique identifier for each generated device constant across simulations 125 | - Added ability to bulk start devices by selecting multiple devices on a given page and “one click start” the simulations for multiple devices 126 | - Migration from VeriSign AWS IoT endpoints to ATS AWS IoT endpoints 127 | 128 | ## [1.0.1] - 2018-05-23 129 | ### Added 130 | - Added fix for creation of Elastic IPs in legacy accounts that are not vpc-by-default 131 | - Added fix for administration microservice IAM policy to include all required permissions to manage users through the simulator console 132 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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-solutions/iot-device-simulator/issues), or [recently closed](https://github.com/aws-solutions/iot-device-simulator/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-solutions/iot-device-simulator/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-solutions/iot-device-simulator/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. -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | IoT Device Simulator 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Licensed under the Apache License Version 2.0 (the "License"). You may not use this file except 4 | in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/ 5 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 6 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the 7 | specific language governing permissions and limitations under the License. 8 | 9 | ********************** 10 | THIRD PARTY COMPONENTS 11 | ********************** 12 | This software includes third party software subject to the following copyrights: 13 | 14 | @aws-amplify/api under the Apache License 2.0 15 | @aws-amplify/auth under the Apache License 2.0 16 | @aws-amplify/core under the Apache License 2.0 17 | @aws-amplify/geo under the Apache License 2.0 18 | @aws-amplify/interactions under the Apache License 2.0 19 | @aws-amplify/storage under the Apache License 2.0 20 | @aws-amplify/ui-react under the Apache License 2.0 21 | @aws-cdk/aws-cloudfront under the Apache License 2.0 22 | @aws-cdk/aws-apigateway under the Apache License 2.0 23 | @aws-cdk/aws-cognito under the Apache License 2.0 24 | @aws-cdk/aws-dynamodb under the Apache License 2.0 25 | @aws-cdk/aws-iam under the Apache License 2.0 26 | @aws-cdk/aws-iot under the Apache License 2.0 27 | @aws-cdk/aws-lambda under the Apache License 2.0 28 | @aws-cdk/aws-location under the Apache License 2.0 29 | @aws-cdk/aws-logs under the Apache License 2.0 30 | @aws-cdk/aws-s3 under the Apache License 2.0 31 | @aws-cdk/aws-servicecatalogappregistry-alpha under the Apache License 2.0 32 | @aws-cdk/aws-stepfunctions under the Apache License 2.0 33 | @aws-cdk/aws-stepfunctions-tasks under the Apache License 2.0 34 | @aws-cdk/core under the Apache License 2.0 35 | @aws-sdk/client-iot under the Apache License 2.0 36 | @aws-sdk/client-s3 under the Apache License 2.0 37 | @aws-sdk/client-dynamodb under the Apache License 2.0 38 | @aws-sdk/client-sfn under the Apache License 2.0 39 | @aws-sdk/client-iot-data-plane under the Apache License 2.0 40 | @aws-sdk/lib-dynamodb under the Apache License 2.0 41 | @aws-sdk/types under the Apache License 2.0 42 | @aws-sdk/util-dynamodb under the Apache License 2.0 43 | @aws-solutions-constructs/aws-cloudfront-s3 under the Apache License 2.0 44 | @aws-solutions-constructs/aws-lambda-stepfunctions under the Apache License 2.0 45 | @smithy/util-stream under the Apache License 2.0 46 | @testing-library/jest-dom under the Massachusetts Institute of Technology (MIT) License 47 | @testing-library/react under the Massachusetts Institute of Technology (MIT) License 48 | @testing-library/user-event under the Massachusetts Institute of Technology (MIT) License 49 | @types/jest under the Massachusetts Institute of Technology (MIT) License 50 | @types/node under the Massachusetts Institute of Technology (MIT) License 51 | @types/uuid under the Massachusetts Institute of Technology (MIT) License 52 | @types/react under the Massachusetts Institute of Technology (MIT) License 53 | @types/react-dom under the Massachusetts Institute of Technology (MIT) License 54 | @types/react-router-dom under the Massachusetts Institute of Technology (MIT) License 55 | axios under the Massachusetts Institute of Technology (MIT) License 56 | aws-cdk under the Apache License 2.0 57 | aws-cdk-lib under the Apache License 2.0 58 | aws-sdk under the Apache License 2.0 59 | aws-sdk-client-mock under the Massachusetts Institute of Technology (MIT) License 60 | aws-sdk-client-mock-jest under the Massachusetts Institute of Technology (MIT) License 61 | bootstrap under the Massachusetts Institute of Technology (MIT) License 62 | bootstrap-icons under the Massachusetts Institute of Technology (MIT) License 63 | cdk-nag under the Apache License 2.0 64 | faker under the Massachusetts Institute of Technology (MIT) license 65 | jest under the Massachusetts Institute of Technology (MIT) License 66 | maplibre-gl under the BSD 3-Clause license 67 | maplibre-gl-js-amplify under the Apache License 2.0 68 | moment under the Massachusetts Institute of Technology (MIT) license 69 | nanoid under the Massachusetts Institute of Technology (MIT) license 70 | random-location under the Massachusetts Institute of Technology (MIT) License 71 | react under the Massachusetts Institute of Technology (MIT) License 72 | react-bootstrap under the Massachusetts Institute of Technology (MIT) License 73 | react-dom under the Massachusetts Institute of Technology (MIT) License 74 | react-router-dom under the Massachusetts Institute of Technology (MIT) License 75 | react-scripts under the Massachusetts Institute of Technology (MIT) License 76 | ts-jest under the Massachusetts Institute of Technology (MIT) License 77 | ts-node under the Massachusetts Institute of Technology (MIT) License 78 | typescript under the Apache License 2.0 79 | web-vitals under the Apache License 2.0 80 | uuid under the Massachusetts Institute of Technology (MIT) License 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Deprecation Notice 2 | 3 | As of 01/29/2025, The IoT Device Simulator solution has been deprecated and will not be receiving any additional features or updates. You can explore other Solutions in the [AWS Solutions Library](https://aws.amazon.com/solutions/). 4 | 5 | **[IoT Device Simulator](https://aws.amazon.com/solutions/implementations/iot-device-simulator/)** | **[🚧 Feature request](https://github.com/aws-solutions/iot-device-simulator/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)** | **[🐛 Bug Report](https://github.com/aws-solutions/iot-device-simulator/issues/new?assignees=&labels=bug&template=bug_report.md&title=)** | **[❓ General Question](https://github.com/aws-solutions/iot-device-simulator/issues/new?assignees=&labels=question&template=general_question.md&title=)** 6 | 7 | **Note**: If you want to use the solution without building from source, navigate to Solution Landing Page. 8 | 9 | ## Table of Content 10 | - [Solution Overview](#solution-overview) 11 | - [Architecture Diagram](#architecture-diagram) 12 | - [AWS CDK and Solutions Constructs](#aws-cdk-and-solutions-constructs) 13 | - [Customizing the Solution](#customizing-the-solution) 14 | - [Prerequisites for Customization](#prerequisites-for-customization) 15 | - [Unit Test](#unit-test) 16 | - [Build](#build) 17 | - [Deploy](#deploy) 18 | - [License](#license) 19 | 20 | # Solution Overview 21 | IoT is a sprawling set of technologies and use cases that has no clear, single definition. Despite enormous advances, we’ve only seen a fraction of what the Internet revolution has yet to deliver. That’s because many powerful technological forces are now converging — poised to magnify, multiply, and exponentially increase the opportunities that software and the Internet can deliver by connecting the devices, or “things”, in the physical world around us. Each of these devices is able to convert valuable information from the real world into digital data that provides increased visibility to businesses of how users interact their products or services. The backend services required to process and uncover these valuable insights can be expensive to prove without a large pool of physical devices for full end to end integration setup or time-consuming development of scripts. 22 | 23 | Often times, teams constantly have the need to quickly replicate the behavior of their devices interacting with AWS IoT to assess their backend services. The IoT Device Simulator solution is a Graphical User Interface (GUI) based engine designed to enable customers to get started quickly assessing AWS IoT services without an existing pool of devices. The IoT Device Simulator leverages managed, highly available, highly scalable AWS-native services to effortlessly create and simulate thousands of connected devices that are defined by the customer. 24 | 25 | For more information and a detailed deployment guide, visit the [IoT Device Simulator](https://aws.amazon.com/solutions/implementations/iot-device-simulator/) solution page. 26 | 27 | **Note**: This solution is designed to simulate device data for testing. It is not recommended for use in production environments. 28 | 29 | # Architecture Diagram 30 | ![Architecture Diagram](./architecture.png) 31 | 32 | # AWS CDK and Solutions Constructs 33 | [AWS Cloud Development Kit (AWS CDK)](https://aws.amazon.com/cdk/) and [AWS Solutions Constructs](https://aws.amazon.com/solutions/constructs/) make it easier to consistently create well-architected infrastructure applications. All AWS Solutions Constructs are reviewed by AWS and use best practices established by the AWS Well-Architected Framework. This solution uses the following AWS Solutions Constructs: 34 | - [aws-cloudfront-s3](https://docs.aws.amazon.com/solutions/latest/constructs/aws-cloudfront-s3.html) 35 | - [aws-lambda-stepfunctions](https://docs.aws.amazon.com/solutions/latest/constructs/aws-lambda-stepfunctions.html) 36 | 37 | In addition to the AWS Solutions Constructs, the solution uses AWS CDK directly to create infrastructure resources. 38 | # Customizing the Solution 39 | ## Prerequisites for Customization 40 | - Node.js 18.x or later 41 | 42 | ### 1. Clone the repository 43 | ```bash 44 | git clone https://github.com/aws-solutions/iot-device-simulator.git 45 | cd iot-device-simulator 46 | export MAIN_DIRECTORY=$PWD 47 | ``` 48 | 49 | ### 2. Declare environment variables 50 | ```bash 51 | export REGION=aws-region-code # the AWS region to launch the solution (e.g. us-east-1) 52 | export DIST_BUCKET_PREFIX=my-bucket-name # bucket where customized code will reside, randomized name recommended 53 | export SOLUTION_NAME=my-solution-name # the solution name 54 | export VERSION=my-version # version number for the customized code 55 | ``` 56 | _Note:_ When you define `DIST_BUCKET_PREFIX`, a randomized value is recommended. You will need to create an S3 bucket where the name is `-`. The solution's CloudFormation template will expect the source code to be located in a bucket matching that name 57 | 58 | When creating and using buckets it is recommeded to: 59 | - Use randomized names or uuid as part of your bucket naming strategy. 60 | - Ensure buckets are not public. 61 | - Verify bucket ownership prior to uploading templates or code artifacts. 62 | 63 | ## Unit Test 64 | After making changes, run unit tests to make sure added customization passes the tests: 65 | ```bash 66 | cd $MAIN_DIRECTORY/deployment 67 | chmod +x run-unit-tests.sh 68 | ./run-unit-tests.sh 69 | ``` 70 | 71 | ## Build 72 | ```bash 73 | cd $MAIN_DIRECTORY/deployment 74 | chmod +x build-s3-dist.sh 75 | ./build-s3-dist.sh $DIST_BUCKET_PREFIX $SOLUTION_NAME $VERSION 76 | ``` 77 | 78 | ## Deploy 79 | 80 | - Deploy the distributable to the Amazon S3 bucket in your account. Make sure you are uploading all files and directories under `deployment/global-s3-assets` and `deployment/regional-s3-assets` to `/` folder in the `-` bucket (e.g. `s3://-///`). 81 | CLI based S3 command to sync the buckets is: 82 | ```bash 83 | aws s3 sync $MAIN_DIRECTORY/deployment/global-s3-assets/ s3://${DIST_BUCKET_PREFIX}-${REGION}/${SOLUTION_NAME}/${VERSION}/ 84 | aws s3 sync $MAIN_DIRECTORY/deployment/regional-s3-assets/ s3://${DIST_BUCKET_PREFIX}-${REGION}/${SOLUTION_NAME}/${VERSION}/ 85 | ``` 86 | - Get the link of the `iot-device-simulator.template` uploaded to your Amazon S3 bucket. 87 | - Deploy the IoT Device Simulator solution to your account by launching a new AWS CloudFormation stack using the S3 link of the `iot-device-simulator.template`. 88 | 89 | CLI based CloudFormation deployment: 90 | 91 | ```bash 92 | export INITIAL_USER=name@example.com # The email used to sign in web interface. 93 | export CF_STACK_NAME=iot # name of the cloudformation stack 94 | 95 | aws cloudformation create-stack \ 96 | --profile ${AWS_PROFILE:-default} \ 97 | --region ${REGION} \ 98 | --template-url https://${DIST_BUCKET_PREFIX}-${REGION}.s3.amazonaws.com/${SOLUTION_NAME}/${VERSION}/iot-device-simulator.template \ 99 | --stack-name ${CF_STACK_NAME} \ 100 | --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ 101 | --parameters \ 102 | ParameterKey=UserEmail,ParameterValue=${INITIAL_USER} 103 | 104 | ``` 105 | 106 | 107 | # Collection of operational metrics 108 | This solution collects anonymous operational metrics to help AWS improve the quality and features of the solution. For more information, including how to disable this capability, please see the [implementation guide](https://docs.aws.amazon.com/solutions/latest/iot-device-simulator/operational-metrics.html). 109 | 110 | # License 111 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 112 | 113 | SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/iot-device-simulator/06735ce5423ee63bdfbc32df9a7523efe3f2450c/architecture.png -------------------------------------------------------------------------------- /deployment/build-s3-dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This assumes all of the OS-level configuration has been completed and git repo has already been cloned 4 | # 5 | # This script should be run from the repo's deployment directory 6 | # cd deployment 7 | # ./build-s3-dist.sh source-bucket-base-name solution-name version-code 8 | # 9 | # Paramenters: 10 | # - source-bucket-base-name: Name for the S3 bucket location where the template will source the Lambda 11 | # code from. The template will append '-[region_name]' to this bucket name. 12 | # For example: ./build-s3-dist.sh solutions my-solution v1.0.0 13 | # The template will then expect the source code to be located in the solutions-[region_name] bucket 14 | # - solution-name: name of the solution for consistency 15 | # - version-code: version of the package 16 | 17 | # Check to see if input has been provided: 18 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 19 | echo "# Please provide all required parameters for the build script." 20 | echo "For example: ./build-s3-dist.sh solutions solution-name v1.0.0" 21 | exit 1 22 | fi 23 | 24 | # Build source 25 | template_dir="$PWD" 26 | template_dist_dir="$template_dir/global-s3-assets" 27 | build_dist_dir="$template_dir/regional-s3-assets" 28 | source_dir="$template_dir/../source" 29 | 30 | echo "------------------------------------------------------------------------------" 31 | echo "Clean up old build files" 32 | echo "------------------------------------------------------------------------------" 33 | echo "rm -rf $template_dist_dir" 34 | rm -rf $template_dist_dir 35 | echo "mkdir -p $template_dist_dir" 36 | mkdir -p $template_dist_dir 37 | echo "rm -rf $build_dist_dir" 38 | rm -rf $build_dist_dir 39 | echo "mkdir -p $build_dist_dir" 40 | mkdir -p $build_dist_dir 41 | 42 | echo "------------------------------------------------------------------------------" 43 | echo "Synth CDK Template" 44 | echo "------------------------------------------------------------------------------" 45 | SUB_BUCKET_NAME="s/BUCKET_NAME_PLACEHOLDER/$1/g" 46 | SUB_SOLUTION_NAME="s/SOLUTION_NAME_PLACEHOLDER/$2/g" 47 | SUB_VERSION="s/VERSION_PLACEHOLDER/$3/g" 48 | export overrideWarningsEnabled=false 49 | 50 | cd $source_dir/infrastructure 51 | npm run clean 52 | npm install 53 | export overrideWarningsEnabled=false 54 | node_modules/aws-cdk/bin/cdk synth --asset-metadata false --path-metadata false > $template_dir/iot-device-simulator.template 55 | sed -e $SUB_BUCKET_NAME -e $SUB_SOLUTION_NAME -e $SUB_VERSION $template_dir/iot-device-simulator.template > $template_dist_dir/iot-device-simulator.template 56 | rm $template_dir/iot-device-simulator.template 57 | 58 | declare -a lambda_packages=( 59 | "microservices" 60 | "simulator" 61 | "custom-resource" 62 | ) 63 | 64 | for lambda_package in "${lambda_packages[@]}" 65 | do 66 | echo "------------------------------------------------------------------------------" 67 | echo "Build Lambda package: $lambda_package" 68 | echo "------------------------------------------------------------------------------" 69 | cd $source_dir/$lambda_package 70 | npm run package 71 | 72 | # Check the result of the package step and exit if a failure is identified 73 | if [ $? -eq 0 ] 74 | then 75 | echo "Package for $lambda_package built successfully" 76 | else 77 | echo "******************************************************************************" 78 | echo "Lambda package build FAILED for $lambda_package" 79 | echo "******************************************************************************" 80 | exit 1 81 | fi 82 | 83 | mv dist/package.zip $build_dist_dir/$lambda_package.zip 84 | done 85 | 86 | echo "------------------------------------------------------------------------------" 87 | echo "Building console" 88 | echo "------------------------------------------------------------------------------" 89 | cd $source_dir/console 90 | [ -e build ] && rm -r build 91 | [ -e node_modules ] && rm -rf node_modules 92 | npm install 93 | npm run build 94 | mkdir $build_dist_dir/console 95 | cp -r ./build/* $build_dist_dir/console/ 96 | 97 | echo "------------------------------------------------------------------------------" 98 | echo "Copying routes files" 99 | echo "------------------------------------------------------------------------------" 100 | cd $source_dir/resources 101 | mkdir $build_dist_dir/routes 102 | cp -r routes/* $build_dist_dir/routes/ 103 | 104 | echo "------------------------------------------------------------------------------" 105 | echo "[Create] UI manifest" 106 | echo "------------------------------------------------------------------------------" 107 | cd $build_dist_dir 108 | manifest=(`find console -type f | sed 's|^./||'`) 109 | manifest_json=$(IFS=,;printf "%s" "${manifest[*]}") 110 | echo "[\"$manifest_json\"]" | sed 's/,/","/g' >> $build_dist_dir/site-manifest.json 111 | 112 | echo "------------------------------------------------------------------------------" 113 | echo "[Create] Routes manifest" 114 | echo "------------------------------------------------------------------------------" 115 | cd "$build_dist_dir" 116 | manifest=(`find routes -type f | sed 's|^./||'`) 117 | manifest_json=$(IFS=,;printf "%s" "${manifest[*]}") 118 | echo "[\"$manifest_json\"]" | sed 's/,/","/g' >> $build_dist_dir/routes-manifest.json -------------------------------------------------------------------------------- /deployment/run-unit-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This assumes all of the OS-level configuration has been completed and git repo has already been cloned 4 | # 5 | # This script should be run from the repo's deployment directory 6 | # cd deployment 7 | # ./run-unit-tests.sh 8 | # 9 | 10 | [ "$DEBUG" == 'true' ] && set -x 11 | set -e 12 | 13 | prepare_jest_coverage_report() { 14 | local component_name=$1 15 | 16 | if [ ! -d "coverage" ]; then 17 | echo "ValidationError: Missing required directory coverage after running unit tests" 18 | exit 129 19 | fi 20 | 21 | # prepare coverage reports 22 | rm -fr coverage/lcov-report 23 | mkdir -p $coverage_reports_top_path/jest 24 | coverage_report_path=$coverage_reports_top_path/jest/$component_name 25 | rm -fr $coverage_report_path 26 | mv coverage $coverage_report_path 27 | } 28 | 29 | run_javascript_test() { 30 | local component_path=$1 31 | local component_name=$2 32 | 33 | echo "------------------------------------------------------------------------------" 34 | echo "[Test] Run javascript unit test with coverage for $component_name" 35 | echo "------------------------------------------------------------------------------" 36 | echo "cd $component_path" 37 | cd $component_path 38 | 39 | # clean and install dependencies 40 | npm run clean 41 | npm install 42 | 43 | # run unit tests 44 | npm test 45 | 46 | # prepare coverage reports 47 | prepare_jest_coverage_report $component_name 48 | } 49 | 50 | # Get reference for all important folders 51 | template_dir="$PWD" 52 | source_dir="$template_dir/../source" 53 | coverage_reports_top_path=$source_dir/test/coverage-reports 54 | 55 | # Test the attached Lambda function 56 | declare -a lambda_packages=( 57 | "custom-resource" 58 | "infrastructure" 59 | "microservices" 60 | "simulator" 61 | ) 62 | 63 | for lambda_package in "${lambda_packages[@]}" 64 | do 65 | run_javascript_test $source_dir/$lambda_package $lambda_package 66 | 67 | # Check the result of the test and exit if a failure is identified 68 | if [ $? -eq 0 ] 69 | then 70 | echo "Test for $lambda_package passed" 71 | else 72 | echo "******************************************************************************" 73 | echo "Lambda test FAILED for $lambda_package" 74 | echo "******************************************************************************" 75 | exit 1 76 | fi 77 | done -------------------------------------------------------------------------------- /solution-manifest.yaml: -------------------------------------------------------------------------------- 1 | id: SO0041 2 | name: iot-device-simulator 3 | version: v3.0.9 4 | cloudformation_templates: 5 | - template: iot-device-simulator.template 6 | build_environment: 7 | build_image: 'aws/codebuild/standard:7.0' 8 | -------------------------------------------------------------------------------- /source/console/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iot-device-simulator-console", 3 | "description": "IoT Device Simulator UI Console", 4 | "license": "Apache-2.0", 5 | "author": { 6 | "name": "Amazon Web Services", 7 | "url": "https://aws.amazon.com/solutions" 8 | }, 9 | "version": "3.0.9", 10 | "private": true, 11 | "dependencies": { 12 | "@aws-amplify/api": "^5.4.13", 13 | "@aws-amplify/auth": "^5.6.13", 14 | "@aws-amplify/core": "^5.8.13", 15 | "@aws-amplify/geo": "^2.3.13", 16 | "@aws-amplify/interactions": "^5.2.19", 17 | "@aws-amplify/storage": "^5.9.14", 18 | "@aws-amplify/ui-react": "^5.1.1", 19 | "@aws-sdk/client-iot": "^3.391.0", 20 | "@testing-library/jest-dom": "^6.0.1", 21 | "@testing-library/react": "^12.1.2", 22 | "@testing-library/user-event": "^14.4.3", 23 | "@types/jest": "^29.5.3", 24 | "@types/node": "^20.5.0", 25 | "@types/react": "^18.2.20", 26 | "@types/react-dom": "^18.2.7", 27 | "@types/react-router-dom": "^5.3.3", 28 | "bootstrap": "^4.6.0", 29 | "bootstrap-icons": "^1.6.0", 30 | "maplibre-gl-js-amplify": "^3.1.0", 31 | "moment": "^2.29.4", 32 | "react": "~16.14.0", 33 | "react-bootstrap": "^1.6.4", 34 | "react-dom": "~16.14.0", 35 | "react-router-dom": "~5.3.4", 36 | "typescript": "^4.4.4", 37 | "web-vitals": "^3.4.0" 38 | }, 39 | "devDependencies": { 40 | "react-scripts": "^5.0.1" 41 | }, 42 | "resolutions": { 43 | "axios": "^1.7.4" 44 | }, 45 | "overrides": { 46 | "css-select@2.1.0": { 47 | "nth-check": "~2.1.1" 48 | } 49 | }, 50 | "scripts": { 51 | "start": "react-scripts start", 52 | "build": "GENERATE_SOURCEMAP=false INLINE_RUNTIME_CHUNK=false react-scripts build", 53 | "test": "react-scripts test", 54 | "eject": "react-scripts eject" 55 | }, 56 | "engines": { 57 | "node": ">=18.0.0" 58 | }, 59 | "eslintConfig": { 60 | "extends": [ 61 | "react-app", 62 | "react-app/jest" 63 | ] 64 | }, 65 | "browserslist": { 66 | "production": [ 67 | "defaults", 68 | "not ie 11" 69 | ], 70 | "development": [ 71 | "last 1 chrome version", 72 | "last 1 firefox version", 73 | "last 1 safari version" 74 | ] 75 | } 76 | } -------------------------------------------------------------------------------- /source/console/public/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | IoT Device Simulator 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /source/console/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "IoT Device Sim", 3 | "name": "IoT Device Simulator", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } 10 | -------------------------------------------------------------------------------- /source/console/src/App.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | **/ 5 | 6 | body { 7 | font-family: 'Amazon Ember', Helvetica, sans-serif; 8 | font-weight: 300; 9 | height: 100%; 10 | margin: 0; 11 | background: #f5f5f5; 12 | } 13 | 14 | .app-wrap { 15 | display: flex; 16 | flex-flow: column; 17 | height: 100vh; 18 | } 19 | 20 | .text-success-alt { 21 | color: #ffac31 22 | } 23 | 24 | /* navbar */ 25 | .topbar { 26 | background: #31465f; 27 | -webkit-box-shadow: 5px 0px 10px rgba(0, 0, 0, 0.5); 28 | box-shadow: 5px 0px 10px rgba(0, 0, 0, 0.5); 29 | padding-left: 0.5rem; 30 | padding-right: 0.5rem; 31 | position: relative; 32 | } 33 | 34 | .navbar-dark .navbar-nav .header-link { 35 | color: rgba(255, 255, 255, .75); 36 | } 37 | 38 | .topbar .navbar-nav .header-link:hover, .topbar .navbar-nav .header-link:focus { 39 | color: rgba(255, 255, 255, .9); 40 | } 41 | 42 | /*page body*/ 43 | .page-content { 44 | height:100%; 45 | width: 100vw; 46 | max-width: 100%; 47 | margin: 0; 48 | padding-left: 10vw; 49 | padding-right: 10vw; 50 | } 51 | /* page title */ 52 | #view-title { 53 | font-size: 21px; 54 | line-height: 30px; 55 | } 56 | 57 | .page-titles { 58 | background-color: #fff; 59 | margin-bottom: 1rem; 60 | margin-right: 0; 61 | margin-left: 0; 62 | margin-top: 1rem; 63 | padding: 1vw; 64 | -webkit-box-shadow: 1px 0 5px rgba(0, 0, 0, 0.1); 65 | box-shadow: 1px 0 5px rgba(0, 0, 0, 0.1); } 66 | .page-titles h3 { 67 | margin-bottom: 0px; 68 | margin-top: 8px; } 69 | .page-titles .breadcrumb { 70 | padding: 0px; 71 | background: transparent; 72 | font-size: 14px; } 73 | .page-titles .breadcrumb li { 74 | margin-top: 0px; 75 | margin-bottom: 0px; } 76 | .page-titles .breadcrumb a { 77 | text-decoration: none; 78 | } 79 | .page-titles .breadcrumb .breadcrumb-item + .breadcrumb-item::before { 80 | 81 | content: url('data:image/svg+xml, '); 82 | color: #a6b7bf; 83 | } 84 | .page-titles .breadcrumb .breadcrumb-item.active { 85 | color: #99abb4; } 86 | 87 | 88 | .chart-text .chart-title { 89 | font-size: 80%; 90 | font-weight: 400; 91 | } 92 | .chart-text .chart-content { 93 | line-height: 22px; 94 | font-size: 18px; 95 | font-weight: 400; 96 | } 97 | /* Content body*/ 98 | .content-card { 99 | padding: 2.25vh; 100 | } 101 | .content-card-title { 102 | font-weight: 400; 103 | margin-bottom: 1.25vh; 104 | text-transform: capitalize; 105 | } 106 | 107 | .content-card-subtitle { 108 | font-weight: 300; 109 | color: #99abb4; 110 | margin-bottom: 1.75vh; 111 | 112 | } 113 | 114 | .content-card-body { 115 | padding: 0; 116 | } 117 | 118 | .content-card-table { 119 | margin-top: 2.5vh; 120 | } 121 | .table-header tr th { 122 | font-weight: 400; 123 | border-top: none; 124 | } 125 | 126 | .header-button{ 127 | float: right; 128 | padding: .25rem .5rem; 129 | } 130 | 131 | .button-theme { 132 | margin-right: 1rem; 133 | background: #ffac31; 134 | border: 1px; 135 | border-color: #ffac31; 136 | -webkit-box-shadow: 0 2px 2px 0 rgb(255 153 0 / 14%), 0 3px 1px -2px rgb(255 153 0 / 20%), 0 1px 5px 0 rgb(255 153 0 / 12%); 137 | box-shadow: 0 2px 2px 0 rgb(255 153 0 / 14%), 0 3px 1px -2px rgb(255 153 0 / 20%), 0 1px 5px 0 rgb(255 153 0 / 12%); 138 | } 139 | 140 | .button-theme-alt { 141 | background: #dd3f5b; 142 | border: 1px; 143 | border-color: #dd3f5b; 144 | -webkit-box-shadow: 0 2px 2px 0 rgb(239 83 80 / 14%), 0 3px 1px -2px rgb(239 83 80 / 20%), 0 1px 5px 0 rgb(239 83 80 / 12%); 145 | box-shadow: 0 2px 2px 0 rgb(239 83 80 / 14%), 0 3px 1px -2px rgb(239 83 80 / 20%), 0 1px 5px 0 rgb(239 83 80 / 12%); 146 | } 147 | 148 | 149 | .button-theme:hover { 150 | background: #ffac31; 151 | -webkit-box-shadow: 0 14px 26px -12px rgba(255, 153, 0, 0.42), 0 4px 23px 0 rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(255, 153, 0, 0.2); 152 | box-shadow: 0 14px 26px -12px rgba(255, 153, 0, 0.42), 0 4px 23px 0 rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(255, 153, 0, 0.2);} 153 | 154 | .button-theme-alt:hover { 155 | background: #dc3545; 156 | -webkit-box-shadow: 0 14px 26px -12px rgba(239, 83, 80, 0.42), 0 4px 23px 0 rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(239, 83, 80, 0.2); 157 | box-shadow: 0 14px 26px -12px rgba(239, 83, 80, 0.42), 0 4px 23px 0 rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(239, 83, 80, 0.2);} 158 | 159 | .button-theme:disabled { 160 | background: #ff9900; 161 | opacity: 0.33; 162 | pointer-events: none; 163 | } 164 | 165 | .button-theme-alt:disabled { 166 | background: #dc3545; 167 | opacity: 0.33; 168 | pointer-events: none; 169 | } 170 | 171 | .button-rounded { 172 | border-radius: 1rem; 173 | } 174 | 175 | .button-file-upload { 176 | margin-bottom: 0; 177 | cursor: pointer; 178 | } 179 | 180 | .table-header { 181 | color: #99abb4; 182 | } 183 | .table { 184 | word-break: break-all; 185 | } 186 | 187 | .form-label{ 188 | font-weight: 400; 189 | } 190 | .form-label .sublabel{ 191 | font-weight: 300; 192 | } 193 | .form-item-spacing { 194 | margin-bottom: 2.5vh; 195 | } 196 | 197 | .detail { 198 | margin-bottom: 2.5vh; 199 | } 200 | 201 | .detail b { 202 | font-weight: 400; 203 | text-transform: uppercase 204 | } 205 | 206 | .dropdown-menu { 207 | max-height: 33vh; 208 | overflow-y: scroll; 209 | } 210 | 211 | .topic-content { 212 | max-height: 33vh; 213 | overflow-y: auto; 214 | } 215 | 216 | .topic-item { 217 | word-break: break-word; 218 | } 219 | 220 | .message-content { 221 | max-height: 33vh; 222 | overflow-y: auto; 223 | } 224 | .footer { 225 | text-align: center; 226 | border-radius: 2px; 227 | padding: 1rem 2rem 1rem 2rem; 228 | margin: 0.7rem; 229 | } 230 | 231 | .text-link { 232 | color: #ff9900; 233 | font-weight: 400; 234 | } 235 | 236 | .capitalize { 237 | text-transform: capitalize; 238 | } 239 | 240 | .empty-alert { 241 | text-align: center; 242 | } 243 | 244 | .map { 245 | margin-top: 1rem; 246 | height: 66vh; 247 | } 248 | 249 | .iot-message-card { 250 | background-color: #eee; 251 | } -------------------------------------------------------------------------------- /source/console/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React from 'react'; 5 | import { render, screen } from '@testing-library/react'; 6 | import App from './App'; 7 | 8 | test('renders learn react link', () => { 9 | render(); 10 | const linkElement = screen.getByText(/learn react/i); 11 | expect(linkElement).toBeInTheDocument(); 12 | }); 13 | -------------------------------------------------------------------------------- /source/console/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Auth } from "@aws-amplify/auth"; 5 | import { Amplify, I18n } from "@aws-amplify/core"; 6 | import { Geo } from "@aws-amplify/geo"; 7 | import { AWSIoTProvider, PubSub } from "@aws-amplify/pubsub"; 8 | import { useAuthenticator, withAuthenticator } from "@aws-amplify/ui-react"; 9 | import "@aws-amplify/ui-react/styles.css"; 10 | import { AttachPolicyCommand, IoTClient as Iot } from "@aws-sdk/client-iot"; 11 | import { useEffect } from "react"; 12 | import { BrowserRouter, Redirect, Route, Switch } from "react-router-dom"; 13 | import Header from "./components/Shared/Header"; 14 | import DeviceTypeCreate from "./views/DeviceTypeCreate"; 15 | import DeviceTypes from "./views/DeviceTypes"; 16 | import PageNotFound from "./views/PageNotFound"; 17 | import SimulationCreate from "./views/SimulationCreate"; 18 | import SimulationDetails from "./views/SimulationDetails"; 19 | import Simulations from "./views/Simulations"; 20 | 21 | // Amplify configuration 22 | declare let config: any; 23 | Amplify.addPluggable( 24 | new AWSIoTProvider({ 25 | aws_pubsub_region: config.aws_project_region, 26 | aws_pubsub_endpoint: "wss://" + config.aws_iot_endpoint + "/mqtt", 27 | }) 28 | ); 29 | PubSub.configure(config); 30 | Amplify.configure(config); 31 | Geo.configure(config); 32 | 33 | /** 34 | * The default application 35 | * @returns Amplify Authenticator with Main and Footer 36 | */ 37 | function App(): JSX.Element { 38 | const { authStatus } = useAuthenticator((context) => [context.authStatus]); 39 | 40 | useEffect(() => { 41 | if (authStatus == "authenticated") { 42 | Auth.currentCredentials().then(async (credentials) => { 43 | const identityId = credentials.identityId; 44 | const awsConfig = { 45 | region: config.aws_project_region, 46 | credentials: Auth.essentialCredentials(credentials), 47 | }; 48 | const iot = new Iot(awsConfig); 49 | const params = { 50 | policyName: config.aws_iot_policy_name, 51 | target: identityId, 52 | }; 53 | try { 54 | await iot.send(new AttachPolicyCommand(params)); 55 | } catch (error) { 56 | console.error("Error occurred while attaching principal policy", error); 57 | } 58 | }); 59 | } 60 | }, [authStatus]); 61 | return ( 62 |
63 |
64 | 65 | 66 | { 70 | return ; 71 | }} 72 | /> 73 | } 77 | /> 78 | ( 82 | 83 | )} 84 | /> 85 | } 89 | /> 90 | } 94 | /> 95 | ( 99 | 100 | )} 101 | /> 102 | } /> 103 | 104 | 105 |
106 | ); 107 | } 108 | 109 | export default withAuthenticator(App); 110 | -------------------------------------------------------------------------------- /source/console/src/components/DeviceTypeCreate/AttributeFields.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { I18n } from '@aws-amplify/core' 5 | import Form from 'react-bootstrap/Form'; 6 | import Col from 'react-bootstrap/Col'; 7 | import { IAttribute, IErrors } from '../Shared/Interfaces'; 8 | import { FormControlProps } from 'react-bootstrap/FormControl'; 9 | 10 | interface IProps { 11 | attr: IAttribute, 12 | handleFormChange: Function, 13 | handleFieldFocus: Function, 14 | errors: IErrors, 15 | showValidation: string[] 16 | } 17 | export default function AttributeFields(props: IProps): JSX.Element { 18 | 19 | /** 20 | * Creates a Form control item for an attribute field 21 | * @param id 22 | * @returns A form control item representing an attribute 23 | */ 24 | const createFormControlItem = (id: keyof IAttribute) => { 25 | const attrType = props.attr.type; 26 | let formControlOptions: FormControlProps & React.InputHTMLAttributes = {}; 27 | let options: Array = []; 28 | 29 | if ((attrType === 'string' && id === "default") || id === 'charSet') { 30 | formControlOptions.type = "text"; 31 | formControlOptions.maxLength = 256; 32 | } else if (id === "arr") { 33 | formControlOptions.type = "text"; 34 | } else if (id === "static" || id === "tsformat") { 35 | formControlOptions.value = props.attr[id]?.toString(); 36 | formControlOptions.as = "select"; 37 | options = id === "static" ? 38 | [I18n.get("false"), I18n.get("true")] : 39 | [I18n.get("timestamp.tsformat.default"), I18n.get("timestamp.tsformat.unix")]; 40 | } else { 41 | formControlNumber(formControlOptions, id); 42 | } 43 | if (id !== 'default' && attrType !== 'id') { 44 | formControlOptions.required = true 45 | formControlOptions.disabled = !!props.attr.default; 46 | } 47 | 48 | return ( 49 | props.handleFormChange(event)} 53 | onFocus={(event: any) => props.handleFieldFocus(event)} 54 | {...formControlOptions} 55 | id={id} 56 | > 57 | { options.length > 0 ? 58 | options.map((option, index) => ( 59 | 62 | )) : 63 | undefined 64 | } 65 | 66 | ) 67 | } 68 | 69 | return ( 70 | 71 | { Object.keys(props.attr).filter( 72 | word => !["name", "type", "payload"].includes(word)).map( 73 | (id, index) => ( 74 | 75 | 76 | { 77 | (["min", "max", "static", "default"].includes(id) && 78 | I18n.get(id)) || 79 | I18n.get(`${props.attr.type.toLowerCase()}.${id}`) 80 | 81 | } 82 | 83 | {createFormControlItem(id as keyof IAttribute)} 84 | {props.errors[id as keyof IAttribute]} 85 | 86 | { 87 | id === "static" || id === "default" ? 88 | I18n.get(`${id}.description`) : 89 | I18n.get(`${props.attr.type.toLowerCase()}.${id}.description`) 90 | } 91 | 92 | 93 | ) 94 | )} 95 | 96 | ) 97 | } 98 | function formControlNumber(formControlOptions: FormControlProps & React.InputHTMLAttributes, id: string) { 99 | formControlOptions.type = "number"; 100 | if (id === "long") { 101 | formControlOptions.min = -180; 102 | formControlOptions.max = 180; 103 | formControlOptions.step = .000001; 104 | } else if (id === "lat") { 105 | formControlOptions.min = -90; 106 | formControlOptions.max = 90; 107 | formControlOptions.step = .000001; 108 | } else if (id === 'precision') { 109 | formControlOptions.step = .000001; 110 | } else if (id === "length") { 111 | formControlOptions.min = 1; 112 | formControlOptions.max = 36; 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /source/console/src/components/Shared/DeleteConfirmation.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { I18n } from '@aws-amplify/core'; 5 | import Button from 'react-bootstrap/Button'; 6 | import Modal from 'react-bootstrap/Modal'; 7 | 8 | interface IDeleteConfirmProps { 9 | id: string; 10 | name: string; 11 | delete: (id: string, index: number) => void; 12 | resetModalValues: Function; 13 | show: boolean; 14 | index: number; 15 | } 16 | 17 | export default function DeleteConfirm(props: IDeleteConfirmProps): JSX.Element { 18 | 19 | /** 20 | * Deletes the provided item 21 | * @param id - the id of the item to delete 22 | * @param index - the index of the item to delete 23 | */ 24 | const deleteItem = (id: string, index: number) => { 25 | props.delete(id, index); 26 | props.resetModalValues(); 27 | } 28 | 29 | return ( 30 | { props.resetModalValues() }}> 31 | 32 | 33 | {I18n.get('confirm.delete.title')} 34 | 35 | 36 | 37 | 38 | {I18n.get('confirm.delete.message')} "{props.name}"? 39 | 40 | 41 | 46 | 53 | 54 | 55 | ) 56 | } -------------------------------------------------------------------------------- /source/console/src/components/Shared/Footer.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { I18n } from '@aws-amplify/core'; 5 | 6 | interface IFooterProps { 7 | pageTitle: string; 8 | } 9 | 10 | export default function Footer(props: IFooterProps): JSX.Element { 11 | const helpLink = props.pageTitle.includes('Create') ? 12 | 'https://docs.aws.amazon.com/solutions/latest/iot-device-simulator/deployment.html' : 13 | 'https://aws.amazon.com/solutions/implementations/iot-device-simulator/'; 14 | 15 | const helpPage = props.pageTitle.includes('Create') ? I18n.get('footer.solution.ig') : I18n.get('footer.solution.page'); 16 | 17 | return ( 18 |
19 |

{I18n.get('footer.help')}  20 | 25 | {helpPage}  26 | 27 | 28 |

29 |
30 | ); 31 | } -------------------------------------------------------------------------------- /source/console/src/components/Shared/Header.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { I18n } from '@aws-amplify/core'; 5 | import Navbar from 'react-bootstrap/Navbar'; 6 | import Nav from 'react-bootstrap/Nav'; 7 | import { signOut } from '../../util/Utils'; 8 | 9 | /** 10 | * Renders the header of the UI. 11 | * @returns The header 12 | */ 13 | export default function Header(): JSX.Element { 14 | return ( 15 |
16 | 17 | {I18n.get('application')} 18 | 19 | 20 | 23 | 26 | 29 | 30 | 31 |
32 | ); 33 | } -------------------------------------------------------------------------------- /source/console/src/components/Shared/Interfaces.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface IPageProps { 5 | region: string, 6 | title: string 7 | } 8 | 9 | export interface IAttribute { 10 | name: string, 11 | type: string, 12 | charSet?: string, 13 | length?: number, 14 | default?: string | number, 15 | static?: boolean, 16 | tsformat?: string, 17 | precision?: number, 18 | min?: number, 19 | max?: number, 20 | lat?: number, 21 | long?: number, 22 | radius?: number, 23 | arr?: string[] | string, 24 | object?: IAttribute 25 | payload?: IAttribute[]; 26 | } 27 | 28 | export interface IDeviceType { 29 | name: string, 30 | topic: string, 31 | typeId: string, 32 | payload: Array 33 | createdAt?: string, 34 | updatedAt?: string 35 | } 36 | 37 | export interface IDevice { 38 | typeId: string, 39 | name: string, 40 | amount: number, 41 | } 42 | 43 | export interface ISimulation { 44 | simId: string, 45 | name: string, 46 | stage: string, 47 | duration: number, 48 | interval: number, 49 | devices: Array, 50 | runs?: number, 51 | lastRun?: string, 52 | createdAt?: string, 53 | updatedAt?: string 54 | checked?: boolean 55 | } 56 | 57 | export type IErrors = { 58 | [key in keyof T]?: string 59 | } 60 | 61 | export const AttributeTypeMap = { 62 | default: ['string', 'number'], 63 | name: 'string', 64 | type: 'string', 65 | charSet: 'string', 66 | length: 'number', 67 | static: 'boolean', 68 | tsformat: 'string', 69 | precision: 'number', 70 | min: 'number', 71 | max: 'number', 72 | lat: 'number', 73 | long: 'number', 74 | radius: 'number', 75 | arr: 'object', 76 | object: 'object', 77 | payload: 'object' 78 | } 79 | 80 | export enum simTypes{ 81 | autoDemo = "idsAutoDemo", 82 | custom = "custom" 83 | } -------------------------------------------------------------------------------- /source/console/src/components/Shared/PageTitleBar.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { I18n, Logger } from '@aws-amplify/core'; 5 | import { useEffect, useState } from 'react'; 6 | import { useLocation } from 'react-router'; 7 | import Row from 'react-bootstrap/Row'; 8 | import Col from 'react-bootstrap/Col'; 9 | import Breadcrumb from 'react-bootstrap/Breadcrumb'; 10 | import { API } from '@aws-amplify/api'; 11 | import { API_NAME } from '../../util/Utils'; 12 | 13 | /** 14 | * Renders the header of the UI. 15 | * @returns page title bar 16 | */ 17 | export default function PageTitleBar(props: any): JSX.Element { 18 | const location = useLocation(); 19 | const logger = new Logger('Page Title'); 20 | const [running, setRunning] = useState({ devices: 0, sims: 0 }); 21 | 22 | /** 23 | * Gets number of simulations and devices running from DynamoDb 24 | */ 25 | const getSimulationStats = async () => { 26 | try { 27 | const results = await API.get(API_NAME, '/simulation', { 28 | queryStringParameters: { op: "getRunningStat" } 29 | }); 30 | setRunning(results); 31 | } catch (err) { 32 | logger.error(I18n.get("simulations.get.error"), err); 33 | throw err; 34 | } 35 | } 36 | /** 37 | * react useEffect hook 38 | * updates title bar every 30 seconds 39 | */ 40 | useEffect(() => { 41 | const interval = setInterval(() => { 42 | getSimulationStats(); 43 | }, 30000); 44 | return () => clearInterval(interval); 45 | }, []); 46 | 47 | /** 48 | * adds a breadcrumb for each item in path 49 | * @returns Breadcrumb item 50 | */ 51 | const getPaths = () => { 52 | const pages = location.pathname.split('/'); 53 | let pageItems: Array = []; 54 | pages.forEach((page, index) => { 55 | pageItems.push(page.replace("-", " ")); 56 | }) 57 | pageItems.splice(0, 1); 58 | 59 | return (pageItems.map((page, index) => ( 60 | 66 | {page} 67 | 68 | )) 69 | ) 70 | } 71 | 72 | return ( 73 | 74 | 75 |

{props.title}

76 | 77 | {I18n.get("home")} 78 | {getPaths()} 79 | 80 | 81 | 82 | 83 | 84 |
{I18n.get('devices')}
85 |

{`${running.devices} ${I18n.get("running")}`}

86 | 87 | 88 |
{I18n.get('simulations')}
89 |

{`${running.sims} ${I18n.get("running")}`}

90 | 91 |
92 | 93 |
94 | ); 95 | } -------------------------------------------------------------------------------- /source/console/src/components/Simulations/TableData.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | import { I18n } from '@aws-amplify/core'; 6 | import { useState } from 'react'; 7 | import Button from 'react-bootstrap/Button'; 8 | import Form from 'react-bootstrap/Form'; 9 | import Modal from 'react-bootstrap/Modal'; 10 | import Table from 'react-bootstrap/Table'; 11 | import { Link } from 'react-router-dom'; 12 | import { ISimulation } from '../Shared/Interfaces'; 13 | 14 | interface IProps { 15 | simulations: ISimulation[], 16 | handleCheckboxSelect: Function, 17 | setSimulations: Function, 18 | handleDeleteButtonClick: Function 19 | } 20 | 21 | export default function TableData(props: IProps): JSX.Element { 22 | const [showDevices, setShowDevices] = useState(-1); 23 | 24 | 25 | 26 | 27 | 28 | 29 | return ( 30 | 31 | { props.simulations.map((sim, i) => ( 32 | 33 | 34 | { props.handleCheckboxSelect(event, i) }} 39 | > 40 | 41 | 42 | {sim.name} 43 | {sim.stage} 44 | 45 | 46 |   47 | 54 | { setShowDevices(-1) }}> 55 | 56 | {sim.name} 57 | 58 | 59 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | {sim.devices.map((device, j) => ( 70 | 71 | 72 | 73 | 74 | ))} 75 | 76 |
{I18n.get("device.types")}{I18n.get("amount")}
{device.name}{device.amount}
77 |
78 |
79 | 80 | {sim.runs} 81 | {sim.lastRun ? sim.lastRun : ""} 82 | 83 | 91 | 94 | 95 | 102 | 103 | 104 | )) 105 | } 106 | 107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /source/console/src/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import App from './App'; 7 | import 'bootstrap/dist/css/bootstrap.min.css'; 8 | import 'bootstrap-icons/font/bootstrap-icons.css'; 9 | import 'maplibre-gl/dist/maplibre-gl.css'; 10 | import "@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css"; 11 | import './App.css'; 12 | import reportWebVitals from './reportWebVitals'; 13 | 14 | // For the internationalization 15 | import { I18n } from '@aws-amplify/core'; 16 | import en from './util/lang/en.json'; // English 17 | 18 | const dict = { en }; 19 | I18n.putVocabularies(dict); 20 | I18n.setLanguage('en'); 21 | 22 | ReactDOM.render( 23 | 24 | 25 | , 26 | document.getElementById('root') 27 | ); 28 | 29 | // If you want to start measuring performance in your app, pass a function 30 | // to log results (for example: reportWebVitals(console.log)) 31 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 32 | reportWebVitals(); 33 | -------------------------------------------------------------------------------- /source/console/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// 5 | 6 | // See https://github.com/facebook/create-react-app/issues/6560 for why this file exists. 7 | -------------------------------------------------------------------------------- /source/console/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { ReportHandler } from 'web-vitals'; 5 | 6 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 7 | if (onPerfEntry && onPerfEntry instanceof Function) { 8 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 9 | getCLS(onPerfEntry); 10 | getFID(onPerfEntry); 11 | getFCP(onPerfEntry); 12 | getLCP(onPerfEntry); 13 | getTTFB(onPerfEntry); 14 | }); 15 | } 16 | }; 17 | 18 | export default reportWebVitals; 19 | -------------------------------------------------------------------------------- /source/console/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 5 | // allows you to do things like: 6 | // expect(element).toHaveTextContent(/react/i) 7 | // learn more: https://github.com/testing-library/jest-dom 8 | import '@testing-library/jest-dom'; 9 | -------------------------------------------------------------------------------- /source/console/src/views/PageNotFound.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { I18n } from '@aws-amplify/core'; 5 | import Container from 'react-bootstrap/Container'; 6 | import Row from 'react-bootstrap/Row'; 7 | import Col from 'react-bootstrap/Col'; 8 | import Jumbotron from 'react-bootstrap/Jumbotron'; 9 | 10 | /** 11 | * PageNotFound returns an error when path does not match. 12 | * @returns Page not found error message 13 | */ 14 | export default function PageNotFound(): JSX.Element { 15 | return ( 16 | 17 | 18 | 19 | 20 |

{I18n.get('page.not.found')}: {window.location.pathname}

21 |
22 | 23 |
24 |
25 | ); 26 | } -------------------------------------------------------------------------------- /source/console/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /source/custom-resource/interfaces.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * The namespace for the custom resource types 6 | * @namespace CustomResourceTypes 7 | */ 8 | export namespace CustomResourceTypes { 9 | /** 10 | * @enum The custom resource request types 11 | */ 12 | export enum RequestTypes { 13 | CREATE = 'Create', 14 | DELETE = 'Delete', 15 | UPDATE = 'Update' 16 | } 17 | 18 | /** 19 | * @enum The custom resource resource properties resource types 20 | */ 21 | export enum ResourceTypes { 22 | CREATE_UUID = 'CreateUUID', 23 | SEND_ANONYMOUS_METRICS = 'SendAnonymousMetrics', 24 | DESCRIBE_IOT_ENDPOINT = 'DescribeIoTEndpoint', 25 | COPY_S3_ASSETS = 'CopyS3Assets', 26 | CREATE_CONFIG = 'CreateConfig', 27 | DETACH_IOT_POLICY = 'DetachIoTPolicy' 28 | } 29 | 30 | /** 31 | * @enum The custom resource status types 32 | */ 33 | export enum StatusTypes { 34 | SUCCESS = 'SUCCESS', 35 | FAILED = 'FAILED' 36 | } 37 | 38 | /** 39 | * The Lambda function context type 40 | * @interface LambdaContext 41 | */ 42 | export interface LambdaContext { 43 | getRemainingTimeInMillis: () => number; 44 | functionName: string; 45 | functionVersion: string; 46 | invokedFunctionArn: string; 47 | memoryLimitInMB: number; 48 | awsRequestId: string; 49 | logGroupName: string; 50 | logStreamName: string; 51 | identity: any; 52 | clientContext: any; 53 | callbackWaitsForEmptyEventLoop: boolean; 54 | } 55 | 56 | /** 57 | * The custom resource event request type 58 | * @interface EventRequest 59 | */ 60 | export interface EventRequest { 61 | RequestType: RequestTypes; 62 | PhysicalResourceId: string; 63 | StackId: string; 64 | ServiceToken: string; 65 | RequestId: string; 66 | LogicalResourceId: string; 67 | ResponseURL: string; 68 | ResourceType: string; 69 | ResourceProperties: ResourcePropertyTypes; 70 | } 71 | 72 | /** 73 | * @type The resource property types 74 | */ 75 | type ResourcePropertyTypes = ResourceProperty | SendAnonymousMetricProperties | CopyFilesProperties | 76 | CreateConsoleConfigProperties | DetachIoTPolicyRequestProps 77 | 78 | /** 79 | * The custom resource resource type 80 | * @interface ResourceProperty 81 | */ 82 | interface ResourceProperty { 83 | Resource: ResourceTypes; 84 | StackName?: string; 85 | } 86 | 87 | /** 88 | * Sending anonymous metric custom resource properties type 89 | * @interface SendAnonymousMetricProperties 90 | * @extends ResourceProperty 91 | */ 92 | export interface SendAnonymousMetricProperties extends ResourceProperty { 93 | SolutionUUID: string; 94 | } 95 | 96 | /** 97 | * Copying UI assets custom resource properties type 98 | * @interface CopyFilesProperties 99 | * @extends ResourceProperty 100 | */ 101 | export interface CopyFilesProperties extends ResourceProperty { 102 | DestinationBucket: string; 103 | ManifestFile: string; 104 | SourceBucket: string; 105 | SourcePrefix: string; 106 | } 107 | 108 | /** 109 | * Creating console config custom resource properties type 110 | * @interface CreateConsoleConfigProperties 111 | * @extends ResourceProperty 112 | */ 113 | export interface CreateConsoleConfigProperties extends ResourceProperty { 114 | DestinationBucket: string; 115 | ConfigFileName: string; 116 | configObj: string; 117 | } 118 | 119 | /** 120 | * @interface DetachIoTPolicyRequestProps 121 | * @extends ResourceProperty 122 | */ 123 | export interface DetachIoTPolicyRequestProps extends ResourceProperty { 124 | IotPolicyName: string 125 | } 126 | 127 | /** 128 | * The custom resource response type 129 | * @interface CustomResourceResponse 130 | */ 131 | export interface CustomResourceResponse { 132 | Status: StatusTypes; 133 | Data: any; 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /source/custom-resource/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [''], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | }, 7 | coverageReporters: [ 8 | 'text', 9 | ['lcov', { 'projectRoot': '../' }] 10 | ], 11 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 12 | }; -------------------------------------------------------------------------------- /source/custom-resource/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iot-device-simulator-custom-resource", 3 | "version": "3.0.9", 4 | "description": "custom resource lambda for iot-device-simulator", 5 | "author": { 6 | "name": "Amazon Web Services", 7 | "url": "https://aws.amazon.com/solutions" 8 | }, 9 | "main": "index.js", 10 | "scripts": { 11 | "clean": "rm -rf node_modules dist coverage package-lock.json", 12 | "compile": "node_modules/typescript/bin/tsc --project tsconfig.json", 13 | "build": "npm run clean && npm install && npm run compile", 14 | "copy-modules": "npm prune --production && rsync -avrq ./node_modules ./dist", 15 | "package": "npm run build && npm run copy-modules && cd dist && zip -q -r9 package.zip * -x '**/test/*' && cd ..", 16 | "test": "jest --config jest.config.js --coverage --silent" 17 | }, 18 | "devDependencies": { 19 | "@smithy/util-stream": "^2.0.4", 20 | "@types/jest": "^29.5.3", 21 | "@types/node": "^20.5.0", 22 | "@types/uuid": "^9.0.2", 23 | "aws-sdk-client-mock": "^3.0.0", 24 | "aws-sdk-client-mock-jest": "^3.0.0", 25 | "jest": "^29.6.2", 26 | "ts-jest": "^29.1.1", 27 | "ts-node": "^10.9.1", 28 | "typescript": "^5.1.6" 29 | }, 30 | "dependencies": { 31 | "@aws-sdk/client-iot": "^3.391.0", 32 | "@aws-sdk/client-s3": "^3.391.0", 33 | "axios": "^1.7.4", 34 | "uuid": "^9.0.0" 35 | }, 36 | "engines": { 37 | "node": ">=18.0.0" 38 | }, 39 | "license": "Apache-2.0" 40 | } -------------------------------------------------------------------------------- /source/custom-resource/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true 10 | }, 11 | "include": [ 12 | "**/*.ts" 13 | ], 14 | "exclude": [ 15 | "node_modules", 16 | "package" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /source/infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /source/infrastructure/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /source/infrastructure/bin/iot-device-simulator.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { addCfnSuppressRules } from "@aws-solutions-constructs/core"; 5 | import { App, Aspects, CfnResource, DefaultStackSynthesizer, IAspect } from "aws-cdk-lib"; 6 | import { CfnFunction } from "aws-cdk-lib/aws-lambda"; 7 | import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag'; 8 | import { IConstruct } from "constructs"; 9 | import { IDSStack } from "../lib/iot-device-simulator-stack"; 10 | 11 | /** 12 | * CDK Aspect implementation to add common metadata to suppress CFN rules 13 | */ 14 | class LambdaFunctionAspect implements IAspect { 15 | visit(node: IConstruct): void { 16 | const resource = node as CfnResource; 17 | if (resource instanceof CfnFunction) { 18 | const rules = [ 19 | { id: "W58", reason: "The function does have permission to write CloudWatch Logs." }, 20 | { id: "W89", reason: "The Lambda function does not require any VPC connection at all." }, 21 | { id: "W92", reason: "The Lambda function does not require ReservedConcurrentExecutions." }, 22 | ]; 23 | 24 | addCfnSuppressRules(resource, rules); 25 | } 26 | } 27 | } 28 | 29 | const app = new App(); 30 | const stack = new IDSStack(app, "IDSStack", { 31 | description: 32 | "(SO0041) - The AWS cloud formation template for the deployment of SOLUTION_NAME_PLACEHOLDER. Version VERSION_PLACEHOLDER", 33 | synthesizer: new DefaultStackSynthesizer({ 34 | generateBootstrapVersionRule: false, 35 | }), 36 | }); 37 | NagSuppressions.addStackSuppressions(stack, [ 38 | { id: 'AwsSolutions-IAM5', reason: 'All IAM policies defined in this solution grant only least-privilege permissions. Wild card for resources is used only for services which either do not have a resource arn or does not allow for resource specification' }, 39 | { id: 'AwsSolutions-APIG3', reason: 'No need to enable WAF as it is up to users.' }, 40 | { id: 'AwsSolutions-APIG4', reason: 'Authorized by IAM' }, 41 | { id: 'AwsSolutions-COG4', reason: 'Authorized by IAM' }, 42 | { id: 'AwsSolutions-IAM4', reason: 'AmazonAPIGatewayPushToCloudWatchLogs managed policy is used by CDK itself.'} 43 | ]); 44 | Aspects.of(stack).add(new LambdaFunctionAspect()); 45 | Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); -------------------------------------------------------------------------------- /source/infrastructure/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/iot-device-simulator.ts", 3 | "context": { 4 | "@aws-cdk/core:newStyleStackSynthesis": false, 5 | "@aws-cdk/aws-lambda:recognizeVersionProps":false, 6 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": false, 7 | "constructs:stackRelativeExports": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /source/infrastructure/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /source/infrastructure/lib/api.ts: -------------------------------------------------------------------------------- 1 | 2 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | import { addCfnSuppressRules } from "@aws-solutions-constructs/core"; 6 | import { Aws, RemovalPolicy } from "aws-cdk-lib"; 7 | import { 8 | AccessLogFormat, 9 | AuthorizationType, 10 | ContentHandling, 11 | Deployment, 12 | EndpointType, 13 | Integration, 14 | IntegrationType, 15 | LogGroupLogDestination, 16 | MethodLoggingLevel, 17 | MethodOptions, 18 | PassthroughBehavior, 19 | RequestValidator, 20 | RestApi, 21 | Stage 22 | } from "aws-cdk-lib/aws-apigateway"; 23 | import { Table } from "aws-cdk-lib/aws-dynamodb"; 24 | import { Policy, ServicePrincipal } from "aws-cdk-lib/aws-iam"; 25 | import { Function as LambdaFunction } from "aws-cdk-lib/aws-lambda"; 26 | import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs"; 27 | import { IBucket } from "aws-cdk-lib/aws-s3"; 28 | import { Construct } from "constructs"; 29 | 30 | /** 31 | * ApiConstructProps props 32 | * @interface ApiConstructProps 33 | */ 34 | export interface ApiConstructProps { 35 | // Policy for CloudWatch Logs 36 | readonly cloudWatchLogsPolicy: Policy; 37 | // IoT endpoint address 38 | readonly stepFunctionsARN: string; 39 | // Simulation data DynamoDB table name 40 | readonly simulationTable: Table; 41 | // Device Type data DynamoDB table name 42 | readonly deviceTypeTable: Table; 43 | //Routes S3 bucket 44 | readonly routesBucketArn: string; 45 | //microservices lambda 46 | readonly microservicesLambda: LambdaFunction; 47 | /** 48 | * Solution config properties. 49 | * Logging level, solution ID, version, source code bucket, and source code prefix 50 | */ 51 | readonly solutionConfig: { 52 | sendAnonymousUsage: string; 53 | solutionId: string; 54 | solutionVersion: string; 55 | sourceCodeBucket: IBucket; 56 | sourceCodePrefix: string; 57 | }; 58 | // Solution UUID 59 | readonly uuid: string; 60 | } 61 | 62 | /** 63 | * @class 64 | * IoT Device Simulator Framework API Construct. 65 | * It creates an API Gateway REST API and other resources. 66 | */ 67 | export class ApiConstruct extends Construct { 68 | // API endpoint 69 | public apiEndpoint: string; 70 | // API ID 71 | public apiId: string; 72 | //microservices lambda 73 | public microservicesLambdaFunction: LambdaFunction; 74 | 75 | constructor(scope: Construct, id: string, props: ApiConstructProps) { 76 | super(scope, id); 77 | 78 | const apiLogGroup = new LogGroup(this, 'Logs', { 79 | removalPolicy: RemovalPolicy.DESTROY, 80 | retention: RetentionDays.THREE_MONTHS 81 | }); 82 | addCfnSuppressRules(apiLogGroup, [{ 83 | id: 'W84', reason: 'CloudWatch Logs are already encrypted by default.' 84 | }]); 85 | 86 | const api = new RestApi(this, 'IoTDeviceSimulatorApi', { 87 | defaultCorsPreflightOptions: { 88 | allowOrigins: ['*'], 89 | allowHeaders: [ 90 | 'Authorization', 91 | 'Content-Type', 92 | 'X-Amz-Date', 93 | 'X-Amz-Security-Token', 94 | 'X-Api-Key' 95 | ], 96 | allowMethods: [ 97 | 'GET', 98 | 'POST', 99 | 'PUT', 100 | 'DELETE', 101 | 'OPTIONS' 102 | ], 103 | statusCode: 200 104 | }, 105 | deploy: true, 106 | deployOptions: { 107 | accessLogDestination: new LogGroupLogDestination(apiLogGroup), 108 | accessLogFormat: AccessLogFormat.jsonWithStandardFields(), 109 | loggingLevel: MethodLoggingLevel.INFO, 110 | stageName: 'prod', 111 | tracingEnabled: true 112 | }, 113 | description: 'IoT Device Simulator Rest API', 114 | endpointTypes: [EndpointType.REGIONAL] 115 | }); 116 | this.apiEndpoint = `https://${api.restApiId}.execute-api.${Aws.REGION}.amazonaws.com/prod`; 117 | this.apiId = api.restApiId; 118 | 119 | const requestValidator = new RequestValidator(this, 'ApiRequestValidator', { 120 | restApi: api, 121 | validateRequestParameters: true, 122 | validateRequestBody: true 123 | }); 124 | 125 | addCfnSuppressRules(api.node.findChild('Deployment') as Deployment, [{ 126 | id: 'W68', reason: 'The solution does not require the usage plan.' 127 | }]); 128 | addCfnSuppressRules(api.node.findChild('DeploymentStage.prod') as Stage, [{ 129 | id: 'W64', reason: 'The solution does not require the usage plan.' 130 | }]); 131 | 132 | /** 133 | * method options for all methods 134 | */ 135 | const universalMethodOptions: MethodOptions = { 136 | authorizationType: AuthorizationType.IAM, 137 | methodResponses: [{ 138 | statusCode: '200', 139 | responseModels: { 140 | 'application/json': { modelId: 'Empty' } 141 | } 142 | }], 143 | requestParameters: { 'method.request.querystring.nextToken': false }, 144 | requestValidator: requestValidator 145 | }; 146 | 147 | /** 148 | * Integration for all resources 149 | */ 150 | const universalIntegration = new Integration({ 151 | type: IntegrationType.AWS_PROXY, 152 | integrationHttpMethod: 'POST', 153 | options: { 154 | contentHandling: ContentHandling.CONVERT_TO_TEXT, 155 | integrationResponses: [{ statusCode: '200' }], 156 | passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH 157 | }, 158 | uri: `arn:${Aws.PARTITION}:apigateway:${Aws.REGION}:lambda:path/2015-03-31/functions/${props.microservicesLambda.functionArn}/invocations` 159 | }); 160 | 161 | /** 162 | * simulation API 163 | * ANY /simulation 164 | * ANY /simulation/{simid} 165 | */ 166 | const simulationResource = api.root.addResource('simulation'); 167 | simulationResource.addMethod( 168 | 'ANY', 169 | universalIntegration, 170 | universalMethodOptions 171 | ); 172 | const simulationSimIdResource = simulationResource.addResource('{simid}'); 173 | simulationSimIdResource.addMethod('ANY', universalIntegration, universalMethodOptions); 174 | 175 | /** 176 | * Devive Types API 177 | * ANY /devicetypes 178 | * ANY /devicetypes/{typeid} 179 | */ 180 | const deviceTypesResource = api.root.addResource('devicetypes'); 181 | deviceTypesResource.addMethod('ANY', universalIntegration, universalMethodOptions); 182 | const typeIdResource = deviceTypesResource.addResource('{typeid}'); 183 | typeIdResource.addMethod('ANY', universalIntegration, universalMethodOptions); 184 | 185 | props.microservicesLambda.addPermission('ApiLambdaInvokePermission', { 186 | action: 'lambda:InvokeFunction', 187 | principal: new ServicePrincipal('apigateway.amazonaws.com'), 188 | sourceArn: api.arnForExecuteApi() 189 | }); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /source/infrastructure/lib/application-resource.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Application, AttributeGroup } from "@aws-cdk/aws-servicecatalogappregistry-alpha"; 5 | import { Aws, CfnMapping, Fn, Stack, Tags } from "aws-cdk-lib"; 6 | 7 | declare module "aws-cdk-lib" { 8 | export interface CfnMapping { 9 | setDataMapValue(key: string, value: string): void; 10 | findInDataMap(key: string): string; 11 | } 12 | } 13 | 14 | CfnMapping.prototype.setDataMapValue = function (key: string, value: string): void { 15 | this.setValue("Data", key, value); 16 | }; 17 | 18 | CfnMapping.prototype.findInDataMap = function (key: string): string { 19 | return this.findInMap("Data", key); 20 | }; 21 | 22 | // Set an arbitrary value to use as a prefix for the DefaultApplicationAttributeGroup name 23 | // This may change in the future, and must not match the previous two prefixes 24 | const attributeGroupPrefix = "S01"; 25 | 26 | // Declare KVP object for type checked values 27 | const AppRegistryMetadata = { 28 | ID: "ID", 29 | Version: "Version", 30 | AppRegistryApplicationName: "AppRegistryApplicationName", 31 | SolutionName: "SolutionName", 32 | ApplicationType: "ApplicationType" 33 | }; 34 | 35 | /** 36 | * Solution config properties. 37 | * Logging level, solution ID, version, source code bucket, and source code prefix 38 | */ 39 | interface SolutionProps { 40 | readonly solutionId: string; 41 | readonly solutionName: string; 42 | readonly solutionVersion: string; 43 | readonly solutionDescription: string; 44 | } 45 | 46 | export function applyAppRegistry(stack: Stack, solutionProps: SolutionProps) { 47 | // Declare CFN Mappings 48 | const map = new CfnMapping(stack, "AppRegistry", { lazy: true }); 49 | map.setDataMapValue(AppRegistryMetadata.ID, solutionProps.solutionId); 50 | map.setDataMapValue(AppRegistryMetadata.Version, solutionProps.solutionVersion); 51 | map.setDataMapValue(AppRegistryMetadata.AppRegistryApplicationName, solutionProps.solutionName); 52 | map.setDataMapValue(AppRegistryMetadata.SolutionName, solutionProps.solutionName); 53 | map.setDataMapValue(AppRegistryMetadata.ApplicationType, "AWS-Solutions"); 54 | 55 | const application = new Application(stack, "Application", { 56 | applicationName: Fn.join("-", [ 57 | map.findInDataMap(AppRegistryMetadata.AppRegistryApplicationName), 58 | Aws.REGION, 59 | Aws.ACCOUNT_ID, 60 | Aws.STACK_NAME 61 | ]), 62 | description: solutionProps.solutionDescription ?? "Iot Device Simulator Description" 63 | }); 64 | application.associateApplicationWithStack(stack); 65 | 66 | Tags.of(application).add("Solutions:SolutionID", map.findInDataMap(AppRegistryMetadata.ID)); 67 | Tags.of(application).add("Solutions:SolutionName", map.findInDataMap(AppRegistryMetadata.SolutionName)); 68 | Tags.of(application).add("Solutions:SolutionVersion", map.findInDataMap(AppRegistryMetadata.Version)); 69 | Tags.of(application).add("Solutions:ApplicationType", map.findInDataMap(AppRegistryMetadata.ApplicationType)); 70 | 71 | const attributeGroup = new AttributeGroup(stack, "DefaultApplicationAttributeGroup", { 72 | // Use SolutionName as a unique prefix for the attribute group name 73 | attributeGroupName: Fn.join("-", [attributeGroupPrefix, Aws.REGION, Aws.STACK_NAME]), 74 | description: "Attribute group for solution information", 75 | attributes: { 76 | applicationType: map.findInDataMap(AppRegistryMetadata.ApplicationType), 77 | version: map.findInDataMap(AppRegistryMetadata.Version), 78 | solutionID: map.findInDataMap(AppRegistryMetadata.ID), 79 | solutionName: map.findInDataMap(AppRegistryMetadata.SolutionName) 80 | } 81 | }); 82 | attributeGroup.associateWith(application); 83 | } 84 | -------------------------------------------------------------------------------- /source/infrastructure/lib/common-resources.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { addCfnSuppressRules } from "@aws-solutions-constructs/core"; 5 | import {Construct} from "constructs"; 6 | import {Effect, Policy, PolicyStatement} from "aws-cdk-lib/aws-iam"; 7 | import { 8 | BlockPublicAccess, 9 | Bucket, 10 | BucketAccessControl, 11 | BucketEncryption, 12 | IBucket, 13 | ObjectOwnership 14 | } from "aws-cdk-lib/aws-s3"; 15 | import {ArnFormat, RemovalPolicy, Stack} from "aws-cdk-lib"; 16 | 17 | /** 18 | * CommonResourcesConstruct props 19 | * @interface CommonResourcesConstructProps 20 | */ 21 | export interface CommonResourcesConstructProps { 22 | // Source code bucket 23 | sourceCodeBucket: string; 24 | } 25 | 26 | /** 27 | * @class 28 | * IoT Device Simulator Common Resources Construct. 29 | * It creates a common CloudWatch Logs policy for Lambda functions, a logging S3 bucket, and M2C2 S3 bucket. 30 | */ 31 | export class CommonResourcesConstruct extends Construct { 32 | // CloudWatch Logs policy 33 | public cloudWatchLogsPolicy: Policy; 34 | // S3 Logging bucket 35 | public s3LoggingBucket: Bucket; 36 | // Code S3 bucket 37 | public sourceCodeBucket: IBucket; 38 | 39 | constructor(scope: Construct, id: string, props: CommonResourcesConstructProps) { 40 | super(scope, id); 41 | 42 | this.cloudWatchLogsPolicy = new Policy(this, 'CloudWatchLogsPolicy', { 43 | statements: [ 44 | new PolicyStatement({ 45 | effect: Effect.ALLOW, 46 | actions: [ 47 | 'logs:CreateLogGroup', 48 | 'logs:CreateLogStream', 49 | 'logs:PutLogEvents' 50 | ], 51 | resources: [ 52 | Stack.of(this).formatArn({ service: 'logs', resource: 'log-group', resourceName: '/aws/lambda/*', arnFormat: ArnFormat.COLON_RESOURCE_NAME }) 53 | ] 54 | }) 55 | ] 56 | }); 57 | 58 | this.s3LoggingBucket = new Bucket(this, 'LogBucket', { 59 | objectOwnership: ObjectOwnership.OBJECT_WRITER, 60 | accessControl: BucketAccessControl.LOG_DELIVERY_WRITE, 61 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 62 | encryption: BucketEncryption.S3_MANAGED, 63 | removalPolicy: RemovalPolicy.RETAIN, 64 | enforceSSL: true 65 | }); 66 | 67 | addCfnSuppressRules(this.s3LoggingBucket, [ 68 | { id: 'W35', reason: 'This bucket is to store S3 logs, so it does not require access logs.' }, 69 | { id: 'W51', reason: 'This bucket is to store S3 logs, so it does not require S3 policy.' } 70 | ]); 71 | 72 | this.sourceCodeBucket = Bucket.fromBucketName(this, 'SourceCodeBucket', props.sourceCodeBucket); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /source/infrastructure/lib/iot-device-simulator-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { ApiConstruct} from "./api"; 5 | import { CommonResourcesConstruct } from "./common-resources"; 6 | import { CustomResourcesConstruct } from "./custom-resource"; 7 | import { StorageContruct } from "./storage"; 8 | import { SimulatorConstruct } from "./simulator"; 9 | import { ConsoleConstruct } from "./console"; 10 | import { CfnMap, CfnPlaceIndex } from 'aws-cdk-lib/aws-location'; 11 | import { Construct } from "constructs"; 12 | import { Aws, CfnMapping, CfnOutput, CfnParameter, Fn, Stack, StackProps, Tags } from "aws-cdk-lib"; 13 | import {applyAppRegistry} from "./application-resource"; 14 | 15 | export class IDSStack extends Stack { 16 | constructor(scope: Construct, id: string, props?: StackProps) { 17 | super(scope, id, props); 18 | 19 | // The code that defines your stack goes here 20 | this.templateOptions.templateFormatVersion = '2010-09-09'; 21 | 22 | // CFN Parameters 23 | // Admin E-mail parameter 24 | const adminEmail = new CfnParameter(this, 'UserEmail', { 25 | type: 'String', 26 | description: 'The user E-Mail to access the UI', 27 | allowedPattern: '^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$', 28 | constraintDescription: 'User E-Mail must be a valid E-Mail address.' 29 | }); 30 | 31 | // CloudFormation metadata 32 | this.templateOptions.metadata = { 33 | 'AWS::CloudFormation::Interface': { 34 | ParameterGroups: [ 35 | { 36 | Label: { default: 'Console access' }, 37 | Parameters: [adminEmail.logicalId] 38 | }, 39 | ], 40 | ParameterLabels: { 41 | [adminEmail.logicalId]: { default: '* Console Administrator Email' }, 42 | } 43 | } 44 | }; 45 | 46 | // CFN Mappings 47 | const solutionMapping = new CfnMapping(this, 'Solution', { 48 | mapping: { 49 | Config: { 50 | SolutionId: 'SO0041', 51 | SolutionName: 'SOLUTION_NAME_PLACEHOLDER', 52 | Version: 'VERSION_PLACEHOLDER', 53 | SendAnonymousUsage: 'Yes', 54 | S3Bucket: 'BUCKET_NAME_PLACEHOLDER', 55 | KeyPrefix: 'SOLUTION_NAME_PLACEHOLDER/VERSION_PLACEHOLDER' 56 | } 57 | } 58 | }); 59 | const sendAnonymousUsage = solutionMapping.findInMap('Config', 'SendAnonymousUsage'); 60 | const solutionId = solutionMapping.findInMap('Config', 'SolutionId'); 61 | const solutionName = solutionMapping.findInMap('Config', 'SolutionName'); 62 | const solutionVersion = solutionMapping.findInMap('Config', 'Version'); 63 | const solutionDescription = `(${solutionId}) - ${solutionName} Version ${solutionVersion}`; 64 | const sourceCodeBucket = Fn.join('-', [solutionMapping.findInMap('Config', 'S3Bucket'), Aws.REGION]); 65 | const sourceCodePrefix = solutionMapping.findInMap('Config', 'KeyPrefix'); 66 | 67 | // Common Resources 68 | const commonResources = new CommonResourcesConstruct(this, 'CommonResources', { 69 | sourceCodeBucket 70 | }); 71 | 72 | //Databases 73 | const storage = new StorageContruct(this, 'storage', { 74 | solutionId: solutionId, 75 | s3LogsBucket: commonResources.s3LoggingBucket 76 | }); 77 | 78 | // Custom Resources 79 | const customResources = new CustomResourcesConstruct(this, 'CustomResources', { 80 | cloudWatchLogsPolicy: commonResources.cloudWatchLogsPolicy, 81 | solutionConfig: { 82 | solutionId, 83 | solutionVersion, 84 | sourceCodeBucket: commonResources.sourceCodeBucket, 85 | sourceCodePrefix 86 | } 87 | }); 88 | 89 | //Simulator 90 | const simulator = new SimulatorConstruct(this, 'simulator', { 91 | cloudWatchLogsPolicy: commonResources.cloudWatchLogsPolicy, 92 | iotEndpointAddress: customResources.iotEndpoint, 93 | simulationTable: storage.simulationsTable, 94 | deviceTypeTable: storage.deviceTypesTable, 95 | routesBucket: storage.routesBucket, 96 | uniqueSuffix: customResources.uniqueSuffix, 97 | solutionConfig: { 98 | sendAnonymousUsage: sendAnonymousUsage, 99 | solutionId: solutionId, 100 | solutionVersion: solutionVersion, 101 | sourceCodeBucket: commonResources.sourceCodeBucket, 102 | sourceCodePrefix: sourceCodePrefix 103 | }, 104 | // Solution UUID 105 | uuid: customResources.uuid 106 | }); 107 | 108 | const api = new ApiConstruct(this, 'API', { 109 | microservicesLambda: simulator.microservicesLambdaFunction, 110 | cloudWatchLogsPolicy: commonResources.cloudWatchLogsPolicy, 111 | stepFunctionsARN: simulator.simulatorStepFunctions.stateMachineArn, 112 | simulationTable: storage.simulationsTable, 113 | deviceTypeTable: storage.deviceTypesTable, 114 | routesBucketArn: storage.routesBucket.bucketArn, 115 | solutionConfig: { 116 | sendAnonymousUsage: sendAnonymousUsage, 117 | solutionId: solutionId, 118 | solutionVersion: solutionVersion, 119 | sourceCodeBucket: commonResources.sourceCodeBucket, 120 | sourceCodePrefix: sourceCodePrefix 121 | }, 122 | uuid: customResources.uuid 123 | }); 124 | 125 | const idsMap = new CfnMap(this, "IotDeviceSimulatorMap", { 126 | configuration: { 127 | style: 'VectorEsriNavigation' 128 | }, 129 | mapName: `${customResources.reducedStackName}-IotDeviceSimulatorMap-${customResources.uniqueSuffix}`, 130 | pricingPlan: 'RequestBasedUsage' 131 | }); 132 | 133 | const idsPlaceIndex = new CfnPlaceIndex(this, "IotDeviceSimulatorPlaceIndex", { 134 | dataSource: "Esri", 135 | indexName: `${customResources.reducedStackName}-IoTDeviceSimulatorPlaceIndex-${customResources.uniqueSuffix}`, 136 | pricingPlan: "RequestBasedUsage" 137 | }); 138 | 139 | const console = new ConsoleConstruct(this, 'Console', { 140 | mapArn: idsMap.attrMapArn, 141 | placeIndexArn: idsPlaceIndex.attrIndexArn, 142 | apiId: api.apiId, 143 | s3LogsBucket: commonResources.s3LoggingBucket, 144 | adminEmail: adminEmail.valueAsString 145 | }); 146 | 147 | customResources.setupUi({ 148 | mapName: idsMap.mapName, 149 | placeIndexName: idsPlaceIndex.indexName, 150 | apiEndpoint: api.apiEndpoint, 151 | iotPolicyName: console.iotPolicy.ref, 152 | cognitoIdentityPool: console.identityPoolId, 153 | cognitoUserPool: console.userPoolId, 154 | cognitoUserPoolClient: console.webClientId, 155 | routesBucket: storage.routesBucket, 156 | consoleBucket: console.consoleBucket, 157 | }); 158 | 159 | customResources.setupDetachIotPolicyCustomResource({ 160 | iotPolicyName: console.iotPolicy.ref 161 | }); 162 | 163 | // Register this application in App Registry 164 | applyAppRegistry(this, { 165 | solutionId: solutionId, 166 | solutionName: solutionName, 167 | solutionVersion: solutionVersion, 168 | solutionDescription: solutionDescription, 169 | }); 170 | 171 | //Outputs 172 | new CfnOutput(this, 'DeviceTypesTable', { // NOSONAR: typescript:S1848 173 | description: 'The device types table name.', 174 | value: storage.deviceTypesTable.tableName 175 | }); 176 | new CfnOutput(this, 'SimulationsTable', { // NOSONAR: typescript:S1848 177 | description: 'The simulations table name', 178 | value: storage.simulationsTable.tableName 179 | }); 180 | new CfnOutput(this, 'API-Endpoint', { // NOSONAR: typescript:S1848 181 | description: 'The API endpoint', 182 | value: api.apiEndpoint 183 | }); 184 | new CfnOutput(this, 'ConsoleClientId', { // NOSONAR: typescript:S1848 185 | description: 'The console client ID', 186 | value: console.webClientId 187 | }); 188 | new CfnOutput(this, 'IdentityPoolId', { // NOSONAR: typescript:S1848 189 | description: 'The ID for the Cognitio Identity Pool', 190 | value: console.identityPoolId 191 | }); 192 | new CfnOutput(this, 'UserPoolId', { // NOSONAR: typescript:S1848 193 | description: 'The Cognito User Pool ID', 194 | value: console.userPoolId 195 | }); 196 | new CfnOutput(this, 'Console URL', { // NOSONAR: typescript:S1848 197 | description: 'The URL to access the console', 198 | value: `https://${console.cloudFrontDomainName}` 199 | }); 200 | new CfnOutput(this, 'UUID', { // NOSONAR: typescript:S1848 201 | description: 'The solution UUID', 202 | value: customResources.uuid 203 | }); 204 | 205 | //tag resources 206 | Tags.of(this).add('SolutionId', solutionId); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /source/infrastructure/lib/storage.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { RemovalPolicy } from "aws-cdk-lib"; 5 | import { AttributeType, BillingMode, Table, TableEncryption } from "aws-cdk-lib/aws-dynamodb"; 6 | import { BlockPublicAccess, Bucket, BucketEncryption } from "aws-cdk-lib/aws-s3"; 7 | import { Construct } from "constructs"; 8 | 9 | /**, 10 | * @interface StorageContructProps 11 | * StorageContruct props 12 | */ 13 | export interface StorageContructProps { 14 | readonly solutionId: string; 15 | readonly s3LogsBucket: Bucket 16 | } 17 | 18 | /** 19 | * IoT Device Simulator storage construct 20 | * Creates an S3 bucket to store test scenarios and 21 | * a DynamoDB table to store tests and test configuration 22 | */ 23 | export class StorageContruct extends Construct { 24 | public simulationsTable: Table; 25 | public deviceTypesTable: Table; 26 | public routesBucket: Bucket 27 | 28 | constructor(scope: Construct, id: string, props: StorageContructProps) { 29 | super(scope, id); 30 | 31 | this.simulationsTable = new Table(this, 'IDS-Simulations-Table', { 32 | billingMode: BillingMode.PAY_PER_REQUEST, 33 | encryption: TableEncryption.AWS_MANAGED, 34 | partitionKey: { name: 'simId', type: AttributeType.STRING }, 35 | pointInTimeRecovery: true 36 | }); 37 | 38 | this.deviceTypesTable = new Table(this, 'IDS-Device-Types-Table', { 39 | billingMode: BillingMode.PAY_PER_REQUEST, 40 | encryption: TableEncryption.AWS_MANAGED, 41 | partitionKey: { name: 'typeId', type: AttributeType.STRING }, 42 | pointInTimeRecovery: true 43 | }); 44 | 45 | this.routesBucket = new Bucket(this, 'RoutesBucket', { 46 | removalPolicy: RemovalPolicy.RETAIN, 47 | serverAccessLogsBucket: props.s3LogsBucket, 48 | serverAccessLogsPrefix: 'routes-bucket-access/', 49 | encryption: BucketEncryption.S3_MANAGED, 50 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 51 | enforceSSL: true 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /source/infrastructure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iot-device-simulator-infrastructure", 3 | "version": "3.0.9", 4 | "description": "IoT Device Simulator Infrastructure", 5 | "license": "Apache-2.0", 6 | "author": { 7 | "name": "Amazon Web Services", 8 | "url": "https://aws.amazon.com/solutions" 9 | }, 10 | "bin": { 11 | "infrastructure": "bin/infrastructure.js" 12 | }, 13 | "scripts": { 14 | "clean": "rm -rf node_modules package-lock.json", 15 | "build": "tsc", 16 | "watch": "tsc -w", 17 | "test": "export overrideWarningsEnabled=false && jest --coverage", 18 | "cdk": "cdk" 19 | }, 20 | "devDependencies": { 21 | "@types/jest": "^29.5.3", 22 | "@types/node": "^20.5.0", 23 | "aws-cdk": "2.92.0", 24 | "jest": "^29.6.2", 25 | "ts-jest": "^29.1.1", 26 | "ts-node": "^10.9.1", 27 | "typescript": "^5.1.6" 28 | }, 29 | "dependencies": { 30 | "@aws-cdk/aws-servicecatalogappregistry-alpha": "^2.92.0-alpha.0", 31 | "@aws-solutions-constructs/aws-cloudfront-s3": "~2.42.0", 32 | "@aws-solutions-constructs/aws-lambda-stepfunctions": "~2.42.0", 33 | "aws-cdk-lib": "~2.92.0", 34 | "cdk-nag": "2.27.104", 35 | "constructs": "~10.2.69" 36 | }, 37 | "engines": { 38 | "node": ">=18.0.0" 39 | } 40 | } -------------------------------------------------------------------------------- /source/infrastructure/test/api.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Stack } from "aws-cdk-lib"; 5 | import { Capture, Match, Template } from 'aws-cdk-lib/assertions'; 6 | import { AttributeType, Table } from "aws-cdk-lib/aws-dynamodb"; 7 | import { Policy, PolicyStatement } from "aws-cdk-lib/aws-iam"; 8 | import { Code, Function as LambdaFunction, Runtime } from "aws-cdk-lib/aws-lambda"; 9 | import { Bucket } from "aws-cdk-lib/aws-s3"; 10 | import { ApiConstruct } from "../lib/api"; 11 | 12 | describe('When IoT Device Simulator API is created', () => { 13 | let template: Template; 14 | 15 | beforeAll(() => { 16 | template = buildStack(); 17 | }); 18 | 19 | const simulatorApiCapture = new Capture(); 20 | const apiLogGroupCapture = new Capture(); 21 | const lambdaRoleCapture = new Capture(); 22 | 23 | it('it should have api gateway resources', () => { 24 | template.resourceCountIs('AWS::ApiGateway::RequestValidator', 1); 25 | template.resourceCountIs('AWS::ApiGateway::Deployment', 1); 26 | 27 | template.hasResourceProperties('AWS::ApiGateway::Deployment', { 28 | "RestApiId": { 29 | "Ref": simulatorApiCapture, 30 | } 31 | }); 32 | expect(template.toJSON()['Resources'][simulatorApiCapture.asString()]['Type']).toStrictEqual('AWS::ApiGateway::RestApi'); 33 | 34 | template.resourceCountIs('AWS::ApiGateway::Stage', 1); 35 | template.hasResourceProperties('AWS::ApiGateway::Stage', { 36 | "AccessLogSetting": { 37 | "DestinationArn": { 38 | "Fn::GetAtt": [ 39 | apiLogGroupCapture, 40 | "Arn", 41 | ], 42 | }, 43 | "Format": Match.anyValue() 44 | } 45 | }); 46 | expect(template.toJSON()['Resources'][apiLogGroupCapture.asString()]['Type']).toStrictEqual('AWS::Logs::LogGroup'); 47 | 48 | template.hasResourceProperties('AWS::ApiGateway::Method', { 49 | 'HttpMethod': 'OPTIONS' 50 | }); 51 | template.hasResourceProperties('AWS::ApiGateway::Method', { 52 | 'HttpMethod': 'ANY' 53 | }); 54 | }); 55 | 56 | it('it should have dynamoDB Tables', () => { 57 | template.resourceCountIs('AWS::DynamoDB::Table', 2); 58 | template.hasResource('AWS::DynamoDB::Table', { 59 | "DeletionPolicy": "Retain", 60 | "Properties": { 61 | "AttributeDefinitions": [ 62 | { 63 | "AttributeName": "typeId", 64 | "AttributeType": "S", 65 | }, 66 | ], 67 | "KeySchema": [ 68 | { 69 | "AttributeName": "typeId", 70 | "KeyType": "HASH", 71 | }, 72 | ], 73 | "ProvisionedThroughput": { 74 | "ReadCapacityUnits": 5, 75 | "WriteCapacityUnits": 5, 76 | }, 77 | }, 78 | "Type": "AWS::DynamoDB::Table", 79 | "UpdateReplacePolicy": "Retain", 80 | }); 81 | template.hasResource('AWS::DynamoDB::Table', { 82 | "DeletionPolicy": "Retain", 83 | "Properties": { 84 | "AttributeDefinitions": [ 85 | { 86 | "AttributeName": "simId", 87 | "AttributeType": "S", 88 | }, 89 | ], 90 | "KeySchema": [ 91 | { 92 | "AttributeName": "simId", 93 | "KeyType": "HASH", 94 | }, 95 | ], 96 | "ProvisionedThroughput": { 97 | "ReadCapacityUnits": 5, 98 | "WriteCapacityUnits": 5, 99 | }, 100 | }, 101 | "Type": "AWS::DynamoDB::Table", 102 | "UpdateReplacePolicy": "Retain", 103 | }); 104 | }); 105 | it('it should have dynamoDB Tables', () => { 106 | template.resourceCountIs('AWS::Lambda::Function', 1); 107 | template.hasResourceProperties('AWS::Lambda::Function', { 108 | 'Role' : { 109 | 'Fn::GetAtt': [ 110 | lambdaRoleCapture, 111 | 'Arn' 112 | ]} 113 | }); 114 | expect(template.toJSON()['Resources'][lambdaRoleCapture.asString()]['Type']).toStrictEqual('AWS::IAM::Role'); 115 | template.resourceCountIs('AWS::Lambda::Permission', 1); 116 | }); 117 | 118 | }); 119 | 120 | function buildStack() { 121 | const stack = new Stack(); 122 | const testPolicy = new Policy(stack, 'TestPolicy', { 123 | statements: [ 124 | new PolicyStatement({ 125 | resources: ['*'], 126 | actions: ['cloudwatch:Get*'] 127 | }) 128 | ] 129 | }); 130 | const testDTypeTable = new Table(stack, 'TestDTypeTable', { 131 | partitionKey: { 132 | name: "typeId", type: AttributeType.STRING 133 | } 134 | }); 135 | const testSimTable = new Table(stack, 'TestSimTable', { 136 | partitionKey: { 137 | name: "simId", type: AttributeType.STRING 138 | } 139 | }); 140 | const testSourceBucket = Bucket.fromBucketName(stack, 'SourceCodeBucket', 'test-bucket-region'); 141 | const testFunction = new LambdaFunction(stack, 'testFunction', { 142 | handler: 'index.handler', 143 | runtime: Runtime.NODEJS_18_X, 144 | code: Code.fromBucket(testSourceBucket, `prefix/custom-resource.zip`) 145 | }); 146 | 147 | const api = new ApiConstruct(stack, 'TestAPI', { 148 | cloudWatchLogsPolicy: testPolicy, 149 | stepFunctionsARN: 'arn:aws:states:us-east-1:someAccount:stateMachine:HelloWorld-StateMachine', 150 | simulationTable: testDTypeTable, 151 | deviceTypeTable: testSimTable, 152 | routesBucketArn: 'arn:aws:s3:::testRouteBucket', 153 | microservicesLambda: testFunction, 154 | solutionConfig: { 155 | sendAnonymousUsage: 'Yes', 156 | solutionId: 'testId', 157 | solutionVersion: 'testVersion', 158 | sourceCodeBucket: testSourceBucket, 159 | sourceCodePrefix: 'testPrefix/', 160 | 161 | }, 162 | uuid: 'abc123' 163 | }); 164 | return Template.fromStack(stack); 165 | } 166 | -------------------------------------------------------------------------------- /source/infrastructure/test/common-resources.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | import { Stack } from "aws-cdk-lib"; 6 | import { Template } from 'aws-cdk-lib/assertions'; 7 | import { CommonResourcesConstruct } from "../lib/common-resources"; 8 | 9 | test('IoT Device Simulator CommonResourceConstruct Test', () => { 10 | const stack = new Stack(); 11 | 12 | const commonResources = new CommonResourcesConstruct(stack, 'TestCommonResource', { 13 | sourceCodeBucket: "test-bucket" 14 | }); 15 | 16 | const template = Template.fromStack(stack); 17 | template.resourceCountIs('AWS::S3::Bucket', 1); 18 | template.hasResource('AWS::S3::Bucket', { 19 | Type:'AWS::S3::Bucket', 20 | Properties : { 21 | 'AccessControl': 'LogDeliveryWrite', 22 | 'BucketEncryption': { 23 | 'ServerSideEncryptionConfiguration': [ 24 | { 25 | 'ServerSideEncryptionByDefault': { 26 | 'SSEAlgorithm': 'AES256', 27 | }, 28 | }, 29 | ], 30 | }, 31 | 'OwnershipControls': { 32 | 'Rules': [ 33 | { 34 | 'ObjectOwnership': 'ObjectWriter' 35 | }, 36 | ], 37 | }, 38 | 'PublicAccessBlockConfiguration': { 39 | 'BlockPublicAcls': true, 40 | 'BlockPublicPolicy': true, 41 | 'IgnorePublicAcls': true, 42 | 'RestrictPublicBuckets': true, 43 | }, 44 | }, 45 | UpdateReplacePolicy: 'Retain' 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /source/infrastructure/test/console.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Stack } from "aws-cdk-lib"; 5 | import { Capture, Template } from 'aws-cdk-lib/assertions'; 6 | import { Bucket } from "aws-cdk-lib/aws-s3"; 7 | import { ConsoleConstruct } from "../lib/console"; 8 | 9 | describe('When IoT Device Simulator ConsoleConstruct is created', () => { 10 | let template: Template; 11 | 12 | beforeAll(() => { 13 | template = buildStack(); 14 | }); 15 | 16 | const loggingBucketCapture = new Capture(); 17 | const commonResourceDistributionS3BucketCapture = new Capture(); 18 | const cloudFrontDistributionOriginCapture = new Capture(); 19 | 20 | it('it should have s3 buckets', () => { 21 | template.resourceCountIs('AWS::S3::Bucket', 2); 22 | template.hasResource('AWS::S3::Bucket', { 23 | Type:'AWS::S3::Bucket', 24 | "Properties": { 25 | "BucketEncryption": { 26 | "ServerSideEncryptionConfiguration": [ 27 | { 28 | "ServerSideEncryptionByDefault": { 29 | "SSEAlgorithm": "AES256", 30 | }, 31 | }, 32 | ], 33 | }, 34 | "LifecycleConfiguration": { 35 | "Rules": [ 36 | { 37 | "NoncurrentVersionTransitions": [ 38 | { 39 | "StorageClass": "GLACIER", 40 | "TransitionInDays": 90, 41 | }, 42 | ], 43 | "Status": "Enabled", 44 | }, 45 | ], 46 | }, 47 | "LoggingConfiguration": { 48 | "DestinationBucketName": { 49 | "Ref": loggingBucketCapture, 50 | }, 51 | "LogFilePrefix": "console-s3/", 52 | }, 53 | "PublicAccessBlockConfiguration": { 54 | "BlockPublicAcls": true, 55 | "BlockPublicPolicy": true, 56 | "IgnorePublicAcls": true, 57 | "RestrictPublicBuckets": true, 58 | }, 59 | "VersioningConfiguration": { 60 | "Status": "Enabled", 61 | }, 62 | }, 63 | UpdateReplacePolicy: 'Retain' 64 | }); 65 | expect(template.toJSON()['Resources'][loggingBucketCapture.asString()]['Type']).toStrictEqual('AWS::S3::Bucket'); 66 | template.resourceCountIs('AWS::S3::BucketPolicy', 1); 67 | }); 68 | 69 | it('it should have cloudfront distribution', () => { 70 | template.resourceCountIs('AWS::CloudFront::Distribution', 1); 71 | template.hasResourceProperties('AWS::CloudFront::Distribution', { 72 | 'DistributionConfig' : { 73 | 'Origins': [ 74 | { 75 | 'DomainName': { 76 | 'Fn::GetAtt': [ 77 | commonResourceDistributionS3BucketCapture, 78 | 'RegionalDomainName' 79 | ], 80 | }, 81 | 'S3OriginConfig': { 82 | 'OriginAccessIdentity': { 83 | 'Fn::Join': [ 84 | '', 85 | [ 86 | 'origin-access-identity/cloudfront/', 87 | { 88 | 'Ref': cloudFrontDistributionOriginCapture, 89 | }, 90 | ], 91 | ], 92 | }, 93 | }, 94 | }, 95 | ] 96 | } 97 | }); 98 | expect(template.toJSON()['Resources'][commonResourceDistributionS3BucketCapture.asString()]['Type']).toStrictEqual('AWS::S3::Bucket'); 99 | expect(template.toJSON()['Resources'][cloudFrontDistributionOriginCapture.asString()]['Type']).toStrictEqual('AWS::CloudFront::CloudFrontOriginAccessIdentity'); 100 | }); 101 | 102 | it('it should have identity resources', () => { 103 | template.resourceCountIs('AWS::IoT::Policy', 1); 104 | template.resourceCountIs('AWS::Cognito::UserPoolUser', 1); 105 | template.resourceCountIs('AWS::Cognito::IdentityPool', 1); 106 | template.resourceCountIs('AWS::IAM::Role', 1); 107 | }); 108 | 109 | }); 110 | 111 | function buildStack() { 112 | const stack = new Stack(); 113 | const testBucket = new Bucket(stack, "testBucket", {}); 114 | 115 | const console = new ConsoleConstruct(stack, 'TestCommonResource', { 116 | apiId: '12ab34cde5', 117 | s3LogsBucket: testBucket, 118 | adminEmail: "someEmail", 119 | mapArn: "arn:aws:geo:region:accountID:map/ExampleMap", 120 | placeIndexArn: "arn:aws:geo:region:accountID:place-index/ExamplePlaceIndex" 121 | }); 122 | return Template.fromStack(stack); 123 | } 124 | -------------------------------------------------------------------------------- /source/infrastructure/test/simulator.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Stack } from "aws-cdk-lib"; 5 | import { Capture, Match, Template } from 'aws-cdk-lib/assertions'; 6 | import { AttributeType, Table } from "aws-cdk-lib/aws-dynamodb"; 7 | import { Policy, PolicyStatement } from "aws-cdk-lib/aws-iam"; 8 | import { Bucket } from "aws-cdk-lib/aws-s3"; 9 | import { SimulatorConstruct } from "../lib/simulator"; 10 | 11 | describe('When IoT Device Simulator SimulatorConstruct is created', () => { 12 | let template: Template; 13 | 14 | beforeAll(() => { 15 | template = buildStack(); 16 | }); 17 | 18 | const deviceTypeTableCapture = new Capture(); 19 | const testSimTableCapture = new Capture(); 20 | const simulatorStepFunctionsStateMachineCapture = new Capture(); 21 | 22 | it('it should have custom resource', () => { 23 | template.resourceCountIs('AWS::Lambda::Function', 2); 24 | template.hasResource('AWS::Lambda::Function', { 25 | Type:'AWS::Lambda::Function', 26 | Properties: { 27 | Code: Match.anyValue(), 28 | Runtime: 'nodejs18.x', 29 | Handler: 'index.handler', 30 | Timeout: 60, 31 | Description: 'IoT Device Simulator microservices function', 32 | Environment: { 33 | Variables: { 34 | 'DEVICE_TYPES_TBL': { 35 | Ref: deviceTypeTableCapture 36 | }, 37 | 'SEND_ANONYMOUS_METRIC': 'Yes', 38 | 'SIMULATIONS_TBL': { 39 | Ref: testSimTableCapture 40 | }, 41 | "SIM_STEP_FUNCTION": { 42 | Ref: simulatorStepFunctionsStateMachineCapture 43 | }, 44 | 'SOLUTION_ID': 'testId', 45 | 'UUID': 'abc123', 46 | 'VERSION': 'testVersion' 47 | } 48 | }, 49 | Role: {} 50 | }}); 51 | expect(template.toJSON()['Resources'][deviceTypeTableCapture.asString()]['Type']).toStrictEqual('AWS::DynamoDB::Table'); 52 | expect(template.toJSON()['Resources'][testSimTableCapture.asString()]['Type']).toStrictEqual('AWS::DynamoDB::Table'); 53 | expect(template.toJSON()['Resources'][simulatorStepFunctionsStateMachineCapture.asString()]['Type']).toStrictEqual('AWS::StepFunctions::StateMachine'); 54 | }); 55 | 56 | it('it should have dynamoDB Tables', () => { 57 | template.resourceCountIs('AWS::DynamoDB::Table', 2); 58 | template.hasResource('AWS::DynamoDB::Table', { 59 | "Type": "AWS::DynamoDB::Table", 60 | "DeletionPolicy": "Retain", 61 | "Properties": { 62 | "AttributeDefinitions": [ 63 | { 64 | "AttributeName": "typeId", 65 | "AttributeType": "S", 66 | }], 67 | "KeySchema": [ 68 | { 69 | "AttributeName": "typeId", 70 | "KeyType": "HASH", 71 | }], 72 | "ProvisionedThroughput": { 73 | "ReadCapacityUnits": 5, 74 | "WriteCapacityUnits": 5, 75 | }, 76 | }, 77 | "UpdateReplacePolicy": "Retain" 78 | }); 79 | 80 | template.hasResource('AWS::DynamoDB::Table', { 81 | "Type": "AWS::DynamoDB::Table", 82 | "DeletionPolicy": "Retain", 83 | "Properties": { 84 | "AttributeDefinitions": [ 85 | { 86 | "AttributeName": "simId", 87 | "AttributeType": "S", 88 | }, 89 | ], 90 | "KeySchema": [ 91 | { 92 | "AttributeName": "simId", 93 | "KeyType": "HASH", 94 | }, 95 | ], 96 | "ProvisionedThroughput": { 97 | "ReadCapacityUnits": 5, 98 | "WriteCapacityUnits": 5, 99 | }, 100 | }, 101 | "UpdateReplacePolicy": "Retain" 102 | }); 103 | }); 104 | it('it should have IAM policies', () => { 105 | template.resourceCountIs('AWS::IAM::Role', 3); 106 | template.resourceCountIs('AWS::IAM::Policy', 4); 107 | const simulatorEngineLambdaRoleCapture = new Capture(); 108 | const simulatorMicroservicesRole = new Capture(); 109 | template.hasResourceProperties('AWS::IAM::Policy', { 110 | "PolicyDocument": { 111 | "Statement": [ 112 | { 113 | "Action": "cloudwatch:Get*", 114 | "Effect": "Allow", 115 | "Resource": "*", 116 | }, 117 | ], 118 | "Version": "2012-10-17", 119 | }, 120 | "Roles": [ 121 | { 122 | "Ref": simulatorEngineLambdaRoleCapture, 123 | }, 124 | { 125 | "Ref": simulatorMicroservicesRole, 126 | }, 127 | ] 128 | }); 129 | expect(template.toJSON()['Resources'][simulatorEngineLambdaRoleCapture.asString()]['Type']).toStrictEqual('AWS::IAM::Role'); 130 | expect(template.toJSON()['Resources'][simulatorMicroservicesRole.asString()]['Type']).toStrictEqual('AWS::IAM::Role'); 131 | }); 132 | it('it should have CloudWatch resources', () => { 133 | template.resourceCountIs('AWS::CloudWatch::Alarm', 3); 134 | }); 135 | }); 136 | 137 | function buildStack() { 138 | const stack = new Stack(); 139 | const testRouteBucket = new Bucket(stack, 'testRouteBucket'); 140 | const testPolicy = new Policy(stack, 'TestPolicy', { 141 | statements: [ 142 | new PolicyStatement({ 143 | resources: ['*'], 144 | actions: ['cloudwatch:Get*'] 145 | }) 146 | ] 147 | }); 148 | const testDTypeTable = new Table(stack, 'TestDTypeTable', { 149 | partitionKey: { 150 | name: "typeId", type: AttributeType.STRING 151 | } 152 | }); 153 | const testSimTable = new Table(stack, 'TestSimTable', { 154 | partitionKey: { 155 | name: "simId", type: AttributeType.STRING 156 | } 157 | }); 158 | const testSourceBucket = Bucket.fromBucketName(stack, 'SourceCodeBucket', 'test-bucket-region'); 159 | 160 | const simulator = new SimulatorConstruct(stack, 'simulator', { 161 | cloudWatchLogsPolicy: testPolicy, 162 | iotEndpointAddress: 'abcd123efg45h-ats.iot.some-region.amazonaws.com', 163 | simulationTable: testSimTable, 164 | deviceTypeTable: testDTypeTable, 165 | routesBucket: testRouteBucket, 166 | uniqueSuffix: "testSuffix", 167 | solutionConfig: { 168 | sendAnonymousUsage: 'Yes', 169 | solutionId: 'testId', 170 | solutionVersion: 'testVersion', 171 | sourceCodeBucket: testSourceBucket, 172 | sourceCodePrefix: 'testPrefix/', 173 | }, 174 | uuid: 'abc123' 175 | }) 176 | 177 | return Template.fromStack(stack); 178 | } 179 | -------------------------------------------------------------------------------- /source/infrastructure/test/storage.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Stack } from "aws-cdk-lib"; 5 | import { Capture, Template } from 'aws-cdk-lib/assertions'; 6 | import { Bucket } from "aws-cdk-lib/aws-s3"; 7 | import { StorageContruct } from "../lib/storage"; 8 | 9 | 10 | test('IoT Device Simulator storageConstruct Test', () => { 11 | const stack = new Stack(); 12 | const testBucket = new Bucket(stack, "testBucket", {}); 13 | 14 | const storage = new StorageContruct(stack, 'storage', { 15 | solutionId: 'testId', 16 | s3LogsBucket: testBucket 17 | }) 18 | 19 | const loggingBucketCapture = new Capture(); 20 | const template = Template.fromStack(stack); 21 | template.resourceCountIs('AWS::DynamoDB::Table', 2); 22 | template.hasResource('AWS::DynamoDB::Table', { 23 | "Type": "AWS::DynamoDB::Table", 24 | "DeletionPolicy": "Retain", 25 | "Properties": { 26 | "AttributeDefinitions": [ 27 | { 28 | "AttributeName": "typeId", 29 | "AttributeType": "S", 30 | }, 31 | ], 32 | "BillingMode": "PAY_PER_REQUEST", 33 | "KeySchema": [ 34 | { 35 | "AttributeName": "typeId", 36 | "KeyType": "HASH", 37 | }, 38 | ], 39 | "PointInTimeRecoverySpecification": { 40 | "PointInTimeRecoveryEnabled": true, 41 | }, 42 | "SSESpecification": { 43 | "SSEEnabled": true, 44 | }, 45 | }, 46 | "UpdateReplacePolicy": "Retain" 47 | }); 48 | template.hasResource('AWS::DynamoDB::Table', { 49 | "Type": "AWS::DynamoDB::Table", 50 | "DeletionPolicy": "Retain", 51 | "Properties": { 52 | "AttributeDefinitions": [ 53 | { 54 | "AttributeName": "simId", 55 | "AttributeType": "S", 56 | }, 57 | ], 58 | "BillingMode": "PAY_PER_REQUEST", 59 | "KeySchema": [ 60 | { 61 | "AttributeName": "simId", 62 | "KeyType": "HASH", 63 | }, 64 | ], 65 | "PointInTimeRecoverySpecification": { 66 | "PointInTimeRecoveryEnabled": true, 67 | }, 68 | "SSESpecification": { 69 | "SSEEnabled": true, 70 | }, 71 | }, 72 | "UpdateReplacePolicy": "Retain" 73 | }); 74 | template.resourceCountIs('AWS::S3::Bucket', 2); 75 | 76 | template.hasResource('AWS::S3::Bucket', { 77 | Type:'AWS::S3::Bucket', 78 | "Properties": { 79 | "BucketEncryption": { 80 | "ServerSideEncryptionConfiguration": [ 81 | { 82 | "ServerSideEncryptionByDefault": { 83 | "SSEAlgorithm": "AES256", 84 | }, 85 | }, 86 | ], 87 | }, 88 | "LoggingConfiguration": { 89 | "DestinationBucketName": { 90 | "Ref": loggingBucketCapture, 91 | }, 92 | "LogFilePrefix": "routes-bucket-access/", 93 | }, 94 | "PublicAccessBlockConfiguration": { 95 | "BlockPublicAcls": true, 96 | "BlockPublicPolicy": true, 97 | "IgnorePublicAcls": true, 98 | "RestrictPublicBuckets": true, 99 | }, 100 | }, 101 | UpdateReplacePolicy: 'Retain' 102 | }); 103 | expect(template.toJSON()['Resources'][loggingBucketCapture.asString()]['Type']).toStrictEqual('AWS::S3::Bucket'); 104 | template.resourceCountIs('AWS::S3::BucketPolicy', 1); 105 | }); 106 | -------------------------------------------------------------------------------- /source/infrastructure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "lib": ["es2022"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /source/infrastructure/utils/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CfnResource, Resource } from "aws-cdk-lib"; 5 | 6 | /** 7 | * The CFN NAG suppress rule interface 8 | * @interface CfnNagSuppressRule 9 | */ 10 | interface CfnNagSuppressRule { 11 | id: string; 12 | reason: string; 13 | } 14 | 15 | /** 16 | * Adds CFN NAG suppress rules to the CDK resource. 17 | * @param resource The CDK resource 18 | * @param rules The CFN NAG suppress rules 19 | */ 20 | export function addCfnSuppressRules(resource: Resource | CfnResource, rules: CfnNagSuppressRule[]) { 21 | if (resource instanceof Resource) { 22 | resource = resource.node.defaultChild as CfnResource; 23 | } 24 | 25 | resource.addMetadata('cfn_nag', { 26 | rules_to_suppress: rules 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /source/microservices/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | let lib = require('./lib/responseManager.js'); 10 | 11 | exports.handler = async function (event) { 12 | 13 | try { 14 | return await lib.respond(event); 15 | } catch (err) { 16 | return ({ 17 | statusCode: 400, 18 | headers: { 19 | 'Access-Control-Allow-Origin': '*', 20 | 'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token', 21 | 'Access-Control-Allow-Methods': 'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT' 22 | }, 23 | body: err 24 | }); 25 | } 26 | } -------------------------------------------------------------------------------- /source/microservices/lib/deviceTypeManager.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | "use strict"; 9 | const { sendAnonymousMetric } = require("../metrics/index"); 10 | const { DynamoDBDocumentClient, ScanCommand, PutCommand, GetCommand, DeleteCommand } = require("@aws-sdk/lib-dynamodb"), 11 | { DynamoDBClient } = require("@aws-sdk/client-dynamodb"); 12 | const { nanoid } = require("nanoid"); 13 | const { SOLUTION_ID, VERSION } = process.env; 14 | let options = {}; 15 | if (SOLUTION_ID && VERSION && SOLUTION_ID.trim() && VERSION.trim()) { 16 | const solutionUserAgent = `AwsSolution/${SOLUTION_ID}/${VERSION}`; 17 | const capability = `AwsSolution-Capability/${SOLUTION_ID}-C001/${VERSION}`; 18 | options.customUserAgent = [[`${solutionUserAgent}`], [`${capability}`]]; 19 | } 20 | let docClient = DynamoDBDocumentClient.from(new DynamoDBClient(options)); 21 | 22 | /** 23 | * Performs crud actions for a device type, such as, creating, retrieving, updating and deleting device types. 24 | * 25 | * @class DeviceTypeManager 26 | */ 27 | class DeviceTypeManager { 28 | /** 29 | * Get device types for the user. 30 | * 31 | */ 32 | async getDeviceTypes() { 33 | const params = { 34 | TableName: process.env.DEVICE_TYPES_TBL, 35 | }; 36 | try { 37 | let results = await docClient.send(new ScanCommand(params)); 38 | let lastEvalKey = results.LastEvaluatedKey; 39 | while (lastEvalKey && Object.keys(lastEvalKey).length > 0) { 40 | params.ExclusiveStartKey = lastEvalKey; 41 | let newResults = await docClient.send(new ScanCommand(params)); 42 | results.Items.push(...newResults.Items); 43 | lastEvalKey = newResults.LastEvaluatedKey; 44 | } 45 | return results.Items; 46 | } catch (err) { 47 | console.error(err); 48 | console.error(`Error occurred while attempting to retrieve device types.`); 49 | throw err; 50 | } 51 | } 52 | 53 | /** 54 | * Creates a device type for user. 55 | * @param {object} deviceType - device type object 56 | */ 57 | async createDeviceType(deviceType) { 58 | try { 59 | let _id; 60 | if (deviceType.typeId && deviceType.typeId !== "idsAutoDemo") { 61 | _id = deviceType.typeId; 62 | } else { 63 | const suffix = deviceType.typeId || ""; 64 | _id = nanoid(9) + suffix; 65 | } 66 | const date = new Date().toISOString(); 67 | let _deviceType = { 68 | typeId: _id, 69 | name: deviceType.name, 70 | topic: deviceType.topic, 71 | payload: deviceType.payload, 72 | createdAt: date, 73 | updatedAt: date, 74 | }; 75 | let params = { 76 | TableName: process.env.DEVICE_TYPES_TBL, 77 | Item: _deviceType, 78 | }; 79 | await docClient.send(new PutCommand(params)); 80 | if (process.env.SEND_ANONYMOUS_METRIC === "Yes") { 81 | let metricData = { 82 | eventType: "create device type", 83 | }; 84 | metricData.uniquePayloadAttrs = deviceType.payload.reduce((acc, curValue) => { 85 | if (!acc.includes(curValue.type)) acc.push(curValue.type); 86 | return acc; 87 | }, []); 88 | await sendAnonymousMetric(metricData); 89 | } 90 | return _deviceType; 91 | } catch (err) { 92 | console.error(err); 93 | console.error(`Error occurred while attempting to create device type.`); 94 | throw err; 95 | } 96 | } 97 | 98 | /** 99 | * Deletes a device type for user. 100 | * @param {string} deviceTypeId - id of device type to delete 101 | */ 102 | async deleteDeviceType(deviceTypeId) { 103 | try { 104 | let params = { 105 | TableName: process.env.DEVICE_TYPES_TBL, 106 | Key: { 107 | typeId: deviceTypeId, 108 | }, 109 | }; 110 | let deviceType = await docClient.send(new GetCommand(params)); 111 | if (deviceType.Item) { 112 | return docClient.send(new DeleteCommand(params)); 113 | } else { 114 | let error = new Error(); 115 | error.code = 400; 116 | error.error = "MissingDeviceType"; 117 | error.message = `The requested device type ${deviceTypeId} does not exist.`; 118 | throw error; 119 | } 120 | } catch (err) { 121 | console.error(err); 122 | console.error(`Error occurred while attempting to delete device type.`); 123 | throw err; 124 | } 125 | } 126 | } 127 | 128 | module.exports = DeviceTypeManager; 129 | -------------------------------------------------------------------------------- /source/microservices/metrics/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const axios = require('axios'); 5 | 6 | const METRICS_ENDPOINT = 'https://metrics.awssolutionsbuilder.com/generic'; 7 | 8 | /** 9 | * Sends anonymous usage metrics. 10 | * @param data Data to send a anonymous metric 11 | */ 12 | async function sendAnonymousMetric(data) { 13 | try { 14 | const body = { 15 | Solution: process.env.SOLUTION_ID, 16 | Version: process.env.VERSION, 17 | UUID: process.env.UUID, 18 | TimeStamp: new Date().toISOString().replace('T', ' ').replace('Z', ''), 19 | Data: data 20 | }; 21 | 22 | const config = { 23 | headers: { 'Content-Type': 'application/json' } 24 | }; 25 | 26 | await axios.post(METRICS_ENDPOINT, JSON.stringify(body), config); 27 | } catch (error) { 28 | console.error('Error sending an anonymous metric: ', error); 29 | } 30 | } 31 | 32 | module.exports = { sendAnonymousMetric } -------------------------------------------------------------------------------- /source/microservices/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iot-sim-device-service", 3 | "description": "The device microservice for the iot device simulator", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "main": "index.js", 9 | "license": "Apache-2.0", 10 | "version": "3.0.9", 11 | "private": "true", 12 | "dependencies": { 13 | "@aws-sdk/client-dynamodb": "^3.391.0", 14 | "@aws-sdk/client-sfn": "^3.391.0", 15 | "@aws-sdk/lib-dynamodb": "^3.391.0", 16 | "axios": "^1.7.4", 17 | "nanoid": "^3.1.25" 18 | }, 19 | "devDependencies": { 20 | "aws-sdk-client-mock": "^3.0.0", 21 | "aws-sdk-client-mock-jest": "^3.0.0", 22 | "expect": "^29.6.2", 23 | "jest": "^29.6.2" 24 | }, 25 | "scripts": { 26 | "test": "jest lib/*.spec.js --coverage --silent", 27 | "clean": "rm -rf node_modules dist coverage package-lock.json", 28 | "build": "npm run clean && npm install --production", 29 | "package": "npm run build && mkdir dist && zip -q -r9 ./dist/package.zip *" 30 | } 31 | } -------------------------------------------------------------------------------- /source/resources/routes/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [{ 3 | "id": "route-b", 4 | "s3path": "routes/route-b.json" 5 | }, { 6 | "id": "route-c", 7 | "s3path": "routes/route-c.json" 8 | }, { 9 | "id": "route-d", 10 | "s3path": "routes/route-d.json" 11 | }, { 12 | "id": "route-e", 13 | "s3path": "routes/route-e.json" 14 | }, { 15 | "id": "route-f", 16 | "s3path": "routes/route-f.json" 17 | }, { 18 | "id": "route-g", 19 | "s3path": "routes/route-g.json" 20 | }, { 21 | "id": "route-h", 22 | "s3path": "routes/route-h.json" 23 | }, { 24 | "id": "route-i", 25 | "s3path": "routes/route-i.json" 26 | }, { 27 | "id": "route-j", 28 | "s3path": "routes/route-j.json" 29 | }, { 30 | "id": "route-k", 31 | "s3path": "routes/route-k.json" 32 | }, { 33 | "id": "route-l", 34 | "s3path": "routes/route-l.json" 35 | }, { 36 | "id": "route-m", 37 | "s3path": "routes/route-m.json" 38 | }, { 39 | "id": "route-n", 40 | "s3path": "routes/route-n.json" 41 | }, { 42 | "id": "route-o", 43 | "s3path": "routes/route-o.json" 44 | }, { 45 | "id": "route-p", 46 | "s3path": "routes/route-p.json" 47 | }, { 48 | "id": "route-q", 49 | "s3path": "routes/route-q.json" 50 | }] 51 | } -------------------------------------------------------------------------------- /source/resources/routes/route-a.json: -------------------------------------------------------------------------------- 1 | { 2 | "end": ["-74.00001", "40.733004"], 3 | "description": "Short aggressive route", 4 | "route_id": "route-a", 5 | "name": "Route A", 6 | "start": ["-73.9889", "40.733123"], 7 | "profile": "aggressive", 8 | "stages": [{ 9 | "stage": 0, 10 | "start": [-73.988903, 40.733119], 11 | "end": [-73.989868, 40.733528], 12 | "km": 0.10802858534553632 13 | }, { 14 | "stage": 1, 15 | "start": [-73.989868, 40.733528], 16 | "end": [-73.98983, 40.733689], 17 | "km": 0.00649911616782174 18 | }, { 19 | "stage": 2, 20 | "start": [-73.98983, 40.733689], 21 | "end": [-73.989816, 40.733829], 22 | "km": 0.004567539593562537 23 | }, { 24 | "stage": 3, 25 | "start": [-73.989816, 40.733829], 26 | "end": [-73.989817, 40.73386], 27 | "km": 0.0009587533819547928 28 | }, { 29 | "stage": 4, 30 | "start": [-73.989817, 40.73386], 31 | "end": [-73.989819, 40.733958], 32 | "km": 0.003012461868185379 33 | }, { 34 | "stage": 5, 35 | "start": [-73.989819, 40.733958], 36 | "end": [-73.98982, 40.733995], 37 | "km": 0.0011391687665557385 38 | }, { 39 | "stage": 6, 40 | "start": [-73.98982, 40.733995], 41 | "end": [-73.989876, 40.734312], 42 | "km": 0.01154452922517191 43 | }, { 44 | "stage": 7, 45 | "start": [-73.989876, 40.734312], 46 | "end": [-73.9899, 40.734447], 47 | "km": 0.004925432262932563 48 | }, { 49 | "stage": 8, 50 | "start": [-73.9899, 40.734447], 51 | "end": [-73.990028, 40.734502], 52 | "km": 0.014331710971763131 53 | }, { 54 | "stage": 9, 55 | "start": [-73.990028, 40.734502], 56 | "end": [-73.990611, 40.734749], 57 | "km": 0.06526458382795684 58 | }, { 59 | "stage": 10, 60 | "start": [-73.990611, 40.734749], 61 | "end": [-73.990717, 40.734794], 62 | "km": 0.011866341318290641 63 | }, { 64 | "stage": 11, 65 | "start": [-73.990717, 40.734794], 66 | "end": [-73.990831, 40.734842], 67 | "km": 0.012760681880119543 68 | }, { 69 | "stage": 12, 70 | "start": [-73.990831, 40.734842], 71 | "end": [-73.99161, 40.735171], 72 | "km": 0.08720223436878388 73 | }, { 74 | "stage": 13, 75 | "start": [-73.99161, 40.735171], 76 | "end": [-73.991691, 40.735205], 77 | "km": 0.009066257524159026 78 | }, { 79 | "stage": 14, 80 | "start": [-73.991691, 40.735205], 81 | "end": [-73.991782, 40.735243], 82 | "km": 0.010185496725584628 83 | }, { 84 | "stage": 15, 85 | "start": [-73.991782, 40.735243], 86 | "end": [-73.992043, 40.735353], 87 | "km": 0.02921600230109299 88 | }, { 89 | "stage": 16, 90 | "start": [-73.992043, 40.735353], 91 | "end": [-73.993064, 40.735784], 92 | "km": 0.11429115906740918 93 | }, { 94 | "stage": 17, 95 | "start": [-73.993064, 40.735784], 96 | "end": [-73.993607, 40.736013], 97 | "km": 0.06078273161305825 98 | }, { 99 | "stage": 18, 100 | "start": [-73.993607, 40.736013], 101 | "end": [-73.994118, 40.735313], 102 | "km": 0.06073601074219223 103 | }, { 104 | "stage": 19, 105 | "start": [-73.994118, 40.735313], 106 | "end": [-73.994585, 40.734672], 107 | "km": 0.055519951879738565 108 | }, { 109 | "stage": 20, 110 | "start": [-73.994585, 40.734672], 111 | "end": [-73.994768, 40.734421], 112 | "km": 0.0217540557845025 113 | }, { 114 | "stage": 21, 115 | "start": [-73.994768, 40.734421], 116 | "end": [-73.994822, 40.734347], 117 | "km": 0.006418890009075189 118 | }, { 119 | "stage": 22, 120 | "start": [-73.994822, 40.734347], 121 | "end": [-73.99505, 40.734034], 122 | "km": 0.027106339342743598 123 | }, { 124 | "stage": 23, 125 | "start": [-73.99505, 40.734034], 126 | "end": [-73.995489, 40.733437], 127 | "km": 0.05213062962220662 128 | }, { 129 | "stage": 24, 130 | "start": [-73.995489, 40.733437], 131 | "end": [-73.995904, 40.732859], 132 | "km": 0.049428837036788194 133 | }, { 134 | "stage": 25, 135 | "start": [-73.995904, 40.732859], 136 | "end": [-73.996351, 40.732255], 137 | "km": 0.05303859009830634 138 | }, { 139 | "stage": 26, 140 | "start": [-73.996351, 40.732255], 141 | "end": [-73.996639, 40.731856], 142 | "km": 0.03427893340568467 143 | }, { 144 | "stage": 27, 145 | "start": [-73.996639, 40.731856], 146 | "end": [-73.996932, 40.731469], 147 | "km": 0.034671166955862164 148 | }, { 149 | "stage": 28, 150 | "start": [-73.996932, 40.731469], 151 | "end": [-73.996976, 40.731414], 152 | "km": 0.0051752487075804425 153 | }, { 154 | "stage": 29, 155 | "start": [-73.996976, 40.731414], 156 | "end": [-73.997072, 40.731465], 157 | "km": 0.010787907502244535 158 | }, { 159 | "stage": 30, 160 | "start": [-73.997072, 40.731465], 161 | "end": [-73.99856, 40.73219], 162 | "km": 0.16693587186610906 163 | }, { 164 | "stage": 31, 165 | "start": [-73.99856, 40.73219], 166 | "end": [-73.998612, 40.732215], 167 | "km": 0.00583264173404858 168 | }, { 169 | "stage": 32, 170 | "start": [-73.998612, 40.732215], 171 | "end": [-74.000066, 40.732929], 172 | "km": 0.16314400511524402 173 | }, { 174 | "stage": 33, 175 | "start": [-74.000066, 40.732929], 176 | "end": [-74.000011, 40.733004], 177 | "km": 0.006533000175979086 178 | }], 179 | "km": 1.3091348661882451 180 | } -------------------------------------------------------------------------------- /source/simulator/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const Engine = require("./lib/engine"); 5 | const { unmarshall } = require("@aws-sdk/util-dynamodb"); 6 | 7 | exports.handler = async (event, context) => { 8 | "use strict"; 9 | context.callbackWaitsForEmptyEventLoop = false; 10 | let results = []; 11 | let options = event.options ? event.options : {}; 12 | let simulation = event.simulation; 13 | let devices = event.devices ? event.devices : simulation.devices; 14 | delete simulation.devices; 15 | options.context = context; 16 | 17 | //Restart already started simulation 18 | if (options.restart === true) { 19 | options.restart = false; 20 | results = await Promise.all( 21 | devices.map(async (device) => { 22 | try { 23 | const engine = new Engine(options, simulation, device); 24 | return engine.start(); 25 | } catch (err) { 26 | console.error("Error occurred restarting simulation", err); 27 | throw err; 28 | } 29 | }) 30 | ); 31 | } else { 32 | //start engine for each device type 33 | results = await Promise.all( 34 | devices.map(async (device) => { 35 | device.info = unmarshall(device.info); 36 | try { 37 | // create engine instance and start 38 | const engine = new Engine(options, simulation, device); 39 | return engine.start(); 40 | } catch (err) { 41 | console.error("Error occurred while create engine instance and start", err); 42 | throw err; 43 | } 44 | }) 45 | ); 46 | } 47 | //make sure every device for every device type is completed 48 | options.restart = !results.every((result) => result === "complete"); 49 | //delete unncessary information to reduce data passed through step fucntions 50 | delete options.context; 51 | delete options.currentState; 52 | delete options.staticValues; 53 | results = { 54 | options: options, 55 | simulation: simulation, 56 | devices: results, 57 | }; 58 | return results; 59 | }; 60 | -------------------------------------------------------------------------------- /source/simulator/index.spec.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.AWS_REGION = "us-east-1"; 5 | process.env.IOT_ENDPOINT = "endpoint.fake"; 6 | const Engine = require("./lib/engine/index"); 7 | let index = require("./index.js"); 8 | jest.mock("./lib/engine/index"); 9 | 10 | const simulation = { 11 | simId: "123", 12 | name: "abc", 13 | stage: "running", 14 | devices: [{ typeId: "456", name: "xyz", amount: 1 }], 15 | interval: 2, 16 | duration: 5, 17 | }; 18 | 19 | const deviceInfo = { 20 | id: { S: "1234560" }, 21 | typeId: { S: "456" }, 22 | name: { S: "xyz" }, 23 | topic: { S: "topic/test" }, 24 | payload: { 25 | L: [{ S: "aString" }, { S: "string" }, { S: "2" }, { S: "4" }, { BOOL: false }], 26 | }, 27 | }; 28 | 29 | const device = { 30 | amount: 1, 31 | typeId: "456", 32 | info: { ...deviceInfo }, 33 | }; 34 | 35 | const devices = [device]; 36 | const context = {}; 37 | 38 | describe("index", function () { 39 | describe("restart", function () { 40 | it("should restart simulation for devices", async () => { 41 | const event = { 42 | options: { 43 | restart: true, 44 | }, 45 | simulation: simulation, 46 | devices: devices, 47 | }; 48 | await index.handler(event, context); 49 | expect(Engine.prototype.start).toBeCalled(); 50 | }); 51 | 52 | it("should start engine for each device type", async () => { 53 | const event = { 54 | options: { 55 | restart: false, 56 | }, 57 | simulation: simulation, 58 | devices: devices, 59 | }; 60 | await index.handler(event, context); 61 | expect(Engine.prototype.start).toBeCalled(); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/random/generator.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | const faker = require('faker'); 10 | const { nanoid, customAlphabet } = require('nanoid'); 11 | const randomLocation = require('random-location') 12 | const moment = require('moment'); 13 | 14 | /** 15 | * @class Generator - Generates messages for devices 16 | */ 17 | class Generator { 18 | constructor(options) { 19 | this.options = options; 20 | this.currentState = options.currentState || {}; 21 | this.staticValues = options.staticValues || {}; 22 | this.isRunning = true; 23 | this.messages = []; 24 | } 25 | 26 | /** 27 | * generates the complete message to be sent 28 | * @param {object} payload 29 | * @param {string} topic 30 | * @param {string} id 31 | */ 32 | generateMessagePayload(payload, topic, id) { 33 | let _message = { 34 | topic: topic, 35 | payload: {} 36 | }; 37 | _message.payload = this.generatePayload(payload); 38 | _message.payload._id_ = id; 39 | this.messages.push(_message); 40 | } 41 | 42 | //generats the attribute payload for the messages 43 | generatePayload(payload) { 44 | let generatedPayload = {}; 45 | for (let attribute of payload) { 46 | if (attribute.static) { 47 | // check to see if attribute already generated 48 | if (this.staticValues.hasOwnProperty(attribute.name)) { 49 | generatedPayload[attribute.name] = this.staticValues[attribute.name]; 50 | } else { 51 | generatedPayload[attribute.name] = this._processSpecAttribute(attribute); 52 | } 53 | } else { 54 | generatedPayload[attribute.name] = this._processSpecAttribute(attribute); 55 | } 56 | } 57 | 58 | return (generatedPayload); 59 | 60 | } 61 | 62 | //process a specific attribute from payload 63 | _processSpecAttribute(attribute) { 64 | let _value = null; 65 | //Use default property if available 66 | if (attribute.hasOwnProperty('default')) { 67 | if(typeof attribute.default === 'string') { 68 | if (attribute.default.trim() !== '') { 69 | return (attribute.default.trim()); 70 | } 71 | } else if (attribute.type === 'bool') { 72 | return (!!attribute.default); 73 | } else { 74 | return (attribute.default); 75 | } 76 | } 77 | 78 | //Add attribute to state if not already added 79 | if (!this.currentState.hasOwnProperty(attribute.name)) { 80 | this.currentState[attribute.name] = { 81 | cnt: 1 82 | }; 83 | } 84 | //generate value according to type 85 | _value = this.generateValueByType(attribute, _value); 86 | //increment current state for attribute 87 | this.currentState[attribute.name] = { 88 | cnt: this.currentState[attribute.name].cnt + 1 89 | } 90 | return (_value); 91 | } 92 | 93 | generateValueByType(attribute, _value) { 94 | switch (attribute.type) { 95 | case 'id': { 96 | let length = attribute.length || 21; 97 | if (attribute.charSet) { 98 | _value = customAlphabet(attribute.charSet, length)(); 99 | } else { 100 | _value = nanoid(length); 101 | } 102 | if (attribute.static) { 103 | this.staticValues[attribute.name] = _value; 104 | } 105 | break; 106 | } 107 | case 'string': { 108 | let { min, max } = attribute; 109 | let length = faker.datatype.number({ min: min, max: max, precision: 1 }); 110 | _value = faker.datatype.string(length); 111 | if (attribute.static) { 112 | this.staticValues[attribute.name] = _value; 113 | } 114 | break; 115 | } 116 | case 'int': { 117 | let { min, max } = attribute; 118 | _value = faker.datatype.number({ min: min, max: max }); 119 | break; 120 | } 121 | case 'timestamp': { 122 | if (attribute.tsformat === 'unix') { 123 | _value = moment().format('x'); 124 | } else { 125 | _value = moment().utc().format('YYYY-MM-DDTHH:mm:ss'); 126 | } 127 | break; 128 | } 129 | case 'bool': { 130 | _value = faker.datatype.boolean(); 131 | break; 132 | } 133 | case 'float': { 134 | let { min, max, precision } = attribute; 135 | _value = faker.datatype.number({ min: min, max: max, precision: precision }); 136 | break; 137 | } 138 | case 'pickOne': { 139 | _value = faker.random.arrayElement(attribute.arr); 140 | if (attribute.hasOwnProperty('static') && attribute.static) { 141 | this.staticValues[attribute.name] = _value; 142 | } 143 | break; 144 | } 145 | case 'location': { 146 | const _center = { 147 | latitude: attribute.lat, 148 | longitude: attribute.long 149 | }; 150 | _value = randomLocation.randomCirclePoint(_center, attribute.radius); 151 | break; 152 | } 153 | case 'sinusoidal': { 154 | _value = this.sin(attribute.min, attribute.max, this.currentState[attribute.name].cnt); 155 | break; 156 | } 157 | case 'decay': { 158 | _value = this.decay(attribute.min, attribute.max, this.currentState[attribute.name].cnt); 159 | break; 160 | } 161 | case 'object': { 162 | _value = this.generatePayload(attribute.payload); 163 | break; 164 | } 165 | default: { 166 | _value = ''; 167 | break; 168 | } 169 | } 170 | return _value; 171 | } 172 | 173 | /** 174 | * Clear all messages waiting to be sent 175 | */ 176 | clearMessages() { 177 | this.messages = []; 178 | } 179 | 180 | /** 181 | * stop the generator 182 | */ 183 | stop() { 184 | this.isRunning = false; 185 | } 186 | 187 | /** 188 | * Calculates sin 189 | * @param {number} min 190 | * @param {number} max 191 | * @param {number} step 192 | * @returns calculated sin 193 | */ 194 | sin(min, max, step) { 195 | let _sin = Math.sin(2 * Math.PI * step / 100 * 5) * Math.random(); 196 | return this._median([min, max]) + this._median([0, max - min]) * _sin; 197 | } 198 | 199 | /** 200 | * Calculates decay 201 | * @param {number} min 202 | * @param {number} max 203 | * @param {number} step 204 | * @returns calculated decay 205 | */ 206 | decay(min, max, step) { 207 | return max - ((max - min) * (1 - Math.exp(-(0.05 * step)))); 208 | } 209 | 210 | /** 211 | * Calculates the median 212 | * @param {_median} values 213 | * @returns the calculated median 214 | */ 215 | _median(values) { 216 | if (values.length === 0) return 0; 217 | values.sort(function (a, b) { 218 | return (a - b); 219 | }); 220 | let half = Math.floor(values.length / 2); 221 | if (values.length % 2) 222 | return (values[half]); 223 | else 224 | return ((values[half - 1] + values[half]) / 2.0); 225 | } 226 | 227 | } 228 | 229 | module.exports = Generator; -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/acceleration-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | const moment = require('moment'); 12 | 13 | class AccelerationCalc extends DataCalc { 14 | 15 | constructor() { 16 | super(); 17 | this.data = 0; 18 | this.lastCalc = moment(); 19 | this.startSpeed = 0.0; 20 | this.name = 'acceleration'; 21 | } 22 | 23 | iterate(snapshot) { 24 | 25 | let accelPeriod = 1000; // one second 26 | let _curSpeed = snapshot.vehicleSpeed; 27 | 28 | let _currentTime = moment(); 29 | let _timeDelta = _currentTime.diff(this.lastCalc); 30 | 31 | if (_timeDelta >= accelPeriod) { 32 | let _accel = (_curSpeed - this.startSpeed) / 1; // speed difference / 1 sec = km/h/s 33 | this.startSpeed = _curSpeed; 34 | this.data = _accel; 35 | this.lastCalc = moment(); 36 | } 37 | 38 | } 39 | 40 | } 41 | 42 | module.exports = AccelerationCalc; 43 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/data-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | class DataCalc { 11 | 12 | constructor() { 13 | this.data = 0; 14 | this.name = 'data'; 15 | } 16 | 17 | get() { 18 | return this.data; 19 | } 20 | 21 | put(newValue) { 22 | this.data = newValue; 23 | } 24 | 25 | /** 26 | * abstract function to be implemented in classes that extend data-calc 27 | * @param {*} data 28 | */ 29 | iterate(data) { 30 | //This is intentional abstract function to be implemented in classes that extend data-calc 31 | } 32 | 33 | } 34 | 35 | module.exports = DataCalc; 36 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/dynamics-model.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | "use strict"; 9 | 10 | const AccelerationCalc = require("./acceleration-calc.js"); 11 | const SpeedCalc = require("./speed-calc.js"); 12 | const GearCalc = require("./gear-calc.js"); 13 | const GearIntCalc = require("./gear-int-calc.js"); 14 | const TorqueCalc = require("./torque-calc.js"); 15 | const EngineSpeedCalc = require("./engine-speed-calc.js"); 16 | const FuelConsumedCalc = require("./fuel-consumed-calc.js"); 17 | const OdometerCalc = require("./odometer-calc.js"); 18 | const FuelLevelCalc = require("./fuel-level-calc.js"); 19 | const OilTempCalc = require("./oil-temp-calc.js"); 20 | const RouteCalc = require("./route-calc.js"); 21 | const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3"); 22 | const { customAlphabet } = require("nanoid"); 23 | const moment = require("moment"); 24 | const { SOLUTION_ID, VERSION } = process.env; 25 | let options = {}; 26 | if (SOLUTION_ID && VERSION && SOLUTION_ID.trim() && VERSION.trim()) { 27 | const solutionUserAgent = `AwsSolution/${SOLUTION_ID}/${VERSION}`; 28 | const capability = `AwsSolution-Capability/${SOLUTION_ID}-C004/${VERSION}`; 29 | options.customUserAgent = [[`${solutionUserAgent}`], [`${capability}`]]; 30 | } 31 | const s3 = new S3Client(options); 32 | 33 | /** 34 | * Simulator for vehicle data 35 | * @class Dynamics Model 36 | */ 37 | class DynamicsModel { 38 | constructor(params) { 39 | this._initializeData(params); 40 | } 41 | 42 | /** 43 | * Initializes data for class 44 | * @param {object} params 45 | */ 46 | async _initializeData(params) { 47 | this.pollerDelay = 500; 48 | this.snapshot = params.snapshot; 49 | this.routeParams = await this.getRoute(this.snapshot); 50 | this.calculations = []; 51 | this.calculations.push(new SpeedCalc(this.snapshot)); 52 | this.calculations.push(new AccelerationCalc(this.snapshot)); 53 | this.calculations.push(new GearCalc(this.snapshot)); 54 | this.calculations.push(new GearIntCalc(this.snapshot)); 55 | this.calculations.push(new TorqueCalc(this.snapshot)); 56 | this.calculations.push(new EngineSpeedCalc(this.snapshot)); 57 | this.calculations.push(new FuelConsumedCalc(this.snapshot)); 58 | this.calculations.push(new OdometerCalc(this.snapshot)); 59 | this.calculations.push(new FuelLevelCalc(this.snapshot)); 60 | this.calculations.push(new OilTempCalc(this.snapshot)); 61 | this.calculations.push(new RouteCalc(this.routeParams, this.snapshot)); 62 | //add initial calulationdata to snapshot 63 | for (let calculation of this.calculations) { 64 | //Add back data from previous lambda if available 65 | if (this.snapshot[calculation.name]) { 66 | calculation.put(this.snapshot[calculation.name]); 67 | } else { 68 | this.snapshot[calculation.name] = calculation.get(); 69 | } 70 | } 71 | this.accelerator = this.snapshot.acceleratorPedalPosition || 0.0; 72 | this.brake = this.snapshot.brake || 0.0; 73 | this.steeringWheelAngle = this.snapshot.steeringWheelAngle || 0.0; 74 | this.parkingBrakeStatus = !!this.snapshot.parkingBrakeStatus; 75 | this.engineRunning = this.snapshot.engineRunning || true; 76 | this.ignitionData = this.snapshot.ignitionStatus || "run"; 77 | this.gearLever = this.snapshot.gearLeverPosition || "drive"; 78 | this.manualTransStatus = !!this.snapshot.manualTrans; 79 | this.triggers = {}; 80 | 81 | this.snapshot.acceleratorPedalPosition = this.accelerator; 82 | this.snapshot.brake = this.brake; 83 | this.snapshot.steeringWheelAngle = this.steeringWheelAngle; 84 | this.snapshot.parkingBrakeStatus = this.parkingBrakeStatus; 85 | this.snapshot.engineRunning = this.engineRunning; 86 | this.snapshot.ignitionStatus = this.ignitionData; 87 | this.snapshot.brakePedalStatus = this.brakePedalStatus; 88 | this.snapshot.gearLeverPosition = this.gearLever; 89 | this.snapshot.manualTrans = this.manualTransStatus; 90 | this.snapshot.triggers = this.triggers; 91 | //start calculating vehicle data 92 | this.startPhysicsLoop(); 93 | } 94 | 95 | /** 96 | * Get random route to run from S3 or get previous route 97 | * from last lambda iteration 98 | * @param {object} snapshot 99 | * @returns relevant route information 100 | */ 101 | async getRoute(snapshot) { 102 | //Select random route 103 | let route = customAlphabet("abcdefghijklmnopq", 1)(); 104 | let routeName = snapshot.routeInfo?.routeName || `route-${route}.json`; 105 | let params = { 106 | Bucket: process.env.ROUTE_BUCKET, 107 | Key: routeName, 108 | }; 109 | 110 | try { 111 | let data = await s3.send(new GetObjectCommand(params)); 112 | const readableStream = Buffer.concat(await data.Body.toArray()); 113 | return { 114 | routeName: routeName, 115 | odometer: snapshot.odometer || 0, 116 | routeStage: snapshot.routeInfo?.routeStage || 0, 117 | burndown: !!snapshot.routeInfo?.burndown, 118 | burndownCalc: snapshot.routeInfo?.burndownCalc || moment().toISOString(), 119 | routeEnded: !!snapshot.routeEnded, 120 | route: JSON.parse(readableStream), 121 | randomTriggers: snapshot.routeInfo?.randomTriggers, 122 | }; 123 | } catch (err) { 124 | console.error("Error while getting route", err); 125 | throw err; 126 | } 127 | } 128 | 129 | /** 130 | * Start the phsyics loop 131 | * Calculates simulated vehicle data every 0.5ms 132 | */ 133 | startPhysicsLoop() { 134 | let _this = this; 135 | this.pollerInterval = setInterval(function () { 136 | _this._getSnapshotData(); 137 | }, _this.pollerDelay); 138 | } 139 | 140 | /** 141 | * stops the timer for calculating simulated vechile data 142 | */ 143 | stopPhysicsLoop() { 144 | clearInterval(this.pollerInterval); 145 | } 146 | 147 | /** 148 | * Updates the snapshot with the newest data 149 | */ 150 | _getSnapshotData() { 151 | let newSnapshot = {}; 152 | //For each calculation, calculate new data, then update snapshot 153 | for (let calculation of this.calculations) { 154 | //calculate new data 155 | calculation.iterate(this.snapshot); 156 | newSnapshot[calculation.name] = calculation.get(); 157 | //aggregate necessary route info into single object 158 | if (calculation.name === "routeInfo") { 159 | newSnapshot.latitude = calculation.latitude; 160 | newSnapshot.longitude = calculation.longitude; 161 | newSnapshot.acceleratorPedalPosition = calculation.throttlePosition; 162 | newSnapshot.brake = calculation.brakePosition; 163 | this.brakePedalStatus = calculation.brakePosition > 0; 164 | //check if route has ended 165 | if (calculation.routeEnded) { 166 | newSnapshot.routeDuration = calculation.routeDuration; 167 | newSnapshot.routeEnded = true; 168 | } 169 | //check if random triggers were triggered 170 | if (calculation.updateTriggers) { 171 | this.triggers = calculation.triggers; 172 | } 173 | } 174 | } 175 | newSnapshot.steeringWheelAngle = this.steeringWheelAngle; 176 | newSnapshot.parkingBrakeStatus = this.parkingBrakeStatus; 177 | newSnapshot.engineRunning = this.engineRunning; 178 | newSnapshot.ignitionStatus = this.ignitionData; 179 | newSnapshot.brakePedalStatus = this.brakePedalStatus; 180 | newSnapshot.gearLeverPosition = this.gearLever; 181 | newSnapshot.manualTrans = this.manualTransStatus; 182 | newSnapshot.triggers = this.triggers; 183 | //if route ended, clear timer for calculating vehicle data 184 | if (newSnapshot.routeEnded) { 185 | this.stopPhysicsLoop(); 186 | } 187 | //update the snapshot 188 | this.snapshot = newSnapshot; 189 | } 190 | 191 | get engineSpeed() { 192 | return this.snapshot.engineSpeed; 193 | } 194 | 195 | get vehicleSpeed() { 196 | return this.snapshot.vehicleSpeed; 197 | } 198 | } 199 | 200 | module.exports = DynamicsModel; 201 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/engine-speed-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | 12 | class EngineSpeedCalc extends DataCalc { 13 | 14 | constructor() { 15 | super(); 16 | this.data = 0; 17 | this.name = 'engineSpeed'; 18 | } 19 | 20 | iterate(snapshot) { 21 | let vehicleSpeed = snapshot['vehicleSpeed']; 22 | let gear = snapshot['transmissionGearInt']; 23 | this.data = 16382 * vehicleSpeed / (100.0 * gear); 24 | } 25 | 26 | } 27 | 28 | module.exports = EngineSpeedCalc; 29 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/fuel-consumed-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | const moment = require('moment'); 12 | 13 | class FuelConsumedCalc extends DataCalc { 14 | 15 | constructor() { 16 | super(); 17 | this.data = 0.0; 18 | this.lastCalc = moment(); 19 | this.maxFuel = 0.0015; // #In liters per second at full throttle. 20 | this.idleFuel = 0.000015; 21 | this.name = 'fuelConsumedSinceRestart'; 22 | } 23 | 24 | iterate(snapshot) { 25 | let acceleratorPercent = snapshot['acceleratorPedalPosition']; 26 | let ignitionStatus = snapshot['engineRunning']; 27 | 28 | let currentTime = moment(); 29 | let timeDelta = moment.duration(currentTime.diff(this.lastCalc)); 30 | let timeStep = timeDelta.get('seconds') + (timeDelta.get('milliseconds').toFixed(4) / 1000); 31 | this.lastCalc = moment(); 32 | 33 | if (ignitionStatus) { 34 | this.data = this.data + this.idleFuel + (this.maxFuel * (acceleratorPercent / 100) * timeStep); 35 | } 36 | } 37 | 38 | } 39 | 40 | module.exports = FuelConsumedCalc; 41 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/fuel-level-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | 12 | class FuelLevelCalc extends DataCalc { 13 | 14 | constructor() { 15 | super(); 16 | this.data = 0.0; 17 | this.tankSize = 40.0; //liters 18 | this.name = 'fuelLevel'; 19 | } 20 | 21 | iterate(snapshot) { 22 | let fuelConsumed = snapshot['fuelConsumedSinceRestart'] 23 | 24 | this.data = 100.0 * (this.tankSize - fuelConsumed) / this.tankSize; 25 | } 26 | 27 | } 28 | 29 | module.exports = FuelLevelCalc; 30 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/gear-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | 12 | class GearCalc extends DataCalc { 13 | 14 | constructor() { 15 | super(); 16 | this.gears = ['neutral', 'first', 'second', 'third', 'fourth', 'fifth', 'sixth']; 17 | this.data = this.gears[0]; 18 | this.name = 'transmissionGearPosition'; 19 | } 20 | 21 | iterate(snapshot) { 22 | let gear = snapshot['transmissionGearInt']; 23 | this.data = this.gears[gear]; 24 | } 25 | 26 | } 27 | 28 | module.exports = GearCalc; 29 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/gear-int-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | 12 | class GearIntCalc extends DataCalc { 13 | 14 | constructor() { 15 | super(); 16 | this.speeds = [ 17 | [0, 0], 18 | [0, 25], 19 | [20, 50], 20 | [45, 75], 21 | [70, 100], 22 | [95, 125], 23 | [120, 500] 24 | ]; 25 | this.data = 1; 26 | this.name = 'transmissionGearInt'; 27 | } 28 | 29 | shiftUp() { 30 | this.data = this.data + 1; 31 | if (this.data > 6) { 32 | this.data = 6; 33 | } 34 | } 35 | 36 | shiftDown() { 37 | this.data = this.data - 1; 38 | if (this.data < 1) { 39 | this.data = 1; 40 | } 41 | } 42 | 43 | iterate(snapshot) { 44 | let manual = snapshot['manualTrans']; 45 | let vehicleSpeed = snapshot['vehicleSpeed']; 46 | 47 | if (!manual) { 48 | if (vehicleSpeed < this.speeds[this.data][0]) { 49 | this.data = this.data - 1; 50 | } else if (vehicleSpeed > this.speeds[this.data][1]) { 51 | this.data = this.data + 1; 52 | } 53 | } 54 | } 55 | 56 | } 57 | 58 | module.exports = GearIntCalc; 59 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/heading-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | const moment = require('moment'); 12 | 13 | class HeadingCalc extends DataCalc { 14 | 15 | constructor() { 16 | super(); 17 | this.data = 0.0; 18 | this.lastCalc = moment(); 19 | this.name = 'heading'; 20 | } 21 | 22 | iterate(snapshot) { 23 | let vehicleSpeed = snapshot.vehicleSpeed; 24 | let steeringWheelAngle = snapshot.steeringWheelAngle; 25 | 26 | // 600 degree steering == 45 degree wheels. 27 | let wheelAngle = steeringWheelAngle / 13.33; 28 | let wheelAngleRad = wheelAngle * Math.PI / 180; 29 | let calcAngle = -wheelAngleRad; 30 | if (wheelAngle < 0) { 31 | calcAngle = calcAngle - (Math.PI / 2); 32 | } else { 33 | calcAngle = calcAngle + (Math.PI / 2); 34 | } 35 | 36 | // should return number between 28 m and infinity 37 | let turningCircumferenceKm = 0.028 * Math.tan(calcAngle); 38 | 39 | let currentTime = moment(); 40 | let timeDelta = moment.duration(currentTime.diff(this.lastCalc)); 41 | let timeStep = timeDelta.get('seconds') + (timeDelta.get('milliseconds').toFixed(4) / 1000); 42 | this.lastCalc = moment(); 43 | 44 | let distance = timeStep * (vehicleSpeed / 3600); // Time * km / s. 45 | 46 | let deltaHeading = (distance / turningCircumferenceKm) * 2 * Math.PI; 47 | let tempHeading = this.data + deltaHeading; 48 | while (tempHeading >= (2 * Math.PI)) { 49 | tempHeading = tempHeading - (2 * Math.PI); 50 | } 51 | 52 | while (tempHeading < 0) { 53 | tempHeading = tempHeading + (2 * Math.PI); 54 | } 55 | 56 | this.data = tempHeading; 57 | } 58 | 59 | } 60 | 61 | module.exports = HeadingCalc; 62 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/lat-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | const moment = require('moment'); 12 | 13 | class LatCalc extends DataCalc { 14 | 15 | constructor() { 16 | super(); 17 | this.data = 42.292834; 18 | this.lastCalc = moment(); 19 | this.earthCircumferenceKm = 40075.0; 20 | this.kmPerDeg = this.earthCircumferenceKm / 360.0; 21 | this.name = 'latitude'; 22 | } 23 | 24 | iterate(snapshot) { 25 | let vehicleSpeed = snapshot.vehicleSpeed; 26 | let heading = snapshot.heading; 27 | 28 | let currentTime = moment(); 29 | let timeDelta = moment.duration(currentTime.diff(this.lastCalc)); 30 | let timeStep = timeDelta.get('seconds') + (timeDelta.get('milliseconds').toFixed(4) / 1000); 31 | this.lastCalc = moment(); 32 | 33 | let distance = timeStep * (vehicleSpeed / 3600); // Time * km / s. 34 | 35 | let NSDist = distance * Math.cos(heading); 36 | let deltaLat = NSDist / this.kmPerDeg; 37 | 38 | this.data = this.data + deltaLat; 39 | 40 | } 41 | 42 | } 43 | 44 | module.exports = LatCalc; 45 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/lon-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | const moment = require('moment'); 12 | 13 | class LonCalc extends DataCalc { 14 | 15 | constructor() { 16 | super(); 17 | this.data = 42.292834; 18 | this.lastCalc = moment(); 19 | this.earthCircumferenceEquatorKm = 40075.0; 20 | this.kmPerDegEquator = this.earthCircumferenceEquatorKm / 360.0; 21 | this.name = 'longitude'; 22 | } 23 | 24 | iterate(snapshot) { 25 | let vehicleSpeed = snapshot.vehicleSpeed; 26 | let heading = snapshot.heading; 27 | let lat = snapshot.latitude; 28 | 29 | let currentTime = moment(); 30 | let timeDelta = moment.duration(currentTime.diff(this.lastCalc)); 31 | let timeStep = timeDelta.get('seconds') + (timeDelta.get('milliseconds').toFixed(4) / 1000); 32 | this.lastCalc = moment(); 33 | 34 | let distance = timeStep * (vehicleSpeed / 3600); // Time * km / s. 35 | let EWDist = distance * Math.sin(heading); 36 | let latRad = lat * Math.PI / 180; 37 | let kmPerDeg = Math.abs(this.kmPerDegEquator * Math.sin(latRad)); 38 | 39 | let deltaLon = EWDist / kmPerDeg; 40 | let newLon = this.data + deltaLon; 41 | while (newLon >= 180.0) { 42 | newLon = newLon - 360; 43 | } 44 | 45 | while (newLon < -180) { 46 | newLon = newLon + 360; 47 | } 48 | 49 | this.data = newLon; 50 | 51 | } 52 | 53 | } 54 | 55 | module.exports = LonCalc; 56 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/odometer-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | const moment = require('moment'); 12 | 13 | class OdometerCalc extends DataCalc { 14 | // ..and an (optional) custom class constructor. If one is 15 | // not supplied, a default constructor is used instead: 16 | // constructor() { } 17 | constructor() { 18 | super(); 19 | this.data = 0.0; 20 | this.lastCalc = moment(); 21 | this.KPHToKPS = 60 * 60; 22 | this.name = 'odometer'; 23 | } 24 | 25 | iterate(snapshot) { 26 | let vehicleSpeed = snapshot['vehicleSpeed'] // Any necessary data should be passed in 27 | 28 | let currentTime = moment(); 29 | let timeDelta = moment.duration(currentTime.diff(this.lastCalc)); 30 | let timeStep = timeDelta.get('seconds') + (timeDelta.get('milliseconds').toFixed(4) / 1000); 31 | this.lastCalc = moment(); 32 | 33 | this.data = this.data + (vehicleSpeed * timeStep / this.KPHToKPS); 34 | } 35 | 36 | } 37 | 38 | module.exports = OdometerCalc; 39 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/oil-temp-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | const moment = require('moment'); 12 | 13 | class OilTempCalc extends DataCalc { 14 | 15 | constructor() { 16 | super(); 17 | this.data = 0; 18 | this.lastCalc = moment(); 19 | this.totalTime = 0; 20 | this.operatingZone = 0.0; 21 | this.triggerTripped = false; 22 | this.name = 'oilTemp'; 23 | } 24 | 25 | iterate(snapshot) { 26 | 27 | let TEMP_COEFFICIENT = 2.0417; 28 | let snapshotTriggers = snapshot.triggers; 29 | 30 | let currentTime = moment(); 31 | let timeDelta = moment.duration(currentTime.diff(this.lastCalc)); 32 | let timeStep = timeDelta.get('seconds') + (timeDelta.get('milliseconds').toFixed(4) / 1000); 33 | this.lastCalc = moment(); 34 | this.totalTime = this.totalTime + timeStep; 35 | 36 | if (this.totalTime <= 120 && !snapshotTriggers.highOilTemp) { 37 | let _oiltemp = timeStep * TEMP_COEFFICIENT; // Time * degree/sec. 38 | this.data = this.data + _oiltemp; 39 | this.operatingZone = this.data; 40 | } else { 41 | // normal oil temp jitter 42 | let _upper = this.operatingZone + 5; 43 | let _lower = this.operatingZone - 5; 44 | this.data = (Math.random() * (_upper - _lower) + _lower); 45 | } 46 | 47 | if (snapshotTriggers) { 48 | if (snapshotTriggers.highOilTemp && !this.triggerTripped) { 49 | this.operatingZone = this._getHighTemp(); 50 | this.triggerTripped = true; 51 | } 52 | } 53 | } 54 | 55 | _getHighTemp() { 56 | return Math.floor(Math.random() * (320 - 275)) + 275; 57 | } 58 | 59 | } 60 | 61 | module.exports = OilTempCalc; 62 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/speed-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | const moment = require('moment'); 12 | 13 | class SpeedCalc extends DataCalc { 14 | 15 | constructor() { 16 | super(); 17 | this.data = 0.0; 18 | this.lastCalc = moment(); 19 | this.name = 'vehicleSpeed'; 20 | } 21 | 22 | iterate(snapshot) { 23 | let acceleratorPercent = snapshot.acceleratorPedalPosition; 24 | let brake = snapshot.brake; 25 | let parkingBrakeStatus = snapshot.parkingBrakeStatus; 26 | let ignitionStatus = snapshot.engineRunning; 27 | let engineSpeed = snapshot.engineSpeed; 28 | let gear = snapshot.transmissionGearInt; 29 | 30 | // Any necessary data should be passed in 31 | let AIR_DRAG_COEFFICIENT = .000008; 32 | let ENGINE_DRAG_COEFFICIENT = 0.0004; 33 | let BRAKE_CONSTANT = 0.1; 34 | let ENGINE_V0_FORCE = 30; //units are cars * km / h / s 35 | let speed = this.data; //Just to avoid confution 36 | 37 | let airDrag = speed * speed * speed * AIR_DRAG_COEFFICIENT; 38 | 39 | let engineDrag = engineSpeed * ENGINE_DRAG_COEFFICIENT; 40 | 41 | let engineForce = 0.0; 42 | if (ignitionStatus) { 43 | //acceleratorPercent is 0.0 to 100.0, not 0 44 | engineForce = (ENGINE_V0_FORCE * acceleratorPercent / (50 * gear)); 45 | } 46 | 47 | let acceleration = engineForce - airDrag - engineDrag - .1 - (brake * BRAKE_CONSTANT); 48 | 49 | if (parkingBrakeStatus) { 50 | acceleration = acceleration - (BRAKE_CONSTANT * 100); 51 | } 52 | 53 | let currentTime = moment(); 54 | let timeDelta = moment.duration(currentTime.diff(this.lastCalc)); 55 | let timeStep = timeDelta.get('seconds') + (timeDelta.get('milliseconds').toFixed(4) / 1000); 56 | this.lastCalc = moment(); 57 | 58 | let impulse = acceleration * timeStep; 59 | if ((impulse + speed) < 0.0) { 60 | impulse = -speed; 61 | } 62 | 63 | this.data = speed + impulse; 64 | } 65 | } 66 | 67 | module.exports = SpeedCalc; 68 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/dynamics/torque-calc.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const DataCalc = require('./data-calc.js'); 11 | 12 | class TorqueCalc extends DataCalc { 13 | 14 | constructor() { 15 | super(); 16 | this.data = 0.0; 17 | this.engineToTorque = 500.0 / 16382.0; 18 | this.name = 'torqueAtTransmission'; 19 | this.gearNumbers = { 20 | 'neutral': 0, 21 | 'first': 1, 22 | 'second': 2, 23 | 'third': 3, 24 | 'fourth': 4, 25 | 'fifth': 5, 26 | 'sixth': 6 27 | }; 28 | } 29 | 30 | iterate(snapshot) { 31 | let accelerator = snapshot['acceleratorPedalPosition']; 32 | let engineSpeed = snapshot['engineSpeed']; 33 | let engineRunning = snapshot['engineRunning']; 34 | let gearNumber = this.gearNumbers[snapshot['transmissionGearPosition']]; 35 | gearNumber = gearNumber - 1; //First gear is the basline. 36 | 37 | if (gearNumber < 1) { 38 | gearNumber = 1; 39 | } 40 | 41 | // Giving sixth gear half the torque of first. 42 | let gearRatio = 1 - (gearNumber * .1); 43 | 44 | let drag = engineSpeed * this.engineToTorque; 45 | let power = accelerator * 15 * gearRatio; 46 | 47 | if (engineRunning) { 48 | this.data = power - drag; 49 | } else { 50 | this.data = -drag; 51 | } 52 | } 53 | 54 | } 55 | 56 | module.exports = TorqueCalc; 57 | -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/generator.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | 'use strict'; 5 | 6 | const DynamicsModel = require('./dynamics/dynamics-model.js'); 7 | const moment = require('moment'); 8 | const { nanoid, customAlphabet } = require('nanoid'); 9 | 10 | /** 11 | * @class Generator - Generates messages for devices 12 | */ 13 | class Generator { 14 | 15 | constructor(options) { 16 | this.options = options; 17 | this.VIN = options.staticValues?.VIN || this._createVIN(); 18 | this.tripId = options.staticValues?.tripId || nanoid(); 19 | let dynamicsModelParams = {} 20 | dynamicsModelParams.snapshot = options.currentState || {}; 21 | this.dynamicsModel = new DynamicsModel(dynamicsModelParams); 22 | this.isRunning = true; 23 | this.messages = []; 24 | this.lastCalc = moment(); 25 | this.currentState = options.currentState || {}; 26 | this.staticValues = options.staticValues || { VIN: this.VIN, tripId: this.tripId }; 27 | } 28 | 29 | /** 30 | * Stop the generator and the vehicle dynamics model 31 | */ 32 | stop() { 33 | let _self = this; 34 | _self.dynamicsModel.stopPhysicsLoop(); 35 | _self.currentState = _self.dynamicsModel.snapshot; 36 | _self.dynamicsModel.engineRunning = false; 37 | _self.dynamicsModel.ignitionData = 'off'; 38 | _self.isRunning = false; 39 | } 40 | 41 | /** 42 | * Generates the message to be sent 43 | * @param {object} payload 44 | * @param {string} topic 45 | * @param {string} id 46 | */ 47 | generateMessagePayload(payload, topic, id) { 48 | 49 | if (this.dynamicsModel.ignitionData === 'run') { 50 | let snapshot = this.dynamicsModel.snapshot; 51 | 52 | let _message = { 53 | topic: topic, 54 | payload: { 55 | timestamp: moment.utc().format('YYYY-MM-DD HH:mm:ss.SSSSSSSSS'), 56 | trip_id: this.tripId, 57 | VIN: this.VIN 58 | } 59 | } 60 | 61 | for (let attribute of payload) { 62 | if (snapshot.hasOwnProperty(attribute.name) || attribute.name === 'location') { 63 | 64 | let value; 65 | if (attribute.name === "location") { 66 | value = { 67 | latitude: snapshot.latitude, 68 | longitude: snapshot.longitude 69 | } 70 | } else { 71 | value = snapshot[attribute.name]; 72 | } 73 | value = this.calculateValue(attribute, value); 74 | 75 | _message.payload[attribute.name] = value; 76 | } 77 | } 78 | 79 | // save last processed snapshot 80 | this.currentState = snapshot; 81 | //add device id for filtering in UI 82 | _message.payload._id_ = id; 83 | 84 | this.messages.push(_message); 85 | } 86 | } 87 | 88 | calculateValue(attribute, value) { 89 | if (attribute.precision) { 90 | let rounding = Math.round(Math.log10(1 / attribute.precision)); 91 | value = Number(Number(value).toFixed(rounding)); 92 | } 93 | return value; 94 | } 95 | 96 | /** 97 | * clears all the messages waiting to be sent 98 | */ 99 | clearMessages() { 100 | this.messages = []; 101 | } 102 | 103 | /** 104 | * Creates a random VIN number 105 | * @returns a random VIN number 106 | */ 107 | _createVIN() { 108 | return (customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 17)()); 109 | } 110 | 111 | 112 | } 113 | 114 | module.exports = Generator; -------------------------------------------------------------------------------- /source/simulator/lib/device/generators/vehicle/generator.spec.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | 'use strict'; 5 | let Generator = require('./generator.js'); 6 | let DynamicsModel = require('./dynamics/dynamics-model.js'); 7 | let { nanoid, customAlphabet } = require('nanoid'); 8 | let options = {}; 9 | 10 | jest.mock('nanoid'); 11 | jest.mock('./dynamics/dynamics-model.js'); 12 | describe('Generator', function() { 13 | describe('constructor()', function() { 14 | it('should use default values when none are provided via options', async() => { 15 | DynamicsModel.mockImplementationOnce(() => ({})); 16 | nanoid.mockImplementationOnce(() => "xyz"); 17 | jest.spyOn(Generator.prototype, "_createVIN").mockImplementationOnce(() => "abc"); 18 | const generator = new Generator(options); 19 | 20 | expect(generator.options).toEqual(options); 21 | expect(generator.currentState).toEqual({}); 22 | expect(generator.staticValues).toEqual({ 23 | VIN: "abc", 24 | tripId: "xyz" 25 | }); 26 | expect(generator.isRunning).toEqual(true); 27 | expect(generator.messages).toEqual([]); 28 | }) 29 | it('should use provided values when provided via options', async() => { 30 | options.currentState = {state: "current"}; 31 | options.staticValues = {static: "values"}; 32 | options.staticValues.VIN = "a1b2"; 33 | options.staticValues.tripId = "tripA"; 34 | const generator = new Generator(options); 35 | expect(generator.options).toEqual(options); 36 | expect(generator.VIN).toEqual("a1b2"); 37 | expect(generator.tripId).toEqual("tripA") 38 | expect(generator.currentState).toEqual(options.currentState); 39 | expect(generator.staticValues).toEqual(options.staticValues); 40 | expect(generator.isRunning).toEqual(true); 41 | expect(generator.messages).toEqual([]); 42 | expect(DynamicsModel).toHaveBeenCalledWith(expect.objectContaining({ 43 | snapshot: options.currentState, 44 | })) 45 | }) 46 | }) 47 | describe("stop()", function() { 48 | it('should stop dynamics model and generator', async() => { 49 | DynamicsModel.mockImplementationOnce(() => ({ 50 | stopPhysicsLoop: jest.fn(), 51 | snapshot: {} 52 | })) 53 | const generator = new Generator(options); 54 | generator.stop(); 55 | expect(generator.isRunning).toBe(false); 56 | expect(generator.dynamicsModel.engineRunning).toBe(false); 57 | expect(generator.dynamicsModel.ignitionData).toBe('off'); 58 | expect(generator.dynamicsModel.stopPhysicsLoop).toHaveBeenCalledTimes(1); 59 | expect(generator.currentState).toEqual({}); 60 | }) 61 | }) 62 | describe("clearMessages()", function() { 63 | it('should clear generator messages', async() => { 64 | DynamicsModel.mockImplementationOnce(() => ({ 65 | stopPhysicsLoop: jest.fn(), 66 | snapshot: {} 67 | })) 68 | const generator = new Generator(options); 69 | generator.messages = [1,2,3] 70 | generator.clearMessages(); 71 | expect(generator.messages).toHaveLength(0); 72 | }) 73 | }) 74 | describe("_createVin()", function() { 75 | it('should return result of customAlphabet function', async() => { 76 | customAlphabet.mockImplementationOnce(() => () => 'ABC123') 77 | const generator = new Generator(options); 78 | const result = generator._createVIN(); 79 | expect(result).toEqual('ABC123'); 80 | }) 81 | }) 82 | describe("generatePayload()", function() { 83 | it('should add dynamicsModel snapshot to current state', async() => { 84 | const generator = new Generator(options); 85 | generator.dynamicsModel.snapshot = {test: "snapshot"}; 86 | generator.dynamicsModel.ignitionData = "run"; 87 | generator.generateMessagePayload([], 'topic', 'id'); 88 | expect(generator.currentState).toEqual({test: "snapshot"}); 89 | expect(generator.messages).toEqual( 90 | expect.arrayContaining([ 91 | expect.objectContaining({ 92 | topic: "topic", 93 | payload: expect.objectContaining({ 94 | _id_: 'id', 95 | VIN: generator.VIN, 96 | trip_id: generator.tripId 97 | }) 98 | })])) 99 | }) 100 | it('should add correspondingdynamicsModel snapshot attributes to current state and message payload', async() => { 101 | const generator = new Generator(options); 102 | const payload = [ 103 | { 104 | name: 'test' 105 | }, 106 | { 107 | name: 'location' 108 | }, 109 | { 110 | name: 'roundedNumber', 111 | precision: .01 112 | } 113 | ] 114 | generator.dynamicsModel.snapshot = { 115 | test: "snapshot", 116 | latitude: 85, 117 | longitude: 85, 118 | roundedNumber: 1.23456 119 | }; 120 | 121 | generator.dynamicsModel.ignitionData = "run"; 122 | generator.generateMessagePayload(payload, 'topic', 'id'); 123 | expect(generator.currentState).toEqual(generator.dynamicsModel.snapshot); 124 | expect(generator.messages).toEqual( 125 | expect.arrayContaining([ 126 | expect.objectContaining({ 127 | topic: "topic", 128 | payload: expect.objectContaining({ 129 | _id_: 'id', 130 | VIN: generator.VIN, 131 | trip_id: generator.tripId, 132 | location: { 133 | latitude: 85, 134 | longitude: 85 135 | }, 136 | test: "snapshot", 137 | roundedNumber: 1.23 138 | }) 139 | })])) 140 | }) 141 | it('should not add anything to messages if dynamics model is not running', async () => { 142 | const generator = new Generator(options); 143 | generator.generateMessagePayload(); 144 | expect(generator.messages).toHaveLength(0); 145 | expect(generator.currentState).toEqual(options.currentState); 146 | }) 147 | }) 148 | }); -------------------------------------------------------------------------------- /source/simulator/lib/device/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | "use strict"; 8 | const { IoTDataPlaneClient: IotData, PublishCommand } = require("@aws-sdk/client-iot-data-plane"); 9 | const moment = require("moment"); 10 | const Random = require("./generators/random/generator"); 11 | const Vehicle = require("./generators/vehicle/generator.js"); 12 | let awsOptions = { endpoint: "https://" + process.env.IOT_ENDPOINT }; 13 | const { SOLUTION_ID, VERSION } = process.env; 14 | if (SOLUTION_ID && VERSION && SOLUTION_ID.trim() && VERSION.trim()) { 15 | const solutionUserAgent = `AwsSolution/${SOLUTION_ID}/${VERSION}`; 16 | const capability = `AwsSolution-Capability/${SOLUTION_ID}-C003/${VERSION}`; 17 | awsOptions.customUserAgent = [[`${solutionUserAgent}`], [`${capability}`]]; 18 | } 19 | const iotData = new IotData(awsOptions); 20 | /** 21 | * @Class Device - represents a single device 22 | */ 23 | class Device { 24 | constructor(options, sim, device) { 25 | this.options = options; 26 | this.id = device.id; 27 | this.started = device.started || moment().toISOString(); 28 | this.simId = sim.simId; 29 | this.sendInterval = null; 30 | this.stage = device.stage || sim.stage; 31 | if (device.generator) { 32 | this.options.currentState = device.generator.currentState; 33 | this.options.staticValues = device.generator.staticValues; 34 | } 35 | this.generator = sim.simId.includes("idsAutoDemo") ? new Vehicle(this.options) : new Random(this.options); 36 | this.payload = device.payload; 37 | this.topic = device.topic; 38 | this.duration = sim.duration * 1000; 39 | this.interval = sim.interval * 1000; 40 | } 41 | 42 | /** 43 | * generates and sends a message on the given interval 44 | */ 45 | sendOnInterval = () => { 46 | return new Promise((resolve, reject) => { 47 | //generate message on interval 48 | this.sendInterval = setInterval(async () => { 49 | //generate the message and publish 50 | this._generateMessage(); 51 | if (this.stage === "sleeping") { 52 | //stop device if no longer running 53 | this.generator.stop(); 54 | clearInterval(this.sendInterval); 55 | resolve("complete"); 56 | } else if (this.options.context.getRemainingTimeInMillis() <= this.interval) { 57 | //stop device if not enough time to run another interval 58 | this.generator.stop(); 59 | clearInterval(this.sendInterval); 60 | //return information necessary to restart device 61 | resolve({ 62 | stage: this.stage, 63 | started: this.started, 64 | id: this.id, 65 | generator: { 66 | currentState: this.generator.currentState, 67 | staticValues: this.generator.staticValues, 68 | }, 69 | }); 70 | } 71 | }, this.interval); 72 | }); 73 | }; 74 | 75 | /** 76 | * Starts the device 77 | * @returns the result of the device run 78 | */ 79 | async run() { 80 | this.stage = "running"; 81 | try { 82 | //start generating messages on given interval 83 | return this.sendOnInterval(); 84 | } catch (err) { 85 | console.error("Error occurred while starting to generate messages", err); 86 | throw err; 87 | } 88 | } 89 | 90 | /** 91 | * Stops the device 92 | */ 93 | stop() { 94 | this.stage = "sleeping"; 95 | } 96 | 97 | /** 98 | * publishes the message payload to the given IoT topic 99 | * @param {string} topic 100 | * @param {object} messagePayload 101 | * @returns the data from the publish call 102 | */ 103 | async _publishMessage(topic, messagePayload) { 104 | //set iot publish params 105 | let params = { 106 | topic: topic, 107 | payload: messagePayload, 108 | qos: 0, 109 | }; 110 | //publish to IoT topic 111 | try { 112 | return await iotData.send(new PublishCommand(params)); 113 | } catch (err) { 114 | console.error("Error occurred while publishinig message to IoT topic", err); 115 | throw err; 116 | } 117 | } 118 | 119 | /** 120 | * Generates the message to be published 121 | * and publishes to the given IoT topic 122 | */ 123 | async _generateMessage() { 124 | //check if the duration has elapsed 125 | let timeDelta = moment().diff(moment(this.started)); 126 | if (timeDelta > this.duration || !this.generator.isRunning) { 127 | console.log(`Device '${this.id}' run duration has elapsed.`); 128 | this.stop(); 129 | } else { 130 | //generate the message payload 131 | this.generator.generateMessagePayload(this.payload, this.topic, this.id); 132 | //publish all messages 133 | let messagePromises = this.generator.messages.map((_message) => { 134 | let _payload = JSON.stringify(_message.payload); 135 | return this._publishMessage(_message.topic, _payload); 136 | }); 137 | //wait for messages to publish then clear the generator messages 138 | try { 139 | await Promise.all(messagePromises); 140 | this.generator.clearMessages(); 141 | } catch (err) { 142 | console.error("Error occurred while clearing the generator messages", err); 143 | throw err; 144 | } 145 | } 146 | } 147 | } 148 | 149 | module.exports = Device; 150 | -------------------------------------------------------------------------------- /source/simulator/lib/engine/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @author Solution Builders 6 | */ 7 | 8 | "use strict"; 9 | const { DynamoDBDocumentClient, GetCommand } = require("@aws-sdk/lib-dynamodb"), 10 | { DynamoDBClient } = require("@aws-sdk/client-dynamodb"); 11 | const { SOLUTION_ID, VERSION } = process.env; 12 | let awsOptions = {}; 13 | if (SOLUTION_ID && VERSION && SOLUTION_ID.trim() && VERSION.trim()) { 14 | const solutionUserAgent = `AwsSolution/${SOLUTION_ID}/${VERSION}`; 15 | const capability = `AwsSolution-Capability/${SOLUTION_ID}-C005/${VERSION}`; 16 | awsOptions.customUserAgent = [[`${solutionUserAgent}`], [`${capability}`]]; 17 | } 18 | const docClient = DynamoDBDocumentClient.from(new DynamoDBClient(awsOptions)); 19 | const Device = require("../device/index.js"); 20 | 21 | /** 22 | * @class Engine - Engine for simulating devices of a device type 23 | */ 24 | class Engine { 25 | constructor(options, simulation, device) { 26 | this.options = options; 27 | this.device = device; 28 | this.simulation = simulation; 29 | //set to poll DDB for stage every 30 sec 30 | this.stagePoller = setInterval(() => { 31 | this._pollDeviceStage(); 32 | }, 30000); 33 | this.deviceInstances = []; 34 | } 35 | 36 | /** 37 | * Starts the number of devices specified for a device type 38 | */ 39 | async start() { 40 | let promises = []; 41 | 42 | for (let i = 0; i < this.device.amount; i++) { 43 | //check for device state from previous lambda run 44 | let deviceState; 45 | if (this.device.states) { 46 | deviceState = this.device.states[i]; 47 | this.device.info = { ...this.device.info, ...deviceState }; 48 | } else { 49 | //set predictable device ID for access in front end 50 | this.device.info.id = `${this.simulation.simId.slice(0, 3)}${this.device.typeId.slice(0, 3)}${i}`; 51 | } 52 | //create new Device and run if not already complete 53 | if (deviceState && deviceState === "complete") { 54 | promises.push(deviceState); 55 | } else { 56 | let device = new Device(this.options, this.simulation, this.device.info); 57 | this.deviceInstances.push(device); 58 | promises.push(device.run()); 59 | } 60 | } 61 | //Wait for all devices to finish 62 | console.log(`Running ${this.device.amount} devices of type ${this.device.typeId}`); 63 | let results = await Promise.all(promises); 64 | clearInterval(this.stagePoller); 65 | //make sure every device for this device type are complete 66 | if (results.every((result) => result === "complete")) { 67 | return "complete"; 68 | } else { 69 | //if not complete return necessary info for resart 70 | return { 71 | typeId: this.device.typeId, 72 | amount: this.device.amount, 73 | info: { 74 | topic: this.device.info.topic, 75 | payload: this.device.info.payload, 76 | }, 77 | states: results, 78 | }; 79 | } 80 | } 81 | 82 | /** 83 | * Polls DynamoDB to check for stopping signal 84 | */ 85 | async _pollDeviceStage() { 86 | console.log("Polling for current simulation stage"); 87 | let params = { 88 | TableName: process.env.SIM_TABLE, 89 | Key: { 90 | simId: this.simulation.simId, 91 | }, 92 | }; 93 | try { 94 | let simulation = await docClient.send(new GetCommand(params)); 95 | if (simulation.Item) { 96 | console.log(`Attempting to stop ${this.device.amount} devices of type ${this.device.typeId}.`); 97 | if (simulation.Item.stage === "stopping") { 98 | this.deviceInstances.forEach((device) => { 99 | device.stop(); 100 | }); 101 | } 102 | } else { 103 | throw Error("simulation not found"); 104 | } 105 | } catch (err) { 106 | console.error(err); 107 | console.error(`Error retrieving simulation from ddb to check stage change, simId:, ${this.simulation.simId}`); 108 | throw err; 109 | } 110 | } 111 | } 112 | 113 | module.exports = Engine; 114 | -------------------------------------------------------------------------------- /source/simulator/lib/engine/index.spec.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | "use strict"; 5 | const { DynamoDBDocumentClient, GetCommand } = require("@aws-sdk/lib-dynamodb"); 6 | const AWSMock = require("aws-sdk-client-mock"); 7 | const dynamodbDocMock = AWSMock.mockClient(DynamoDBDocumentClient); 8 | require("aws-sdk-client-mock-jest"); 9 | process.env.AWS_REGION = "us-east-1"; 10 | process.env.IOT_ENDPOINT = "endpoint.fake"; 11 | const Device = require("../device/index.js"); 12 | jest.mock("../device/index.js"); 13 | const Engine = require("./index.js"); 14 | 15 | let simulation = { 16 | simId: "123", 17 | name: "abc", 18 | stage: "running", 19 | devices: [{ typeId: "456", name: "xyz", amount: 1 }], 20 | interval: 2, 21 | duration: 5, 22 | }; 23 | 24 | let deviceInfo = { 25 | id: "1234560", 26 | typeId: "456", 27 | name: "xyz", 28 | topic: "topic/test", 29 | payload: [ 30 | { 31 | name: "aString", 32 | type: "string", 33 | min: "2", 34 | max: "4", 35 | static: false, 36 | }, 37 | ], 38 | }; 39 | 40 | let device = { 41 | amount: 1, 42 | typeId: "456", 43 | info: { ...deviceInfo }, 44 | }; 45 | 46 | let options = { 47 | timeLeft: 50000, 48 | context: { 49 | getRemainingTimeInMillis: () => { 50 | return options.timeLeft; 51 | }, 52 | }, 53 | }; 54 | 55 | jest.useFakeTimers(); 56 | 57 | describe("Engine", function () { 58 | beforeEach(() => { 59 | dynamodbDocMock.reset(); 60 | }); 61 | afterEach(() => { 62 | Device.mockImplementation(() => ({ 63 | run: jest.fn(), 64 | stage: "running", 65 | stop: jest.fn(), 66 | })); 67 | dynamodbDocMock.restore(); 68 | }); 69 | 70 | describe("start()", function () { 71 | it("should return complete for each succesful device run", async () => { 72 | Device.mockImplementation(() => ({ 73 | run: jest.fn(() => "complete"), 74 | })); 75 | const engine = new Engine(options, simulation, device); 76 | clearInterval(engine.stagePoller); 77 | device.amount = 5; 78 | const result = await engine.start(); 79 | engine.deviceInstances.forEach((device) => { 80 | expect(device.run).toHaveReturnedWith("complete"); 81 | expect(device.run).toHaveBeenCalledTimes(1); 82 | }); 83 | expect(result).toEqual("complete"); 84 | }); 85 | it("should return relevant info for each device when not complete", async () => { 86 | Device.mockImplementation(() => ({ 87 | run: jest.fn(() => ({ info: {} })), 88 | })); 89 | const engine = new Engine(options, simulation, device); 90 | clearInterval(engine.stagePoller); 91 | device.amount = 2; 92 | const result = await engine.start(); 93 | engine.deviceInstances.forEach((device) => { 94 | expect(device.run).toHaveReturnedWith({ info: {} }); 95 | }); 96 | expect(result).toEqual({ 97 | typeId: device.typeId, 98 | amount: device.amount, 99 | info: { 100 | topic: deviceInfo.topic, 101 | payload: deviceInfo.payload, 102 | }, 103 | states: [{ info: {} }, { info: {} }], 104 | }); 105 | }); 106 | it("should run only non-complete devices", async () => { 107 | Device.mockImplementation(() => ({ 108 | run: jest.fn(() => ({ info: {} })), 109 | })); 110 | device.states = ["complete", { info: {} }]; 111 | const engine = new Engine(options, simulation, device); 112 | clearInterval(engine.stagePoller); 113 | device.amount = 2; 114 | const result = await engine.start(); 115 | expect(engine.deviceInstances).toHaveLength(1); 116 | expect(engine.deviceInstances[0].run).toHaveReturnedWith({ info: {} }); 117 | expect(result).toEqual({ 118 | typeId: device.typeId, 119 | amount: device.amount, 120 | info: { 121 | topic: deviceInfo.topic, 122 | payload: deviceInfo.payload, 123 | }, 124 | states: ["complete", { info: {} }], 125 | }); 126 | }); 127 | }); 128 | describe("_pollDeviceStage()", function () { 129 | it('should get simulation from DynamoDB and keep running if simulation stage is not "stopping"', async () => { 130 | dynamodbDocMock.on(GetCommand).resolves({ Item: { stage: "running" } }); 131 | jest.spyOn(Device.prototype, "stop"); 132 | const engine = new Engine(options, simulation, device); 133 | clearInterval(engine.stagePoller); 134 | engine.deviceInstances.push(new Device(options, simulation, deviceInfo)); 135 | 136 | try { 137 | await engine._pollDeviceStage(); 138 | expect(engine.deviceInstances[0].stop).toHaveBeenCalledTimes(0); 139 | expect(engine.deviceInstances[0].stage).toEqual("running"); 140 | } catch (err) { 141 | console.error(err); 142 | throw err; 143 | } 144 | }); 145 | it('should get simulation from DynamoDB and stop if simulation stage is "stopping"', async () => { 146 | dynamodbDocMock.on(GetCommand).resolves({ Item: { stage: "stopping" } }); 147 | const engine = new Engine(options, simulation, device); 148 | clearInterval(engine.stagePoller); 149 | engine.deviceInstances.push(new Device(options, simulation, deviceInfo)); 150 | jest.spyOn(engine.deviceInstances[0], "stop"); 151 | await engine._pollDeviceStage(); 152 | expect(engine.deviceInstances[0].stop).toHaveBeenCalledTimes(1); 153 | }); 154 | it("should thow error if DynamoDB get returns error", async () => { 155 | dynamodbDocMock.on(GetCommand).rejects("error"); 156 | jest.spyOn(Device.prototype, "stop"); 157 | const engine = new Engine(options, simulation, device); 158 | engine.deviceInstances.push(new Device(options, simulation, deviceInfo)); 159 | clearInterval(engine.stagePoller); 160 | try { 161 | await engine._pollDeviceStage(); 162 | } catch (err) { 163 | expect(err).toEqual(Error("error")); 164 | } 165 | }); 166 | it("should throw error if DynamoDB returns empty value", async () => { 167 | dynamodbDocMock.on(GetCommand).resolves({}); 168 | const engine = new Engine(options, simulation, device); 169 | clearInterval(engine.stagePoller); 170 | try { 171 | await engine._pollDeviceStage(); 172 | } catch (err) { 173 | expect(err).toEqual(Error("simulation not found")); 174 | } 175 | }); 176 | it("should poll device stage after 30 seconds", async () => { 177 | const engine = new Engine(options, simulation, device); 178 | engine.deviceInstances.push(new Device(options, simulation, deviceInfo)); 179 | jest.spyOn(engine, "_pollDeviceStage").mockImplementation(async () => { 180 | return; 181 | }); 182 | jest.advanceTimersByTime(30000); 183 | expect(engine._pollDeviceStage).toHaveBeenCalledTimes(1); 184 | }); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /source/simulator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iot-sim-engine-simulator", 3 | "description": "The simulator Lambda for the IoT Device Simulator solution", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "main": "index.js", 9 | "license": "Apache-2.0", 10 | "version": "3.0.9", 11 | "private": true, 12 | "dependencies": { 13 | "@aws-sdk/client-dynamodb": "^3.391.0", 14 | "@aws-sdk/client-iot-data-plane": "^3.391.0", 15 | "@aws-sdk/client-s3": "^3.391.0", 16 | "@aws-sdk/lib-dynamodb": "^3.391.0", 17 | "@aws-sdk/util-dynamodb": "^3.391.0", 18 | "faker": "^5.5.3", 19 | "moment": "^2.29.4", 20 | "nanoid": "^3.1.25", 21 | "random-location": "^1.1.3" 22 | }, 23 | "devDependencies": { 24 | "aws-sdk-client-mock": "^3.0.0", 25 | "aws-sdk-client-mock-jest": "^3.0.0", 26 | "jest": "^29.6.2" 27 | }, 28 | "scripts": { 29 | "test": "jest --coverage --silent", 30 | "clean": "rm -rf node_modules coverage dist package-lock.json", 31 | "build": "npm run clean && npm install --production", 32 | "package": "npm run build && mkdir dist && zip -q -r9 ./dist/package.zip *" 33 | } 34 | } --------------------------------------------------------------------------------