diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5b627cf..a0ea08c 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,4 @@ ## Code of Conduct This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. +opensource-codeofconduct@amazon.com with any additional questions or comments. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4b6a1c..3a6fbda 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,4 +56,4 @@ If you discover a potential security issue in this project we ask that you notif ## Licensing -See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. +See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 1bb4f21..6aa0c45 100644 --- a/LICENSE +++ b/LICENSE @@ -12,4 +12,3 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.md b/README.md index 7f92204..54b92c2 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,72 @@ -## My Project +# AWS IoT EduKit Tutorials +This repository contains the tutorial content for the AWS IoT EduKit program. To view the AWS IoT EduKit content live and learn more about the program, visit (https://edukit.workshop.aws)[https://edukit.workshop.aws]. -TODO: Fill this README out! +## Repo structure +```bash +. +├── metadata.yml <-- Metadata file with descriptive information about the workshop +├── README.md <-- This instructions file +├── deck <-- Directory for presentation deck +├── resources <-- Directory for workshop resources +│ ├── code <-- Directory for workshop modules code +│ ├── policies <-- Directory for workshop modules IAM Roles and Policies +│ └── templates <-- Directory for workshop modules CloudFormation templates +└── workshop + ├── config.toml <-- Hugo configuration file for the workshop website + └── content <-- Markdown files for pages/steps in workshop + └── static <-- Any static assets to be hosted alongside the workshop (ie. images, scripts, documents, etc) + └── themes <-- AWS Style Hugo Theme (Do not edit!) +``` -Be sure to: +## What's Included +This project the following folders: -* Change the title in this README -* Edit your repository description on GitHub +* `deck`: Future location to store presentation materials. Currently unused. +* `resources`: Future location to store any example code, IAM policies, or Cloudformation templates. Currently unused. +* `workshop`: This is the core workshop folder. This is generated as HTML and hosted for presentation for customers. -## Security -See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. +## Requirements to web application locally +1. [Clone this repository](https://help.github.com/articles/fork-a-repo/). +2. [Install Hugo locally](https://gohugo.io/overview/quickstart/). As of 1 Mar 2020, the workshop.aws build process uses [Hugo 0.64.1](https://github.com/gohugoio/hugo/releases/tag/v0.64.1) -## License -This library is licensed under the MIT-0 License. See the LICENSE file. +# Running the web application locally +## Navigate to the `workshop` directory +All command line directions in this documentation assume you are in the `workshop` directory. Navigate there now, if you aren't there already. +```bash +cd aws-iot-edukit-tutorials/workshop +``` + +## Launching the website locally +Launch by using the following command: + +```bash +hugo serve +``` + +Go to `http://localhost:1313` in your browser + +You should notice three things: + +1. You have a left-side **Intro** menu, containing two submenus with names equal to the `title` properties in the previously created files. +2. The home page explains how to customize it by following the instructions. +3. When you run `hugo server`, when the contents of the files change, the page automatically refreshes with the changes. Neat! + +Alternatively, you can run the following command in a terminal window to tell Hugo to automatically rebuild whenever a file is changed. This can be helpful when rapidly iterating over content changes. + +```bash +hugo server +``` + +## Adding language translations: +To add additional languages, update the config.toml with the language you're adding. +The example below demonstrates how to add French language translations. To add individual pages in French, go to the corresponding tutorial and section directory in the **workshop/content** folder and add a new file following the pattern `_index.<>.md`. So to match the config.toml example below for french, the page filename would be **_index.fr.md**. + +``` +[Languages.fr] +title = "Mon atelier AWS" +weight = 2 +languageName = "Français" +``` \ No newline at end of file diff --git a/deck/.gitkeep b/deck/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/metadata.yml b/metadata.yml new file mode 100644 index 0000000..cd97036 --- /dev/null +++ b/metadata.yml @@ -0,0 +1,29 @@ +#name - DNS-friendly name for the workshop. This will be used when generating the hosting URL (ie. https://my-first-workshop.workshops.aws/) +name: edukit +#title - The title of your workshop +title: AWS IoT EduKit +#description - A short description that will be displayed in search results +description: AWS IoT EduKit is a prescriptive learning program for developers. With AWS IoT EduKit, students working on their first IoT project, professionals who want to learn more about IoT, and engineers who want to develop new IoT skills, can use a reference hardware kit and self-service tutorials for a hands-on introduction to building IoT applications. +#categories - Refer to official AWS categories covered by the workshop content here +categories: + - IoT + - Alexa +#services - Refer to the official AWS service names covered by the workshop content here +services: + - IoT Core + - IoT Analytics + - Amazon Sagemaker Autopilot + - Alexa +#level - Approximate skill level needed for this workshop +level: 100 +#duration - Estimated duration in minutes +duration: 360 +#cost - Cost in USD. If the content is offered without cost, enter 0 +cost: 0 +#author - Amazon alias of the primary author of the content +author: trashed, burkery, erifahsl +#audience - Names of the personas associated with this workshop +audience: + - IT Professional + - Developer + - Student \ No newline at end of file diff --git a/resources/code/.gitkeep b/resources/code/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/policies/.gitkeep b/resources/policies/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/templates/.gitkeep b/resources/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/workshop/config.toml b/workshop/config.toml new file mode 100644 index 0000000..79da687 --- /dev/null +++ b/workshop/config.toml @@ -0,0 +1,71 @@ +RelativeURLs=true +CanonifyURLs=true +languageCode = "en-US" +defaultContentLanguage = "en" + +title = "AWS IoT EduKit" +theme = "learn" +metaDataFormat = "yaml" +defaultContentLanguageInSubdir= true + +uglyurls = true +sectionPagesMenu = "main" +pygmentsCodeFences = true +pygmentsStyle = "monokai" + +[params] + description = "AWS IoT EduKit" + author = "Rashed Talukder, Ryan Burke, Z Sun, Eric Fahsl" + disableBreadcrumb = false + disableNextPrev = false + themeVariant = "aws" + disableSearch = false + disableAssetsBusting = true + disableLanguageSwitchingButton = false + disableShortcutsTitle = true + disableInlineCopyToClipBoard = true + +[outputs] +home = [ "HTML", "RSS", "JSON"] + +[markup] + defaultMarkdownHandler = "goldmark" + [markup.goldmark] + [markup.goldmark.renderer] + unsafe = true + +[Languages] +[Languages.en] +title = "AWS IoT EduKit" +weight = 1 +languageName = "English" +[Languages.en_uk] +title = "AWS IoT EduKit" +weight = 2 +languageName = "English UK" + +# + +[menu] + [[menu.main]] + identifier = " " + name = " " + title = " " + url = "/" + weight = -110 + +[[menu.shortcuts]] +name = " GitHub Repository ( Jan 19, 2021)" +url = "https://github.com/m5stack/Core2-for-AWS-IoT-EduKit" +weight = 1 + +[[menu.shortcuts]] +name = " Community Support" +url = "https://community.m5stack.com/category/41/core2-for-aws" +weight = 2 + +[[menu.shortcuts]] +name = " Report Bugs" +url = "https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" +weight = 3 + diff --git a/workshop/content/Blinky hello world/Blinking the LEDs/_index.en.md b/workshop/content/Blinky hello world/Blinking the LEDs/_index.en.md new file mode 100644 index 0000000..b4aab11 --- /dev/null +++ b/workshop/content/Blinky hello world/Blinking the LEDs/_index.en.md @@ -0,0 +1,49 @@ ++++ +title = "Blinking the LEDs" +weight = 40 +pre = "d. " ++++ + +At this point, your device software is running and you are connected to AWS IoT Core, are sending messages, and are ready to receive and act on messages from the cloud. In this chapter, you will subscribe to an MQTT topic, view the messages coming into AWS IoT Core, and send a message to blink your device LEDs. Since MQTT is a publish-subscribe protocol, you can subscribe and/or publish to specific topics. The policy that was set earlier in the registration script constrained the topics that the device can subscribe and publish to. This is critical for filtering, and potentially for security. Your device can only send or receive to topics that matches the client Id, which in this case is the same as the unique serial number of the device's secure element. + +## Subscribing via the AWS IoT MQTT client +The AWS IoT MQTT client in the AWS IoT Core console allows you to both view and publish MQTT messages. To start, go to [AWS IoT console](https://us-west-2.console.aws.amazon.com/iot/home?region=us-west-2#/) and choose **Test** to open the client view. + +{{< img "choose-test.png" "Choose test in AWS IoT console" >}} + +In the **Subscription topic** field, enter `#` to subscribe to all MQTT topic names. The multi-level wild card [topic filter](https://docs.aws.amazon.com/iot/latest/developerguide/topics.html#topicfilters) is **#** and can only be used once as the last character of a topic filter. Once you press the **Subscribe to topic** button, you will see messages being sent from your device. The device is only allowed to publish messages to the topic beginning with `<>/`. This gives the ability for another subscriber (e.g. Cloud application) to filter topics for specific client devices, and can also be narrowed to be more specific topics (e.g. temperature reading of a specific device). + +## Blinking the LED +To start/stop the blinking of the LED bars on the sides of the M5Stack Core2 for AWS IoT EduKit reference hardware, we're going to publish from the console's AWS IoT MQTT client on a topic that the Core2 for AWS IoT EduKit is subscribed to. To do so, we first need to get the device's client Id. Going back to the shell prompt where the serial monitor is still running, copy the **client Id**. + +{{% notice note %}} +If you do not see **(edukit)** prefix at your shell prompt, ensure you activate your conda environment by running `conda activate edukit`. +If the idf.py command is not found, add the ESP-IDF to your path with the command `. $HOME/esp/esp-idf/export.sh` (macOS/Linux) or `%userprofile%\Desktop\esp-idf\export.bat` (Windows). +{{% /notice %}} + +In the Publish box, enter the command below, but replacing the **<>** text with your actual client Id that was just copied and then press the **Publish to topic** button: +``` +<>/blink +``` +{{< img "aws_iot_mqtt_client.png" "Subscribing to messages and publishing with AWS IoT console MQTT client" >}} +Your device should now have the side bar LEDs blinking. To pause the blinking, simply press the **Publish to topic** button again to the same topic and it will pause the blinking task that was looping the LEDs on and off. These tasks are part of the FreeRTOS kernel (can also be thought of as threads) that should perform a single task in a loop. The FreeRTOS kernel gives microcontroller applications the ability to optimize the processor by scheduling individual tasks to run once another task has put itself into a suspended or blocked state (e.g. with **vTaskSuspend** or **vTaskDelay**). Learn more about about scheduling with FreeRTOS [here](https://www.freertos.org/implementation/a00005.html). + +{{% notice info %}} +To exit out of the serial monitor, press **CTRL** + **]**. +{{% /notice %}} + +## Cleaning up +To optimize resources used and avoid unwanted possible AWS Cloud service charges, you will be erasing the flash on your device to get it ready for the subsequent tutorials. To erase the flash, you'll need to be in a project that has already been built (such as the one you just completed), exit a running serial monitor that might block the port (press **CTRL** + **]**), and use the command: + +``` +idf.py erase_flash -p <> +``` + +## Conclusion +Hopefully you enjoyed the journey of building your cloud connected blinky project. Using Espressif's toolchain (ESP-IDF), you were able to build, flash, and monitor your Core2 for AWS IoT EduKit's firmware. Your device is now registered as an AWS IoT thing with AWS for secure cloud connectivity using embedded device certificates and private keys that can never leave the device. You connected to AWS IoT Core, sent messages from the device over MQTT, and received an MQTT message from the AWS IoT console's MQTT client which was used to toggle the lights on the device. + + +On to the next tutorial, [**Smart Thermostat**](/en/smart-thermostat.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Blinky hello world/Blinking the LEDs/_index.en_uk.md b/workshop/content/Blinky hello world/Blinking the LEDs/_index.en_uk.md new file mode 100644 index 0000000..1cd1691 --- /dev/null +++ b/workshop/content/Blinky hello world/Blinking the LEDs/_index.en_uk.md @@ -0,0 +1,49 @@ ++++ +title = "Blinking the LEDs" +weight = 40 +pre = "d. " ++++ + +At this point, your device software is running and you are connected to AWS IoT Core, are sending messages, and are ready to receive and act on messages from the cloud. In this chapter, you will subscribe to an MQTT topic, view the messages coming into AWS IoT Core, and send a message to blink your device LEDs. Since MQTT is a publish-subscribe protocol, you can subscribe and/or publish to specific topics. The policy that was set earlier in the registration script constrained the topics that the device can subscribe and publish to. This is critical for filtering, and potentially for security. Your device can only send or receive to topics that matches the client Id, which in this case is the same as the unique serial number of the device's secure element. + +## Subscribing via the AWS IoT MQTT client +The AWS IoT MQTT client in the AWS IoT Core console allows you to both view and publish MQTT messages. To start, go to [AWS IoT console](https://us-west-2.console.aws.amazon.com/iot/home?region=us-west-2#/) and choose **Test** to open the client view. + +{{< img "choose-test.png" "Choose test in AWS IoT console" >}} + +In the **Subscription topic** field, enter `#` to subscribe to all MQTT topic names. The multi-level wild card [topic filter](https://docs.aws.amazon.com/iot/latest/developerguide/topics.html#topicfilters) is **#** and can only be used once as the last character of a topic filter. Once you press the **Subscribe to topic** button, you will see messages being sent from your device. The device is only allowed to publish messages to the topic beginning with `<>/`. This gives the ability for another subscriber (e.g. Cloud application) to filter topics for specific client devices, and can also be narrowed to be more specific topics (e.g. temperature reading of a specific device). + +## Blinking the LED +To start/stop the blinking of the LED bars on the sides of the M5Stack Core2 for AWS IoT EduKit reference hardware, we're going to publish from the console's AWS IoT MQTT client on a topic that the Core2 for AWS IoT EduKit is subscribed to. To do so, we first need to get the device's client Id. Going back to the shell prompt where the serial monitor is still running, copy the **client Id**. + +{{% notice note %}} +If you do not see **(edukit)** prefix at your shell prompt, ensure you activate your conda environment by running `conda activate edukit`. +If the idf.py command is not found, add the ESP-IDF to your path with the command `. $HOME/esp/esp-idf/export.sh` (macOS/Linux) or `%userprofile%\Desktop\esp-idf\export.bat` (Windows). +{{% /notice %}} + +In the Publish box, enter the command below, but replacing the **<>** text with your actual client Id that was just copied and then press the **Publish to topic** button: +``` +<>/blink +``` +{{< img "aws_iot_mqtt_client.png" "Subscribing to messages and publishing with AWS IoT console MQTT client" >}} +Your device should now have the side bar LEDs blinking. To pause the blinking, simply press the **Publish to topic** button again to the same topic and it will pause the blinking task that was looping the LEDs on and off. These tasks are part of the FreeRTOS kernel (can also be thought of as threads) that should perform a single task in a loop. The FreeRTOS kernel gives microcontroller applications the ability to optimize the processor by scheduling individual tasks to run once another task has put itself into a suspended or blocked state (e.g. with **vTaskSuspend** or **vTaskDelay**). Learn more about about scheduling with FreeRTOS [here](https://www.freertos.org/implementation/a00005.html). + +{{% notice info %}} +To exit out of the serial monitor, press **CTRL** + **]**. +{{% /notice %}} + +## Cleaning up +To optimize resources used and avoid unwanted possible AWS Cloud service charges, you will be erasing the flash on your device to get it ready for the subsequent tutorials. To erase the flash, you'll need to be in a project that has already been built (such as the one you just completed), exit a running serial monitor that might block the port (press **CTRL** + **]**), and use the command: + +``` +idf.py erase_flash -p <> +``` + +## Conclusion +Hopefully you enjoyed the journey of building your cloud connected blinky project. Using Espressif's toolchain (ESP-IDF), you were able to build, flash, and monitor your Core2 for AWS IoT EduKit's firmware. Your device is now registered as an AWS IoT thing with AWS for secure cloud connectivity using embedded device certificates and private keys that can never leave the device. You connected to AWS IoT Core, sent messages from the device over MQTT, and received an MQTT message from the AWS IoT console's MQTT client which was used to toggle the lights on the device. + + +On to the next tutorial, [**Smart Thermostat**](/en_uk/smart-thermostat.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Blinky hello world/Blinking the LEDs/aws_iot_mqtt_client.png b/workshop/content/Blinky hello world/Blinking the LEDs/aws_iot_mqtt_client.png new file mode 100644 index 0000000..5c0eb78 Binary files /dev/null and b/workshop/content/Blinky hello world/Blinking the LEDs/aws_iot_mqtt_client.png differ diff --git a/workshop/content/Blinky hello world/Blinking the LEDs/choose-test.png b/workshop/content/Blinky hello world/Blinking the LEDs/choose-test.png new file mode 100644 index 0000000..75aea14 Binary files /dev/null and b/workshop/content/Blinky hello world/Blinking the LEDs/choose-test.png differ diff --git a/workshop/content/Blinky hello world/Connecting to AWS/_index.en.md b/workshop/content/Blinky hello world/Connecting to AWS/_index.en.md new file mode 100644 index 0000000..2e76e56 --- /dev/null +++ b/workshop/content/Blinky hello world/Connecting to AWS/_index.en.md @@ -0,0 +1,53 @@ ++++ +title = "Connecting to AWS IoT Core" +weight = 30 +pre = "c. " ++++ + +In this chapter you'll configure, build, and flash your device firmware—which will connect to AWS IoT Core. In order to connect and communicate to AWS IoT Core, you need to configure your device with Wi-Fi credentials and the URL of your AWS endpoint. Connectivity is simplified with the pre-provisioned certificates in the Microchip ATECC608 Trust&GO, which is assigned to our AWS IoT thing and attached to a security policy from the previous chapter. You do not need to retrieve certificates from AWS IoT Core or generate your own. To configure, build, and flash our firmware on to the Espressif ESP32-D0WD you will use the ESP-IDF. + +## Configuring the ESP32 Firmware +Configuration of your source code is handled through [Kconfig](https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html). Kconfig is the same configuration system used by the Linux kernel and helps simplify available configuration options (symbols) into a tree structure. Before you set the configuration, you will first need to retrieve your AWS IoT endpoint. + +```bash +aws iot describe-endpoint --endpoint-type iot:Data-ATS +``` +Copy your AWS IoT endpoint without the quotes. It will look something like `3duk1t3xampl3.iot.us-west-2.amazonaws.com`. We'll use this in a moment. + +You can enter the configuration menu from the **Blink-Hello-World** directory of the repository: +```bash +idf.py menuconfig +``` +{{< img "hello_world-menuconfig.webp" "Configuring Core2 for AWS IoT EduKit with idf.py menuconfig" >}} +Here you will set the configuration. Use the direction keys on your keyboard to go to **Component config** --> **Amazon Web Services IoT Platform** and open **AWS IoT Endpoint Hostname** to set the string. You can paste the address you copied moments ago into the box and hit _enter_ to set that symbol. Next, go back to the configuration home screen by pressing the *ESC* key twice. Then select **AWS IoT EduKit Configuration** from the menu. Set your **WiFi SSID** and **WiFi Password** with your Wi-FI credentials. Once you are finished, press the *s* button on your keyboard to save, confirm the location of the file by pressing *enter*, followed by *q* to quit. + +{{% notice warning %}} +Be sure your SSID is for a 2.4GHz network. The ESP32-D0WD on the M5Stack Core2 for AWS hardware does not support 5GHz Wi-Fi bands. +{{% /notice %}} + +## Building the ESP32 Firmware +Building the firmware is easy, but can take quite a while the first time. ESP-IDF uses CMAKE as it's build system and will link all the files necessary and begin compiling the code to a flashable binary in ELF format that can be flashed on to the device. +```bash +idf.py build +``` + +## Erasing Old Firmware +If you completed the Getting Started or Alexa demo, it is recommended to erase the flash memory of your device. The flash is split into separate partitions where the application data, OTA data, and keys are stored. The other walk-throughs use a different partition table than used for this tutorial and could cause issues if not wiped. Replace **<>** with the serial port your Core2 for AWS IoT EduKit device is connected to: +```bash +idf.py erase_flash -p <> +``` + +## Flashing the ESP32 Firmware and monitoring the device output over the serial port +To flash the firmware you just built and view all the outputs by the device over the serial connection, you're going to daisy chain commands and enter: +```bash +idf.py flash monitor -p <> +``` +Once the device is flashed, it will reboot and run the application. You should see the device connect to your Wi-Fi network, establish a secure MQTT connection to AWS IoT Core, subscribe to the preset MQTT topic, and begin sending messages. + +## Chapter conclusion +In this chapter, you have successfully compiled and flash your device and are actively monitoring it's serial outputs. Using the AWS IoT Device SDK for Embedded C, the reference hardware authenticated with the MQTT message broker (AWS IoT Core) and is ready to receive messages. + +You are now ready to head to the final chapter in this tutorial, [**Blinking the LED**](/en/blinky-hello-world/blinking-the-leds.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Blinky hello world/Connecting to AWS/_index.en_uk.md b/workshop/content/Blinky hello world/Connecting to AWS/_index.en_uk.md new file mode 100644 index 0000000..51c2127 --- /dev/null +++ b/workshop/content/Blinky hello world/Connecting to AWS/_index.en_uk.md @@ -0,0 +1,53 @@ ++++ +title = "Connecting to AWS IoT Core" +weight = 30 +pre = "c. " ++++ + +In this chapter you'll configure, build, and flash your device firmware—which will connect to AWS IoT Core. In order to connect and communicate to AWS IoT Core, you need to configure your device with Wi-Fi credentials and the URL of your AWS endpoint. Connectivity is simplified with the pre-provisioned certificates in the Microchip ATECC608 Trust&GO, which is assigned to our AWS IoT thing and attached to a security policy from the previous chapter. You do not need to retrieve certificates from AWS IoT Core or generate your own. To configure, build, and flash our firmware on to the Espressif ESP32-D0WD you will use the ESP-IDF. + +## Configuring the ESP32 Firmware +Configuration of your source code is handled through [Kconfig](https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html). Kconfig is the same configuration system used by the Linux kernel and helps simplify available configuration options (symbols) into a tree structure. Before you set the configuration, you will first need to retrieve your AWS IoT endpoint. + +```bash +aws iot describe-endpoint --endpoint-type iot:Data-ATS +``` +Copy your AWS IoT endpoint without the quotes. It will look something like `3duk1t3xampl3.iot.us-west-2.amazonaws.com`. We'll use this in a moment. + +You can enter the configuration menu from the **Blink-Hello-World** directory of the repository: +```bash +idf.py menuconfig +``` +{{< img "hello_world-menuconfig.webp" "Configuring Core2 for AWS IoT EduKit with idf.py menuconfig" >}} +Here you will set the configuration. Use the direction keys on your keyboard to go to **Component config** --> **Amazon Web Services IoT Platform** and open **() AWS IoT Endpoint Hostname** to set the string. You can paste the address you copied moments ago into the box and hit `enter` to set that symbol. Next, go back to the configuration home screen by pressing the `ESC` key *twice*. Then select `AWS IoT EduKit Configuration` from the menu. Set your **() WiFi SSID** and **() WiFi Password** with your Wi-FI credentials. Once you are finished, press the `s` button on your keyboard to save, confirm the location of the file by hitting `enter`, followed by `q` to quit. + +{{% notice warning %}} +Be sure your SSID is for a 2.4GHz network. The ESP32-D0WD on the M5Stack Core2 for AWS hardware does not support 5GHz Wi-Fi bands. +{{% /notice %}} + +## Building the ESP32 Firmware +Building the firmware is easy, but can take quite a while the first time. ESP-IDF uses CMAKE as it's build system and will link all the files necessary and begin compiling the code to a flashable binary in ELF format that can be flashed on to the device. +```bash +idf.py build +``` + +## Erasing Old Firmware +If you completed the Getting Started or Alexa demo, it is recommended to erase the flash memory of your device. The flash is split into separate partitions where the application data, OTA data, and keys are stored. The other walk-throughs use a different partition table than used for this tutorial and could cause issues if not wiped. Replace **<>** with the serial port your Core2 for AWS IoT EduKit device is connected to: +```bash +idf.py erase_flash -p <> +``` + +## Flashing the ESP32 Firmware and monitoring the device output over the serial port +To flash the firmware you just built and view all the outputs by the device over the serial connection, you're going to daisy chain commands and enter: +```bash +idf.py flash monitor -p <> +``` +Once the device is flashed, it will reboot and run the application. You should see the device connect to your Wi-Fi network, establish a secure MQTT connection to AWS IoT Core, subscribe to the preset MQTT topic, and begin sending messages. + +## Chapter conclusion +In this chapter, you have successfully compiled and flash your device and are actively monitoring it's serial outputs. Using the AWS IoT Device SDK for Embedded C, the reference hardware authenticated with the MQTT message broker (AWS IoT Core) and is ready to receive messages. + +You are now ready to head to the final chapter in this tutorial, [**Blinking the LED**](/en_uk/blinky-hello-world/blinking-the-leds.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Blinky hello world/Connecting to AWS/hello_world-menuconfig.webp b/workshop/content/Blinky hello world/Connecting to AWS/hello_world-menuconfig.webp new file mode 100644 index 0000000..83d69b0 Binary files /dev/null and b/workshop/content/Blinky hello world/Connecting to AWS/hello_world-menuconfig.webp differ diff --git a/workshop/content/Blinky hello world/Device provisioning/_index.en.md b/workshop/content/Blinky hello world/Device provisioning/_index.en.md new file mode 100644 index 0000000..1356eb0 --- /dev/null +++ b/workshop/content/Blinky hello world/Device provisioning/_index.en.md @@ -0,0 +1,41 @@ ++++ +title = "Device Provisioning" +weight = 20 +pre = "b. " ++++ + +In this chapter, you'll provision the device for connectivity to AWS IoT Core using the on-board Microchip ATTECC608 Trust&GO secure element to establish a TLS connection. The built-in hardware root of trust allows you to have a simplified and expedited provisioning path while never exposing the private key. You can retrieve the device certificate that is built into the device and create a manifest file to create a AWS IoT thing (a representation and record of your device). This device's client Id will be registered and identified in AWS IoT Core by the secure element serial number. You can use similar processes to automate the fleet deployment of thousands or millions of devices at a time. + +## Identifying the serial port on host machine +Please reference [Espressif's offical doc for establishing serial connections with the ESP32](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/establish-serial-connection.html). The port of your device will vary based on your OS. For macOS, the device is typically on `/dev/cu.SLAB_USBtoUART`. For Linux, the device is typically on `/dev/ttyUSB0` (user needs to be added to dialout group). For Windows, it will start with a `COM` and end with a number. + +## Retrieving the Device Certificate and Registering your AWS IoT thing +We have simplified the process of retrieving the device certificate from the Core2 for AWS IoT EduKit reference hardware's secure element, generating a device manifest by signing the device certificate with a x.509 certificate (includes your AWS IoT registration code), registering the device in AWS IoT with the device certificate, and attaching a secure policy to the AWS IoT thing. + +Go into the project's AWS IoT registration helper directory and install necessary dependencies with pip: +```bash +cd Core2-for-AWS-IoT-EduKit/Blinky-Hello-World/utilities/AWS_IoT_registration_helper/ +pip3 install -r requirements.txt +``` + +Next, you'll need to run the Python script that executes all the steps for registering the device to your AWS account. Be sure to first replace **<>** with the serial port your Core2 for AWS IoT EduKit device is connected to: +```bash +python registration_helper.py -p <> +``` + +{{% notice info %}} +If you close your shell or open a new shell, you'll need to re-enter `conda activate edukit` to reactivate the virtual environment and run ESP-IDF's `export.sh` (macOS/Linux) or `export.bat` (Windows) to re-add the ESP-IDF tools to your path each time. +{{% /notice %}} + +With the device successfully registered and provisioned in AWS IoT, go back to the **Blinky-Hello-World** directory by entering: +```bash +cd ../.. +``` + +## Chapter Conclusion +In this chapter, you used the secure element to create an AWS IoT [thing](https://docs.aws.amazon.com/iot/latest/developerguide/thing-registry.html), set up a permissions [policy](https://docs.aws.amazon.com/iot/latest/developerguide/thing-policy-variables.html) for your thing, and attached the device certificate to it. All of this was done without ever exposing the secret private key and removing a potential avenue for it to be compromised. + +On to [**Connecting to AWS IoT Core**](connecting-to-aws.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Blinky hello world/Device provisioning/_index.en_uk.md b/workshop/content/Blinky hello world/Device provisioning/_index.en_uk.md new file mode 100644 index 0000000..1356eb0 --- /dev/null +++ b/workshop/content/Blinky hello world/Device provisioning/_index.en_uk.md @@ -0,0 +1,41 @@ ++++ +title = "Device Provisioning" +weight = 20 +pre = "b. " ++++ + +In this chapter, you'll provision the device for connectivity to AWS IoT Core using the on-board Microchip ATTECC608 Trust&GO secure element to establish a TLS connection. The built-in hardware root of trust allows you to have a simplified and expedited provisioning path while never exposing the private key. You can retrieve the device certificate that is built into the device and create a manifest file to create a AWS IoT thing (a representation and record of your device). This device's client Id will be registered and identified in AWS IoT Core by the secure element serial number. You can use similar processes to automate the fleet deployment of thousands or millions of devices at a time. + +## Identifying the serial port on host machine +Please reference [Espressif's offical doc for establishing serial connections with the ESP32](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/establish-serial-connection.html). The port of your device will vary based on your OS. For macOS, the device is typically on `/dev/cu.SLAB_USBtoUART`. For Linux, the device is typically on `/dev/ttyUSB0` (user needs to be added to dialout group). For Windows, it will start with a `COM` and end with a number. + +## Retrieving the Device Certificate and Registering your AWS IoT thing +We have simplified the process of retrieving the device certificate from the Core2 for AWS IoT EduKit reference hardware's secure element, generating a device manifest by signing the device certificate with a x.509 certificate (includes your AWS IoT registration code), registering the device in AWS IoT with the device certificate, and attaching a secure policy to the AWS IoT thing. + +Go into the project's AWS IoT registration helper directory and install necessary dependencies with pip: +```bash +cd Core2-for-AWS-IoT-EduKit/Blinky-Hello-World/utilities/AWS_IoT_registration_helper/ +pip3 install -r requirements.txt +``` + +Next, you'll need to run the Python script that executes all the steps for registering the device to your AWS account. Be sure to first replace **<>** with the serial port your Core2 for AWS IoT EduKit device is connected to: +```bash +python registration_helper.py -p <> +``` + +{{% notice info %}} +If you close your shell or open a new shell, you'll need to re-enter `conda activate edukit` to reactivate the virtual environment and run ESP-IDF's `export.sh` (macOS/Linux) or `export.bat` (Windows) to re-add the ESP-IDF tools to your path each time. +{{% /notice %}} + +With the device successfully registered and provisioned in AWS IoT, go back to the **Blinky-Hello-World** directory by entering: +```bash +cd ../.. +``` + +## Chapter Conclusion +In this chapter, you used the secure element to create an AWS IoT [thing](https://docs.aws.amazon.com/iot/latest/developerguide/thing-registry.html), set up a permissions [policy](https://docs.aws.amazon.com/iot/latest/developerguide/thing-policy-variables.html) for your thing, and attached the device certificate to it. All of this was done without ever exposing the secret private key and removing a potential avenue for it to be compromised. + +On to [**Connecting to AWS IoT Core**](connecting-to-aws.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Blinky hello world/Device provisioning/notebook-configure.png b/workshop/content/Blinky hello world/Device provisioning/notebook-configure.png new file mode 100644 index 0000000..b85a98c Binary files /dev/null and b/workshop/content/Blinky hello world/Device provisioning/notebook-configure.png differ diff --git a/workshop/content/Blinky hello world/Device provisioning/notebook-restart-run.png b/workshop/content/Blinky hello world/Device provisioning/notebook-restart-run.png new file mode 100644 index 0000000..502c993 Binary files /dev/null and b/workshop/content/Blinky hello world/Device provisioning/notebook-restart-run.png differ diff --git a/workshop/content/Blinky hello world/Prerequisites/_index.en.md b/workshop/content/Blinky hello world/Prerequisites/_index.en.md new file mode 100644 index 0000000..d1dd695 --- /dev/null +++ b/workshop/content/Blinky hello world/Prerequisites/_index.en.md @@ -0,0 +1,188 @@ ++++ +title = "Prerequisites" +weight = 10 +pre = "a. " ++++ + +In this chapter, you'll install required software for this and all subsequent tutorials. Starting with the SiLabs CP210x drivers to communicate with the Core 2 for AWS IoT EduKit over USB, then the ESP-IDF toolchain for the on-board ESP32-D0WD microcontroller unit (MCU). You will also install Miniconda to manage your Python versions and your dependencies in isolated virtual environments to avoid conflicts. Additionally, we will download, install, and configure the AWS command line interface (CLI). This tutorial assumes that you have an [AWS account](https://signin.aws.amazon.com/signin). + +## Silicon Labs USB to UART bridge driver installation +Download and install the SiLabs CP210x drivers to enable your computer to communicate with the Core2 for AWS IoT EduKit device. The on-board CP2104 is an USB-to-UART bridge to facilitate host communication with the ESP32-D0WD microcontroller: +{{%expand "macOS 10.9+" %}} +Since OS X Mavericks, Apple has included the necessary USB to serial drivers and no other steps are necessary. To verify that the drivers are installed, loaded, and the device is read for programming, connect the device via the provided USB-C cable and run: +```bash +ls -l /dev/cu.S* +``` +If your host machine recognizes and is able to communicate with the device, you will see a return value of `/dev/cu.SLAB_USBtoUART` printed. If the results of the command above returns empty, check the physical connection first. If it doesn't resolve the issue, follow the instructions for macOS <= 10.8. +{{% notice info %}} +On macOS 10.13 and later, the installation of the SiLabs system extension may be blocked. To unblock, open your Mac's **System Preferences** **Security & Privacy** pane, unlock changes by clicking the , **allow** the developer, and then relock by clicking the . For more information, see [Apple Technical Note TN2459](https://developer.apple.com/library/archive/technotes/tn2459/_index.html). +{{% /notice %}} +{{% /expand%}} +{{%expand "macOS <= 10.8" %}} +1) Disconnect the USB-C cable connecting your Core2 for AWS IoT EduKit device to your computer if it's already connected. +2) Download and extract the [CP210x VCP macOS driver](https://www.silabs.com/documents/public/software/Mac_OSX_VCP_Driver.zip). +3) Expand the **SiLabsUSBDriverDisk.dmg** file from within the extracted folder. +4) Open the **Install CP210x VCP Driver** application and run through the installer. +5) Reboot your computer and reconnect your Core2 for AWS IoT EduKit device to your computer via the provided USB-C cable. +{{% /expand%}} +{{%expand "Linux" %}} +Linux kernel version 3.x.x and 4.x.x already include the drivers as part of the distribution. To verify they are installed and loaded, run the command in your terminal: +```bash +modinfo usbserial +modinfo cp210x +``` +If they are *not* loaded, run the command: +```bash +sudo modprobe usbserial +sudo modprobe cp210x +``` +{{% /expand%}} +{{%expand "Windows (64-bit)" %}} +1) Disconnect the USB-C cable connecting your Core2 for AWS IoT EduKit device to your computer if it's already connected. +2) Download and extract the [CP210x Universal Windows driver](https://www.silabs.com/documents/public/software/CP210x_Universal_Windows_Driver.zip). +3) Open **CP210xVCPInstaller_x64.exe** and run through the installer to install the drivers. +4) Reboot your computer and reconnect your Core2 for AWS IoT EduKit device to your computer via the provided USB-C cable. +{{% notice warning %}} +The tutorials were only tested on the 64-bit version of Windows 10. We do not support any other configuration of the Windows operating system. +{{% /notice %}} +{{% /expand%}} + +## Installing ESP-IDF v4.2 +The **M5Stack Core2 for AWS** reference hardware you're using is equipped with an Espressif ESP32-D0WD microcontroller unit (MCU). The ESP-IDF (Espressif IoT Development Framework) allows you to configure, build, and flash firmware onto an ESP32 board. In the Getting Started tutorial, you used PlatformIO to simplify compilation and flashing of the firmware on the device—which provides a GUI to the behind-the-scene calls to ESP-IDF version 4.1 and other tools. For the subsequent steps, you will use ESP-IDF *version 4.2* due to enhancements to the mbedTLS library and improved integration of the secure element. + +{{%expand "macOS" %}} +Copy and paste the command block below to: +1) Install Homebrew if you do not have it yet. +2) Install cmake, ninja, and the DFU utility using Homebrew. +3) Clone the ESP-IDF into the `$HOME/esp/` directory (the full ESP-IDF path will be **$HOME/esp/esp-idf**). +4) Install the ESP-IDF dependencies. +5) Check your installation and export the ESP-IDF environmental variables to your **$PATH**. + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +brew install cmake ninja dfu-util +mkdir $HOME/esp +cd $HOME/esp +git clone -b release/v4.2 --recursive https://github.com/espressif/esp-idf.git +cd $HOME/esp/esp-idf +. $HOME/esp/esp-idf/install.sh +. $HOME/esp/esp-idf/export.sh +``` +{{% /expand%}} +{{%expand "Linux" %}} +1) Install all your dependencies. + - CentOS 7: + ```bash + sudo yum install git wget flex bison gperf cmake ninja-build ccache dfu-util + ``` + - Ubuntu and Debian: + ```bash + sudo apt-get install git wget flex bison gperf python-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util + ``` + - Arch: + ```bash + sudo pacman -S --needed gcc git make flex bison gperf cmake ninja ccache dfu-util + ``` +2) Clone the ESP-IDF into the `$HOME/esp/` directory (the full ESP-IDF path is $HOME/esp/esp-idf). +3) Install the ESP-IDF dependencies. +4) Check your installation and export the ESP-IDF environmental variables to your **$PATH**. + +```bash +mkdir $HOME/esp + +cd $HOME/esp + +git clone -b release/v4.2 --recursive https://github.com/espressif/esp-idf.git + +cd $HOME/esp/esp-idf + +. $HOME/esp/esp-idf/install.sh + +. $HOME/esp/esp-idf/export.sh +``` +{{% /expand%}} +{{%expand "Windows (64-bit)" %}} +1) Download and run the [ESP-IDF Tools Installer](https://dl.espressif.com/dl/esp-idf-tools-setup-2.3.exe) to install **ESP-IDF v4.2** and all other dependencies. +2) In the **Python choice** stage of the installer, select **Install Python 3.7**. If Python 3.7 is already installed, you might not have the install option—select your installed Python 3.7 (do *not* use the Python installed with Anaconda/miniconda if that is an available option). +3) In the **Download ESP-IDF** stage of the installer to download, select **release/v4.2 (release branch)**. +4) Reboot your machine if you haven't done so yet after the installer ran. +5) `cd %userprofile%\Desktop\esp-idf` +6) Run the ESP-IDF installation script to ensure everything is set correctly: `install.bat` +7) Run the export script add ESP-IDF tools to your path: `export.bat` +{{% /expand%}} + +## Installing and configuring the AWS CLI version 2 +### AWS CLI Installation +The AWS Command Line Interface (CLI) is a unified tool to manage your AWS services. With just one tool to download and configure, you can control multiple AWS services from the command line and automate them through scripts. To be able to configure the AWS CLI, you'll first need to have an AWS account. Please [signin](https://signin.aws.amazon.com/signin) or [create an account](https://portal.aws.amazon.com/billing/signup#/start) first before proceeding. After you've signed in follow the official [AWS CLI installation instructions for your OS](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html). + +### AWS CLI Configuration +Once you have that installed, it is time to set it for your account and region. It's important to keep in mind that the region you're currently using stays consistent—for purposes of this tutorial, we are standardizing on **us-west-2**. Changing regions can cause other challenges in subsequent steps. To view regional service availability, reference the [AWS Regional Services List](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) for a list of services that are available. + +Use the command: +```bash +aws configure +``` +For steps on obtaining the credentials for the AWS CLI, view the [quick configuration](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config) documentation. To get the AWS access key Id and AWS secret access key, you will need to give necessary [permissions to your IAM user](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config). Your inputs should look like the following: +``` +AWS Access Key ID [None]: EXAMPLEKEYIDEXAMPLE +AWS Secret Access Key [None]: EXAMPLEtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +Default region name [None]: us-west-2 +Default output format [None]: json +``` + +## Miniconda setup and installation +Miniconda is a bootstrap version of the full-fledge Anaconda—which simplifies package and Python virtual environment management. Many installation errors are due to incompatible Python versions (e.g. Python 2.x or 3.8.x), the python PATH variable being set to the wrong Python version, or dependency conflicts. It is *strongly* encouraged to always use Conda to handle your Python development environment. + +Go to https://docs.conda.io/en/latest/miniconda.html and download the corresponding installer for your OS. Follow the official Miniconda installation instructions and then return to this page. + +{{%expand "macOS and Linux" %}} +1) After miniconda is installed, quit and then reopen your terminal application so that the source files are added and your PATH includes the environment variables you'll need. +2) Creating a new conda environment to isolate your work and avoid dependency conflicts. After running the command below, enter **y** to install the necessary dependencies for the environment. + ```bash + conda create -n edukit python=3.7 + ``` +3) After successfully creating the new environment, you'll switch out of the *base* conda environment and into the newly created environment. You'll know you're activated because you'll see **(edukit)** at the beginning of the current prompt. + ```bash + conda activate edukit + ``` +4) You'll run the ESP-IDF toolchain export script to add the ESP tools to your path. + ```bash + . $HOME/esp/esp-idf/export.sh + ``` +{{% /expand%}} +{{%expand "Windows (64-bit)" %}} +1) After the miniconda installer has finished, reboot your machine. +2) Open Anaconda Prompt by going to **start menu** → **Anaconda prompt**. +3) Creating a new conda environment to isolate your work and avoid dependency conflicts. After running the command below, enter **y** to install the necessary dependencies for the environment. + ```bash + conda create -n edukit python=3.7 + ``` +4) After successfully creating the new environment, you'll switch out of the *base* conda environment and into the newly created environment. You'll know you're activated because you'll see **(edukit)** at the beginning of the current prompt. + ```bash + conda activate edukit + ``` +5) Run the export script add ESP-IDF tools to your path: + ```bash + %userprofile%\Desktop\esp-idf\export.bat + ``` +{{% /expand%}} + +{{% notice note %}} +If you close your shell or open a new shell, you'll need to re-enter `conda activate edukit` to reactivate the virtual environment and run ESP-IDF's `export.sh` (macOS/Linux) or `export.bat` (Windows) to re-add the ESP-IDF tools to your path each time. +{{% /notice %}} + +## Cloning the code repository +If you have misplaced the code repository from the [Getting Started Prerequisites](/en/getting-started/prerequisites.html) instructions, you can clone the repository hosted on [GitHub](https://github.com/m5stack/Core2-for-AWS-IoT-EduKit), or download and extract the [zip file](https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/archive/master.zip). To clone: +```bash +git clone https://github.com/m5stack/Core2-for-AWS-IoT-EduKit.git +``` +{{%expand "Windows" %}} +{{% notice info %}} +Windows has file path length limitations that can cause errors depending on where the project is placed. It is recommended that you clone this repo or extract the zip with a shorter initial path length if this error should occur (e.g. **C:\\**). +{{% /notice %}} +{{% /expand%}} + +With everything installed and configured, let's move to the next chapter, [**Device Provisioning**](device-provisioning.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Blinky hello world/Prerequisites/_index.en_uk.md b/workshop/content/Blinky hello world/Prerequisites/_index.en_uk.md new file mode 100644 index 0000000..a37480e --- /dev/null +++ b/workshop/content/Blinky hello world/Prerequisites/_index.en_uk.md @@ -0,0 +1,188 @@ ++++ +title = "Prerequisites" +weight = 10 +pre = "a. " ++++ + +In this chapter, you'll install required software for this and all subsequent tutorials. Starting with the SiLabs CP210x drivers to communicate with the Core 2 for AWS IoT EduKit over USB, then the ESP-IDF toolchain for the on-board ESP32-D0WD microcontroller unit (MCU). You will also install Miniconda to manage your Python versions and your dependencies in isolated virtual environments to avoid conflicts. Additionally, we will download, install, and configure the AWS command line interface (CLI). This tutorial assumes that you have an [AWS account](https://signin.aws.amazon.com/signin). + +## Silicon Labs USB to UART bridge driver installation +Download and install the SiLabs CP210x drivers to enable your computer to communicate with the Core2 for AWS IoT EduKit device. The on-board CP2104 is an USB-to-UART bridge to facilitate host communication with the ESP32-D0WD microcontroller: +{{%expand "macOS 10.9+" %}} +Since OS X Mavericks, Apple has included the necessary USB to serial drivers and no other steps are necessary. To verify that the drivers are installed, loaded, and the device is read for programming, connect the device via the provided USB-C cable and run: +```bash +ls -l /dev/cu.S* +``` +If your host machine recognizes and is able to communicate with the device, you will see a return value of `/dev/cu.SLAB_USBtoUART` printed. If the results of the command above returns empty, check the physical connection first. If it doesn't resolve the issue, follow the instructions for macOS <= 10.8. +{{% notice info %}} +On macOS 10.13 and later, the installation of the SiLabs system extension may be blocked. To unblock, open your Mac's **System Preferences** **Security & Privacy** pane, unlock changes by clicking the , **allow** the developer, and then relock by clicking the . For more information, see [Apple Technical Note TN2459](https://developer.apple.com/library/archive/technotes/tn2459/_index.html). +{{% /notice %}} +{{% /expand%}} +{{%expand "macOS <= 10.8" %}} +1) Disconnect the USB-C cable connecting your Core2 for AWS IoT EduKit device to your computer if it's already connected. +2) Download and extract the [CP210x VCP macOS driver](https://www.silabs.com/documents/public/software/Mac_OSX_VCP_Driver.zip). +3) Expand the **SiLabsUSBDriverDisk.dmg** file from within the extracted folder. +4) Open the **Install CP210x VCP Driver** application and run through the installer. +5) Reboot your computer and reconnect your Core2 for AWS IoT EduKit device to your computer via the provided USB-C cable. +{{% /expand%}} +{{%expand "Linux" %}} +Linux kernel version 3.x.x and 4.x.x already include the drivers as part of the distribution. To verify they are installed and loaded, run the command in your terminal: +```bash +modinfo usbserial +modinfo cp210x +``` +If they are *not* loaded, run the command: +```bash +sudo modprobe usbserial +sudo modprobe cp210x +``` +{{% /expand%}} +{{%expand "Windows (64-bit)" %}} +1) Disconnect the USB-C cable connecting your Core2 for AWS IoT EduKit device to your computer if it's already connected. +2) Download and extract the [CP210x Universal Windows driver](https://www.silabs.com/documents/public/software/CP210x_Universal_Windows_Driver.zip). +3) Open **CP210xVCPInstaller_x64.exe** and run through the installer to install the drivers. +4) Reboot your computer and reconnect your Core2 for AWS IoT EduKit device to your computer via the provided USB-C cable. +{{% notice warning %}} +The tutorials were only tested on the 64-bit version of Windows 10. We do not support any other configuration of the Windows operating system. +{{% /notice %}} +{{% /expand%}} + +## Installing ESP-IDF v4.2 +The **M5Stack Core2 for AWS** reference hardware you're using is equipped with an Espressif ESP32-D0WD microcontroller unit (MCU). The ESP-IDF (Espressif IoT Development Framework) allows you to configure, build, and flash firmware onto an ESP32 board. In the Getting Started tutorial, you used PlatformIO to simplify compilation and flashing of the firmware on the device—which provides a GUI to the behind-the-scene calls to ESP-IDF version 4.1 and other tools. For the subsequent steps, you will use ESP-IDF *version 4.2* due to enhancements to the mbedTLS library and improved integration of the secure element. + +{{%expand "macOS" %}} +Copy and paste the command block below to: +1) Install Homebrew if you do not have it yet. +2) Install cmake, ninja, and the DFU utility. +3) Clone the ESP-IDF into the `$HOME/esp/` directory (the full ESP-IDF path is **$HOME/esp/esp-idf**). +4) Install the ESP-IDF dependencies. +5) Check your installation and export the ESP-IDF environmental variables to your **$PATH**. + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +brew install cmake ninja dfu-util +mkdir $HOME/esp +cd $HOME/esp +git clone -b release/v4.2 --recursive https://github.com/espressif/esp-idf.git +cd $HOME/esp/esp-idf +. $HOME/esp/esp-idf/install.sh +. $HOME/esp/esp-idf/export.sh +``` +{{% /expand%}} +{{%expand "Linux" %}} +1) Install all your dependencies. + - CentOS 7: + ```bash + sudo yum install git wget flex bison gperf cmake ninja-build ccache dfu-util + ``` + - Ubuntu and Debian: + ```bash + sudo apt-get install git wget flex bison gperf python-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util + ``` + - Arch: + ```bash + sudo pacman -S --needed gcc git make flex bison gperf cmake ninja ccache dfu-util + ``` +2) Clone the ESP-IDF into the `$HOME/esp/` directory (the full ESP-IDF path is $HOME/esp/esp-idf). +3) Install the ESP-IDF dependencies. +4) Check your installation and export the ESP-IDF environmental variables to your **$PATH**. + +```bash +mkdir $HOME/esp + +cd $HOME/esp + +git clone -b release/v4.2 --recursive https://github.com/espressif/esp-idf.git + +cd $HOME/esp/esp-idf + +. $HOME/esp/esp-idf/install.sh + +. $HOME/esp/esp-idf/export.sh +``` +{{% /expand%}} +{{%expand "Windows (64-bit)" %}} +1) Download and run the [ESP-IDF Tools Installer](https://dl.espressif.com/dl/esp-idf-tools-setup-2.3.exe) to install **ESP-IDF v4.2** and all other dependencies. +2) In the **Python choice** stage of the installer, select **Install Python 3.7**. If Python 3.7 is already installed, you might not have the install option—select your installed Python 3.7 (do *not* use the Python installed with Anaconda/miniconda if that is an available option). +3) In the **Download ESP-IDF** stage of the installer to download, select **release/v4.2 (release branch)**. +4) Reboot your machine if you haven't done so yet after the installer ran. +5) `cd %userprofile%\Desktop\esp-idf` +6) Run the ESP-IDF installation script to ensure everything is set correctly: `install.bat` +7) Run the export script add ESP-IDF tools to your path: `export.bat` +{{% /expand%}} + +## Installing and configuring the AWS CLI version 2 +### AWS CLI Installation +The AWS Command Line Interface (CLI) is a unified tool to manage your AWS services. With just one tool to download and configure, you can control multiple AWS services from the command line and automate them through scripts. To be able to configure the AWS CLI, you'll first need to have an AWS account. Please [signin](https://signin.aws.amazon.com/signin) or [create an account](https://portal.aws.amazon.com/billing/signup#/start) first before proceeding. After you've signed in follow the official [AWS CLI installation instructions for your OS](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html). + +### AWS CLI Configuration +Once you have that installed, it is time to set it for your account and region. It's important to keep in mind that the region you're currently using stays consistent—for purposes of this tutorial, we are standardizing on **us-west-2**. Changing regions can cause other challenges in subsequent steps. To view regional service availability, reference the [AWS Regional Services List](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) for a list of services that are available. + +Use the command: +```bash +aws configure +``` +For steps on obtaining the credentials for the AWS CLI, view the [quick configuration](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config) documentation. To get the AWS access key Id and AWS secret access key, you will need to give necessary [permissions to your IAM user](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config). Your inputs should look like the following: +``` +AWS Access Key ID [None]: EXAMPLEKEYIDEXAMPLE +AWS Secret Access Key [None]: EXAMPLEtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +Default region name [None]: us-west-2 +Default output format [None]: json +``` + +## Miniconda setup and installation +Miniconda is a bootstrap version of the full-fledge Anaconda—which simplifies package and Python virtual environment management. Many installation errors are due to incompatible Python versions (e.g. Python 2.x), the python PATH variable being set to the wrong Python version, or dependency conflicts. It is *strongly* encouraged to always use Conda to handle your Python development environment. + +Go to https://docs.conda.io/en/latest/miniconda.html and download the corresponding installer for your OS. Follow the official Miniconda installation instructions and then return to this page. + +{{%expand "macOS and Linux" %}} +1) After miniconda is installed, quit and then reopen your terminal application so that the source files are added and your PATH includes the environment variables you'll need. +2) Creating a new conda environment to isolate your work and avoid dependency conflicts. After running the command below, enter **y** to install the necessary dependencies for the environment: + ```bash + conda create -n edukit python=3.7 + ``` +3) After successfully creating the new environment, you'll switch out of the *base* conda environment and into the newly created environment. You'll know you're activated because you'll see **(edukit)** at the beginning of the current prompt: + ```bash + conda activate edukit + ``` +4) You'll run the ESP-IDF toolchain export script to add the ESP tools to your path: + ```bash + . $HOME/esp/esp-idf/export.sh + ``` +{{% /expand%}} +{{%expand "Windows (64-bit)" %}} +1) After the miniconda installer has finished, reboot your machine. +2) Open Anaconda Prompt by going to **start menu** → **Anaconda prompt**. +3) Creating a new conda environment to isolate your work and avoid dependency conflicts. After running the command below, enter **y** to install the necessary dependencies for the environment: + ```bash + conda create -n edukit python=3.7 + ``` +4) After successfully creating the new environment, you'll switch out of the *base* conda environment and into the newly created environment. You'll know you're activated because you'll see **(edukit)** at the beginning of the current prompt: + ```bash + conda activate edukit + ``` +5) Run the export script add ESP-IDF tools to your path: + ```bash + %userprofile%\Desktop\esp-idf\export.bat + ``` +{{% /expand%}} + +{{% notice note %}} +If you close your shell or open a new shell, you'll need to re-enter `conda activate edukit` to reactivate the virtual environment and run ESP-IDF's `export.sh` (macOS/Linux) or `export.bat` (Windows) to re-add the ESP-IDF tools to your path each time. +{{% /notice %}} + +## Cloning the code repository +If you have misplaced the code repository from the [Getting Started Prerequisites](/en_uk/getting-started/prerequisites.html) instructions, you can clone the repository hosted on [GitHub](https://github.com/m5stack/Core2-for-AWS-IoT-EduKit), or download and extract the [zip file](https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/archive/master.zip). To clone: +```bash +git clone https://github.com/m5stack/Core2-for-AWS-IoT-EduKit.git +``` +{{%expand "Windows" %}} +{{% notice info %}} +Windows has file path length limitations that can cause errors depending on where the project is placed. It is recommended that you clone this repo or extract the zip with a shorter initial path length if this error should occur (e.g. **C:\\**). +{{% /notice %}} +{{% /expand%}} + +With everything installed and configured, let's move to the next chapter, [**Device Provisioning**](device-provisioning.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Blinky hello world/_index.en.md b/workshop/content/Blinky hello world/_index.en.md new file mode 100644 index 0000000..91242d9 --- /dev/null +++ b/workshop/content/Blinky hello world/_index.en.md @@ -0,0 +1,20 @@ ++++ +title = "Blinky Hello World" +chapter = true +weight = 20 +pre = "2. " ++++ + +Learn how to create a "blinky" (the hello world for microcontrollers) application with your M5Stack Core2 for AWS IoT EduKit reference hardware. Using your own AWS account, you'll walk-through how to connect to AWS IoT Core, send and receive messages, and view the messages on both the device and in AWS cloud. All of the steps and skills used here will provide the foundation to be successful in subsequent tutorials. Specifically, you will: +- Install and configure additional necessary software. +- Register a "thing" in AWS IoT Core using the security certificates pre-provisioned on the onboard secure element. +- Connect and send MQTT messages from the reference hardware to AWS IoT Core using the ESP-IDF (FreeRTOS kernel with symmetric multiprocessing). +- Receive an MQTT message on the reference to trigger blinking an LED. + +All the content in this tutorial assumes you presently have the [M5Stack Core2 ESP32 IoT Development Kit for AWS IoT EduKit](https://www.amazon.com/dp/B08NP5LVFH) in your possession, an [AWS account](https://signin.aws.amazon.com/signin) that isn't running production workloads, and are comfortable with basic technical concepts and tools—such as the command prompt/terminal. To purchase your own kit, check out [Amazon.com](https://www.amazon.com/dp/B08NP5LVFH), the [M5Stack store](https://m5stack.com/products/m5stack-core2-esp32-iot-development-kit-for-aws-iot-edukit) or their global distributors. If you haven't registered for an AWS account yet, please [create an account](https://portal.aws.amazon.com/billing/signup). + + +To get started with this tutorial, go to the first chapter, [**Prerequisites**](/en/blinky-hello-world/prerequisites.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Blinky hello world/_index.en_uk.md b/workshop/content/Blinky hello world/_index.en_uk.md new file mode 100644 index 0000000..91ab048 --- /dev/null +++ b/workshop/content/Blinky hello world/_index.en_uk.md @@ -0,0 +1,20 @@ ++++ +title = "Blinky Hello World" +chapter = true +weight = 20 +pre = "2. " ++++ + +Learn how to create a "blinky" (the hello world for microcontrollers) application with your M5Stack Core2 for AWS IoT EduKit reference hardware. Using your own AWS account, you'll walk-through how to connect to AWS IoT Core, send and receive messages, and view the messages on both the device and in AWS cloud. All of the steps and skills used here will provide the foundation to be successful in subsequent tutorials. Specifically, you will: +- Install and configure additional necessary software. +- Register a "thing" in AWS IoT Core using the security certificates pre-provisioned on the onboard secure element. +- Connect and send MQTT messages from the reference hardware to AWS IoT Core using the ESP-IDF (FreeRTOS kernel with symmetric multiprocessing). +- Receive an MQTT message on the reference to trigger blinking an LED. + +All the content in this tutorial assumes you presently have the [M5Stack Core2 ESP32 IoT Development Kit for AWS IoT EduKit](https://www.amazon.com/dp/B08NP5LVFH) in your possession, an [AWS account](https://signin.aws.amazon.com/signin) that isn't running production workloads, and are comfortable with basic technical concepts and tools—such as the command prompt/terminal. To purchase your own kit, check out [Amazon.com](https://www.amazon.com/dp/B08NP5LVFH), the [M5Stack store](https://m5stack.com/products/m5stack-core2-esp32-iot-development-kit-for-aws-iot-edukit) or their global distributors. If you haven't registered for an AWS account yet, please [create an account](https://portal.aws.amazon.com/billing/signup). + + +To get started with this tutorial, go to the first chapter, [**Prerequisites**](/en_uk/blinky-hello-world/prerequisites.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Getting started/Prerequisites/_index.en.md b/workshop/content/Getting started/Prerequisites/_index.en.md new file mode 100644 index 0000000..f215b2e --- /dev/null +++ b/workshop/content/Getting started/Prerequisites/_index.en.md @@ -0,0 +1,87 @@ ++++ +title = "Prerequisites" +weight = 11 +pre = "a. " ++++ + +In this chapter, we download and install the software that you will need to view, edit, compile a binary firmware, and transfer the firmware to the device for it to run. Additionally, you'll be downloading an app on your phone to control the device. + +## Silicon Labs USB to UART bridge driver installation +Download and install the SiLabs CP210x drivers to enable your computer to communicate with the Core2 for AWS IoT EduKit device. The on-board CP2104 is an USB-to-UART bridge to facilitate host communication with the ESP32-D0WD microcontroller: +{{%expand "macOS 10.9+" %}} +Since OS X Mavericks, Apple has included the necessary USB to serial drivers and no other steps are necessary. To verify that the drivers are installed, loaded, and the device is read for programming, connect the device via the provided USB-C cable and run: +```bash +ls -l /dev/cu.S* +``` +If your host machine recognizes and is able to communicate with the device, you will see a return value of `/dev/cu.SLAB_USBtoUART` printed. If the results of the command above returns empty, check the physical connection first. If it doesn't resolve the issue, follow the instructions for macOS <= 10.8. +{{% notice info %}} +On macOS 10.13 and later, the installation of the SiLabs system extension may be blocked. To unblock, open your Mac's **System Preferences** **Security & Privacy** pane, unlock changes by clicking the , **allow** the developer, and then relock by clicking the . For more information, see [Apple Technical Note TN2459](https://developer.apple.com/library/archive/technotes/tn2459/_index.html). +{{% /notice %}} +{{% /expand%}} +{{%expand "macOS <= 10.8" %}} +1) Disconnect the USB-C cable connecting your Core2 for AWS IoT EduKit device to your computer if it's already connected. +2) Download and extract the [CP210x VCP macOS driver](https://www.silabs.com/documents/public/software/Mac_OSX_VCP_Driver.zip). +3) Expand the **SiLabsUSBDriverDisk.dmg** file from within the extracted folder. +4) Open the **Install CP210x VCP Driver** application and run through the installer. +5) Reboot your computer and reconnect your Core2 for AWS IoT EduKit device to your computer via the provided USB-C cable. +{{% /expand%}} +{{%expand "Linux" %}} +Linux kernel version 3.x.x and 4.x.x already include the drivers as part of the distribution. To verify they are installed and loaded, run the command in your terminal: +```bash +modinfo usbserial +modinfo cp210x +``` +If they are *not* loaded, run the command: +```bash +sudo modprobe usbserial +sudo modprobe cp210x +``` +{{% /expand%}} +{{%expand "Windows" %}} +1) Disconnect the USB-C cable connecting your Core2 for AWS IoT EduKit device to your computer if it's already connected. +2) Download and extract the [CP210x Universal Windows driver](https://www.silabs.com/documents/public/software/CP210x_Universal_Windows_Driver.zip). +3) Open **CP210xVCPInstaller_x64.exe** and run through the installer to install the drivers. +4) Reboot your computer and reconnect your Core2 for AWS IoT EduKit device to your computer via the provided USB-C cable. +{{% notice warning %}} +The tutorials were only tested on Windows 10 64-bit. We do not support any other configuration of the Windows operating system. +{{% /notice %}} +{{% /expand%}} + +## Visual Studio Code installation +Visual Studio Code is an open source integrated development environment (IDE) which allows you to view, edit, manage code and more. Download the latest [Visual Studio Code](https://code.visualstudio.com/) for your operating system. For troubleshooting issues with Visual Studio Code installation or usage, please refer to [their documentation](https://code.visualstudio.com/docs/setup/setup-overview). + +## Installing PlatformIO +[PlatformIO](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide) provides a professional embedded development platform which simplifies embedded software development. The Visual Studio Code extension provides the functionality of the Platform IO command line interface (CLI) in a graphical interface. You can download the extension and read more about PlatformIO [here](https://platformio.org/install/ide?install=vscode). + +{{%expand "Windows" %}} +{{% notice info %}} +If you already have Espressif IoT Development Framework (ESP-IDF) installed on your computer, you might see an error about missing files or unable to install dependencies such as the **esp-windows-curses**. This is due to conflicts. To resolve the issue at this time, please reference [this post](https://community.platformio.org/t/cant-create-esp-idf-project-correctly-in-platformio/16370/17) on the PlatformIO Community forum. +{{% /notice %}} +{{% /expand%}} + +## Downloading and installing the phone apps. +The ESP RainMaker Phone Apps are available for iOS and Android phones to provide Wi-Fi network configuration, user-creation, user-device association and device control. The apps can be found here: +* Android: [Google PlayStore](https://play.google.com/store/apps/details?id=com.espressif.rainmaker), [Direct APK](https://github.com/espressif/esp-rainmaker-android/releases) +* iOS: [Apple App Store](https://apps.apple.com/app/esp-rainmaker/id1497491540) + +If you do not posess a compatible Android or iOS device, you can use the [Rainmaker CLI](https://rainmaker.espressif.com/docs/cli-setup.html) and substitute instructions. + +## Downloading the code +You can either download the code you'll be using throughout this program by cloning the repository hosted on [GitHub](https://github.com/m5stack/Core2-for-AWS-IoT-EduKit), or download and extract the [zip file](https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/archive/master.zip). To clone: +```bash +git clone https://github.com/m5stack/Core2-for-AWS-IoT-EduKit.git +``` +{{%expand "Windows" %}} +{{% notice info %}} +Windows has file path length limitations that can cause errors depending on where the project is placed. It is recommended that you clone this repo or extract the zip with a shorter initial path length if this error should occur (e.g. **C:\\**). +{{% /notice %}} +{{% /expand%}} + +## Connecting and powering on the device +Lastly, let's make sure the device is plugged in to your computer and powered on. The device should automatically turn on once plugged in and connected to a power source, but if you need to turn it on, press the power button. +![How to turn M5Stack Core2 for AWS on or off](prerequisites/core2foraws_power_on_off.jpg?width=500px&classes=shadow) + +With the device ready, let's go to the next chapter — [**Running the ESP RainMaker Agent**](/en/getting-started/run-rainmaker.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Getting started/Prerequisites/_index.en_uk.md b/workshop/content/Getting started/Prerequisites/_index.en_uk.md new file mode 100644 index 0000000..1fcfe61 --- /dev/null +++ b/workshop/content/Getting started/Prerequisites/_index.en_uk.md @@ -0,0 +1,87 @@ ++++ +title = "Prerequisites" +weight = 11 +pre = "a. " ++++ + +In this chapter, we download and install the software that you will need to view, edit, compile a binary firmware, and transfer the firmware to the device for it to run. Additionally, you'll be downloading an app on your phone to control the device. + +## Silicon Labs USB to UART bridge driver installation +Download and install the SiLabs CP210x drivers to enable your computer to communicate with the Core2 for AWS IoT EduKit device. The on-board CP2104 is an USB-to-UART bridge to facilitate host communication with the ESP32-D0WD microcontroller: +{{%expand "macOS 10.9+" %}} +Since OS X Mavericks, Apple has included the necessary USB to serial drivers and no other steps are necessary. To verify that the drivers are installed, loaded, and the device is read for programming, connect the device via the provided USB-C cable and run: +```bash +ls -l /dev/cu.S* +``` +If your host machine recognizes and is able to communicate with the device, you will see a return value of `/dev/cu.SLAB_USBtoUART` printed. If the results of the command above returns empty, check the physical connection first. If it doesn't resolve the issue, follow the instructions for macOS <= 10.8. +{{% notice info %}} +On macOS 10.13 and later, the installation of the SiLabs system extension may be blocked. To unblock, open your Mac's **System Preferences** **Security & Privacy** pane, unlock changes by clicking the , **allow** the developer, and then relock by clicking the . For more information, see [Apple Technical Note TN2459](https://developer.apple.com/library/archive/technotes/tn2459/_index.html). +{{% /notice %}} +{{% /expand%}} +{{%expand "macOS <= 10.8" %}} +1) Disconnect the USB-C cable connecting your Core2 for AWS IoT EduKit device to your computer if it's already connected. +2) Download and extract the [CP210x VCP macOS driver](https://www.silabs.com/documents/public/software/Mac_OSX_VCP_Driver.zip). +3) Expand the **SiLabsUSBDriverDisk.dmg** file from within the extracted folder. +4) Open the **Install CP210x VCP Driver** application and run through the installer. +5) Reboot your computer and reconnect your Core2 for AWS IoT EduKit device to your computer via the provided USB-C cable. +{{% /expand%}} +{{%expand "Linux" %}} +Linux kernel version 3.x.x and 4.x.x already include the drivers as part of the distribution. To verify they are installed and loaded, run the command in your terminal: +```bash +modinfo usbserial +modinfo cp210x +``` +If they are *not* loaded, run the command: +```bash +sudo modprobe usbserial +sudo modprobe cp210x +``` +{{% /expand%}} +{{%expand "Windows" %}} +1) Disconnect the USB-C cable connecting your Core2 for AWS IoT EduKit device to your computer if it's already connected. +2) Download and extract the [CP210x Universal Windows driver](https://www.silabs.com/documents/public/software/CP210x_Universal_Windows_Driver.zip). +3) Open **CP210xVCPInstaller_x64.exe** and run through the installer to install the drivers. +4) Reboot your computer and reconnect your Core2 for AWS IoT EduKit device to your computer via the provided USB-C cable. +{{% notice warning %}} +The tutorials were only tested on Windows 10 64-bit. We do not support any other configuration of the Windows operating system. +{{% /notice %}} +{{% /expand%}} + +## Visual Studio Code installation +Visual Studio Code is an open source integrated development environment (IDE) which allows you to view, edit, manage code and more. Download the latest [Visual Studio Code](https://code.visualstudio.com/) for your operating system. For troubleshooting issues with Visual Studio Code installation or usage, please refer to [their documentation](https://code.visualstudio.com/docs/setup/setup-overview). + +## Installing PlatformIO +[PlatformIO](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide) provides a professional embedded development platform which simplifies embedded software development. The Visual Studio Code extension provides the functionality of the Platform IO command line interface (CLI) in a graphical interface. You can download the extension and read more about PlatformIO [here](https://platformio.org/install/ide?install=vscode). + +{{%expand "Windows" %}} +{{% notice info %}} +If you already have Espressif IoT Development Framework (ESP-IDF) installed on your computer, you might see an error about missing files or unable to install dependencies such as the **esp-windows-curses**. This is due to conflicts. To resolve the issue at this time, please reference [this post](https://community.platformio.org/t/cant-create-esp-idf-project-correctly-in-platformio/16370/17) on the PlatformIO Community forum. +{{% /notice %}} +{{% /expand%}} + +## Downloading and installing the phone apps. +The ESP RainMaker Phone Apps are available for iOS and Android phones to provide Wi-Fi network configuration, user-creation, user-device association and device control. The apps can be found here: +* Android: [Google PlayStore](https://play.google.com/store/apps/details?id=com.espressif.rainmaker), [Direct APK](https://github.com/espressif/esp-rainmaker-android/releases) +* iOS: [Apple App Store](https://apps.apple.com/app/esp-rainmaker/id1497491540) + +If you do not posess a compatible Android or iOS device, you can use the [Rainmaker CLI](https://rainmaker.espressif.com/docs/cli-setup.html) and substitute instructions. + +## Downloading the code +You can either download the code you'll be using throughout this program by cloning the repository hosted on [GitHub](https://github.com/m5stack/Core2-for-AWS-IoT-EduKit), or download and extract the [zip file](https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/archive/master.zip). To clone: +```bash +git clone https://github.com/m5stack/Core2-for-AWS-IoT-EduKit.git +``` +{{%expand "Windows" %}} +{{% notice info %}} +Windows has file path length limitations that can cause errors depending on where the project is placed. It is recommended that you clone this repo or extract the zip with a shorter initial path length if this error should occur (e.g. **C:\\**). +{{% /notice %}} +{{% /expand%}} + +## Connecting and powering on the device +Lastly, let's make sure the device is plugged in to your computer and powered on. The device should automatically turn on once plugged in and connected to a power source, but if you need to turn it on, press the power button. +![How to turn M5Stack Core2 for AWS on or off](prerequisites/core2foraws_power_on_off.jpg?width=500px&classes=shadow) + +With the device ready, let's go to the next chapter — [**Running the ESP RainMaker Agent**](/en_uk/getting-started/run-rainmaker.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Getting started/Prerequisites/appstore.svg b/workshop/content/Getting started/Prerequisites/appstore.svg new file mode 100644 index 0000000..8166253 --- /dev/null +++ b/workshop/content/Getting started/Prerequisites/appstore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/workshop/content/Getting started/Prerequisites/core2foraws_power_on_off.jpg b/workshop/content/Getting started/Prerequisites/core2foraws_power_on_off.jpg new file mode 100644 index 0000000..3cae4c9 Binary files /dev/null and b/workshop/content/Getting started/Prerequisites/core2foraws_power_on_off.jpg differ diff --git a/workshop/content/Getting started/Prerequisites/playstore.png b/workshop/content/Getting started/Prerequisites/playstore.png new file mode 100644 index 0000000..84b04a9 Binary files /dev/null and b/workshop/content/Getting started/Prerequisites/playstore.png differ diff --git a/workshop/content/Getting started/Run Rainmaker/_index.en.md b/workshop/content/Getting started/Run Rainmaker/_index.en.md new file mode 100644 index 0000000..3d120e4 --- /dev/null +++ b/workshop/content/Getting started/Run Rainmaker/_index.en.md @@ -0,0 +1,55 @@ ++++ +title = "Running the ESP RainMaker Agent" +weight = 12 +pre = "b. " ++++ + +We're ready to compile and flash our application on to the device, provision the Wi-Fi through the ESP Rainmaker Phone App, register a user to assign the device to, and begin controlling our peripherals over AWS IoT. + +## Opening the project in PlatformIO +There are several folders at the root of the project you cloned or downloaded and extracted. For this tutorial, we will open the Getting-Started project in PlatformIO. First open Visual Studio Code and after waiting a few seconds for the PlatformIO extension to load, click the **PlatformIO logo** on the left bar, select **Open** from the left PlatformIO menu, click **Open Project**, navigate to the `Core2-for-AWS-IoT-EduKit/Getting-Started` folder, and click **open**. +{{< img "pio_home.png" "PlatformIO home screen" >}} + +## Identifying and setting the serial port on host machine +You need to know which port you'll use for communications with the device for uploading firmware or monitoring the serial output. For macOS, the device is typically on **/dev/cu.SLAB_USBtoUART** and the included configuration should not require any changes. Linux is typically **/dev/ttyUSB0** and requires the user be added to the dialout group. For Windows, you'll need to replace the value with **COM** and end with the corresponding port number (e.g. COM3). For more specific Windows instructions or Linux distros that require additional configuration, please reference [Espressif's offical doc for establishing serial connections with the ESP32](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/establish-serial-connection.html). + +Choose the Visual Studio Code file explorer ( on the left bar), open the **platformio.ini** file, and change your **upload_port** value to match the serial port the device is connected to. + +{{% notice note %}} +View the official PlatformIO docs about the [upload_port](https://docs.platformio.org/en/latest/projectconf/section_env_upload.html#upload-port) option to see formatting and other examples. +{{% /notice %}} + +## Building and uploading the RainMaker Agent firmware +You are now ready to build and flash your firmware. Start by clicking the PlatformIO logo on the left bar. Then click the icon (hover your mouse next to **PROJECT TASKS**) to refresh the available options. You'll click the **Build** button to start building the device firmware. This will take a few minutes depending on your computer. Once that is complete, click **Upload and Monitor** option from the PlatformIO project tasks menu. Once the upload has completed successfully, the device will boot with the firmware that was just compiled and uploaded. It will also begin display the serial output from the device in that terminal viewport (now the serial monitor window). The device will be going through the process of generating security keys and performing a self claim. Key generation can take a few seconds, up to a few minutes to complete but once claiming completes a QR code will display on the serial monitor window. +{{< img "pio_menu.png" "PlatformIO Menu to build, flash, monitor" >}} + +{{% notice note %}} +For additional troubleshooting or FAQs, visit the official [Espressif RainMaker FAQs](https://rainmaker.espressif.com/docs/faqs.html). +{{% /notice %}} + +## Claiming and provisioning the device +On your mobile phone, open the ESP RainMaker Phone App, grant the requested mobile app permissions, press **Add Device**, and then scan the QR code displayed on your computer's serial monitor (*not* the QR code on the device). It will then go through the provisioning process, which includes Wi-Fi provisioning with your Wi-Fi credentials for your 2.4GHz wireless home network. After a successful Wi-Fi connection, the device will authenticate itself and your phone app will populate with multiple virtual devices that can be viewed and/or controlled. If the virtual devices are marked **offline** on the phone app after a minute or two, try scrolling down to refresh. +{{< img "qr_code_scan.png" "Scan the QR code in serial output" >}} + +With the virtual device listed and online in your mobile app, you can turn the on-board motor or LEDs on or off, adjust the speed of the motor, set the color and brightness of the LED bars, and view the internal device temperature. + +{{% notice info %}} +If you entered the wrong Wi-Fi credentials, you'll need to [erase the firmware](/en/getting-started/run-rainmaker.html#erasing-the-firmware-with-platformio) first, re-upload the Espressif RainMaker Agent firmware to the device, and add the device with your mobile phone again. +{{% /notice %}} + +## Erasing the firmware with PlatformIO +Once you are done with this application and ready to move on to other tutorials, you'll need to first wipe the device firmware. However, you'll need to stop the active serial monitor first since it's blocking other communication to the port. You can stop the serial monitor by selecting the Visual Studio Code pane with the serial monitor running and pressing **CTRL** + **C**. Then to erase the flash memory, enter the PlatformIO menu, expand the **Platform** menu list, and then click the **Erase Flash** option. This will remove all the data on the device, including the RainMaker certificates that were used. +{{< img "pio_platform_menu.png" "Erasing flash with PlatformIO" >}} + +{{% notice note %}} +Powercycling the device with a empty flash will result in the device screen being blank and a audible ticking sound from the speaker. This is expected behavior as the device is continually rebooting itself without an application to run. +{{% /notice %}} + +## Conclusion +You've just built a connected home application through the AWS IoT EduKit program! Not only that, but you have the tools necessary to create, edit, compile, and flash embedded code on to your device! In the next tutorials, you'll get more hands-on and learn the skills to start building your own end-to-end IoT solutions. + + +On to [**Blinky Hello World**](/en/blinky-hello-world.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Getting started/Run Rainmaker/_index.en_uk.md b/workshop/content/Getting started/Run Rainmaker/_index.en_uk.md new file mode 100644 index 0000000..8aa8f0c --- /dev/null +++ b/workshop/content/Getting started/Run Rainmaker/_index.en_uk.md @@ -0,0 +1,55 @@ ++++ +title = "Running the ESP RainMaker Agent" +weight = 12 +pre = "b. " ++++ + +We're ready to compile and flash our application on to the device, provision the Wi-Fi through the ESP Rainmaker Phone App, register a user to assign the device to, and begin controlling our peripherals over AWS IoT. + +## Opening the project in PlatformIO +There are several folders at the root of the project you cloned or downloaded and extracted. For this tutorial, we will open the Getting-Started project in PlatformIO. First open Visual Studio Code and after waiting a few seconds for the PlatformIO extension to load, click the **PlatformIO logo** on the left bar, select **Open** from the left PlatformIO menu, click **Open Project**, navigate to the `Core2-for-AWS-IoT-EduKit/Getting-Started` folder, and click **open**. +{{< img "pio_home.png" "PlatformIO home screen" >}} + +## Identifying and setting the serial port on host machine +You need to know which port you'll use for communications with the device for uploading firmware or monitoring the serial output. For macOS, the device is typically on **/dev/cu.SLAB_USBtoUART** and the included configuration should not require any changes. Linux is typically **/dev/ttyUSB0** and requires the user be added to the dialout group. For Windows, you'll need to replace the value with **COM** and end with the corresponding port number (e.g. COM3). For more specific Windows instructions or Linux distros that require additional configuration, please reference [Espressif's offical doc for establishing serial connections with the ESP32](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/establish-serial-connection.html). + +Choose the Visual Studio Code file explorer ( on the left bar), open the **platformio.ini** file, and change your **upload_port** value to match the serial port the device is connected to. + +{{% notice note %}} +View the official PlatformIO docs about the [upload_port](https://docs.platformio.org/en/latest/projectconf/section_env_upload.html#upload-port) option to see formatting and other examples. +{{% /notice %}} + +## Building and uploading the RainMaker Agent firmware +You are now ready to build and flash your firmware. Start by clicking the PlatformIO logo on the left bar. Then click the icon (hover your mouse next to **PROJECT TASKS**) to refresh the available options. You'll click the **Build** button to start building the device firmware. This will take a few minutes depending on your computer. Once that is complete, click **Upload and Monitor** option from the PlatformIO project tasks menu. Once the upload has completed successfully, the device will boot with the firmware that was just compiled and uploaded. It will also begin display the serial output from the device in that terminal viewport (now the serial monitor window). The device will be going through the process of generating security keys and performing a self claim. Key generation can take a few seconds, up to a few minutes to complete but once claiming completes a QR code will display on the serial monitor window. +{{< img "pio_menu.png" "PlatformIO Menu to build, flash, monitor" >}} + +{{% notice note %}} +For additional troubleshooting or FAQs, visit the official [Espressif RainMaker FAQs](https://rainmaker.espressif.com/docs/faqs.html). +{{% /notice %}} + +## Claiming and provisioning the device +On your mobile phone, open the ESP RainMaker Phone App, grant the requested mobile app permissions, press **Add Device**, and then scan the QR code displayed on your computer's serial monitor (*not* the QR code on the device). It will then go through the provisioning process, which includes Wi-Fi provisioning with your Wi-Fi credentials for your 2.4GHz wireless home network. After a successful Wi-Fi connection, the device will authenticate itself and your phone app will populate with multiple virtual devices that can be viewed and/or controlled. If the virtual devices are marked **offline** on the phone app after a minute or two, try scrolling down to refresh. +{{< img "qr_code_scan.png" "Scan the QR code in serial output" >}} + +With the virtual device listed and online in your mobile app, you can turn the on-board motor or LEDs on or off, adjust the speed of the motor, set the color and brightness of the LED bars, and view the internal device temperature. + +{{% notice info %}} +If you entered the wrong Wi-Fi credentials, you'll need to [erase the firmware](/en_uk/getting-started/run-rainmaker.html#erasing-the-firmware-with-platformio) first, re-upload the Espressif RainMaker Agent firmware to the device, and add the device with your mobile phone again. +{{% /notice %}} + +## Erasing the firmware with PlatformIO +Once you are done with this application and ready to move on to other tutorials, you'll need to first wipe the device firmware. However, you'll need to stop the active serial monitor first since it's blocking other communication to the port. You can stop the serial monitor by selecting the Visual Studio Code pane with the serial monitor running and pressing **CTRL** + **C**. Then to erase the flash memory, enter the PlatformIO menu, expand the **Platform** menu list, and then click the **Erase Flash** option. This will remove all the data on the device, including the RainMaker certificates that were used. +{{< img "pio_platform_menu.png" "Erasing flash with PlatformIO" >}} + +{{% notice note %}} +Powercycling the device with a empty flash will result in the device screen being blank and a audible ticking sound from the speaker. This is expected behavior as the device is continually rebooting itself without an application to run. +{{% /notice %}} + +## Conclusion +You've just built a connected home application through the AWS IoT EduKit program! Not only that, but you have the tools necessary to create, edit, compile, and flash embedded code on to your device! In the next tutorials, you'll get more hands-on and learn the skills to start building your own end-to-end IoT solutions. + + +On to [**Blinky Hello World**](/en_uk/blinky-hello-world.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Getting started/Run Rainmaker/notebook-configure.png b/workshop/content/Getting started/Run Rainmaker/notebook-configure.png new file mode 100644 index 0000000..b85a98c Binary files /dev/null and b/workshop/content/Getting started/Run Rainmaker/notebook-configure.png differ diff --git a/workshop/content/Getting started/Run Rainmaker/notebook-restart-run.png b/workshop/content/Getting started/Run Rainmaker/notebook-restart-run.png new file mode 100644 index 0000000..502c993 Binary files /dev/null and b/workshop/content/Getting started/Run Rainmaker/notebook-restart-run.png differ diff --git a/workshop/content/Getting started/Run Rainmaker/pio_home.png b/workshop/content/Getting started/Run Rainmaker/pio_home.png new file mode 100644 index 0000000..ddf33dc Binary files /dev/null and b/workshop/content/Getting started/Run Rainmaker/pio_home.png differ diff --git a/workshop/content/Getting started/Run Rainmaker/pio_menu.png b/workshop/content/Getting started/Run Rainmaker/pio_menu.png new file mode 100644 index 0000000..c5d818b Binary files /dev/null and b/workshop/content/Getting started/Run Rainmaker/pio_menu.png differ diff --git a/workshop/content/Getting started/Run Rainmaker/pio_platform_menu.png b/workshop/content/Getting started/Run Rainmaker/pio_platform_menu.png new file mode 100644 index 0000000..1ac23c0 Binary files /dev/null and b/workshop/content/Getting started/Run Rainmaker/pio_platform_menu.png differ diff --git a/workshop/content/Getting started/Run Rainmaker/qr_code_scan.png b/workshop/content/Getting started/Run Rainmaker/qr_code_scan.png new file mode 100644 index 0000000..58cea12 Binary files /dev/null and b/workshop/content/Getting started/Run Rainmaker/qr_code_scan.png differ diff --git a/workshop/content/Getting started/_index.en.md b/workshop/content/Getting started/_index.en.md new file mode 100644 index 0000000..f640691 --- /dev/null +++ b/workshop/content/Getting started/_index.en.md @@ -0,0 +1,15 @@ ++++ +title = "Getting Started" +chapter = true +weight = 10 +pre = "1. " ++++ + +Welcome to the first AWS IoT EduKit tutorial. To complete this exercise, you will need the [M5Stack Core2 ESP32 IoT Development Kit for AWS](https://www.amazon.com/dp/B08NP5LVFH) reference hardware. This getting started tutorial will walk you through a smart home solution to control some of the onboard peripherals via the iOS or Android Rainmaker Phone Application or through the Rainmaker CLI. This walkthrough does not require an AWS account and is meant to provide a quick demonstration of an embedded application that is enriched by connectivity to AWS IoT. + +The solution you will walkthrough uses the Espressif RainMaker platform, which is powered by AWS, to provide a customizable smart home application without managing the infrastructure. There is no additional cost to using Espressif's RainMaker Platform. + +To get started with this tutorial, continue on to [**Prerequisites**](/en/getting-started/prerequisites.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Getting started/_index.en_uk.md b/workshop/content/Getting started/_index.en_uk.md new file mode 100644 index 0000000..c129a66 --- /dev/null +++ b/workshop/content/Getting started/_index.en_uk.md @@ -0,0 +1,15 @@ ++++ +title = "Getting Started" +chapter = true +weight = 10 +pre = "1. " ++++ + +Welcome to the first AWS IoT EduKit tutorial. To complete this exercise, you will need the [M5Stack Core2 ESP32 IoT Development Kit for AWS](https://www.amazon.com/dp/B08NP5LVFH) reference hardware. This getting started tutorial will walk you through a smart home solution to control some of the onboard peripherals via the iOS or Android Rainmaker Phone Application or through the Rainmaker CLI. This walkthrough does not require an AWS account and is meant to provide a quick demonstration of an embedded application that is enriched by connectivity to AWS IoT. + +The solution you will walkthrough uses the Espressif RainMaker platform, which is powered by AWS, to provide a customizable smart home application without managing the infrastructure. There is no additional cost to using Espressif's RainMaker Platform. + +To get started with this tutorial, continue on to [**Prerequisites**](/en_uk/getting-started/prerequisites.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/Alexa-Time.webp b/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/Alexa-Time.webp new file mode 100644 index 0000000..cdfcec1 Binary files /dev/null and b/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/Alexa-Time.webp differ diff --git a/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/AlexaApp-LightDevice.png b/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/AlexaApp-LightDevice.png new file mode 100644 index 0000000..613a7b2 Binary files /dev/null and b/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/AlexaApp-LightDevice.png differ diff --git a/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/_index.en.md b/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/_index.en.md new file mode 100644 index 0000000..b452b88 --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/_index.en.md @@ -0,0 +1,99 @@ ++++ +title = "Building and Testing AFI" +weight = 30 +pre = "c. " ++++ + +## Chapter introduction +In this chapter we will build the ported ESP VA-SDK firmware, flash it on to the device, provision the Wi-Fi and authorize the device to your Alexa Account, and test some of the smart home capabilities using Alexa voice commands available in this beta version of AFI. + +## Flash the Firmware +Use ESP-IDF to flash the firmware onto the device. Replace **<>** with your device port. If you do not know your device port, follow the [Identifying the serial port on host machine](/en/blinky-hello-world/device-provisioning.html#identifying-the-serial-port-on-host-machine) instructions in the **Blinky Hello World** example. +```bash +cd path/to/Core2-for-AWS-IoT-EduKit/Alexa_For_IoT-Intro/esp-va-sdk-core2foraws/examples/amazon_aia/ +idf.py build flash monitor -p <> +``` + +## Provision the Device +For the provisioning process, you will need to configure your Wi-Fi network credentials and authorize the application with your Alexa account using ESP Alexa Phone Application. + +Download the application from your mobile app store. +[iOS](https://apps.apple.com/in/app/esp-alexa/id1464127534) / [Android](https://play.google.com/store/apps/details?id=com.espressif.provbleavs) + +Provision steps: +1. Launch the companion app. + - Make sure you have enabled the Bluetooth and the app has proper permissions to access Bluetooth. +2. Select the option **Add New Device**. +3. Your device will display in the app. + - You will see a **Proof of Possession** modal with a value of `abcdf1234` or similar pop up. Simply press **Done** to continue. +4. After connecting to the device, you will sign into your Amazon account. +5. Select the Wi-Fi network and enter the credentials. +6. After a successful Wi-Fi connection, you will see a list of sample utterances. + +## Using Alexa +With the prior steps completed, you will see a number of logs in your serial monitor, including some like the following: + +```bash +I (17325) [http_transport]: Subscribing /capabilities/acknowledge... +I (17535) [http_transport]: Subscribing /connection/fromservice... +I (17735) [http_transport]: Subscribing /directive... +I (17945) [http_transport]: Subscribing /speaker... +... +I (20735) [directive_proc]: Name: EndpointForwarding +... +I (22675) [directive_proc]: Name: SetAttentionState +... +E (22685) [app_va_cb]: Enabling Mic +``` + +In order to interact with Alexa, you will need to say *Alexa* to the device. This will trigger the **Espressif Wake Word Engine** running on the device to enter the **LISTENING** attention state. For full details on the different attention states, please refer to our [documentation](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/ux-design-attention.html#states). For more information on audio capture, see the [SpeechRecognizer API Documentation](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/avs-speechrecognizer-concepts.html) + +{{< img "speechrecognizer-state.png" "Audio Capture Speech Recognizer Attention States" >}} + +{{% notice info %}} +Just like any Alexa device, when the device is in the IDLE state, it is listening ONLY for the keyword "Alexa". Only once the keyword is triggered will the device start streaming audio to the cloud. +{{% /notice %}} + +Try a variety of utterances to Alexa - the side LEDs should light blue up when **Alexa** is heard (if Alexa does not "wake up", try speaking closer to the device): +* _Alexa, what time is it?_ +* _Alexa, tell me a joke?_ +* _Alexa, turn on all of the lights_ (Only works if you already have some Alexa smart home devices on your same account) + +{{< img "Alexa-Time.webp" "Alexa, what time is it?">}} + +## Testing Alexa Smart Home Capabilities (Beta) +The AFI device has **Alexa Built-In**, which means you can speak to Alexa directly to the device and Alexa will respond with voice on the device. However, this version of AFI from Espressif also supports Alexa Smart Home commands as a beta feature, which allows you to control attributes on the device. + +The Alexa for AWS IoT sample application creates a virtual device called **Light** in your Alexa app, which supports two interfaces: + +* [PowerController](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/alexa-powercontroller.html) to turn the light on and off. +* [RangeController](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/alexa-rangecontroller.html) to adjust the brightness of the device. + +![The device named "Light" should show up in your Alexa App](building-and-testing-afi/AlexaApp-LightDevice.png?height=500px) + +Since it's a virtual device, it is prints the updated status to the screen. We can test this out via voice or the Alexa app. + +* By voice - say _Alexa, turn on the light_ - if successful, Alexa may respond with "OK" or some other confirmation sound +* Via the Alexa app - open your Alexa mobile phone app (not the ESP Alexa mobile phone app), go to Devices and then either **Lights** or **All Devices** and you should see the device named **Light** (see screenshots below). Tap the power icon and you should see the icon toggle between off and on. + +Through either option, you should see a message like the following in your terminal: +```bash +I (97445) [alexa_smart_home]: Namespace: Alexa.PowerController, Name: TurnOn +``` + +Similarly, you can try to control the range of the brightness by one of the following: + +* By voice - say _Alexa, set brightness on the light to 80_ - if successful, Alexa may respond with "OK" or some other confirmation response. +* Via the Alexa app - adjust the slider of brightness. + +Through either option, you should see a message like the following in the serial monitor: +```bash +[app_smart_home]: *************** Light's Brightness changed to 60 *************** +``` + +This is useful not only because we have voice assistant on our device with Alexa, but we can use Alexa to control properties on the device itself! + +On to creating a [**Custom Smart Home Device**](/en/intro-to-alexa-for-iot/custom-smart-home-device.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/_index.en_uk.md b/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/_index.en_uk.md new file mode 100644 index 0000000..8b5edf4 --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/_index.en_uk.md @@ -0,0 +1,97 @@ ++++ +title = "Building and Testing AFI" +weight = 30 +pre = "c. " ++++ + +## Chapter introduction +In this chapter we will build the ported ESP VA-SDK firmware, flash it on to the device, provision the Wi-Fi and authorize the device to your Alexa Account, and test some of the smart home capabilities using Alexa voice commands available in this beta version of AFI. + +## Flash the Firmware +Use ESP-IDF to flash the firmware onto the device. Replace **<>** with your device port. If you do not know your device port, follow the [Identifying the serial port on host machine](/en_uk/blinky-hello-world/device-provisioning.html#identifying-the-serial-port-on-host-machine) instructions in the **Blinky Hello World** example. +```bash +cd path/to/Core2-for-AWS-IoT-EduKit/Alexa_For_IoT-Intro/esp-va-sdk-core2foraws/examples/amazon_aia/ +idf.py build flash monitor -p <> +``` + +## Provision the Device +For the provisioning process, you will need to configure your Wi-Fi network credentials and authorize the application with your Alexa account using ESP Alexa Phone Application. + +Download the application from your mobile app store. +[iOS](https://apps.apple.com/in/app/esp-alexa/id1464127534) / [Android](https://play.google.com/store/apps/details?id=com.espressif.provbleavs) + +Provision steps: +1. Launch the companion app. + - Make sure you have enabled the Bluetooth and the app has proper permissions to access Bluetooth. +2. Select the option **Add New Device**. +3. Your device will display in the app. + - You will see a **Proof of Possession** modal with a value of `abcdf1234` or similar pop up. Simply press **Done** to continue. +4. After connecting to the device, you will sign into your Amazon account. +5. Select the Wi-Fi network and enter the credentials. +6. After a successful Wi-Fi connection, you will see a list of sample utterances. + +## Using Alexa +With the prior steps completed, you will see a number of logs in your serial monitor, including some like the following: +```bash +I (17325) [http_transport]: Subscribing /capabilities/acknowledge... +I (17535) [http_transport]: Subscribing /connection/fromservice... +I (17735) [http_transport]: Subscribing /directive... +I (17945) [http_transport]: Subscribing /speaker... +... +I (20735) [directive_proc]: Name: EndpointForwarding +... +I (22675) [directive_proc]: Name: SetAttentionState +... +E (22685) [app_va_cb]: Enabling Mic +``` + +In order to interact with Alexa, you will need to say *Alexa* to the device. This will trigger the **Espressif Wake Word Engine** running on the device to enter the **LISTENING** attention state. For full details on the different attention states, please refer to our [documentation](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/ux-design-attention.html#states). For more information on audio capture, see the [SpeechRecognizer API Documentation](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/avs-speechrecognizer-concepts.html). + +{{< img "speechrecognizer-state.png" "Audio Capture Speech Recognizer Attention States" >}} + +{{% notice info %}} +Just like any Alexa device, when the device is in the IDLE state, it is listening ONLY for the keyword "Alexa". Only once the keyword is triggered will the device start streaming audio to the cloud. +{{% /notice %}} + +Try a variety of utterances to Alexa - the side LEDs should light blue up when **Alexa** is heard (if Alexa does not "wake up", try speaking closer to the device): + +* _Alexa, what time is it?_ +* _Alexa, tell me a joke?_ +* _Alexa, turn on all of the lights_ (Only works if you already have some Alexa smart home devices on your same account) + +{{< img "Alexa-Time.webp" "Alexa, what time is it?">}} + +## Testing Alexa Smart Home Capabilities (Beta) +The AFI device has **Alexa Built-In**, which means you can speak to Alexa directly to the device and Alexa will respond with voice on the device. However, this version of AFI from Espressif also supports Alexa Smart Home commands as a beta feature, which allows you to control attributes on the device. + +The Alexa for AWS IoT sample application creates a virtual device called **Light** in your Alexa app, which supports two interfaces: +* [PowerController](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/alexa-powercontroller.html) to turn the light on and off. +* [RangeController](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/alexa-rangecontroller.html) to adjust the brightness of the device. + +![The device named "Light" should show up in your Alexa App](building-and-testing-afi/AlexaApp-LightDevice.png?height=500px) + +Since it's a virtual device, it is prints the updated status to the screen. We can test this out via voice or the Alexa app. + +* By voice - say _Alexa, turn on the light_ - if successful, Alexa may respond with "OK" or some other confirmation sound +* Via the Alexa app - open your Alexa mobile phone app (not the ESP Alexa mobile phone app), go to Devices and then either **Lights** or **All Devices** and you should see the device named **Light** (see screenshots below). Tap the power icon and you should see the icon toggle between off and on. + +Through either option, you should see a message like the following in your terminal: +```bash +I (97445) [alexa_smart_home]: Namespace: Alexa.PowerController, Name: TurnOn +``` + +Similarly, you can try to control the range of the brightness by one of the following: +* By voice - say _Alexa, set brightness on the light to 80_ - if successful, Alexa may respond with "OK" or some other confirmation response. +* Via the Alexa app - adjust the slider of brightness. + +Through either option, you should see a message like the following in the serial monitor: +```bash +[app_smart_home]: *************** Light's Brightness changed to 60 *************** +``` + +This is useful not only because we have voice assistant on our device with Alexa, but we can use Alexa to control properties on the device itself! + +On to creating a [**Custom Smart Home Device**](/en_uk/intro-to-alexa-for-iot/custom-smart-home-device.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/speechrecognizer-state.png b/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/speechrecognizer-state.png new file mode 100644 index 0000000..65b4969 Binary files /dev/null and b/workshop/content/Intro to Alexa For IoT/Building and Testing AFI/speechrecognizer-state.png differ diff --git a/workshop/content/Intro to Alexa For IoT/Conclusion/_index.en.md b/workshop/content/Intro to Alexa For IoT/Conclusion/_index.en.md new file mode 100644 index 0000000..5fde91e --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/Conclusion/_index.en.md @@ -0,0 +1,22 @@ + ++++ +title = "Conclusion" +weight = 50 +pre = "e. " ++++ + +## Conclusion +You have completed the AWS IoT EduKit hands-on tutorial for running Alexa for AWS IoT (AFI) using the custom ported Espressif Voice Assistant SDK and Alexa to control on-board peripherals using Alexa Smart Home commands. For additional information about Smart Home for AVS, see the [documentation](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/smart-home-for-avs.html) or the following [blog post](https://developer.amazon.com/en-US/blogs/alexa/device-makers/2020/04/create-a-sample-alexa-built-in-disco-ball-with-smart-home-for-av). + +If you would like to learn more about other interfaces available or to come up with your own implementations of using other features of the M5Stack Core2 for AWS IoT EduKit reference hardware, take a look at [ModeController](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/alexa-modecontroller.html) and [ToggleController](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/alexa-togglecontroller.html). Use your creativity and what you've learned in the other chapters of this tutorial to explore new capabilities for voice! + +This is the last tutorial currently available. We will be publishing more, but for now, you have the skills to build your own IoT solution with AWS services with the skills you have learned so far. + +## Clean up +In this solution you did not use any of your own resources in AWS. However, you can wipe your device software (and remove the certificates from flash) by entering the command (replace **<>** with the serial port your device is connected to): +```bash +idf.py erase_flash -p <> +``` + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/Conclusion/_index.en_uk.md b/workshop/content/Intro to Alexa For IoT/Conclusion/_index.en_uk.md new file mode 100644 index 0000000..10623ac --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/Conclusion/_index.en_uk.md @@ -0,0 +1,22 @@ + ++++ +title = "Conclusion" +weight = 50 +pre = "e. " ++++ + +## Conclusion +You have completed the AWS IoT EduKit hands-on tutorial for running Alexa for AWS IoT (AFI) using the custom ported Espressif Voice Assistant SDK and Alexa to control on-board peripherals using Alexa Smart Home commands. For additional information about Smart Home for AVS, see the [documentation](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/smart-home-for-avs.html) or the following [blog post](https://developer.amazon.com/en-US/blogs/alexa/device-makers/2020/04/create-a-sample-alexa-built-in-disco-ball-with-smart-home-for-av). + +If you would like to learn more about other interfaces available or to come up with your own implementations of using other features of the M5Stack Core2 for AWS IoT EduKit reference hardware, take a look at [ModeController](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/alexa-modecontroller.html) and [ToggleController](https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/alexa-togglecontroller.html). Use your creativity and what you've learned in the other chapters of this tutorial to explore new capabilities for voice! + +This is the last tutorial currently available. We will be publishing more. + +## Clean up +In this solution you did not use any of your own resources in AWS. However, you can wipe your device software (and remove the certificates from flash) by entering the command (replace **<>** with the serial port your device is connected to): +```bash +idf.py erase_flash -p <> +``` + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLight-Blink.webp b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLight-Blink.webp new file mode 100644 index 0000000..2cd49b6 Binary files /dev/null and b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLight-Blink.webp differ diff --git a/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLight-Power480.webp b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLight-Power480.webp new file mode 100644 index 0000000..8ed9bb6 Binary files /dev/null and b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLight-Power480.webp differ diff --git a/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLight.png b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLight.png new file mode 100644 index 0000000..97efaa1 Binary files /dev/null and b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLight.png differ diff --git a/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLightFound.jpg b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLightFound.jpg new file mode 100644 index 0000000..d26d066 Binary files /dev/null and b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/AlexaApp-GreenLightFound.jpg differ diff --git a/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/_index.en.md b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/_index.en.md new file mode 100644 index 0000000..2f680e0 --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/_index.en.md @@ -0,0 +1,128 @@ ++++ +title = "Custom Smart Home Device" +weight = 40 +pre = "d. " ++++ + +## Customizing Smart Home Control + +Now that we have an understanding of what Smart Home control capabilities are included with the kit, you will modify those capabilities to control attributes of the device itself versus simply printing to the serial monitor. For this workshop, you will create a simple implementation that turns on the side green LED light via the **PowerController** and we will set the device to blink at a set rate using the Range Controller. + +## Customizing smart home device attributes +Using your IDE, open the cloned **Core2-for-AWS-IoT-EduKit** folder and open the `Alexa_for_IoT-Intro/esp-va-sdk-core2foraws/examples/additional_components/app_smart_home/app_smart_home.c` to take a look at where the Smart Home device attributes are defined. Scrolling down to line 109 **app_smart_home_init()** function and you will see the following code blocks: +```c +/* Add device */ +smart_home_device_t *device = smart_home_device_create("Light", alexa_smart_home_get_device_type_str(LIGHT), NULL); +smart_home_device_add_cb(device, write_cb, NULL); +smart_home_node_add_device(node, device); +``` +The above block defines the "friendly name" of the device, that is - the name of the device. Be more explicit with the name by changing **Light** to **Green Light**. The second line should look like the following: + +`smart_home_device_t *device = smart_home_device_create("Green Light", alexa_smart_home_get_device_type_str(LIGHT), NULL);` + +Next, modify the Range Controller definition to change **Brightness** to **Blink**. Scrolling down a bit further to line 141, you can see where the different attributes of our device is defined: +```c +/* Add device parameters */ +smart_home_param_t *power_param = smart_home_param_create("Power", SMART_HOME_PARAM_POWER, smart_home_bool(true), SMART_HOME_PROP_FLAG_READ | SMART_HOME_PROP_FLAG_WRITE | SMART_HOME_PROP_FLAG_PERSIST); +smart_home_device_add_param(device, power_param); + +smart_home_param_t *brightness_param = smart_home_param_create("Brightness", SMART_HOME_PARAM_RANGE, smart_home_int(100), SMART_HOME_PROP_FLAG_READ | SMART_HOME_PROP_FLAG_WRITE | SMART_HOME_PROP_FLAG_PERSIST); +smart_home_param_add_bounds(brightness_param, smart_home_int(0), smart_home_int(100), smart_home_int(1)); +smart_home_device_add_param(device, brightness_param); +``` + +You will need to update the max blink count from 100 to 10. So you will make a change to the **brightness_param** variable definition and to the **smart_home_param_add_bounds** function call, so it should now look like: +```c +smart_home_param_t *brightness_param = smart_home_param_create("Blink", SMART_HOME_PARAM_RANGE, smart_home_int(10), SMART_HOME_PROP_FLAG_READ | SMART_HOME_PROP_FLAG_WRITE | SMART_HOME_PROP_FLAG_PERSIST); +smart_home_param_add_bounds(brightness_param, smart_home_int(0), smart_home_int(10), smart_home_int(1)); +``` + +## Adding smart home device logic +By now you've modified the properties but need to implement the logic to control the green LED. First, we need to include a library for controlling the light and also define a few global variables: +```c +#include "axp192.h" +static uint8_t CURRENT_BLINK_DELAY = 20; +static uint8_t GREEN_LIGHT_STATUS = 0; +``` + +Next, modify the power controller implementation to turn on/off the green LED. Look for the **write_cb** function (line 89 of the original file) and the following code snippet: +```c +if (val.type == SMART_HOME_VAL_TYPE_BOOLEAN) { + printf("%s: *************** %s's %s turned %s ***************\n", TAG, device_name, param_name, val.val.b ? "ON" : "OFF"); + +} +``` + +Now to add toggle logic based on the values received by adding three lines of code so that the if statement looks like the following: +```c +if (val.type == SMART_HOME_VAL_TYPE_BOOLEAN) { + printf("%s: *************** %s's %s turned %s ***************\n", TAG, device_name, param_name, val.val.b ? "ON" : "OFF"); + + //Set the global GREEN_LIGHT_STATUS variable to the desired value and set the GPIO1 value the right setting (on/off) + GREEN_LIGHT_STATUS = val.val.b ? 0 : 1; + Axp192_SetGPIO1Mode(GREEN_LIGHT_STATUS); + +} +``` + +You are taking the updated value of the power controller (1 or 0) to indicate whether the customer wanted to turn the device on or off, and will turn the side green light on or off. + +Next, implement the "blink" functionality. First, you'll need to create a method to blink the green light a set number of times. Add the following function to the same **app_smart_home.c** file: +```c +static void startLightBlink(int count) +{ + //we want to start toggling based on the opposite status of what the light currently is + for(int i = 1 + GREEN_LIGHT_STATUS; i <= count*2+GREEN_LIGHT_STATUS ; i++) { + Axp192_SetGPIO1Mode( i % 2 ); + vTaskDelay(CURRENT_BLINK_DELAY); + } +} +``` + +Now update the RangeController logic to call this function. In this case it's in the following code block back in **write_cb**: +```c +else if (val.type == SMART_HOME_VAL_TYPE_INTEGER) { + printf("%s: *************** %s's %s changed to %d ***************\n", TAG, device_name, param_name, val.val.i); + +} +``` +We just need to add a call to our newly created *startLinkBlink* function, so we can add the following lines here: +```c +else if (val.type == SMART_HOME_VAL_TYPE_INTEGER) { + printf("%s: *************** %s's %s changed to %d ***************\n", TAG, device_name, param_name, val.val.i); + + //call the Trigger Light Blink based on the desired number of actions + startLightBlink(val.val.i); + +} +``` + +## Flashing and testing updated Alexa firmware +With the project modified to have the necessary device attributes and the control logic to blink the onboard green LED at a specified rate, it is time to build the firmware, flash it on to the reference hardware, and test the capabilities. Run the same command as other tutorials, replacing the **<>** with the serial port your Core2 for AWS IoT EduKit device is connected to. +```bash +idf.py build flash monitor -p <> +``` +{{% notice info %}} +You can exist the serial monitor with the key combination `CTRL` + `]`. You can restart the serial monitor by entering the command `idf.py monitor -p <>`. +{{% /notice %}} + +If all goes well, you will see an update in your Alexa app that you have a new device called **Green Light**. Try controlling the Power: +!["Green Light" device added notification](custom-smart-home-device/AlexaApp-GreenLightFound.jpg?height=500px&classes=shadow) + +![Device named "Green Light" listed in your Alexa App](custom-smart-home-device/AlexaApp-GreenLight.png?height=500px&classes=shadow) + +* Voice: _Alexa, turn on/off green light light_ - the side green LED light should turn off and on +* Via the Alexa app - open your Alexa app (not the Espressif app), go to Devices and then either "Lights" or "All Devices" and you should see the device named "Green Light" (see screenshots below). Tap the power icon and you should see the icon toggle between off and on. + +{{< img "AlexaApp-GreenLight-Power480.webp" "Power Controller implemented with the green light" >}} + +You can also test the Blink functionality: + +* Voice: _Alexa, set blink to 7_ - the side green LED light should blink 7 times +* Via the Alexa app - From the **Green Light** device in your Alexa app (not the ESP Alexa mobile phone app), adjust the slider between 1 and 10 and the device should blink the specified number of times. +{{< img "AlexaApp-GreenLight-Blink.webp" "Blinking the green LED with power controller slider" >}} + +Congratulations, you've completed this tutorial! On to the [**Conclusion**](/en/intro-to-alexa-for-iot/conclusion.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/_index.en_uk.md b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/_index.en_uk.md new file mode 100644 index 0000000..8cd09dc --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/Custom Smart Home Device/_index.en_uk.md @@ -0,0 +1,124 @@ ++++ +title = "Custom Smart Home Device" +weight = 40 +pre = "d. " ++++ + +## Customizing Smart Home Control +Now that we have an understanding of what Smart Home control capabilities are included with the kit, you will modify those capabilities to control attributes of the device itself versus simply printing to the serial monitor. For this workshop, you will create a simple implementation that turns on the side green LED light via the **PowerController** and we will set the device to blink at a set rate using the Range Controller. + +## Customizing smart home device attributes +Using your IDE, open the cloned **Core2-for-AWS-IoT-EduKit** folder and open the `Alexa_for_IoT-Intro/esp-va-sdk-core2foraws/examples/additional_components/app_smart_home/app_smart_home.c` to take a look at where the Smart Home device attributes are defined. Scrolling down to line 109 **app_smart_home_init()** function and you will see the following code blocks: +```c +/* Add device */ +smart_home_device_t *device = smart_home_device_create("Light", alexa_smart_home_get_device_type_str(LIGHT), NULL); +smart_home_device_add_cb(device, write_cb, NULL); +smart_home_node_add_device(node, device); +``` +The above block defines the "friendly name" of the device, that is - the name of the device. Be more explicit with the name by changing **Light** to **Green Light**. The second line should look like the following: + +`smart_home_device_t *device = smart_home_device_create("Green Light", alexa_smart_home_get_device_type_str(LIGHT), NULL);` + +Next, modify the Range Controller definition to change **Brightness** to **Blink**. Scrolling down a bit further to line 141, you can see where the different attributes of our device is defined: +```c +/* Add device parameters */ +smart_home_param_t *power_param = smart_home_param_create("Power", SMART_HOME_PARAM_POWER, smart_home_bool(true), SMART_HOME_PROP_FLAG_READ | SMART_HOME_PROP_FLAG_WRITE | SMART_HOME_PROP_FLAG_PERSIST); +smart_home_device_add_param(device, power_param); + +smart_home_param_t *brightness_param = smart_home_param_create("Brightness", SMART_HOME_PARAM_RANGE, smart_home_int(100), SMART_HOME_PROP_FLAG_READ | SMART_HOME_PROP_FLAG_WRITE | SMART_HOME_PROP_FLAG_PERSIST); +smart_home_param_add_bounds(brightness_param, smart_home_int(0), smart_home_int(100), smart_home_int(1)); +smart_home_device_add_param(device, brightness_param); +``` + +You will need to update the max blink count from 100 to 10. So you will make a change to the **brightness_param** variable definition and to the **smart_home_param_add_bounds** function call, so it should now look like: +```c +smart_home_param_t *brightness_param = smart_home_param_create("Blink", SMART_HOME_PARAM_RANGE, smart_home_int(10), SMART_HOME_PROP_FLAG_READ | SMART_HOME_PROP_FLAG_WRITE | SMART_HOME_PROP_FLAG_PERSIST); +smart_home_param_add_bounds(brightness_param, smart_home_int(0), smart_home_int(10), smart_home_int(1)); +``` + +## Adding smart home device logic +By now you've modified the properties but need to implement the logic to control the green LED. First, we need to include a library for controlling the light and also define a few global variables: +```c +#include "axp192.h" +static uint8_t CURRENT_BLINK_DELAY = 20; +static uint8_t GREEN_LIGHT_STATUS = 0; +``` + +Next, modify the power controller implementation to turn on/off the green LED. Look for the **write_cb** function (line 89 of the original file) and the following code snippet: +```c +if (val.type == SMART_HOME_VAL_TYPE_BOOLEAN) { + printf("%s: *************** %s's %s turned %s ***************\n", TAG, device_name, param_name, val.val.b ? "ON" : "OFF"); +} +``` + +Now to add toggle logic based on the values received by adding three lines of code so that the if statement looks like the following: +```c +if (val.type == SMART_HOME_VAL_TYPE_BOOLEAN) { + printf("%s: *************** %s's %s turned %s ***************\n", TAG, device_name, param_name, val.val.b ? "ON" : "OFF"); + + //Set the global GREEN_LIGHT_STATUS variable to the desired value and set the GPIO1 value the right setting (on/off) + GREEN_LIGHT_STATUS = val.val.b ? 0 : 1; + Axp192_SetGPIO1Mode(GREEN_LIGHT_STATUS); +} +``` + +You are taking the updated value of the power controller (1 or 0) to indicate whether the customer wanted to turn the device on or off, and will turn the side green light on or off. + +Next, implement the "blink" functionality. First, you'll need to create a method to blink the green light a set number of times. Add the following function to the same **app_smart_home.c** file: +```c +static void startLightBlink(int count) +{ + //we want to start toggling based on the opposite status of what the light currently is + for(int i = 1 + GREEN_LIGHT_STATUS; i <= count*2+GREEN_LIGHT_STATUS ; i++) { + Axp192_SetGPIO1Mode( i % 2 ); + vTaskDelay(CURRENT_BLINK_DELAY); + } +} +``` + +Now update the RangeController logic to call this function. In this case it's in the following code block back in **write_cb**: +```c +else if (val.type == SMART_HOME_VAL_TYPE_INTEGER) { + printf("%s: *************** %s's %s changed to %d ***************\n", TAG, device_name, param_name, val.val.i); +} +``` + +We just need to add a call to our newly created *startLinkBlink* function, so we can add the following lines here: +```c +else if (val.type == SMART_HOME_VAL_TYPE_INTEGER) { + printf("%s: *************** %s's %s changed to %d ***************\n", TAG, device_name, param_name, val.val.i); + + //call the Trigger Light Blink based on the desired number of actions + startLightBlink(val.val.i); +} +``` + +## Flashing and testing updated Alexa firmware +With the project modified to have the necessary device attributes and the control logic to blink the onboard green LED at a specified rate, it is time to build the firmware, flash it on to the reference hardware, and test the capabilities. Run the same command as other tutorials, replacing the **<>** with the serial port your Core2 for AWS IoT EduKit device is connected to. +```bash +idf.py build flash monitor -p <> +``` +{{% notice info %}} +You can exist the serial monitor with the key combination `CTRL` + `]`. You can restart the serial monitor by entering the command `idf.py monitor -p <>`. +{{% /notice %}} + +If all goes well, you will see an update in your Alexa app that you have a new device called **Green Light**. Try controlling the Power: +!["Green Light" device added notification](custom-smart-home-device/AlexaApp-GreenLightFound.jpg?height=500px&classes=shadow) + +![Device named "Green Light" listed in your Alexa App](custom-smart-home-device/AlexaApp-GreenLight.png?height=500px&classes=shadow) + +* Voice: _Alexa, turn on/off green light light_ - the side green LED light should turn off and on +* Via the Alexa app - open your Alexa app (not the Espressif app), go to Devices and then either "Lights" or "All Devices" and you should see the device named "Green Light" (see screenshots below). Tap the power icon and you should see the icon toggle between off and on. + +{{< img "AlexaApp-GreenLight-Power480.webp" "Power Controller implemented with the green light" >}} + +You can also test the Blink functionality: + +* Voice: _Alexa, set blink to 7_ - the side green LED light should blink 7 times +* Via the Alexa app - From the **Green Light** device in your Alexa app (not the ESP Alexa mobile phone app), adjust the slider between 1 and 10 and the device should blink the specified number of times. +{{< img "AlexaApp-GreenLight-Blink.webp" "Blinking the green LED with power controller slider" >}} + +Congratulations, you've completed this tutorial! On to the [**Conclusion**](/en_uk/intro-to-alexa-for-iot/conclusion.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/Flashing Espressif Certificates/_index.en.md b/workshop/content/Intro to Alexa For IoT/Flashing Espressif Certificates/_index.en.md new file mode 100644 index 0000000..c050060 --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/Flashing Espressif Certificates/_index.en.md @@ -0,0 +1,37 @@ ++++ +title = "Flashing Espressif Certificates" +weight = 20 +pre = "b. " ++++ + +## Chapter introduction +By the end of this chapter, you will have the companion phone applications installed on your mobile device, cloned the necessary code repository, received certificates for connecting to AWS IoT using Espressif Alexa's AWS account, and flashed the certificates to separate flash partitions of the reference hardware. + +## Companion Mobile Applications +In order to complete the authentication with Alexa, you will need Espressif's companion app to provision the device to WiFi and provision the reference hardware with your Alexa account. + +Download the ESP Alexa Phone App: +[iOS](https://apps.apple.com/in/app/esp-alexa/id1464127534) / [Android](https://play.google.com/store/apps/details?id=com.espressif.provbleavs) + +It is recommended (_optional_ for this tutorial) to have the Amazon Alexa app available for [iOS](https://apps.apple.com/us/app/amazon-alexa/id944011620) and [Android](https://play.google.com/store/apps/details?id=com.amazon.dee.app) - this is the same app used to provision most Alexa-enabled devices. + +## Accessing the code +All the code for this tutorial is located in the `Alexa_for_IoT-Intro` folder from the repo you cloned in the [**Blinky Hello World**](/en/blinky-hello-world.html) tutorial. To clone the repo again: +``` +git clone https://github.com/m5stack/Core2-for-AWS-IoT-EduKit.git +``` + +## Set up AWS IoT Certificates +You need to create the AWS IoT certificates to communicate with AWS IoT core. For this workshop and device, Espressif has provided the AWS IoT certificates that can be used with the M5Stack Core2 for AWS IoT EduKit reference hardware. Follow the steps [here](https://espressif.github.io/esp-va-sdk/#/) to obtain your certificates. + +After unzipping the downloaded certificates, you will have a folder called **espcredentials**. Please navigate to the folder and modify **mfg_config.csv** (provided with the certificates) to reflect the correct paths for each file. Then run the commands below in the same folder to flash the certificates to device. Replace **<>** with the serial port your Core2 for AWS IoT EduKit device is connected to: +```bash +python $IDF_PATH/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py generate /path/to/mfg_config.csv mfg.bin 0x6000 + +python $IDF_PATH/components/esptool_py/esptool/esptool.py --chip esp32 --port <> write_flash 0x10000 mfg.bin +``` + +With everthing set up and ready, let's move on to [**Building and Testing AFI**](/en/intro-to-alexa-for-iot/building-and-testing-afi.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/Flashing Espressif Certificates/_index.en_uk.md b/workshop/content/Intro to Alexa For IoT/Flashing Espressif Certificates/_index.en_uk.md new file mode 100644 index 0000000..614247a --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/Flashing Espressif Certificates/_index.en_uk.md @@ -0,0 +1,37 @@ ++++ +title = "Flashing Espressif Certificates" +weight = 20 +pre = "b. " ++++ + +## Chapter introduction +By the end of this chapter, you will have the companion phone applications installed on your mobile device, cloned the necessary code repository, received certificates for connecting to AWS IoT using Espressif Alexa's AWS account, and flashed the certificates to separate flash partitions of the reference hardware. + +## Companion Mobile Applications +In order to complete the authentication with Alexa, you will need Espressif's companion app to provision the device to WiFi and provision the reference hardware with your Alexa account. + +Download the ESP Alexa Phone App: +[iOS](https://apps.apple.com/in/app/esp-alexa/id1464127534) / [Android](https://play.google.com/store/apps/details?id=com.espressif.provbleavs) + +It is recommended (_optional_ for this tutorial) to have the Amazon Alexa app available for [iOS](https://apps.apple.com/us/app/amazon-alexa/id944011620) and [Android](https://play.google.com/store/apps/details?id=com.amazon.dee.app) - this is the same app used to provision most Alexa-enabled devices. + +## Accessing the code +All the code for this tutorial is located in the `Alexa_for_IoT-Intro` folder from the repo you cloned in the [**Blinky Hello World**](/en_uk/blinky-hello-world.html) tutorial. To clone the repo again: +``` +git clone https://github.com/m5stack/Core2-for-AWS-IoT-EduKit.git +``` + +## Set up AWS IoT Certificates +You need to create the AWS IoT certificates to communicate with AWS IoT core. For this workshop and device, Espressif has provided the AWS IoT certificates that can be used with the M5Stack Core2 for AWS IoT EduKit reference hardware. Follow the steps [here](https://espressif.github.io/esp-va-sdk/#/) to obtain your certificates. + +After unzipping the downloaded certificates, you will have a folder called **espcredentials**. Please navigate to the folder and modify **mfg_config.csv** (provided with the certificates) to reflect the correct paths for each file. Then run the commands below in the same folder to flash the certificates to device. Replace **<>** with the serial port your Core2 for AWS IoT EduKit device is connected to: +```bash +python $IDF_PATH/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py generate /path/to/mfg_config.csv mfg.bin 0x6000 + +python $IDF_PATH/components/esptool_py/esptool/esptool.py --chip esp32 --port <> write_flash 0x10000 mfg.bin +``` + +With everthing set up and ready, let's move on to [**Building and Testing AFI**](/en_uk/intro-to-alexa-for-iot/building-and-testing-afi.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/Introduction/_index.en.md b/workshop/content/Intro to Alexa For IoT/Introduction/_index.en.md new file mode 100644 index 0000000..dcdfccd --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/Introduction/_index.en.md @@ -0,0 +1,21 @@ ++++ +title = "Introduction" +weight = 10 +pre = "a. " ++++ + +## What is Alexa Voice Service (AVS)? +The Alexa Voice Service (AVS) enables developers to turn just about any connected device into an Alexa device! AVS software integrates directly into products and provides the ability to add the Alexa voice assistant. AVS provides developers with access to a suite of resources to build Alexa-enabled products, including APIs, hardware development kits, software development kits, and documentation. +[Learn more >>](https://developer.amazon.com/en-US/alexa/devices/alexa-built-in) + +## What is Alexa Voice Service Integration for AWS IoT (AFI)? +Alexa Voice Service (AVS) Integration is a new feature of AWS IoT Core that enables device makers to make any connected device an Alexa Built-in device. AVS for AWS IoT reduces both the cost and complexity of producing Alexa Built-in devices by offloading compute and memory intensive tasks from physical devices to the cloud. With the reduction in the engineering bill of materials (eBoM) cost, device makers can now cost-effectively build new categories of differentiated voice-enabled products such as light switches, thermostats, small appliances and more. +[Learn more >>](https://docs.aws.amazon.com/iot/latest/developerguide/avs-integration-aws-iot.html) + +## Let's go! +All content in this tutorial assumes you presently have the [M5Stack Core2 ESP32 IoT Development Kit for AWS](https://www.amazon.com/dp/B08NP5LVFH) in your possession, a compatible iOS or Android smart phone to use the Amazon Alexa app and ESP Alexa Phone Application, and are comfortable with basic technical concepts and tools—such as the command prompt/terminal. To first purchase your own kit, visit [Amazon.com](https://www.amazon.com/dp/B08NP5LVFH) or [M5Stack.com](https://m5stack.com/products/m5stack-core2-esp32-iot-development-kit-for-aws-iot-edukit). + +If you're ready, let's begin by [**Flashing Espressif Certificates**](/en/intro-to-alexa-for-iot/flashing-espressif-certificates.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/Introduction/_index.en_uk.md b/workshop/content/Intro to Alexa For IoT/Introduction/_index.en_uk.md new file mode 100644 index 0000000..94cae15 --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/Introduction/_index.en_uk.md @@ -0,0 +1,21 @@ ++++ +title = "Introduction" +weight = 10 +pre = "a. " ++++ + +## What is Alexa Voice Service (AVS)? +The Alexa Voice Service (AVS) enables developers to turn just about any connected device into an Alexa device! AVS software integrates directly into products and provides the ability to add the Alexa voice assistant. AVS provides developers with access to a suite of resources to build Alexa-enabled products, including APIs, hardware development kits, software development kits, and documentation. +[Learn more >>](https://developer.amazon.com/en-US/alexa/devices/alexa-built-in) + +## What is Alexa Voice Service Integration for AWS IoT (AFI)? +Alexa Voice Service (AVS) Integration is a new feature of AWS IoT Core that enables device makers to make any connected device an Alexa Built-in device. AVS for AWS IoT reduces both the cost and complexity of producing Alexa Built-in devices by offloading compute and memory intensive tasks from physical devices to the cloud. With the reduction in the engineering bill of materials (eBoM) cost, device makers can now cost-effectively build new categories of differentiated voice-enabled products such as light switches, thermostats, small appliances and more. +[Learn more >>](https://docs.aws.amazon.com/iot/latest/developerguide/avs-integration-aws-iot.html) + +## Let's go! +All content in this tutorial assumes you presently have the [M5Stack Core2 ESP32 IoT Development Kit for AWS](https://www.amazon.com/dp/B08NP5LVFH) in your possession, a compatible iOS or Android smart phone to use the Amazon Alexa app and ESP Alexa Phone Application, and are comfortable with basic technical concepts and tools—such as the command prompt/terminal. To first purchase your own kit, visit [Amazon.com](https://www.amazon.com/dp/B08NP5LVFH) or [M5Stack.com](https://m5stack.com/products/m5stack-core2-esp32-iot-development-kit-for-aws-iot-edukit). + +If you're ready, let's begin by [**Flashing Espressif Certificates**](/en_uk/intro-to-alexa-for-iot/flashing-espressif-certificates.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/_index.en.md b/workshop/content/Intro to Alexa For IoT/_index.en.md new file mode 100644 index 0000000..7ff2d43 --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/_index.en.md @@ -0,0 +1,18 @@ ++++ +title = "Intro to Alexa for IoT" +chapter = true +weight = 50 +pre = "5. " ++++ + +In this tutorial, you will implement Alexa Voice Service Integration for AWS IoT (AFI) on the M5Stack Core2 for AWS IoT EduKit reference hardware kit. You will learn how to use the Espressif Voice Assistant SDK (VA-SDK) and Alexa to control the onboard LED using Alexa Smart Home commands. This tutorial currently uses an AWS account provided by Espressif. This is a *beta* port of the Espressif VA-SDK for the M5Stack Core2 for AWS IoT EduKit reference hardware. + +Assumptions. Before starting this tutorial, verify the following prerequisites: + +1. You have an [M5Stack Core2 ESP32 IoT Development Kit for AWS IoT EduKit](https://www.amazon.com/dp/B08NP5LVFH). +2. You have the necessary toolchain and dependent software installed to build and flash firmware to your M5Stack Core2 for AWS IoT EduKit reference hardware and know which serial port your Core2 for AWS IoT EduKit device is connected to. It is highly recommended you complete the full [**Blinky Hello World**](/en/blinky-hello-world.html) tutorial first. +3. You have at least a basic technical understanding of AWS IoT messaging concepts such as topics, publishing, and subscribing. +4. You have an IDE installed like Visual Studio Code, as covered in the [**Getting Started**](/en/getting-started.html) tutorial. + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Intro to Alexa For IoT/_index.en_uk.md b/workshop/content/Intro to Alexa For IoT/_index.en_uk.md new file mode 100644 index 0000000..d959f2b --- /dev/null +++ b/workshop/content/Intro to Alexa For IoT/_index.en_uk.md @@ -0,0 +1,18 @@ ++++ +title = "Intro to Alexa for IoT" +chapter = true +weight = 50 +pre = "5. " ++++ + +In this tutorial, you will implement Alexa Voice Service Integration for AWS IoT (AFI) on the M5Stack Core2 for AWS IoT EduKit reference hardware kit. You will learn how to use the Espressif Voice Assistant SDK (VA-SDK) and Alexa to control the onboard LED using Alexa Smart Home commands. This tutorial currently uses an AWS account provided by Espressif. This is a *beta* port of the Espressif VA-SDK for the M5Stack Core2 for AWS IoT EduKit reference hardware. + +Assumptions. Before starting this tutorial, verify the following prerequisites: + +1. You have an [M5Stack Core2 ESP32 IoT Development Kit for AWS IoT EduKit](https://www.amazon.com/dp/B08NP5LVFH). +2. You have the necessary toolchain and dependent software installed to build and flash firmware to your M5Stack Core2 for AWS IoT EduKit reference hardware and know which serial port your Core2 for AWS IoT EduKit device is connected to. It is highly recommended you complete the full [**Blinky Hello World**](/en_uk/blinky-hello-world.html) tutorial first. +3. You have at least a basic technical understanding of AWS IoT messaging concepts such as topics, publishing, and subscribing. +4. You have an IDE installed like Visual Studio Code, as covered in the [**Getting Started**](/en_uk/getting-started.html) tutorial. + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/Conclusion/_index.en.md b/workshop/content/Smart spaces/Conclusion/_index.en.md new file mode 100644 index 0000000..487a4e3 --- /dev/null +++ b/workshop/content/Smart spaces/Conclusion/_index.en.md @@ -0,0 +1,55 @@ ++++ +title = "Conclusion" +weight = 50 +pre = "e. " ++++ + +## Conclusion +You have completed this hands-on module to upgrade your smart thermostat application to take advantage of a trained machine learning model. You have now previewed the end-to-end experience of building an IoT application: acquiring physical data from sensors, synchronizing state between the edge and the cloud, building a serverless application to control an edge device, and transforming data in to actionable insights with machine learning. + +If you are looking for ideas how to continue extending the solution, here are a few: + +* Add a timer to the IoT Analytics data set so that it builds an updated content file once per day +* Build a simple web UI or provision Amazon QuickSight to visualize the data set created in IoT Analytics +* Add a new pipeline actitivy in IoT Analytics to fetch the local weather via a Lambda function and add it to your thermostat data +* Use the Core2 for AWS IoT EduKit reference hardware's libraries to emit a sound whenever the hvacStatus changes +* Use the Core2 for AWS IoT EduKit reference hardware's libraries to flash the LED strips whenever the roomOccupancy changes + +## Clean up +Between this solution and the previous one (**Smart thermostat**), you created the following resources in AWS: + +* IoT Core rules +* IAM roles +* IoT Events detector model +* IoT Analytics project (channel, pipeline, data store, data set) +* Lambda function +* SageMaker Studio +* S3 bucket (part of the SageMaker Studio project) +* SageMaker model group +* SageMaker endpoint + +Your account will continue to accrue metered charges in three ways. First, for provisioned compute resources like the SageMaker endpoint that is hosted on a virtual instance. These resources have hourly charges. Second, for storage of data in resources like S3 buckets and your IoT Analytics project (channel, data store, and processed data set results). These resources have billing dimensions like Gigabyte-month. Third, for event-driven activity like publishing messages to IoT Core, invoking the Lambda function, and processing events in IoT Events, you are billed per event. So if you were to turn off your device, the event-driven activity would cease. You should destroy any of these AWS resources if you are done with the solution and don't intend to continue using them. + +The SageMaker endpoint is the most expensive resource as it it has an hourly charge. It is recommended to destroy this resource when you are done using it. Your model itself will persist and could be redeployed later. Steps to destroy the SageMaker endpoint: + +1. Go to the SageMaker management console, choose Endpoints, find your endpoint in the list and delete it. + +Steps to destroy IoT Analytics storage resources: + +1. Go to AWS IoT Analytics, choose Channels, find your channel in the list and delete it. +2. Go to AWS IoT Analytics, choose Data stores, find your data store in the list and delete it. +3. Go to AWS IoT Analytics, choose Data sets, find your data set in the list and delete it. + +The IoT Core rule, Lambda function, IoT Events input and detector, and IoT Analytics pipeline only incur further charges as you use them. If you no longer need them, you can destroy each by deleting them from their respective resource detail pages in their respective management consoles. + +1. Go to AWS IoT Core, choose Act, choose Rules, find your rules in the list and delete it. +2. Go to AWS IoT Analytics, choose Pipelines, find your pipeline in the list and delete it. +3. Go to AWS Lambda, choose Functions, find your function in the list and delete it. +4. Go to AWS IoT Events, choose Inputs, find your input in the list and delete it. +5. Go to AWS IoT Events, choose Detector models, find your model in the list and delete it. + + +Go to the next tutorial, [**Intro to Alexa for IoT**](/en/intro-to-alexa-for-iot.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/Conclusion/_index.en_uk.md b/workshop/content/Smart spaces/Conclusion/_index.en_uk.md new file mode 100644 index 0000000..0edc852 --- /dev/null +++ b/workshop/content/Smart spaces/Conclusion/_index.en_uk.md @@ -0,0 +1,55 @@ ++++ +title = "Conclusion" +weight = 50 +pre = "e. " ++++ + +## Conclusion +You have completed this hands-on module to upgrade your smart thermostat application to take advantage of a trained machine learning model. You have now previewed the end-to-end experience of building an IoT application: acquiring physical data from sensors, synchronizing state between the edge and the cloud, building a serverless application to control an edge device, and transforming data in to actionable insights with machine learning. + +If you are looking for ideas how to continue extending the solution, here are a few: + +* Add a timer to the IoT Analytics data set so that it builds an updated content file once per day +* Build a simple web UI or provision Amazon QuickSight to visualize the data set created in IoT Analytics +* Add a new pipeline actitivy in IoT Analytics to fetch the local weather via a Lambda function and add it to your thermostat data +* Use the Core2 for AWS IoT EduKit reference hardware's libraries to emit a sound whenever the hvacStatus changes +* Use the Core2 for AWS IoT EduKit reference hardware's libraries to flash the LED strips whenever the roomOccupancy changes + +## Clean up +Between this solution and the previous one (**Smart thermostat**), you created the following resources in AWS: + +* IoT Core rules +* IAM roles +* IoT Events detector model +* IoT Analytics project (channel, pipeline, data store, data set) +* Lambda function +* SageMaker Studio +* S3 bucket (part of the SageMaker Studio project) +* SageMaker model group +* SageMaker endpoint + +Your account will continue to accrue metered charges in three ways. First, for provisioned compute resources like the SageMaker endpoint that is hosted on a virtual instance. These resources have hourly charges. Second, for storage of data in resources like S3 buckets and your IoT Analytics project (channel, data store, and processed data set results). These resources have billing dimensions like Gigabyte-month. Third, for event-driven activity like publishing messages to IoT Core, invoking the Lambda function, and processing events in IoT Events, you are billed per event. So if you were to turn off your device, the event-driven activity would cease. You should destroy any of these AWS resources if you are done with the solution and don't intend to continue using them. + +The SageMaker endpoint is the most expensive resource as it it has an hourly charge. It is recommended to destroy this resource when you are done using it. Your model itself will persist and could be redeployed later. Steps to destroy the SageMaker endpoint: + +1. Go to the SageMaker management console, choose Endpoints, find your endpoint in the list and delete it. + +Steps to destroy IoT Analytics storage resources: + +1. Go to AWS IoT Analytics, choose Channels, find your channel in the list and delete it. +2. Go to AWS IoT Analytics, choose Data stores, find your data store in the list and delete it. +3. Go to AWS IoT Analytics, choose Data sets, find your data set in the list and delete it. + +The IoT Core rule, Lambda function, IoT Events input and detector, and IoT Analytics pipeline only incur further charges as you use them. If you no longer need them, you can destroy each by deleting them from their respective resource detail pages in their respective management consoles. + +1. Go to AWS IoT Core, choose Act, choose Rules, find your rules in the list and delete it. +2. Go to AWS IoT Analytics, choose Pipelines, find your pipeline in the list and delete it. +3. Go to AWS Lambda, choose Functions, find your function in the list and delete it. +4. Go to AWS IoT Events, choose Inputs, find your input in the list and delete it. +5. Go to AWS IoT Events, choose Detector models, find your model in the list and delete it. + + +Go to the next tutorial, [**Intro to Alexa for IoT**](/en_uk/intro-to-alexa-for-iot.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/Data routing and storage/_index.en.md b/workshop/content/Smart spaces/Data routing and storage/_index.en.md new file mode 100644 index 0000000..a76f7bc --- /dev/null +++ b/workshop/content/Smart spaces/Data routing and storage/_index.en.md @@ -0,0 +1,58 @@ ++++ +title = "Data routing and storage" +weight = 20 +pre = "b. " ++++ + +## Chapter introduction +By the end of this chapter, your serverless application should do the following: + +* Forward messages received from the smart thermostat to a managed storage and analytics service +* Be able to run a query against your processed data to produce a materialized view of the results + +## Concepts for storing and analyzing IoT data +Up until this point, every aspect of your IoT solution has been ephemeral in the sense that each message is received, processed, and then discarded. In the case of the IoT Events detector model, there is a stateful entity that reacts to new messages, but otherwise there is no stored history of your thermostat messages. This is the step that establishes a data store for your thermostat messages. + +AWS offers many ways to store data in the cloud, and storing IoT data is no different. This solution advocates the use of a service called [AWS IoT Analytics](https://docs.aws.amazon.com/iotanalytics/latest/userguide/welcome.html), which is a managed service purpose built to receive, store, process, and analyze IoT data in bulk. You will use IoT Analytics to store a subset of each update to the device shadow. + +The AWS IoT Analytics documentation covers this in more detail, but here is a brief summary of how it works. The entry point to the service is a resource called a channel. A channel stores all the raw data for your workflow. It also sends a copy of each message received to the next resource, called a pipeline. A pipeline is a series of activities to process, cleanse, filter, and enrich data before it is used in analytics use cases. The processed messages are put from the pipeline into a data store. A data store, like a channel, is a long-lived storage unit for processed data. Finally, a data set is the last resource in the AWS IoT Analytics project. A data set defines a SQL-like query that can read messages out of a data store as a materialized view and deliver the contents of the query to a destination like an S3 bucket. + +Your smart space solution will accumulate multiple hours of runtime data from your thermostat in AWS IoT Analytics. After that, the result of a data set query will make this data available for use in our machine learning toolchain provided by Amazon SageMaker. + +There is much, much more to [AWS IoT Analytics](https://aws.amazon.com/iot-analytics/) but for the purposes of this module it is the easiest way to store the history of our thermostat messages and aggregate them as a training data set for our machine learning model. + +{{% notice warning %}} +Leaving this application running beyond 6 hours can result in AWS charges. It is recommended you finish this tutorial in that time and perform the [cleanup steps](/en/smart-spaces/conclusion.html#clean-up) to avoid any unwanted costs. +{{% /notice %}} + +## How to set up the serverless infrastructure +The following steps detail how to create a new IoT Core rule, a new AWS IoT Analytics project, and uses the rule to forward device shadow messages to your AWS IoT Analytics project. There's a handy interface in the IoT Core rule creation wizard for creating the entire AWS IoT Analytics project on your behalf! + +1. In the [AWS IoT Core console](https://us-west-2.console.aws.amazon.com/iot/home?region=us-west-2#/), choose **Act** then **Rules** then **Create**. +2. Give your rule a name like *storeInIoTAnalytics* and a description. +3. Use the following query. Be sure to replace the **<>** with the client Id/serial number printed on the screen of your Core2 for AWS IoT Edukit reference hardware kit. + +```SQL +SELECT current.state.reported.sound, current.state.reported.temperature, current.state.reported.hvacStatus, current.state.reported.roomOccupancy, timestamp FROM '$aws/things/<>/shadow/update/documents' +``` + +4. Choose **Add action** for *Set one or more actions*. +5. Select *Send a message to IoT Analytics* and choose **Configure action**. +6. Select *Quick create IoT Analytics resources* and provide a project name for *Resource prefix*. Further module steps assume the prefix is `smartspace`. Choose **Quick Create** and all your AWS IoT Analytics resources will be created and configured automatically. +7. Choose **Add action** to finish configuring this action and return to the rule creation form. +8. Choose **Create rule** to finish creating your new rule. + +## Validation steps + +Before moving on to the next chapter, you can validate that your serverless application is configured as intended by... + +1. Ensure that your smart thermostat is powered on, publishing data, and deployed in the room you want to train on. +2. Using the AWS IoT Analytics console, review the most recent data set contents and verify there are historical records of ambient noise levels, temperature, and room occupancy. To check this, find your data set in the [IoT Analytics console](https://us-west-2.console.aws.amazon.com/iotanalytics/home?region=us-west-2#/datasets), choose **Actions** and **Run now**, then wait for the *Result preview* to update with the latest content. You should see results similar to the following: + +{{< img "dataset-run.png" "Running the data set" >}} +{{< img "dataset-preview.png" "Preview of the data set" >}} + +If these are working as expected, let's move on to [**Machine learning**](/en/smart-spaces/machine-learning.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/Data routing and storage/_index.en_uk.md b/workshop/content/Smart spaces/Data routing and storage/_index.en_uk.md new file mode 100644 index 0000000..9485ede --- /dev/null +++ b/workshop/content/Smart spaces/Data routing and storage/_index.en_uk.md @@ -0,0 +1,52 @@ ++++ +title = "Data routing and storage" +weight = 20 +pre = "b. " ++++ + +## Chapter introduction +By the end of this chapter, your serverless application should do the following: + +* Forward messages received from the smart thermostat to a managed storage and analytics service +* Be able to run a query against your processed data to produce a materialized view of the results + +## Concepts for storing and analyzing IoT data +Up until this point, every aspect of your IoT solution has been ephemeral in the sense that each message is received, processed, and then discarded. In the case of the IoT Events detector model, there is a stateful entity that reacts to new messages, but otherwise there is no stored history of your thermostat messages. This is the step that establishes a data store for your thermostat messages. + +AWS offers many ways to store data in the cloud, and storing IoT data is no different. This solution advocates the use of a service called [AWS IoT Analytics](https://docs.aws.amazon.com/iotanalytics/latest/userguide/welcome.html), which is a managed service purpose built to receive, store, process, and analyze IoT data in bulk. You will use IoT Analytics to store a subset of each update to the device shadow. + +The AWS IoT Analytics documentation covers this in more detail, but here is a brief summary of how it works. The entry point to the service is a resource called a channel. A channel stores all the raw data for your workflow. It also sends a copy of each message received to the next resource, called a pipeline. A pipeline is a series of activities to process, cleanse, filter, and enrich data before it is used in analytics use cases. The processed messages are put from the pipeline into a data store. A data store, like a channel, is a long-lived storage unit for processed data. Finally, a data set is the last resource in the AWS IoT Analytics project. A data set defines a SQL-like query that can read messages out of a data store as a materialized view and deliver the contents of the query to a destination like an S3 bucket. + +Your smart space solution will accumulate multiple hours of runtime data from your thermostat in AWS IoT Analytics. After that, the result of a data set query will make this data available for use in our machine learning toolchain provided by Amazon SageMaker. + +There is much, much more to [AWS IoT Analytics](https://aws.amazon.com/iot-analytics/) but for the purposes of this module it is the easiest way to store the history of our thermostat messages and aggregate them as a training data set for our machine learning model. + +{{% notice warning %}} +Leaving this application running beyond 6 hours can result in AWS charges. It is recommended you finish this tutorial in that time and perform the [cleanup steps](/en_uk/smart-spaces/conclusion.html#clean-up) to avoid any unwanted costs. +{{% /notice %}} + +## How to set up the serverless infrastructure +The following steps detail how to create a new IoT Core rule, a new AWS IoT Analytics project, and uses the rule to forward device shadow messages to your AWS IoT Analytics project. There's a handy interface in the IoT Core rule creation wizard for creating the entire AWS IoT Analytics project on your behalf! + +1. In the [AWS IoT Core console](https://us-west-2.console.aws.amazon.com/iot/home?region=us-west-2#/), choose **Act** then **Rules** then **Create**. +2. Give your rule a name like *storeInIoTAnalytics* and a description. +3. Use the following query. Be sure to replace the **<>** with the client Id/serial number printed on the screen of your Core2 for AWS IoT Edukit reference hardware kit. +```SQL +SELECT current.state.reported.sound, current.state.reported.temperature, current.state.reported.hvacStatus, current.state.reported.roomOccupancy, timestamp FROM '$aws/things/<>/shadow/update/documents' +``` +4. Choose **Add action** for *Set one or more actions*. +5. Select *Send a message to IoT Analytics* and choose **Configure action**. +6. Select *Quick create IoT Analytics resources* and provide a project name for *Resource prefix*. Further module steps assume the prefix is `smartspace`. Choose **Quick Create** and all your AWS IoT Analytics resources will be created and configured automatically. +7. Choose **Add action** to finish configuring this action and return to the rule creation form. +8. Choose **Create rule** to finish creating your new rule. + +## Validation steps +Before moving on to the next chapter, you can validate that your serverless application is configured as intended by... +1. Ensure that your smart thermostat is powered on, publishing data, and deployed in the room you want to train on. +2. Using the AWS IoT Analytics console, review the most recent data set contents and verify there are historical records of ambient noise levels, temperature, and room occupancy. To check this, find your data set in the [IoT Analytics console](https://us-west-2.console.aws.amazon.com/iotanalytics/home?region=us-west-2#/datasets), choose **Actions** and **Run now**, then wait for the *Result preview* to update with the latest content. You should see results similar to the following: +{{< img "dataset-run.png" "Running the data set" >}} +{{< img "dataset-preview.png" "Preview of the data set" >}} +If these are working as expected, let's move on to [**Machine learning**](/en_uk/smart-spaces/machine-learning.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/Data routing and storage/dataset-preview.png b/workshop/content/Smart spaces/Data routing and storage/dataset-preview.png new file mode 100644 index 0000000..7311d95 Binary files /dev/null and b/workshop/content/Smart spaces/Data routing and storage/dataset-preview.png differ diff --git a/workshop/content/Smart spaces/Data routing and storage/dataset-run.png b/workshop/content/Smart spaces/Data routing and storage/dataset-run.png new file mode 100644 index 0000000..3b05f89 Binary files /dev/null and b/workshop/content/Smart spaces/Data routing and storage/dataset-run.png differ diff --git a/workshop/content/Smart spaces/Introduction/_index.en.md b/workshop/content/Smart spaces/Introduction/_index.en.md new file mode 100644 index 0000000..05007eb --- /dev/null +++ b/workshop/content/Smart spaces/Introduction/_index.en.md @@ -0,0 +1,39 @@ ++++ +title = "Introduction" +weight = 10 +pre = "a. " ++++ + +## Your task + +In this scenario, you are resuming the role of a full-stack developer tasked with automating thermostat functions of a meeting room to conserve energy. Now you are exploring how to improve the mechanism for determining whether the room is occupied over the simple noise threshold from the previous module. A simple machine learning model could help you identify a more intelligent classification than a static numerical threshold and take into consideration the past room occupation data to reduce jitter or false positives. + +Training a machine learning model is relatively easy these days with the modern data science tool chain. Training a *good* model, however, is hard and requires expertise, a comprehensive understanding of inputs, and cycles to optimize. Today, you will exercise the Amazon SageMaker tool chain to learn how IoT data can be used to train a model, but we can't expect that a *good* model will be produced on the first try. + +Please note that completion of this module will span approximately two days' time. While you only have an hour of hands-on work to complete, there are two steps where you will be hands-off. The first step is deploying your smart thermostat device in the room you want to study and gather telemetry to store for ML training. You will want to deploy your device and gather data for a few hours at least (preferably 24 hours), but the more data you gather, the more accurate the ML model can be. The second hands-off step will be when the ML model is going through an automated training process once you have sufficient data gathered. This training process can take several hours and the author recommends starting it in the morning and returning to it in the afternoon or letting it run overnight. + +## Problem solving + +The solution produced in the last module used a static numeric threshold on the incoming sound level to coerce a new key-value called *roomOccupancy*. To train a simple machine learning model to perform a similar function, you will use existing data as a baseline for training. This means you will need to run the existing solution for several hours in a room that has alternating states of the room actively being occupied or not. You will use an aggregated data set of that device telemetry to power an automated ML training experiment that will then be used to classify new device reports with a new *roomOccupancy* value inferred from your trained ML model. Again, your first trained model may not be all that accurate, but the purpose of this solution is to give you hands-on experience with the process of storing IoT data and exercising the workflow of training and consuming a new ML model. + +## Solution architecture +![Smart Spaces architecture](introduction/smartspace-overview.png) + +The workflow that you will deliver in this module has the following key components: + +1. A new IoT Core rule will forward device shadow updates to a service called AWS IoT Analytics. IoT Analytics is a service for storing raw IoT data in bulk, performing transforms and cleansing operations to turn raw data into processed data, and provides a query engine for slicing processed data for analytical workflows or ML training. +2. Your IoT Analytics project will consist of four resources linked together: a channel for storing raw IoT data from the device shadow, a pipeline for data transforms/filtering/enriching, a data store for processed data, and a data set that runs saved queries and can send the results for processing. +3. Amazon SageMaker Studio is an integrated machine learning environment where you can build, train, deploy, and analyze your models all in the same application. +4. An Amazon SageMaker endpoint that hosts your trained model as a consumable API. +5. An AWS Lambda function that will run some simple code to process your thermostat published messages and make inferences against your new ML model endpoint. + +## Let's go! +Are you ready to start building? Let's review you have the following prerequisites sorted: +1. Have you completed the previous module in this set of tutorials, titled **Smart Thermostat**? +2. Do you know which serial port your Core2 for AWS IoT EduKit device is connected to? This is also covered in the **Blinky Hello World** tutorial. You will paste it in to several of the steps of this tutorial for placeholder values like `<>`. +3. Have you confirmed that you can see messages arriving from your smart thermostat using a test client like the one in the AWS IoT Core console? You should be able to subscribe to the topic `$aws/things/<>/shadow/update/accepted` (replacing <> with your device's client Id/serial number) and see messages arrive in the test client. + +If so, let's begin by moving on to the next chapter, [**Data routing and storage**](/en/smart-spaces/data-routing-and-storage.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/Introduction/_index.en_uk.md b/workshop/content/Smart spaces/Introduction/_index.en_uk.md new file mode 100644 index 0000000..2b96ebc --- /dev/null +++ b/workshop/content/Smart spaces/Introduction/_index.en_uk.md @@ -0,0 +1,39 @@ ++++ +title = "Introduction" +weight = 10 +pre = "a. " ++++ + +## Your task + +In this scenario, you are resuming the role of a full-stack developer tasked with automating thermostat functions of a meeting room to conserve energy. Now you are exploring how to improve the mechanism for determining whether the room is occupied over the simple noise threshold from the previous module. A simple machine learning model could help you identify a more intelligent classification than a static numerical threshold and take into consideration the past room occupation data to reduce jitter or false positives. + +Training a machine learning model is relatively easy these days with the modern data science tool chain. Training a *good* model, however, is hard and requires expertise, a comprehensive understanding of inputs, and cycles to optimize. Today, you will exercise the Amazon SageMaker tool chain to learn how IoT data can be used to train a model, but we can't expect that a *good* model will be produced on the first try. + +Please note that completion of this module will span approximately two days' time. While you only have an hour of hands-on work to complete, there are two steps where you will be hands-off. The first step is deploying your smart thermostat device in the room you want to study and gather telemetry to store for ML training. You will want to deploy your device and gather data for a few hours at least (preferably 24 hours), but the more data you gather, the more accurate the ML model can be. The second hands-off step will be when the ML model is going through an automated training process once you have sufficient data gathered. This training process can take several hours and the author recommends starting it in the morning and returning to it in the afternoon or letting it run overnight. + +## Problem solving + +The solution produced in the last module used a static numeric threshold on the incoming sound level to coerce a new key-value called *roomOccupancy*. To train a simple machine learning model to perform a similar function, you will use existing data as a baseline for training. This means you will need to run the existing solution for several hours in a room that has alternating states of the room actively being occupied or not. You will use an aggregated data set of that device telemetry to power an automated ML training experiment that will then be used to classify new device reports with a new *roomOccupancy* value inferred from your trained ML model. Again, your first trained model may not be all that accurate, but the purpose of this solution is to give you hands-on experience with the process of storing IoT data and exercising the workflow of training and consuming a new ML model. + +## Solution architecture +![Smart Spaces architecture](introduction/smartspace-overview.png) + +The workflow that you will deliver in this module has the following key components: + +1. A new IoT Core rule will forward device shadow updates to a service called AWS IoT Analytics. IoT Analytics is a service for storing raw IoT data in bulk, performing transforms and cleansing operations to turn raw data into processed data, and provides a query engine for slicing processed data for analytical workflows or ML training. +2. Your IoT Analytics project will consist of four resources linked together: a channel for storing raw IoT data from the device shadow, a pipeline for data transforms/filtering/enriching, a data store for processed data, and a data set that runs saved queries and can send the results for processing. +3. Amazon SageMaker Studio is an integrated machine learning environment where you can build, train, deploy, and analyze your models all in the same application. +4. An Amazon SageMaker endpoint that hosts your trained model as a consumable API. +5. An AWS Lambda function that will run some simple code to process your thermostat published messages and make inferences against your new ML model endpoint. + +## Let's go! +Are you ready to start building? Let's review you have the following prerequisites sorted: +1. Have you completed the previous module in this set of tutorials, titled **Smart Thermostat**? +2. Do you know which serial port your Core2 for AWS IoT EduKit device is connected to? This is also covered in the **Blinky Hello World** tutorial. You will paste it in to several of the steps of this tutorial for placeholder values like `<>`. +3. Have you confirmed that you can see messages arriving from your smart thermostat using a test client like the one in the AWS IoT Core console? You should be able to subscribe to the topic `$aws/things/<>/shadow/update/accepted` (replacing <> with your device's client Id/serial number) and see messages arrive in the test client. + +If so, let's begin by moving on to the next chapter, [**Data routing and storage**](/en_uk/smart-spaces/data-routing-and-storage.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/Introduction/smartspace-overview.png b/workshop/content/Smart spaces/Introduction/smartspace-overview.png new file mode 100644 index 0000000..3a158f6 Binary files /dev/null and b/workshop/content/Smart spaces/Introduction/smartspace-overview.png differ diff --git a/workshop/content/Smart spaces/Machine learning/_index.en.md b/workshop/content/Smart spaces/Machine learning/_index.en.md new file mode 100644 index 0000000..aafe15d --- /dev/null +++ b/workshop/content/Smart spaces/Machine learning/_index.en.md @@ -0,0 +1,98 @@ ++++ +title = "Machine learning" +weight = 30 +pre = "c. " ++++ + +## Chapter introduction +By the end of this chapter, your serverless application should do the following: + +* Trains a machine learning model that translates new thermostat messages into inferences of roomOccupancy +* Hosts a machine learning model on an API endpoint for consumption + +## Concepts for training a machine learning model +Data science and machine learning are enormous domains in their own right. It is far beyond the scope of this module to teach the basics of how machine learning model training works. Fortunately, the tool chain for creating new models has been simplified to the point that we can use that tool chain to experiment with ML just by knowing enough about our data. + +In this solution, you are trying to replace a simple threshold in the IoT Core rules engine that evalutes the sound level reported by the device and coerces a new Boolean key-value called *roomOccupancy*. You know from looking at the sound data in the reported messages that the values are low when it is quiet and higher when there is noise. That's how you know a simple threshold like "greater than 10" was an okay starting place to generate the *roomOccupancy* value. (In your specific case, a different threshold for ambient noise versus registered activity may have been more appropriate!) + +You will apply a similar approach by giving the machine learning tool chain a sample of thermostat data recorded from the room where your device is deployed and telling the training job "this is what the data looks like when the roomOccupancy should be true, and what it looks like when it is false." The training job will evaluate your data set, target the *roomOccupancy* column, and try to build a model that accurately reproduces *roomOccupancy* values based on the sound level ranges and even the other columns like time, HVAC status, and temperature. + +Once your first model is trained, you will in the following chapter _replace_ that simple static threshold on the sound level with the inferred classification of *roomOccupancy* as determined by your model! + +## How to set up the serverless infrastructure +First, you will need to set up Amazon SageMaker Studio in order to configure a new experiment for automatic model training. + +1. Go to the Amazon SageMaker console and choose **Amazon SageMaker Studio**. +2. Select *Quick start* and optionally enter a new *User name*. +3. For *Execution role* select the drop down and choose **Create a new IAM role**. +4. For *S3 buckets you specify* select **None** and then choose **Create role**. The other defaults for buckets with "sagemaker" in the name are sufficient for this project. +5. Choose **Submit** to start the provisioning process of SageMaker Studio. This step will take a few minutes to complete on your behalf. + +Once your SageMaker Studio has finished provisioning, the next step is to open your Studio and configure a new project. + +1. From the SageMaker Studio Control Panel, choose **Open Studio**. +2. In the Launcher tab, choose **New project**. +3. Under *SageMaker project templates* select *MLOps template for model building, training, and deployment* then choose **Select project template**. +4. Give your project a name and description, then choose **Create project**. + +Once the project is created, you will see a project dashboard with tabs like Repositories, Pipelines, Experiments, and so on. Leave open this browser tab to SageMaker Studio so you can quickly return to this page. + +The next task is to return to AWS IoT Analytics so you can export the aggregated thermostat data for use by your new ML project. + +1. Open the [AWS IoT Analytics console](https://us-west-2.console.aws.amazon.com/iotanalytics/home?region=us-west-2#/datasets) and choose your data set (assumed name is `smartspace_dataset`). +2. Under *Data set content delivery rules* choose **Edit**. +3. Choose **Add rule**, then choose **Deliver result to S3**. +4. Under **S3 bucket** choose **Please select a resource** and find the S3 bucket created for your SageMaker Studio project. It will be named like `sagemaker-project-p-somehashhere`. If there are multiple buckets named like this, you'll need to check the SageMaker Studio project for the random hash ID of your project. You can see the hash in other resources of your project like the Repositories and Pipelines tabs. +5. Under *Bucket key expression* use this expression: `data/smartspace/Version/!{iotanalytics:scheduleTime}_!{iotanalytics:versionId}.csv` +6. Under *Role* choose **Create new** and provide a name for the IAM role that will grant IoT Analytics access to write data to your S3 bucket. Choose **Create role**. +7. Choose **Save** to finalize your new delivery rule. + +{{< img "iota-dataset-delivery.png" "Content delivery rule" >}} + +8. To generate your data set that gets saved to your new Amazon S3 bucket for training, choose **Actions** then **Run now**. You should see the *Result preview* update when the data set content is done generating. + +{{< img "dataset-run.png" "Running the data set" >}} +{{< img "dataset-preview.png" "Preview of the data set" >}} + +You are now ready to start your ML experiment back in SageMaker Studio. An experiment will use the reported thermostat data that was just exported by your IoT Analytics data set as inputs. You will configure the experiment to look for ways to accurately predict the existing roomOccupancy column. The automatic training job will analyze your data for relevant algorithms to try, then run 250 training jobs with varying hyperparameters, selecting the one that gives the best fit to your input training data. + +Before starting your ML experiment, you should have several hours of data reported from your thermostat in the room you want to analyze, and in that time the room should have had a mix of active and inactive periods. An automatic ML experiment needs at least 500 rows of data to work, but the more data you bring the better the result will be. If you still need to generate more data before proceeding, don't forget to re-run the data set in the IoT Analytics console (last step of the previous instruction list) so that those results are available to SageMaker in your project S3 bucket. When you're ready to start your experiment, read on. + +1. Return to your SageMaker Studio, open your project, select the *Experiments* tab and choose **Create autopilot experiment**. +2. Give your experiment a name. +3. Under *Project* select your project from the list. +4. Under *Connect your data* and *S3 bucket name* find and select your project's S3 bucket in the list. This is the same one you selected for the IoT Analytics data set content delivery rule in the previous chapter. +5. Under *Dataset file name* find and select your IoT Analytics dataset content like `data/smartspace/Version/1607276270943_3b4eb6bb-8533-4ac0-b8fd-1b62ac0020a2.csv`. +6. Under *Target* choose `roomoccupancy`. +7. Under *Output data location* and *S3 bucket name* find and choose the same project S3 bucket in this list that you picked in step 4. +8. Under *Dataset directory name* type in `output/smartspace` and choose **Use input as S3 object key prefix "output/smartspace"**. This defines a new prefix in the S3 bucket that will be used for your output files. +9. Choose **Create Experiment** to start the automated ML experiment. + +Running the experiment might take minutes to hours. You can follow along the experiment's progress in the SageMaker Studio browser tab, but it is also safe to close the tab and come back later to check progress. + +Once the experiment has concluded, the resultant output is 250 trials that SageMaker used to find the best tuning job parameters. Sort the table of trials to find the one marked *Best*. The next milestone is to deploy this trial as a model endpoint so that you can invoke it as an API. + +1. Select the trial marked *Best* and choose **Deploy model**. + {{< img "sagemaker-trials.png" "SageMaker trials" >}} +2. Give your endpoint a name. Further steps in this module assume the name `roomOccupancyEndpoint`. +3. Under *Inference Response Content*, select both *predicted_label* and *probability*. *predicted_label* may already have been added to the list. +4. Choose **Deploy model** to tell SageMaker to deploy your model as a new consumable API endpoint. This will take several minutes. + +{{< img "sagemaker-deploy.png" "SageMaker deploy" >}} + +Now your machine learning model is deployed as an API endpoint, managed by Amazon SageMaker. In the next chapter, **Working with ML models**, you will consume the API endpoint with a serverless function and replace the simple threshold logic in the IoT Core rule that determines the `roomOccupancy` value with inferences generated by your model. + +{{% notice warning %}} +Leaving this application running beyond 6 hours can result in AWS charges due to number of requests to the S3 bucket. It is recommended you finish this tutorial in that time and perform the [cleanup steps](/en/smart-spaces/conclusion.html#clean-up) to avoid any unwanted costs. +{{% /notice %}} + +## Validation steps +Before moving on to the next chapter, you can validate that your serverless application is configured as intended by... +1. Using the [Amazon SageMaker console](https://us-west-2.console.aws.amazon.com/sagemaker/home?region=us-west-2#/endpoints), you should be able to see your new endpoint with status *InService*, on the *Endpoints* page. + +{{< img "sagemaker-endpoints.png" "SageMaker endpoints" >}} + +If these are working as expected, let's move on to [**Working with ML models**](/en/smart-spaces/working-with-ml-models.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/Machine learning/_index.en_uk.md b/workshop/content/Smart spaces/Machine learning/_index.en_uk.md new file mode 100644 index 0000000..c372af6 --- /dev/null +++ b/workshop/content/Smart spaces/Machine learning/_index.en_uk.md @@ -0,0 +1,98 @@ ++++ +title = "Machine learning" +weight = 30 +pre = "c. " ++++ + +## Chapter introduction +By the end of this chapter, your serverless application should do the following: + +* Trains a machine learning model that translates new thermostat messages into inferences of roomOccupancy +* Hosts a machine learning model on an API endpoint for consumption + +## Concepts for training a machine learning model +Data science and machine learning are enormous domains in their own right. It is far beyond the scope of this module to teach the basics of how machine learning model training works. Fortunately, the tool chain for creating new models has been simplified to the point that we can use that tool chain to experiment with ML just by knowing enough about our data. + +In this solution, you are trying to replace a simple threshold in the IoT Core rules engine that evalutes the sound level reported by the device and coerces a new Boolean key-value called *roomOccupancy*. You know from looking at the sound data in the reported messages that the values are low when it is quiet and higher when there is noise. That's how you know a simple threshold like "greater than 10" was an okay starting place to generate the *roomOccupancy* value. (In your specific case, a different threshold for ambient noise versus registered activity may have been more appropriate!) + +You will apply a similar approach by giving the machine learning tool chain a sample of thermostat data recorded from the room where your device is deployed and telling the training job "this is what the data looks like when the roomOccupancy should be true, and what it looks like when it is false." The training job will evaluate your data set, target the *roomOccupancy* column, and try to build a model that accurately reproduces *roomOccupancy* values based on the sound level ranges and even the other columns like time, HVAC status, and temperature. + +Once your first model is trained, you will in the following chapter _replace_ that simple static threshold on the sound level with the inferred classification of *roomOccupancy* as determined by your model! + +## How to set up the serverless infrastructure +First, you will need to set up Amazon SageMaker Studio in order to configure a new experiment for automatic model training. + +1. Go to the Amazon SageMaker console and choose **Amazon SageMaker Studio**. +2. Select *Quick start* and optionally enter a new *User name*. +3. For *Execution role* select the drop down and choose **Create a new IAM role**. +4. For *S3 buckets you specify* select **None** and then choose **Create role**. The other defaults for buckets with "sagemaker" in the name are sufficient for this project. +5. Choose **Submit** to start the provisioning process of SageMaker Studio. This step will take a few minutes to complete on your behalf. + +Once your SageMaker Studio has finished provisioning, the next step is to open your Studio and configure a new project. + +1. From the SageMaker Studio Control Panel, choose **Open Studio**. +2. In the Launcher tab, choose **New project**. +3. Under *SageMaker project templates* select *MLOps template for model building, training, and deployment* then choose **Select project template**. +4. Give your project a name and description, then choose **Create project**. + +Once the project is created, you will see a project dashboard with tabs like Repositories, Pipelines, Experiments, and so on. Leave open this browser tab to SageMaker Studio so you can quickly return to this page. + +The next task is to return to AWS IoT Analytics so you can export the aggregated thermostat data for use by your new ML project. + +1. Open the [AWS IoT Analytics console](https://us-west-2.console.aws.amazon.com/iotanalytics/home?region=us-west-2#/datasets) and choose your data set (assumed name is `smartspace_dataset`). +2. Under *Data set content delivery rules* choose **Edit**. +3. Choose **Add rule**, then choose **Deliver result to S3**. +4. Under **S3 bucket** choose **Please select a resource** and find the S3 bucket created for your SageMaker Studio project. It will be named like `sagemaker-project-p-somehashhere`. If there are multiple buckets named like this, you'll need to check the SageMaker Studio project for the random hash ID of your project. You can see the hash in other resources of your project like the Repositories and Pipelines tabs. +5. Under *Bucket key expression* use this expression: `data/smartspace/Version/!{iotanalytics:scheduleTime}_!{iotanalytics:versionId}.csv` +6. Under *Role* choose **Create new** and provide a name for the IAM role that will grant IoT Analytics access to write data to your S3 bucket. Choose **Create role**. +7. Choose **Save** to finalize your new delivery rule. + +{{< img "iota-dataset-delivery.png" "Content delivery rule" >}} + +8. To generate your data set that gets saved to your new Amazon S3 bucket for training, choose **Actions** then **Run now**. You should see the *Result preview* update when the data set content is done generating. + +{{< img "dataset-run.png" "Running the data set" >}} +{{< img "dataset-preview.png" "Preview of the data set" >}} + +You are now ready to start your ML experiment back in SageMaker Studio. An experiment will use the reported thermostat data that was just exported by your IoT Analytics data set as inputs. You will configure the experiment to look for ways to accurately predict the existing roomOccupancy column. The automatic training job will analyze your data for relevant algorithms to try, then run 250 training jobs with varying hyperparameters, selecting the one that gives the best fit to your input training data. + +Before starting your ML experiment, you should have several hours of data reported from your thermostat in the room you want to analyze, and in that time the room should have had a mix of active and inactive periods. An automatic ML experiment needs at least 500 rows of data to work, but the more data you bring the better the result will be. If you still need to generate more data before proceeding, don't forget to re-run the data set in the IoT Analytics console (last step of the previous instruction list) so that those results are available to SageMaker in your project S3 bucket. When you're ready to start your experiment, read on. + +1. Return to your SageMaker Studio, open your project, select the *Experiments* tab and choose **Create autopilot experiment**. +2. Give your experiment a name. +3. Under *Project* select your project from the list. +4. Under *Connect your data* and *S3 bucket name* find and select your project's S3 bucket in the list. This is the same one you selected for the IoT Analytics data set content delivery rule in the previous chapter. +5. Under *Dataset file name* find and select your IoT Analytics dataset content like `data/smartspace/Version/1607276270943_3b4eb6bb-8533-4ac0-b8fd-1b62ac0020a2.csv`. +6. Under *Target* choose `roomoccupancy`. +7. Under *Output data location* and *S3 bucket name* find and choose the same project S3 bucket in this list that you picked in step 4. +8. Under *Dataset directory name* type in `output/smartspace` and choose **Use input as S3 object key prefix "output/smartspace"**. This defines a new prefix in the S3 bucket that will be used for your output files. +9. Choose **Create Experiment** to start the automated ML experiment. + +Running the experiment might take minutes to hours. You can follow along the experiment's progress in the SageMaker Studio browser tab, but it is also safe to close the tab and come back later to check progress. + +Once the experiment has concluded, the resultant output is 250 trials that SageMaker used to find the best tuning job parameters. Sort the table of trials to find the one marked *Best*. The next milestone is to deploy this trial as a model endpoint so that you can invoke it as an API. + +1. Select the trial marked *Best* and choose **Deploy model**. + {{< img "sagemaker-trials.png" "SageMaker trials" >}} +2. Give your endpoint a name. Further steps in this module assume the name `roomOccupancyEndpoint`. +3. Under *Inference Response Content*, select both *predicted_label* and *probability*. *predicted_label* may already have been added to the list. +4. Choose **Deploy model** to tell SageMaker to deploy your model as a new consumable API endpoint. This will take several minutes. + +{{< img "sagemaker-deploy.png" "SageMaker deploy" >}} + +Now your machine learning model is deployed as an API endpoint, managed by Amazon SageMaker. In the next chapter, **Working with ML models**, you will consume the API endpoint with a serverless function and replace the simple threshold logic in the IoT Core rule that determines the `roomOccupancy` value with inferences generated by your model. + +{{% notice warning %}} +Leaving this application running beyond 6 hours can result in AWS charges due to number of requests to the S3 bucket. It is recommended you finish this tutorial in that time and perform the [cleanup steps](/en_uk/smart-spaces/conclusion.html#clean-up) to avoid any unwanted costs. +{{% /notice %}} + +## Validation steps +Before moving on to the next chapter, you can validate that your serverless application is configured as intended by... +1. Using the [Amazon SageMaker console](https://us-west-2.console.aws.amazon.com/sagemaker/home?region=us-west-2#/endpoints), you should be able to see your new endpoint with status *InService*, on the *Endpoints* page. + +{{< img "sagemaker-endpoints.png" "SageMaker endpoints" >}} + +If these are working as expected, let's move on to [**Working with ML models**](/en_uk/smart-spaces/working-with-ml-models.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/Machine learning/dataset-preview.png b/workshop/content/Smart spaces/Machine learning/dataset-preview.png new file mode 100644 index 0000000..7311d95 Binary files /dev/null and b/workshop/content/Smart spaces/Machine learning/dataset-preview.png differ diff --git a/workshop/content/Smart spaces/Machine learning/dataset-run.png b/workshop/content/Smart spaces/Machine learning/dataset-run.png new file mode 100644 index 0000000..3b05f89 Binary files /dev/null and b/workshop/content/Smart spaces/Machine learning/dataset-run.png differ diff --git a/workshop/content/Smart spaces/Machine learning/iota-dataset-delivery.png b/workshop/content/Smart spaces/Machine learning/iota-dataset-delivery.png new file mode 100644 index 0000000..0ee00c7 Binary files /dev/null and b/workshop/content/Smart spaces/Machine learning/iota-dataset-delivery.png differ diff --git a/workshop/content/Smart spaces/Machine learning/sagemaker-deploy.png b/workshop/content/Smart spaces/Machine learning/sagemaker-deploy.png new file mode 100644 index 0000000..6846cd5 Binary files /dev/null and b/workshop/content/Smart spaces/Machine learning/sagemaker-deploy.png differ diff --git a/workshop/content/Smart spaces/Machine learning/sagemaker-endpoints.png b/workshop/content/Smart spaces/Machine learning/sagemaker-endpoints.png new file mode 100644 index 0000000..10049aa Binary files /dev/null and b/workshop/content/Smart spaces/Machine learning/sagemaker-endpoints.png differ diff --git a/workshop/content/Smart spaces/Machine learning/sagemaker-trials.png b/workshop/content/Smart spaces/Machine learning/sagemaker-trials.png new file mode 100644 index 0000000..4d4d9af Binary files /dev/null and b/workshop/content/Smart spaces/Machine learning/sagemaker-trials.png differ diff --git a/workshop/content/Smart spaces/Working with ML models/_index.en.md b/workshop/content/Smart spaces/Working with ML models/_index.en.md new file mode 100644 index 0000000..2c2653a --- /dev/null +++ b/workshop/content/Smart spaces/Working with ML models/_index.en.md @@ -0,0 +1,140 @@ ++++ +title = "Working with ML models" +weight = 40 +pre = "d. " ++++ + +## Chapter introduction +By the end of this chapter, your serverless application should do the following: +* Consumes a machine learning model that translates new thermostat messages into inferences of roomOccupancy + +## How to set up the serverless infrastructure +The following steps will walk you through creation of a serverless function in AWS Lambda. The function defines a small bit of code that expect device shadow messages from IoT Core, transform the message into the format used with your ML endpoint, then invoke your ML endpoint to return the classification of *roomOccupancy* and the confidence score of the inference. + +1. From the AWS Lambda console, choose **Create function**. +2. Provide a name for your function. Further steps assume the name `classifyRoomOccupancy`. +3. Under *Runtime*, select *Python 3.8*. +4. Choose **Create function**. +5. Under *Function code*, in the file *lambda_function.py*, copy and paste the following code to replace the placeholder code: +```python +import json +import boto3 +import os + +# Receives a device shadow Accepted document from IoT Core rules engine. +# Event has signature like {"state": {"reported": {"sound": 5}}}. +# See expectedAttributes for full list of attributes expected in state.reported. +# Builds CSV input to send to SageMaker endpoint, name of which stored in +# environment variable SAGEMAKER_ENDPOINT. +# +# Returns the prediction and confidence score from the ML model endpoint. +def lambda_handler(event, context): + client = boto3.client('sagemaker-runtime') + + print('event received: {}'.format(event)) + + # Order of attributes must match order expected by ML model endpoint. E.g. + # the same order of columns used to train the model. + expectedAttributes = ['sound', 'temperature', 'hvacStatus', 'roomOccupancy', 'timestamp'] + reported = event['state']['reported'] + reported['timestamp'] = event['timestamp'] + reportedAttributes = reported.keys() + + # Validates the input event has all the expected attributes. + if(len(set(expectedAttributes) & set(reportedAttributes)) < len(expectedAttributes)): + return { + 'statusCode': 400, + 'body': 'Error: missing attributes from event. Expected: {}. Received: {}.'.format(','.join(expectedAttributes), ','.join(reportedAttributes)) + } + + # Build the input CSV string to send to the ML model endpoint. + reportedValues = [] + for attr in expectedAttributes: + reportedValues.append(str(reported[attr])) + input = ','.join(reportedValues) + print('sending this input for inference: {}'.format(input)) + + endpoint_name = os.environ['SAGEMAKER_ENDPOINT'] + content_type = "text/csv" + accept = "application/json" + payload = input + response = client.invoke_endpoint( + EndpointName=endpoint_name, + ContentType=content_type, + Accept=accept, + Body=payload + ) + + body = response['Body'].read() + + print('received this response from inference endpoint: {}'.format(body)) + + return { + 'statusCode': 200, + 'body': json.loads(body)['predictions'][0] + } +``` +6. Under *Environment variables*, choose **Edit**. +7. For *Key* enter `SAGEMAKER_ENDPOINT` and for *Value* enter the name of your SageMaker endpoint. You named this resource as the last step of the previous chapter and this module assumes the name is `roomOccupancyEndpoint`. +8. Choose **Save** to commit this new environment variable and return to the main Lambda editor interface. +9. In the *Designer* panel, choose *+ Add trigger*. +10. For *Trigger configuration* select *AWS IoT* from the list. +11. For *IoT type*, select *Custom IoT rule*. +12. For *Rule*, find your rule in the list that processes the device shadow messages from your thermostat and publishes a new message with the *roomOccupancy* value. In the previous module, **Smart thermostat**, this rule was assumed to be named `thermostatRule`. +13. Verify the *Enable trigger* checkbox is enabled, then choose **Add**. This grants permission to your IoT Core rule to invoke this Lambda function. +14. Select the *Permissions* tab, then choose the link under *Role name* so you can add permissions for this Lambda function to invoke your SageMaker endpoint. +15. From the new tab opened to the IAM console, under *Permissions policies* choose **Add inline policy**. +16. For *Service* choose *SageMaker*. +17. For *Actions* choose *InvokeEndpoint*. +18. For *Resources* choose *All resources*. +19. Choose **Review policy**. +20. Give your policy a name like `invokeSageMakerEndpoint` and choose **Create policy**. You can now close this new browser tab. + +These steps conclude configuration of your AWS Lambda function. For example, when the Lambda function receives this device shadow update... +```JSON +{ + "state": { + "reported": { + "sound": 20, + "temperature": 58.8, + "hvacStatus": "HEATING", + "roomOccupancy": true + } + }, + "timestamp": 1234567890 +} +``` + +...it will return this response after invoking the SageMaker endpoint... +```JSON +{ + "statusCode": 200, + "body": { + "predicted_label": "false", + "probability": "0.9999991655349731" + } +} +``` + +The next step is to update your IoT Core rule (assumed name of `thermostatRule`) to use this Lambda function integration. +1. Return to the IoT Core console, choose **Act**, **Rules**, and choose your thermostat rule. +2. Choose **Edit** near *Rule query statement*. It should currently read `SELECT CASE state.reported.sound > 10 WHEN true THEN true ELSE false END AS state.desired.roomOccupancy FROM '$aws/things/<>/shadow/update/accepted' WHERE state.reported.sound <> Null`. +3. Replace this query with this new one: +```SQL +SELECT cast(get(get(aws_lambda("arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME", *), "body"), "predicted_label") AS Boolean) AS state.desired.roomOccupancy FROM '$aws/things/<>/shadow/update/accepted' WHERE state.reported.sound <> Null +``` +4. Be sure to replace the placeholders REGION (check your current region in the console header, it must be in the format like `us-west-2` and not `Oregon`), ACCOUNT_ID (also available as 12-digit number, no hyphens, in the console header menu where your username is printed), and FUNCTION_NAME (name of the AWS Lambda function you created, assumed name is `classifyRoomOccupancy`). Don't forget to update the <> placeholder as well in the FROM topic. +5. Under Actions, find the action called *Send a message to a Lambda function* choose **Remove**. This was added by default when you created the trigger in the Lambda function configuration to allow this IoT Core rule to invoke it, but you don't need the action. Instead, you are using an inline invocation in the rule query, but you still needed the same permissions that the trigger added. +6. Choose **Save**. + +At this point, your IoT workflow is now consuming your trained machine learning model from its deployed endpoint to classify messages published by your smart thermostat as new *roomOccupancy* values! + +## Validation steps +Before moving on to the next chapter, you can validate that your serverless application is configured as intended by... + +1. Using the AWS IoT Core Test client, you can subscribe to the topic `$aws/things/<>/shadow/update` (replacing your <>) and you should see two kinds of messages here. The first is the payload published by your smart thermostat with the `state.reported` path. The other is the payload now being published by your thermostat rule with the `state.desired.roomOccupancy` value determined by your ML model. + +If these are working as expected, you have completed this module and can move on to [**Conclusion**](/en/smart-spaces/conclusion.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/Working with ML models/_index.en_uk.md b/workshop/content/Smart spaces/Working with ML models/_index.en_uk.md new file mode 100644 index 0000000..0c21959 --- /dev/null +++ b/workshop/content/Smart spaces/Working with ML models/_index.en_uk.md @@ -0,0 +1,140 @@ ++++ +title = "Working with ML models" +weight = 40 +pre = "d. " ++++ + +## Chapter introduction +By the end of this chapter, your serverless application should do the following: +* Consumes a machine learning model that translates new thermostat messages into inferences of roomOccupancy + +## How to set up the serverless infrastructure +The following steps will walk you through creation of a serverless function in AWS Lambda. The function defines a small bit of code that expect device shadow messages from IoT Core, transform the message into the format used with your ML endpoint, then invoke your ML endpoint to return the classification of *roomOccupancy* and the confidence score of the inference. + +1. From the AWS Lambda console, choose **Create function**. +2. Provide a name for your function. Further steps assume the name `classifyRoomOccupancy`. +3. Under *Runtime*, select *Python 3.8*. +4. Choose **Create function**. +5. Under *Function code*, in the file *lambda_function.py*, copy and paste the following code to replace the placeholder code: +```python +import json +import boto3 +import os + +# Receives a device shadow Accepted document from IoT Core rules engine. +# Event has signature like {"state": {"reported": {"sound": 5}}}. +# See expectedAttributes for full list of attributes expected in state.reported. +# Builds CSV input to send to SageMaker endpoint, name of which stored in +# environment variable SAGEMAKER_ENDPOINT. +# +# Returns the prediction and confidence score from the ML model endpoint. +def lambda_handler(event, context): + client = boto3.client('sagemaker-runtime') + + print('event received: {}'.format(event)) + + # Order of attributes must match order expected by ML model endpoint. E.g. + # the same order of columns used to train the model. + expectedAttributes = ['sound', 'temperature', 'hvacStatus', 'roomOccupancy', 'timestamp'] + reported = event['state']['reported'] + reported['timestamp'] = event['timestamp'] + reportedAttributes = reported.keys() + + # Validates the input event has all the expected attributes. + if(len(set(expectedAttributes) & set(reportedAttributes)) < len(expectedAttributes)): + return { + 'statusCode': 400, + 'body': 'Error: missing attributes from event. Expected: {}. Received: {}.'.format(','.join(expectedAttributes), ','.join(reportedAttributes)) + } + + # Build the input CSV string to send to the ML model endpoint. + reportedValues = [] + for attr in expectedAttributes: + reportedValues.append(str(reported[attr])) + input = ','.join(reportedValues) + print('sending this input for inference: {}'.format(input)) + + endpoint_name = os.environ['SAGEMAKER_ENDPOINT'] + content_type = "text/csv" + accept = "application/json" + payload = input + response = client.invoke_endpoint( + EndpointName=endpoint_name, + ContentType=content_type, + Accept=accept, + Body=payload + ) + + body = response['Body'].read() + + print('received this response from inference endpoint: {}'.format(body)) + + return { + 'statusCode': 200, + 'body': json.loads(body)['predictions'][0] + } +``` +6. Under *Environment variables*, choose **Edit**. +7. For *Key* enter `SAGEMAKER_ENDPOINT` and for *Value* enter the name of your SageMaker endpoint. You named this resource as the last step of the previous chapter and this module assumes the name is `roomOccupancyEndpoint`. +8. Choose **Save** to commit this new environment variable and return to the main Lambda editor interface. +9. In the *Designer* panel, choose *+ Add trigger*. +10. For *Trigger configuration* select *AWS IoT* from the list. +11. For *IoT type*, select *Custom IoT rule*. +12. For *Rule*, find your rule in the list that processes the device shadow messages from your thermostat and publishes a new message with the *roomOccupancy* value. In the previous module, **Smart thermostat**, this rule was assumed to be named `thermostatRule`. +13. Verify the *Enable trigger* checkbox is enabled, then choose **Add**. This grants permission to your IoT Core rule to invoke this Lambda function. +14. Select the *Permissions* tab, then choose the link under *Role name* so you can add permissions for this Lambda function to invoke your SageMaker endpoint. +15. From the new tab opened to the IAM console, under *Permissions policies* choose **Add inline policy**. +16. For *Service* choose *SageMaker*. +17. For *Actions* choose *InvokeEndpoint*. +18. For *Resources* choose *All resources*. +19. Choose **Review policy**. +20. Give your policy a name like `invokeSageMakerEndpoint` and choose **Create policy**. You can now close this new browser tab. + +These steps conclude configuration of your AWS Lambda function. For example, when the Lambda function receives this device shadow update... +```JSON +{ + "state": { + "reported": { + "sound": 20, + "temperature": 58.8, + "hvacStatus": "HEATING", + "roomOccupancy": true + } + }, + "timestamp": 1234567890 +} +``` + +...it will return this response after invoking the SageMaker endpoint... +```JSON +{ + "statusCode": 200, + "body": { + "predicted_label": "false", + "probability": "0.9999991655349731" + } +} +``` + +The next step is to update your IoT Core rule (assumed name of `thermostatRule`) to use this Lambda function integration. +1. Return to the IoT Core console, choose **Act**, **Rules**, and choose your thermostat rule. +2. Choose **Edit** near *Rule query statement*. It should currently read `SELECT CASE state.reported.sound > 10 WHEN true THEN true ELSE false END AS state.desired.roomOccupancy FROM '$aws/things/<>/shadow/update/accepted' WHERE state.reported.sound <> Null`. +3. Replace this query with this new one: +```SQL +SELECT cast(get(get(aws_lambda("arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME", *), "body"), "predicted_label") AS Boolean) AS state.desired.roomOccupancy FROM '$aws/things/<>/shadow/update/accepted' WHERE state.reported.sound <> Null +``` +4. Be sure to replace the placeholders REGION (check your current region in the console header, it must be in the format like `us-west-2` and not `Oregon`), ACCOUNT_ID (also available as 12-digit number, no hyphens, in the console header menu where your username is printed), and FUNCTION_NAME (name of the AWS Lambda function you created, assumed name is `classifyRoomOccupancy`). Don't forget to update the <> placeholder as well in the FROM topic. +5. Under Actions, find the action called *Send a message to a Lambda function* choose **Remove**. This was added by default when you created the trigger in the Lambda function configuration to allow this IoT Core rule to invoke it, but you don't need the action. Instead, you are using an inline invocation in the rule query, but you still needed the same permissions that the trigger added. +6. Choose **Save**. + +At this point, your IoT workflow is now consuming your trained machine learning model from its deployed endpoint to classify messages published by your smart thermostat as new *roomOccupancy* values! + +## Validation steps +Before moving on to the next chapter, you can validate that your serverless application is configured as intended by... + +1. Using the AWS IoT Core Test client, you can subscribe to the topic `$aws/things/<>/shadow/update` (replacing your <>) and you should see two kinds of messages here. The first is the payload published by your smart thermostat with the `state.reported` path. The other is the payload now being published by your thermostat rule with the `state.desired.roomOccupancy` value determined by your ML model. + +If these are working as expected, you have completed this module and can move on to [**Conclusion**](/en_uk/smart-spaces/conclusion.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/_index.en.md b/workshop/content/Smart spaces/_index.en.md new file mode 100644 index 0000000..643e1b6 --- /dev/null +++ b/workshop/content/Smart spaces/_index.en.md @@ -0,0 +1,25 @@ ++++ +title = "Smart Spaces" +chapter = true +weight = 40 +pre = "4. " ++++ + +In this tutorial, you will extend the smart thermostat solution from the previous module, **Smart Thermostat**, into a Smart Space solution. A smart space is the concept of using insights about a space to enhance the space or bring further capabilities to it. You will use analytics and machine learning capabilities to derive predictions from raw data about room occupancy where your smart thermostat is deployed. The Smart Space solution will guide you how to create a new machine learning model from your thermostat data and how to improve the classification of room occupancy to operate your smart thermostat even better. + +Assumptions. Before starting this lab, verify the following prerequisites: +1. You have a M5Stack Core2 ESP32 IoT Development Kit for AWS IoT EduKit. +2. You have an AWS account that is not running any production workloads (i.e. an account safe for sandbox and evaluation purposes). +3. You have a user login or role to the AWS account with administrator access. +3. Your Core2 for AWS IoT EduKit has been provisioned in AWS IoT Core and is already communicating with AWS via MQTT. Start with the [**Smart Thermostat**](/en/smart-thermostat.html) tutorial if you have not already completed that baseline of functionality. + +Learning Objectives. By the end of this lab, you should know: +1. How to forward device telemetry to AWS IoT Analytics for storage, transformation, and creating analytical data sets. +2. How to use Amazon SageMaker Studio to create an experiment that automatically generates a machine learning model from your data sets. +3. How to deploy a machine learning model to a consumable API endpoint. +4. How a serverless function consumes an ML inference API to augment an application. + +Let's begin with the [**Introduction**](/en/smart-spaces/introduction.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart spaces/_index.en_uk.md b/workshop/content/Smart spaces/_index.en_uk.md new file mode 100644 index 0000000..4bf91cc --- /dev/null +++ b/workshop/content/Smart spaces/_index.en_uk.md @@ -0,0 +1,25 @@ ++++ +title = "Smart Spaces" +chapter = true +weight = 40 +pre = "4. " ++++ + +In this tutorial, you will extend the smart thermostat solution from the previous module, **Smart Thermostat**, into a Smart Space solution. A smart space is the concept of using insights about a space to enhance the space or bring further capabilities to it. You will use analytics and machine learning capabilities to derive predictions from raw data about room occupancy where your smart thermostat is deployed. The Smart Space solution will guide you how to create a new machine learning model from your thermostat data and how to improve the classification of room occupancy to operate your smart thermostat even better. + +Assumptions. Before starting this lab, verify the following prerequisites: +1. You have a M5Stack Core2 ESP32 IoT Development Kit for AWS IoT EduKit. +2. You have an AWS account that is not running any production workloads (i.e. an account safe for sandbox and evaluation purposes). +3. You have a user login or role to the AWS account with administrator access. +3. Your Core2 for AWS IoT EduKit has been provisioned in AWS IoT Core and is already communicating with AWS via MQTT. Start with the [**Smart Thermostat**](/en_uk/smart-thermostat.html) tutorial if you have not already completed that baseline of functionality. + +Learning Objectives. By the end of this lab, you should know: +1. How to forward device telemetry to AWS IoT Analytics for storage, transformation, and creating analytical data sets. +2. How to use Amazon SageMaker Studio to create an experiment that automatically generates a machine learning model from your data sets. +3. How to deploy a machine learning model to a consumable API endpoint. +4. How a serverless function consumes an ML inference API to augment an application. + +Let's begin with the [**Introduction**](/en_uk/smart-spaces/introduction.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Cloud application/_index.en.md b/workshop/content/Smart thermostat/Cloud application/_index.en.md new file mode 100644 index 0000000..3b54bf6 --- /dev/null +++ b/workshop/content/Smart thermostat/Cloud application/_index.en.md @@ -0,0 +1,224 @@ ++++ +title = "Cloud application" +weight = 50 +pre = "e. " ++++ + +## Chapter introduction +By the end of this chapter, your solution will do the following: + +* Process telemetry messages from the Core2 for AWS IoT EduKit device in a managed cloud application +* Determine which state the HVAC system should be in: HEATING, COOLING, or STANDBY +* Send a desired state message to your device shadow based on processed inputs +* Sync the desired shadow state to your device +* Update the device display with the desired HVAC system state + +## How to build the cloud application +In this chapter you will construct a serverless application without any code that analyzes the inputs from your smart thermostat and determines the behavior for the corresponding (fictitious) HVAC system. You will use IoT Events to deploy a resource called a detector model that processes the device shadow messages forwarded by your IoT Core rule, evaluate if any state changes should occur from heating to cooling to standby, then send a message back to your smart thermostat with the updated state change, if any. + +Here is a preview of the detector model to create: +{{< img "detector-model.png" "Detector model" >}} + +As you can see, there are three states for the HVAC application: heating, cooling, and standby. The application will always be in one of those states and initializes to the *standby* state. As new messages arrive from the smart thermostat, the detector model evaluates the inputs against conditional logic to determine if the model should transition to a new state. + +For example, from the *standby* state, if a new temperature reading comes in over 80 (in degrees Fahrenheit), the model will evaluate a transition to the *cooling* state. Then upon entering the *cooling* state, the model will publish a new message back to IoT Core to update the device shadow of the smart thermostat with a message like: `{ "state": { "desired": { "hvacStatus": "COOLING" } } }`. Based on the work you initially did in the chapter **Data sync**, these new commands on the desired node will be acknowledged and the state on the device updated to reflect the command by updating the LED strips to be red, blue, or off. + +It is beyond the scope of this learning module to fully explore how to create detector models in IoT Events. Instead, below are steps for getting your detector model up and running, with a few notes on key pieces so you know how _this_ model works. For a primer on getting started with AWS IoT Events, please review the [developer guide](https://docs.aws.amazon.com/iotevents/latest/developerguide/what-is-iotevents.html). + +To create the detector model previewed in the image above, complete the following steps. + +First, you will create a file on your local desktop with the following contents. Name the file something like `model.json`. You must edit the file to find and replace the three instances of the term **<>** with the client Id/serial number of your device (it is printed on your device screen). + +```json +{ + "detectorModelDefinition": { + "states": [ + { + "stateName": "standby", + "onInput": { + "events": [], + "transitionEvents": [ + { + "eventName": "startHeating", + "condition": "($input.thermostat.current.state.reported.temperature <= $variable.heatingThresholdUnoccupied) || ($input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature <= $variable.heatingThresholdOccupied) ", + "actions": [], + "nextState": "heating" + }, + { + "eventName": "startCooling", + "condition": "($input.thermostat.current.state.reported.temperature >= $variable.coolingThresholdUnoccupied) || ($input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature >= $variable.coolingThresholdOccupied) ", + "actions": [], + "nextState": "cooling" + } + ] + }, + "onEnter": { + "events": [ + { + "eventName": "setThresholds", + "condition": "true", + "actions": [ + { + "setVariable": { + "variableName": "heatingThresholdUnoccupied", + "value": "60" + } + }, + { + "setVariable": { + "variableName": "heatingThresholdOccupied", + "value": "68" + } + }, + { + "setVariable": { + "variableName": "coolingThresholdOccupied", + "value": "72" + } + }, + { + "setVariable": { + "variableName": "coolingThresholdUnoccupied", + "value": "80" + } + } + ] + }, + { + "eventName": "setStandbyInShadow", + "condition": "true", + "actions": [ + { + "iotTopicPublish": { + "mqttTopic": "'$aws/things/<>/shadow/update'", + "payload": { + "contentExpression": "'{\"state\":{\"desired\":{\"hvacStatus\":\"STANDBY\"}}}'", + "type": "JSON" + } + } + } + ] + } + ] + }, + "onExit": { + "events": [] + } + }, + { + "stateName": "cooling", + "onInput": { + "events": [], + "transitionEvents": [ + { + "eventName": "stopCooling", + "condition": "(!$input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature < $variable.coolingThresholdUnoccupied) || ($input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature < $variable.coolingThresholdOccupied) ", + "actions": [], + "nextState": "standby" + } + ] + }, + "onEnter": { + "events": [ + { + "eventName": "setCoolingInShadow", + "condition": "true", + "actions": [ + { + "iotTopicPublish": { + "mqttTopic": "'$aws/things/<>/shadow/update'", + "payload": { + "contentExpression": "'{\"state\":{\"desired\":{\"hvacStatus\":\"COOLING\"}}}'", + "type": "JSON" + } + } + } + ] + } + ] + }, + "onExit": { + "events": [] + } + }, + { + "stateName": "heating", + "onInput": { + "events": [], + "transitionEvents": [ + { + "eventName": "stopHeating", + "condition": "(!$input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature > $variable.heatingThresholdUnoccupied) || ($input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature > $variable.heatingThresholdOccupied) ", + "actions": [], + "nextState": "standby" + } + ] + }, + "onEnter": { + "events": [ + { + "eventName": "setHeatingInShadow", + "condition": "true", + "actions": [ + { + "iotTopicPublish": { + "mqttTopic": "'$aws/things/<>/shadow/update'", + "payload": { + "contentExpression": "'{\"state\":{\"desired\":{\"hvacStatus\":\"HEATING\"}}}'", + "type": "JSON" + } + } + } + ] + } + ] + }, + "onExit": { + "events": [] + } + } + ], + "initialStateName": "standby" + }, + "detectorModelName": "hvacApplication", + "detectorModelDescription": "cloud application to manage HVAC state", + "evaluationMethod": "BATCH" +} +``` + +Next, you will use this file in the steps to import a new detector model in the AWS IoT Events console. +1. Go to the AWS IoT Events management console. +2. Choose **Detector models**. +3. Choose **Action** and then **Import detector model**. +4. Choose **Import**. +5. Find the file you created in the previous step and choose **Open** (button name may vary per browser). +6. Choose **Publish**. +7. Under *Role*, provide a new name that will be used to create a new IAM role, like `edukit-iotevents`. The console will create a new IAM role that gives IoT Events permission to operate your model. +8. Choose **Save and publish**. + +You have now deployed the detector model to IoT Events. As new messages are published from your smart thermostat, the IoT Core rule you created in the previous chapter forwards them to the IoT Events input resource, then IoT Events pushes copies of the messages from any inputs to any consuming detector models, like this one, for evaluation of any state changes. + +Now for an explanation of a few key pieces of the model. Each state of the model (heating/cooling/standby) is nearly the same. The *standby* state has one extra action to set the numeric thresholds that every state uses to determine whether it should change states. These could be set once in a one-time initialization state before moving to *standby* for the first time, but they are included in *standby* for simplicity of the demonstration. + +The rest of the states' configuration is similar. There is an OnEnter event to publish a message to the smart thermostat's device shadow, indicating which state the HVAC should now be in. There is a conditional transition that gets evaluated after each message is processed that checks if the state should change. For example, if the model is in the *heating* state and the conditional expression for *stopHeating* evalutes true, the model will move to the *standby* state. Note that the *standby* state has two transitions, one each for moving to *heating* or *cooling*. This model is simplified in that the system will always go through the *standby* state instead of directly from *heating* to *cooling* or vice versa. The conditional transition logic takes into consideration the two temperature boundaries; one when the room is occupied and another when the room is unoccupied. + +Below is a sample transition from the *heating* state labeled *stopHeating*. There are two boolean expressions joined with OR `||` logic. In plainer English, this expression means "stop heating if the room is unoccupied and the room temperature is warm enough while unoccupied, OR if the room is occupied and the room is warm enough for people." + +```js +(!$input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature > $variable.heatingThresholdUnoccupied) || ($input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature > $variable.heatingThresholdOccupied) +``` + +If everything has been configured per this module's instructions, you should now be seeing status changes delivered to your smart thermostat in the form of updating LED strips, completing the end-to-end solution! + +## Validation steps +Before moving on to the next chapter, you can validate that the solution is configured as intended by... + +1. Apply a temperature change to your device that will take it outside the comfort bounds set in the IoT Events detector model to see the LED strips on the side change to red (heating), blue (cooling), or off (for standby). Use the warmth of your hands or a fan to raise or lower the detected temperature. +1. Alternatively, choose new temperature thresholds for your event detector that exclude your room's true ambient temperature and redeploy your detector model to see it work. You can find the threshold values stored as variables in the model state labeled "standby" under the OnEnter action labeled "setThresholds." You can update those numbers to anything that makes sense for your room and publish the changes to test. + +Note: the temperature sensor is inside the housing of the kit and the code includes a hardcoded offset. You may see variance in the temperature reading up to 10 degrees from reading to reading. + +If the solution is working as expected, you can move on to the [**Conclusion**](/en/smart-thermostat/conclusion.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Cloud application/_index.en_uk.md b/workshop/content/Smart thermostat/Cloud application/_index.en_uk.md new file mode 100644 index 0000000..d78b7e2 --- /dev/null +++ b/workshop/content/Smart thermostat/Cloud application/_index.en_uk.md @@ -0,0 +1,224 @@ ++++ +title = "Cloud application" +weight = 50 +pre = "e. " ++++ + +## Chapter introduction +By the end of this chapter, your solution will do the following: + +* Process telemetry messages from the Core2 for AWS IoT EduKit device in a managed cloud application +* Determine which state the HVAC system should be in: HEATING, COOLING, or STANDBY +* Send a desired state message to your device shadow based on processed inputs +* Sync the desired shadow state to your device +* Update the device display with the desired HVAC system state + +## How to build the cloud application +In this chapter you will construct a serverless application without any code that analyzes the inputs from your smart thermostat and determines the behavior for the corresponding (fictitious) HVAC system. You will use IoT Events to deploy a resource called a detector model that processes the device shadow messages forwarded by your IoT Core rule, evaluate if any state changes should occur from heating to cooling to standby, then send a message back to your smart thermostat with the updated state change, if any. + +Here is a preview of the detector model to create: +{{< img "detector-model.png" "Detector model" >}} + +As you can see, there are three states for the HVAC application: heating, cooling, and standby. The application will always be in one of those states and initializes to the *standby* state. As new messages arrive from the smart thermostat, the detector model evaluates the inputs against conditional logic to determine if the model should transition to a new state. + +For example, from the *standby* state, if a new temperature reading comes in over 80 (in degrees Fahrenheit), the model will evaluate a transition to the *cooling* state. Then upon entering the *cooling* state, the model will publish a new message back to IoT Core to update the device shadow of the smart thermostat with a message like: `{ "state": { "desired": { "hvacStatus": "COOLING" } } }`. Based on the work you initially did in the chapter **Data sync**, these new commands on the desired node will be acknowledged and the state on the device updated to reflect the command by updating the LED strips to be red, blue, or off. + +It is beyond the scope of this learning module to fully explore how to create detector models in IoT Events. Instead, below are steps for getting your detector model up and running, with a few notes on key pieces so you know how _this_ model works. For a primer on getting started with AWS IoT Events, please review the [developer guide](https://docs.aws.amazon.com/iotevents/latest/developerguide/what-is-iotevents.html). + +To create the detector model previewed in the image above, complete the following steps. + +First, you will create a file on your local desktop with the following contents. Name the file something like `model.json`. You must edit the file to find and replace the three instances of the term **<>** with the client Id/serial number of your device (it is printed on your device screen). + +```json +{ + "detectorModelDefinition": { + "states": [ + { + "stateName": "standby", + "onInput": { + "events": [], + "transitionEvents": [ + { + "eventName": "startHeating", + "condition": "($input.thermostat.current.state.reported.temperature <= $variable.heatingThresholdUnoccupied) || ($input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature <= $variable.heatingThresholdOccupied) ", + "actions": [], + "nextState": "heating" + }, + { + "eventName": "startCooling", + "condition": "($input.thermostat.current.state.reported.temperature >= $variable.coolingThresholdUnoccupied) || ($input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature >= $variable.coolingThresholdOccupied) ", + "actions": [], + "nextState": "cooling" + } + ] + }, + "onEnter": { + "events": [ + { + "eventName": "setThresholds", + "condition": "true", + "actions": [ + { + "setVariable": { + "variableName": "heatingThresholdUnoccupied", + "value": "60" + } + }, + { + "setVariable": { + "variableName": "heatingThresholdOccupied", + "value": "68" + } + }, + { + "setVariable": { + "variableName": "coolingThresholdOccupied", + "value": "72" + } + }, + { + "setVariable": { + "variableName": "coolingThresholdUnoccupied", + "value": "80" + } + } + ] + }, + { + "eventName": "setStandbyInShadow", + "condition": "true", + "actions": [ + { + "iotTopicPublish": { + "mqttTopic": "'$aws/things/<>/shadow/update'", + "payload": { + "contentExpression": "'{\"state\":{\"desired\":{\"hvacStatus\":\"STANDBY\"}}}'", + "type": "JSON" + } + } + } + ] + } + ] + }, + "onExit": { + "events": [] + } + }, + { + "stateName": "cooling", + "onInput": { + "events": [], + "transitionEvents": [ + { + "eventName": "stopCooling", + "condition": "(!$input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature < $variable.coolingThresholdUnoccupied) || ($input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature < $variable.coolingThresholdOccupied) ", + "actions": [], + "nextState": "standby" + } + ] + }, + "onEnter": { + "events": [ + { + "eventName": "setCoolingInShadow", + "condition": "true", + "actions": [ + { + "iotTopicPublish": { + "mqttTopic": "'$aws/things/<>/shadow/update'", + "payload": { + "contentExpression": "'{\"state\":{\"desired\":{\"hvacStatus\":\"COOLING\"}}}'", + "type": "JSON" + } + } + } + ] + } + ] + }, + "onExit": { + "events": [] + } + }, + { + "stateName": "heating", + "onInput": { + "events": [], + "transitionEvents": [ + { + "eventName": "stopHeating", + "condition": "(!$input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature > $variable.heatingThresholdUnoccupied) || ($input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature > $variable.heatingThresholdOccupied) ", + "actions": [], + "nextState": "standby" + } + ] + }, + "onEnter": { + "events": [ + { + "eventName": "setHeatingInShadow", + "condition": "true", + "actions": [ + { + "iotTopicPublish": { + "mqttTopic": "'$aws/things/<>/shadow/update'", + "payload": { + "contentExpression": "'{\"state\":{\"desired\":{\"hvacStatus\":\"HEATING\"}}}'", + "type": "JSON" + } + } + } + ] + } + ] + }, + "onExit": { + "events": [] + } + } + ], + "initialStateName": "standby" + }, + "detectorModelName": "hvacApplication", + "detectorModelDescription": "cloud application to manage HVAC state", + "evaluationMethod": "BATCH" +} +``` + +Next, you will use this file in the steps to import a new detector model in the AWS IoT Events console. +1. Go to the AWS IoT Events management console. +2. Choose **Detector models**. +3. Choose **Action** and then **Import detector model**. +4. Choose **Import**. +5. Find the file you created in the previous step and choose **Open** (button name may vary per browser). +6. Choose **Publish**. +7. Under *Role*, provide a new name that will be used to create a new IAM role, like `edukit-iotevents`. The console will create a new IAM role that gives IoT Events permission to operate your model. +8. Choose **Save and publish**. + +You have now deployed the detector model to IoT Events. As new messages are published from your smart thermostat, the IoT Core rule you created in the previous chapter forwards them to the IoT Events input resource, then IoT Events pushes copies of the messages from any inputs to any consuming detector models, like this one, for evaluation of any state changes. + +Now for an explanation of a few key pieces of the model. Each state of the model (heating/cooling/standby) is nearly the same. The *standby* state has one extra action to set the numeric thresholds that every state uses to determine whether it should change states. These could be set once in a one-time initialization state before moving to *standby* for the first time, but they are included in *standby* for simplicity of the demonstration. + +The rest of the states' configuration is similar. There is an OnEnter event to publish a message to the smart thermostat's device shadow, indicating which state the HVAC should now be in. There is a conditional transition that gets evaluated after each message is processed that checks if the state should change. For example, if the model is in the *heating* state and the conditional expression for *stopHeating* evalutes true, the model will move to the *standby* state. Note that the *standby* state has two transitions, one each for moving to *heating* or *cooling*. This model is simplified in that the system will always go through the *standby* state instead of directly from *heating* to *cooling* or vice versa. The conditional transition logic takes into consideration the two temperature boundaries; one when the room is occupied and another when the room is unoccupied. + +Below is a sample transition from the *heating* state labeled *stopHeating*. There are two boolean expressions joined with OR `||` logic. In plainer English, this expression means "stop heating if the room is unoccupied and the room temperature is warm enough while unoccupied, OR if the room is occupied and the room is warm enough for people." + +```js +(!$input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature > $variable.heatingThresholdUnoccupied) || ($input.thermostat.current.state.reported.roomOccupancy && $input.thermostat.current.state.reported.temperature > $variable.heatingThresholdOccupied) +``` + +If everything has been configured per this module's instructions, you should now be seeing status changes delivered to your smart thermostat in the form of updating LED strips, completing the end-to-end solution! + +## Validation steps +Before moving on to the next chapter, you can validate that the solution is configured as intended by... + +1. Apply a temperature change to your device that will take it outside the comfort bounds set in the IoT Events detector model to see the LED strips on the side change to red (heating), blue (cooling), or off (for standby). Use the warmth of your hands or a fan to raise or lower the detected temperature. +1. Alternatively, choose new temperature thresholds for your event detector that exclude your room's true ambient temperature and redeploy your detector model to see it work. You can find the threshold values stored as variables in the model state labeled "standby" under the OnEnter action labeled "setThresholds." You can update those numbers to anything that makes sense for your room and publish the changes to test. + +Note: the temperature sensor is inside the housing of the kit and the code includes a hardcoded offset. You may see variance in the temperature reading up to 10 degrees from reading to reading. + +If the solution is working as expected, you can move on to the [**Conclusion**](/en_uk/smart-thermostat/conclusion.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Cloud application/detector-model.png b/workshop/content/Smart thermostat/Cloud application/detector-model.png new file mode 100644 index 0000000..5cd086d Binary files /dev/null and b/workshop/content/Smart thermostat/Cloud application/detector-model.png differ diff --git a/workshop/content/Smart thermostat/Conclusion/_index.en.md b/workshop/content/Smart thermostat/Conclusion/_index.en.md new file mode 100644 index 0000000..5f3c6a0 --- /dev/null +++ b/workshop/content/Smart thermostat/Conclusion/_index.en.md @@ -0,0 +1,36 @@ ++++ +title = "Conclusion" +weight = 60 +pre = "f. " ++++ + +## Conclusion +You have completed this AWS IoT EduKit hands-on tutorial to connect your device to AWS as a smart thermostat and deploy a simple application to detect room occupancy and drive HVAC state changes. + +As a next step, consider following along with the next hands-on tutorial in this series, **Smart spaces**: + +If you are looking for additional ideas to experiment with your new solution, think of a way to enhance the customer experience. For example, how could you update the solution so that the HVAC setting stays engaged for a minimum timer instead of turning off after no ambient noise is heard for just 10 seconds? Can you achieve this without pushing new code to your device? (Hint: yes, you can.) + +## Clean up +In this solution you created the following resources in AWS: + +* IoT Core rule +* IAM roles +* IoT Events input and detector model + +None of these resources incur ongoing metered charges just by existing. You will only incur metered charges as the resources are used to process new messages from your device. If you will proceed with the next hands-on tutorial **Smart spaces**, it is recommended that you do not destroy any of the resources from this tutorial. + +If you have concluded experimenting with this tutorial's solution and do not intend to explore the **Smart spaces** tutorial, you can destroy these resources. + +1. Go to AWS IoT Core, choose Act, choose Rules, find your rule in the list and delete it. +2. Go to AWS IoT Events, choose Detector models, find your model in the list and delete it. Choose Inputs, find your input in the list and delete it. +3. Go to IAM, choose Roles, find the roles for your IoT Core rules and IoT Events detector model in the list and delete them. +4. Power off your Core2 for AWS IoT EduKit reference hardware kit, or run the command in your shell from the **Smart-Thermostat** folder to prevent the device from being connected and sending messages to AWS IoT Core: +```bash +idf.py erase_flash -p <> +``` + +The next tutorial to complete is [**Smart Spaces**](/en/smart-spaces.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Conclusion/_index.en_uk.md b/workshop/content/Smart thermostat/Conclusion/_index.en_uk.md new file mode 100644 index 0000000..096dbbf --- /dev/null +++ b/workshop/content/Smart thermostat/Conclusion/_index.en_uk.md @@ -0,0 +1,34 @@ ++++ +title = "Conclusion" +weight = 60 +pre = "f. " ++++ + +## Conclusion +You have completed this AWS IoT EduKit hands-on tutorial to connect your device to AWS as a smart thermostat and deploy a simple application to detect room occupancy and drive HVAC state changes. + +As a next step, consider following along with the next hands-on tutorial in this series, **Smart spaces**: +If you are looking for additional ideas to experiment with your new solution, think of a way to enhance the customer experience. For example, how could you update the solution so that the HVAC setting stays engaged for a minimum timer instead of turning off after no ambient noise is heard for just 10 seconds? Can you achieve this without pushing new code to your device? (Hint: yes, you can.) + +## Clean up +In this solution you created the following resources in AWS: +* IoT Core rule +* IAM roles +* IoT Events input and detector model + +None of these resources incur ongoing metered charges just by existing. You will only incur metered charges as the resources are used to process new messages from your device. If you will proceed with the next hands-on tutorial **Smart spaces**, it is recommended that you do not destroy any of the resources from this tutorial. + +If you have concluded experimenting with this tutorial's solution and do not intend to explore the **Smart spaces** tutorial, you can destroy these resources. + +1. Go to AWS IoT Core, choose Act, choose Rules, find your rule in the list and delete it. +2. Go to AWS IoT Events, choose Detector models, find your model in the list and delete it. Choose Inputs, find your input in the list and delete it. +3. Go to IAM, choose Roles, find the roles for your IoT Core rules and IoT Events detector model in the list and delete them. +4. Power off your Core2 for AWS IoT EduKit reference hardware kit, or run the command in your shell from the **Smart-Thermostat** folder to prevent the device from being connected and sending messages to AWS IoT Core: +```bash +idf.py erase_flash -p <> +``` + +The next tutorial to complete is [**Smart Spaces**](/en_uk/smart-spaces.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Data acquisition/_index.en.md b/workshop/content/Smart thermostat/Data acquisition/_index.en.md new file mode 100644 index 0000000..dce91bc --- /dev/null +++ b/workshop/content/Smart thermostat/Data acquisition/_index.en.md @@ -0,0 +1,135 @@ ++++ +title = "Data acquisition" +weight = 20 +pre = "b. " ++++ + +## Chapter introduction +By the end of this chapter, your device will do the following: + +* Power on and start the local smart thermostat application +* Report to the logs with the current temperature sensed, ambient noise level measured, an HVAC status of heating/cooling/standby, and an indicator of room occupation + +The Core2 for AWS IoT EduKit reference hardware kit has several sensors ready to use. For this solution, you will take readings from the temperature sensor and microphone using local application code. The application will keep track of sensor readings and status flags that are used to render a summary to the display. + +## How to program the thermostat application +Your smart thermostat will sample the integrated sensors using implementations already created and included in the bundled software components. In this first step, you will simply capture the values and print them out to the logger before we move on to publishing sensor values up to AWS IoT Core. + +Reading from the temperature sensor of the included MPU6886 module is easy. Here's a code snippet: + +```c +// include libraries for interfacing with the kit's modules +#include "freertos/FreeRTOS.h" +#include "core2forAWS.h" +#include "mpu6886.h" +#include "esp_log.h" + +// the application to run on the device +void app_main() +{ + // initialize a float to store our temperature reading + float temperature = 0.0f; + + // initialize the kit modules + Core2ForAWS_Init(); + MPU6886_Init(); + + // read from the temperature sensor + MPU6886_GetTempData(&temperature); + + // convert the reading to Fahrenheit and apply a calibration offset of -50 + temperature = (temperature * 1.8) + 32 - 50; + + // write the value to the ESP32 logger + ESP_LOGI("thermostat", "measured temperature is: %f", temperature); +} +``` + +If you were to build, flash, and monitor your device logs with this code, the device would take one reading from the temperature sensor, write it to the logger, and stop. To continuously sample every second without blocking other processes, you would create a separate **[FreeRTOS task](https://docs.espressif.com/projects/esp-idf/en/v4.2/esp32/api-reference/system/freertos.html#_CPPv423xTaskCreatePinnedToCore14TaskFunction_tPCKcK8uint32_tPCv11UBaseType_tPC12TaskHandle_tK10BaseType_t)** and pin it to an MCU core, like so: + +```c +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "core2forAWS.h" +#include "mpu6886.h" +#include "esp_log.h" + +// store our application logic in a task function +void temperature_task(void *arg) { + float temperature = 0.0f; + + // loop forever! + for (;;) { + MPU6886_GetTempData(&temperature); + temperature = (temperature * 1.8) + 32 - 50; + ESP_LOGI("thermostat", "measured temperature is: %f", temperature); + + // sleep for 1000ms before continuing the loop + vTaskDelay(1000 / portTICK_RATE_MS); + } +} + +void app_main() +{ + Core2ForAWS_Init(); + MPU6886_Init(); + + // FreeRTOS concept: operations that run in a continuous loop are done in tasks + xTaskCreatePinnedToCore(&temperature_task, "temperature_task", 4096, NULL, 5, NULL, 1); +} +``` + +## Code sample +A sample application has already been prepared for you to build and deploy to your device. Follow these steps to complete this chapter. + +1. Clone the code repo for this tutorial if you haven't already from another tutorial: + ```bash + git clone https://github.com/m5stack/Core2-for-AWS-IoT-EduKit.git + ``` +2. Change directory to the **Smart-Thermostat** project: + ```bash + cd Core2ForAWS-AWS-IoT-EduKit/Smart-Thermostat + ``` +3. Copy your `sdkconfig` file from the **Blinky-Hello-World** demo to simplify setting your Wi-Fi and AWS endpoint: + {{%expand "macOS & Linux" %}} + ```bash + cp ../Blinky-Hello-World/sdkconfig . + ``` + {{% /expand%}} + {{%expand "Windows (64-bit) Command Prompt" %}} + ```bash + copy ..\Blinky-Hello-World\sdkconfig . + ``` + {{% /expand%}} + {{% notice note %}} + You can optionally manually set your Wi-Fi credentials and AWS endpoint for your firmware by following the **[Configuring the ESP32 Firmware](/en/blinky-hello-world/connecting-to-aws.html#configuring-the-esp32-firmware)** step from the **Blinky Hello World** example. + {{% /notice %}} + +4. Build, flash, and start the serial monitor, replacing **<>** with the serial port your Core2 for AWS IoT EduKit device is connected. (To get the serial port the device is connected to, please see the [**Blinky Hello World — Identifying the serial port on host machine**](/en/blinky-hello-world/device-provisioning.html#identifying-the-serial-port-on-host-machine)): + ```bash + idf.py build flash monitor -p <> + ``` + +5. It will take some time to build and flash the app, but after that's done you should see the stream of device logs in your terminal. You can close the monitor session with the **Ctrl** + **]** keystroke combination. + +{{% notice note %}} +If you do not see **(edukit)** prefix at your shell prompt, ensure you activate your conda environment by running `conda activate edukit`. +If the idf.py command is not found, add the ESP-IDF to your path with the command `. $HOME/esp/esp-idf/export.sh` (macOS/Linux) or `%userprofile%\Desktop\esp-idf\export.bat` (Windows). +{{% /notice %}} + +## Validation steps +Before moving on to the next chapter, you can validate that your device is configured as intended by... + +1. When your Core2 for AWS IoT EduKit is powered on and running the application, you should be able to see in your terminal window logs that look like the following: + +``` +I (16128) shadow: On Device: roomOccupancy false +I (16132) shadow: On Device: hvacStatus STANDBY +I (16137) shadow: On Device: temperature 64.057533 +I (16143) shadow: On Device: sound 8 +``` + +If these are working as expected, let's move on to [**Data sync**](/en/smart-thermostat/data-sync.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Data acquisition/_index.en_uk.md b/workshop/content/Smart thermostat/Data acquisition/_index.en_uk.md new file mode 100644 index 0000000..6ff4531 --- /dev/null +++ b/workshop/content/Smart thermostat/Data acquisition/_index.en_uk.md @@ -0,0 +1,135 @@ ++++ +title = "Data acquisition" +weight = 20 +pre = "b. " ++++ + +## Chapter introduction +By the end of this chapter, your device will do the following: + +* Power on and start the local smart thermostat application +* Report to the logs with the current temperature sensed, ambient noise level measured, an HVAC status of heating/cooling/standby, and an indicator of room occupation + +The Core2 for AWS IoT EduKit reference hardware kit has several sensors ready to use. For this solution, you will take readings from the temperature sensor and microphone using local application code. The application will keep track of sensor readings and status flags that are used to render a summary to the display. + +## How to program the thermostat application +Your smart thermostat will sample the integrated sensors using implementations already created and included in the bundled software components. In this first step, you will simply capture the values and print them out to the logger before we move on to publishing sensor values up to AWS IoT Core. + +Reading from the temperature sensor of the included MPU6886 module is easy. Here's a code snippet: + +```c +// include libraries for interfacing with the kit's modules +#include "freertos/FreeRTOS.h" +#include "core2forAWS.h" +#include "mpu6886.h" +#include "esp_log.h" + +// the application to run on the device +void app_main() +{ + // initialize a float to store our temperature reading + float temperature = 0.0f; + + // initialize the kit modules + Core2ForAWS_Init(); + MPU6886_Init(); + + // read from the temperature sensor + MPU6886_GetTempData(&temperature); + + // convert the reading to Fahrenheit and apply a calibration offset of -50 + temperature = (temperature * 1.8) + 32 - 50; + + // write the value to the ESP32 logger + ESP_LOGI("thermostat", "measured temperature is: %f", temperature); +} +``` + +If you were to build, flash, and monitor your device logs with this code, the device would take one reading from the temperature sensor, write it to the logger, and stop. To continuously sample every second without blocking other processes, you would create a separate **[FreeRTOS task](https://docs.espressif.com/projects/esp-idf/en/v4.2/esp32/api-reference/system/freertos.html#_CPPv423xTaskCreatePinnedToCore14TaskFunction_tPCKcK8uint32_tPCv11UBaseType_tPC12TaskHandle_tK10BaseType_t)** and pin it to an MCU core, like so: + +```c +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "core2forAWS.h" +#include "mpu6886.h" +#include "esp_log.h" + +// store our application logic in a task function +void temperature_task(void *arg) { + float temperature = 0.0f; + + // loop forever! + for (;;) { + MPU6886_GetTempData(&temperature); + temperature = (temperature * 1.8) + 32 - 50; + ESP_LOGI("thermostat", "measured temperature is: %f", temperature); + + // sleep for 1000ms before continuing the loop + vTaskDelay(1000 / portTICK_RATE_MS); + } +} + +void app_main() +{ + Core2ForAWS_Init(); + MPU6886_Init(); + + // FreeRTOS concept: operations that run in a continuous loop are done in tasks + xTaskCreatePinnedToCore(&temperature_task, "temperature_task", 4096, NULL, 5, NULL, 1); +} +``` + +## Code sample +A sample application has already been prepared for you to build and deploy to your device. Follow these steps to complete this chapter. + +1. Clone the code repo for this tutorial if you haven't already from another tutorial: + ```bash + git clone https://github.com/m5stack/Core2-for-AWS-IoT-EduKit.git + ``` +2. Change directory to the **Smart-Thermostat** project: + ```bash + cd Core2ForAWS-AWS-IoT-EduKit/Smart-Thermostat + ``` +3. Copy your `sdkconfig` file from the **Blinky-Hello-World** demo to simplify setting your Wi-Fi and AWS endpoint: + {{%expand "macOS & Linux" %}} + ```bash + cp ../Blinky-Hello-World/sdkconfig . + ``` + {{% /expand%}} + {{%expand "Windows (64-bit) Command Prompt" %}} + ```bash + copy ..\Blinky-Hello-World\sdkconfig . + ``` + {{% /expand%}} + {{% notice note %}} + You can optionally manually set your Wi-Fi credentials and AWS endpoint for your firmware by following the **[Configuring the ESP32 Firmware](/en_uk/blinky-hello-world/connecting-to-aws.html#configuring-the-esp32-firmware)** step from the **Blinky Hello World** example. + {{% /notice %}} + +4. Build, flash, and start the serial monitor, replacing **<>** with the serial port your Core2 for AWS IoT EduKit device is connected. (To get the serial port the device is connected to, please see the [**Blinky Hello World — Identifying the serial port on host machine**](/en_uk/blinky-hello-world/device-provisioning.html#identifying-the-serial-port-on-host-machine)): + ```bash + idf.py build flash monitor -p <> + ``` + +5. It will take some time to build and flash the app, but after that's done you should see the stream of device logs in your terminal. You can close the monitor session with the **Ctrl** + **]** keystroke combination. + +{{% notice note %}} +If you do not see **(edukit)** prefix at your shell prompt, ensure you activate your conda environment by running `conda activate edukit`. +If the idf.py command is not found, add the ESP-IDF to your path with the command `. $HOME/esp/esp-idf/export.sh` (macOS/Linux) or `%userprofile%\Desktop\esp-idf\export.bat` (Windows). +{{% /notice %}} + +## Validation steps +Before moving on to the next chapter, you can validate that your device is configured as intended by... + +1. When your Core2 for AWS IoT EduKit is powered on and running the application, you should be able to see in your terminal window logs that look like the following: + +``` +I (16128) shadow: On Device: roomOccupancy false +I (16132) shadow: On Device: hvacStatus STANDBY +I (16137) shadow: On Device: temperature 64.057533 +I (16143) shadow: On Device: sound 8 +``` + +If these are working as expected, let's move on to [**Data sync**](/en_uk/smart-thermostat/data-sync.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Data sync/_index.en.md b/workshop/content/Smart thermostat/Data sync/_index.en.md new file mode 100644 index 0000000..e71289a --- /dev/null +++ b/workshop/content/Smart thermostat/Data sync/_index.en.md @@ -0,0 +1,195 @@ ++++ +title = "Data sync" +weight = 30 +pre = "c. " ++++ + +## Chapter introduction +By the end of this chapter, your Core2 for AWS IoT EduKit reference hardware kit should do the following: + +* Publish the sampled ambient noise level and room temperature to AWS IoT Core every 10 seconds +* Subscribe to your device's shadow in AWS IoT Core to receive new commands + +## Messaging concepts +In this chapter, you will connect your device to your custom AWS IoT Core endpoint, then exchange messages between the device and cloud in a model called publish and subscribe, or "PubSub." AWS IoT Core is heavily based on a protocol called [Message Queueing Telemetry Transport](https://mqtt.org/) (MQTT). From the home page: + +> MQTT is an OASIS standard messaging protocol for the Internet of Things (IoT). It is designed as an extremely lightweight publish/subscribe messaging transport that is ideal for connecting remote devices with a small code footprint and minimal network bandwidth. MQTT today is used in a wide variety of industries, such as automotive, manufacturing, telecommunications, oil and gas, etc. + +The messages that are exchanged between your device and the cloud can be anything from text, to numbers, to binary, to JSON. JSON is a best practice for exchanging messages between decoupled systems and a recommended pattern for exploring AWS IoT services. + +Messages are published on a topic, which is a textual string that other clients on AWS IoT Core can subscribe to in order to receive copies of published messages. Topics can look like the following examples: + +* `this is a topic` +* `foo/bar` +* `domain/stuff/thing/whatever` +* `dt/device123/temperature` +* `tokyo/weather/report` +* `$aws/things/edukit/shadow/update` + +As you can see, a topic can look like any text. The MQTT protocol defines topic partitions as separated by the forward slash '/' character. This is a foundational concept in designing topic architecture for an IoT solution. This module will help you implement topics to follow best practices. + +In your smart thermostat solution, you will use a feature of AWS IoT Core called device shadows. Simply put, a device shadow is a JSON document used to store and retrieve current state information for a device. You can store key-value pairs in a JSON document, publish the document on a special topic, and AWS IoT Core will store the document in the cloud. It's a useful way for keeping the latest state changes of a device in the cloud so that other systems can get updates in real time. It's also useful for other systems to send desired commands back to your device because AWS IoT Core will keep your device in sync with any new desired commands stored in the device shadow. + +For example, the smart thermostat needs to report the latest room temperature and ambient noise level to power our solution. The message published by your device will look like this: + +```JSON +{ + "state": { + "reported": { + "temperature": 68, + "noise": 10 + } + } +} +``` + +When AWS IoT Core receives a message like this, it will store those key-value pairs until they are overridden by a new published message. + +To illustrate sending a command back to your smart thermostat, another system could publish a message like this one to your device shadow: + +```JSON +{ + "state": { + "desired": { + "hvacStatus": "COOLING" + } + } +} +``` + +AWS IoT Core will merge the JSON documents such that the aggregated state of your device shadow will look like this: + +```JSON +{ + "state": { + "reported": { + "temperature": 68, + "noise": 10 + }, + "desired": { + "hvacStatus": "COOLING" + } + } +} +``` + +This means when your device next connects to AWS IoT Core, or if it is already connected at the time the desired command was published, it would receive this aggregate state document. It is up to the device code to act on that new key-value pair `"hvacStatus": "COOLING"` but AWS IoT Core makes it easy to report new values and process desired commands. + +Your smart thermostat will report the latest values of temperature and noise level as reviewed in the previous chapter. Your thermostat will also receive commands and track the state of two more values called "hvacStatus" and "roomOccupancy". These values will be determined by the cloud application in the coming chapters. + +## How to program publishing messages to AWS IoT Core +You will use the AWS IoT Device SDK for Embedded C ("C SDK") to communicate between your smart thermostat device and AWS IoT Core. This is a best practice for abstracting away security, network, and data layers so you can focus on the application logic of your device and solution. The C SDK bundles libraries for connecting to AWS IoT Core over the MQTT protocol, interfacing with the hardware secure element to sign requests, and for integrating with higher order features like the device shadow. + +Let's look at a few critical lines of code and analyze what they do. + +```C +jsonStruct_t temperatureHandler; +temperatureHandler.cb = NULL; +temperatureHandler.pKey = "temperature"; +temperatureHandler.pData = &temperature; +temperatureHandler.type = SHADOW_JSON_FLOAT; +temperatureHandler.dataLength = sizeof(float); +``` + +The code above defines a new `jsonStruct_t` that we use as a tool for packing individual key-value pairs, making them ready for use in IoT Core device shadow, and indicating the callback function to use (if any) should a new "desired" message arrive for the indicated **pKey**. This example defines a new device shadow key **temperature**, setting an initial **pData** to the value of the `temperature` variable, and indicating it is of type `SHADOW_JSON_FLOAT`. These structs are used later when registering shadow delta behavior and publishing the "reported" values up to IoT Core. + +```C +rc = aws_iot_shadow_register_delta(&iotCoreClient, &roomOccupancyActuator); +``` + +The example above registers the delta behavior with the AWS IoT Device SDK. It passes the reference to the initialized SDK client and the `jsonStruct_t` that represents the key-value pair to which we want to respond to changes. The example registers the **roomOccupancyActuator** so when the client gets a new shadow update from the cloud that includes a new value for `state.desired.roomOccupancy`, the callback function defined in the actuator is processed. + +```C +MPU6886_GetTempData(&temperature); +temperature = (temperature * 1.8) + 32 - 50; +``` + +Above is an example of how you read from the MPU6886 component to get the local temperature reading. Note the conversion to Fahrenheit and hardcoded callibration offset. You can modify this expression to work in Centigrade or a different offset if this default of 50 doesn't produce values in the range of what you expect. + +```C +rc = aws_iot_shadow_add_reported(JsonDocumentBuffer, + sizeOfJsonDocumentBuffer, 4, &temperatureHandler, + &soundHandler, &roomOccupancyActuator, &hvacStatusActuator); +``` + +This code packs any values we want to publish to the cloud in to the shadow document that is expected by the IoT Core shadow service. You can add or remove key-value pairs for `state.reported` here by modifying this variadic function. Don't forget to update the third parameter to match the number of key-value pairs in the list! + +```C +rc = aws_iot_shadow_update(&iotCoreClient, + CLIENT_ID, JsonDocumentBuffer, + ShadowUpdateStatusCallback, NULL, 4, true); +``` + +This code actually publishes the marshalled shadow document as a payload over the network to IoT Core on the topic `$aws/things/<>/shadow/update`. The definition of **<>** resolves to the client Id/serial number of your Core2 for AWS device as displayed on your device screen and in the serial monitor output. + +```C +while(NETWORK_ATTEMPTING_RECONNECT == rc || NETWORK_RECONNECTED == rc || SUCCESS == rc) { +... + vTaskDelay(1000 / portTICK_RATE_MS); +} +``` + +The `while` loop of this **xTask** will effectively run forever unless the network connection is dropped. After publishing the latest message to the device shadow service and responding to any delta callbacks from received messages, the task will sleep for the indicated amount of time before continuing to the next cycle of the loop. You can modify the `vTaskDelay()` expression to publish more or less frequently. You may find it helpful to use a faster interval like 1000 (one second) while testing and reduce it to ten, thirty, or sixty seconds for a real deployment. + +```C +void hvac_Callback(const char *pJsonString, uint32_t JsonStringDataLen, jsonStruct_t *pContext) { + IOT_UNUSED(pJsonString); + IOT_UNUSED(JsonStringDataLen); + + char * status = (char *) (pContext->pData); + + if(pContext != NULL) { + ESP_LOGI(TAG, "Delta - hvacStatus state changed to %s", status); + } + + if(strcmp(status, HEATING) == 0) { + ESP_LOGI(TAG, "setting side LEDs to red"); + Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_LEFT, 0xFF0000); + Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_RIGHT, 0xFF0000); + Core2ForAWS_Sk6812_Show(); + } else if(strcmp(status, COOLING) == 0) { + ESP_LOGI(TAG, "setting side LEDs to blue"); + Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_LEFT, 0x0000FF); + Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_RIGHT, 0x0000FF); + Core2ForAWS_Sk6812_Show(); + } else { + ESP_LOGI(TAG, "clearing side LEDs"); + Core2ForAWS_Sk6812_Clear(); + Core2ForAWS_Sk6812_Show(); + } +} +``` + +This code represents a callback function for our **hvacStatus** actuator. This is the code that is executed when a new message is received by the device that includes the `state.desired.hvacStatus` key-value pair. Note, you don't have to do anything to accept the desired state change; it is applied to the local **hvacStatusActuator** and its **pData** element automatically. + +The `IOTUNUSED()` function is used to suppress compiler warnings for unused parameters. + +The if/else block is used to evaluate the text value of the new **hvacStatus** key-value and uses that to determine the color of the LED strips. + +## Code sample +You can use the following code sample if you don't want to write the application yourself. This is recommended for students unfamiliar with embedded development, FreeRTOS, or programming an ESP32. + +1. From the **Core2-for-AWS-IoT-EduKit** directory, change directory in to the **Smart-Thermostat** folder: + ```bash + cd Smart-Thermostat + ``` +2. Build, flash, and start the serial monitor (replace **<>** with the serial port identified in the [**Blinky Hello World**](/en/blinky-hello-world/device-provisioning.html#identifying-the-serial-port-on-host-machine) tutorial): + ```bash + idf.py build flash monitor -p <> + ``` +3. It will take some time to build and flash the app, but after that's done you should see the stream of device logs in your terminal. You can close the serial monitor session with the **CTRL** + **]** keystroke combination. + +## Validation steps +Before moving on to the next chapter, you can validate that your device is configured as intended by: + +1. Using the AWS IoT Core console test page, subscribe to the topic `$aws/things/<>/shadow/update/accepted` and you should see new messages arriving in time with your **vTaskDelay()**. (Replace <> with your device client Id/serial number printed on the screen.) +2. Using the AWS IoT Core console test page, publish a new shadow message on the topic `$aws/things/<>/shadow/update`. You should see the Core for AWS IoT EduKit's LED bars change from blue, red, none to represent the **COOLING**, **HEATING**, and **STANDBY** published values. See below for a sample shadow message. Test the effects by toggling the **hvacStatus** (set to **HEATING** or **COOLING**) and/or **roomOccupied** values (set to **true** or **false**) each time you publish the message. + +``` +{ "state": { "desired": { "hvacStatus": "HEATING", "roomOccupancy": true } } } +``` + +If these are working as expected, let's move on to [**Data transforms and routing**](/en/smart-thermostat/data-transforms-and-routing.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Data sync/_index.en_uk.md b/workshop/content/Smart thermostat/Data sync/_index.en_uk.md new file mode 100644 index 0000000..9c4c6e2 --- /dev/null +++ b/workshop/content/Smart thermostat/Data sync/_index.en_uk.md @@ -0,0 +1,195 @@ ++++ +title = "Data sync" +weight = 30 +pre = "c. " ++++ + +## Chapter introduction +By the end of this chapter, your Core2 for AWS IoT EduKit reference hardware kit should do the following: + +* Publish the sampled ambient noise level and room temperature to AWS IoT Core every 10 seconds +* Subscribe to your device's shadow in AWS IoT Core to receive new commands + +## Messaging concepts +In this chapter, you will connect your device to your custom AWS IoT Core endpoint, then exchange messages between the device and cloud in a model called publish and subscribe, or "PubSub." AWS IoT Core is heavily based on a protocol called [Message Queueing Telemetry Transport](https://mqtt.org/) (MQTT). From the home page: + +> MQTT is an OASIS standard messaging protocol for the Internet of Things (IoT). It is designed as an extremely lightweight publish/subscribe messaging transport that is ideal for connecting remote devices with a small code footprint and minimal network bandwidth. MQTT today is used in a wide variety of industries, such as automotive, manufacturing, telecommunications, oil and gas, etc. + +The messages that are exchanged between your device and the cloud can be anything from text, to numbers, to binary, to JSON. JSON is a best practice for exchanging messages between decoupled systems and a recommended pattern for exploring AWS IoT services. + +Messages are published on a topic, which is a textual string that other clients on AWS IoT Core can subscribe to in order to receive copies of published messages. Topics can look like the following examples: + +* `this is a topic` +* `foo/bar` +* `domain/stuff/thing/whatever` +* `dt/device123/temperature` +* `tokyo/weather/report` +* `$aws/things/edukit/shadow/update` + +As you can see, a topic can look like any text. The MQTT protocol defines topic partitions as separated by the forward slash '/' character. This is a foundational concept in designing topic architecture for an IoT solution. This module will help you implement topics to follow best practices. + +In your smart thermostat solution, you will use a feature of AWS IoT Core called device shadows. Simply put, a device shadow is a JSON document used to store and retrieve current state information for a device. You can store key-value pairs in a JSON document, publish the document on a special topic, and AWS IoT Core will store the document in the cloud. It's a useful way for keeping the latest state changes of a device in the cloud so that other systems can get updates in real time. It's also useful for other systems to send desired commands back to your device because AWS IoT Core will keep your device in sync with any new desired commands stored in the device shadow. + +For example, the smart thermostat needs to report the latest room temperature and ambient noise level to power our solution. The message published by your device will look like this: + +```JSON +{ + "state": { + "reported": { + "temperature": 68, + "noise": 10 + } + } +} +``` + +When AWS IoT Core receives a message like this, it will store those key-value pairs until they are overridden by a new published message. + +To illustrate sending a command back to your smart thermostat, another system could publish a message like this one to your device shadow: + +```JSON +{ + "state": { + "desired": { + "hvacStatus": "COOLING" + } + } +} +``` + +AWS IoT Core will merge the JSON documents such that the aggregated state of your device shadow will look like this: + +```JSON +{ + "state": { + "reported": { + "temperature": 68, + "noise": 10 + }, + "desired": { + "hvacStatus": "COOLING" + } + } +} +``` + +This means when your device next connects to AWS IoT Core, or if it is already connected at the time the desired command was published, it would receive this aggregate state document. It is up to the device code to act on that new key-value pair `"hvacStatus": "COOLING"` but AWS IoT Core makes it easy to report new values and process desired commands. + +Your smart thermostat will report the latest values of temperature and noise level as reviewed in the previous chapter. Your thermostat will also receive commands and track the state of two more values called "hvacStatus" and "roomOccupancy". These values will be determined by the cloud application in the coming chapters. + +## How to program publishing messages to AWS IoT Core +You will use the AWS IoT Device SDK for Embedded C ("C SDK") to communicate between your smart thermostat device and AWS IoT Core. This is a best practice for abstracting away security, network, and data layers so you can focus on the application logic of your device and solution. The C SDK bundles libraries for connecting to AWS IoT Core over the MQTT protocol, interfacing with the hardware secure element to sign requests, and for integrating with higher order features like the device shadow. + +Let's look at a few critical lines of code and analyze what they do. + +```C +jsonStruct_t temperatureHandler; +temperatureHandler.cb = NULL; +temperatureHandler.pKey = "temperature"; +temperatureHandler.pData = &temperature; +temperatureHandler.type = SHADOW_JSON_FLOAT; +temperatureHandler.dataLength = sizeof(float); +``` + +The code above defines a new `jsonStruct_t` that we use as a tool for packing individual key-value pairs, making them ready for use in IoT Core device shadow, and indicating the callback function to use (if any) should a new "desired" message arrive for the indicated **pKey**. This example defines a new device shadow key **temperature**, setting an initial **pData** to the value of the `temperature` variable, and indicating it is of type `SHADOW_JSON_FLOAT`. These structs are used later when registering shadow delta behavior and publishing the "reported" values up to IoT Core. + +```C +rc = aws_iot_shadow_register_delta(&iotCoreClient, &roomOccupancyActuator); +``` + +The example above registers the delta behavior with the AWS IoT Device SDK. It passes the reference to the initialized SDK client and the `jsonStruct_t` that represents the key-value pair to which we want to respond to changes. The example registers the **roomOccupancyActuator** so when the client gets a new shadow update from the cloud that includes a new value for `state.desired.roomOccupancy`, the callback function defined in the actuator is processed. + +```C +MPU6886_GetTempData(&temperature); +temperature = (temperature * 1.8) + 32 - 50; +``` + +Above is an example of how you read from the MPU6886 component to get the local temperature reading. Note the conversion to Fahrenheit and hardcoded callibration offset. You can modify this expression to work in Centigrade or a different offset if this default of 50 doesn't produce values in the range of what you expect. + +```C +rc = aws_iot_shadow_add_reported(JsonDocumentBuffer, + sizeOfJsonDocumentBuffer, 4, &temperatureHandler, + &soundHandler, &roomOccupancyActuator, &hvacStatusActuator); +``` + +This code packs any values we want to publish to the cloud in to the shadow document that is expected by the IoT Core shadow service. You can add or remove key-value pairs for `state.reported` here by modifying this variadic function. Don't forget to update the third parameter to match the number of key-value pairs in the list! + +```C +rc = aws_iot_shadow_update(&iotCoreClient, + CLIENT_ID, JsonDocumentBuffer, + ShadowUpdateStatusCallback, NULL, 4, true); +``` + +This code actually publishes the marshalled shadow document as a payload over the network to IoT Core on the topic `$aws/things/<>/shadow/update`. The definition of **<>** resolves to the client Id/serial number of your Core2 for AWS device as displayed on your device screen and in the serial monitor output. + +```C +while(NETWORK_ATTEMPTING_RECONNECT == rc || NETWORK_RECONNECTED == rc || SUCCESS == rc) { +... + vTaskDelay(1000 / portTICK_RATE_MS); +} +``` + +The `while` loop of this **xTask** will effectively run forever unless the network connection is dropped. After publishing the latest message to the device shadow service and responding to any delta callbacks from received messages, the task will sleep for the indicated amount of time before continuing to the next cycle of the loop. You can modify the `vTaskDelay()` expression to publish more or less frequently. You may find it helpful to use a faster interval like 1000 (one second) while testing and reduce it to ten, thirty, or sixty seconds for a real deployment. + +```C +void hvac_Callback(const char *pJsonString, uint32_t JsonStringDataLen, jsonStruct_t *pContext) { + IOT_UNUSED(pJsonString); + IOT_UNUSED(JsonStringDataLen); + + char * status = (char *) (pContext->pData); + + if(pContext != NULL) { + ESP_LOGI(TAG, "Delta - hvacStatus state changed to %s", status); + } + + if(strcmp(status, HEATING) == 0) { + ESP_LOGI(TAG, "setting side LEDs to red"); + Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_LEFT, 0xFF0000); + Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_RIGHT, 0xFF0000); + Core2ForAWS_Sk6812_Show(); + } else if(strcmp(status, COOLING) == 0) { + ESP_LOGI(TAG, "setting side LEDs to blue"); + Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_LEFT, 0x0000FF); + Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_RIGHT, 0x0000FF); + Core2ForAWS_Sk6812_Show(); + } else { + ESP_LOGI(TAG, "clearing side LEDs"); + Core2ForAWS_Sk6812_Clear(); + Core2ForAWS_Sk6812_Show(); + } +} +``` + +This code represents a callback function for our **hvacStatus** actuator. This is the code that is executed when a new message is received by the device that includes the `state.desired.hvacStatus` key-value pair. Note, you don't have to do anything to accept the desired state change; it is applied to the local **hvacStatusActuator** and its **pData** element automatically. + +The `IOTUNUSED()` function is used to suppress compiler warnings for unused parameters. + +The if/else block is used to evaluate the text value of the new **hvacStatus** key-value and uses that to determine the color of the LED strips. + +## Code sample +You can use the following code sample if you don't want to write the application yourself. This is recommended for students unfamiliar with embedded development, FreeRTOS, or programming an ESP32. + +1. From the **Core2-for-AWS-IoT-EduKit** directory, change directory in to the **Smart-Thermostat** folder: + ```bash + cd Smart-Thermostat + ``` +2. Build, flash, and start the serial monitor (replace **<>** with the serial port identified in the [**Blinky Hello World**](/en_uk/blinky-hello-world/device-provisioning.html#identifying-the-serial-port-on-host-machine) tutorial): + ```bash + idf.py build flash monitor -p <> + ``` +3. It will take some time to build and flash the app, but after that's done you should see the stream of device logs in your terminal. You can close the serial monitor session with the **CTRL** + **]** keystroke combination. + +## Validation steps +Before moving on to the next chapter, you can validate that your device is configured as intended by: + +1. Using the AWS IoT Core console test page, subscribe to the topic `$aws/things/<>/shadow/update/accepted` and you should see new messages arriving in time with your **vTaskDelay()**. (Replace <> with your device client Id/serial number printed on the screen.) +2. Using the AWS IoT Core console test page, publish a new shadow message on the topic `$aws/things/<>/shadow/update`. You should see the Core for AWS IoT EduKit's LED bars change from blue, red, none to represent the **COOLING**, **HEATING**, and **STANDBY** published values. See below for a sample shadow message. Test the effects by toggling the **hvacStatus** (set to **HEATING** or **COOLING**) and/or **roomOccupied** values (set to **true** or **false**) each time you publish the message. + +``` +{ "state": { "desired": { "hvacStatus": "HEATING", "roomOccupancy": true } } } +``` + +If these are working as expected, let's move on to [**Data transforms and routing**](/en_uk/smart-thermostat/data-transforms-and-routing.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Data transforms and routing/IoT_Events-Create-Input.webp b/workshop/content/Smart thermostat/Data transforms and routing/IoT_Events-Create-Input.webp new file mode 100644 index 0000000..d6bfd88 Binary files /dev/null and b/workshop/content/Smart thermostat/Data transforms and routing/IoT_Events-Create-Input.webp differ diff --git a/workshop/content/Smart thermostat/Data transforms and routing/_index.en.md b/workshop/content/Smart thermostat/Data transforms and routing/_index.en.md new file mode 100644 index 0000000..67e467b --- /dev/null +++ b/workshop/content/Smart thermostat/Data transforms and routing/_index.en.md @@ -0,0 +1,107 @@ ++++ +title = "Data transforms and routing" +weight = 40 +pre = "d. " ++++ + +## Chapter introduction +By the end of this chapter, your cloud solution should do the following: + +* Transform raw sound data received from the device into actionable intelligence of room occupancy with application logic stored in the cloud +* Synchronize the state of room occupancy from the device shadow down to the device +* Set up a resource in AWS IoT Events to receive messages from your device +* Forward messages published by the thermostat device to AWS IoT Events for further processing + +## How to set up the cloud solution +### Deriving room occupancy from sound +The first part of your cloud solution is to add the intelligence that derives whether the room is occupied by people using it. To approximate room occupation, you will use the sound level reported by the smart thermostat device. If the sampled sound level is over a given threshold, you will mark the room as occupied. If it is under the threshold, you will mark the room as unoccupied. You can store this status in the device shadow and use that to sync changes back down to the device. + +Why use sound level and simple threshold banding to classify room occupancy? Why not use a motion sensor? In this case, a microphone is the sensor that is available for use. When designing IoT solutions, you will not always have the budget for the best possible input data. This approach strikes a balance of frugality and achieving the use case. The author acknowledges this would not work for every use case, such as a team meeting for people who are hard of hearing and using sign language, or for team meetings starting with a silent document review. + +You will use two concepts of AWS IoT Core to achieve this use case of deriving room occupancy. These are *topic rules* and *device shadow*. A topic rule lets you define behavior for messages arriving on topic filter, such as performing live transformations on the payload and routing payloads to new destinations. The device shadow is a semi-structured JSON document for synchronizing the reported and desired state of a device. Any modifications made to the device shadow of your thermostat will be sent as new payloads to your device. You have already set up and tested that your device is receiving these shadow updates in the previous chapter. + +You can combine topic rules and device shadows to functionally make updates to the device shadow based on application logic stored in the cloud. This enables rapid updating of application logic without pushing new code to your device. + +Your first milestone in this chapter is to create an IoT Core topic rule that receives the messages published by your smart thermostat, inspects the sampled sound level, and updates the room occupancy state of your device shadow as it changes. The topic rule will use conditional logic in the SQL query to construct a new payload and the IoT Core republish action to send the new payload to the device shadow. + +1. Go to the AWS IoT Core management console, choose *Act*, choose *Rules*, and choose **Create**. +2. Give your rule a name and description. Further steps in this material assume the name is `thermostatRule`. +3. Use the following query and be sure to replace **<>** with your device client Id/serial number. +```SQL +SELECT CASE state.reported.sound > 10 WHEN true THEN true ELSE false END AS state.desired.roomOccupancy FROM '$aws/things/<>/shadow/update/accepted' WHERE state.reported.sound <> Null +``` +4. Choose **Add action**. +5. Select *Republish a message to an AWS IoT topic* and choose **Configure action**. +6. For *Topic*, use `$$aws/things/<>/shadow/update`. Be sure to replace **<>** with your device's client Id/serial number. +7. For *Choose or create a role to grant AWS IoT access to perform this action.* choose **Create Role** and in the pop-up give your new IAM role a name, then choose **Create role**. +8. Choose **Add action** to finish configuring your action and return to the rule creation form. +9. Click **Create Rule** to create this rule in AWS IoT rules engine. + +Let's break down this rule and explain the parts. The SELECT clause uses a CASE statement to achieve our simple threshold banding. If the sound level reported by the device is over 10 (on a scale of 0-255), then we treat this as an occupied room. You can modify the 10 to set your solution's threshold for an occupied room based on the observed degree of ambient noise. + +The output of the CASE statement is saved to payload key `state.desired.roomOccupancy` with the AS keyword. This means we are creating a new payload like `{"state": {"desired": {"roomOccupancy": false}}}` and sending this payload on to the action. + +The FROM clause describes the topic filter on which this rule will receive new messages. In this case, we want to take action when the device shadow service has accepted a new change, so we use the topic filter `$aws/things/<>/shadow/update/accepted`. There's no way to intercept the message published from the device on `$aws/things/<>/shadow/update` and modify it in-flight before it goes to the device shadow service because the rules engine and device shadow service receive copies of the published message in parallel. This behavior is just like any other two subscribers on the same topic when a message is published; they will each receive a copy. The trade off of this pattern is that for each shadow reported by the device, a second shadow update is published that computes the roomOccupancy, effectively doubling the traffic on the shadow. (One way to optimize here is not to publish a new shadow message for roomOccupancy if the new value is the same as the previous one. This can be achieved by modifying the WHERE clause. This educational solution prefers simplicity over optimization.) + +The WHERE clause defines a conditional statement such that the rule's actions are only executed if this statement resolves true. We use it here to prevent an infinite loop. By including the conditional statement `state.reported.sound <> Null` we are configuring this rule to only act on shadow updates where the sound value is included (or rather, not null). The thermostat reports a `state.reported.sound` value in the shadow update, so this rule will act when the thermostat publishes messages. When the rule publishes its own shadow update, the rule will not fire again because the new shadow update payload only includes the `state.desired.roomOccupancy` key and no value for `state.reported.sound`. + +The action of this rule is "republish" or in other words, publish the output of the rule query as a new message on an indicated topic. The publish topic we use in this action is `$$aws/things/<>/shadow/update` to send a new message to the device shadow. This is the same topic that your thermostat is publishing on via the device shadow interface of the C SDK. + +(Optional depth) Why do we use two $ characters here but a single $ in other uses of the device shadow topics? The reason is the rules engine action optionally supports substitution templates. Substitution templates let you define an expression that gets evaluated at runtime. Substitution templates use the notation `${ YOUR_EXPRESSION_HERE }` and this conflicts with the `$aws` prefix of the device shadow topic. To use the correct device shadow topic in a republish action, you must escape the first $ character and this looks like `$$aws/things/<>/shadow/update`. + +Once you have deployed this rule in IoT Core, you should start to see the room occupancy status updated on your smart thermostat logger (`idf.py monitor -p <>`). + +### Preparing to determine commands for HVAC +The next milestone in this chapter is to prepare the cloud infrastructure needed to dictate new HVAC states (e.g. heating/cooling/standby) based on current temperature and room occupancy. You will provision the IoT Events service to receive messages and then create a second IoT Core rule to integrate with IoT Events. This will create the data flow from IoT Core to IoT Events that is necessary before you move on to create the detector model that dictates the HVAC state. + +IoT Events has two resource types: inputs and detector models. An input is a pre-defined schema for mapping inbound messages to detector models. A detector model is a finite state machine that processes messages from one or more inputs and determines if the state of the model should change. + +Follow these steps to create the input resource in IoT Events: +1. Go to [IoT Events management console](https://us-west-2.console.aws.amazon.com/iotevents/home?region=us-west-2). Expand the left menu, choose **Inputs**, then choose **Create input**. + {{< img "IoT_Events-Create-Input.webp" "Choose test in AWS IoT console" >}} +2. Name your input `thermostat` and give it a description. Further steps in this module are dependent upon the name being `thermostat`. +3. You must upload a JSON file to define the schema. Create a new file on your computer with the following contents and a file name like `input.json`: +```JSON +{ + "current": { + "state": { + "reported": { + "sound": 10, + "temperature": 35, + "roomOccupancy": false, + "hvacStatus": "HEATING" + } + }, + "version": 13 + }, + "timestamp": 1606282489 +} +``` +4. Choose **Upload file** and select the new file you created. You should see a preview of the schema interpreted from the file. +5. Choose **Create** to finish creating the input. + +Now that the input resource is created in IoT Events, you can return to IoT Core to create a new rule that will forward device shadow updates to it that you will use to create the HVAC control application. You will create the control application in the next chapter. +1. Go back to [AWS IoT Core management console](https://us-west-2.console.aws.amazon.com/iot/home?region=us-west-2#/), choose *Act*, choose *Rules* and choose **Create**. +2. Give your rule a name and description. +3. Use this statement for the query and be sure to replace **<>** with your device's client Id/serial number: +```SQL +SELECT current.state as current.state, current.version as current.version, timestamp FROM '$aws/things/CLIENT_ID/shadow/update/documents' +``` +4. Choose **Add action**. +5. Select *Send a message to an IoT Events Input* and choose **Configure action**. +6. For *Input name*, find the name of the Input resource you created in the previous step of the IoT Events console. The assumed name is `thermostat`. +7. For *Role*, choose **Create Role**, give your role a name, then choose **Create role** to finalize your new IAM role that gives permission to IoT Core to send messages to IoT Events. +8. Choose **Add action** to finalize your new rule action. You should be returned to the rule creation form. +9. Choose **Create rule** to finish creating the rule. + +This rule is much simpler compared to the previous one! The rule is configured to receive the full JSON document whenever the smart thermostat device shadow is updated, then forward it to your new IoT Events Input. The Input is configured to parse only a few of the fields from the device shadow document and will discard the unneeded ones. + +## Validation steps +Before moving on to the next chapter, you can validate that your solution is configured as intended by... + +1. As your thermostat device detects varying noise levels, you should see the device receive updated status of roomOccupancy from your rule. Try alternating playing some music to make noise for ten seconds and being quiet for ten seconds to see the state change in the device logger (`idf.py monitor -p <>`). + +If these are working as expected, let's move on to [**Cloud application**](/en/smart-thermostat/cloud-application.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Data transforms and routing/_index.en_uk.md b/workshop/content/Smart thermostat/Data transforms and routing/_index.en_uk.md new file mode 100644 index 0000000..2756d69 --- /dev/null +++ b/workshop/content/Smart thermostat/Data transforms and routing/_index.en_uk.md @@ -0,0 +1,107 @@ ++++ +title = "Data transforms and routing" +weight = 40 +pre = "d. " ++++ + +## Chapter introduction +By the end of this chapter, your cloud solution should do the following: + +* Transform raw sound data received from the device into actionable intelligence of room occupancy with application logic stored in the cloud +* Synchronize the state of room occupancy from the device shadow down to the device +* Set up a resource in AWS IoT Events to receive messages from your device +* Forward messages published by the thermostat device to AWS IoT Events for further processing + +## How to set up the cloud solution +### Deriving room occupancy from sound +The first part of your cloud solution is to add the intelligence that derives whether the room is occupied by people using it. To approximate room occupation, you will use the sound level reported by the smart thermostat device. If the sampled sound level is over a given threshold, you will mark the room as occupied. If it is under the threshold, you will mark the room as unoccupied. You can store this status in the device shadow and use that to sync changes back down to the device. + +Why use sound level and simple threshold banding to classify room occupancy? Why not use a motion sensor? In this case, a microphone is the sensor that is available for use. When designing IoT solutions, you will not always have the budget for the best possible input data. This approach strikes a balance of frugality and achieving the use case. The author acknowledges this would not work for every use case, such as a team meeting for people who are hard of hearing and using sign language, or for team meetings starting with a silent document review. + +You will use two concepts of AWS IoT Core to achieve this use case of deriving room occupancy. These are *topic rules* and *device shadow*. A topic rule lets you define behavior for messages arriving on topic filter, such as performing live transformations on the payload and routing payloads to new destinations. The device shadow is a semi-structured JSON document for synchronizing the reported and desired state of a device. Any modifications made to the device shadow of your thermostat will be sent as new payloads to your device. You have already set up and tested that your device is receiving these shadow updates in the previous chapter. + +You can combine topic rules and device shadows to functionally make updates to the device shadow based on application logic stored in the cloud. This enables rapid updating of application logic without pushing new code to your device. + +Your first milestone in this chapter is to create an IoT Core topic rule that receives the messages published by your smart thermostat, inspects the sampled sound level, and updates the room occupancy state of your device shadow as it changes. The topic rule will use conditional logic in the SQL query to construct a new payload and the IoT Core republish action to send the new payload to the device shadow. + +1. Go to the AWS IoT Core management console, choose *Act*, choose *Rules*, and choose **Create**. +2. Give your rule a name and description. Further steps in this material assume the name is `thermostatRule`. +3. Use the following query and be sure to replace **<>** with your device client Id/serial number. +```SQL +SELECT CASE state.reported.sound > 10 WHEN true THEN true ELSE false END AS state.desired.roomOccupancy FROM '$aws/things/<>/shadow/update/accepted' WHERE state.reported.sound <> Null +``` +4. Choose **Add action**. +5. Select *Republish a message to an AWS IoT topic* and choose **Configure action**. +6. For *Topic*, use `$$aws/things/<>/shadow/update`. Be sure to replace **<>** with your device's client Id/serial number. +7. For *Choose or create a role to grant AWS IoT access to perform this action.* choose **Create Role** and in the pop-up give your new IAM role a name, then choose **Create role**. +8. Choose **Add action** to finish configuring your action and return to the rule creation form. +9. Click **Create Rule** to create this rule in AWS IoT rules engine. + +Let's break down this rule and explain the parts. The SELECT clause uses a CASE statement to achieve our simple threshold banding. If the sound level reported by the device is over 10 (on a scale of 0-255), then we treat this as an occupied room. You can modify the 10 to set your solution's threshold for an occupied room based on the observed degree of ambient noise. + +The output of the CASE statement is saved to payload key `state.desired.roomOccupancy` with the AS keyword. This means we are creating a new payload like `{"state": {"desired": {"roomOccupancy": false}}}` and sending this payload on to the action. + +The FROM clause describes the topic filter on which this rule will receive new messages. In this case, we want to take action when the device shadow service has accepted a new change, so we use the topic filter `$aws/things/<>/shadow/update/accepted`. There's no way to intercept the message published from the device on `$aws/things/<>/shadow/update` and modify it in-flight before it goes to the device shadow service because the rules engine and device shadow service receive copies of the published message in parallel. This behavior is just like any other two subscribers on the same topic when a message is published; they will each receive a copy. The trade off of this pattern is that for each shadow reported by the device, a second shadow update is published that computes the roomOccupancy, effectively doubling the traffic on the shadow. (One way to optimize here is not to publish a new shadow message for roomOccupancy if the new value is the same as the previous one. This can be achieved by modifying the WHERE clause. This educational solution prefers simplicity over optimization.) + +The WHERE clause defines a conditional statement such that the rule's actions are only executed if this statement resolves true. We use it here to prevent an infinite loop. By including the conditional statement `state.reported.sound <> Null` we are configuring this rule to only act on shadow updates where the sound value is included (or rather, not null). The thermostat reports a `state.reported.sound` value in the shadow update, so this rule will act when the thermostat publishes messages. When the rule publishes its own shadow update, the rule will not fire again because the new shadow update payload only includes the `state.desired.roomOccupancy` key and no value for `state.reported.sound`. + +The action of this rule is "republish" or in other words, publish the output of the rule query as a new message on an indicated topic. The publish topic we use in this action is `$$aws/things/<>/shadow/update` to send a new message to the device shadow. This is the same topic that your thermostat is publishing on via the device shadow interface of the C SDK. + +(Optional depth) Why do we use two $ characters here but a single $ in other uses of the device shadow topics? The reason is the rules engine action optionally supports substitution templates. Substitution templates let you define an expression that gets evaluated at runtime. Substitution templates use the notation `${ YOUR_EXPRESSION_HERE }` and this conflicts with the `$aws` prefix of the device shadow topic. To use the correct device shadow topic in a republish action, you must escape the first $ character and this looks like `$$aws/things/<>/shadow/update`. + +Once you have deployed this rule in IoT Core, you should start to see the room occupancy status updated on your smart thermostat logger (`idf.py monitor -p <>`). + +### Preparing to determine commands for HVAC +The next milestone in this chapter is to prepare the cloud infrastructure needed to dictate new HVAC states (e.g. heating/cooling/standby) based on current temperature and room occupancy. You will provision the IoT Events service to receive messages and then create a second IoT Core rule to integrate with IoT Events. This will create the data flow from IoT Core to IoT Events that is necessary before you move on to create the detector model that dictates the HVAC state. + +IoT Events has two resource types: inputs and detector models. An input is a pre-defined schema for mapping inbound messages to detector models. A detector model is a finite state machine that processes messages from one or more inputs and determines if the state of the model should change. + +Follow these steps to create the input resource in IoT Events: +1. Go to [IoT Events management console](https://us-west-2.console.aws.amazon.com/iotevents/home?region=us-west-2). Expand the left menu, choose **Inputs**, then choose **Create input**. + {{< img "IoT_Events-Create-Input.webp" "Choose test in AWS IoT console" >}} +2. Name your input `thermostat` and give it a description. Further steps in this module are dependent upon the name being `thermostat`. +3. You must upload a JSON file to define the schema. Create a new file on your computer with the following contents and a file name like `input.json`: +```JSON +{ + "current": { + "state": { + "reported": { + "sound": 10, + "temperature": 35, + "roomOccupancy": false, + "hvacStatus": "HEATING" + } + }, + "version": 13 + }, + "timestamp": 1606282489 +} +``` +4. Choose **Upload file** and select the new file you created. You should see a preview of the schema interpreted from the file. +5. Choose **Create** to finish creating the input. + +Now that the input resource is created in IoT Events, you can return to IoT Core to create a new rule that will forward device shadow updates to it that you will use to create the HVAC control application. You will create the control application in the next chapter. +1. Go back to [AWS IoT Core management console](https://us-west-2.console.aws.amazon.com/iot/home?region=us-west-2#/), choose *Act*, choose *Rules* and choose **Create**. +2. Give your rule a name and description. +3. Use this statement for the query and be sure to replace **<>** with your device's client Id/serial number: +```SQL +SELECT current.state as current.state, current.version as current.version, timestamp FROM '$aws/things/CLIENT_ID/shadow/update/documents' +``` +4. Choose **Add action**. +5. Select *Send a message to an IoT Events Input* and choose **Configure action**. +6. For *Input name*, find the name of the Input resource you created in the previous step of the IoT Events console. The assumed name is `thermostat`. +7. For *Role*, choose **Create Role**, give your role a name, then choose **Create role** to finalize your new IAM role that gives permission to IoT Core to send messages to IoT Events. +8. Choose **Add action** to finalize your new rule action. You should be returned to the rule creation form. +9. Choose **Create rule** to finish creating the rule. + +This rule is much simpler compared to the previous one! The rule is configured to receive the full JSON document whenever the smart thermostat device shadow is updated, then forward it to your new IoT Events Input. The Input is configured to parse only a few of the fields from the device shadow document and will discard the unneeded ones. + +## Validation steps +Before moving on to the next chapter, you can validate that your solution is configured as intended by... + +1. As your thermostat device detects varying noise levels, you should see the device receive updated status of roomOccupancy from your rule. Try alternating playing some music to make noise for ten seconds and being quiet for ten seconds to see the state change in the device logger (`idf.py monitor -p <>`). + +If these are working as expected, let's move on to [**Cloud application**](/en_uk/smart-thermostat/cloud-application.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Introduction/_index.en.md b/workshop/content/Smart thermostat/Introduction/_index.en.md new file mode 100644 index 0000000..cf8c212 --- /dev/null +++ b/workshop/content/Smart thermostat/Introduction/_index.en.md @@ -0,0 +1,33 @@ ++++ +title = "Introduction" +weight = 10 +pre = "a. " ++++ + +## Your task +In this scenario, you are taking on the role of a full-stack developer tasked with automating thermostat functions of a meeting room in order to conserve energy. You will use the Core2 for AWS IoT EduKit reference hardware for the thermostat hardware and deploy an end-to-end solution combining the reference hardware kit as an HVAC controller with the power of the AWS cloud. We assume a fictional HVAC system and terminate the solution at the edge with your Core2 for AWS IoT EduKit as a thermostat. + +The thermostat should use a narrower range of temperatures when employees are occupying the room to maximize their comfort. When the room is not occupied, a wider range of temperatures is permitted to save energy. The solution should recognize when employees are present in the room and engage the HVAC to deliver the comfort zone of temperature. + +## Problem solving +To minimize costs of needing camera equipment to detect when the room is occupied, this solution will use the kit microphone to sample a simple noise level. You will capture the ambient noise level from the microphone and the room temperature from the device's sensors, then publish these values to AWS. + +Your serverless solution in AWS will convert noise levels into a boolean and use that to determine whether the room is occupied. You can use a simple threshold of noise level to make that determination. + +While the room is occupied and the measured temperature is out of comfort bounds, the solution will send a command to the device to start heating or cooling, as needed. When the temperature is then measured as within bounds, the solution will send a command to the device to resume a standby mode. + +## Solution architecture +![Smart thermostat solution architecture](introduction/thermostat-overview.png) + +## Let's go! +Are you ready to start building? Let's review you have the following prerequisites sorted: + +1. Have you completed the **Blinky Hello World** tutorial? +2. Is your Core2 for AWS IoT EduKit already provisioned in AWS IoT Core? Meaning there is a registered AWS IoT thing with a certificate and policy attached that enables publish and subscribe operations? These steps are in the **Blinky Hello World** tutorial. +3. Have you confirmed that you can see messages arriving from your device using a test MQTT client like the one in the AWS IoT Core console? You should be able to subscribe to a topic that your device is publishing on and see those messages arrive in the test client. +4. Do you know which serial port your Core2 for AWS IoT EduKit device is connected to? This is also covered in the **Blinky Hello World** tutorial. You will paste it in to several of the steps of this tutorial for placeholder values like **<>**. + +If so, let's begin by moving on to the next chapter, [**Data acquisition**](/en/smart-thermostat/data-acquisition.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Introduction/_index.en_uk.md b/workshop/content/Smart thermostat/Introduction/_index.en_uk.md new file mode 100644 index 0000000..24eee17 --- /dev/null +++ b/workshop/content/Smart thermostat/Introduction/_index.en_uk.md @@ -0,0 +1,33 @@ ++++ +title = "Introduction" +weight = 10 +pre = "a. " ++++ + +## Your task +In this scenario, you are taking on the role of a full-stack developer tasked with automating thermostat functions of a meeting room in order to conserve energy. You will use the Core2 for AWS IoT EduKit reference hardware for the thermostat hardware and deploy an end-to-end solution combining the reference hardware kit as an HVAC controller with the power of the AWS cloud. We assume a fictional HVAC system and terminate the solution at the edge with your Core2 for AWS IoT EduKit as a thermostat. + +The thermostat should use a narrower range of temperatures when employees are occupying the room to maximize their comfort. When the room is not occupied, a wider range of temperatures is permitted to save energy. The solution should recognize when employees are present in the room and engage the HVAC to deliver the comfort zone of temperature. + +## Problem solving +To minimize costs of needing camera equipment to detect when the room is occupied, this solution will use the kit microphone to sample a simple noise level. You will capture the ambient noise level from the microphone and the room temperature from the device's sensors, then publish these values to AWS. + +Your serverless solution in AWS will convert noise levels into a boolean and use that to determine whether the room is occupied. You can use a simple threshold of noise level to make that determination. + +While the room is occupied and the measured temperature is out of comfort bounds, the solution will send a command to the device to start heating or cooling, as needed. When the temperature is then measured as within bounds, the solution will send a command to the device to resume a standby mode. + +## Solution architecture +![Smart thermostat solution architecture](introduction/thermostat-overview.png) + +## Let's go! +Are you ready to start building? Let's review you have the following prerequisites sorted: + +1. Have you completed the **Blinky Hello World** tutorial? +2. Is your Core2 for AWS IoT EduKit already provisioned in AWS IoT Core? Meaning there is a registered AWS IoT thing with a certificate and policy attached that enables publish and subscribe operations? These steps are in the **Blinky Hello World** tutorial. +3. Have you confirmed that you can see messages arriving from your device using a test MQTT client like the one in the AWS IoT Core console? You should be able to subscribe to a topic that your device is publishing on and see those messages arrive in the test client. +4. Do you know which serial port your Core2 for AWS IoT EduKit device is connected to? This is also covered in the **Blinky Hello World** tutorial. You will paste it in to several of the steps of this tutorial for placeholder values like **<>**. + +If so, let's begin by moving on to the next chapter, [**Data acquisition**](/en_uk/smart-thermostat/data-acquisition.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/Introduction/thermostat-overview.png b/workshop/content/Smart thermostat/Introduction/thermostat-overview.png new file mode 100644 index 0000000..9d3db43 Binary files /dev/null and b/workshop/content/Smart thermostat/Introduction/thermostat-overview.png differ diff --git a/workshop/content/Smart thermostat/_index.en.md b/workshop/content/Smart thermostat/_index.en.md new file mode 100644 index 0000000..5796e65 --- /dev/null +++ b/workshop/content/Smart thermostat/_index.en.md @@ -0,0 +1,28 @@ ++++ +title = "Smart Thermostat" +chapter = true +weight = 30 +pre = "3. " ++++ + +In this tutorial, you will configure your reference hardware into a smart thermostat that controls a fictional HVAC system. Your smart thermostat will report the measured room temperature and noise level to its device shadow in the cloud. You will also configure a serverless application that will listen for the reported measurements, determine the state to which the thermostat should be set, and send commands back to the device that tells it what to do do. + +Assumptions. Before starting this tutorial, verify the following prerequisites: +1. You have an [M5Stack Core2 ESP32 IoT Development Kit for AWS IoT EduKit](https://www.amazon.com/dp/B08NP5LVFH). +2. You have an AWS account that is not running any production workloads (i.e. an account safe for sandbox and evaluation purposes). +3. You have a user login or role to the AWS account with administrator access. +4. Your Core2 for AWS IoT EduKit has been provisioned in AWS IoT Core and is already communicating with AWS via MQTT. Start with the [**Blinky Hello World**](/en/blinky-hello-world.html) tutorial if you have not completed provisioning and established connectivity. +5. You have at least a basic technical understanding of AWS IoT messaging concepts such as topics, publishing, and subscribing. + +Learning Objectives. By the end of this lab, you should know: +1. How to acquire temperature and sound levels from the Core2 for AWS IoT EduKit device. +2. How to publish temperature and sound measurements from the device to AWS IoT Core. +3. How to report measured values to the device shadow. +4. How to perform message transforms with the AWS IoT Core rules engine. +5. How to build a serverless application that responds to inputs and detects complex events. +6. How to send commands to your device via the device shadow. + +To get started with this tutorial, go to the first chapter, [**Introduction**](/en/smart-thermostat/introduction.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/Smart thermostat/_index.en_uk.md b/workshop/content/Smart thermostat/_index.en_uk.md new file mode 100644 index 0000000..632bc79 --- /dev/null +++ b/workshop/content/Smart thermostat/_index.en_uk.md @@ -0,0 +1,28 @@ ++++ +title = "Smart Thermostat" +chapter = true +weight = 30 +pre = "3. " ++++ + +In this tutorial, you will configure your reference hardware into a smart thermostat that controls a fictional HVAC system. Your smart thermostat will report the measured room temperature and noise level to its device shadow in the cloud. You will also configure a serverless application that will listen for the reported measurements, determine the state to which the thermostat should be set, and send commands back to the device that tells it what to do do. + +Assumptions. Before starting this tutorial, verify the following prerequisites: +1. You have an [M5Stack Core2 ESP32 IoT Development Kit for AWS IoT EduKit](https://www.amazon.com/dp/B08NP5LVFH). +2. You have an AWS account that is not running any production workloads (i.e. an account safe for sandbox and evaluation purposes). +3. You have a user login or role to the AWS account with administrator access. +4. Your Core2 for AWS IoT EduKit has been provisioned in AWS IoT Core and is already communicating with AWS via MQTT. Start with the [**Blinky Hello World**](/en_uk/blinky-hello-world.html) tutorial if you have not completed provisioning and established connectivity. +5. You have at least a basic technical understanding of AWS IoT messaging concepts such as topics, publishing, and subscribing. + +Learning Objectives. By the end of this lab, you should know: +1. How to acquire temperature and sound levels from the Core2 for AWS IoT EduKit device. +2. How to publish temperature and sound measurements from the device to AWS IoT Core. +3. How to report measured values to the device shadow. +4. How to perform message transforms with the AWS IoT Core rules engine. +5. How to build a serverless application that responds to inputs and detects complex events. +6. How to send commands to your device via the device shadow. + +To get started with this tutorial, go to the first chapter, [**Introduction**](/en_uk/smart-thermostat/introduction.html). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/_index.en.md b/workshop/content/_index.en.md new file mode 100644 index 0000000..59cfbe6 --- /dev/null +++ b/workshop/content/_index.en.md @@ -0,0 +1,26 @@ ++++ +title = "AWS IoT EduKit" +chapter = true +weight = 1 ++++ +AWS IoT EduKit is an easy way to learn how to build IoT applications using AWS services through a prescriptive learning program. AWS IoT EduKit helps developers – from students to experienced +engineers and professionals – receive hands-on experience building end-to-end IoT applications by combining a reference hardware kit with a set of easy to follow guides and example code. + +### Benefits of AWS IoT EduKit +#### Simplified hardware selection +- AWS IoT EduKit’s reference hardware kit – made and sold by our manufacturing partner, M5Stack – is packed with onboard features to enable a multitude of IoT applications out of the box Developers can broaden their capabilities to cover additional use-cases with plug & play expansion options. +- The reference hardware kit provides powerful and secure hardware to build a range of IoT applications (entry level to advanced/professional) at an entry level price tag. It is powered by an Espressif ESP32 MCU, coupled with a Microchip ATECC608 Trust&GO secure element. It is Alexa capable, comes with multiple on-board peripherals, a wide range of separately available expansion modules for additional connectivity options (e.g. LoRaWAN, NB-IoT, LTE) and plug & play grove-connector peripherals (e.g. sensors, actuators) to cover a wider breadth of use-cases. + +#### Supports a wide range of software frameworks +- AWS IoT EduKit’s reference hardware supports a wide range of application frameworks (e.g. FreeRTOS, Arduino, MicroPython) which allows developers to code in the language of their choice and progress in building cloud connected embedded applications on a single hardware platform. +- AWS IoT EduKit simplifies on-boarding to AWS IoT through through Espressif's Rainmaker platform and PlatformIO development platform. The Espressif Rainmaker platform allows you to control the reference hardware kit and control the smart home application without an AWS account. The PlatformIO development platform simplifies embedded development to quickly view, edit, and flash code. + +#### Easy access to Sample Code +- AWS IoT EduKit provides up-to-date content and example code to the most common IoT solutions. +- Through AWS IoT EduKit [tutorials](https://edukit.workshop.aws/en/getting-started.html), developers can access a variety of free content to gain expertise in building and managing IoT applications using AWS services. + +### How it works +To get started, purchase your reference hardware kit from our manufacturing partner, M5Stack, either on [Amazon.com](https://www.amazon.com/dp/B08NP5LVFH) or directly on the [M5Stack store](https://m5stack.com/products/m5stack-core2-esp32-iot-development-kit-for-aws-iot-edukit). Once you receive the hardware kit, access the [getting started tutorial](https://edukit.workshop.aws/en/getting-started.html) and follow the steps in the prerequisite section to install the RainMaker Agent firmware on the device and the Espressif Rainmaker Mobile App on your mobile phone. The Espressif Rainmaker Mobile App will allow you to control the AWS IoT EduKit reference hardware and connect to AWS IoT. Next, you can select from a list of free projects available on AWS IoT EduKit's website. Start with building a basic connected home application and progress over time to running a machine learning model with Amazon SageMaker Autopilot or building a Voice Assisted Smart Home application with Alexa Voice Service Integration for AWS IoT (AFI). To learn more about the hardware specifications, please visit the [M5Stack Docs](https://docs.m5stack.com/#/en/core/core2_for_aws) or [Partner Device Catalog](https://devices.amazonaws.com/detail/a3G0h000007djMLEAY). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/content/_index.en_uk.md b/workshop/content/_index.en_uk.md new file mode 100644 index 0000000..615ecba --- /dev/null +++ b/workshop/content/_index.en_uk.md @@ -0,0 +1,26 @@ ++++ +title = "AWS IoT EduKit" +chapter = true +weight = 1 ++++ +AWS IoT EduKit is an easy way to learn how to build IoT applications using AWS services through a prescriptive learning program. AWS IoT EduKit helps developers – from students to experienced +engineers and professionals – receive hands-on experience building end-to-end IoT applications by combining a reference hardware kit with a set of easy to follow guides and example code. + +### Benefits of AWS IoT EduKit +#### Simplified hardware selection +- AWS IoT EduKit’s reference hardware kit – made and sold by our manufacturing partner, M5Stack – is packed with onboard features to enable a multitude of IoT applications out of the box Developers can broaden their capabilities to cover additional use-cases with plug & play expansion options. +- The reference hardware kit provides powerful and secure hardware to build a range of IoT applications (entry level to advanced/professional) at an entry level price tag. It is powered by an Espressif ESP32 MCU, coupled with a Microchip ATECC608 Trust&GO secure element. It is Alexa capable, comes with multiple on-board peripherals, a wide range of separately available expansion modules for additional connectivity options (e.g. LoRaWAN, NB-IoT, LTE) and plug & play grove-connector peripherals (e.g. sensors, actuators) to cover a wider breadth of use-cases. + +#### Supports a wide range of software frameworks +- AWS IoT EduKit’s reference hardware supports a wide range of application frameworks (e.g. FreeRTOS, Arduino, MicroPython) which allows developers to code in the language of their choice and progress in building cloud connected embedded applications on a single hardware platform. +- AWS IoT EduKit simplifies on-boarding to AWS IoT through through Espressif's Rainmaker platform and PlatformIO development platform. The Espressif Rainmaker platform allows you to control the reference hardware kit and control the smart home application without an AWS account. The PlatformIO development platform simplifies embedded development to quickly view, edit, and flash code. + +#### Easy access to Sample Code +- AWS IoT EduKit provides up-to-date content and example code to the most common IoT solutions. +- Through AWS IoT EduKit [tutorials](https://edukit.workshop.aws/en_uk/getting-started.html), developers can access a variety of free content to gain expertise in building and managing IoT applications using AWS services. + +### How it works +To get started, purchase your reference hardware kit from our manufacturing partner, M5Stack, either on [Amazon.com](https://www.amazon.com/dp/B08NP5LVFH) or directly on the [M5Stack store](https://m5stack.com/products/m5stack-core2-esp32-iot-development-kit-for-aws-iot-edukit). Once you receive the hardware kit, access the [getting started tutorial](https://edukit.workshop.aws/en_uk/getting-started.html) and follow the steps in the prerequisite section to install the RainMaker Agent firmware on the device and the Espressif Rainmaker Mobile App on your mobile phone. The Espressif Rainmaker Mobile App will allow you to control the AWS IoT EduKit reference hardware and connect to AWS IoT. Next, you can select from a list of free projects available on AWS IoT EduKit's website. Start with building a basic connected home application and progress over time to running a machine learning model with Amazon SageMaker Autopilot or building a Voice Assisted Smart Home application with Alexa Voice Service Integration for AWS IoT (AFI). To learn more about the hardware specifications, please visit the [M5Stack Docs](https://docs.m5stack.com/#/en/core/core2_for_aws) or [Partner Device Catalogue](https://devices.amazonaws.com/detail/a3G0h000007djMLEAY). + +--- +{{% button href="https://github.com/m5stack/Core2-for-AWS-IoT-EduKit/issues" icon="fas fa-bug" %}}Report bugs{{% /button %}} {{% button href="https://community.m5stack.com/category/41/core2-for-aws" icon="far fa-question-circle" %}}Community support{{% /button %}} \ No newline at end of file diff --git a/workshop/layouts/_default/_markup/render-link.html b/workshop/layouts/_default/_markup/render-link.html new file mode 100644 index 0000000..a5cd49a --- /dev/null +++ b/workshop/layouts/_default/_markup/render-link.html @@ -0,0 +1 @@ +{{ .Text | safeHTML }} \ No newline at end of file diff --git a/workshop/layouts/index.json b/workshop/layouts/index.json new file mode 100644 index 0000000..89ea0f8 --- /dev/null +++ b/workshop/layouts/index.json @@ -0,0 +1,12 @@ +[ + { "pageTitle": "Index", + "relativePagePath": "/{{ $.Site.Language}}/" + },{{ $totalPage := newScratch }}{{ $totalPage.Add "pageCount" 1}}{{ $currentNode := . }}{{ $showvisitedlinks := .Site.Params.showVisitedLinks }}{{range .Site.Home.Sections.ByWeight}}{{ template "section-tree-nav1" dict "parent" .Parent "sect" . "currentnode" $currentNode "showvisitedlinks" $showvisitedlinks "pageCount" 0 "totalPage" $totalPage}}{{end}}{{ define "section-tree-nav1" }}{{ $showvisitedlinks := .showvisitedlinks }}{{ $currentNode := .currentnode }}{{with .sect}}{{if .IsSection}}{{$pageCount := $.totalPage.Get "pageCount"}}{{$res := add 1 $pageCount}}{{$.totalPage.Set "pageCount" $res}}{{ $numberOfPages := (add (len .Pages) (len .Sections)) }}{{ if ne $numberOfPages 0 }}{{ $currentNode.Scratch.Set "pages" .Pages }}{{ if .Sections}}{{ $currentNode.Scratch.Set "pages" (.Pages | union .Sections) }}{{end}}{{ $pages := ($currentNode.Scratch.Get "pages") }}{{if eq .Site.Params.ordersectionsby "title"}} {{ range $pages.ByTitle }}{{ if and .Params.hidden (not $.showhidden) }} {{else}}{{ template "section-tree-nav1" dict "sect" . "currentnode" $currentNode "showvisitedlinks" $showvisitedlinks "totalPage" $.totalPaget }}{{end}}{{ end }}{{else}}{{ range $pages.ByWeight }}{{ if and .Params.hidden (not $.showhidden) }} {{else}}{{ template "section-tree-nav1" dict "sect" . "currentnode" $currentNode "showvisitedlinks" $showvisitedlinks "totalPage" $.totalPage}}{{end}}{{ end }}{{end}}{{ end }}{{else}}{{ if not .Params.Hidden }}{{$pageCount := $.totalPage.Get "pageCount"}}{{$res := add 1 $pageCount}}{{$.totalPage.Set "pageCount" $res}}{{ end }}{{end}}{{ end }}{{ end }}{{ $totalPage2 := newScratch }}{{ $totalPage2.Add "pageCount" 1}}{{ $currentNode := . }}{{ $showvisitedlinks := .Site.Params.showVisitedLinks }}{{range .Site.Home.Sections.ByWeight}}{{ template "section-tree-nav2" dict "parent" .Parent "sect" . "currentnode" $currentNode "showvisitedlinks" $showvisitedlinks "pageCount" 0 "totalPage" $totalPage "totalPage2" $totalPage2}}{{end}}]{{ define "section-tree-nav2" }}{{ $showvisitedlinks := .showvisitedlinks }}{{ $currentNode := .currentnode }}{{with .sect}}{{if .IsSection}}{{$pageCount := $.totalPage2.Get "pageCount"}}{{$res := add 1 $pageCount}}{{$.totalPage2.Set "pageCount" $res}}{{$maxPage := $.totalPage.Get "pageCount"}} + { + "pageTitle": "{{.Title}}", + "relativePagePath": "{{.RelPermalink}}" + }{{if ne $res $maxPage }},{{end}}{{ $numberOfPages := (add (len .Pages) (len .Sections)) }}{{ if ne $numberOfPages 0 }}{{ $currentNode.Scratch.Set "pages" .Pages }}{{ if .Sections}}{{ $currentNode.Scratch.Set "pages" (.Pages | union .Sections) }}{{end}}{{ $pages := ($currentNode.Scratch.Get "pages") }}{{if eq .Site.Params.ordersectionsby "title"}} {{ range $pages.ByTitle }}{{ if and .Params.hidden (not $.showhidden) }} {{else}}{{ template "section-tree-nav2" dict "sect" . "currentnode" $currentNode "showvisitedlinks" $showvisitedlinks "totalPage" $.totalPage "totalPage2" $.totalPage2t }}{{end}}{{ end }}{{else}}{{ range $pages.ByWeight }}{{ if and .Params.hidden (not $.showhidden) }} {{else}}{{ template "section-tree-nav2" dict "sect" . "currentnode" $currentNode "showvisitedlinks" $showvisitedlinks "totalPage" $.totalPage "totalPage2" $.totalPage2}}{{end}}{{ end }}{{end}}{{ end }}{{else}}{{ if not .Params.Hidden }}{{$pageCount := $.totalPage2.Get "pageCount"}}{{$res := add 1 $pageCount}}{{$.totalPage2.Set "pageCount" $res}}{{$maxPage := $.totalPage.Get "pageCount"}} + { + "pageTitle": "{{.Title}}", + "relativePagePath": "{{.RelPermalink}}" + }{{if ne $res $maxPage }},{{end}}{{ end }}{{end}}{{ end }}{{ end }} \ No newline at end of file diff --git a/workshop/layouts/partials/custom-footer.html b/workshop/layouts/partials/custom-footer.html new file mode 100644 index 0000000..2f41c17 --- /dev/null +++ b/workshop/layouts/partials/custom-footer.html @@ -0,0 +1,9 @@ +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/workshop/post_build.py b/workshop/post_build.py new file mode 100644 index 0000000..75d4fb7 --- /dev/null +++ b/workshop/post_build.py @@ -0,0 +1,152 @@ +import boto3 +from boto3.dynamodb.conditions import Key, Attr +import json +import os +import sys +import toml +import logging +import datetime +import base64 +import requests +from requests_aws4auth import AWS4Auth +from botocore.utils import ContainerMetadataFetcher +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +# DOES: Analize language used in the content, then update structure of content. + +def main(): + # Setup environment + region = os.environ['RT_REGION'] + workshop_name = os.environ['WORKSHOP_NAME'] + gql_endpoint = os.environ['GQL_ENDPOINT'] + gql_assume_role = os.environ['GQL_ROLE'] + # for now content handle only workshop, in future will be added delivery + content_id = workshop_name + content_structures = [] + + + # Analize languages in toml file + dict_toml = toml.load(open('./config.toml')) + number_of_languages = len(dict_toml['Languages']) + for i, lang in enumerate(dict_toml['Languages']): + print('Load JSON structure\n') + if number_of_languages == 1: + index_location = "./public/index.json" + else: + index_location = "./public/" + lang +"/index.json" + with open(index_location, 'r') as j: + current_structure = json.load(j) + print('Finished Load JSON\n') + content_structures.append({"language": lang, "structure": current_structure}) + + + # Change json format from Python to Javascript because GraphQL can only recognize Javascript type of Json object. + # Javascript Json object {key: "value"} ; Python Json object {"key":"value"} + # TODO: Json_keys is just hardcoded. It should find keys in JSON object. + json_keys = ["language", "structure", "pageTitle", "relativePagePath"] + structures_str = str(content_structures) + for key in json_keys: + target_key = "\'"+key+"\'" + structures_str = structures_str.replace(target_key, key) + structures_str = structures_str.replace("\'", "\"") + + + # AssumeRole + sts_client=boto3.client('sts') + assume_role=sts_client.assume_role( + RoleArn=gql_assume_role, + RoleSessionName='GraphQLExecuter' + ) + access_key_id=assume_role['Credentials']['AccessKeyId'] + secret_access_key=assume_role['Credentials']['SecretAccessKey'] + session_token = assume_role['Credentials']['SessionToken'] + + # # Setting Sigv4 + # uri = os.environ.get('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI') + # credential = ContainerMetadataFetcher().retrieve_uri(uri) + # access_key_id = credential.get('AccessKeyId') + # secret_access_key = credential.get('SecretAccessKey') + # session_token = credential.get('Token') + + + auth = AWS4Auth(access_key_id, secret_access_key, region, 'appsync', session_token=session_token) + + # Load latest version + body = {"query":"""" + query ListContents{ + listContents( + hostName: "%s", + limit: 1, + sortDirection: DESC + ) { + items{ + hostName + version + } + } + } + """%workshop_name + } + + body_json = json.dumps(body) + method = 'POST' + headers = {} + response = requests.request(method, gql_endpoint, auth=auth, data=body_json, headers=headers) + print(response.content.decode('utf-8')) + gql_data = json.loads(response.content.decode('utf-8'))['data']['listContents']['items'][0] + current_version = gql_data['version'] + + # Send latest version + body = {"query":"""" + mutation createContent{ + createContent(input:{ + hostName: "%s" + version: %d + structures: %s + }){ + version + } + } + """% (content_id, current_version,structures_str) + } + body_json = json.dumps(body) + response = requests.request(method, gql_endpoint, auth=auth, data=body_json, headers=headers) + + + # # Send structure data to DynamoDB Table + # print('Start to send structure data\n') + # query_response = version_table.query( + # KeyConditionExpression=Key('content_id').eq(content_id), + # ScanIndexForward = False, + # Limit = 1 + # ) + # # Current version is already updated in pre_build.py + # current_version = int(query_response['Items'][0]['version']) + # unix_timestamp = datetime.datetime.now().strftime('%s') + # update_response = version_table.update_item( + # Key = + # { + # 'content_id': content_id, + # 'version' : current_version + # }, + # UpdateExpression='SET updated_at = :val1, structure = :val2', + # ExpressionAttributeValues={ + # ':val1': unix_timestamp, + # ':val2': structure_json + # } + # ) + print('Finished sending sructure\n') + + print('Post build phase done\n') + +if __name__ == "__main__": + debug = False + if debug: + logger.setLevel(logging.DEBUG) + os.environ['WORKSHOP_NAME'] = 'testName' + os.environ['RT_REGION'] = 'us-east-1' + os.environ['GQL_ENDPOINT'] = 'testEndpoint' + os.environ['GQL_ROLE'] = 'testRole' + main() diff --git a/workshop/pre_build.py b/workshop/pre_build.py new file mode 100644 index 0000000..39fd776 --- /dev/null +++ b/workshop/pre_build.py @@ -0,0 +1,161 @@ +import json +import toml +import os +import requests +import logging +import datetime +import boto3 +from boto3.dynamodb.conditions import Key, Attr +from requests_aws4auth import AWS4Auth +from botocore.utils import ContainerMetadataFetcher +logger = logging.getLogger() + +def main(): + # Parameters from ENV + region = os.environ['RT_REGION'] + workshop_name = os.environ['WORKSHOP_NAME'] + kinesis_stream_name = os.environ['RT_KINESIS'] + cognito_pool_id = os.environ['RT_COGNITO'] + #version_table_name = os.environ['VERSION_TABLE'] + gql_endpoint = os.environ['GQL_ENDPOINT'] + gql_assume_role = os.environ['GQL_ROLE'] + # for now content handle only workshop, in future will be added delivery + content_id = workshop_name + + # AssumeRole + sts_client=boto3.client('sts') + assume_role=sts_client.assume_role( + RoleArn=gql_assume_role, + RoleSessionName='GraphQLExecuter' + ) + access_key_id=assume_role['Credentials']['AccessKeyId'] + secret_access_key=assume_role['Credentials']['SecretAccessKey'] + session_token = assume_role['Credentials']['SessionToken'] + + # # Setting Sigv4 + # uri = os.environ.get('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI') + # credential = ContainerMetadataFetcher().retrieve_uri(uri) + # access_key_id = credential.get('AccessKeyId') + # secret_access_key = credential.get('SecretAccessKey') + # session_token = credential.get('Token') + auth = AWS4Auth(access_key_id, secret_access_key, region, 'appsync', session_token=session_token) + + # Load previous version + body = {"query":"""" + query ListContents{ + listContents( + hostName: "%s", + limit: 1, + sortDirection: DESC + ) { + items{ + hostName + version + } + } + } + """%workshop_name + } + + body_json = json.dumps(body) + method = 'POST' + headers = {} + response = requests.request(method, gql_endpoint, auth=auth, data=body_json, headers=headers) + print(response.content.decode('utf-8')) + try : + gql_data = json.loads(response.content.decode('utf-8'))['data']['listContents']['items'][0] + previous_version = gql_data['version'] + current_version = previous_version + 1 + except: + current_version = 1 + + # Send latest version + body = {"query":"""" + mutation CreateContent{ + createContent(input:{ + hostName: "%s" + version: %d + }){ + version + } + } + """% (content_id, current_version) + } + body_json = json.dumps(body) + response = requests.request(method, gql_endpoint, auth=auth, data=body_json, headers=headers) + print(response.content.decode('utf-8')) + + # Query content version from DDB + # If this commit is first commit, then version = 1, is not first commit then version += 1 + # TODO : Content id will be changed as hostName + + + # print('Load version from DDB\n') + # content_id = workshop_name + # version_table = boto3.resource('dynamodb').Table(version_table_name) + # try: + # query_response = version_table.query( + # KeyConditionExpression=Key('content_id').eq(content_id), + # ScanIndexForward = False, + # Limit = 1 + # ) + # previous_version = int(query_response['Items'][0]['version']) + # print('Older record is found\n') + # current_version = previous_version + 1 + # unix_timestamp = datetime.datetime.now().strftime('%s') + + # update_response = version_table.update_item( + # Key = + # { + # 'content_id': content_id, + # 'version' : current_version + # }, + # UpdateExpression='SET updated_at = :val1', + # ExpressionAttributeValues={ + # ':val1': unix_timestamp + # } + # ) + # print('Update the version succeeded\n') + # except Exception as err: + # #print(err) + # print('First time record\n') + # current_version = 1 + # unix_timestamp = datetime.datetime.now().strftime('%s') + # insert_response = version_table.put_item( + # Item={ + # "content_id": content_id, + # "version": current_version, + # "workshop_name": workshop_name, + # "created_at": unix_timestamp, + # "updated_at": unix_timestamp + # } + # ) + # print('Insert a record succeeded\n') + + #Toml File Injection + try: + dict_toml = toml.load(open('./config.toml')) + dict_toml['params']['contentid'] = content_id + dict_toml['params']['kinesisstreamname'] = kinesis_stream_name + dict_toml['params']['cognitopoolid'] = cognito_pool_id + dict_toml['params']['awsregion'] = region + dict_toml['params']['contentversion'] = current_version + toml.dump(dict_toml, open('./config.toml', mode='w')) + except Exception as err: + logger.warning('Toml File Injection Failed') + logger.exception('Raise Exception: %s', err) + raise + + print('Pre build phase done\n') + +if __name__ == "__main__": + debug = False + if debug: + logger.setLevel(logging.DEBUG) + os.environ['RT_REGION'] = 'us-east-1' + os.environ['RT_KINESIS'] = 'your_kinesisstream_name' + os.environ['RT_COGNITO'] = 'your_cognito_pool_id' + os.environ['WORKSHOP_NAME'] = 'test' + os.environ['GQL_ENDPOINT'] = 'testEndpoint' + os.environ['GQL_ROLE'] = 'testARN' + main() \ No newline at end of file diff --git a/workshop/requirements.txt b/workshop/requirements.txt new file mode 100644 index 0000000..d5396d9 --- /dev/null +++ b/workshop/requirements.txt @@ -0,0 +1,4 @@ +toml == 0.10.0 +boto3 == 1.10.32 +requests == 2.22.0 +requests-aws4auth ==0.9 \ No newline at end of file diff --git a/workshop/static/images/apn-logo.jpg b/workshop/static/images/apn-logo.jpg new file mode 100644 index 0000000..ed7c429 Binary files /dev/null and b/workshop/static/images/apn-logo.jpg differ diff --git a/workshop/static/images/aws-open-source.jpg b/workshop/static/images/aws-open-source.jpg new file mode 100644 index 0000000..c1759f3 Binary files /dev/null and b/workshop/static/images/aws-open-source.jpg differ diff --git a/workshop/static/images/broken-qr.png b/workshop/static/images/broken-qr.png new file mode 100644 index 0000000..4d67c61 Binary files /dev/null and b/workshop/static/images/broken-qr.png differ diff --git a/workshop/static/images/chapter.en.png b/workshop/static/images/chapter.en.png new file mode 100644 index 0000000..beab789 Binary files /dev/null and b/workshop/static/images/chapter.en.png differ diff --git a/workshop/static/js/kinesis.js b/workshop/static/js/kinesis.js new file mode 100644 index 0000000..77bd000 --- /dev/null +++ b/workshop/static/js/kinesis.js @@ -0,0 +1,90 @@ +// variable injection +var awsRegion = document.getElementById('awsRegion').title; +var kinesisStreamName = document.getElementById('kinesisStreamName').title; +var cognitoPoolId = document.getElementById('cognitoPoolId').title; +var deliveryId = document.getElementById('contentId').title; +var version = document.getElementById('versions').title; +var language = document.getElementById('language').title; +var scriptVersion = '2019-12-16' + +// Send log to Kinesis +var sendLog = function () { + console.log('Send Log') + // Configure Credentials to use Cognito + AWS.config.credentials = new AWS.CognitoIdentityCredentials({ + IdentityPoolId: cognitoPoolId + }); + AWS.config.region = awsRegion; + AWS.config.credentials.get(function (err) { + if (err) { + // alert('Error retrieving credentials.'); + console.error(err); + return; + } + // create Amazon Kinesis service object + var kinesis = new AWS.Kinesis({ + apiVersion: '2013-12-02' + }); + + // create user Id + var userId + // check whether user use HTML5 + if (window.localStorage) { + // generate userId if not data in localstorage + userId = localStorage.getItem('userId'); + isRegistered = 'false'; + if (userId == null) { + userId = AWS.config.credentials.identityId; + localStorage.setItem('userId', userId); + isRegistered = 'true'; + } + } else { + userId = 'guestUser' + } + + var recordData = []; + var record = { + Data: JSON.stringify({ + page_path: window.location.pathname, + delivery_id: deliveryId, + user_id: userId, + is_regitered: isRegistered, + version: version, + language: language, + scriptVersion: scriptVersion + }), + PartitionKey: 'partition-' + userId + }; + recordData.push(record); + + kinesis.putRecords({ + Records: recordData, + StreamName: kinesisStreamName + }, function (err, data) { + if (err) { + console.error(err); + } + }); + + }); +}; + +// Call Send Log Func by every 10 sec when the Tab is focused +var interval_sec = 10; +var is_focus = true; + +window.onfocus = function () { + console.log('Start focus') + is_focus = true; +} +window.onblur = function () { + console.log('End focus') + is_focus = false; +} + +var check_interval = setInterval(function () { + console.log(is_focus) + if (is_focus) { + sendLog(); + } +}, interval_sec * 1000); \ No newline at end of file diff --git a/workshop/themes/learn/.editorconfig b/workshop/themes/learn/.editorconfig new file mode 100644 index 0000000..12f0ef5 --- /dev/null +++ b/workshop/themes/learn/.editorconfig @@ -0,0 +1,16 @@ +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true + +[*.js] +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/workshop/themes/learn/.gitignore b/workshop/themes/learn/.gitignore new file mode 100644 index 0000000..6429751 --- /dev/null +++ b/workshop/themes/learn/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +public/ +exampleSite/public diff --git a/workshop/themes/learn/.grenrc.yml b/workshop/themes/learn/.grenrc.yml new file mode 100644 index 0000000..ad31ac8 --- /dev/null +++ b/workshop/themes/learn/.grenrc.yml @@ -0,0 +1,25 @@ +--- + dataSource: "prs" + prefix: "v" + onlyMilestones: false + changelogFilename: "CHANGELOG.md" + includeMessages: "all" + ignoreIssuesWith: + - "support" + ignoreLabels: + - "duplicate" + - "invalid" + - "wontfix" + groupBy: + New features: + - "feature" + Bug Fixes: + - "bug" + Enhancements: + - "enhancement" + Internationalisation: + - "i18n" + Theme Meta: + - "meta" + Uncategorised: + - "closed" diff --git a/workshop/themes/learn/CHANGELOG.md b/workshop/themes/learn/CHANGELOG.md new file mode 100644 index 0000000..c4bc0ea --- /dev/null +++ b/workshop/themes/learn/CHANGELOG.md @@ -0,0 +1,154 @@ +# Changelog + +## v2.3.0 (16/04/2019) + +#### New features + +- [**feature**] Added support for tags [#196](https://github.com/matcornic/hugo-theme-learn/pull/196) + +#### Bug Fixes + +- [**bug**] Fix issue where "children" shortcode only shows top level. [#252](https://github.com/matcornic/hugo-theme-learn/pull/252) +- [**bug**] Fix translation when using a custom baseURL [#234](https://github.com/matcornic/hugo-theme-learn/pull/234) +- [**bug**] Preventing left/right arrow key navigation in textareas [#241](https://github.com/matcornic/hugo-theme-learn/pull/241) +- [**bug**] Update menu.html to include the check icon [#229](https://github.com/matcornic/hugo-theme-learn/pull/229) + +#### Enhancements + +- [**enhancement**] Various Updates [#237](https://github.com/matcornic/hugo-theme-learn/pull/237) +- [**enhancement**] Use style instead of width and height in logo [#250](https://github.com/matcornic/hugo-theme-learn/pull/250) +- [**enhancement**] [ImgBot] Optimize images [#222](https://github.com/matcornic/hugo-theme-learn/pull/222) +- [**enhancement**] Render the ```mermaid blocks into graphs [#226](https://github.com/matcornic/hugo-theme-learn/pull/226) +- [**enhancement**] remove duplicate icon and fix incorrect favicon type [#227](https://github.com/matcornic/hugo-theme-learn/pull/227) +- [**enhancement**] HTTPS links in exampleSite menu-footer [#223](https://github.com/matcornic/hugo-theme-learn/pull/223) + +#### Internationalisation + +- [**i18n**] Dutch i18n [#239](https://github.com/matcornic/hugo-theme-learn/pull/239) + +#### Theme Meta + +- [**meta**] Add .editorconfig [#224](https://github.com/matcornic/hugo-theme-learn/pull/224) + +--- + +## v2.2.0 (28/01/2019) + +#### New features + +- [**feature**] Sitewide param to enable or disable next/previous page buttons and breadcrumbs [#184](https://github.com/matcornic/hugo-theme-learn/pull/184) + +#### Bug Fixes + +- [**bug**] Fix baseurl used by search to load json data [#177](https://github.com/matcornic/hugo-theme-learn/pull/177) +- [**bug**] Updated CSS link to fontawesome library [#186](https://github.com/matcornic/hugo-theme-learn/pull/186) +- [**bug**] Close / Cancel search icon not showing in input box [#215](https://github.com/matcornic/hugo-theme-learn/pull/215) +- [**bug**] Prevent left and right keydown events while in input fields [#219](https://github.com/matcornic/hugo-theme-learn/pull/219) +- [**bug**] xss fix [#182](https://github.com/matcornic/hugo-theme-learn/pull/182) +- [**bug**] Fix error in blockquote documentation fixes #165 [#190](https://github.com/matcornic/hugo-theme-learn/pull/190) + +#### Enhancements + +- [**enhancement**] Update mermaid.js to a499296 [#199](https://github.com/matcornic/hugo-theme-learn/pull/199) +- [**enhancement**] Update Font Awesome to 5.0.6 [#129](https://github.com/matcornic/hugo-theme-learn/pull/129) +- [**enhancement**] Update 404.html alttext [#161](https://github.com/matcornic/hugo-theme-learn/pull/161) +- [**enhancement**] Remove CSS source map metadata [#167](https://github.com/matcornic/hugo-theme-learn/pull/167) +- [**enhancement**] Load github images in examplesite via https instead of http [#180](https://github.com/matcornic/hugo-theme-learn/pull/180) +- [**enhancement**] Load main site logo via BaseUrl [#185](https://github.com/matcornic/hugo-theme-learn/pull/185) +- [**enhancement**] HTTPS links in examplesite sidebar [#200](https://github.com/matcornic/hugo-theme-learn/pull/200) +- [**enhancement**] Use correct input type for search [#205](https://github.com/matcornic/hugo-theme-learn/pull/205) +- [**enhancement**] HTTPS link to learn.getgrav.org [#207](https://github.com/matcornic/hugo-theme-learn/pull/207) +- [**enhancement**] Update html5shiv-printshiv.min.js [#208](https://github.com/matcornic/hugo-theme-learn/pull/208) +- [**enhancement**] Remove whitespace from clippy.svg [#211](https://github.com/matcornic/hugo-theme-learn/pull/211) +- [**enhancement**] Upgrade fontawesome to 5.6.3 [#218](https://github.com/matcornic/hugo-theme-learn/pull/218) +- [**enhancement**] fix clickable nodes style in mermaid [#169](https://github.com/matcornic/hugo-theme-learn/pull/169) + +#### Internationalisation + +- [**i18n**] French language correction [#157](https://github.com/matcornic/hugo-theme-learn/pull/157) +- [**i18n**] French language correction [#158](https://github.com/matcornic/hugo-theme-learn/pull/158) +- [**i18n**] Add indonesian translation [#159](https://github.com/matcornic/hugo-theme-learn/pull/159) +- [**i18n**] Add Turkish i18n config file [#175](https://github.com/matcornic/hugo-theme-learn/pull/175) + +#### Theme Meta + +- [**meta**] Fix wercker builds [#178](https://github.com/matcornic/hugo-theme-learn/pull/178) +- [**meta**] Declare netlify buildsteps in repo file rather than in webui [#217](https://github.com/matcornic/hugo-theme-learn/pull/217) + +--- + +## v2.1.0 (10/08/2018) + +#### Internationalisation + +- [**i18n**] Clean up the English language phrasing [#146](https://github.com/matcornic/hugo-theme-learn/pull/146) +- [**i18n**] Updated _index.en.md for typo "names" [#150](https://github.com/matcornic/hugo-theme-learn/pull/150) + +#### Theme Meta + +- [**meta**] Add license scan report and status [#151](https://github.com/matcornic/hugo-theme-learn/pull/151) + +#### Uncategorised + +- [**closed**] Different viewport [#122](https://github.com/matcornic/hugo-theme-learn/pull/122) +- [**closed**] TranslationBaseName replaced for Name on archetypes template [#145](https://github.com/matcornic/hugo-theme-learn/pull/145) +- [**closed**] Improved variant of #119, as per request [#124](https://github.com/matcornic/hugo-theme-learn/pull/124) +- [**closed**] role="" is invalid [#121](https://github.com/matcornic/hugo-theme-learn/pull/121) +- [**closed**] Related to previous PR [#120](https://github.com/matcornic/hugo-theme-learn/pull/120) +- [**closed**] Issue #111 for _index.fr.md [#117](https://github.com/matcornic/hugo-theme-learn/pull/117) +- [**closed**] Issue #111 for _index.en.md [#116](https://github.com/matcornic/hugo-theme-learn/pull/116) +- [**closed**] Issue #111 for theme-green.css [#114](https://github.com/matcornic/hugo-theme-learn/pull/114) +- [**closed**] Issue #111 for theme-red.css [#113](https://github.com/matcornic/hugo-theme-learn/pull/113) +- [**closed**] Issue #111 for theme-blue.css [#112](https://github.com/matcornic/hugo-theme-learn/pull/112) +- [**closed**] Issue #111 for theme-mine.css [#115](https://github.com/matcornic/hugo-theme-learn/pull/115) +- [**closed**] fix #77 : no wrap images that already wrapped [#118](https://github.com/matcornic/hugo-theme-learn/pull/118) +- [**closed**] doc: Keep icons aligned [#110](https://github.com/matcornic/hugo-theme-learn/pull/110) +- [**closed**] Add Portuguese translation [#109](https://github.com/matcornic/hugo-theme-learn/pull/109) +- [**closed**] Use `relURL` where possible [#102](https://github.com/matcornic/hugo-theme-learn/pull/102) +- [**closed**] Bug fix in sidebar menu and children description generation [#105](https://github.com/matcornic/hugo-theme-learn/pull/105) +- [**closed**] fix some typo [#104](https://github.com/matcornic/hugo-theme-learn/pull/104) +- [**closed**] Added a `menuTitle` attribute to partials/menu.html [#90](https://github.com/matcornic/hugo-theme-learn/pull/90) +- [**closed**] allowing comments system [#86](https://github.com/matcornic/hugo-theme-learn/pull/86) +- [**closed**] Add spanish translation [#85](https://github.com/matcornic/hugo-theme-learn/pull/85) +- [**closed**] Replace horsey with Pixabay's autocomplete [#75](https://github.com/matcornic/hugo-theme-learn/pull/75) +- [**closed**] Added info about 'draft:true' [#74](https://github.com/matcornic/hugo-theme-learn/pull/74) +- [**closed**] Remove white space from `align` parameter [#63](https://github.com/matcornic/hugo-theme-learn/pull/63) + +--- + +## v2.0.0 (20/08/2017) + +#### Uncategorised + +- [**closed**] V2 [#56](https://github.com/matcornic/hugo-theme-learn/pull/56) + +--- + +## v1.1.0 (22/07/2017) + +#### Bug Fixes + +- [**bug**] Fix sticky header jumpiness [#45](https://github.com/matcornic/hugo-theme-learn/pull/45) + +#### Uncategorised + +- [**closed**] Fix anchor scrolling that hides behind top nav bar [#46](https://github.com/matcornic/hugo-theme-learn/pull/46) +- [**closed**] feat: Change the default code color [#43](https://github.com/matcornic/hugo-theme-learn/pull/43) +- [**closed**] Use index pages + automatic navigation arrows [#36](https://github.com/matcornic/hugo-theme-learn/pull/36) + +--- + +## v1.0.0 (25/03/2017) + +#### Uncategorised + +- [**closed**] Search [#18](https://github.com/matcornic/hugo-theme-learn/pull/18) +- [**closed**] Extracted menu footer content to separate partial file [#35](https://github.com/matcornic/hugo-theme-learn/pull/35) +- [**closed**] feat: style Hugo figure shortcode [#33](https://github.com/matcornic/hugo-theme-learn/pull/33) +- [**closed**] Fix URL for 'Edit this page' on Windows [#27](https://github.com/matcornic/hugo-theme-learn/pull/27) +- [**closed**] Revert "Menu generation using Content file instead directories" [#10](https://github.com/matcornic/hugo-theme-learn/pull/10) +- [**closed**] (#25) use .Site.BaseURL to locate static assets [#26](https://github.com/matcornic/hugo-theme-learn/pull/26) +- [**closed**] Menu ordering and definition in config.toml [#8](https://github.com/matcornic/hugo-theme-learn/pull/8) +- [**closed**] Menu generation using Content file instead directories [#5](https://github.com/matcornic/hugo-theme-learn/pull/5) +- [**closed**] Add Checkmark On visited Links [#4](https://github.com/matcornic/hugo-theme-learn/pull/4) +- [**closed**] doc: Add theme installation instructions [#1](https://github.com/matcornic/hugo-theme-learn/pull/1) diff --git a/workshop/themes/learn/LICENSE.md b/workshop/themes/learn/LICENSE.md new file mode 100644 index 0000000..0cf9fcb --- /dev/null +++ b/workshop/themes/learn/LICENSE.md @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Grav +Copyright (c) 2016 MATHIEU CORNIC +Copyright (c) 2017 Valere JEANTET + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/workshop/themes/learn/README.md b/workshop/themes/learn/README.md new file mode 100644 index 0000000..b0f2cca --- /dev/null +++ b/workshop/themes/learn/README.md @@ -0,0 +1,60 @@ +# Hugo Learn Theme + +This repository contains a theme for [Hugo](https://gohugo.io/), based on great [Grav Learn Theme](https://learn.getgrav.org/). + +Visit the [theme documentation](https://learn.netlify.com/en/) to see what is going on. It is actually built with this theme. + +[![wercker status](https://app.wercker.com/status/233466a2be73fcea400e7dc02ef6adf9/s/master "wercker status")](https://app.wercker.com/project/byKey/233466a2be73fcea400e7dc02ef6adf9) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmatcornic%2Fhugo-theme-learn.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmatcornic%2Fhugo-theme-learn?ref=badge_shield) + +## Main features + +- Automatic Search +- Multilingual mode +- Unlimited menu levels +- Automatic next/prev buttons to navigate through menu entries +- Image resizing, shadow… +- Attachments files +- List child pages +- Mermaid diagram (flowchart, sequence, gantt) +- Customizable look and feel and themes variants +- Buttons, Tip/Note/Info/Warning boxes, Expand + +## Installation + +Navigate to your themes folder in your Hugo site and use the following commands: + +``` +$ cd themes +$ git clone https://github.com/matcornic/hugo-theme-learn.git +``` + +Check that your Hugo version is minimum `0.25` with `hugo version`. + +![Overview](https://github.com/matcornic/hugo-theme-learn/raw/master/images/tn.png) + +## Usage + +- [Visit the documentation](https://learn.netlify.com/en/) + +## Download old versions (prior to 2.0.0) + +If you need old version for compatibility purpose, either download [theme source code from releases](https://github.com/matcornic/hugo-theme-learn/releases) or use the right git tag. For example, with `1.1.0` + +- Direct download way: https://github.com/matcornic/hugo-theme-learn/archive/1.1.0.zip +- Git way: + +```shell +cd themes/hugo-theme-learn +git checkout tags/1.1.0 +``` + +For both solutions, the documentation is available at https://github.com/matcornic/hugo-theme-learn/releases/download/1.1.0/hugo-learn-doc-1.1.0.zip + +## Credits + +Many thanks to [@vjeantet](https://github.com/vjeantet/) for the fork [docdock](https://github.com/vjeantet/hugo-theme-docdock). The v2 of this theme is mainly based on his work ! + + +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmatcornic%2Fhugo-theme-learn.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmatcornic%2Fhugo-theme-learn?ref=badge_large) diff --git a/workshop/themes/learn/archetypes/chapter.md b/workshop/themes/learn/archetypes/chapter.md new file mode 100644 index 0000000..1782b7c --- /dev/null +++ b/workshop/themes/learn/archetypes/chapter.md @@ -0,0 +1,9 @@ ++++ +title = "{{ replace .Name "-" " " | title }}" +date = {{ .Date }} +weight = 5 +chapter = true +pre = "X. " ++++ + +Lorem Ipsum. \ No newline at end of file diff --git a/workshop/themes/learn/archetypes/default.md b/workshop/themes/learn/archetypes/default.md new file mode 100644 index 0000000..0e4f3e1 --- /dev/null +++ b/workshop/themes/learn/archetypes/default.md @@ -0,0 +1,7 @@ ++++ +title = "{{ replace .Name "-" " " | title }}" +date = {{ .Date }} +weight = 5 ++++ + +Lorem Ipsum. \ No newline at end of file diff --git a/workshop/themes/learn/i18n/ar.toml b/workshop/themes/learn/i18n/ar.toml new file mode 100644 index 0000000..66e90e1 --- /dev/null +++ b/workshop/themes/learn/i18n/ar.toml @@ -0,0 +1,26 @@ +[Search-placeholder] +other = "...البحث" + +[Clear-History] +other = "مسح السجل" + +[Attachments-label] +other = "مرفقات" + +[title-404] +other = "خطأ" + +[message-404] +other = ".¯\\_(ツ)_/¯أوبس. يبدو أن هذه الصفحة غير موجودة" + +[Go-to-homepage] +other = "الذهاب إلى الصفحة الرئيسية" + +[Edit-this-page] +other = "تعديل هذه الصفحة" + +[Shortcuts-Title] +other = "المزيد" + +[Expand-title] +other = "...قم بتوسيع" \ No newline at end of file diff --git a/workshop/themes/learn/i18n/de.toml b/workshop/themes/learn/i18n/de.toml new file mode 100644 index 0000000..55ced7a --- /dev/null +++ b/workshop/themes/learn/i18n/de.toml @@ -0,0 +1,26 @@ +[Search-placeholder] +other = "Suchen..." + +[Clear-History] +other = "Verlauf löschen" + +[Attachments-label] +other = "Anhänge" + +[title-404] +other = "Fehler" + +[message-404] +other = "Huch. Diese Seite scheint nicht zu existieren ¯\\_(ツ)_/¯." + +[Go-to-homepage] +other = "Gehe zur Homepage" + +[Edit-this-page] +other = "Bearbeite diese Seite" + +[Shortcuts-Title] +other = "Mehr" + +[Expand-title] +other = "Erweitere mich..." \ No newline at end of file diff --git a/workshop/themes/learn/i18n/en.toml b/workshop/themes/learn/i18n/en.toml new file mode 100644 index 0000000..118345f --- /dev/null +++ b/workshop/themes/learn/i18n/en.toml @@ -0,0 +1,26 @@ +[Search-placeholder] +other = "Search..." + +[Clear-History] +other = "Clear History" + +[Attachments-label] +other = "Attachments" + +[title-404] +other = "Error" + +[message-404] +other = "Woops. Looks like this page doesn't exist ¯\\_(ツ)_/¯." + +[Go-to-homepage] +other = "Go to homepage" + +[Edit-this-page] +other = "Edit this page" + +[Shortcuts-Title] +other = "More" + +[Expand-title] +other = "Expand me..." \ No newline at end of file diff --git a/workshop/themes/learn/i18n/es.toml b/workshop/themes/learn/i18n/es.toml new file mode 100644 index 0000000..57b7a33 --- /dev/null +++ b/workshop/themes/learn/i18n/es.toml @@ -0,0 +1,26 @@ +[Search-placeholder] +other = "Buscar..." + +[Clear-History] +other = "Borrar Historial" + +[Attachments-label] +other = "Adjuntos" + +[title-404] +other = "Error" + +[message-404] +other = "Ups. Parece que la página no existe ¯\\_(ツ)_/¯." + +[Go-to-homepage] +other = "Ir al inicio" + +[Edit-this-page] +other = "Editar esta página" + +[Shortcuts-Title] +other = "Más" + +[Expand-title] +other = "Expandir..." diff --git a/workshop/themes/learn/i18n/fr.toml b/workshop/themes/learn/i18n/fr.toml new file mode 100644 index 0000000..106f20d --- /dev/null +++ b/workshop/themes/learn/i18n/fr.toml @@ -0,0 +1,26 @@ +[Search-placeholder] +other = "Rechercher..." + +[Clear-History] +other = "Supprimer l'historique" + +[Attachments-label] +other = "Pièces jointes" + +[title-404] +other = "Erreur" + +[message-404] +other = "Oups. On dirait que cette page n'existe pas ¯\\_(ツ)_/¯" + +[Go-to-homepage] +other = "Vers la page d'accueil" + +[Edit-this-page] +other = "Modifier la page" + +[Shortcuts-Title] +other = "Aller plus loin" + +[Expand-title] +other = "Déroulez-moi..." \ No newline at end of file diff --git a/workshop/themes/learn/i18n/hi.toml b/workshop/themes/learn/i18n/hi.toml new file mode 100644 index 0000000..b0f2515 --- /dev/null +++ b/workshop/themes/learn/i18n/hi.toml @@ -0,0 +1,26 @@ +[Search-placeholder] +other = "खोजे..." + +[Clear-History] +other = "इतिहास मिटाएँ" + +[Attachments-label] +other = "संलग्नंक (अटैचमेंट)" + +[title-404] +other = "त्रुटि" + +[message-404] +other = "यह पृष्ठ अभि अनुपलब्ध है!" + +[Go-to-homepage] +other = "मुख्य पृष्ठ पर जाऐ" + +[Edit-this-page] +other = "यह पृष्ठ संपादित करें" + +[Shortcuts-Title] +other = "अधिक सामग्री दिखाएं" + +[Expand-title] +other = "विस्तार करे..." diff --git a/workshop/themes/learn/i18n/id.toml b/workshop/themes/learn/i18n/id.toml new file mode 100644 index 0000000..e0aa5ae --- /dev/null +++ b/workshop/themes/learn/i18n/id.toml @@ -0,0 +1,26 @@ +[Search-placeholder] +other = "Telusuri..." + +[Clear-History] +other = "Bersihkan Riwayat" + +[Attachments-label] +other = "Lampiran" + +[title-404] +other = "Kesalahan" + +[message-404] +other = "Oops. Sepertinya halaman ini tidak ada ¯\\_(ツ)_/¯." + +[Go-to-homepage] +other = "Ke halaman depan" + +[Edit-this-page] +other = "Edit halaman ini" + +[Shortcuts-Title] +other = "Lainnya" + +[Expand-title] +other = "Bentangkan..." diff --git a/workshop/themes/learn/i18n/nl.toml b/workshop/themes/learn/i18n/nl.toml new file mode 100644 index 0000000..cca84b4 --- /dev/null +++ b/workshop/themes/learn/i18n/nl.toml @@ -0,0 +1,26 @@ +[Search-placeholder] +other = "Zoeken..." + +[Clear-History] +other = "Wis geschiedenis" + +[Attachments-label] +other = "Bijlagen" + +[title-404] +other = "Error" + +[message-404] +other = "Blijkbaar bestaat deze pagina niet ¯\\_(ツ)_/¯." + +[Go-to-homepage] +other = "Naar startpagina" + +[Edit-this-page] +other = "Deze pagina bewerken" + +[Shortcuts-Title] +other = "Snelkoppelingen" + +[Expand-title] +other = "Lees meer..." diff --git a/workshop/themes/learn/i18n/pt.toml b/workshop/themes/learn/i18n/pt.toml new file mode 100644 index 0000000..b110533 --- /dev/null +++ b/workshop/themes/learn/i18n/pt.toml @@ -0,0 +1,26 @@ +[Search-placeholder] +other = "Procurar..." + +[Clear-History] +other = "Limpar Histórico" + +[Attachments-label] +other = "Anexos" + +[title-404] +other = "Erro" + +[message-404] +other = "Ops. Parece que a página não existe ¯\\_(ツ)_/¯." + +[Go-to-homepage] +other = "Ir para o início" + +[Edit-this-page] +other = "Editar esta página" + +[Shortcuts-Title] +other = "Mais" + +[Expand-title] +other = "Expandir..." diff --git a/workshop/themes/learn/i18n/tr.toml b/workshop/themes/learn/i18n/tr.toml new file mode 100644 index 0000000..17249ed --- /dev/null +++ b/workshop/themes/learn/i18n/tr.toml @@ -0,0 +1,26 @@ +[Search-placeholder] +other = "Ara..." + +[Clear-History] +other = "Geçmişi Temizle" + +[Attachments-label] +other = "Ekler" + +[title-404] +other = "Hata" + +[message-404] +other = "Uups. Görünüşe göre böyle bir sayfa yok ¯\\_(ツ)_/¯" + +[Go-to-homepage] +other = "Anasayfaya dön" + +[Edit-this-page] +other = "Sayfayı düzenle" + +[Shortcuts-Title] +other = "Dahası Var" + +[Expand-title] +other = "Genişlet..." diff --git a/workshop/themes/learn/images/screenshot.png b/workshop/themes/learn/images/screenshot.png new file mode 100644 index 0000000..85966a2 Binary files /dev/null and b/workshop/themes/learn/images/screenshot.png differ diff --git a/workshop/themes/learn/images/tn.png b/workshop/themes/learn/images/tn.png new file mode 100644 index 0000000..b1d64bd Binary files /dev/null and b/workshop/themes/learn/images/tn.png differ diff --git a/workshop/themes/learn/layouts/404.html b/workshop/themes/learn/layouts/404.html new file mode 100644 index 0000000..ebfade5 --- /dev/null +++ b/workshop/themes/learn/layouts/404.html @@ -0,0 +1,54 @@ + + + + + {{ partial "meta.html" . }} {{ partial "favicon.html" . }} {{ .Scratch.Add "title" "" }}{{ if eq .Site.Data.titles .Title }}{{ .Scratch.Set "title" (index .Site.Data.titles .Title).title }}{{ else }}{{ .Scratch.Set "title" .Title}}{{end}} + {{ .Scratch.Get "title" }} + + {{ $assetBusting := not .Site.Params.disableAssetsBusting }} + + + + + + + + {{with .Site.Params.themeVariant}} + + {{end}} + +{{ partial "custom-header.html" . }} + + + + +
+
+
+
+

{{T "title-404"}}

+

Page not found!

+

{{T "message-404"}}

+

+

{{T "Go-to-homepage"}}

+
+
+ +
+ + + diff --git a/workshop/themes/learn/layouts/_default/list.html b/workshop/themes/learn/layouts/_default/list.html new file mode 100644 index 0000000..b56e8d4 --- /dev/null +++ b/workshop/themes/learn/layouts/_default/list.html @@ -0,0 +1,22 @@ +{{ partial "header.html" . }} + +{{ if eq .Kind "section" }} + {{ .Content }} +{{end}} + +{{ if or (eq .Kind "taxonomy") (eq .Kind "taxonomyTerm") }} + +{{end}} + +
+ {{with .Params.LastModifierDisplayName}} + {{ . }} {{with $.Date}} {{ .Format "02/01/2006" }}{{end}} + + {{end}} +
+ +{{ partial "footer.html" . }} \ No newline at end of file diff --git a/workshop/themes/learn/layouts/_default/single.html b/workshop/themes/learn/layouts/_default/single.html new file mode 100644 index 0000000..ab1424f --- /dev/null +++ b/workshop/themes/learn/layouts/_default/single.html @@ -0,0 +1,13 @@ +{{ partial "header.html" . }} + +{{ .Content }} + +
+ {{with .Params.LastModifierDisplayName}} + {{ . }} {{with $.Date}} {{ .Format "02/01/2006" }}{{end}} + + {{end}} +
+ + +{{ partial "footer.html" . }} diff --git a/workshop/themes/learn/layouts/index.html b/workshop/themes/learn/layouts/index.html new file mode 100644 index 0000000..5780360 --- /dev/null +++ b/workshop/themes/learn/layouts/index.html @@ -0,0 +1,8 @@ +{{ partial "header.html" . }} + + navigation + + +{{.Site.Home.Content}} + +{{ partial "footer.html" . }} diff --git a/workshop/themes/learn/layouts/index.json b/workshop/themes/learn/layouts/index.json new file mode 100644 index 0000000..367f3b3 --- /dev/null +++ b/workshop/themes/learn/layouts/index.json @@ -0,0 +1,12 @@ +[{{ range $index, $page := .Site.Pages }} +{{- if ne $page.Type "json" -}} +{{- if and $index (gt $index 0) -}},{{- end }} +{ + "uri": "{{ $page.Permalink }}", + "title": "{{ htmlEscape $page.Title}}", + "tags": [{{ range $tindex, $tag := $page.Params.tags }}{{ if $tindex }}, {{ end }}"{{ $tag| htmlEscape }}"{{ end }}], + "description": "{{ htmlEscape .Description}}", + "content": {{$page.Plain | jsonify}} +} +{{- end -}} +{{- end -}}] \ No newline at end of file diff --git a/workshop/themes/learn/layouts/partials/custom-comments.html b/workshop/themes/learn/layouts/partials/custom-comments.html new file mode 100644 index 0000000..6f5a034 --- /dev/null +++ b/workshop/themes/learn/layouts/partials/custom-comments.html @@ -0,0 +1,3 @@ + diff --git a/workshop/themes/learn/layouts/partials/custom-footer.html b/workshop/themes/learn/layouts/partials/custom-footer.html new file mode 100644 index 0000000..e69de29 diff --git a/workshop/themes/learn/layouts/partials/custom-header.html b/workshop/themes/learn/layouts/partials/custom-header.html new file mode 100644 index 0000000..294af29 --- /dev/null +++ b/workshop/themes/learn/layouts/partials/custom-header.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/workshop/themes/learn/layouts/partials/favicon.html b/workshop/themes/learn/layouts/partials/favicon.html new file mode 100644 index 0000000..741b118 --- /dev/null +++ b/workshop/themes/learn/layouts/partials/favicon.html @@ -0,0 +1,2 @@ + + diff --git a/workshop/themes/learn/layouts/partials/footer.html b/workshop/themes/learn/layouts/partials/footer.html new file mode 100644 index 0000000..91a1c99 --- /dev/null +++ b/workshop/themes/learn/layouts/partials/footer.html @@ -0,0 +1,78 @@ + {{ if .Params.chapter }} + + {{ end }} + + {{ partial "custom-comments.html" . }} + + + + + + +
+
+
+ + + + + + + + + + + + + + + + {{ partial "custom-footer.html" . }} + + diff --git a/workshop/themes/learn/layouts/partials/header.html b/workshop/themes/learn/layouts/partials/header.html new file mode 100644 index 0000000..d7ecb27 --- /dev/null +++ b/workshop/themes/learn/layouts/partials/header.html @@ -0,0 +1,100 @@ + + + + + + + + + + {{ .Hugo.Generator }} + {{ partial "meta.html" . }} + {{ partial "favicon.html" . }} + {{ .Title }} :: {{ .Site.Title }} + + {{ $assetBusting := not .Site.Params.disableAssetsBusting }} + + + + + + + + + + {{with .Site.Params.themeVariant}} + + {{end}} + + + + + {{ partial "custom-header.html" . }} + + + + {{ partial "menu.html" . }} +
+
+
+ {{if not .IsHome}} +
+
+ {{ if and (or .IsPage .IsSection) .Site.Params.editURL }} + {{ $File := .File }} + {{ $Site := .Site }} + {{with $File.Path }} + + {{ end }} + {{ end }} + {{$toc := (and (not .Params.disableToc) (not .Params.chapter))}} + + {{ if $toc }} + {{ partial "toc.html" . }} + {{ end }} +
+
+ {{ end }} + + {{ if .Params.chapter }} +
+ {{ end }} +
+

{{.Title}}

+ + {{define "breadcrumb"}} + {{$parent := .page.Parent }} + {{ if $parent }} + {{ $value := (printf "%s > %s" $parent.Permalink $parent.Title .value) }} + {{ template "breadcrumb" dict "page" $parent "value" $value }} + {{else}} + {{.value|safeHTML}} + {{end}} + {{end}} diff --git a/workshop/themes/learn/layouts/partials/logo.html b/workshop/themes/learn/layouts/partials/logo.html new file mode 100644 index 0000000..9633f9c --- /dev/null +++ b/workshop/themes/learn/layouts/partials/logo.html @@ -0,0 +1,5 @@ + diff --git a/workshop/themes/learn/layouts/partials/menu-footer.html b/workshop/themes/learn/layouts/partials/menu-footer.html new file mode 100644 index 0000000..ede5a7a --- /dev/null +++ b/workshop/themes/learn/layouts/partials/menu-footer.html @@ -0,0 +1,5 @@ + + + Privacy | Site Terms | © 2020, Amazon Web Services, Inc. or its affiliates. All rights reserved. + + diff --git a/workshop/themes/learn/layouts/partials/menu.html b/workshop/themes/learn/layouts/partials/menu.html new file mode 100644 index 0000000..b9ef231 --- /dev/null +++ b/workshop/themes/learn/layouts/partials/menu.html @@ -0,0 +1,150 @@ + + + +{{ define "section-tree-nav" }} +{{ $showvisitedlinks := .showvisitedlinks }} +{{ $currentNode := .currentnode }} + {{with .sect}} + {{if .IsSection}} + {{safeHTML .Params.head}} +
  • + + {{safeHTML .Params.Pre}}{{or .Params.menuTitle .LinkTitle .Title}}{{safeHTML .Params.Post}} + {{ if $showvisitedlinks}} + + {{ end }} + + {{ $numberOfPages := (add (len .Pages) (len .Sections)) }} + {{ if ne $numberOfPages 0 }} +
      + {{ $currentNode.Scratch.Set "pages" .Pages }} + {{ if .Sections}} + {{ $currentNode.Scratch.Set "pages" (.Pages | union .Sections) }} + {{end}} + {{ $pages := ($currentNode.Scratch.Get "pages") }} + + {{if eq .Site.Params.ordersectionsby "title"}} + {{ range $pages.ByTitle }} + {{ if and .Params.hidden (not $.showhidden) }} + {{else}} + {{ template "section-tree-nav" dict "sect" . "currentnode" $currentNode "showvisitedlinks" $showvisitedlinks }} + {{end}} + {{ end }} + {{else}} + {{ range $pages.ByWeight }} + {{ if and .Params.hidden (not $.showhidden) }} + {{else}} + {{ template "section-tree-nav" dict "sect" . "currentnode" $currentNode "showvisitedlinks" $showvisitedlinks }} + {{end}} + {{ end }} + {{end}} +
    + {{ end }} +
  • + {{else}} + {{ if not .Params.Hidden }} +
  • + + {{safeHTML .Params.Pre}}{{or .Params.menuTitle .LinkTitle .Title}}{{safeHTML .Params.Post}} + {{ if $showvisitedlinks}}{{end}} + +
  • + {{ end }} + {{end}} + {{ end }} +{{ end }} + diff --git a/workshop/themes/learn/layouts/partials/meta.html b/workshop/themes/learn/layouts/partials/meta.html new file mode 100644 index 0000000..71d9634 --- /dev/null +++ b/workshop/themes/learn/layouts/partials/meta.html @@ -0,0 +1,2 @@ + +{{ with .Site.Params.author }}{{ end }} diff --git a/workshop/themes/learn/layouts/partials/search.html b/workshop/themes/learn/layouts/partials/search.html new file mode 100644 index 0000000..41daf84 --- /dev/null +++ b/workshop/themes/learn/layouts/partials/search.html @@ -0,0 +1,16 @@ + +{{ $assetBusting := not .Site.Params.disableAssetsBusting }} + + + + diff --git a/workshop/themes/learn/layouts/partials/tags.html b/workshop/themes/learn/layouts/partials/tags.html new file mode 100644 index 0000000..48790d8 --- /dev/null +++ b/workshop/themes/learn/layouts/partials/tags.html @@ -0,0 +1,7 @@ +{{ if .Params.tags }} +
    +{{range .Params.tags}} + {{ . }} +{{end}} +
    +{{end}} \ No newline at end of file diff --git a/workshop/themes/learn/layouts/partials/toc.html b/workshop/themes/learn/layouts/partials/toc.html new file mode 100644 index 0000000..6b69051 --- /dev/null +++ b/workshop/themes/learn/layouts/partials/toc.html @@ -0,0 +1,5 @@ +
    +
    +{{ .TableOfContents }} +
    +
    diff --git a/workshop/themes/learn/layouts/shortcodes/attachments.html b/workshop/themes/learn/layouts/shortcodes/attachments.html new file mode 100644 index 0000000..be4d6f9 --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/attachments.html @@ -0,0 +1,36 @@ +{{ $_hugo_config := `{ "version": 1 }` }} +
    + + {{if eq .Page.File.BaseFileName "index"}} + {{$.Scratch.Add "filesName" "files"}} + {{else}} + {{$.Scratch.Add "filesName" (printf "%s.files" .Page.File.BaseFileName)}} + {{end}} +
    + {{ range (readDir (printf "./content/%s%s" .Page.File.Dir ($.Scratch.Get "filesName")) ) }} + {{ $fileDir := replace $.Page.File.Dir "\\" "/" }} + {{if ($.Get "pattern")}} + {{if (findRE ($.Get "pattern") .Name)}} +
  • + + {{.Name}} + + ({{div .Size 1024 }} kb) +
  • + {{end}} + {{else}} +
  • + + {{.Name}} + + ({{div .Size 1024 }} kb) +
  • + {{end}} + {{end}} +
    + {{.Inner}} +
    + diff --git a/workshop/themes/learn/layouts/shortcodes/button.html b/workshop/themes/learn/layouts/shortcodes/button.html new file mode 100644 index 0000000..7cfc389 --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/button.html @@ -0,0 +1,14 @@ +{{ $_hugo_config := `{ "version": 1 }` }} + + {{ $icon := .Get "icon" }} + {{ $iconposition := .Get "icon-position" }} + {{ if ($icon) }} + {{ if or (not ($iconposition)) (eq $iconposition "left") }} + + {{ end }} + {{ end }} + {{ .Inner }} + {{ if and ($icon) (eq $iconposition "right")}} + + {{ end }} + diff --git a/workshop/themes/learn/layouts/shortcodes/children.html b/workshop/themes/learn/layouts/shortcodes/children.html new file mode 100644 index 0000000..6432791 --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/children.html @@ -0,0 +1,101 @@ +{{ $_hugo_config := `{ "version": 1 }` }} +{{ $showhidden := .Get "showhidden"}} +{{ $style := .Get "style" | default "li" }} +{{ $depth := .Get "depth" | default 1 }} +{{ $withDescription := .Get "description" | default false }} +{{ $sortTerm := .Get "sort" | default "Weight" }} + +
      + {{ .Scratch.Set "pages" .Page.Pages }} + + {{if .Page.IsHome}} + + {{ $rootPage := where .Page.Pages "Dir" "" }} + {{ .Scratch.Set "pages" (.Page.Sections | union $rootPage)}} + {{else}} + {{ if .Page.Sections}} + {{ .Scratch.Set "pages" (.Page.Pages | union .Page.Sections) }} + {{end}} + {{end}} + + {{ $pages := (.Scratch.Get "pages") }} + + {{if eq $sortTerm "Weight"}} + {{template "childs" dict "menu" $pages.ByWeight "style" $style "showhidden" $showhidden "count" 1 "depth" $depth "pages" .Site.Pages "description" $withDescription "sortTerm" $sortTerm}} + {{else if eq $sortTerm "Name"}} + {{template "childs" dict "menu" $pages.ByTitle "style" $style "showhidden" $showhidden "count" 1 "depth" $depth "pages" .Site.Pages "description" $withDescription "sortTerm" $sortTerm}} + {{else if eq $sortTerm "PublishDate"}} + {{template "childs" dict "menu" $pages.ByPublishDate "style" $style "showhidden" $showhidden "count" 1 "depth" $depth "pages" .Site.Pages "description" $withDescription "sortTerm" $sortTerm}} + {{else if eq $sortTerm "Date"}} + {{template "childs" dict "menu" $pages.ByDate "style" $style "showhidden" $showhidden "count" 1 "depth" $depth "pages" .Site.Pages "description" $withDescription "sortTerm" $sortTerm}} + {{else if eq $sortTerm "Length"}} + {{template "childs" dict "menu" $pages.ByLength "style" $style "showhidden" $showhidden "count" 1 "depth" $depth "pages" .Site.Pages "description" $withDescription "sortTerm" $sortTerm}} + {{else}} + {{template "childs" dict "menu" $pages "style" $style "showhidden" $showhidden "count" 1 "depth" $depth "pages" .Site.Pages "description" $withDescription "sortTerm" $sortTerm}} + {{end}} +
    + +{{.Inner|safeHTML}} + +{{ define "childs" }} + {{ range .menu }} + {{ if and .Params.hidden (not $.showhidden) }} + {{else}} + {{if not .IsHome}} + {{if hasPrefix $.style "h"}} + {{$num := sub ( int (trim $.style "h") ) 1 }} + {{$numn := add $num $.count }} + +{{(printf "" $numn)|safeHTML}} +{{ .Title }} +{{(printf "" $numn)|safeHTML}} + + {{else}} +{{(printf "<%s>" $.style)|safeHTML}} +{{ .Title }} +{{(printf "" $.style)|safeHTML}} + {{end}} + + {{if $.description}} + {{if .Description}} +

    {{.Description}}

    + {{else}} +

    {{.Summary}}

    + {{end}} + {{end}} + {{end}} + {{ if lt $.count $.depth}} + + {{if eq $.style "li"}} +
      + {{end}} + + {{ if .Sections}} + {{ .Scratch.Set "pages" (.Pages | union .Sections) }} + {{else}} + {{ .Scratch.Set "pages" .Pages }} + {{end}} + + {{ $pages := (.Scratch.Get "pages") }} + + {{if eq $.sortTerm "Weight"}} + {{template "childs" dict "menu" $pages.ByWeight "style" $.style "showhidden" $.showhidden "count" (add $.count 1) "depth" $.depth "pages" $.pages "description" $.description "sortTerm" $.sortTerm}} + {{else if eq $.sortTerm "Name"}} + {{template "childs" dict "menu" $pages.ByTitle "style" $.style "showhidden" $.showhidden "count" (add $.count 1) "depth" $.depth "pages" $.pages "description" $.description "sortTerm" $.sortTerm}} + {{else if eq $.sortTerm "PublishDate"}} + {{template "childs" dict "menu" $pages.ByPublishDate "style" $.style "showhidden" $.showhidden "count" (add $.count 1) "depth" $.depth "pages" $.pages "description" $.description "sortTerm" $.sortTerm}} + {{else if eq $.sortTerm "Date"}} + {{template "childs" dict "menu" $pages.ByDate "style" $.style "showhidden" $.showhidden "count" (add $.count 1) "depth" $.depth "pages" $.pages "description" $.description "sortTerm" $.sortTerm}} + {{else if eq $.sortTerm "Length"}} + {{template "childs" dict "menu" $pages.ByLength "style" $.style "showhidden" $.showhidden "count" (add $.count 1) "depth" $.depth "pages" $.pages "description" $.description "sortTerm" $.sortTerm}} + {{else}} + {{template "childs" dict "menu" $pages "style" $.style "showhidden" $.showhidden "count" (add $.count 1) "depth" $.depth "pages" $.pages "description" $.description "sortTerm" $.sortTerm}} + {{end}} + + {{if eq $.style "li"}} +
    + {{end}} + {{end}} + {{end}} + {{end}} +{{end}} \ No newline at end of file diff --git a/workshop/themes/learn/layouts/shortcodes/expand.html b/workshop/themes/learn/layouts/shortcodes/expand.html new file mode 100644 index 0000000..0e7ddb1 --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/expand.html @@ -0,0 +1,17 @@ +{{ $_hugo_config := `{ "version": 1 }` }} +
    +
    + + + {{$expandMessage := T "Expand-title"}} + {{ if .IsNamedParams }} + {{.Get "default" | default $expandMessage}} + {{else}} + {{.Get 0 | default $expandMessage}} + {{end}} + +
    + +
    \ No newline at end of file diff --git a/workshop/themes/learn/layouts/shortcodes/img.html b/workshop/themes/learn/layouts/shortcodes/img.html new file mode 100644 index 0000000..a7e1397 --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/img.html @@ -0,0 +1,4 @@ +{{ $img := $.Page.Resources.GetMatch (.Get 0)}} +
    + {{(.Get 1)}} +
    \ No newline at end of file diff --git a/workshop/themes/learn/layouts/shortcodes/mermaid.html b/workshop/themes/learn/layouts/shortcodes/mermaid.html new file mode 100644 index 0000000..287bc38 --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/mermaid.html @@ -0,0 +1,2 @@ +{{ $_hugo_config := `{ "version": 1 }` }} +
    {{ safeHTML .Inner }}
    diff --git a/workshop/themes/learn/layouts/shortcodes/notice.html b/workshop/themes/learn/layouts/shortcodes/notice.html new file mode 100644 index 0000000..0e33247 --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/notice.html @@ -0,0 +1,2 @@ +{{ $_hugo_config := `{ "version": 1 }` }} +
    {{ .Inner }}
    diff --git a/workshop/themes/learn/layouts/shortcodes/ref.html b/workshop/themes/learn/layouts/shortcodes/ref.html new file mode 100644 index 0000000..7b7eb42 --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/ref.html @@ -0,0 +1,14 @@ +{{- if in (.Get 0) "/_index.md" -}} + {{- $paths := (split (.Get 0) "_index.md") -}} + {{- $pagepath := index $paths 0 -}} + {{- $anchor := index $paths 1 -}} + {{- with .Site.GetPage "section" (trim $pagepath "/") -}} + {{- ( printf "%s%s" $pagepath $anchor ) | relLangURL -}} + {{- end -}} +{{- else -}} + {{- with .Site.GetPage "section" (.Get 0) }} + {{- .RelPermalink -}} + {{- else -}} + {{- .Get 0 | relref .Page -}} + {{- end -}} +{{- end -}} \ No newline at end of file diff --git a/workshop/themes/learn/layouts/shortcodes/relref.html b/workshop/themes/learn/layouts/shortcodes/relref.html new file mode 100644 index 0000000..7b7eb42 --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/relref.html @@ -0,0 +1,14 @@ +{{- if in (.Get 0) "/_index.md" -}} + {{- $paths := (split (.Get 0) "_index.md") -}} + {{- $pagepath := index $paths 0 -}} + {{- $anchor := index $paths 1 -}} + {{- with .Site.GetPage "section" (trim $pagepath "/") -}} + {{- ( printf "%s%s" $pagepath $anchor ) | relLangURL -}} + {{- end -}} +{{- else -}} + {{- with .Site.GetPage "section" (.Get 0) }} + {{- .RelPermalink -}} + {{- else -}} + {{- .Get 0 | relref .Page -}} + {{- end -}} +{{- end -}} \ No newline at end of file diff --git a/workshop/themes/learn/layouts/shortcodes/siteparam.html b/workshop/themes/learn/layouts/shortcodes/siteparam.html new file mode 100644 index 0000000..20cd2ca --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/siteparam.html @@ -0,0 +1,7 @@ +{{- $paramName := (.Get 0) -}} +{{- $siteParams := .Site.Params -}} +{{- with $paramName -}} + {{- with $siteParams -}} + {{- index . (lower $paramName) -}} + {{- end -}} +{{- end -}} \ No newline at end of file diff --git a/workshop/themes/learn/layouts/shortcodes/siteurl.html b/workshop/themes/learn/layouts/shortcodes/siteurl.html new file mode 100644 index 0000000..af2adbe --- /dev/null +++ b/workshop/themes/learn/layouts/shortcodes/siteurl.html @@ -0,0 +1 @@ +{{ .Page.Site.BaseURL }} \ No newline at end of file diff --git a/workshop/themes/learn/static/css/atom-one-dark-reasonable.css b/workshop/themes/learn/static/css/atom-one-dark-reasonable.css new file mode 100644 index 0000000..fd41c99 --- /dev/null +++ b/workshop/themes/learn/static/css/atom-one-dark-reasonable.css @@ -0,0 +1,77 @@ +/* + +Atom One Dark With support for ReasonML by Gidi Morris, based off work by Daniel Gamage + +Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax + +*/ +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + line-height: 1.3em; + color: #abb2bf; + background: #282c34; + border-radius: 5px; +} +.hljs-keyword, .hljs-operator { + color: #F92672; +} +.hljs-pattern-match { + color: #F92672; +} +.hljs-pattern-match .hljs-constructor { + color: #61aeee; +} +.hljs-function { + color: #61aeee; +} +.hljs-function .hljs-params { + color: #A6E22E; +} +.hljs-function .hljs-params .hljs-typing { + color: #FD971F; +} +.hljs-module-access .hljs-module { + color: #7e57c2; +} +.hljs-constructor { + color: #e2b93d; +} +.hljs-constructor .hljs-string { + color: #9CCC65; +} +.hljs-comment, .hljs-quote { + color: #b18eb1; + font-style: italic; +} +.hljs-doctag, .hljs-formula { + color: #c678dd; +} +.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst { + color: #e06c75; +} +.hljs-literal { + color: #56b6c2; +} +.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string { + color: #98c379; +} +.hljs-built_in, .hljs-class .hljs-title { + color: #e6c07b; +} +.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number { + color: #d19a66; +} +.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title { + color: #61aeee; +} +.hljs-emphasis { + font-style: italic; +} +.hljs-strong { + font-weight: bold; +} +.hljs-link { + text-decoration: underline; +} diff --git a/workshop/themes/learn/static/css/auto-complete.css b/workshop/themes/learn/static/css/auto-complete.css new file mode 100644 index 0000000..ac6979a --- /dev/null +++ b/workshop/themes/learn/static/css/auto-complete.css @@ -0,0 +1,47 @@ +.autocomplete-suggestions { + text-align: left; + cursor: default; + border: 1px solid #ccc; + border-top: 0; + background: #fff; + box-shadow: -1px 1px 3px rgba(0,0,0,.1); + + /* core styles should not be changed */ + position: absolute; + display: none; + z-index: 9999; + max-height: 254px; + overflow: hidden; + overflow-y: auto; + box-sizing: border-box; + +} +.autocomplete-suggestion { + position: relative; + cursor: pointer; + padding: 7px; + line-height: 23px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #333; +} + +.autocomplete-suggestion b { + font-weight: normal; + color: #1f8dd6; +} + +.autocomplete-suggestion.selected { + background: #333; + color: #fff; +} + +.autocomplete-suggestion:hover { + background: #444; + color: #fff; +} + +.autocomplete-suggestion > .context { + font-size: 12px; +} diff --git a/workshop/themes/learn/static/css/featherlight.min.css b/workshop/themes/learn/static/css/featherlight.min.css new file mode 100644 index 0000000..058487f --- /dev/null +++ b/workshop/themes/learn/static/css/featherlight.min.css @@ -0,0 +1,8 @@ +/** + * Featherlight - ultra slim jQuery lightbox + * Version 1.7.13 - http://noelboss.github.io/featherlight/ + * + * Copyright 2018, Noël Raoul Bossart (http://www.noelboss.com) + * MIT Licensed. +**/ +html.with-featherlight{overflow:hidden}.featherlight{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:2147483647;text-align:center;white-space:nowrap;cursor:pointer;background:#333;background:rgba(0,0,0,0)}.featherlight:last-of-type{background:rgba(0,0,0,.8)}.featherlight:before{content:'';display:inline-block;height:100%;vertical-align:middle}.featherlight .featherlight-content{position:relative;text-align:left;vertical-align:middle;display:inline-block;overflow:auto;padding:25px 25px 0;border-bottom:25px solid transparent;margin-left:5%;margin-right:5%;max-height:95%;background:#fff;cursor:auto;white-space:normal}.featherlight .featherlight-inner{display:block}.featherlight link.featherlight-inner,.featherlight script.featherlight-inner,.featherlight style.featherlight-inner{display:none}.featherlight .featherlight-close-icon{position:absolute;z-index:9999;top:0;right:0;line-height:25px;width:25px;cursor:pointer;text-align:center;font-family:Arial,sans-serif;background:#fff;background:rgba(255,255,255,.3);color:#000;border:0;padding:0}.featherlight .featherlight-close-icon::-moz-focus-inner{border:0;padding:0}.featherlight .featherlight-image{width:100%}.featherlight-iframe .featherlight-content{border-bottom:0;padding:0;-webkit-overflow-scrolling:touch}.featherlight iframe{border:0}.featherlight *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@media only screen and (max-width:1024px){.featherlight .featherlight-content{margin-left:0;margin-right:0;max-height:98%;padding:10px 10px 0;border-bottom:10px solid transparent}}@media print{html.with-featherlight>*>:not(.featherlight){display:none}} \ No newline at end of file diff --git a/workshop/themes/learn/static/css/fontawesome-all.min.css b/workshop/themes/learn/static/css/fontawesome-all.min.css new file mode 100644 index 0000000..d42e216 --- /dev/null +++ b/workshop/themes/learn/static/css/fontawesome-all.min.css @@ -0,0 +1 @@ +.fa,.fab,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-balance-scale:before{content:"\f24e"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-icicles:before{content:"\f7ad"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-nintendo-switch:before{content:"\f418"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-volume:before{content:"\f2a0"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;src:url(https://fonts.workshops.aws/webfonts/fa-brands-400.eot);src:url(https://fonts.workshops.aws/webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(https://fonts.workshops.aws/webfonts/fa-brands-400.woff2) format("woff2"),url(https://fonts.workshops.aws/webfonts/fa-brands-400.woff) format("woff"),url(https://fonts.workshops.aws/webfonts/fa-brands-400.ttf) format("truetype"),url(https://fonts.workshops.aws/webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;src:url(https://fonts.workshops.aws/webfonts/fa-regular-400.eot);src:url(https://fonts.workshops.aws/webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(https://fonts.workshops.aws/webfonts/fa-regular-400.woff2) format("woff2"),url(https://fonts.workshops.aws/webfonts/fa-regular-400.woff) format("woff"),url(https://fonts.workshops.aws/webfonts/fa-regular-400.ttf) format("truetype"),url(https://fonts.workshops.aws/webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;src:url(https://fonts.workshops.aws/webfonts/fa-solid-900.eot);src:url(https://fonts.workshops.aws/webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(https://fonts.workshops.aws/webfonts/fa-solid-900.woff2) format("woff2"),url(https://fonts.workshops.aws/webfonts/fa-solid-900.woff) format("woff"),url(https://fonts.workshops.aws/webfonts/fa-solid-900.ttf) format("truetype"),url(https://fonts.workshops.aws/webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/workshop/themes/learn/static/css/hugo-theme.css b/workshop/themes/learn/static/css/hugo-theme.css new file mode 100644 index 0000000..741cab1 --- /dev/null +++ b/workshop/themes/learn/static/css/hugo-theme.css @@ -0,0 +1,254 @@ +/* Insert here special css for hugo theme, on top of any other imported css */ + + +/* Table of contents */ + +.progress ul { + list-style: none; + margin: 0; + padding: 0 5px; +} + +#TableOfContents { + font-size: 13px !important; + max-height: 85vh; + overflow: auto; + padding: 15px !important; +} + + +#TableOfContents > ul > li > ul > li > ul li { + margin-right: 8px; +} + +#TableOfContents > ul > li > a { + font-weight: bold; padding: 0 18px; margin: 0 2px; +} + +#TableOfContents > ul > li > ul > li > a { + font-weight: bold; +} + +#TableOfContents > ul > li > ul > li > ul > li > ul > li > ul > li { + display: none; +} + +body { + font-size: 16px !important; + color: #323232 !important; +} + +#body a.highlight, #body a.highlight:hover, #body a.highlight:focus { + text-decoration: none; + outline: none; + outline: 0; +} +#body a.highlight { + line-height: 1.1; + display: inline-block; +} +#body a.highlight:after { + display: block; + content: ""; + height: 1px; + width: 0%; + background-color: #0082a7; /*#CE3B2F*/ + -webkit-transition: width 0.5s ease; + -moz-transition: width 0.5s ease; + -ms-transition: width 0.5s ease; + transition: width 0.5s ease; +} +#body a.highlight:hover:after, #body a.highlight:focus:after { + width: 100%; +} +.progress { + position:absolute; + background-color: rgba(246, 246, 246, 0.97); + width: auto; + border: thin solid #ECECEC; + display:none; + z-index:200; +} + +#toc-menu { + border-right: thin solid #DAD8D8 !important; + padding-right: 1rem !important; + margin-right: 0.5rem !important; +} + +#sidebar-toggle-span { + border-right: thin solid #DAD8D8 !important; + padding-right: 0.5rem !important; + margin-right: 1rem !important; +} + +.btn { + display: inline-block !important; + padding: 6px 12px !important; + margin-bottom: 0 !important; + font-size: 14px !important; + font-weight: normal !important; + line-height: 1.42857143 !important; + text-align: center !important; + white-space: nowrap !important; + vertical-align: middle !important; + -ms-touch-action: manipulation !important; + touch-action: manipulation !important; + cursor: pointer !important; + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; + background-image: none !important; + border: 1px solid transparent !important; + border-radius: 4px !important; + -webkit-transition: all 0.15s !important; + -moz-transition: all 0.15s !important; + transition: all 0.15s !important; +} +.btn:focus { + /*outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px;*/ + outline: none !important; +} +.btn:hover, +.btn:focus { + color: #2b2b2b !important; + text-decoration: none !important; +} + +.btn-default { + color: #333 !important; + background-color: #fff !important; + border-color: #ccc !important; +} +.btn-default:hover, +.btn-default:focus, +.btn-default:active { + color: #fff !important; + background-color: #9e9e9e !important; + border-color: #9e9e9e !important; +} +.btn-default:active { + background-image: none !important; +} + +/* anchors */ +.anchor { + color: #00bdf3; + font-size: 0.5em; + cursor:pointer; + visibility:hidden; + margin-left: 0.5em; + position: absolute; + margin-top:0.1em; +} + +h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor { + visibility:visible; +} + +/* Redfines headers style */ + +h2, h3, h4, h5, h6 { + font-weight: 400; + line-height: 1.1; +} + +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + font-weight: inherit; +} + +h2 { + font-size: 2.5rem; + line-height: 110% !important; + margin: 2.5rem 0 1.5rem 0; +} + +h3 { + font-size: 2rem; + line-height: 110% !important; + margin: 2rem 0 1rem 0; +} + +h4 { + font-size: 1.5rem; + line-height: 110% !important; + margin: 1.5rem 0 0.75rem 0; +} + +h5 { + font-size: 1rem; + line-height: 110% !important; + margin: 1rem 0 0.2rem 0; +} + +h6 { + font-size: 0.5rem; + line-height: 110% !important; + margin: 0.5rem 0 0.2rem 0; +} + +p { + margin: 1rem 0; +} + +figcaption h4 { + font-weight: 300 !important; + opacity: .85; + font-size: 1em; + text-align: center; + margin-top: -1.5em; +} + +.select-style { + border: 0; + width: 150px; + border-radius: 0px; + overflow: hidden; + display: inline-flex; +} + +.select-style svg { + fill: #ccc; + width: 14px; + height: 14px; + pointer-events: none; + margin: auto; +} + +.select-style svg:hover { + fill: #e6e6e6; +} + +.select-style select { + padding: 0; + width: 130%; + border: none; + box-shadow: none; + background: transparent; + background-image: none; + -webkit-appearance: none; + margin: auto; + margin-left: 0px; + margin-right: -20px; +} + +.select-style select:focus { + outline: none; +} + +.select-style :hover { + cursor: pointer; +} + +@media only all and (max-width: 47.938em) { + #breadcrumbs .links, #top-github-link-text { + display: none; + } +} + +.is-sticky #top-bar { + box-shadow: -1px 2px 5px 1px rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/workshop/themes/learn/static/css/hybrid.css b/workshop/themes/learn/static/css/hybrid.css new file mode 100644 index 0000000..29735a1 --- /dev/null +++ b/workshop/themes/learn/static/css/hybrid.css @@ -0,0 +1,102 @@ +/* + +vim-hybrid theme by w0ng (https://github.com/w0ng/vim-hybrid) + +*/ + +/*background color*/ +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #1d1f21; +} + +/*selection color*/ +.hljs::selection, +.hljs span::selection { + background: #373b41; +} + +.hljs::-moz-selection, +.hljs span::-moz-selection { + background: #373b41; +} + +/*foreground color*/ +.hljs { + color: #c5c8c6; +} + +/*color: fg_yellow*/ +.hljs-title, +.hljs-name { + color: #f0c674; +} + +/*color: fg_comment*/ +.hljs-comment, +.hljs-meta, +.hljs-meta .hljs-keyword { + color: #707880; +} + +/*color: fg_red*/ +.hljs-number, +.hljs-symbol, +.hljs-literal, +.hljs-deletion, +.hljs-link { + color: #cc6666 +} + +/*color: fg_green*/ +.hljs-string, +.hljs-doctag, +.hljs-addition, +.hljs-regexp, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #b5bd68; +} + +/*color: fg_purple*/ +.hljs-attribute, +.hljs-code, +.hljs-selector-id { + color: #b294bb; +} + +/*color: fg_blue*/ +.hljs-keyword, +.hljs-selector-tag, +.hljs-bullet, +.hljs-tag { + color: #81a2be; +} + +/*color: fg_aqua*/ +.hljs-subst, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable { + color: #8abeb7; +} + +/*color: fg_orange*/ +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-quote, +.hljs-section, +.hljs-selector-class { + color: #de935f; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/workshop/themes/learn/static/css/nucleus.css b/workshop/themes/learn/static/css/nucleus.css new file mode 100644 index 0000000..1897fc5 --- /dev/null +++ b/workshop/themes/learn/static/css/nucleus.css @@ -0,0 +1,615 @@ +*, *::before, *::after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +@-webkit-viewport { + width: device-width; } +@-moz-viewport { + width: device-width; } +@-ms-viewport { + width: device-width; } +@-o-viewport { + width: device-width; } +@viewport { + width: device-width; } +html { + font-size: 100%; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; } + +body { + margin: 0; } + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; } + +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; } + +audio:not([controls]) { + display: none; + height: 0; } + +[hidden], +template { + display: none; } + +a { + background: transparent; + text-decoration: none; } + +a:active, +a:hover { + outline: 0; } + +abbr[title] { + border-bottom: 1px dotted; } + +b, +strong { + font-weight: bold; } + +dfn { + font-style: italic; } + +mark { + background: #FFFF27; + color: #333; } + +sub, +sup { + font-size: 0.8rem; + line-height: 0; + position: relative; + vertical-align: baseline; } + +sup { + top: -0.5em; } + +sub { + bottom: -0.25em; } + +img { + border: 0; + max-width: 100%; } + +svg:not(:root) { + overflow: hidden; } + +figure { + margin: 1em 40px; } + +hr { + height: 0; } + +pre { + overflow: auto; } + +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; } + +button { + overflow: visible; } + +button, +select { + text-transform: none; } + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; } + +button[disabled], +html input[disabled] { + cursor: default; } + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; } + +input { + line-height: normal; } + +input[type="checkbox"], +input[type="radio"] { + padding: 0; } + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; } + +input[type="search"] { + -webkit-appearance: textfield; } + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; } + +legend { + border: 0; + padding: 0; } + +textarea { + overflow: auto; } + +optgroup { + font-weight: bold; } + +table { + border-collapse: collapse; + border-spacing: 0; + table-layout: fixed; + width: 100%; } + +tr, td, th { + vertical-align: middle; } + +th, td { + padding: 0.425rem 0; } + +th { + text-align: left; } + +.container { + width: 75em; + margin: 0 auto; + padding: 0; } + @media only all and (min-width: 60em) and (max-width: 74.938em) { + .container { + width: 60em; } } + @media only all and (min-width: 48em) and (max-width: 59.938em) { + .container { + width: 48em; } } + @media only all and (min-width: 30.063em) and (max-width: 47.938em) { + .container { + width: 30em; } } + @media only all and (max-width: 30em) { + .container { + width: 100%; } } + +.grid { + display: -webkit-box; + display: -moz-box; + display: box; + display: -webkit-flex; + display: -moz-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row; + -moz-flex-flow: row; + flex-flow: row; + list-style: none; + margin: 0; + padding: 0; } + @media only all and (max-width: 47.938em) { + .grid { + -webkit-flex-flow: row wrap; + -moz-flex-flow: row wrap; + flex-flow: row wrap; } } + +.block { + -webkit-box-flex: 1; + -moz-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -moz-flex: 1; + -ms-flex: 1; + flex: 1; + min-width: 0; + min-height: 0; } + @media only all and (max-width: 47.938em) { + .block { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 100%; + -moz-flex: 0 100%; + -ms-flex: 0 100%; + flex: 0 100%; } } + +.content { + margin: 0.625rem; + padding: 0.938rem; } + +@media only all and (max-width: 47.938em) { + body [class*="size-"] { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 100%; + -moz-flex: 0 100%; + -ms-flex: 0 100%; + flex: 0 100%; } } + +.size-1-2 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 50%; + -moz-flex: 0 50%; + -ms-flex: 0 50%; + flex: 0 50%; } + +.size-1-3 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 33.33333%; + -moz-flex: 0 33.33333%; + -ms-flex: 0 33.33333%; + flex: 0 33.33333%; } + +.size-1-4 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 25%; + -moz-flex: 0 25%; + -ms-flex: 0 25%; + flex: 0 25%; } + +.size-1-5 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 20%; + -moz-flex: 0 20%; + -ms-flex: 0 20%; + flex: 0 20%; } + +.size-1-6 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 16.66667%; + -moz-flex: 0 16.66667%; + -ms-flex: 0 16.66667%; + flex: 0 16.66667%; } + +.size-1-7 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 14.28571%; + -moz-flex: 0 14.28571%; + -ms-flex: 0 14.28571%; + flex: 0 14.28571%; } + +.size-1-8 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 12.5%; + -moz-flex: 0 12.5%; + -ms-flex: 0 12.5%; + flex: 0 12.5%; } + +.size-1-9 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 11.11111%; + -moz-flex: 0 11.11111%; + -ms-flex: 0 11.11111%; + flex: 0 11.11111%; } + +.size-1-10 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 10%; + -moz-flex: 0 10%; + -ms-flex: 0 10%; + flex: 0 10%; } + +.size-1-11 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 9.09091%; + -moz-flex: 0 9.09091%; + -ms-flex: 0 9.09091%; + flex: 0 9.09091%; } + +.size-1-12 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 8.33333%; + -moz-flex: 0 8.33333%; + -ms-flex: 0 8.33333%; + flex: 0 8.33333%; } + +@media only all and (min-width: 48em) and (max-width: 59.938em) { + .size-tablet-1-2 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 50%; + -moz-flex: 0 50%; + -ms-flex: 0 50%; + flex: 0 50%; } + + .size-tablet-1-3 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 33.33333%; + -moz-flex: 0 33.33333%; + -ms-flex: 0 33.33333%; + flex: 0 33.33333%; } + + .size-tablet-1-4 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 25%; + -moz-flex: 0 25%; + -ms-flex: 0 25%; + flex: 0 25%; } + + .size-tablet-1-5 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 20%; + -moz-flex: 0 20%; + -ms-flex: 0 20%; + flex: 0 20%; } + + .size-tablet-1-6 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 16.66667%; + -moz-flex: 0 16.66667%; + -ms-flex: 0 16.66667%; + flex: 0 16.66667%; } + + .size-tablet-1-7 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 14.28571%; + -moz-flex: 0 14.28571%; + -ms-flex: 0 14.28571%; + flex: 0 14.28571%; } + + .size-tablet-1-8 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 12.5%; + -moz-flex: 0 12.5%; + -ms-flex: 0 12.5%; + flex: 0 12.5%; } + + .size-tablet-1-9 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 11.11111%; + -moz-flex: 0 11.11111%; + -ms-flex: 0 11.11111%; + flex: 0 11.11111%; } + + .size-tablet-1-10 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 10%; + -moz-flex: 0 10%; + -ms-flex: 0 10%; + flex: 0 10%; } + + .size-tablet-1-11 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 9.09091%; + -moz-flex: 0 9.09091%; + -ms-flex: 0 9.09091%; + flex: 0 9.09091%; } + + .size-tablet-1-12 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 8.33333%; + -moz-flex: 0 8.33333%; + -ms-flex: 0 8.33333%; + flex: 0 8.33333%; } } +@media only all and (max-width: 47.938em) { + @supports not (flex-wrap: wrap) { + .grid { + display: block; + -webkit-box-lines: inherit; + -moz-box-lines: inherit; + box-lines: inherit; + -webkit-flex-wrap: inherit; + -moz-flex-wrap: inherit; + -ms-flex-wrap: inherit; + flex-wrap: inherit; } + + .block { + display: block; + -webkit-box-flex: inherit; + -moz-box-flex: inherit; + box-flex: inherit; + -webkit-flex: inherit; + -moz-flex: inherit; + -ms-flex: inherit; + flex: inherit; } } } +.first-block { + -webkit-box-ordinal-group: 0; + -webkit-order: -1; + -ms-flex-order: -1; + order: -1; } + +.last-block { + -webkit-box-ordinal-group: 2; + -webkit-order: 1; + -ms-flex-order: 1; + order: 1; } + +.fixed-blocks { + -webkit-flex-flow: row wrap; + -moz-flex-flow: row wrap; + flex-flow: row wrap; } + .fixed-blocks .block { + -webkit-box-flex: inherit; + -moz-box-flex: inherit; + box-flex: inherit; + -webkit-flex: inherit; + -moz-flex: inherit; + -ms-flex: inherit; + flex: inherit; + width: 25%; } + @media only all and (min-width: 60em) and (max-width: 74.938em) { + .fixed-blocks .block { + width: 33.33333%; } } + @media only all and (min-width: 48em) and (max-width: 59.938em) { + .fixed-blocks .block { + width: 50%; } } + @media only all and (max-width: 47.938em) { + .fixed-blocks .block { + width: 100%; } } + +body { + font-size: 1.05rem; + line-height: 1.7; } + +h1, h2, h3, h4, h5, h6 { + margin: 0.85rem 0 1.7rem 0; + text-rendering: optimizeLegibility; } + +h1 { + font-size: 3.25rem; } + +h2 { + font-size: 2.55rem; } + +h3 { + font-size: 2.15rem; } + +h4 { + font-size: 1.8rem; } + +h5 { + font-size: 1.4rem; } + +h6 { + font-size: 0.9rem; } + +p { + margin: 1.7rem 0; } + +ul, ol { + margin-top: 1.7rem; + margin-bottom: 1.7rem; } + ul ul, ul ol, ol ul, ol ol { + margin-top: 0; + margin-bottom: 0; } + +blockquote { + margin: 1.7rem 0; + padding-left: 0.85rem; } + +cite { + display: block; + font-size: 0.925rem; } + cite:before { + content: "\2014 \0020"; } + +pre { + margin: 1.7rem 0; + padding: 0.938rem; } + +code { + vertical-align: bottom; } + +small { + font-size: 0.925rem; } + +hr { + border-left: none; + border-right: none; + border-top: none; + margin: 1.7rem 0; } + +fieldset { + border: 0; + padding: 0.938rem; + margin: 0 0 1.7rem 0; } + +input, +label, +select { + display: block; } + +label { + margin-bottom: 0.425rem; } + label.required:after { + content: "*"; } + label abbr { + display: none; } + +textarea, input[type="email"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="text"], input[type="url"], input[type="color"], input[type="date"], input[type="datetime"], input[type="datetime-local"], input[type="month"], input[type="time"], input[type="week"], select[multiple=multiple] { + -webkit-transition: border-color; + -moz-transition: border-color; + transition: border-color; + border-radius: 0.1875rem; + margin-bottom: 0.85rem; + padding: 0.425rem 0.425rem; + width: 100%; } + textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { + outline: none; } + +textarea { + resize: vertical; } + +input[type="checkbox"], input[type="radio"] { + display: inline; + margin-right: 0.425rem; } + +input[type="file"] { + width: 100%; } + +select { + width: auto; + max-width: 100%; + margin-bottom: 1.7rem; } + +button, +input[type="submit"] { + cursor: pointer; + user-select: none; + vertical-align: middle; + white-space: nowrap; + border: inherit; } diff --git a/workshop/themes/learn/static/css/perfect-scrollbar.min.css b/workshop/themes/learn/static/css/perfect-scrollbar.min.css new file mode 100644 index 0000000..ebd2cb4 --- /dev/null +++ b/workshop/themes/learn/static/css/perfect-scrollbar.min.css @@ -0,0 +1,2 @@ +/* perfect-scrollbar v0.6.13 */ +.ps-container{-ms-touch-action:auto;touch-action:auto;overflow:hidden !important;-ms-overflow-style:none}@supports (-ms-overflow-style: none){.ps-container{overflow:auto !important}}@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none){.ps-container{overflow:auto !important}}.ps-container.ps-active-x>.ps-scrollbar-x-rail,.ps-container.ps-active-y>.ps-scrollbar-y-rail{display:block;background-color:transparent}.ps-container.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail{background-color:#eee;opacity:.9}.ps-container.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail>.ps-scrollbar-x{background-color:#999;height:11px}.ps-container.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail{background-color:#eee;opacity:.9}.ps-container.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail>.ps-scrollbar-y{background-color:#999;width:11px}.ps-container>.ps-scrollbar-x-rail{display:none;position:absolute;opacity:0;-webkit-transition:background-color .2s linear, opacity .2s linear;-o-transition:background-color .2s linear, opacity .2s linear;-moz-transition:background-color .2s linear, opacity .2s linear;transition:background-color .2s linear, opacity .2s linear;bottom:0px;height:15px}.ps-container>.ps-scrollbar-x-rail>.ps-scrollbar-x{position:absolute;background-color:#aaa;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out;-o-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out;-moz-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -webkit-border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out;bottom:2px;height:6px}.ps-container>.ps-scrollbar-x-rail:hover>.ps-scrollbar-x,.ps-container>.ps-scrollbar-x-rail:active>.ps-scrollbar-x{height:11px}.ps-container>.ps-scrollbar-y-rail{display:none;position:absolute;opacity:0;-webkit-transition:background-color .2s linear, opacity .2s linear;-o-transition:background-color .2s linear, opacity .2s linear;-moz-transition:background-color .2s linear, opacity .2s linear;transition:background-color .2s linear, opacity .2s linear;right:0;width:15px}.ps-container>.ps-scrollbar-y-rail>.ps-scrollbar-y{position:absolute;background-color:#aaa;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out;-o-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out;-moz-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -webkit-border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out;right:2px;width:6px}.ps-container>.ps-scrollbar-y-rail:hover>.ps-scrollbar-y,.ps-container>.ps-scrollbar-y-rail:active>.ps-scrollbar-y{width:11px}.ps-container:hover.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail{background-color:#eee;opacity:.9}.ps-container:hover.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail>.ps-scrollbar-x{background-color:#999;height:11px}.ps-container:hover.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail{background-color:#eee;opacity:.9}.ps-container:hover.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail>.ps-scrollbar-y{background-color:#999;width:11px}.ps-container:hover>.ps-scrollbar-x-rail,.ps-container:hover>.ps-scrollbar-y-rail{opacity:.6}.ps-container:hover>.ps-scrollbar-x-rail:hover{background-color:#eee;opacity:.9}.ps-container:hover>.ps-scrollbar-x-rail:hover>.ps-scrollbar-x{background-color:#999}.ps-container:hover>.ps-scrollbar-y-rail:hover{background-color:#eee;opacity:.9}.ps-container:hover>.ps-scrollbar-y-rail:hover>.ps-scrollbar-y{background-color:#999} diff --git a/workshop/themes/learn/static/css/tags.css b/workshop/themes/learn/static/css/tags.css new file mode 100644 index 0000000..495d2f9 --- /dev/null +++ b/workshop/themes/learn/static/css/tags.css @@ -0,0 +1,49 @@ +/* Tags */ + +#head-tags{ + margin-left:1em; + margin-top:1em; +} + +#body .tags a.tag-link { + display: inline-block; + line-height: 2em; + font-size: 0.8em; + position: relative; + margin: 0 16px 8px 0; + padding: 0 10px 0 12px; + background: #8451a1; + + -webkit-border-bottom-right-radius: 3px; + border-bottom-right-radius: 3px; + -webkit-border-top-right-radius: 3px; + border-top-right-radius: 3px; + + -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.2); + box-shadow: 0 1px 2px rgba(0,0,0,0.2); + color: #fff; +} + +#body .tags a.tag-link:before { + content: ""; + position: absolute; + top:0; + left: -1em; + width: 0; + height: 0; + border-color: transparent #8451a1 transparent transparent; + border-style: solid; + border-width: 1em 1em 1em 0; +} + +#body .tags a.tag-link:after { + content: ""; + position: absolute; + top: 10px; + left: 1px; + width: 5px; + height: 5px; + -webkit-border-radius: 50%; + border-radius: 100%; + background: #fff; +} diff --git a/workshop/themes/learn/static/css/theme-aws.css b/workshop/themes/learn/static/css/theme-aws.css new file mode 100644 index 0000000..c73f02e --- /dev/null +++ b/workshop/themes/learn/static/css/theme-aws.css @@ -0,0 +1,281 @@ + +:root{ + + --MAIN-TEXT-color:#232F3E; /* Color of text by default */ + --MAIN-TITLES-TEXT-color: #161E2D; /* Color of titles h2-h3-h4-h5 */ + --MAIN-LINK-color:#95b0ff; /* Color of links */ + --MAIN-LINK-HOVER-color:#527FFF; /* Color of hovered links */ + --MAIN-ANCHOR-color: #95b0ff; /* color of anchors on titles */ + + --MENU-HEADER-BG-color:#161E2D; /* Background color of menu header */ + --MENU-HEADER-BORDER-color:#161E2D; /*Color of menu header border */ + + --MENU-SEARCH-BG-color:#202c3c; /* Search field background color (by default borders + icons) */ + --MENU-SEARCH-BOX-color: #4d6584; /* Override search field border color */ + --MENU-SEARCH-BOX-ICONS-color: #4d6584; /* Override search field icons color */ + + --MENU-SECTIONS-ACTIVE-BG-color:#232F3E; /* Background color of the active section and its childs */ + --MENU-SECTIONS-BG-color:#161E2D; /* Background color of other sections */ + --MENU-SECTIONS-TEXT-color: #FF9900; /*Color of pre text */ + --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ + --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ + --MENU-SECTION-ACTIVE-CATEGORY-color: #232F3E; /* Color of active category text */ + --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #FF9900; /* Color of background for the active category (only) */ + --MENU-SECTION-ACTIVE-CATEGORY-TEXT-color: #fff; /* Color of pre text when selected */ + + --MENU-VISITED-color: #527FFF; /* Color of 'page visited' icons in menu */ + --MENU-SECTION-HR-color: #20272b; /* Color of
    separator in menu */ + +} + +@font-face { + font-family: 'Amazon Ember'; + src: url("https://fonts.workshops.aws/webfonts/AmazonEmber_W_Lt.eot"); + src: url("https://fonts.workshops.aws/webfonts/AmazonEmber_W_Lt.eot?#iefix") format("embedded-opentype"), url("https://fonts.workshops.aws/webfonts/AmazonEmber_W_Lt.woff2") format("woff2"), url("https://fonts.workshops.aws/webfonts/AmazonEmber_W_Lt.woff") format("woff"); + font-style: normal; + font-weight: 200; +} + +@font-face { + font-family: 'Amazon Ember'; + src: url("https://fonts.workshops.aws/webfonts/AmazonEmber_W_Bd.eot"); + src: url("https://fonts.workshops.aws/webfonts/AmazonEmber_W_Bd.eot?#iefix") format("embedded-opentype"), url("https://fonts.workshops.aws/webfonts/AmazonEmber_W_Bd.woff2") format("woff2"), url("https://fonts.workshops.aws/webfonts/AmazonEmber_W_Lt.woff") format("woff"); + font-style: bold; + font-weight: 400; +} + +body { + font-family: "Amazon Ember", "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + color: var(--MAIN-TEXT-color) !important; +} + +#chapter h3 { + font-family: "Amazon Ember","Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; +} + +textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { + border-color: none; + box-shadow: none; +} + +h2, h3, h4, h5, h6 { + font-family: "Amazon Ember", "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + color: var(--MAIN-TITLES-TEXT-color) !important; +} + +h1 { + font-family: "Amazon Ember", "Novacento Sans Wide", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; +} + +a { + color: var(--MAIN-LINK-color); +} + +.anchor { + color: var(--MAIN-ANCHOR-color); +} + +a:hover { + color: var(--MAIN-LINK-HOVER-color); +} + +#sidebar ul li.visited > a .read-icon { + color: var(--MENU-VISITED-color); +} + +#sidebar #footer { + padding-top: 20px !important; +} + +#sidebar #footer h2.github-title { + font-size: 20px; + color: #fd9827 !important; + margin: 10px 0px 5px; + padding: 0px; + font-weight: normal !important; + margin-top: 10px; + padding-top: 30px; + border-top: 1px dotted #384657; +} + +#sidebar #footer h3.github-title { + font-size: 14px; + margin: 10px 0px 5px; + padding: 0px; + text-transform: uppercase; + letter-spacing: .15px; +} + +#sidebar #footer h5.copyright, #sidebar #footer p.build-number { + color: var(--MENU-SECTIONS-LINK-color) !important; + font-size: 10px; + letter-spacing: .15px; + line-height: 150% !important; +} + +#body a.highlight:after { + display: block; + content: ""; + height: 1px; + width: 0%; + -webkit-transition: width 0.5s ease; + -moz-transition: width 0.5s ease; + -ms-transition: width 0.5s ease; + transition: width 0.5s ease; + background-color: var(--MAIN-LINK-HOVER-color); +} +#sidebar { + background-color: var(--MENU-SECTIONS-BG-color); +} +#sidebar #header-wrapper { + background: var(--MENU-HEADER-BG-color); + color: var(--MENU-SEARCH-BOX-color); + border-color: var(--MENU-HEADER-BORDER-color); +} +#sidebar .searchbox { + border-color: var(--MENU-SEARCH-BOX-color); + background: var(--MENU-SEARCH-BG-color); +} +#sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { + background: var(--MENU-SECTIONS-ACTIVE-BG-color); +} +#sidebar .searchbox * { + color: var(--MENU-SEARCH-BOX-ICONS-color); +} + +#sidebar a { + color: var(--MENU-SECTIONS-LINK-color); +} + +#sidebar a b { + color: var(--MENU-SECTIONS-TEXT-color); +} + +#sidebar a:hover { + color: var(--MENU-SECTIONS-LINK-HOVER-color); +} + +#sidebar ul li.active > a { + background: var(--MENU-SECTION-ACTIVE-CATEGORY-BG-color); + color: var(--MENU-SECTION-ACTIVE-CATEGORY-color) !important; +} + +#sidebar ul.topics > li > a b { + color: var(--MENU-SECTION-ACTIVE-CATEGORY-TEXT-color) !important; + opacity: 1; +} + +#sidebar hr { + border-color: var(--MENU-SECTION-HR-color); +} + +#sidebar #shortcuts h3 { + font-family: "Amazon Eber", "Novacento Sans Wide", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + color: white !important; + margin-top:1rem; + padding-left: 1rem; +} + +#navigation a.nav-prev, #navigation a.nav-next { + color: #f19e39 !important; +} + +#navigation a.nav-prev:hover, #navigation a.nav-next:hover { + color: #e07d04 !important; +} + +div.notices p:first-child:before { + position: absolute; + top: 2px; + color: #fff; + font-family: 'Font Awesome\ 5 Free'; + content: #F06A; + font-weight: 900; /* Fix version 5.0.9 */ + left: 10px; +} + +.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default, .ui-button, html .ui-button.ui-state-disabled:hover, html .ui-button.ui-state-disabled:active { + border: 1px solid #dddddd; + font-weight: normal; + color: #454545; +} + +.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active, a.ui-button:active, .ui-button:active, .ui-button.ui-state-active:hover { + border: 1px solid var(--MENU-HEADER-BG-color); + background: var(--MENU-HEADER-BG-color); + font-weight: normal; + color: #fff; +} + +.ui-widget.ui-widget-content { + border: 1px solid #eeeeee; +} + +.ui-widget-header { + border: 1px solid #eeeeee; +} + +.hljs { + background-color: none; +} + +pre { + background-color: var(--MENU-SECTIONS-BG-color) !important; +} + +div.notices.info p { + border-top: 30px solid #fd9827; + background: #FFF2DB; +} + +.btn { + display: inline-block !important; + padding: 6px 12px !important; + margin-bottom: 0 !important; + font-size: 14px !important; + font-weight: normal !important; + line-height: 1.42857143 !important; + text-align: center !important; + white-space: nowrap !important; + vertical-align: middle !important; + -ms-touch-action: manipulation !important; + touch-action: manipulation !important; + cursor: pointer !important; + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; + background-image: none !important; + border: 1px solid transparent !important; + border-radius: 4px !important; + -webkit-transition: all 0.15s !important; + -moz-transition: all 0.15s !important; + transition: all 0.15s !important; + } + .btn:focus { + /*outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px;*/ + outline: none !important; + } + .btn:hover, + .btn:focus { + color: #2b2b2b !important; + text-decoration: none !important; + } + + .btn-default { + color: #fff !important; + background-color: #527FFF !important; + border-color: #527FFF !important; + } + .btn-default:hover, + .btn-default:focus, + .btn-default:active { + color: #fff !important; + background-color: #95b0ff !important; + border-color: #95b0ff !important; + } + .btn-default:active { + background-image: none !important; + } \ No newline at end of file diff --git a/workshop/themes/learn/static/css/theme-blue.css b/workshop/themes/learn/static/css/theme-blue.css new file mode 100644 index 0000000..9771ae5 --- /dev/null +++ b/workshop/themes/learn/static/css/theme-blue.css @@ -0,0 +1,111 @@ + +:root{ + + --MAIN-TEXT-color:#323232; /* Color of text by default */ + --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5 */ + --MAIN-LINK-color:#1C90F3; /* Color of links */ + --MAIN-LINK-HOVER-color:#167ad0; /* Color of hovered links */ + --MAIN-ANCHOR-color: #1C90F3; /* color of anchors on titles */ + + --MENU-HEADER-BG-color:#1C90F3; /* Background color of menu header */ + --MENU-HEADER-BORDER-color:#33a1ff; /*Color of menu header border */ + + --MENU-SEARCH-BG-color:#167ad0; /* Search field background color (by default borders + icons) */ + --MENU-SEARCH-BOX-color: #33a1ff; /* Override search field border color */ + --MENU-SEARCH-BOX-ICONS-color: #a1d2fd; /* Override search field icons color */ + + --MENU-SECTIONS-ACTIVE-BG-color:#20272b; /* Background color of the active section and its childs */ + --MENU-SECTIONS-BG-color:#252c31; /* Background color of other sections */ + --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ + --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ + --MENU-SECTION-ACTIVE-CATEGORY-color: #777; /* Color of active category text */ + --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #fff; /* Color of background for the active category (only) */ + + --MENU-VISITED-color: #33a1ff; /* Color of 'page visited' icons in menu */ + --MENU-SECTION-HR-color: #20272b; /* Color of
    separator in menu */ + +} + +body { + color: var(--MAIN-TEXT-color) !important; +} + +textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { + border-color: none; + box-shadow: none; +} + +h2, h3, h4, h5 { + color: var(--MAIN-TITLES-TEXT-color) !important; +} + +a { + color: var(--MAIN-LINK-color); +} + +.anchor { + color: var(--MAIN-ANCHOR-color); +} + +a:hover { + color: var(--MAIN-LINK-HOVER-color); +} + +#sidebar ul li.visited > a .read-icon { + color: var(--MENU-VISITED-color); +} + +#body a.highlight:after { + display: block; + content: ""; + height: 1px; + width: 0%; + -webkit-transition: width 0.5s ease; + -moz-transition: width 0.5s ease; + -ms-transition: width 0.5s ease; + transition: width 0.5s ease; + background-color: var(--MAIN-LINK-HOVER-color); +} +#sidebar { + background-color: var(--MENU-SECTIONS-BG-color); +} +#sidebar #header-wrapper { + background: var(--MENU-HEADER-BG-color); + color: var(--MENU-SEARCH-BOX-color); + border-color: var(--MENU-HEADER-BORDER-color); +} +#sidebar .searchbox { + border-color: var(--MENU-SEARCH-BOX-color); + background: var(--MENU-SEARCH-BG-color); +} +#sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { + background: var(--MENU-SECTIONS-ACTIVE-BG-color); +} +#sidebar .searchbox * { + color: var(--MENU-SEARCH-BOX-ICONS-color); +} + +#sidebar a { + color: var(--MENU-SECTIONS-LINK-color); +} + +#sidebar a:hover { + color: var(--MENU-SECTIONS-LINK-HOVER-color); +} + +#sidebar ul li.active > a { + background: var(--MENU-SECTION-ACTIVE-CATEGORY-BG-color); + color: var(--MENU-SECTION-ACTIVE-CATEGORY-color) !important; +} + +#sidebar hr { + border-color: var(--MENU-SECTION-HR-color); +} + +#body .tags a.tag-link { + background-color: var(--MENU-HEADER-BG-color); +} + +#body .tags a.tag-link:before { + border-right-color: var(--MENU-HEADER-BG-color); +} \ No newline at end of file diff --git a/workshop/themes/learn/static/css/theme-green.css b/workshop/themes/learn/static/css/theme-green.css new file mode 100644 index 0000000..3b0b1f7 --- /dev/null +++ b/workshop/themes/learn/static/css/theme-green.css @@ -0,0 +1,111 @@ + +:root{ + + --MAIN-TEXT-color:#323232; /* Color of text by default */ + --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5 */ + --MAIN-LINK-color:#599a3e; /* Color of links */ + --MAIN-LINK-HOVER-color:#3f6d2c; /* Color of hovered links */ + --MAIN-ANCHOR-color: #599a3e; /* color of anchors on titles */ + + --MENU-HEADER-BG-color:#74b559; /* Background color of menu header */ + --MENU-HEADER-BORDER-color:#9cd484; /*Color of menu header border */ + + --MENU-SEARCH-BG-color:#599a3e; /* Search field background color (by default borders + icons) */ + --MENU-SEARCH-BOX-color: #84c767; /* Override search field border color */ + --MENU-SEARCH-BOX-ICONS-color: #c7f7c4; /* Override search field icons color */ + + --MENU-SECTIONS-ACTIVE-BG-color:#1b211c; /* Background color of the active section and its childs */ + --MENU-SECTIONS-BG-color:#222723; /* Background color of other sections */ + --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ + --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ + --MENU-SECTION-ACTIVE-CATEGORY-color: #777; /* Color of active category text */ + --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #fff; /* Color of background for the active category (only) */ + + --MENU-VISITED-color: #599a3e; /* Color of 'page visited' icons in menu */ + --MENU-SECTION-HR-color: #18211c; /* Color of
    separator in menu */ + +} + +body { + color: var(--MAIN-TEXT-color) !important; +} + +textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { + border-color: none; + box-shadow: none; +} + +h2, h3, h4, h5 { + color: var(--MAIN-TITLES-TEXT-color) !important; +} + +a { + color: var(--MAIN-LINK-color); +} + +.anchor { + color: var(--MAIN-ANCHOR-color); +} + +a:hover { + color: var(--MAIN-LINK-HOVER-color); +} + +#sidebar ul li.visited > a .read-icon { + color: var(--MENU-VISITED-color); +} + +#body a.highlight:after { + display: block; + content: ""; + height: 1px; + width: 0%; + -webkit-transition: width 0.5s ease; + -moz-transition: width 0.5s ease; + -ms-transition: width 0.5s ease; + transition: width 0.5s ease; + background-color: var(--MAIN-LINK-HOVER-color); +} +#sidebar { + background-color: var(--MENU-SECTIONS-BG-color); +} +#sidebar #header-wrapper { + background: var(--MENU-HEADER-BG-color); + color: var(--MENU-SEARCH-BOX-color); + border-color: var(--MENU-HEADER-BORDER-color); +} +#sidebar .searchbox { + border-color: var(--MENU-SEARCH-BOX-color); + background: var(--MENU-SEARCH-BG-color); +} +#sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { + background: var(--MENU-SECTIONS-ACTIVE-BG-color); +} +#sidebar .searchbox * { + color: var(--MENU-SEARCH-BOX-ICONS-color); +} + +#sidebar a { + color: var(--MENU-SECTIONS-LINK-color); +} + +#sidebar a:hover { + color: var(--MENU-SECTIONS-LINK-HOVER-color); +} + +#sidebar ul li.active > a { + background: var(--MENU-SECTION-ACTIVE-CATEGORY-BG-color); + color: var(--MENU-SECTION-ACTIVE-CATEGORY-color) !important; +} + +#sidebar hr { + border-color: var(--MENU-SECTION-HR-color); +} + +#body .tags a.tag-link { + background-color: var(--MENU-HEADER-BG-color); +} + +#body .tags a.tag-link:before { + border-right-color: var(--MENU-HEADER-BG-color); +} \ No newline at end of file diff --git a/workshop/themes/learn/static/css/theme-red.css b/workshop/themes/learn/static/css/theme-red.css new file mode 100644 index 0000000..36c9278 --- /dev/null +++ b/workshop/themes/learn/static/css/theme-red.css @@ -0,0 +1,111 @@ + +:root{ + + --MAIN-TEXT-color:#323232; /* Color of text by default */ + --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5 */ + --MAIN-LINK-color:#f31c1c; /* Color of links */ + --MAIN-LINK-HOVER-color:#d01616; /* Color of hovered links */ + --MAIN-ANCHOR-color: #f31c1c; /* color of anchors on titles */ + + --MENU-HEADER-BG-color:#dc1010; /* Background color of menu header */ + --MENU-HEADER-BORDER-color:#e23131; /*Color of menu header border */ + + --MENU-SEARCH-BG-color:#b90000; /* Search field background color (by default borders + icons) */ + --MENU-SEARCH-BOX-color: #ef2020; /* Override search field border color */ + --MENU-SEARCH-BOX-ICONS-color: #fda1a1; /* Override search field icons color */ + + --MENU-SECTIONS-ACTIVE-BG-color:#2b2020; /* Background color of the active section and its childs */ + --MENU-SECTIONS-BG-color:#312525; /* Background color of other sections */ + --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ + --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ + --MENU-SECTION-ACTIVE-CATEGORY-color: #777; /* Color of active category text */ + --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #fff; /* Color of background for the active category (only) */ + + --MENU-VISITED-color: #ff3333; /* Color of 'page visited' icons in menu */ + --MENU-SECTION-HR-color: #2b2020; /* Color of
    separator in menu */ + +} + +body { + color: var(--MAIN-TEXT-color) !important; +} + +textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { + border-color: none; + box-shadow: none; +} + +h2, h3, h4, h5 { + color: var(--MAIN-TITLES-TEXT-color) !important; +} + +a { + color: var(--MAIN-LINK-color); +} + +.anchor { + color: var(--MAIN-ANCHOR-color); +} + +a:hover { + color: var(--MAIN-LINK-HOVER-color); +} + +#sidebar ul li.visited > a .read-icon { + color: var(--MENU-VISITED-color); +} + +#body a.highlight:after { + display: block; + content: ""; + height: 1px; + width: 0%; + -webkit-transition: width 0.5s ease; + -moz-transition: width 0.5s ease; + -ms-transition: width 0.5s ease; + transition: width 0.5s ease; + background-color: var(--MAIN-LINK-HOVER-color); +} +#sidebar { + background-color: var(--MENU-SECTIONS-BG-color); +} +#sidebar #header-wrapper { + background: var(--MENU-HEADER-BG-color); + color: var(--MENU-SEARCH-BOX-color); + border-color: var(--MENU-HEADER-BORDER-color); +} +#sidebar .searchbox { + border-color: var(--MENU-SEARCH-BOX-color); + background: var(--MENU-SEARCH-BG-color); +} +#sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { + background: var(--MENU-SECTIONS-ACTIVE-BG-color); +} +#sidebar .searchbox * { + color: var(--MENU-SEARCH-BOX-ICONS-color); +} + +#sidebar a { + color: var(--MENU-SECTIONS-LINK-color); +} + +#sidebar a:hover { + color: var(--MENU-SECTIONS-LINK-HOVER-color); +} + +#sidebar ul li.active > a { + background: var(--MENU-SECTION-ACTIVE-CATEGORY-BG-color); + color: var(--MENU-SECTION-ACTIVE-CATEGORY-color) !important; +} + +#sidebar hr { + border-color: var(--MENU-SECTION-HR-color); +} + +#body .tags a.tag-link { + background-color: var(--MENU-HEADER-BG-color); +} + +#body .tags a.tag-link:before { + border-right-color: var(--MENU-HEADER-BG-color); +} \ No newline at end of file diff --git a/workshop/themes/learn/static/css/theme.css b/workshop/themes/learn/static/css/theme.css new file mode 100644 index 0000000..4e3a7c7 --- /dev/null +++ b/workshop/themes/learn/static/css/theme.css @@ -0,0 +1,1132 @@ +@charset "UTF-8"; + +/* Tags */ +@import "tags.css"; + +#top-github-link, #body #breadcrumbs { + position: relative; + top: 50%; + -webkit-transform: translateY(-50%); + -moz-transform: translateY(-50%); + -o-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} +.button, .button-secondary { + display: inline-block; + padding: 7px 12px; +} +.button:active, .button-secondary:active { + margin: 2px 0 -2px 0; +} +@font-face { + font-family: 'Novacento Sans Wide'; + src: url("../fonts/Novecentosanswide-UltraLight-webfont.eot"); + src: url("../fonts/Novecentosanswide-UltraLight-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/Novecentosanswide-UltraLight-webfont.woff2") format("woff2"), url("../fonts/Novecentosanswide-UltraLight-webfont.woff") format("woff"), url("../fonts/Novecentosanswide-UltraLight-webfont.ttf") format("truetype"), url("../fonts/Novecentosanswide-UltraLight-webfont.svg#novecento_sans_wideultralight") format("svg"); + font-style: normal; + font-weight: 200; +} +@font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 300; + src: url("../fonts/Work_Sans_300.eot?#iefix") format("embedded-opentype"), url("../fonts/Work_Sans_300.woff") format("woff"), url("../fonts/Work_Sans_300.woff2") format("woff2"), url("../fonts/Work_Sans_300.svg#WorkSans") format("svg"), url("../fonts/Work_Sans_300.ttf") format("truetype"); +} +@font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 500; + src: url("../fonts/Work_Sans_500.eot?#iefix") format("embedded-opentype"), url("../fonts/Work_Sans_500.woff") format("woff"), url("../fonts/Work_Sans_500.woff2") format("woff2"), url("../fonts/Work_Sans_500.svg#WorkSans") format("svg"), url("../fonts/Work_Sans_500.ttf") format("truetype"); +} +body { + background: #fff; + color: #777; +} +body #chapter h1 { + font-size: 3.5rem; +} +@media only all and (min-width: 48em) and (max-width: 59.938em) { + body #chapter h1 { + font-size: 3rem; + } +} +@media only all and (max-width: 47.938em) { + body #chapter h1 { + font-size: 2rem; + } +} +a { + color: #00bdf3; +} +a:hover { + color: #0082a7; +} +pre { + position: relative; + color: #ffffff; +} +.bg { + background: #fff; + border: 1px solid #eaeaea; +} +b, strong, label, th { + font-weight: 400; +} +.default-animation, #header #logo-svg, #header #logo-svg path, #sidebar, #sidebar ul, #body, #body .padding, #body .nav { + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + transition: all 0.5s ease; +} +#grav-logo { + max-width: 60%; +} +#grav-logo path { + fill: #fff !important; +} +#sidebar { + font-weight: 300 !important; +} +fieldset { + border: 1px solid #ddd; +} +textarea, input[type="email"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="text"], input[type="url"], input[type="color"], input[type="date"], input[type="datetime"], input[type="datetime-local"], input[type="month"], input[type="time"], input[type="week"], select[multiple=multiple] { + background-color: white; + border: 1px solid #ddd; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.06); +} +textarea:hover, input[type="email"]:hover, input[type="number"]:hover, input[type="password"]:hover, input[type="search"]:hover, input[type="tel"]:hover, input[type="text"]:hover, input[type="url"]:hover, input[type="color"]:hover, input[type="date"]:hover, input[type="datetime"]:hover, input[type="datetime-local"]:hover, input[type="month"]:hover, input[type="time"]:hover, input[type="week"]:hover, select[multiple=multiple]:hover { + border-color: #c4c4c4; +} +textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { + border-color: #00bdf3; + box-shadow: inset 0 1px 3px rgba(0,0,0,.06),0 0 5px rgba(0,169,218,.7) +} +#header-wrapper { + background: #8451a1; + color: #fff; + text-align: center; + border-bottom: 4px solid #9c6fb6; + padding: 1rem; +} +#header a { + display: inline-block; +} +#header #logo-svg { + width: 8rem; + height: 2rem; +} +#header #logo-svg path { + fill: #fff; +} +.searchbox { + margin-top: 1rem; + position: relative; + border: 1px solid #915eae; + background: #764890; + border-radius: 4px; +} +.searchbox label { + color: rgba(255, 255, 255, 0.8); + position: absolute; + left: 10px; + top: 3px; +} +.searchbox span { + color: rgba(255, 255, 255, 0.6); + position: absolute; + right: 10px; + top: 3px; + cursor: pointer; +} +.searchbox span:hover { + color: rgba(255, 255, 255, 0.9); +} +.searchbox input { + display: inline-block; + color: #fff; + width: 100%; + height: 30px; + background: transparent; + border: 0; + padding: 0 25px 0 30px; + margin: 0; + font-weight: 300; +} +.searchbox input::-webkit-input-placeholder { + color: rgba(255, 255, 255, 0.6); +} +.searchbox input::-moz-placeholder { + color: rgba(255, 255, 255, 0.6); +} +.searchbox input:-moz-placeholder { + color: rgba(255, 255, 255, 0.6); +} +.searchbox input:-ms-input-placeholder { + color: rgba(255, 255, 255, 0.6); +} +#sidebar-toggle-span { + display: none; +} +@media only all and (max-width: 47.938em) { + #sidebar-toggle-span { + display: inline; + } +} +#sidebar { + background-color: #322A38; + position: fixed; + top: 0; + width: 300px; + bottom: 0; + left: 0; + font-weight: 400; + font-size: 15px; +} +#sidebar a { + color: #ccc; +} +#sidebar a:hover { + color: #e6e6e6; +} +#sidebar a.subtitle { + color: rgba(204, 204, 204, 0.6); +} +#sidebar hr { + border-bottom: 1px solid #2a232f; +} +#sidebar a.padding { + padding: 0 1rem; +} +#sidebar h5 { + margin: 2rem 0 0; + position: relative; + line-height: 2; +} +#sidebar h5 a { + display: block; + margin-left: 0; + margin-right: 0; + padding-left: 1rem; + padding-right: 1rem; +} +#sidebar h5 i { + color: rgba(204, 204, 204, 0.6); + position: absolute; + right: 0.6rem; + top: 0.7rem; + font-size: 80%; +} +#sidebar h5.parent a { + background: #201b24; + color: #d9d9d9 !important; +} +#sidebar h5.active a { + background: #fff; + color: #777 !important; +} +#sidebar h5.active i { + color: #777 !important; +} +#sidebar h5 + ul.topics { + display: none; + margin-top: 0; +} +#sidebar h5.parent + ul.topics, #sidebar h5.active + ul.topics { + display: block; +} +#sidebar ul { + list-style: none; + padding: 0; + margin: 0; +} +#sidebar ul.searched a { + color: #999999; +} +#sidebar ul.searched .search-match a { + color: #e6e6e6; +} +#sidebar ul.searched .search-match a:hover { + color: white; +} +#sidebar ul.topics { + margin: 0 1rem; +} +#sidebar ul.topics.searched ul { + display: block; +} +#sidebar ul.topics ul { + display: none; + padding-bottom: 1rem; +} +#sidebar ul.topics ul ul { + padding-bottom: 0; +} +#sidebar ul.topics li.parent > ul { + display: block; +} +#sidebar ul.topics > li > a { + line-height: 2rem; + font-size: 1.1rem; +} +#sidebar ul.topics > li > a b { + opacity: 0.5; + font-weight: normal; +} +#sidebar ul.topics > li > a .fa { + margin-top: 9px; +} +#sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { + background: #251f29; + margin-left: -1rem; + margin-right: -1rem; + padding-left: 1rem; + padding-right: 1rem; +} +#sidebar ul li.active > a { + background: #fff; + color: #777 !important; + margin-left: -1rem; + margin-right: -1rem; + padding-left: 1rem; + padding-right: 1rem; +} +#sidebar ul li { + padding: 0; +} +#sidebar ul li.visited + span { + margin-right: 16px; +} +#sidebar ul li a { + display: block; + padding: 2px 0; +} +#sidebar ul li a span { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: block; +} +#sidebar ul li > a { + padding: 4px 0; +} +#sidebar ul li.visited > a .read-icon { + color: #9c6fb6; + display: inline; +} +#sidebar ul li li { + padding-left: 1rem; + text-indent: 0.2rem; +} +#main { + background: #f7f7f7; + margin: 0 0 1.563rem 0; +} +#body { + position: relative; + margin-left: 300px; + min-height: 100%; +} +#body img, #body .video-container { + margin: 3rem auto; + display: block; + text-align: center; +} +#body img.border, #body .video-container.border { + border: 2px solid #e6e6e6 !important; + padding: 2px; +} +#body img.shadow, #body .video-container.shadow { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} +#body img.inline { + display: inline !important; + margin: 0 !important; + vertical-align: bottom; +} +#body .bordered { + border: 1px solid #ccc; +} +#body .padding { + padding: 3rem 6rem; +} +@media only all and (max-width: 59.938em) { + #body .padding { + position: static; + padding: 15px 3rem; + } +} +@media only all and (max-width: 47.938em) { + #body .padding { + padding: 5px 1rem; + } +} +#body h1 + hr { + margin-top: -1.7rem; + margin-bottom: 3rem; +} +@media only all and (max-width: 59.938em) { + #body #navigation { + position: static; + margin-right: 0 !important; + width: 100%; + display: table; + } +} +#body .nav { + position: fixed; + top: 0; + bottom: 0; + width: 4rem; + font-size: 50px; + height: 100%; + cursor: pointer; + display: table; + text-align: center; +} +#body .nav > i { + display: table-cell; + vertical-align: middle; + text-align: center; +} +@media only all and (max-width: 59.938em) { + #body .nav { + display: table-cell; + position: static; + top: auto; + width: 50%; + text-align: center; + height: 100px; + line-height: 100px; + padding-top: 0; + } + #body .nav > i { + display: inline-block; + } +} +#body .nav:hover { + background: #F6F6F6; +} +#body .nav.nav-pref { + left: 0; +} +#body .nav.nav-next { + right: 0; +} +#body-inner { + margin-bottom: 5rem; +} +#chapter { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 2rem 0; +} +#chapter #body-inner { + padding-bottom: 3rem; + max-width: 80%; +} +#chapter h3 { + font-family: "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + font-weight: 300; + text-align: center; +} +#chapter h1 { + font-size: 5rem; + border-bottom: 4px solid #F0F2F4; +} +#chapter p { + /* text-align: justify; */ + font-size: 1.2rem; +} +#footer { + padding: 3rem 1rem; + color: #b3b3b3; + font-size: 13px; +} +#footer p { + margin: 0; +} +body { + font-family: "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + font-weight: 300; + line-height: 1.6; + font-size: 18px !important; +} +h2, h3, h4, h5, h6 { + font-family: "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + text-rendering: optimizeLegibility; + color: #5e5e5e; + font-weight: 400; + letter-spacing: -1px; +} +h1 { + font-family: "Novacento Sans Wide", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + text-align: center; + /* text-transform: uppercase; */ + color: #222; + font-weight: 200; +} +blockquote { + border-left: 10px solid #F0F2F4; +} +blockquote p { + font-size: 1.1rem; + color: #999; +} +blockquote cite { + display: block; + text-align: right; + color: #666; + font-size: 1.2rem; +} +div.notices { + margin: 2rem 0; + position: relative; +} +div.notices p { + padding: 15px; + display: block; + font-size: 1rem; + margin-top: 0rem; + margin-bottom: 0rem; + color: #666; +} +div.notices p:first-child:before { + position: absolute; + top: 2px; + color: #fff; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + content: "\f06a"; + left: 10px; +} +div.notices p:first-child:after { + position: absolute; + top: 2px; + color: #fff; + left: 2rem; +} +div.notices.info p { + border-top: 30px solid #F0B37E; + background: #FFF2DB; +} +div.notices.info p:first-child:after { + content: 'Info'; +} +div.notices.warning p { + border-top: 30px solid rgba(217, 83, 79, 0.8); + background: #FAE2E2; +} +div.notices.warning p:first-child:after { + content: 'Warning'; +} +div.notices.note p { + border-top: 30px solid #6AB0DE; + background: #E7F2FA; +} +div.notices.note p:first-child:after { + content: 'Note'; +} +div.notices.tip p { + border-top: 30px solid rgba(92, 184, 92, 0.8); + background: #E6F9E6; +} +div.notices.tip p:first-child:after { + content: 'Tip'; +} + +/* attachments shortcode */ + +section.attachments { + margin: 2rem 0; + position: relative; +} + +section.attachments label { + font-weight: 400; + padding-left: 0.5em; + padding-top: 0.2em; + padding-bottom: 0.2em; + margin: 0; +} + +section.attachments .attachments-files { + padding: 15px; + display: block; + font-size: 1rem; + margin-top: 0rem; + margin-bottom: 0rem; + color: #666; +} + +section.attachments.orange label { + color: #fff; + background: #F0B37E; +} + +section.attachments.orange .attachments-files { + background: #FFF2DB; +} + +section.attachments.green label { + color: #fff; + background: rgba(92, 184, 92, 0.8); +} + +section.attachments.green .attachments-files { + background: #E6F9E6; +} + +section.attachments.blue label { + color: #fff; + background: #6AB0DE; +} + +section.attachments.blue .attachments-files { + background: #E7F2FA; +} + +section.attachments.grey label { + color: #fff; + background: #505d65; +} + +section.attachments.grey .attachments-files { + background: #f4f4f4; +} + +/* Children shortcode */ + +/* Children shortcode */ +.children p { + font-size: small; + margin-top: 0px; + padding-top: 0px; + margin-bottom: 0px; + padding-bottom: 0px; +} +.children-li p { + font-size: small; + font-style: italic; + +} +.children-h2 p, .children-h3 p { + font-size: small; + margin-top: 0px; + padding-top: 0px; + margin-bottom: 0px; + padding-bottom: 0px; +} +.children h3,.children h2 { + margin-bottom: 0px; + margin-top: 5px; +} + +code, kbd, pre, samp { + font-family: "Consolas", menlo, monospace; + font-size: 92%; +} +code { + border-radius: 2px; + white-space: nowrap; + color: #5e5e5e; + background: #FFF7DD; + border: 1px solid #fbf0cb; + padding: 0px 2px; +} +code + .copy-to-clipboard { + margin-left: -1px; + border-left: 0 !important; + font-size: inherit !important; + vertical-align: middle; + height: 21px; + top: 0; +} +pre { + padding: 1rem; + margin: 2rem 0; + background: #282c34; + border: 0; + border-radius: 2px; + line-height: 1.15; +} +pre code { + color: whitesmoke; + background: inherit; + white-space: inherit; + border: 0; + padding: 0; + margin: 0; + font-size: 15px; +} +hr { + border-bottom: 4px solid #F0F2F4; +} +.page-title { + margin-top: -25px; + padding: 25px; + float: left; + clear: both; + background: #9c6fb6; + color: #fff; +} +#body a.anchor-link { + color: #ccc; +} +#body a.anchor-link:hover { + color: #9c6fb6; +} +#body-inner .tabs-wrapper.ui-theme-badges { + background: #1d1f21; +} +#body-inner .tabs-wrapper.ui-theme-badges .tabs-nav li { + font-size: 0.9rem; + text-transform: uppercase; +} +#body-inner .tabs-wrapper.ui-theme-badges .tabs-nav li a { + background: #35393c; +} +#body-inner .tabs-wrapper.ui-theme-badges .tabs-nav li.current a { + background: #4d5257; +} +#body-inner pre { + white-space: pre-wrap; +} +.tabs-wrapper pre { + margin: 1rem 0; + border: 0; + padding: 0; + background: inherit; +} +table { + border: 1px solid #eaeaea; + table-layout: auto; +} +th { + background: #f7f7f7; + padding: 0.5rem; +} +td { + padding: 0.5rem; + border: 1px solid #eaeaea; +} +.button { + background: #9c6fb6; + color: #fff; + box-shadow: 0 3px 0 #00a5d4; +} +.button:hover { + background: #00a5d4; + box-shadow: 0 3px 0 #008db6; + color: #fff; +} +.button:active { + box-shadow: 0 1px 0 #008db6; +} +.button-secondary { + background: #F8B450; + color: #fff; + box-shadow: 0 3px 0 #f7a733; +} +.button-secondary:hover { + background: #f7a733; + box-shadow: 0 3px 0 #f69b15; + color: #fff; +} +.button-secondary:active { + box-shadow: 0 1px 0 #f69b15; +} +.bullets { + margin: 1.7rem 0; + margin-left: -0.85rem; + margin-right: -0.85rem; + overflow: auto; +} +.bullet { + float: left; + padding: 0 0.85rem; +} +.two-column-bullet { + width: 50%; +} +@media only all and (max-width: 47.938em) { + .two-column-bullet { + width: 100%; + } +} +.three-column-bullet { + width: 33.33333%; +} +@media only all and (max-width: 47.938em) { + .three-column-bullet { + width: 100%; + } +} +.four-column-bullet { + width: 25%; +} +@media only all and (max-width: 47.938em) { + .four-column-bullet { + width: 100%; + } +} +.bullet-icon { + float: left; + background: #9c6fb6; + padding: 0.875rem; + width: 3.5rem; + height: 3.5rem; + border-radius: 50%; + color: #fff; + font-size: 1.75rem; + text-align: center; +} +.bullet-icon-1 { + background: #9c6fb6; +} +.bullet-icon-2 { + background: #00f3d8; +} +.bullet-icon-3 { + background: #e6f300; +} +.bullet-content { + margin-left: 4.55rem; +} +.tooltipped { + position: relative; +} +.tooltipped:after { + position: absolute; + z-index: 1000000; + display: none; + padding: 5px 8px; + font: normal normal 11px/1.5 "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + color: #fff; + text-align: center; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-wrap: break-word; + white-space: pre; + pointer-events: none; + content: attr(aria-label); + background: rgba(0, 0, 0, 0.8); + border-radius: 3px; + -webkit-font-smoothing: subpixel-antialiased; +} +.tooltipped:before { + position: absolute; + z-index: 1000001; + display: none; + width: 0; + height: 0; + color: rgba(0, 0, 0, 0.8); + pointer-events: none; + content: ""; + border: 5px solid transparent; +} +.tooltipped:hover:before, .tooltipped:hover:after, .tooltipped:active:before, .tooltipped:active:after, .tooltipped:focus:before, .tooltipped:focus:after { + display: inline-block; + text-decoration: none; +} +.tooltipped-s:after, .tooltipped-se:after, .tooltipped-sw:after { + top: 100%; + right: 50%; + margin-top: 5px; +} +.tooltipped-s:before, .tooltipped-se:before, .tooltipped-sw:before { + top: auto; + right: 50%; + bottom: -5px; + margin-right: -5px; + border-bottom-color: rgba(0, 0, 0, 0.8); +} +.tooltipped-se:after { + right: auto; + left: 50%; + margin-left: -15px; +} +.tooltipped-sw:after { + margin-right: -15px; +} +.tooltipped-n:after, .tooltipped-ne:after, .tooltipped-nw:after { + right: 50%; + bottom: 100%; + margin-bottom: 5px; +} +.tooltipped-n:before, .tooltipped-ne:before, .tooltipped-nw:before { + top: -5px; + right: 50%; + bottom: auto; + margin-right: -5px; + border-top-color: rgba(0, 0, 0, 0.8); +} +.tooltipped-ne:after { + right: auto; + left: 50%; + margin-left: -15px; +} +.tooltipped-nw:after { + margin-right: -15px; +} +.tooltipped-s:after, .tooltipped-n:after { + transform: translateX(50%); +} +.tooltipped-w:after { + right: 100%; + bottom: 50%; + margin-right: 5px; + transform: translateY(50%); +} +.tooltipped-w:before { + top: 50%; + bottom: 50%; + left: -5px; + margin-top: -5px; + border-left-color: rgba(0, 0, 0, 0.8); +} +.tooltipped-e:after { + bottom: 50%; + left: 100%; + margin-left: 5px; + transform: translateY(50%); +} +.tooltipped-e:before { + top: 50%; + right: -5px; + bottom: 50%; + margin-top: -5px; + border-right-color: rgba(0, 0, 0, 0.8); +} +.highlightable { + padding: 1rem 0 1rem; + overflow: auto; + position: relative; +} +.hljs::selection, .hljs span::selection { + background: #b7b7b7; +} +.lightbox-active #body { + overflow: visible; +} +.lightbox-active #body .padding { + overflow: visible; +} +#github-contrib i { + vertical-align: middle; +} +.featherlight img { + margin: 0 !important; +} +.lifecycle #body-inner ul { + list-style: none; + margin: 0; + padding: 2rem 0 0; + position: relative; +} +.lifecycle #body-inner ol { + margin: 1rem 0 1rem 0; + padding: 2rem; + position: relative; +} +.lifecycle #body-inner ol li { + margin-left: 1rem; +} +.lifecycle #body-inner ol strong, .lifecycle #body-inner ol label, .lifecycle #body-inner ol th { + text-decoration: underline; +} +.lifecycle #body-inner ol ol { + margin-left: -1rem; +} +.lifecycle #body-inner h3[class*='level'] { + font-size: 20px; + position: absolute; + margin: 0; + padding: 4px 10px; + right: 0; + z-index: 1000; + color: #fff; + background: #1ABC9C; +} +.lifecycle #body-inner ol h3 { + margin-top: 1rem !important; + right: 2rem !important; +} +.lifecycle #body-inner .level-1 + ol { + background: #f6fefc; + border: 4px solid #1ABC9C; + color: #16A085; +} +.lifecycle #body-inner .level-1 + ol h3 { + background: #2ECC71; +} +.lifecycle #body-inner .level-2 + ol { + background: #f7fdf9; + border: 4px solid #2ECC71; + color: #27AE60; +} +.lifecycle #body-inner .level-2 + ol h3 { + background: #3498DB; +} +.lifecycle #body-inner .level-3 + ol { + background: #f3f9fd; + border: 4px solid #3498DB; + color: #2980B9; +} +.lifecycle #body-inner .level-3 + ol h3 { + background: #34495E; +} +.lifecycle #body-inner .level-4 + ol { + background: #e4eaf0; + border: 4px solid #34495E; + color: #2C3E50; +} +.lifecycle #body-inner .level-4 + ol h3 { + background: #34495E; +} +#top-bar { + background: #F6F6F6; + border-radius: 2px; + padding: 0 1rem; + height: 0; + min-height: 3rem; +} +#top-github-link { + position: relative; + z-index: 1; + float: right; + display: block; +} +#body #breadcrumbs { + height: auto; + margin-bottom: 0; + padding-left: 0; + line-height: 1.4; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 70%; + display: inline-block; + float: left; +} +#body #breadcrumbs span { + padding: 0 0.1rem; +} +@media only all and (max-width: 59.938em) { + #sidebar { + width: 230px; + } + #body { + margin-left: 230px; + } +} +@media only all and (max-width: 47.938em) { + #sidebar { + width: 230px; + left: -230px; + } + #body { + margin-left: 0; + width: 100%; + } + .sidebar-hidden { + overflow: hidden; + } + .sidebar-hidden #sidebar { + left: 0; + } + .sidebar-hidden #body { + margin-left: 230px; + overflow: hidden; + } + .sidebar-hidden #overlay { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 10; + background: rgba(255, 255, 255, 0.5); + cursor: pointer; + } +} +.copy-to-clipboard { + background-image: url(../images/clippy.svg); + background-position: 50% 50%; + background-size: 16px 16px; + background-repeat: no-repeat; + width: 27px; + height: 1.45rem; + top: -1px; + display: inline-block; + vertical-align: middle; + position: relative; + color: #5e5e5e; + background-color: #FFF7DD; + margin-left: -.2rem; + cursor: pointer; + border-radius: 0 2px 2px 0; + margin-bottom: 1px; +} +.copy-to-clipboard:hover { + background-color: #E8E2CD; +} +pre .copy-to-clipboard { + position: absolute; + right: 4px; + top: 4px; + background-color: #949bab; + color: #ccc; + border-radius: 2px; +} +pre .copy-to-clipboard:hover { + background-color: #656c72; + color: #fff; +} +.parent-element { + -webkit-transform-style: preserve-3d; + -moz-transform-style: preserve-3d; + transform-style: preserve-3d; +} + +#sidebar ul.topics > li > a .read-icon { + margin-top: 9px; +} + +#sidebar ul { + list-style: none; + padding: 0; + margin: 0; +} + +#sidebar #shortcuts li { + padding: 2px 0; + list-style: none; +} + +#sidebar ul li .read-icon { + display: none; + float: right; + font-size: 13px; + min-width: 16px; + margin: 4px 0 0 0; + text-align: right; +} +#sidebar ul li.visited > a .read-icon { + color: #00bdf3; + display: inline; +} + +#sidebar #shortcuts h3 { + font-family: "Novacento Sans Wide", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + color: white ; + margin-top:1rem; + padding-left: 1rem; +} + +#searchResults { + text-align: left; +} diff --git a/workshop/themes/learn/static/fonts/Inconsolata.eot b/workshop/themes/learn/static/fonts/Inconsolata.eot new file mode 100644 index 0000000..0a705d6 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Inconsolata.eot differ diff --git a/workshop/themes/learn/static/fonts/Inconsolata.svg b/workshop/themes/learn/static/fonts/Inconsolata.svg new file mode 100644 index 0000000..b7f97c8 --- /dev/null +++ b/workshop/themes/learn/static/fonts/Inconsolata.svg @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/workshop/themes/learn/static/fonts/Inconsolata.ttf b/workshop/themes/learn/static/fonts/Inconsolata.ttf new file mode 100644 index 0000000..4b8a36d Binary files /dev/null and b/workshop/themes/learn/static/fonts/Inconsolata.ttf differ diff --git a/workshop/themes/learn/static/fonts/Inconsolata.woff b/workshop/themes/learn/static/fonts/Inconsolata.woff new file mode 100644 index 0000000..6f39625 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Inconsolata.woff differ diff --git a/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.eot b/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.eot new file mode 100644 index 0000000..9984682 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.eot differ diff --git a/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.svg b/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.svg new file mode 100644 index 0000000..c412ea8 --- /dev/null +++ b/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.svg @@ -0,0 +1,1019 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.ttf b/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.ttf new file mode 100644 index 0000000..8cfb62d Binary files /dev/null and b/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.ttf differ diff --git a/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.woff b/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.woff new file mode 100644 index 0000000..d5c4290 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.woff differ diff --git a/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.woff2 b/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.woff2 new file mode 100644 index 0000000..eefb4a3 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Novecentosanswide-Normal-webfont.woff2 differ diff --git a/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.eot b/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.eot new file mode 100644 index 0000000..2a26561 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.eot differ diff --git a/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.svg b/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.svg new file mode 100644 index 0000000..e642ab0 --- /dev/null +++ b/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.svg @@ -0,0 +1,918 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.ttf b/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.ttf new file mode 100644 index 0000000..9ce9c7f Binary files /dev/null and b/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.ttf differ diff --git a/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.woff b/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.woff new file mode 100644 index 0000000..381650c Binary files /dev/null and b/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.woff differ diff --git a/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.woff2 b/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.woff2 new file mode 100644 index 0000000..7e65954 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Novecentosanswide-UltraLight-webfont.woff2 differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_200.eot b/workshop/themes/learn/static/fonts/Work_Sans_200.eot new file mode 100644 index 0000000..4052e4f Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_200.eot differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_200.svg b/workshop/themes/learn/static/fonts/Work_Sans_200.svg new file mode 100644 index 0000000..58ab4ba --- /dev/null +++ b/workshop/themes/learn/static/fonts/Work_Sans_200.svg @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/workshop/themes/learn/static/fonts/Work_Sans_200.ttf b/workshop/themes/learn/static/fonts/Work_Sans_200.ttf new file mode 100644 index 0000000..68019e1 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_200.ttf differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_200.woff b/workshop/themes/learn/static/fonts/Work_Sans_200.woff new file mode 100644 index 0000000..a1bd9e4 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_200.woff differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_200.woff2 b/workshop/themes/learn/static/fonts/Work_Sans_200.woff2 new file mode 100644 index 0000000..20c68a7 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_200.woff2 differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_300.eot b/workshop/themes/learn/static/fonts/Work_Sans_300.eot new file mode 100644 index 0000000..ace7993 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_300.eot differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_300.svg b/workshop/themes/learn/static/fonts/Work_Sans_300.svg new file mode 100644 index 0000000..f29d0c8 --- /dev/null +++ b/workshop/themes/learn/static/fonts/Work_Sans_300.svg @@ -0,0 +1,331 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/workshop/themes/learn/static/fonts/Work_Sans_300.ttf b/workshop/themes/learn/static/fonts/Work_Sans_300.ttf new file mode 100644 index 0000000..35387c2 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_300.ttf differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_300.woff b/workshop/themes/learn/static/fonts/Work_Sans_300.woff new file mode 100644 index 0000000..8d789ea Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_300.woff differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_300.woff2 b/workshop/themes/learn/static/fonts/Work_Sans_300.woff2 new file mode 100644 index 0000000..f6e216d Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_300.woff2 differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_500.eot b/workshop/themes/learn/static/fonts/Work_Sans_500.eot new file mode 100644 index 0000000..9df6929 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_500.eot differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_500.svg b/workshop/themes/learn/static/fonts/Work_Sans_500.svg new file mode 100644 index 0000000..4b030b7 --- /dev/null +++ b/workshop/themes/learn/static/fonts/Work_Sans_500.svg @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/workshop/themes/learn/static/fonts/Work_Sans_500.ttf b/workshop/themes/learn/static/fonts/Work_Sans_500.ttf new file mode 100644 index 0000000..5b8cc53 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_500.ttf differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_500.woff b/workshop/themes/learn/static/fonts/Work_Sans_500.woff new file mode 100644 index 0000000..df05851 Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_500.woff differ diff --git a/workshop/themes/learn/static/fonts/Work_Sans_500.woff2 b/workshop/themes/learn/static/fonts/Work_Sans_500.woff2 new file mode 100644 index 0000000..b06c54d Binary files /dev/null and b/workshop/themes/learn/static/fonts/Work_Sans_500.woff2 differ diff --git a/workshop/themes/learn/static/images/clippy.svg b/workshop/themes/learn/static/images/clippy.svg new file mode 100644 index 0000000..e786900 --- /dev/null +++ b/workshop/themes/learn/static/images/clippy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/workshop/themes/learn/static/images/logo.png b/workshop/themes/learn/static/images/logo.png new file mode 100644 index 0000000..7de3126 Binary files /dev/null and b/workshop/themes/learn/static/images/logo.png differ diff --git a/workshop/themes/learn/static/js/auto-complete.js b/workshop/themes/learn/static/js/auto-complete.js new file mode 100644 index 0000000..7fbde99 --- /dev/null +++ b/workshop/themes/learn/static/js/auto-complete.js @@ -0,0 +1,223 @@ +/* + JavaScript autoComplete v1.0.4 + Copyright (c) 2014 Simon Steinberger / Pixabay + GitHub: https://github.com/Pixabay/JavaScript-autoComplete + License: http://www.opensource.org/licenses/mit-license.php +*/ + +var autoComplete = (function(){ + // "use strict"; + function autoComplete(options){ + if (!document.querySelector) return; + + // helpers + function hasClass(el, className){ return el.classList ? el.classList.contains(className) : new RegExp('\\b'+ className+'\\b').test(el.className); } + + function addEvent(el, type, handler){ + if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler); + } + function removeEvent(el, type, handler){ + // if (el.removeEventListener) not working in IE11 + if (el.detachEvent) el.detachEvent('on'+type, handler); else el.removeEventListener(type, handler); + } + function live(elClass, event, cb, context){ + addEvent(context || document, event, function(e){ + var found, el = e.target || e.srcElement; + while (el && !(found = hasClass(el, elClass))) el = el.parentElement; + if (found) cb.call(el, e); + }); + } + + var o = { + selector: 0, + source: 0, + minChars: 3, + delay: 150, + offsetLeft: 0, + offsetTop: 1, + cache: 1, + menuClass: '', + renderItem: function (item, search){ + // escape special characters + search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + var re = new RegExp("(" + search.split(' ').join('|') + ")", "gi"); + return '
    ' + item.replace(re, "$1") + '
    '; + }, + onSelect: function(e, term, item){} + }; + for (var k in options) { if (options.hasOwnProperty(k)) o[k] = options[k]; } + + // init + var elems = typeof o.selector == 'object' ? [o.selector] : document.querySelectorAll(o.selector); + for (var i=0; i 0) + that.sc.scrollTop = selTop + that.sc.suggestionHeight + scrTop - that.sc.maxHeight; + else if (selTop < 0) + that.sc.scrollTop = selTop + scrTop; + } + } + } + addEvent(window, 'resize', that.updateSC); + document.body.appendChild(that.sc); + + live('autocomplete-suggestion', 'mouseleave', function(e){ + var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); + if (sel) setTimeout(function(){ sel.className = sel.className.replace('selected', ''); }, 20); + }, that.sc); + + live('autocomplete-suggestion', 'mouseover', function(e){ + var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); + if (sel) sel.className = sel.className.replace('selected', ''); + this.className += ' selected'; + }, that.sc); + + live('autocomplete-suggestion', 'mousedown', function(e){ + if (hasClass(this, 'autocomplete-suggestion')) { // else outside click + var v = this.getAttribute('data-val'); + that.value = v; + o.onSelect(e, v, this); + that.sc.style.display = 'none'; + } + }, that.sc); + + that.blurHandler = function(){ + try { var over_sb = document.querySelector('.autocomplete-suggestions:hover'); } catch(e){ var over_sb = 0; } + if (!over_sb) { + that.last_val = that.value; + that.sc.style.display = 'none'; + setTimeout(function(){ that.sc.style.display = 'none'; }, 350); // hide suggestions on fast input + } else if (that !== document.activeElement) setTimeout(function(){ that.focus(); }, 20); + }; + addEvent(that, 'blur', that.blurHandler); + + var suggest = function(data){ + var val = that.value; + that.cache[val] = data; + if (data.length && val.length >= o.minChars) { + var s = ''; + for (var i=0;i 40) && key != 13 && key != 27) { + var val = that.value; + if (val.length >= o.minChars) { + if (val != that.last_val) { + that.last_val = val; + clearTimeout(that.timer); + if (o.cache) { + if (val in that.cache) { suggest(that.cache[val]); return; } + // no requests if previous suggestions were empty + for (var i=1; i https://github.com/noelboss/featherlight/issues/317 +!function(u){"use strict";if(void 0!==u)if(u.fn.jquery.match(/-ajax/))"console"in window&&window.console.info("Featherlight needs regular jQuery, not the slim version.");else{var r=[],i=function(t){return r=u.grep(r,function(e){return e!==t&&0','
    ','",'
    '+n.loading+"
    ","
    ","
    "].join("")),o="."+n.namespace+"-close"+(n.otherClose?","+n.otherClose:"");return n.$instance=i.clone().addClass(n.variant),n.$instance.on(n.closeTrigger+"."+n.namespace,function(e){if(!e.isDefaultPrevented()){var t=u(e.target);("background"===n.closeOnClick&&t.is("."+n.namespace)||"anywhere"===n.closeOnClick||t.closest(o).length)&&(n.close(e),e.preventDefault())}}),this},getContent:function(){if(!1!==this.persist&&this.$content)return this.$content;var t=this,e=this.constructor.contentFilters,n=function(e){return t.$currentTarget&&t.$currentTarget.attr(e)},r=n(t.targetAttr),i=t.target||r||"",o=e[t.type];if(!o&&i in e&&(o=e[i],i=t.target&&r),i=i||n("href")||"",!o)for(var a in e)t[a]&&(o=e[a],i=t[a]);if(!o){var s=i;if(i=null,u.each(t.contentFilters,function(){return(o=e[this]).test&&(i=o.test(s)),!i&&o.regex&&s.match&&s.match(o.regex)&&(i=s),!i}),!i)return"console"in window&&window.console.error("Featherlight: no content filter found "+(s?' for "'+s+'"':" (no target specified)")),!1}return o.process.call(t,i)},setContent:function(e){return this.$instance.removeClass(this.namespace+"-loading"),this.$instance.toggleClass(this.namespace+"-iframe",e.is("iframe")),this.$instance.find("."+this.namespace+"-inner").not(e).slice(1).remove().end().replaceWith(u.contains(this.$instance[0],e[0])?"":e),this.$content=e.addClass(this.namespace+"-inner"),this},open:function(t){var n=this;if(n.$instance.hide().appendTo(n.root),!(t&&t.isDefaultPrevented()||!1===n.beforeOpen(t))){t&&t.preventDefault();var e=n.getContent();if(e)return r.push(n),s(!0),n.$instance.fadeIn(n.openSpeed),n.beforeContent(t),u.when(e).always(function(e){n.setContent(e),n.afterContent(t)}).then(n.$instance.promise()).done(function(){n.afterOpen(t)})}return n.$instance.detach(),u.Deferred().reject().promise()},close:function(e){var t=this,n=u.Deferred();return!1===t.beforeClose(e)?n.reject():(0===i(t).length&&s(!1),t.$instance.fadeOut(t.closeSpeed,function(){t.$instance.detach(),t.afterClose(e),n.resolve()})),n.promise()},resize:function(e,t){if(e&&t&&(this.$content.css("width","").css("height",""),this.$content.parent().width()');return n.onload=function(){r.naturalWidth=n.width,r.naturalHeight=n.height,t.resolve(r)},n.onerror=function(){t.reject(r)},n.src=e,t.promise()}},html:{regex:/^\s*<[\w!][^<]*>/,process:function(e){return u(e)}},ajax:{regex:/./,process:function(e){var n=u.Deferred(),r=u("
    ").load(e,function(e,t){"error"!==t&&n.resolve(r.contents()),n.fail()});return n.promise()}},iframe:{process:function(e){var t=new u.Deferred,n=u("