From 34cf3ec28595517b6d01ca7ccab574c119fa025d Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 30 Apr 2025 16:33:57 +0300 Subject: [PATCH] Initial commit. --- .github/ISSUE_TEMPLATE/01_compile_bug.yml | 101 ++ .github/ISSUE_TEMPLATE/02_runtime_bug.yml | 101 ++ .github/ISSUE_TEMPLATE/03_feature_request.yml | 31 + .github/ISSUE_TEMPLATE/04_other_issue.yml | 23 + .github/ISSUE_TEMPLATE/config.yml | 11 + .github/workflows/issue_comment.yml | 20 + .github/workflows/launchpad.yml | 138 +++ .github/workflows/new_issues.yml | 20 + .github/workflows/new_prs.yml | 25 + .github/workflows/upload_components.yml | 20 + .gitignore | 94 ++ .gitmodules | 3 + CHANGES.md | 353 ++++++ LICENSE | 201 ++++ README.md | 65 + components/esp_rainmaker/CMakeLists.txt | 94 ++ components/esp_rainmaker/Kconfig.projbuild | 396 ++++++ components/esp_rainmaker/LICENSE | 201 ++++ components/esp_rainmaker/README.md | 5 + components/esp_rainmaker/component.mk | 12 + components/esp_rainmaker/idf_component.yml | 33 + .../include/esp_rmaker_console.h | 52 + .../esp_rainmaker/include/esp_rmaker_core.h | 1068 +++++++++++++++++ .../esp_rainmaker/include/esp_rmaker_mqtt.h | 125 ++ .../esp_rainmaker/include/esp_rmaker_ota.h | 272 +++++ .../esp_rainmaker/include/esp_rmaker_scenes.h | 38 + .../include/esp_rmaker_schedule.h | 38 + .../include/esp_rmaker_standard_devices.h | 96 ++ .../include/esp_rmaker_standard_params.h | 352 ++++++ .../include/esp_rmaker_standard_services.h | 124 ++ .../include/esp_rmaker_standard_types.h | 102 ++ .../include/esp_rmaker_thread_br.h | 44 + .../include/esp_rmaker_user_mapping.h | 84 ++ components/esp_rainmaker/sdkconfig.rename | 4 + .../rmaker_claim_service_server.crt | 20 + .../server_certs/rmaker_mqtt_server.crt | 20 + .../server_certs/rmaker_ota_server.crt | 54 + .../src/console/esp_rmaker_commands.c | 206 ++++ .../src/console/esp_rmaker_console.c | 24 + .../src/console/esp_rmaker_console_internal.h | 17 + .../esp_rainmaker/src/core/esp_rmaker_claim.c | 1041 ++++++++++++++++ .../esp_rainmaker/src/core/esp_rmaker_claim.h | 49 + .../src/core/esp_rmaker_claim.pb-c.c | 398 ++++++ .../src/core/esp_rmaker_claim.pb-c.h | 175 +++ .../src/core/esp_rmaker_claim.proto | 41 + .../src/core/esp_rmaker_client_data.c | 178 +++ .../src/core/esp_rmaker_client_data.h | 31 + .../src/core/esp_rmaker_cmd_resp_manager.c | 150 +++ .../esp_rainmaker/src/core/esp_rmaker_core.c | 714 +++++++++++ .../src/core/esp_rmaker_device.c | 379 ++++++ .../src/core/esp_rmaker_internal.h | 129 ++ .../src/core/esp_rmaker_local_ctrl.c | 650 ++++++++++ .../src/core/esp_rmaker_mqtt_topics.h | 35 + .../esp_rainmaker/src/core/esp_rmaker_node.c | 375 ++++++ .../src/core/esp_rmaker_node_auth.c | 191 +++ .../src/core/esp_rmaker_node_config.c | 311 +++++ .../esp_rainmaker/src/core/esp_rmaker_param.c | 1001 +++++++++++++++ .../src/core/esp_rmaker_scenes.c | 548 +++++++++ .../src/core/esp_rmaker_schedule.c | 983 +++++++++++++++ .../src/core/esp_rmaker_secure_boot_digest.c | 104 ++ .../src/core/esp_rmaker_secure_boot_digest.h | 38 + .../src/core/esp_rmaker_system_service.c | 90 ++ .../src/core/esp_rmaker_time_service.c | 82 ++ .../src/core/esp_rmaker_user_mapping.c | 414 +++++++ .../src/core/esp_rmaker_user_mapping.pb-c.c | 369 ++++++ .../src/core/esp_rmaker_user_mapping.pb-c.h | 166 +++ .../src/core/esp_rmaker_user_mapping.proto | 31 + .../esp_rainmaker/src/mqtt/esp_rmaker_mqtt.c | 135 +++ .../src/mqtt/esp_rmaker_mqtt_budget.c | 183 +++ .../src/mqtt/esp_rmaker_mqtt_budget.h | 17 + .../esp_rainmaker/src/ota/esp_rmaker_ota.c | 726 +++++++++++ .../src/ota/esp_rmaker_ota_internal.h | 55 + .../src/ota/esp_rmaker_ota_using_params.c | 114 ++ .../src/ota/esp_rmaker_ota_using_topics.c | 346 ++++++ .../esp_rmaker_standard_devices.c | 69 ++ .../esp_rmaker_standard_params.c | 208 ++++ .../esp_rmaker_standard_services.c | 79 ++ .../src/thread_br/esp_rmaker_thread_br.c | 196 +++ .../thread_br/esp_rmaker_thread_br_internal.c | 185 +++ .../thread_br/esp_rmaker_thread_br_launcher.c | 191 +++ .../src/thread_br/esp_rmaker_thread_br_priv.h | 67 ++ .../thread_br/esp_rmaker_thread_br_service.c | 74 ++ components/esp_schedule/CMakeLists.txt | 10 + components/esp_schedule/LICENSE | 201 ++++ components/esp_schedule/README.md | 50 + components/esp_schedule/component.mk | 5 + components/esp_schedule/idf_component.yml | 9 + .../esp_schedule/include/esp_schedule.h | 236 ++++ components/esp_schedule/src/esp_schedule.c | 600 +++++++++ .../esp_schedule/src/esp_schedule_internal.h | 28 + .../esp_schedule/src/esp_schedule_nvs.c | 296 +++++ .../.github/workflows/upload_components.yml | 21 + components/rmaker_common/CMakeLists.txt | 23 + components/rmaker_common/Kconfig | 181 +++ components/rmaker_common/LICENSE | 201 ++++ components/rmaker_common/README.md | 9 + components/rmaker_common/component.mk | 5 + components/rmaker_common/idf_component.yml | 3 + .../include/esp_rmaker_cmd_resp.h | 184 +++ .../include/esp_rmaker_common_console.h | 47 + .../include/esp_rmaker_common_events.h | 60 + .../include/esp_rmaker_factory.h | 73 ++ .../include/esp_rmaker_mqtt_glue.h | 177 +++ .../rmaker_common/include/esp_rmaker_utils.h | 211 ++++ .../include/esp_rmaker_work_queue.h | 82 ++ components/rmaker_common/src/cmd_resp.c | 429 +++++++ .../src/console/rmaker_common_cmds.c | 359 ++++++ .../src/console/rmaker_console.c | 117 ++ .../src/console/rmaker_console_internal.h | 9 + .../src/create_APN3_PPI_string.c | 246 ++++ .../src/esp-mqtt/esp-mqtt-glue.c | 477 ++++++++ components/rmaker_common/src/factory.c | 120 ++ components/rmaker_common/src/time.c | 277 +++++ components/rmaker_common/src/timezone.c | 516 ++++++++ components/rmaker_common/src/utils.c | 174 +++ components/rmaker_common/src/work_queue.c | 148 +++ docs/Doxyfile | 77 ++ docs/Makefile | 20 + docs/README.md | 15 + docs/_static/esp-rainmaker-logo.png | Bin 0 -> 68568 bytes docs/_static/theme_overrides.css | 54 + docs/conf_common.py | 37 + docs/en/c-api-reference/index.rst | 12 + docs/en/c-api-reference/rainmaker_common.rst | 18 + docs/en/c-api-reference/rainmaker_console.rst | 3 + docs/en/c-api-reference/rainmaker_core.rst | 18 + docs/en/c-api-reference/rainmaker_mqtt.rst | 3 + docs/en/c-api-reference/rainmaker_ota.rst | 3 + .../rainmaker_standard_types.rst | 18 + docs/en/conf.py | 31 + docs/en/index.rst | 12 + docs/local_util.py | 77 ++ docs/make.bat | 35 + docs/requirements.txt | 4 + docs/utils.sh | 18 + examples/common/app_insights/CMakeLists.txt | 3 + examples/common/app_insights/Kconfig | 11 + examples/common/app_insights/app_insights.c | 122 ++ examples/common/app_insights/app_insights.h | 25 + examples/common/app_insights/component.mk | 2 + .../common/app_insights/idf_component.yml | 7 + examples/common/app_network/CMakeLists.txt | 14 + examples/common/app_network/Kconfig.projbuild | 85 ++ examples/common/app_network/app_network.c | 495 ++++++++ examples/common/app_network/app_network.h | 120 ++ .../common/app_network/app_thread_internal.c | 220 ++++ examples/common/app_network/app_wifi.h | 32 + .../common/app_network/app_wifi_internal.c | 334 ++++++ examples/common/app_network/component.mk | 5 + examples/common/app_network/idf_component.yml | 4 + .../private_include/app_thread_internal.h | 75 ++ .../private_include/app_wifi_internal.h | 56 + examples/common/app_network/sdkconfig.rename | 13 + examples/common/app_reset/CMakeLists.txt | 3 + examples/common/app_reset/app_reset.c | 66 + examples/common/app_reset/app_reset.h | 50 + examples/common/app_reset/component.mk | 2 + examples/common/gpio_button/CMakeLists.txt | 3 + examples/common/gpio_button/Kconfig | 6 + examples/common/gpio_button/button/button.c | 353 ++++++ .../common/gpio_button/button/button_obj.cpp | 54 + .../gpio_button/button/include/iot_button.h | 272 +++++ examples/common/gpio_button/component.mk | 2 + examples/common/ledc_driver/CMakeLists.txt | 5 + examples/common/ledc_driver/component.mk | 2 + examples/common/ledc_driver/ledc_driver.c | 179 +++ examples/common/ledc_driver/ledc_driver.h | 49 + examples/common/ws2812_led/CMakeLists.txt | 9 + examples/common/ws2812_led/Kconfig | 19 + examples/common/ws2812_led/component.mk | 5 + examples/common/ws2812_led/led_strip.h | 126 ++ .../common/ws2812_led/led_strip_rmt_ws2812.c | 171 +++ examples/common/ws2812_led/ws2812_led.c | 159 +++ examples/common/ws2812_led/ws2812_led.h | 44 + examples/gpio/.vscode/settings.json | 11 + examples/gpio/CMakeLists.txt | 16 + examples/gpio/Makefile | 12 + examples/gpio/README.md | 18 + examples/gpio/main/CMakeLists.txt | 2 + examples/gpio/main/Kconfig.projbuild | 29 + examples/gpio/main/app_driver.c | 61 + examples/gpio/main/app_main.c | 109 ++ examples/gpio/main/app_priv.h | 14 + examples/gpio/main/component.mk | 4 + examples/gpio/main/idf_component.yml | 8 + examples/gpio/partitions.csv | 10 + examples/gpio/partitions_4mb_optimised.csv | 11 + examples/gpio/sdkconfig.defaults | 41 + examples/gpio/sdkconfig.defaults.esp32 | 1 + examples/gpio/sdkconfig.defaults.esp32c2 | 121 ++ examples/gpio/sdkconfig.defaults.esp32c6 | 10 + examples/gpio/sdkconfig.defaults.esp32h2 | 14 + examples/gpio/sdkconfig.defaults.esp32s2 | 4 + 193 files changed, 25742 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/01_compile_bug.yml create mode 100644 .github/ISSUE_TEMPLATE/02_runtime_bug.yml create mode 100644 .github/ISSUE_TEMPLATE/03_feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/04_other_issue.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/workflows/issue_comment.yml create mode 100644 .github/workflows/launchpad.yml create mode 100644 .github/workflows/new_issues.yml create mode 100644 .github/workflows/new_prs.yml create mode 100644 .github/workflows/upload_components.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CHANGES.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 components/esp_rainmaker/CMakeLists.txt create mode 100644 components/esp_rainmaker/Kconfig.projbuild create mode 100644 components/esp_rainmaker/LICENSE create mode 100644 components/esp_rainmaker/README.md create mode 100644 components/esp_rainmaker/component.mk create mode 100644 components/esp_rainmaker/idf_component.yml create mode 100644 components/esp_rainmaker/include/esp_rmaker_console.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_core.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_mqtt.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_ota.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_scenes.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_schedule.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_standard_devices.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_standard_params.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_standard_services.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_standard_types.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_thread_br.h create mode 100644 components/esp_rainmaker/include/esp_rmaker_user_mapping.h create mode 100644 components/esp_rainmaker/sdkconfig.rename create mode 100644 components/esp_rainmaker/server_certs/rmaker_claim_service_server.crt create mode 100644 components/esp_rainmaker/server_certs/rmaker_mqtt_server.crt create mode 100644 components/esp_rainmaker/server_certs/rmaker_ota_server.crt create mode 100644 components/esp_rainmaker/src/console/esp_rmaker_commands.c create mode 100644 components/esp_rainmaker/src/console/esp_rmaker_console.c create mode 100644 components/esp_rainmaker/src/console/esp_rmaker_console_internal.h create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_claim.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_claim.h create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_claim.pb-c.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_claim.pb-c.h create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_claim.proto create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_client_data.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_client_data.h create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_cmd_resp_manager.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_core.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_device.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_internal.h create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_local_ctrl.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_mqtt_topics.h create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_node.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_node_auth.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_node_config.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_param.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_scenes.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_schedule.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_secure_boot_digest.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_secure_boot_digest.h create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_system_service.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_time_service.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_user_mapping.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_user_mapping.pb-c.c create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_user_mapping.pb-c.h create mode 100644 components/esp_rainmaker/src/core/esp_rmaker_user_mapping.proto create mode 100644 components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt.c create mode 100644 components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt_budget.c create mode 100644 components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt_budget.h create mode 100644 components/esp_rainmaker/src/ota/esp_rmaker_ota.c create mode 100644 components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h create mode 100644 components/esp_rainmaker/src/ota/esp_rmaker_ota_using_params.c create mode 100644 components/esp_rainmaker/src/ota/esp_rmaker_ota_using_topics.c create mode 100644 components/esp_rainmaker/src/standard_types/esp_rmaker_standard_devices.c create mode 100644 components/esp_rainmaker/src/standard_types/esp_rmaker_standard_params.c create mode 100644 components/esp_rainmaker/src/standard_types/esp_rmaker_standard_services.c create mode 100644 components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br.c create mode 100644 components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_internal.c create mode 100644 components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_launcher.c create mode 100644 components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_priv.h create mode 100644 components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_service.c create mode 100644 components/esp_schedule/CMakeLists.txt create mode 100644 components/esp_schedule/LICENSE create mode 100644 components/esp_schedule/README.md create mode 100644 components/esp_schedule/component.mk create mode 100644 components/esp_schedule/idf_component.yml create mode 100644 components/esp_schedule/include/esp_schedule.h create mode 100644 components/esp_schedule/src/esp_schedule.c create mode 100644 components/esp_schedule/src/esp_schedule_internal.h create mode 100644 components/esp_schedule/src/esp_schedule_nvs.c create mode 100644 components/rmaker_common/.github/workflows/upload_components.yml create mode 100644 components/rmaker_common/CMakeLists.txt create mode 100644 components/rmaker_common/Kconfig create mode 100644 components/rmaker_common/LICENSE create mode 100644 components/rmaker_common/README.md create mode 100644 components/rmaker_common/component.mk create mode 100644 components/rmaker_common/idf_component.yml create mode 100644 components/rmaker_common/include/esp_rmaker_cmd_resp.h create mode 100644 components/rmaker_common/include/esp_rmaker_common_console.h create mode 100644 components/rmaker_common/include/esp_rmaker_common_events.h create mode 100644 components/rmaker_common/include/esp_rmaker_factory.h create mode 100644 components/rmaker_common/include/esp_rmaker_mqtt_glue.h create mode 100644 components/rmaker_common/include/esp_rmaker_utils.h create mode 100644 components/rmaker_common/include/esp_rmaker_work_queue.h create mode 100644 components/rmaker_common/src/cmd_resp.c create mode 100644 components/rmaker_common/src/console/rmaker_common_cmds.c create mode 100644 components/rmaker_common/src/console/rmaker_console.c create mode 100644 components/rmaker_common/src/console/rmaker_console_internal.h create mode 100644 components/rmaker_common/src/create_APN3_PPI_string.c create mode 100644 components/rmaker_common/src/esp-mqtt/esp-mqtt-glue.c create mode 100644 components/rmaker_common/src/factory.c create mode 100644 components/rmaker_common/src/time.c create mode 100644 components/rmaker_common/src/timezone.c create mode 100644 components/rmaker_common/src/utils.c create mode 100644 components/rmaker_common/src/work_queue.c create mode 100644 docs/Doxyfile create mode 100644 docs/Makefile create mode 100644 docs/README.md create mode 100644 docs/_static/esp-rainmaker-logo.png create mode 100644 docs/_static/theme_overrides.css create mode 100644 docs/conf_common.py create mode 100644 docs/en/c-api-reference/index.rst create mode 100644 docs/en/c-api-reference/rainmaker_common.rst create mode 100644 docs/en/c-api-reference/rainmaker_console.rst create mode 100644 docs/en/c-api-reference/rainmaker_core.rst create mode 100644 docs/en/c-api-reference/rainmaker_mqtt.rst create mode 100644 docs/en/c-api-reference/rainmaker_ota.rst create mode 100644 docs/en/c-api-reference/rainmaker_standard_types.rst create mode 100644 docs/en/conf.py create mode 100644 docs/en/index.rst create mode 100644 docs/local_util.py create mode 100644 docs/make.bat create mode 100644 docs/requirements.txt create mode 100644 docs/utils.sh create mode 100644 examples/common/app_insights/CMakeLists.txt create mode 100644 examples/common/app_insights/Kconfig create mode 100644 examples/common/app_insights/app_insights.c create mode 100644 examples/common/app_insights/app_insights.h create mode 100644 examples/common/app_insights/component.mk create mode 100644 examples/common/app_insights/idf_component.yml create mode 100644 examples/common/app_network/CMakeLists.txt create mode 100644 examples/common/app_network/Kconfig.projbuild create mode 100644 examples/common/app_network/app_network.c create mode 100644 examples/common/app_network/app_network.h create mode 100644 examples/common/app_network/app_thread_internal.c create mode 100644 examples/common/app_network/app_wifi.h create mode 100644 examples/common/app_network/app_wifi_internal.c create mode 100644 examples/common/app_network/component.mk create mode 100644 examples/common/app_network/idf_component.yml create mode 100644 examples/common/app_network/private_include/app_thread_internal.h create mode 100644 examples/common/app_network/private_include/app_wifi_internal.h create mode 100644 examples/common/app_network/sdkconfig.rename create mode 100644 examples/common/app_reset/CMakeLists.txt create mode 100644 examples/common/app_reset/app_reset.c create mode 100644 examples/common/app_reset/app_reset.h create mode 100644 examples/common/app_reset/component.mk create mode 100644 examples/common/gpio_button/CMakeLists.txt create mode 100644 examples/common/gpio_button/Kconfig create mode 100644 examples/common/gpio_button/button/button.c create mode 100644 examples/common/gpio_button/button/button_obj.cpp create mode 100644 examples/common/gpio_button/button/include/iot_button.h create mode 100644 examples/common/gpio_button/component.mk create mode 100644 examples/common/ledc_driver/CMakeLists.txt create mode 100644 examples/common/ledc_driver/component.mk create mode 100644 examples/common/ledc_driver/ledc_driver.c create mode 100644 examples/common/ledc_driver/ledc_driver.h create mode 100644 examples/common/ws2812_led/CMakeLists.txt create mode 100644 examples/common/ws2812_led/Kconfig create mode 100644 examples/common/ws2812_led/component.mk create mode 100644 examples/common/ws2812_led/led_strip.h create mode 100644 examples/common/ws2812_led/led_strip_rmt_ws2812.c create mode 100644 examples/common/ws2812_led/ws2812_led.c create mode 100644 examples/common/ws2812_led/ws2812_led.h create mode 100644 examples/gpio/.vscode/settings.json create mode 100644 examples/gpio/CMakeLists.txt create mode 100644 examples/gpio/Makefile create mode 100644 examples/gpio/README.md create mode 100644 examples/gpio/main/CMakeLists.txt create mode 100644 examples/gpio/main/Kconfig.projbuild create mode 100644 examples/gpio/main/app_driver.c create mode 100644 examples/gpio/main/app_main.c create mode 100644 examples/gpio/main/app_priv.h create mode 100644 examples/gpio/main/component.mk create mode 100644 examples/gpio/main/idf_component.yml create mode 100644 examples/gpio/partitions.csv create mode 100644 examples/gpio/partitions_4mb_optimised.csv create mode 100644 examples/gpio/sdkconfig.defaults create mode 100644 examples/gpio/sdkconfig.defaults.esp32 create mode 100644 examples/gpio/sdkconfig.defaults.esp32c2 create mode 100644 examples/gpio/sdkconfig.defaults.esp32c6 create mode 100644 examples/gpio/sdkconfig.defaults.esp32h2 create mode 100644 examples/gpio/sdkconfig.defaults.esp32s2 diff --git a/.github/ISSUE_TEMPLATE/01_compile_bug.yml b/.github/ISSUE_TEMPLATE/01_compile_bug.yml new file mode 100644 index 0000000..661772c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_compile_bug.yml @@ -0,0 +1,101 @@ +name: Compilation error report +description: Report build issues +labels: ['Type: Bug'] +body: + - type: checkboxes + id: checklist + attributes: + label: Answers checklist. + description: Before submitting a new issue, please follow the checklist and try to find the answer. + options: + - label: I have read the [Rainmaker documentation](https://rainmaker.espressif.com/docs/get-started.html) and the issue is not addressed there. + required: true + - label: I have updated my IDF branch (release/vX.Y) to the latest version and checked that the issue is present there. This is not applicable if you are using Rainmaker with Arduino. + required: true + - label: I have searched the [Rainmaker forum](https://www.esp32.com/viewforum.php?f=41) and issue tracker for a similar issue and not found a similar issue. + required: true + - type: input + id: idf_version + attributes: + label: IDF / ESP32-Arduino version. + description: On which IDF version does this issue occur on? Run `git describe --tags` or `idf.py --version` to find it. For Arduino users, mention the version of ESP32-Arduino (from Boards manager). + placeholder: ex. v3.2-dev-1148-g96cd3b75c / ESP32-Arduino 2.0.6 + validations: + required: true + - type: dropdown + id: operating_system + attributes: + label: Operating System used. + multiple: false + options: + - Windows + - Linux + - macOS + validations: + required: true + - type: dropdown + id: build + attributes: + label: How did you build your project? + multiple: false + options: + - Command line with Make + - Command line with CMake + - Command line with idf.py + - Arduino IDE + - Other (please specify in More Information) + validations: + required: true + - type: input + id: devkit + attributes: + label: Development Kit. + description: On which Development Kit does this issue occur on? + placeholder: ex. ESP32-Wrover-Kit v2 | Custom Board + validations: + required: true + - type: textarea + id: expected + attributes: + label: What is the expected behavior? + description: Please provide a clear and concise description of the expected behavior. + placeholder: I expected it to... + validations: + required: true + - type: textarea + id: actual + attributes: + label: What is the actual behavior? + description: Please describe actual behavior. + placeholder: Instead it... + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce. + description: 'How do you trigger this bug? Please walk us through it step by step. Please attach sdkconfig file (from your project folder). Please attach your code here or the name of the rainmaker example.' + value: | + 1. Step + 2. Step + 3. Step + ... + validations: + required: true + - type: textarea + id: debug_logs + attributes: + label: Build Logs. + description: Compilation log goes here, should contain the backtrace, as well as the reset source if it is a crash. + placeholder: Your log goes here. + render: plain + validations: + required: false + - type: textarea + id: more-info + attributes: + label: More Information. + description: Do you have any other information from investigating this? + placeholder: ex. I tried on my friend's Windows 10 PC and the command works there. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/02_runtime_bug.yml b/.github/ISSUE_TEMPLATE/02_runtime_bug.yml new file mode 100644 index 0000000..38bb0a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_runtime_bug.yml @@ -0,0 +1,101 @@ +name: Runtime bug report +description: Report runtime bugs/crashes +labels: ['Type: Bug'] +body: + - type: checkboxes + id: checklist + attributes: + label: Answers checklist. + description: Before submitting a new issue, please follow the checklist and try to find the answer. + options: + - label: I have read the [Rainmaker documentation](https://rainmaker.espressif.com/docs/get-started.html) and the issue is not addressed there. + required: true + - label: I have updated my IDF branch (release/vX.Y) to the latest version and checked that the issue is present there. This is not applicable if you are using Rainmaker with Arduino. + required: true + - label: I have searched the [Rainmaker forum](https://www.esp32.com/viewforum.php?f=41) and issue tracker for a similar issue and not found a similar issue. + required: true + - type: input + id: idf_version + attributes: + label: IDF / ESP32-Arduino version. + description: On which IDF version does this issue occur on? Run `git describe --tags` or `idf.py --version` to find it. For Arduino users, mention the version of ESP32-Arduino (from Boards manager). + placeholder: ex. v3.2-dev-1148-g96cd3b75c / ESP32-Arduino 2.0.6 + validations: + required: true + - type: dropdown + id: operating_system + attributes: + label: Operating System used. + multiple: false + options: + - Windows + - Linux + - macOS + validations: + required: true + - type: dropdown + id: build + attributes: + label: How did you build your project? + multiple: false + options: + - Command line with Make + - Command line with CMake + - Command line with idf.py + - Arduino IDE + - Other (please specify in More Information) + validations: + required: false + - type: input + id: devkit + attributes: + label: Development Kit. + description: On which Development Kit does this issue occur on? + placeholder: ex. ESP32-Wrover-Kit v2 | Custom Board + validations: + required: true + - type: textarea + id: expected + attributes: + label: What is the expected behavior? + description: Please provide a clear and concise description of the expected behavior. + placeholder: I expected it to... + validations: + required: true + - type: textarea + id: actual + attributes: + label: What is the actual behavior? + description: Please describe actual behavior. + placeholder: Instead it... + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce. + description: 'How do you trigger this bug? Please walk us through it step by step. Please attach your code here or the name of the rainmaker example.' + value: | + 1. Step + 2. Step + 3. Step + ... + validations: + required: true + - type: textarea + id: debug_logs + attributes: + label: Debug Logs. + description: Debug log goes here, should contain the backtrace, as well as the reset source if it is a crash. + placeholder: Your log goes here. + render: plain + validations: + required: false + - type: textarea + id: more-info + attributes: + label: More Information. + description: Do you have any other information from investigating this? + placeholder: ex. I tried on my friend's Windows 10 PC and the command works there. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/03_feature_request.yml b/.github/ISSUE_TEMPLATE/03_feature_request.yml new file mode 100644 index 0000000..12bae9a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_feature_request.yml @@ -0,0 +1,31 @@ +name: Feature request +description: Suggest an idea for this project. +labels: ['Type: Feature Request'] +body: + - type: markdown + attributes: + value: We welcome any ideas or feature requests! It’s helpful if you can explain exactly why the feature would be useful. + - type: textarea + id: problem-related + attributes: + label: Is your feature request related to a problem? + description: Please provide a clear and concise description of what the problem is. + placeholder: ex. I'm always frustrated when ... + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like. + description: Please provide a clear and concise description of what you want to happen. + placeholder: ex. When using the Rainmaker app ... + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered. + description: Please provide a clear and concise description of any alternative solutions or features you've considered. + placeholder: ex. Choosing other approach wouldn't work, because ... + - type: textarea + id: context + attributes: + label: Additional context. + description: Please add any other context or screenshots about the feature request here. + placeholder: ex. This would work only when ... diff --git a/.github/ISSUE_TEMPLATE/04_other_issue.yml b/.github/ISSUE_TEMPLATE/04_other_issue.yml new file mode 100644 index 0000000..4bebe13 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04_other_issue.yml @@ -0,0 +1,23 @@ +name: General issue / query report +description: File an issue report +body: + - type: checkboxes + id: checklist + attributes: + label: Answers checklist. + description: Before submitting a new issue, please follow the checklist and try to find the answer. + options: + - label: I have read the [Rainmaker documentation](https://rainmaker.espressif.com/docs/get-started.html) and the issue is not addressed there. + required: true + - label: I have updated my IDF branch (release/vX.Y) to the latest version and checked that the issue is present there. This is not applicable if you are using Rainmaker with Arduino. + required: true + - label: I have searched the [Rainmaker forum](https://www.esp32.com/viewforum.php?f=41) and issue tracker for a similar issue and not found a similar issue. + required: true + - type: textarea + id: issue + attributes: + label: General issue report + description: Your issue report goes here. + placeholder: ex. How do I run... + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ca45991 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Getting Started - ESP Rainmaker + url: https://rainmaker.espressif.com/docs/get-started.html + about: Guide for getting started with Rainmaker + - name: ESP Rainmaker API Guide + url: https://rainmaker.espressif.com/docs/api.html + about: Documentation for using Rainmaker APIs + - name: Espressif Rainmaker Forum + url: https://www.esp32.com/viewforum.php?f=41 + about: For asking questions to the community about using ESP Rainmaker, create a discussion topic here. diff --git a/.github/workflows/issue_comment.yml b/.github/workflows/issue_comment.yml new file mode 100644 index 0000000..aaf3ed6 --- /dev/null +++ b/.github/workflows/issue_comment.yml @@ -0,0 +1,20 @@ +name: Sync issue comments to JIRA + +# This workflow will be triggered when new issue comment is created (including PR comments) +on: issue_comment + +jobs: + sync_issue_comments_to_jira: + name: Sync Issue Comments to Jira + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Sync issue comments to JIRA + uses: espressif/github-actions/sync_issues_to_jira@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JIRA_PASS: ${{ secrets.JIRA_PASS }} + JIRA_PROJECT: MEGH + JIRA_COMPONENT: GitHub + JIRA_URL: ${{ secrets.JIRA_URL }} + JIRA_USER: ${{ secrets.JIRA_USER }} diff --git a/.github/workflows/launchpad.yml b/.github/workflows/launchpad.yml new file mode 100644 index 0000000..6e8187e --- /dev/null +++ b/.github/workflows/launchpad.yml @@ -0,0 +1,138 @@ +# Copyright 2024 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This workflow build examples, store the artifacts and deploy them to github pages. +# Generates the launchpad configuration file that can be used with the url. + +name: Build Examples + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + Build: + # Disable the job in forks + if: ${{ github.repository_owner == 'espressif' }} + + runs-on: ubuntu-latest + container: + image: espressif/idf:v5.3.1 + strategy: + matrix: + example: [fan, led_light, multi_device, switch, temperature_sensor] + target: [esp32, esp32c3, esp32c6, esp32s3] + + steps: + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - run: mkdir -p images + + - name: build application + run: | + . $IDF_PATH/export.sh + export ESP_RMAKER_PATH=$PWD + cd examples/${{ matrix.example }} + + # Lets use the assisted claim by default for launchpad examples + echo "CONFIG_ESP_RMAKER_ASSISTED_CLAIM=y" >> sdkconfig.defaults + + idf.py set-target ${{ matrix.target }} build + + cd build + TARGET_CHIP=`cat project_description.json | python3 -c 'import sys,json; print(json.load(sys.stdin)["target"])'` + APP_BIN=`cat project_description.json | python3 -c 'import sys,json; print(json.load(sys.stdin)["app_bin"])'` + + OUT_BIN=$ESP_RMAKER_PATH/images/"$TARGET_CHIP"_RainMaker_"$APP_BIN" + esptool.py --chip $TARGET_CHIP merge_bin -o $OUT_BIN @flash_args + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: build-images-${{ matrix.target }}-${{ matrix.example }} + path: images + + deploy: + # Disable the job in forks + if: ${{ github.repository_owner == 'espressif' }} + + needs: Build + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + container: + image: espressif/esp-matter:latest + + steps: + - uses: actions/checkout@v4 + id: checkout + with: + submodules: 'recursive' + + - name: Download builds + uses: actions/download-artifact@v4 + with: + pattern: build-images-* + path: images/ + merge-multiple: true + + - name: generate launchpad config + run: | + export ESP_RMAKER_PATH=$PWD + + cd images + $ESP_RMAKER_PATH/tools/launchpad/generate_launchpad_config.sh ${{ github.repository_owner }} `basename ${{ github.repository }}` + + echo "#### Build Config" >> build_cfg.md + echo "" >> build_cfg.md + echo "" >> build_cfg.md + echo "- ESP-IDF: [`git -C $IDF_PATH rev-parse HEAD`](https://github.com/espressif/esp-idf/tree/`git -C $IDF_PATH rev-parse HEAD`)" >> build_cfg.md + echo "- ESP-RainMaker: [${{steps.checkout.outputs.commit}}](https://github.com/espressif/esp-rainmaker/tree/${{steps.checkout.outputs.commit}})" >> build_cfg.md + echo "" >> build_cfg.md + echo "" >> build_cfg.md + + cat $ESP_RMAKER_PATH/tools/launchpad/app_link_guide.md >> build_cfg.md + + tree -H '.' -L 1 --noreport -T 'ESP RainMaker Launchpad Artifacts' -shi --charset utf-8 -I "index.html" -o index.html + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: images/ + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/new_issues.yml b/.github/workflows/new_issues.yml new file mode 100644 index 0000000..0041efc --- /dev/null +++ b/.github/workflows/new_issues.yml @@ -0,0 +1,20 @@ +name: Sync issues to Jira + +# This workflow will be triggered when a new issue is opened +on: issues + +jobs: + sync_issues_to_jira: + name: Sync issues to Jira + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Sync GitHub issues to Jira project + uses: espressif/github-actions/sync_issues_to_jira@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JIRA_PASS: ${{ secrets.JIRA_PASS }} + JIRA_PROJECT: MEGH + JIRA_COMPONENT: GitHub + JIRA_URL: ${{ secrets.JIRA_URL }} + JIRA_USER: ${{ secrets.JIRA_USER }} diff --git a/.github/workflows/new_prs.yml b/.github/workflows/new_prs.yml new file mode 100644 index 0000000..1511887 --- /dev/null +++ b/.github/workflows/new_prs.yml @@ -0,0 +1,25 @@ +name: Sync remain PRs to Jira + +# This workflow will be triggered every hour, to sync remaining PRs (i.e. PRs with zero comment) to Jira project +# Note that, PRs can also get synced when new PR comment is created +on: + schedule: + - cron: "0 * * * *" + +jobs: + sync_prs_to_jira: + name: Sync PRs to Jira + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Sync PRs to Jira project + uses: espressif/github-actions/sync_issues_to_jira@master + with: + cron_job: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JIRA_PASS: ${{ secrets.JIRA_PASS }} + JIRA_PROJECT: MEGH + JIRA_COMPONENT: GitHub + JIRA_URL: ${{ secrets.JIRA_URL }} + JIRA_USER: ${{ secrets.JIRA_USER }} diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml new file mode 100644 index 0000000..96aac1e --- /dev/null +++ b/.github/workflows/upload_components.yml @@ -0,0 +1,20 @@ +name: Push components to Espressif Component Service + +on: + push: + branches: + - master + +jobs: + upload_components: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Upload ESP RainMaker components to Component Registry + uses: espressif/upload-components-ci-action@v1 + with: + directories: > + components/esp_schedule; + components/esp_rainmaker; + namespace: "espressif" + api_token: ${{ secrets.IDF_COMPONENT_API_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6331d3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,94 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf +*.pyc + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# emacs +.dir-locals.el + +# emacs temp file suffixes +*~ +.#* +\#*# + +# eclipse setting +.settings + +# MacOS directory files +.DS_Store + +# Application project files +**/sdkconfig +**/sdkconfig.old +**/build +**/cloud_cfg/*.key +**/cloud_cfg/*.crt +**/cloud_cfg/*.info +**/.cache + +# node_modules +docs/docusaurus/website/node_modules + +# cli logs +**/logs + +# IDF package manager +**/managed_components/ +*.lock + +# Patch files +*.patch + +# Failed patch files +*.rej +*.orig + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6f4e130 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "components/rmaker_common"] + path = components/rmaker_common + url = ../esp-rainmaker-common.git diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..b5c5c4b --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,353 @@ +# Changes + +## 27-Sep-2024: Modified esp-rainmaker examples to use partitions_4mb_optimised.csv + - RainMaker examples now use `partitions_4mb_optimised.csv` by default instead of `partitions.csv` and it is recommended to use the same in any new RainMaker projects. + - The fctry partition address is different for this partition table. So, if the host claiming is used, please pass the new address using: + ```bash +esp-rainmaker-cli claim $ESPPORT --address 0x3fa000 + ``` + +## 7-Aug-2024: Enabled ESP-Insights command response feature + - With ESP-Insights updated to the newer component, the command response feature from rainmaker can be used. + - To use the same, in addition to enabling esp-insights, please set below option from menuconfig: + ```bash +CONFIG_ESP_INSIGHTS_CMD_RESP_ENABLED=y + ``` + - More info on this can be found [here](https://github.com/espressif/esp-insights/blob/main/FEATURES.md#command-response). + +## 18-Jul-2024: Use network_provsioning for ESP-IDF v5.1 or later to support RainMaker over Thread +- The network_provisioning component can be used for provisioning both Wi-Fi or Thread devices. It also stays backward capabitable with wifi_provisioning component. + +## 27-Feb-2024: Add support for closing provisioning window after PoP mismatch + - For ESP IDF v5.1.3 and later, provisioning will be stopped if there are 5 attempts to establish secure session with wrong PoP. This count can be set to any value between 0 and 20. 0 means that provisioning will not be stopped (which will be same as the earlier behaviour before this change). + +## 21-Nov-2022 (esp_rmaker_mqtt: Add MQTT budgeting to control the number of messages sent) + +- Due to some poor, non-optimised coding or bugs, it is possible that the node keeps bombarding the MQTT +broker with publish messages. To prevent this, a concept of MQTT Budgeting has been added. +- By default, a node will be given a budget of 100 (`CONFIG_ESP_RMAKER_MQTT_DEFAULT_BUDGET`), which will + go on incrementing by 1 (`CONFIG_ESP_RMAKER_MQTT_BUDGET_REVIVE_COUNT` every 5 seconds (`CONFIG_ESP_RMAKER_MQTT_BUDGET_REVIVE_PERIOD`), + limited to a max value of 1024 (`CONFIG_ESP_RMAKER_MQTT_MAX_BUDGET`). +- Budget will be decremented by 1 for every MQTT publish and messages will be dropped if budget is 0. +- This behaviour is enabled by default and can be disabled by disabling `CONFIG_ESP_RMAKER_MQTT_ENABLE_BUDGETING`. + +## 16-Nov-2022 (mqtt_topics: Added support for AWS basic ingest topics.) + +- AWS Basic Ingest Topics optimize data flow by removing the publish/subscribe message broker from the ingestion path, making it more cost effective. You can refer the official docs [here](https://docs.aws.amazon.com/iot/latest/developerguide/iot-basic-ingest.html). +- This setting is turned on by default and can be turned off by running `idf.py menuconfig` and disabling `CONFIG_ESP_RMAKER_MQTT_USE_BASIC_INGEST_TOPICS` option. + +## 2-Nov-2022 (Added MQTT disconnect and user node mapping reset calls on WiFi/Factory Reset.) + +- On a Wi-Fi reset triggered via esp_rmaker_wifi_reset(), the rmaker core will first disconnect from MQTT so that its offline state reflects immediately. +- On a Factory reset triggered via esp_rmaker_factory_reset(), the rmaker core will trigger a user mapping reset (if mqtt connection is active) so that the node gets removed from the user's account immediately. +- The reset delay time for the push button based reset has been changed from 0 to 2 seconds to give some time for the above mentioned operations. + +Note: This config is enabled by default. You can disable it using `idf.py menuconfig` +## 28-Jun-2022 (examples: Enable CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE in all examples) + +`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` has been enabled in all examples by default, +as a safety measure to prevent devices getting bricked after a faulty firmware upgrade. +The OTA firmware upgrade will be marked as successful only if the firmware can connect to +MQTT within 90 seconds of calling `esp_rmaker_ota_enable_default()` (or other esp_emaker_enable APIs). +The time out is configurable using `CONFIG_ESP_RMAKER_OTA_ROLLBACK_WAIT_PERIOD`. + +Note that this is a bootloader feature and so, just enabling this feature and pushing out updated +firmware to existing devices won't be of any use. Please flash new bootloader on the devices to +make this work. + +## 26-May-2022 (claiming and ota) + +- claiming: Make self claiming as the default for esp32s3 and esp32c3 +- ota: Make "OTA using Topics" as default and provide a simplified API for that + +Self claiming is much more convenient and fast since the node directly gets the +credentials from the claiming service over HTTPS, instead of using the slower BLE based +Assisted claiming, wherein the phone app acts as a proxy between the node and the +claiming service. However, with self claiming, there was no concept of +[Admin Role](https://rainmaker.espressif.com/docs/user-roles.html#admin-users) and so, it was +not possible to access the node via the RainMaker or Insights dashboards. This was one +reason why Assisted Claiming was kept as a default for esp32c3 and esp32s3 even though +they support self claiming. + +With recent changes in the Public RainMaker backend, the primary user (the user who performs the [user-node +mapping](https://rainmaker.espressif.com/docs/user-node-mapping.html)) for a self claimed +node is now made as the admin. This gives the primary user the access to the node for OTA and Insights. +So, self claiming has now been made as the default for all chips (except esp32) and the OTA Using Topics +has also been made as the default, since it is convenient and also the correct option for +production devices. A simpler API `esp_rmaker_ota_enable_default()` as also been added in esp_rmaker_core.h. + +Note: Nodes that are already claimed via Assisted/Host Claiming will not have any effect, even if the +new firmware is enabled with self claiming. The self claiming will take effect only if the flash is +erased. **This will result in a change of node_id, since mac address is the node_id for self claimed nodes.** +If you want to contine using Assisted Claiming (probably because there is quite some data associated +with the node_id), please set is explicitly in your sdkconfig. + +## 25-Jan-2022 (app_wifi: Minor feature additions to provisioning workflow) + +Added a 30 minute timeout for Wi-Fi provisioning as a security measure. A device reboot will be +required to restart provisioning after it times out. The value can changed using the +`CONFIG_APP_WIFI_PROV_TIMEOUT_PERIOD` config option. A value of 0 will disable the timeout logic. +`APP_WIFI_EVENT_PROV_TIMEOUT` event will be triggerd to indicate that the provisioning has timed out. + +## 25-Jan-2022 (examples: Enable some security features and change order of component dirs) + +A couple of security features were added some time back, viz. + +1. esp_rmaker_local_ctrl: Added support for sec1 +2. esp_rmaker_user_mapping: Add checks for user id for better security + +These are kept disabled by default at component level to maintain backward compatibility and not +change any existing projects. However, since enabling them is recommended, these are added in +the sdkconfig.defaults of all examples. + +A minor change in CMakeLists.txt has also been done for all examples so that the rmaker_common +component from esp-rainmaker gets used, rather than the one from esp-insights. + +## 12-Jan-2022 (esp_rmaker_local_ctrl: Added support for sec1) + +This commit adds support for security1 for local control. This can be enabled by setting +`CONFIG_ESP_RMAKER_LOCAL_CTRL_SECURITY_1` when using local control feature (this is the +default security level when enabling local control). This would also require the latest +phone apps which have the support for security1. + +You can check the docs [here](https://rainmaker.espressif.com/docs/local-control.html) for more details. + +## 24-Aug-2021 (esp_rmaker_user_mapping: Add checks for user id for better security) + +This commit adds some logic to detect a reset to factory or a user change during the +user node association workflow so that the RainMaker cloud can reset any earlier +mappings if this reset state is reported by the device during user node association. + +If an existing, provisioned node is upgraded with a new firmware with this logic enabled, +it will send a request to cloud to reset the user mapping and so a re-provisioning would be +required. Moreover, a simple Wi-Fi reset will be treated as a factory reset in the context of +user node association. A side effect of this would be that the cloud can remove the secondary +users associated with that node and so, those would have to be added back again. +All subsequent Wi-Fi/Factory resets and provisioning + user node association would work fine. +For all new nodes, enabling this logic would have no issues. + +Since this change in behavior is a breaking change, the feature has been kept disabled by default. +However, it is strongly recommended to enable this using CONFIG_ESP_RMAKER_USER_ID_CHECK + +## 02-Jul-2021 (esp_insights: Add facility to enable esp_insights in the examples) + +This commit introduces a breaking change in compilation, not due to any API change, +but introduction of new components under components/esp-insights/components. +You can either choose to include these components in your projects CMakeLists.txt +as per the standard examples as given here: + +``` +set(EXTRA_COMPONENT_DIRS ${RMAKER_PATH}/components ${RMAKER_PATH}/examples/common ${RMAKER_PATH}/components/esp-insights/components) +``` + +You will also have to pull in the new esp-insights submodule by executing this command: + +``` +git submodule update --init --recursive +``` + +Another option is to exclude the common example component (app_insights) that adds these +components to the dependencies by adding this to your project's CMakeLists.txt: + +``` +set(EXCLUDE_COMPONENTS app_insights) +``` + +Check out the [esp-insights](https://github.com/espressif/esp-insights) project to understand more about this. +You can also check the docs [here](https://rainmaker.espressif.com/docs/esp-insights.html) to get started with enabling Insights in ESP RainMaker. + +## 28-May-2021 (esp_rmaker_core: Add a system service for reboot/reset) + +The reboot/reset API prototypes have changed from + +``` +esp_err_t esp_rmaker_reboot(uint8_t seconds); +esp_err_t esp_rmaker_wifi_reset(uint8_t seconds); +esp_err_t esp_rmaker_factory_reset(uint8_t seconds); +``` +To + +``` +esp_err_t esp_rmaker_reboot(int8_t seconds); +esp_err_t esp_rmaker_wifi_reset(int8_t reset_seconds, int8_t reboot_seconds); +esp_err_t esp_rmaker_factory_reset(int8_t reset_seconds, int8_t reboot_seconds); +``` + +- The behavior of `esp_rmaker_reboot()` has changed such that passing a value of 0 would trigger +an immediate reboot without starting any timer. +- The `esp_rmaker_wifi_reset()` and `esp_rmaker_factory_reset()` APIs have been modified such that +they now accept 2 time values. The `reset_seconds` specify the time after which the reset should trigger +and the `reboot_seconds` specify the time after which the reboot should trigger, after the reset +was done. +- `reboot_seconds` is similar to the earlier `seconds` argument, but it allows for 0 and negative values. +0 indicates that the reboot should happen immediately after reset and negative value indicates that the +reboot should be skipped. + +Please refer the [API documentation](https://docs.espressif.com/projects/esp-rainmaker/en/latest/c-api-reference/rainmaker_common.html#utilities) for additional details. + +## 1-Feb-2021 (esp_rmaker: Moved out some generic modules from esp_rainmaker component) + +Some generic code has been moved out of the esp_rainmaker repo and included as submodules at +components/rmaker_common and cli/. + +To get these submodules, you will now have to execute `git submodule update --init --recursive` once. + +For new clones, use `git clone --recursive https://github.com/espressif/esp-rainmaker.git` + +### RainMaker Events + +As part of the above changes, the following events have changed + +- RMAKER_EVENT_MQTT_CONNECTED -> RMAKER_MQTT_EVENT_CONNECTED +- RMAKER_EVENT_MQTT_DISCONNECTED -> RMAKER_MQTT_EVENT_DISCONNECTED +- RMAKER_EVENT_MQTT_PUBLISHED -> RMAKER_MQTT_EVENT_PUBLISHED + +Moreover, the event base for the MQTT events has changed from `RMAKER_EVENT` to `RMAKER_COMMON_EVENT`. The base has similarly changed even for the following: + +- RMAKER_EVENT_REBOOT +- RMAKER_EVENT_WIFI_RESET +- RMAKER_EVENT_FACTORY_RESET + +## 16-Oct-2020 (json: Use upstream json_generator and json_parser as submodules) + +To get these submodules, you will now have to execute `git submodule update --init --recursive` once. + +For new clones, use `git clone --recursive https://github.com/espressif/esp-rainmaker.git` + +## 16-Oct-2020 (app_wifi: Changes in SSID and PoP generation for Provisioning) + +The PoP for Wi-Fi provisioning was being fetched from a random 8 character hex string stored in the fctry partition. +In this commit, the random 8 character hex string has been replaced by 64 byte random number, which can be used for other purposes as well. +PoP is now generated by reading the first 4 bytes of this and converting to 8 character hex string. +Even the SSID now uses the last 3 bytes of this random number as the suffix, instead of last 3 bytes of MAC address. +With this change, it will now be possible to generate the complete Provisioning QR code payload outside the device, +without having to know its MAC address. + +## 29-Sep-2020 (esp_rmaker_standard_types: Start default names of all standard params with capital letter) + +Default parameter names like name, power, etc. have been changed to Name, Power, etc. respectively, so that they look better in the phone app UIs. + +With this change, any user configured device name (the name set from phone apps), or any other persistent parameter for which a +default name was used (Eg. power) will be affected, as the values will no more be found in the NVS storage. +Please edit your application code accordingly if you want to stick with the old names. + +Eg. If you were using the standard lightbulb device API which internally creates power and name parameters +``` +light_device = esp_rmaker_lightbulb_device_create("Light", NULL, DEFAULT_POWER); +``` + +Please change to below, if you want to stick with old parameter names "name" and "power". + +``` +light_device = esp_rmaker_device_create("Light", ESP_RMAKER_DEVICE_LIGHTBULB, NULL); +esp_rmaker_device_add_param(light_device, esp_rmaker_name_param_create("name", "Light")); +esp_rmaker_param_t *power_param = esp_rmaker_power_param_create("power", DEFAULT_POWER); +esp_rmaker_device_add_param(light_device, power_param); +esp_rmaker_device_assign_primary_param(light_device, power_param); +``` + +## 5-Aug-2020 (wifi_provisioning: Use a random pop instead of creating it from MAC address) + +Till date, the last 4 bytes of the MAC address were being used to generate the 8 character Proof of Possession (PoP) PIN for Wi-Fi provisioning. This is not secure enough because MAC address is a public information and can also be sniffed easily by devices in vicinity. A minor risk in this is that somebody else in the vicinity can provision your device, but a major risk is a man in the middle attack, wherein someone in vicinity can read the data being exchanged between a phone and the device and get the Wi-Fi credentials. + +To prevent this, it is best to use a randomly generated PoP which cannot be guessed. So now, a random stream of bytes is generated and flashed in the fctry partition during claiming and then used as PoP. If your device is already claimed, it is recommended to erase the flash and perform the claiming again. If you erase the flash again, the PoP will change. However, if you just do a reset to factory, it will not. + +If for some reason, you want to continue using the earlier mac address based method, please pass `POP_TYPE_MAC` to the `esp_err_t app_wifi_start(app_wifi_pop_type_t pop_type)` function. + +## 31-July-2020 (esp\_rainmaker\_core: Code restructure and API changes) + +Recently we made some significant changes to most ESP RainMaker APIs to make them even more modular and object oriented. You can check the examples to see what has changed, but here is a guide to help you understand some major changes. + +### RainMaker Initialisation + +#### Old +``` +typedef struct { + char *name; + char *type; + char *fw_version; + char *model; +} esp_rmaker_node_info_t; + +typedef struct { + esp_rmaker_node_info_t info; + bool enable_time_sync; +} esp_rmaker_config_t; + +esp_err_t esp_rmaker_init(esp_rmaker_config_t *config); +``` + +#### New +``` +typedef struct { + bool enable_time_sync; +} esp_rmaker_config_t; + +esp_rmaker_node_t *esp_rmaker_node_init(const esp_rmaker_config_t *config, const char *name, const char *type); +``` + +Optional: + +``` +esp_err_t esp_rmaker_node_add_fw_version(const esp_rmaker_node_t *node, const char *fw_version); +esp_err_t esp_rmaker_node_add_model(const esp_rmaker_node_t *node, const char *model); + +``` + +- `esp_rmaker_init()` changed to `esp_rmaker_node_init()`. +- Init function now returns a node handle instead of error code. +- Node Info is no more part of the config structure. The mandatory fields, "name" and "type" are directly passed as strings during initialization. +- Model and fw version are set internally using the project name and version build variables. They can be overriden using `esp_rmaker_node_add_fw_version/model`. + +### Devices + +#### Old +``` +typedef esp_err_t (*esp_rmaker_param_callback_t)(const char *name, const char *dev_name, esp_rmaker_param_val_t val, void *priv_data); + +esp_err_t esp_rmaker_create_device(const char *dev_name, const char *type, esp_rmaker_param_callback_t cb, void *priv_data); +``` + +#### New +``` +typedef esp_err_t (*esp_rmaker_device_write_cb_t)(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx); +typedef esp_err_t (*esp_rmaker_device_read_cb_t)(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + void *priv_data, esp_rmaker_read_ctx_t *ctx); + +esp_rmaker_device_t *esp_rmaker_device_create(const char *dev_name, const char *type, void *priv_data); +esp_err_t esp_rmaker_device_add_cb(const esp_rmaker_device_t *device, esp_rmaker_device_write_cb_t write_cb, esp_rmaker_device_read_cb_t read_cb); +esp_err_t esp_rmaker_node_add_device(const esp_rmaker_node_t *node, const esp_rmaker_device_t *device); +esp_err_t esp_rmaker_node_remove_device(const esp_rmaker_node_t *node, const esp_rmaker_device_t *device); +``` + +- `esp_rmaker_create_device()` changed to `esp_rmaker_device_create()`. It returns a device handle which has to be used for all further operations. +- The callback has changed such that it now gets the device and param handle, rather than the names. A read callback has also been introduced for future use. It can be kept NULL. +- The callbacks have a new "context" which can have additional information related to that callback, populated by the rainmaker core. +- After creating the device, it has to be added to the node explicitly using `esp_rmaker_node_add_device()`. +- Device can be removed using `esp_rmaker_node_remove_device()`. This may be required for bridges. + +### Parameters + +#### Old +``` +esp_err_t esp_rmaker_device_add_param(const char *dev_name, const char *param_name, + esp_rmaker_param_val_t val, uint8_t properties); +esp_rmaker_param_add_type(const char *dev_name, const char *param_name, const char* type); +esp_err_t esp_rmaker_update_param(const char *dev_name, const char *param_name, esp_rmaker_param_val_t val); +``` + +#### New +``` +esp_rmaker_param_t *esp_rmaker_param_create(const char *param_name, const char *type, + esp_rmaker_param_val_t val, uint8_t properties); +esp_err_t esp_rmaker_device_add_param(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param); +esp_err_t esp_rmaker_param_update_and_report(const esp_rmaker_param_t *param, esp_rmaker_param_val_t val); + + +``` + +- New API `esp_rmaker_param_create()` introduced to create a parameter. It returns a param handle which has to be used for all further operations. +- `esp_rmaker_device_add_param()` modified to accept the device and param handles. +- `esp_rmaker_param_add_type()` removed because the type is now included in `esp_rmaker_param_create()` +- `esp_rmaker_update_param()` changed to `esp_rmaker_param_update_and_report()`. It now accepts param handle, instead of device and parameter names. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6dcc61d --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# ESP RainMaker + +> This branch will not support esp-idf v4.x releases anymore as they have reached their end of life. Please check out `idf_4_x_compat` branch if your project requires it. + +> Note: For major changes, please refer [this file](CHANGES.md). + +## Introduction + +ESP RainMaker is an end-to-end solution offered by Espressif to enable remote control and monitoring for products based on ESP32 series of SoCs (e.g., ESP32, ESP32-S2, ESP32-C3, ESP32-C6, ESP32-C2, etc.) without any configuration required in the Cloud.
+ +The primary components of this solution are: + +- Claiming Service (to get the Cloud connectivity credentials) +- RainMaker Agent (i.e. this repo, to develop the firmware) +- RainMaker Cloud (backend, offering remote connectivity) +- RainMaker Phone App/CLI (Client utilities for remote access) + + +The key features of ESP RainMaker are: + +1. Ability to define own devices and parameters, of any type, in the firmware. +2. Zero configuration required on the Cloud. +3. Phone apps that dynamically render the UI as per the device information. + +## Get ESP RainMaker + +Please clone this repository using the below command: + +``` +git clone --recursive https://github.com/espressif/esp-rainmaker.git +``` + +> Note the --recursive option. This is required to pull in the various dependencies into esp-rainmaker. In case you have already cloned the repository without this option, execute this to pull in the submodules: `git submodule update --init --recursive` + +Please check the ESP RainMaker documentation [here](https://rainmaker.espressif.com/docs/get-started.html) to get started. + +Each example has its own README with additional information about using the example. + +## Supported ESP-IDF versions + +ESP RainMaker can work with ESP IDF 4.1 and above. + +## Phone Apps + +### Android + +- [Google PlayStore](https://play.google.com/store/apps/details?id=com.espressif.rainmaker) +- [Direct APK](https://github.com/espressif/esp-rainmaker/wiki) +- [Source Code](https://github.com/espressif/esp-rainmaker-android) + +### iOS +- [Apple App Store](https://apps.apple.com/app/esp-rainmaker/id1497491540) +- [Source Code](https://github.com/espressif/esp-rainmaker-ios) + +## Discussions + +[ESP32 Forum](https://www.esp32.com/viewforum.php?f=41) + +[![Gitter Chat](https://badges.gitter.im/esp-rainmaker/community.svg)](https://gitter.im/esp-rainmaker/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +--- + + + Try it with ESP Launchpad + diff --git a/components/esp_rainmaker/CMakeLists.txt b/components/esp_rainmaker/CMakeLists.txt new file mode 100644 index 0000000..064dd07 --- /dev/null +++ b/components/esp_rainmaker/CMakeLists.txt @@ -0,0 +1,94 @@ +# CORE +set(core_srcs "src/core/esp_rmaker_core.c" + "src/core/esp_rmaker_node.c" + "src/core/esp_rmaker_device.c" + "src/core/esp_rmaker_param.c" + "src/core/esp_rmaker_node_config.c" + "src/core/esp_rmaker_client_data.c" + "src/core/esp_rmaker_time_service.c" + "src/core/esp_rmaker_system_service.c" + "src/core/esp_rmaker_user_mapping.pb-c.c" + "src/core/esp_rmaker_user_mapping.c" + "src/core/esp_rmaker_node_auth.c" + "src/core/esp_rmaker_schedule.c" + "src/core/esp_rmaker_scenes.c" + "src/core/esp_rmaker_cmd_resp_manager.c" + "src/core/esp_rmaker_secure_boot_digest.c" + ) + +set(priv_req protobuf-c json_parser json_generator + nvs_flash esp_http_client app_update esp-tls mbedtls esp_https_ota + console esp_local_ctrl esp_https_server mdns esp_schedule efuse driver rmaker_common wifi_provisioning) + +if ("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.0") + list(APPEND priv_req esp_app_format) +endif() + +if ("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.1") + # NAT64 and DNS64 features were introduced for openthread component in IDF v5.1 + # Network Provisioning component is supported for IDF v5.1+ + list(APPEND priv_req openthread network_provisioning) +endif() + +if(CONFIG_ESP_RMAKER_ASSISTED_CLAIM) + list(APPEND core_srcs + "src/core/esp_rmaker_claim.c" + "src/core/esp_rmaker_claim.pb-c.c") +endif() +if(CONFIG_ESP_RMAKER_SELF_CLAIM) + list(APPEND core_srcs + "src/core/esp_rmaker_claim.c") +endif() + +if(CONFIG_ESP_RMAKER_LOCAL_CTRL_ENABLE) + list(APPEND core_srcs + "src/core/esp_rmaker_local_ctrl.c") +endif() + +set(core_priv_includes "src/core") + +# MQTT +set(mqtt_srcs "src/mqtt/esp_rmaker_mqtt.c" + "src/mqtt/esp_rmaker_mqtt_budget.c") +set(mqtt_priv_includes "src/mqtt") + +# OTA +set(ota_srcs "src/ota/esp_rmaker_ota.c" + "src/ota/esp_rmaker_ota_using_params.c" + "src/ota/esp_rmaker_ota_using_topics.c") +set(ota_priv_includes "src/ota") + +# Thread BR +set(thread_br_srcs ) +set(thread_br_priv_includes ) +if (CONFIG_OPENTHREAD_BORDER_ROUTER) + list(APPEND thread_br_srcs "src/thread_br/esp_rmaker_thread_br.c" + "src/thread_br/esp_rmaker_thread_br_service.c" + "src/thread_br/esp_rmaker_thread_br_internal.c" + "src/thread_br/esp_rmaker_thread_br_launcher.c") + list(APPEND thread_br_priv_includes "src/thread_br") +endif() + +# CONSOLE +set(console_srcs "src/console/esp_rmaker_console.c" + "src/console/esp_rmaker_commands.c") +set(console_priv_includes "src/console") + +# STANDARD TYPES +set(standard_types_srcs "src/standard_types/esp_rmaker_standard_params.c" + "src/standard_types/esp_rmaker_standard_devices.c" + "src/standard_types/esp_rmaker_standard_services.c") + +idf_component_register(SRCS ${core_srcs} ${mqtt_srcs} ${ota_srcs} ${standard_types_srcs} ${console_srcs} ${thread_br_srcs} + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS ${core_priv_includes} ${ota_priv_includes} ${console_priv_includes} ${mqtt_priv_includes} ${thread_br_priv_includes} + REQUIRES rmaker_common + PRIV_REQUIRES ${priv_req}) + +target_add_binary_data(${COMPONENT_TARGET} "server_certs/rmaker_mqtt_server.crt" TEXT) +target_add_binary_data(${COMPONENT_TARGET} "server_certs/rmaker_claim_service_server.crt" TEXT) +target_add_binary_data(${COMPONENT_TARGET} "server_certs/rmaker_ota_server.crt" TEXT) + +# Added just to automatically trigger re-runs of CMake +git_describe(RMAKER_VERSION ${COMPONENT_DIR}) +message("ESP RainMaker Project commit: " ${RMAKER_VERSION}) diff --git a/components/esp_rainmaker/Kconfig.projbuild b/components/esp_rainmaker/Kconfig.projbuild new file mode 100644 index 0000000..8a4ced7 --- /dev/null +++ b/components/esp_rainmaker/Kconfig.projbuild @@ -0,0 +1,396 @@ +menu "ESP RainMaker Config" + + choice ESP_RMAKER_CLAIM_TYPE + bool "Claiming Type" + default ESP_RMAKER_SELF_CLAIM + default ESP_RMAKER_ASSISTED_CLAIM if IDF_TARGET_ESP32 + help + Claiming type to be used. + + config ESP_RMAKER_NO_CLAIM + bool "Do not use Claiming" + help + Do not use any claiming. The MQTT credentials need to + be pre-programmed for this to work. This should be used + for all private RainMaker deployments. + + config ESP_RMAKER_SELF_CLAIM + bool "Use Self Claiming" + depends on !IDF_TARGET_ESP32 && !IDF_TARGET_ESP32C2 + help + Use Self Claiming i.e. get the MQTT credentials + directly from the claiming service. + + config ESP_RMAKER_ASSISTED_CLAIM + bool "Use Assisted Claiming" + depends on BT_ENABLED && !IDF_TARGET_ESP32S2 + help + Use Assisted Claiming i.e. get the MQTT credentials + from the claiming service via assistance from clients, + like the phone apps. + + endchoice + + choice ESP_RMAKER_CHOOSE_PKI_ACCESS_METHOD + prompt "Choose PKI credentials access method" + default ESP_RMAKER_USE_NVS + help + ESP devices support multiple ways to secure store the PKI credentials. + Currently, NVS and ESP Secure Cert Manager are supported. + The default behaviour is to access the PKI credentials from the NVS. + Consult the ESP-TLS documentation in ESP-IDF Programming guide for more details. + + config ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + bool "Use ESP Secure Certificate Manager" + depends on ESP_RMAKER_NO_CLAIM + help + Enable the use of ESP Secure Certificate Manager APIs for the example. + Please refer to ESP Secure Certificate Manager documentation for more details. + + config ESP_RMAKER_USE_NVS + bool "Use NVS (default)" + help + This option expects the Private key and Device certificate to be in the NVS. + This is the default behaviour. + endchoice + + + config ESP_RMAKER_CLAIM_TYPE + int + default 0 if ESP_RMAKER_NO_CLAIM + default 1 if ESP_RMAKER_SELF_CLAIM + default 2 if ESP_RMAKER_ASSISTED_CLAIM + + config ESP_RMAKER_CLAIM_SERVICE_BASE_URL + string "ESP RainMaker Claiming Service Base URL" + default "https://esp-claiming.rainmaker.espressif.com" + depends on ESP_RMAKER_SELF_CLAIM + help + ESP RainMaker Claiming Service Base URL. + + config ESP_RMAKER_READ_MQTT_HOST_FROM_CONFIG + bool "Read MQTT Host from ESP_RMAKER_MQTT_HOST (Read Docs)" + default n + help + Normally, if self claiming or assisted claiming is used, the MQTT Host is anyways read from + ESP_RMAKER_MQTT_HOST, independent of this config option. However, if this is set, even if + an MQTT host value is found in NVS, it will be overriden with ESP_RMAKER_MQTT_HOST. + + config ESP_RMAKER_READ_NODE_ID_FROM_CERT_CN + bool "Read Node ID from Device Certificate" + default n + help + If enabled, the device will get its node id from the device certificate's CN field. If not enabled, + it will read the node id either from nvs factory partition or mac address, depending on the configuration. + + config ESP_RMAKER_MQTT_HOST + string "ESP RainMaker MQTT Host" + depends on ESP_RMAKER_SELF_CLAIM || ESP_RMAKER_ASSISTED_CLAIM || ESP_RMAKER_READ_MQTT_HOST_FROM_CONFIG + default "a1p72mufdu6064-ats.iot.us-east-1.amazonaws.com" + help + ESP RainMaker MQTT Host name. + + config ESP_RMAKER_MQTT_USE_BASIC_INGEST_TOPICS + bool "Use Basic Ingest Topics" + default y + help + This config enables the use of AWS Basic Ingest Topics for Node to Cloud communication, + which eliminates the MQTT Broker and thus reduces messaging cost. + + config ESP_RMAKER_MQTT_ENABLE_BUDGETING + bool "Enable MQTT budgeting" + default y + help + Enable MQTT budgeting, which will control the number of MQTT messages sent by the node. + + config ESP_RMAKER_MQTT_DEFAULT_BUDGET + int "Default MQTT Budget" + depends on ESP_RMAKER_MQTT_ENABLE_BUDGETING + default 100 + range 64 ESP_RMAKER_MQTT_MAX_BUDGET + help + Default MQTT budget. Budget will reduce on sending an MQTT message and increase based on + ESP_RMAKER_MQTT_BUDGET_REVIVE_PERIOD. If no budget is available, MQTT message will be dropped. + + config ESP_RMAKER_MQTT_MAX_BUDGET + int "Max MQTT Budget" + depends on ESP_RMAKER_MQTT_ENABLE_BUDGETING + default 1024 + range 64 2048 + help + Maximum budget that the node can have. No additional budget will be allocated if this count is reached. + + config ESP_RMAKER_MQTT_BUDGET_REVIVE_PERIOD + int "MQTT Budget revive period" + depends on ESP_RMAKER_MQTT_ENABLE_BUDGETING + default 5 + range 5 600 + help + Period in seconds after which the MQTT budget should revive (by ESP_RMAKER_MQTT_BUDGET_REVIVE_COUNT). + This is used to limit the messages being sent by the node. + + config ESP_RMAKER_MQTT_BUDGET_REVIVE_COUNT + int "MQTT Budget revive count" + depends on ESP_RMAKER_MQTT_ENABLE_BUDGETING + default 1 + range 1 16 + help + The count by which the budget will be increased periodically based on ESP_RMAKER_MQTT_BUDGET_REVIVE_PERIOD. + + config ESP_RMAKER_MAX_PARAM_DATA_SIZE + int "Maximum Parameters' data size" + default 1024 + range 64 8192 + help + Maximum size of the payload for reporting parameter values. + + config ESP_RMAKER_DISABLE_USER_MAPPING_PROV + bool "Disable User Mapping during Provisioning" + default n + help + The handlers for User Node Mapping are now registered internally by ESP RainMaker core, + by registering to appropriate Wi-Fi Provisioning events. If your application code also + has the calls to create and register the user mapping handlers, enable this config + option to prevent duplication. + + config ESP_RMAKER_USER_ID_CHECK + bool "User id check for User Node mapping" + default n + help + This enables the additional user id checks during user node mapping. Whenever a new user + id is received, it is checked against the existing user id in NVS. If there is a mismatch, + or if no user id exists in NVS, this is considered as a reset state and the same is reported + to the ESP RainMaker Cloud during the User Node association MQTT Publish so that the cloud + can take appropriate action w.r.t user permissions. It is recommended to enable this option + for security reasons. + + config RMAKER_NAME_PARAM_CB + bool "Call device callback for Name param" + default n + help + By default, the "Name" parameter (esp.param.name) changes are handled internally. If Applications + want to handle this themselves, this config option can be enabled. Please ensure that you update + and report the name parameter in your callback so that it reflects correctly everywhere. + If no device callback is registered, the name paramater will be handled internally. + + config ESP_RMAKER_LOCAL_CTRL_FEATURE_ENABLE + bool "ESP RainMaker Local Control Feature" + default n + select ESP_HTTPS_SERVER_ENABLE + help + Enabling this allows to discover and control the node over local Wi-Fi network. + Note that this uses only Wi-Fi level security and so, any client on the same + Wi-Fi network can potentially control the node. The communication is not encrypted + and uses plain HTTP. Please Check the RainMaker documentation for additional details. + Note that enabling this just means that the APIs to enable/disable local + control will be compiled in and can be used in application code. If CONFIG_ESP_RMAKER_LOCAL_CTRL_AUTO_ENABLE + is also enabled, then no additional APIs are required for actually enabling local control. + + config ESP_RMAKER_LOCAL_CTRL_AUTO_ENABLE + bool "Auto ESP RainMaker Local Control" + default n + select ESP_RMAKER_LOCAL_CTRL_FEATURE_ENABLE + help + Automatically enabled local control when RainMaker starts. + + config ESP_RMAKER_LOCAL_CTRL_HTTP_PORT + int "Local Control HTTP Port" + depends on ESP_RMAKER_LOCAL_CTRL_FEATURE_ENABLE + default 8080 + help + The port number to be used for http for local control. + + config ESP_RMAKER_LOCAL_CTRL_STACK_SIZE + int "Local Control HTTP Server task stack size" + depends on ESP_RMAKER_LOCAL_CTRL_FEATURE_ENABLE + default 6144 + help + The task stack size to be used for http server for local control. + + choice ESP_RMAKER_LOCAL_CTRL_SECURITY + prompt "Local Control Security Type" + depends on ESP_RMAKER_LOCAL_CTRL_FEATURE_ENABLE + default ESP_RMAKER_LOCAL_CTRL_SECURITY_1 + help + Security type to be selected for local control. + + config ESP_RMAKER_LOCAL_CTRL_SECURITY_0 + bool "sec0" + config ESP_RMAKER_LOCAL_CTRL_SECURITY_1 + bool "sec1" + endchoice + + config ESP_RMAKER_LOCAL_CTRL_SECURITY + int + default 0 if ESP_RMAKER_LOCAL_CTRL_SECURITY_0 + default 1 if ESP_RMAKER_LOCAL_CTRL_SECURITY_1 + + choice ESP_RMAKER_CONSOLE_UART_NUM + prompt "UART for console input" + default ESP_RMAKER_CONSOLE_UART_NUM_0 + help + UART to be selected for serial console. + + config ESP_RMAKER_CONSOLE_UART_NUM_0 + bool "UART0" + config ESP_RMAKER_CONSOLE_UART_NUM_1 + bool "UART1" + endchoice + + config ESP_RMAKER_CONSOLE_UART_NUM + int + default 0 if ESP_RMAKER_CONSOLE_UART_NUM_0 + default 1 if ESP_RMAKER_CONSOLE_UART_NUM_1 + + config ESP_RMAKER_USE_CERT_BUNDLE + bool "Use Certificate Bundle" + default y + select ESP_RMAKER_MQTT_USE_CERT_BUNDLE + help + Use Certificate Bundle for server authentication. Enabling this is recommended to safeguard + against any changes in the server certificates in future. This has an impact on the binary + size as well as heap requirement. + + menu "ESP RainMaker OTA Config" + + config ESP_RMAKER_OTA_AUTOFETCH + bool "Auto Fetch OTA" + default y + help + Applicable only for OTA using Topics. + Fetch the OTA (i.e. get the URL and other details) by actively sending an + OTA fetch request to ESP RainMaker Cloud. If this is disabled, the node + will stay subscribed to the OTA Topics, but will get the information only + if someone explicitly triggers it. + + config ESP_RMAKER_OTA_AUTOFETCH_PERIOD + int "OTA Auto Fetch Period" + default 0 + range 0 168 + depends on ESP_RMAKER_OTA_AUTOFETCH + help + Periodically send an OTA fetch request. If set to 0, the request will be sent only once, + when the node connects to the ESP RainMaker Cloud first time after a boot. + Else, this defines the period (in hours) for the periodic fetch request. + + config ESP_RMAKER_SKIP_COMMON_NAME_CHECK + bool "Skip server certificate CN field check" + default n + help + This allows you to skip the validation of OTA server certificate CN field. + + config ESP_RMAKER_SKIP_VERSION_CHECK + bool "Skip firmware version check" + default n + help + This allows you to skip the firmware version check. Useful during development, + but not for production. + + config ESP_RMAKER_SKIP_SECURE_VERSION_CHECK + bool "Skip secure version check" + default n + help + This allows you to skip the secure version check. Useful during development, + but not for production. Check out ESP IDF's Anti-rollback feature for more details. + + config ESP_RMAKER_SKIP_PROJECT_NAME_CHECK + bool "Skip project name check" + default n + help + This allows you to skip the project name check. + + config ESP_RMAKER_OTA_HTTP_RX_BUFFER_SIZE + int "OTA HTTP receive buffer size" + default 1024 + range 512 LWIP_TCP_WND_DEFAULT + help + Increasing this value beyond the default would speed up the OTA download process. + However, please ensure that your application has enough memory headroom to allow this, + else, the OTA may fail. + + config ESP_RMAKER_OTA_ROLLBACK_WAIT_PERIOD + int "OTA Rollback Wait Period (Seconds)" + default 90 + range 30 600 + help + After an OTA Update, if CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is set, then the firmware will wait for MQTT + connection to mark the new firmware as valid. However, if it is not able to do so within + this wait period (in seconds), the firmware will be marked as invalid and the older + firmware will be booted into. + + config ESP_RMAKER_OTA_DISABLE_AUTO_REBOOT + bool "Disable auto reboot" + default n + help + After OTA image is flashed and active partition changed, the device automatically reboots. To disable this + behaviour and handle reboot on your own, based on RMAKER_OTA event, enable this option. + + config ESP_RMAKER_OTA_TIME_SUPPORT + bool "Enable OTA Time Support" + default y + help + OTA Jobs can include additional metadata for time to indicate a range of valid date and the time within + those dates. Eg. Perform OTA between 1 Dec 2022 and 10 Dec 2022 that too only between 2:00am and 5:00am. + If you want to ignore this, disable this option. + endmenu + + menu "ESP RainMaker Scheduling" + + config ESP_RMAKER_SCHEDULING_MAX_SCHEDULES + int "Maximum schedules" + default 10 + range 1 50 + help + Maximum Number of schedules allowed. The json size for report params increases as the number of schedules increases. + + endmenu + + menu "ESP RainMaker Scenes" + + config ESP_RMAKER_SCENES_MAX_SCENES + int "Maximum scenes" + default 10 + range 1 50 + help + Maximum Number of scenes allowed. The json size for report params increases as the number of scenes increases. + + config ESP_RMAKER_SCENES_DEACTIVATE_SUPPORT + bool "Enable Deactivate support" + default n + help + This enables the deactivate callback support. The application callback will be invoked with the source + set to ESP_RMAKER_REQ_SRC_SCENE_DEACTIVATE when the deactivate operation is received. However, the + param values would be the same as those for activate, since the RainMaker core does not know what the + expected values are for scene deactivation. + + endmenu + + menu "ESP RainMaker Command-Response" + + config ESP_RMAKER_CMD_RESP_ENABLE + bool "Enable Command-Response Module" + default y + help + Enable the ESP RainMaker Command-Response module for semi-synchronous communication. Please refer the RainMaker documents + for additional information. + + config ESP_RMAKER_CMD_RESP_TEST_ENABLE + bool "Enable Command-Response Testing" + default n + depends on ESP_RMAKER_CMD_RESP_ENABLE + help + Enable testing for Command-Response module. This enables triggering commands and parsing response from the node itself, + rather than receiving the commands from cloud. C API or the serial console can be used to trigger the commands. + This should be enabled only while testing commands, but should always be disabled in production firmware. + + endmenu + + config ESP_RMAKER_USING_NETWORK_PROV + bool "Using Network Provisioning" + default y + help + RainMaker will use network_provisioning component to provision a device to a Wi-Fi/Thread network if enabling this option. + If the option is not enabled, it will use wifi_provisioning instead. This option only works when IDF verson is later than + v5.1. + +endmenu diff --git a/components/esp_rainmaker/LICENSE b/components/esp_rainmaker/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/components/esp_rainmaker/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/components/esp_rainmaker/README.md b/components/esp_rainmaker/README.md new file mode 100644 index 0000000..f021bc7 --- /dev/null +++ b/components/esp_rainmaker/README.md @@ -0,0 +1,5 @@ +# ESP RainMaker Agent Component + +[![Component Registry](https://components.espressif.com/components/espressif/esp_rainmaker/badge.svg)](https://components.espressif.com/components/espressif/esp_rainmaker) + +This is the main firmware agent for ESP RainMaker, which will then pull in other required components. Please check the [ESP RainMaker documentation](https://rainmaker.espressif.com/) for details. diff --git a/components/esp_rainmaker/component.mk b/components/esp_rainmaker/component.mk new file mode 100644 index 0000000..2d2127c --- /dev/null +++ b/components/esp_rainmaker/component.mk @@ -0,0 +1,12 @@ +COMPONENT_SRCDIRS := src/core src/mqtt src/ota src/standard_types src/console +COMPONENT_ADD_INCLUDEDIRS := include +COMPONENT_PRIV_INCLUDEDIRS := src/core src/ota src/console + +ifndef CONFIG_ESP_RMAKER_ASSISTED_CLAIM +COMPONENT_OBJEXCLUDE += src/core/esp_rmaker_claim.pb-c.o +ifndef CONFIG_ESP_RMAKER_SELF_CLAIM + COMPONENT_OBJEXCLUDE += src/core/esp_rmaker_claim.o +endif +endif + +COMPONENT_EMBED_TXTFILES := server_certs/rmaker_mqtt_server.crt server_certs/rmaker_claim_service_server.crt server_certs/rmaker_ota_server.crt diff --git a/components/esp_rainmaker/idf_component.yml b/components/esp_rainmaker/idf_component.yml new file mode 100644 index 0000000..1b932e8 --- /dev/null +++ b/components/esp_rainmaker/idf_component.yml @@ -0,0 +1,33 @@ +## IDF Component Manager Manifest File +version: "1.5.4" +description: ESP RainMaker firmware agent +url: https://github.com/espressif/esp-rainmaker/tree/master/components/esp_rainmaker +repository: https://github.com/espressif/esp-rainmaker.git +issues: https://github.com/espressif/esp-rainmaker/issues +documentation: https://rainmaker.espressif.com/ +discussion: https://www.esp32.com/viewforum.php?f=41 +dependencies: + espressif/mdns: + version: "^1.2.0" + rules: + - if: "idf_version >=5.0" + espressif/esp_secure_cert_mgr: + version: "^2.2.1" + rules: + - if: "idf_version >=4.3" + espressif/rmaker_common: + version: "~1.4.6" + espressif/json_parser: + version: "~1.0.3" + espressif/json_generator: + version: "~1.1.1" + espressif/esp_schedule: + version: "~1.2.0" + espressif/network_provisioning: + version: "~1.0.0" + rules: + - if: "idf_version >= 5.1" + espressif/esp_rcp_update: + version: "~1.2.0" + rules: + - if: "idf_version >= 5.1" diff --git a/components/esp_rainmaker/include/esp_rmaker_console.h b/components/esp_rainmaker/include/esp_rmaker_console.h new file mode 100644 index 0000000..70c02ff --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_console.h @@ -0,0 +1,52 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Initialize console + * + * Initializes serial console and adds basic commands. + * + * @return ESP_OK on success. + * @return error in case of failures. + */ +esp_err_t esp_rmaker_console_init(void); + +/* Reference for adding custom console commands: +#include + +static int command_console_handler(int argc, char *argv[]) +{ + // Command code here +} + +static void register_console_command() +{ + const esp_console_cmd_t cmd = { + .command = "", + .help = "", + .func = &command_console_handler, + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&cmd)); +} +*/ + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_core.h b/components/esp_rainmaker/include/esp_rmaker_core.h new file mode 100644 index 0000000..833a82b --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_core.h @@ -0,0 +1,1068 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define ESP_RMAKER_CONFIG_VERSION "2020-03-20" + +/* Maximum length of the alert message that can be passed to esp_rmaker_raise_alert() */ +#define ESP_RMAKER_MAX_ALERT_LEN 100 + +/** @cond **/ +/** ESP RainMaker Event Base */ +ESP_EVENT_DECLARE_BASE(RMAKER_EVENT); +/** @endcond **/ + +/** ESP RainMaker Events */ +typedef enum { + /** RainMaker Core Initialisation Done */ + RMAKER_EVENT_INIT_DONE = 1, + /** Self Claiming Started */ + RMAKER_EVENT_CLAIM_STARTED, + /** Self Claiming was Successful */ + RMAKER_EVENT_CLAIM_SUCCESSFUL, + /** Self Claiming Failed */ + RMAKER_EVENT_CLAIM_FAILED, + /** Node side communication for User-Node mapping done. + * Actual mapping state will be managed by the ESP RainMaker cloud based on the user side communication. + * Associated data is the NULL terminated user id. + */ + RMAKER_EVENT_USER_NODE_MAPPING_DONE, + /** Local control started. Associated data is the NULL terminated Service Name */ + RMAKER_EVENT_LOCAL_CTRL_STARTED, + /* User reset request successfully sent to ESP RainMaker Cloud */ + RMAKER_EVENT_USER_NODE_MAPPING_RESET, + /** Local control stopped. */ + RMAKER_EVENT_LOCAL_CTRL_STOPPED +} esp_rmaker_event_t; + +/** ESP RainMaker Node information */ +typedef struct { + /** Name of the Node */ + char *name; + /** Type of the Node */ + char *type; + /** Firmware Version (Optional). If not set, PROJECT_VER is used as default (recommended)*/ + char *fw_version; + /** Model (Optional). If not set, PROJECT_NAME is used as default (recommended)*/ + char *model; + /** Subtype (Optional). */ + char *subtype; + /** An array of digests read from efuse. Should be freed after use*/ + char **secure_boot_digest; +} esp_rmaker_node_info_t; + +/** ESP RainMaker Configuration */ +typedef struct { + /** Enable Time Sync + * Setting this true will enable SNTP and fetch the current time before + * attempting to connect to the ESP RainMaker service + */ + bool enable_time_sync; +} esp_rmaker_config_t; + +/** ESP RainMaker Parameter Value type */ +typedef enum { + /** Invalid */ + RMAKER_VAL_TYPE_INVALID = 0, + /** Boolean */ + RMAKER_VAL_TYPE_BOOLEAN, + /** Integer. Mapped to a 32 bit signed integer */ + RMAKER_VAL_TYPE_INTEGER, + /** Floating point number */ + RMAKER_VAL_TYPE_FLOAT, + /** NULL terminated string */ + RMAKER_VAL_TYPE_STRING, + /** NULL terminated JSON Object string Eg. {"name":"value"} */ + RMAKER_VAL_TYPE_OBJECT, + /** NULL terminated JSON Array string Eg. [1,2,3] */ + RMAKER_VAL_TYPE_ARRAY, +} esp_rmaker_val_type_t; + +/** ESP RainMaker Value */ +typedef union { + /** Boolean */ + bool b; + /** Integer */ + int i; + /** Float */ + float f; + /** NULL terminated string */ + char *s; +} esp_rmaker_val_t; + +/** ESP RainMaker Parameter Value */ +typedef struct { + /** Type of Value */ + esp_rmaker_val_type_t type; + /** Actual value. Depends on the type */ + esp_rmaker_val_t val; +} esp_rmaker_param_val_t; + +/** Param property flags */ +typedef enum { + PROP_FLAG_WRITE = (1 << 0), + PROP_FLAG_READ = (1 << 1), + PROP_FLAG_TIME_SERIES = (1 << 2), + PROP_FLAG_PERSIST = (1 << 3), + PROP_FLAG_SIMPLE_TIME_SERIES = (1 << 4) +} esp_param_property_flags_t; + +/** System Service Reboot Flag */ +#define SYSTEM_SERV_FLAG_REBOOT (1 << 0) + +/** System Service Factory Reset Flag */ +#define SYSTEM_SERV_FLAG_FACTORY_RESET (1 << 1) + +/** System Service Wi-Fi Reset Flag */ +#define SYSTEM_SERV_FLAG_WIFI_RESET (1 << 2) + +/** System Service All Flags */ +#define SYSTEM_SERV_FLAGS_ALL (SYSTEM_SERV_FLAG_REBOOT | SYSTEM_SERV_FLAG_FACTORY_RESET | SYSTEM_SERV_FLAG_WIFI_RESET) + +/** Generic ESP RainMaker handle */ +typedef size_t esp_rmaker_handle_t; + +/** ESP RainMaker Node Handle */ +typedef esp_rmaker_handle_t esp_rmaker_node_t; + +/** ESP RainMaker Device Handle */ +typedef esp_rmaker_handle_t esp_rmaker_device_t; + +/** ESP RainMaker Parameter Handle */ +typedef esp_rmaker_handle_t esp_rmaker_param_t; + +/** Parameter read/write request source */ +typedef enum { + /** Request triggered in the init sequence i.e. when a value is found + * in persistent memory for parameters with PROP_FLAG_PERSIST. + */ + ESP_RMAKER_REQ_SRC_INIT, + /** Request received from cloud */ + ESP_RMAKER_REQ_SRC_CLOUD, + /** Request received when a schedule has triggered */ + ESP_RMAKER_REQ_SRC_SCHEDULE, + /** Request received when a scene has been activated */ + ESP_RMAKER_REQ_SRC_SCENE_ACTIVATE, + /** Request received when a scene has been deactivated */ + ESP_RMAKER_REQ_SRC_SCENE_DEACTIVATE, + /** Request received from a local controller */ + ESP_RMAKER_REQ_SRC_LOCAL, + /** This will always be the last value. Any value equal to or + * greater than this should be considered invalid. + */ + ESP_RMAKER_REQ_SRC_MAX, +} esp_rmaker_req_src_t; + +/** Write request Context */ +typedef struct { + /** Source of request */ + esp_rmaker_req_src_t src; +} esp_rmaker_write_ctx_t; + +/** Read request context */ +typedef struct { + /** Source of request */ + esp_rmaker_req_src_t src; +} esp_rmaker_read_ctx_t; + +/** System service configuration */ +typedef struct { + /** Logical OR of system service flags (SYSTEM_SERV_FLAG_REBOOT, + * SYSTEM_SERV_FLAG_FACTORY_RESET, SYSTEM_SERV_FLAG_WIFI_RESET) as required + * or SYSTEM_SERV_FLAGS_ALL. + */ + uint16_t flags; + /** Time in seconds after which the device should reboot. + * Value of zero would trigger an immediate reboot if a write is received for + * the Reboot parameter. + * Recommended value: 2 + */ + int8_t reboot_seconds; + /** Time in seconds after which the device should reset (Wi-Fi or factory). + * Value of zero would trigger an immediate action if a write is received for + * the Wi-Fi reset or Factory reset parameter. + * Recommended value: 2 + */ + int8_t reset_seconds; + /** Time in seconds after which the device should reboot after it has been reset. + * Value of zero would mean that there won't be any reboot after the reset. + * Recommended value: 2 + */ + int8_t reset_reboot_seconds; +} esp_rmaker_system_serv_config_t; + + +/** Parameter write request payload */ +typedef struct { + /** Parameter handle */ + esp_rmaker_param_t *param; + /** Value to write */ + esp_rmaker_param_val_t val; +} esp_rmaker_param_write_req_t; + +/** Callback for bulk parameter value write requests. + * + * + * This callback is recommended over esp_rmaker_device_write_cb_t since it gives all values of a given device together, + * which will help if the parameters are related to each other. + * + * The callback should call the esp_rmaker_param_update_and_report() API if the new value is to be set + * and reported back. + * + * @param[in] device Device handle. + * @param[in] write_reqs Array of parameter write request payloads. + * @param[in] count Count of parameters and their values passed to this callback + * @param[in] priv_data Pointer to the private data paassed while creating the device. + * @param[in] ctx Context associated with the request. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +typedef esp_err_t (*esp_rmaker_device_bulk_write_cb_t)(const esp_rmaker_device_t *device, const esp_rmaker_param_write_req_t write_req[], + uint8_t count, void *priv_data, esp_rmaker_write_ctx_t *ctx); + +/** Callback for parameter value write requests. + * + * The callback should call the esp_rmaker_param_update_and_report() API if the new value is to be set + * and reported back. + * + * @param[in] device Device handle. + * @param[in] param Parameter handle. + * @param[in] val Pointer to \ref esp_rmaker_param_val_t. Use appropriate elements as per the value type. + * @param[in] priv_data Pointer to the private data paassed while creating the device. + * @param[in] ctx Context associated with the request. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +typedef esp_err_t (*esp_rmaker_device_write_cb_t)(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx); + +/** Callback for bulk parameter value reads + * + * The callback should call the esp_rmaker_param_update_and_report() API if the new value is to be set + * and reported back. + * + * @note Currently, the read callback never gets invoked as the communication between clients (mobile phones, CLI, etc.) + * and node is asynchronous. So, the read request does not reach the node. This callback may however be used in future. + * + * @param[in] device Device handle. + * @param[in] params Array of Parameter handles. + * @param[in] count Count of parameters passed to this callback. + * @param[in] priv_data Pointer to the private data passed while creating the device. + * @param[in] ctx Context associated with the request. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +typedef esp_err_t (*esp_rmaker_device_bulk_read_cb_t)(const esp_rmaker_device_t *device, const esp_rmaker_param_t *params[], + uint8_t count, void *priv_data, esp_rmaker_read_ctx_t *ctx); + +/** Callback for parameter value reads + * + * The callback should call the esp_rmaker_param_update_and_report() API if the new value is to be set + * and reported back. + * + * @note Currently, the read callback never gets invoked as the communication between clients (mobile phones, CLI, etc.) + * and node is asynchronous. So, the read request does not reach the node. This callback may however be used in future. + * + * @param[in] device Device handle. + * @param[in] param Parameter handle. + * @param[in] priv_data Pointer to the private data passed while creating the device. + * @param[in] ctx Context associated with the request. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +typedef esp_err_t (*esp_rmaker_device_read_cb_t)(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + void *priv_data, esp_rmaker_read_ctx_t *ctx); + +/** Convert device callback source to string + * + * Device read/write callback can be via different sources. This is a helper API + * to give the source in string format for printing. + * + * Example Usage: + * @code{c} + * static esp_err_t write_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + * const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx) +{ + if (ctx) { + ESP_LOGI(TAG, "Received write request via : %s", esp_rmaker_device_cb_src_to_str(ctx->src)); + } + * @endcode + * + * @param[in] src The src field as received in the callback context. + * + * @return NULL terminated source string on success + * @return NULL on failure + */ +const char *esp_rmaker_device_cb_src_to_str(esp_rmaker_req_src_t src); + +/** + * Initialise a Boolean value + * + * @param[in] bval Initialising value. + * + * @return Value structure. + */ +esp_rmaker_param_val_t esp_rmaker_bool(bool bval); + +/** + * Initialise an Integer value + * + * @param[in] ival Initialising value. + * + * @return Value structure. + */ +esp_rmaker_param_val_t esp_rmaker_int(int ival); + +/** + * Initialise a Float value + * + * @param[in] fval Initialising value. + * + * @return Value structure. + */ +esp_rmaker_param_val_t esp_rmaker_float(float fval); + +/** + * Initialise a String value + * + * @param[in] sval Initialising value. + * + * @return Value structure. + */ +esp_rmaker_param_val_t esp_rmaker_str(const char *sval); + +/** + * Initialise a json object value + * + * @note the object will not be validated internally. it is the application's + * responsibility to ensure that the object is a valid json object. + * eg. esp_rmaker_obj("{\"name\":\"value\"}"); + * + * param[in] val initialising value + * + * return value structure + */ +esp_rmaker_param_val_t esp_rmaker_obj(const char *val); + +/** + * Initialise a json array value + * + * @note the array will not be validated internally. it is the application's + * responsibility to ensure that the array is a valid json array. + * eg. esp_rmaker_array("[1,2,3]"); + * + * param[in] val initialising value + * + * return value structure + */ +esp_rmaker_param_val_t esp_rmaker_array(const char *val); + + +/** Initialize ESP RainMaker Node + * + * This initializes the ESP RainMaker agent and creates the node. + * The model and firmware version for the node are set internally as per + * the project name and version. These can be overridden (but not recommended) using the + * esp_rmaker_node_add_fw_version() and esp_rmaker_node_add_model() APIs. + * + * @note This should be the first call before using any other ESP RainMaker API. + * + * @param[in] config Configuration to be used by the ESP RainMaker. + * @param[in] name Name of the node. + * @param[in] type Type of the node. + * + * @return Node handle on success. + * @return NULL in case of failure. + */ +esp_rmaker_node_t *esp_rmaker_node_init(const esp_rmaker_config_t *config, const char *name, const char *type); + +/** Start ESP RainMaker Agent + * + * This call starts the actual ESP RainMaker thread. This should preferably be called after a + * successful Wi-Fi connection in order to avoid unnecessary failures. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_start(void); + +/** Stop ESP RainMaker Agent + * + * This call stops the ESP RainMaker Agent instance started earlier by esp_rmaker_start(). + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_stop(void); + +/** Deinitialize ESP RainMaker Node + * + * This API deinitializes the ESP RainMaker agent and the node created using esp_rmaker_node_init(). + * + * @note This should be called after rainmaker has stopped. + * + * @param[in] node Node Handle returned by esp_rmaker_node_init(). + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_node_deinit(const esp_rmaker_node_t *node); + +/** Get a handle to the Node + * + * This API returns handle to a node created using esp_rmaker_node_init(). + * + * @return Node handle on success. + * @return NULL in case of failure. + */ +const esp_rmaker_node_t *esp_rmaker_get_node(void); + +/** Get Node Id + * + * Returns pointer to the NULL terminated Node ID string. + * + * @return Pointer to a NULL terminated Node ID string. + */ +char *esp_rmaker_get_node_id(void); + +/** Get Node Info + * + * Returns pointer to the node info as configured during initialisation. + * + * @param node Node handle. + * + * @return Pointer to the node info on success. + * @return NULL in case of failure. + */ +esp_rmaker_node_info_t *esp_rmaker_node_get_info(const esp_rmaker_node_t *node); + +/** Add Node attribute + * + * Adds a new attribute as the metadata for the node. For the sake of simplicity, + * only string values are allowed. + * + * @param node Node handle. + * @param[in] attr_name Name of the attribute. + * @param[in] val Value for the attribute. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_node_add_attribute(const esp_rmaker_node_t *node, const char *attr_name, const char *val); + +/** Add FW version for a node (Not recommended) + * + * FW version is set internally to the project version. This API can be used to + * override that version. + * + * @param node Node handle. + * @param[in] fw_version New firmware version. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_node_add_fw_version(const esp_rmaker_node_t *node, const char *fw_version); + +/** Add model for a node + * + * Model is set internally to the project name. This API can be used to + * override that name, now that a new field "project" has also been added + * internally to the node info. + * + * @param node Node handle. + * @param[in] model New model string. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_node_add_model(const esp_rmaker_node_t *node, const char *model); + +/** Add subtype for a node + * + * @param node Node handle. + * @param[in] subtype Subtype string. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_node_add_subtype(const esp_rmaker_node_t *node, const char *subtype); + +/** + * Create a Device + * + * This API will create a virtual "Device". + * This could be something like a Switch, Lightbulb, etc. + * + * @note The device created needs to be added to a node using esp_rmaker_node_add_device(). + * + * @param[in] dev_name The unique device name. + * @param[in] type Optional device type. Can be kept NULL. + * @param[in] priv_data (Optional) Private data associated with the device. This will be passed to callbacks. + * It should stay allocated throughout the lifetime of the device. + * + * @return Device handle on success. + * @return NULL in case of any error. + */ +esp_rmaker_device_t *esp_rmaker_device_create(const char *dev_name, const char *type, void *priv_data); + +/** + * Create a Service + * + * This API will create a "Service". It is exactly same like a device in terms of structure and so, all + * APIs for device are also valid for a service. + * A service could be something like OTA, diagnostics, etc. + * + * @note Name of a service should not clash with name of a device. + * @note The service created needs to be added to a node using esp_rmaker_node_add_device(). + * + * @param[in] serv_name The unique service name. + * @param[in] type Optional service type. Can be kept NULL. + * @param[in] priv_data (Optional) Private data associated with the service. This will be passed to callbacks. + * It should stay allocated throughout the lifetime of the device. + * + * @return Device handle on success. + * @return NULL in case of any error. + */ +esp_rmaker_device_t *esp_rmaker_service_create(const char *serv_name, const char *type, void *priv_data); + +/** + * Delete a Device/Service + * + * This API will delete a device created using esp_rmaker_device_create(). + * + * @note The device should first be removed from the node using esp_rmaker_node_remove_device() before deleting. + * + * @param[in] device Device handle. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_device_delete(const esp_rmaker_device_t *device); + +/** + * Add callbacks for a device/service + * + * Add read/write callbacks for a device that will be invoked as per requests received from the cloud (or other paths + * as may be added in future). + * + * @param[in] device Device handle. + * @param[in] write_cb Write callback. + * @param[in] read_cb Read callback. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_device_add_cb(const esp_rmaker_device_t *device, esp_rmaker_device_write_cb_t write_cb, esp_rmaker_device_read_cb_t read_cb); + +/** + * Add bulk callbacks for a device/service + * + * Add bulk read/write callbacks for a device that will be invoked as per requests received from the cloud (or other paths + * as may be added in future). + * + * This is an improvement over the earlier callbacks registered using esp_rmaker_device_add_cb() so that all parameters + * received in a single request are passed to the callback together, instead of one by one. + * + * @param[in] device Device handle. + * @param[in] write_cb Bulk Write callback. + * @param[in] read_cb Bulk Read callback. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_device_add_bulk_cb(const esp_rmaker_device_t *device, esp_rmaker_device_bulk_write_cb_t write_cb, esp_rmaker_device_bulk_read_cb_t read_cb); + +/** + * Add a device to a node + * + * @param[in] node Node handle. + * @param[in] device Device handle. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_node_add_device(const esp_rmaker_node_t *node, const esp_rmaker_device_t *device); + +/** + * Remove a device from a node + * + * @param[in] node Node handle. + * @param[in] device Device handle. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_node_remove_device(const esp_rmaker_node_t *node, const esp_rmaker_device_t *device); + +/** Get device by name + * + * Get handle for a device based on the name. + * + * @param[in] node Node handle. + * @param[in] device_name Device name to search. + * + * @return Device handle on success. + * @return NULL in case of failure. + */ +esp_rmaker_device_t *esp_rmaker_node_get_device_by_name(const esp_rmaker_node_t *node, const char *device_name); + +/** Add a Device attribute + * + * @note Device attributes are reported only once after a boot-up as part of the node + * configuration. + * Eg. Serial Number + * + * @param[in] device Device handle. + * @param[in] attr_name Name of the attribute. + * @param[in] val Value of the attribute. + * + * @return ESP_OK if the attribute was added successfully. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_device_add_attribute(const esp_rmaker_device_t *device, const char *attr_name, const char *val); + +/** Add a Device subtype + * + * This can be something like esp.subtype.rgb-light for a device of type esp.device.lightbulb. + * This would primarily be used by the phone apps to render different icons for the same device type. + * + * @param[in] device Device handle. + * @param[in] subtype String describing the sub type. + * + * @return ESP_OK if the subtype was added successfully. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_device_add_subtype(const esp_rmaker_device_t *device, const char *subtype); + +/** Add a Device model + * + * This would primarily be used by the phone apps to render different icons for the same device type. + * + * @param[in] device Device handle. + * @param[in] model String describing the model. + * + * @return ESP_OK if the model was added successfully. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_device_add_model(const esp_rmaker_device_t *device, const char *model); + +/** Get device name from handle + * + * @param[in] device Device handle. + * + * @return NULL terminated device name string on success. + * @return NULL in case of failure. + */ +char *esp_rmaker_device_get_name(const esp_rmaker_device_t *device); + +/** Get Device Private data from handle + * + * @param[in] device Device handle. + * + * @return void type of pointer on success. + * @return NULL if no private data found. + */ +void *esp_rmaker_device_get_priv_data(const esp_rmaker_device_t *device); + +/** Get device type from handle + * + * @param[in] device Device handle. + * + * @return NULL terminated device type string on success. + * @return NULL in case of failure, or if the type wasn't provided while creating the device. + */ +char *esp_rmaker_device_get_type(const esp_rmaker_device_t *device); + +/** + * Add a parameter to a device/service + * + * @param[in] device Device handle. + * @param[in] param Parameter handle. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_device_add_param(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param); + + +/** Get parameter by type + * + * Get handle for a parameter based on the type. + * + * @note If there are multiple parameters with the same type, this will return the first one. The API + * esp_rmaker_device_get_param_by_name() can be used to get a specific parameter, because the parameter + * names in a device are unique. + * + * @param[in] device Device handle. + * @param[in] param_type Parameter type to search. + * + * @return Parameter handle on success. + * @return NULL in case of failure. + */ +esp_rmaker_param_t *esp_rmaker_device_get_param_by_type(const esp_rmaker_device_t *device, const char *param_type); + +/** Get parameter by name + * + * Get handle for a parameter based on the name. + * + * @param[in] device Device handle. + * @param[in] param_name Parameter name to search. + * + * @return Parameter handle on success. + * @return NULL in case of failure. + */ +esp_rmaker_param_t *esp_rmaker_device_get_param_by_name(const esp_rmaker_device_t *device, const char *param_name); + +/** Assign a primary parameter + * + * Assign a parameter (already added using esp_rmaker_device_add_param()) as a primary parameter, + * which can be used by clients (phone apps specifically) to give prominence to it. + * + * @param[in] device Device handle. + * @param[in] param Parameter handle. + * + * @return ESP_OK if the parameter was assigned as the primary successfully. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_device_assign_primary_param(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param); + +/** + * Create a Parameter + * + * Parameter can be something like Temperature, Outlet state, Lightbulb brightness, etc. + * + * Any changes should be reported using the esp_rmaker_param_update_and_report() API. + * Any remote changes will be reported to the application via the device callback, if registered. + * + * @note The parameter created needs to be added to a device using esp_rmaker_device_add_param(). + * Parameter name should be unique in a given device. + * + * @param[in] param_name Name of the parameter. + a* @param[in] type Optional parameter type. Can be kept NULL. + * @param[in] val Value of the parameter. This also specifies the type that will be assigned + * to this parameter. You can use esp_rmaker_bool(), esp_rmaker_int(), esp_rmaker_float() + * or esp_rmaker_str() functions as the argument here. Eg, esp_rmaker_bool(true). + * @param[in] properties Properties of the parameter, which will be a logical OR of flags in + * \ref esp_param_property_flags_t. + * + * @return Parameter handle on success. + * @return NULL in case of failure. + */ +esp_rmaker_param_t *esp_rmaker_param_create(const char *param_name, const char *type, + esp_rmaker_param_val_t val, uint8_t properties); + +/** + * Add a UI Type to a parameter + * + * This will be used by the Phone apps (or other clients) to render appropriate UI for the given + * parameter. Please refer the RainMaker documetation for supported UI Types. + * + * @param[in] param Parameter handle. + * @param[in] ui_type String describing the UI Type. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_param_add_ui_type(const esp_rmaker_param_t *param, const char *ui_type); + +/** + * Add bounds for an integer/float parameter + * + * This can be used to add bounds (min/max values) for a given integer parameter. Eg. brightness + * will have bounds as 0 and 100 if it is a percentage. + * Eg. esp_rmaker_param_add_bounds(brightness_param, esp_rmaker_int(0), esp_rmaker_int(100), esp_rmaker_int(5)); + * + * @note The RainMaker core does not check the bounds. It is upto the application to handle it. + * + * @param[in] param Parameter handle. + * @param[in] min Minimum allowed value. + * @param[in] max Maximum allowed value. + * @param[in] step Minimum stepping (set to 0 if no specific value is desired). + * + * @return ESP_OK on success. + * return error in case of failure. + */ +esp_err_t esp_rmaker_param_add_bounds(const esp_rmaker_param_t *param, + esp_rmaker_param_val_t min, esp_rmaker_param_val_t max, esp_rmaker_param_val_t step); + +/** + * Add a list of valid strings for a string parameter + * + * This can be used to add a list of valid strings for a given string parameter. + * + * Eg. + * static const char *valid_strs[] = {"None","Yes","No","Can't Say"}; + * esp_rmaker_param_add_valid_str_list(param, valid_strs, 4); + * + * @note The RainMaker core does not check the values. It is upto the application to handle it. + * + * @param[in] param Parameter handle. + * @param[in] strs Pointer to an array of strings. Note that this memory should stay allocated + * throughout the lifetime of this parameter. + * @param[in] count Number of strings in the above array. + * + * @return ESP_OK on success. + * return error in case of failure. + */ +esp_err_t esp_rmaker_param_add_valid_str_list(const esp_rmaker_param_t *param, const char *strs[], uint8_t count); + +/** Add max count for an array parameter + * + * This can be used to put a limit on the maximum number of elements in an array. + * + * @note The RainMaker core does not check the values. It is upto the application to handle it. + * + * @param[in] param Parameter handle. + * @param[in] count Max number of elements allowed in the array. + * + * @return ESP_OK on success. + * return error in case of failure. + */ +esp_err_t esp_rmaker_param_add_array_max_count(const esp_rmaker_param_t *param, int count); + + +/* Update a parameter + * + * This will just update the value of a parameter with esp rainmaker core, without actually reporting + * it. This can be used when multiple parameters need to be reported together. + * Eg. If x parameters are to be reported, this API can be used for the first x -1 parameters + * and the last one can be updated using esp_rmaker_param_update_and_report(). + * This will report all parameters which were updated prior to this call. + * + * @note This does not report to time series even if PROP_FLAG_TIME_SERIES is set. + * + * Sample: + * + * esp_rmaker_param_update(param1, esp_rmaker_float(10.2)); + * esp_rmaker_param_update(param2, esp_rmaker_int(55)); + * esp_rmaker_param_update(param3, esp_rmaker_int(95)); + * esp_rmaker_param_update_and_report(param1, esp_rmaker_bool(true)); + * + * @param[in] param Parameter handle. + * @param[in] val New value of the parameter. + * + * @return ESP_OK if the parameter was updated successfully. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_param_update(const esp_rmaker_param_t *param, esp_rmaker_param_val_t val); + +/** Update and report a parameter + * + * Calling this API will update the parameter and report it to ESP RainMaker cloud. + * This should be used whenever there is any local change. + * + * @param[in] param Parameter handle. + * @param[in] val New value of the parameter. + * + * @return ESP_OK if the parameter was updated successfully. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_param_update_and_report(const esp_rmaker_param_t *param, esp_rmaker_param_val_t val); + +/** Update and notify a parameter + * + * Calling this API will update the parameter and report it to ESP RainMaker cloud similar to + * esp_rmaker_param_update_and_report(). However, additionally, it will also trigger a notification + * on the phone apps (if enabled). + * + * @note This should be used only when some local change requires explicit notification even when the + * phone app is in background, not otherwise. + * Eg. Alarm got triggered, temperature exceeded some threshold, etc. + * + * Alternatively, the esp_rmaker_raise_alert() API can also be used to trigger notification + * on the phone apps with pre-formatted text. + * + * @param[in] param Parameter handle. + * @param[in] val New value of the parameter. + * + * @return ESP_OK if the parameter was updated successfully. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_param_update_and_notify(const esp_rmaker_param_t *param, esp_rmaker_param_val_t val); + +/** Trigger an alert on the phone app + * + * This API will trigger a notification alert on the phone apps (if enabled) using the formatted text + * provided. Note that this does not send a notification directly to the phone, but reports the alert + * to the ESP RainMaker cloud which then uses the Notification framework to send notifications to the + * phone apps. The value does not get stored anywhere, nor is it linked to any node parameters. + * + * @note This should be used only if some event requires explicitly alerting the user even when the + * phone app is in background, not otherwise. + * Eg. "Motion Detected", "Fire alarm triggered" + * + * @param[in] alert_str NULL terminated pre-formatted alert string. + * Maximum length can be ESP_RMAKER_MAX_ALERT_LEN, excluding NULL character. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_raise_alert(const char *alert_str); + +/** Get parameter name from handle + * + * @param[in] param Parameter handle. + * + * @return NULL terminated parameter name string on success. + * @return NULL in case of failure. + */ +char *esp_rmaker_param_get_name(const esp_rmaker_param_t *param); + +/** Get parameter type from handle + * + * @param[in] param Parameter handle. + * + * @return NULL terminated parameter type string on success. + * @return NULL in case of failure, or if the type wasn't provided while creating the parameter. + */ +char *esp_rmaker_param_get_type(const esp_rmaker_param_t *param); + +/** Get parameter value + * + * This gives the parameter value that is stored in the RainMaker core. + * + * @note This does not call any explicit functions to read value from hardware/driver. + * + * @param[in] param Parameter handle + * + * @return Pointer to parameter value on success. + * @return NULL in case of failure. + */ +esp_rmaker_param_val_t *esp_rmaker_param_get_val(esp_rmaker_param_t *param); + +/** Report the node details to the cloud + * + * This API reports node details i.e. the node configuration and values of all the parameters to the ESP RainMaker cloud. + * Eg. If a new device is created (with some parameters and attributes), then this API should be called after that + * to send the node details to the cloud again and the changes to be reflected in the clients (like phone apps). + * + * @note Please use this API only if you need to create or delete devices after esp_rmaker_start() has already + * been called, for use cases like bridges or hubs. + * + * @return ESP_OK if the node details are successfully queued to be published. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_report_node_details(void); + +/** Enable Timezone Service + * + * This enables the ESP RainMaker standard timezone service which can be used to set + * timezone, either in POSIX or location string format. Please refer the specifications + * for additional details. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_timezone_service_enable(void); + +/** Enable System Service + * + * This enables the ESP RainMaker standard system service which can be + * used for operations like reboot, factory reset and Wi-Fi reset. + * + * Please refer the specifications for additional details. + * + * @param[in] config Configuration for the system service. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_system_service_enable(esp_rmaker_system_serv_config_t *config); + +/** + * Check if local_ctrl service has started + * + * @return true if service has started + * @return false if the service has not started + */ +bool esp_rmaker_local_ctrl_service_started(void); + +/** + * Enable Default RainMaker OTA Firmware Upgrade + * + * This enables the default recommended RainMaker OTA Firmware Upgrade, which is + * "Using the Topics", which allows performing OTA from Dashboard. + * This OTA can be triggered by Admin Users only. + * On Public RainMaker deployment, for nodes using "Self Claiming", since there + * is no associated admin user, the Primary user will automatically become the admin + * and can perform OTA from dashboard. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_ota_enable_default(void); + +/* + * Send a command to self (TESTING only) + * + * This is to be passed as an argument to esp_rmaker_cmd_resp_test_send(). + * + * @param[in] cmd The TLV encoded command data. + * @param[in] cmd_len Length of the command data. + * @param[in] priv_data Private data passed to esp_rmaker_cmd_resp_test_send(). + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_test_cmd_resp(const void *cmd, size_t cmd_len, void *priv_data); + +/** This API signs the challenge with RainMaker private key. +* +* @param[in] challenge Pointer to the data to be signed +* @param[in] inlen Length of the challenge +* @param[out] response Pointer to the signature. +* @param[out] outlen Length of the signature +* +* @return ESP_OK on success. response is dynamically allocated, free the response on success. +* @return Apt error on failure. +*/ +esp_err_t esp_rmaker_node_auth_sign_msg(const void *challenge, size_t inlen, void **response, size_t *outlen); + +/* + * @brief Enable Local Control Service. + * + * This enables local control service, which allows users to + * control their device without internet connection. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_local_ctrl_enable(void); + +/* + * @brief Disable Local Control Service. + * + * This will free the memory used by local control service and remove + * local control service from the node. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_local_ctrl_disable(void); +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_mqtt.h b/components/esp_rainmaker/include/esp_rmaker_mqtt.h new file mode 100644 index 0000000..b329489 --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_mqtt.h @@ -0,0 +1,125 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +esp_rmaker_mqtt_conn_params_t *esp_rmaker_mqtt_get_conn_params(void); + +/** Initialize ESP RainMaker MQTT + * + * @param[in] conn_params The MQTT configuration data + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +esp_err_t esp_rmaker_mqtt_init(esp_rmaker_mqtt_conn_params_t *conn_params); + +/* Deinitialize ESP RainMaker MQTT + * + * Call this function after MQTT has disconnected. + */ +void esp_rmaker_mqtt_deinit(void); + +/** MQTT Connect + * + * Starts the connection attempts to the MQTT broker as per the configuration + * provided during initializing. + * This should ideally be called after successful network connection. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +esp_err_t esp_rmaker_mqtt_connect(void); + +/** MQTT Disconnect + * + * Disconnects from the MQTT broker. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +esp_err_t esp_rmaker_mqtt_disconnect(void); + +/** Publish MQTT Message + * + * @param[in] topic The MQTT topic on which the message should be published. + * @param[in] data Data to be published + * @param[in] data_len Length of the data + * @param[in] qos Quality of Service for the Publish. Can be 0, 1 or 2. Also depends on what the MQTT broker supports. + * @param[out] msg_id msg_id for tracking if message is queued + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +esp_err_t esp_rmaker_mqtt_publish(const char *topic, void *data, size_t data_len, uint8_t qos, int *msg_id); + +/** Subscribe to MQTT topic + * + * @param[in] topic The topic to be subscribed to. + * @param[in] cb The callback to be invoked when a message is received on the given topic. + * @param[in] priv_data Optional private data to be passed to the callback + * @param[in] qos Quality of Service for the Subscription. Can be 0, 1 or 2. Also depends on what the MQTT broker supports. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +esp_err_t esp_rmaker_mqtt_subscribe(const char *topic, esp_rmaker_mqtt_subscribe_cb_t cb, uint8_t qos, void *priv_data); + +/** Unsubscribe from MQTT topic + * + * @param[in] topic Topic from which to unsubscribe. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +esp_err_t esp_rmaker_mqtt_unsubscribe(const char *topic); +esp_err_t esp_rmaker_mqtt_setup(esp_rmaker_mqtt_config_t mqtt_config); + +/** Creates appropriate MQTT Topic String based on CONFIG_ESP_RMAKER_MQTT_USE_BASIC_INGEST_TOPICS + * @param[out] buf Buffer to hold topic string + * @param[in] buf_size Size of buffer + * @param[in] topic_suffix MQTT Topic suffix + * @param[in] rule Basic Ingests Rule Name +*/ +void esp_rmaker_create_mqtt_topic(char *buf, size_t buf_size, const char *topic_suffix, const char *rule); + +/** + * @brief Check if budget is available to publish an mqtt message + * + * @return true if budget is available + * @return false if budget is exhausted + * + * @note `esp_rmaker_mqtt_publish` API already does this check. In addition to that, + * some use-cases might still need to check for this. + */ +bool esp_rmaker_mqtt_is_budget_available(void); + +/** + * @brief Check if device is connected to MQTT Server + * + * @return true if device is connected + * @return false if device is not connected + * + */ +bool esp_rmaker_is_mqtt_connected(); +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_ota.h b/components/esp_rainmaker/include/esp_rmaker_ota.h new file mode 100644 index 0000000..e7a9355 --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_ota.h @@ -0,0 +1,272 @@ +/* + * SPDX-FileCopyrightText: 2020-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** @cond **/ +/** ESP RainMaker Event Base */ +ESP_EVENT_DECLARE_BASE(RMAKER_OTA_EVENT); +/** @endcond **/ + +/** ESP RainMaker Events */ +typedef enum { + /* Invalid event. Used for internal handling only */ + RMAKER_OTA_EVENT_INVALID = 0, + /** RainMaker OTA is Starting */ + RMAKER_OTA_EVENT_STARTING, + /** RainMaker OTA has Started */ + RMAKER_OTA_EVENT_IN_PROGRESS, + /** RainMaker OTA Successful */ + RMAKER_OTA_EVENT_SUCCESSFUL, + /** RainMaker OTA Failed */ + RMAKER_OTA_EVENT_FAILED, + /** RainMaker OTA Rejected */ + RMAKER_OTA_EVENT_REJECTED, + /** RainMaker OTA Delayed */ + RMAKER_OTA_EVENT_DELAYED, + /** OTA Image has been flashed and active partition changed. Reboot is requested. Applicable only if Auto reboot is disabled **/ + RMAKER_OTA_EVENT_REQ_FOR_REBOOT, +} esp_rmaker_ota_event_t; + +/** Default ESP RainMaker OTA Server Certificate */ +extern const char *ESP_RMAKER_OTA_DEFAULT_SERVER_CERT; + +/** OTA Status to be reported to ESP RainMaker Cloud */ +typedef enum { + /** OTA is in Progress. This can be reported multiple times as the OTA progresses. */ + OTA_STATUS_IN_PROGRESS = 1, + /** OTA Succeeded. This should be reported only once, at the end of OTA. */ + OTA_STATUS_SUCCESS, + /** OTA Failed. This should be reported only once, at the end of OTA. */ + OTA_STATUS_FAILED, + /** OTA was delayed by the application */ + OTA_STATUS_DELAYED, + /** OTA rejected due to some reason (wrong project, version, etc.) */ + OTA_STATUS_REJECTED, +} ota_status_t; + +/** OTA Workflow type */ +typedef enum { + /** OTA will be performed using services and parameters. */ + OTA_USING_PARAMS = 1, + /** OTA will be performed using pre-defined MQTT topics. */ + OTA_USING_TOPICS +} esp_rmaker_ota_type_t; + +/** The OTA Handle to be used by the OTA callback */ +typedef void *esp_rmaker_ota_handle_t; + +/** OTA Data */ +typedef struct { + /** The OTA URL received from ESP RainMaker Cloud */ + char *url; + /** Size of the OTA File. Can be 0 if the file size isn't received from + * the ESP RainMaker Cloud */ + int filesize; + /** The firmware version of the OTA image **/ + char *fw_version; + /** The OTA Job ID received from cloud **/ + char *ota_job_id; + /** The server certificate passed in esp_rmaker_enable_ota() */ + const char *server_cert; + /** The private data passed in esp_rmaker_enable_ota() */ + char *priv; + /** OTA Metadata. Applicable only for OTA using Topics. Will be received (if applicable) from the backend, along with the OTA URL */ + char *metadata; +} esp_rmaker_ota_data_t; + +/** Function prototype for OTA Callback + * + * This function will be invoked by the ESP RainMaker core whenever an OTA is available. + * The esp_rmaker_report_ota_status() API should be used to indicate the progress and + * success/fail status. + * + * @param[in] handle An OTA handle assigned by the ESP RainMaker Core + * @param[in] ota_data The data to be used for the OTA + * + * @return ESP_OK if the OTA was successful + * @return ESP_FAIL if the OTA failed. + */ +typedef esp_err_t (*esp_rmaker_ota_cb_t) (esp_rmaker_ota_handle_t handle, + esp_rmaker_ota_data_t *ota_data); + +typedef enum { + /** OTA Diagnostics Failed. Rollback the firmware. */ + OTA_DIAG_STATUS_FAIL, + /** OTA Diagnostics Pending. Additional validations will be done later. */ + OTA_DIAG_STATUS_PENDING, + /** OTA Diagnostics Succeeded. Firmware can be considered valid. */ + OTA_DIAG_STATUS_SUCCESS +} esp_rmaker_ota_diag_status_t; + +typedef enum { + /** OTA State: Initialised. */ + OTA_DIAG_STATE_INIT, + /** OTA state: MQTT has connected. */ + OTA_DIAG_STATE_POST_MQTT +} esp_rmaker_ota_diag_state_t; + +typedef struct { + /** OTA diagnostic state */ + esp_rmaker_ota_diag_state_t state; + /** Flag to indicate whether the OTA which has triggered the Diagnostics checks for rollback + * was triggered via RainMaker or not. This would be useful only when your application has some + * other mechanism for OTA too. + */ + bool rmaker_ota; +} esp_rmaker_ota_diag_priv_t; + +/** Function Prototype for Post OTA Diagnostics + * + * If the Application rollback feature is enabled, this callback will be invoked + * as soon as you call esp_rmaker_ota_enable(), if it is the first + * boot after an OTA. You may perform some application specific diagnostics and + * report the status which will decide whether to roll back or not. + * + * This will be invoked once again after MQTT has connected, in case some additional validations + * are to be done later. + * + * If OTA state == OTA_DIAG_STATE_INIT, then + * return OTA_DIAG_STATUS_FAIL to indicate failure and rollback. + * return OTA_DIAG_STATUS_SUCCESS or OTA_DIAG_STATUS_PENDING to tell internal OTA logic to continue further. + * + * If OTA state == OTA_DIAG_STATE_POST_MQTT, then + * return OTA_DIAG_STATUS_FAIL to indicate failure and rollback. + * return OTA_DIAG_STATUS_SUCCESS to indicate validation was successful and mark OTA as valid + * return OTA_DIAG_STATUS_PENDING to indicate that some additional validations will be done later + * and the OTA will eventually be marked valid/invalid using esp_rmaker_ota_mark_valid() or + * esp_rmaker_ota_mark_invalid() respectively. + * + * @return esp_rmaker_ota_diag_status_t as applicable + */ +typedef esp_rmaker_ota_diag_status_t (*esp_rmaker_post_ota_diag_t)(esp_rmaker_ota_diag_priv_t *ota_diag_priv, void *priv); + +/** ESP RainMaker OTA Configuration */ +typedef struct { + /** OTA Callback. + * The callback to be invoked when an OTA Job is available. + * If kept NULL, the internal default callback will be used (Recommended). + */ + esp_rmaker_ota_cb_t ota_cb; + /** OTA Diagnostics Callback. + * A post OTA diagnostic handler to be invoked if app rollback feature is enabled. + * If kept NULL, the new firmware will be assumed to be fine, + * and no rollback will be performed. + */ + esp_rmaker_post_ota_diag_t ota_diag; + /** Server Certificate. + * The certificate to be passed to the OTA callback for server authentication. + * This is mandatory, unless you have disabled it in ESP HTTPS OTA config option. + * If you are using the ESP RainMaker OTA Service, you can just set this to + * `ESP_RMAKER_OTA_DEFAULT_SERVER_CERT`. + */ + const char *server_cert; + /** Private Data. + * Optional private data to be passed to the OTA callback. + */ + void *priv; +} esp_rmaker_ota_config_t; + +/** Enable OTA + * + * Calling this API enables OTA as per the ESP RainMaker specification. + * Please check the various ESP RainMaker configuration options to + * use the different variants of OTA. Refer the documentation for + * additional details. + * + * @param[in] ota_config Pointer to an OTA configuration structure + * @param[in] type The OTA workflow type + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_ota_enable(esp_rmaker_ota_config_t *ota_config, esp_rmaker_ota_type_t type); + +/** Report OTA Status + * + * This API must be called from the OTA Callback to indicate the status of the OTA. The OTA_STATUS_IN_PROGRESS + * can be reported multiple times with appropriate additional information. The final success/failure should + * be reported only once, at the end. + * + * This can be ignored if you are using the default internal OTA callback. + * + * @param[in] ota_handle The OTA handle received by the callback + * @param[in] status Status to be reported + * @param[in] additional_info NULL terminated string indicating additional information for the status + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_ota_report_status(esp_rmaker_ota_handle_t ota_handle, ota_status_t status, char *additional_info); + +/** Default OTA callback + * + * This is the default OTA callback which will get used if you do not pass your own callback. You can call this + * even from your callback, in case you want better control on when the OTA can proceed and yet let the actual + * OTA process be managed by the RainMaker Core. + * + * @param[in] handle An OTA handle assigned by the ESP RainMaker Core + * @param[in] ota_data The data to be used for the OTA + * + * @return ESP_OK if the OTA was successful + * @return ESP_FAIL if the OTA failed. + * */ +esp_err_t esp_rmaker_ota_default_cb(esp_rmaker_ota_handle_t handle, esp_rmaker_ota_data_t *ota_data); + +/** Fetch OTA Info + * + * For OTA using Topics, this API can be used to explicitly ask the backend if an OTA is available. + * If it is, then the OTA callback would get invoked. + * + * @return ESP_OK if the OTA fetch publish message was successful. + * @return error on failure + */ +esp_err_t esp_rmaker_ota_fetch(void); + +/** Fetch OTA Info with a delay + * + * For OTA using Topics, this API can be used to explicitly ask the backend if an OTA is available + * after a delay (in seconds) passed as an argument. + * + * @param[in] time Delay (in seconds) + * + * @return ESP_OK if the OTA fetch timer was created. + * @return error on failure + */ +esp_err_t esp_rmaker_ota_fetch_with_delay(int time); + +/** Mark OTA as valid + * + * This should be called if the OTA validation has been kept pending by returning OTA_DIAG_STATUS_PENDING + * in the ota_diag callback and then, the validation was eventually successful. This can also be used to mark + * the OTA valid even before RainMaker core does its own validations (primarily MQTT connection). + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_ota_mark_valid(void); + +/** Mark OTA as invalid + * + * This should be called if the OTA validation has been kept pending by returning OTA_DIAG_STATUS_PENDING + * in the ota_diag callback and then, the validation eventually failed. This can even be used to rollback + * at any point of time before RainMaker core's internal logic and the application's logic mark the OTA + * as valid. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_ota_mark_invalid(void); +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_scenes.h b/components/esp_rainmaker/include/esp_rmaker_scenes.h new file mode 100644 index 0000000..f2f81f5 --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_scenes.h @@ -0,0 +1,38 @@ +// Copyright 2022 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include + +/** Enable Scenes + * + * This API enables the scenes service for the node. For more information, + * check [here](https://rainmaker.espressif.com/docs/scenes.html) + * + * @note This API should be called after esp_rmaker_node_init() but before esp_rmaker_start(). + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_scenes_enable(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_schedule.h b/components/esp_rainmaker/include/esp_rmaker_schedule.h new file mode 100644 index 0000000..383c094 --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_schedule.h @@ -0,0 +1,38 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Enable Schedules + * + * This API enables the scheduling service for the node. For more information, + * check [here](https://rainmaker.espressif.com/docs/scheduling.html) + * + * It is recommended to set the timezone while using schedules. Check [here](https://rainmaker.espressif.com/docs/time-service.html#time-zone) for more information on timezones + * + * @note This API should be called after esp_rmaker_node_init() but before esp_rmaker_start(). + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_schedule_enable(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_standard_devices.h b/components/esp_rainmaker/include/esp_rmaker_standard_devices.h new file mode 100644 index 0000000..db94806 --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_standard_devices.h @@ -0,0 +1,96 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Create a standard Switch device + * + * This creates a Switch device with the mandatory parameters and also assigns + * the primary parameter. The default parameter names will be used. + * Refer \ref esp_rmaker_standard_params.h for default names. + * + * @param[in] dev_name The unique device name + * @param[in] priv_data (Optional) Private data associated with the device. This should stay + * allocated throughout the lifetime of the device + * #@param[in] power Default value of the mandatory parameter "power" + * + * @return Device handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_device_t *esp_rmaker_switch_device_create(const char *dev_name, + void *priv_data, bool power); + +/** Create a standard Lightbulb device + * + * This creates a Lightbulb device with the mandatory parameters and also assigns + * the primary parameter. The default parameter names will be used. + * Refer \ref esp_rmaker_standard_params.h for default names. + * + * @param[in] dev_name The unique device name + * @param[in] priv_data (Optional) Private data associated with the device. This should stay + * allocated throughout the lifetime of the device + * @param[in] power Default value of the mandatory parameter "power" + * + * @return Device handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_device_t *esp_rmaker_lightbulb_device_create(const char *dev_name, + void *priv_data, bool power); + +/** Create a standard Fan device + * + * This creates a Fan device with the mandatory parameters and also assigns + * the primary parameter. The default parameter names will be used. + * Refer \ref esp_rmaker_standard_params.h for default names. + * + * @param[in] dev_name The unique device name + * @param[in] priv_data (Optional) Private data associated with the device. This should stay + * allocated throughout the lifetime of the device + * @param[in] power Default value of the mandatory parameter "power" + * + * @return Device handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_device_t *esp_rmaker_fan_device_create(const char *dev_name, + void *priv_data, bool power); + +/** Create a standard Temperature Sensor device + * + * This creates a Temperature Sensor device with the mandatory parameters and also assigns + * the primary parameter. The default parameter names will be used. + * Refer \ref esp_rmaker_standard_params.h for default names. + * + * @param[in] dev_name The unique device name + * @param[in] priv_data (Optional) Private data associated with the device. This should stay + * allocated throughout the lifetime of the device + * @param[in] temperature Default value of the mandatory parameter "temperature" + * + * @return Device handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_device_t *esp_rmaker_temp_sensor_device_create(const char *dev_name, + void *priv_data, float temperature); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_standard_params.h b/components/esp_rainmaker/include/esp_rmaker_standard_params.h new file mode 100644 index 0000000..f88e104 --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_standard_params.h @@ -0,0 +1,352 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Suggested default names for the parameters. + * These will also be used by default if you use any standard device helper APIs. + * + * @note These names are not mandatory. You can use the ESP RainMaker Core APIs + * to create your own parameters with custom names, if required. + */ + +#define ESP_RMAKER_DEF_NAME_PARAM "Name" +#define ESP_RMAKER_DEF_POWER_NAME "Power" +#define ESP_RMAKER_DEF_BRIGHTNESS_NAME "Brightness" +#define ESP_RMAKER_DEF_HUE_NAME "Hue" +#define ESP_RMAKER_DEF_SATURATION_NAME "Saturation" +#define ESP_RMAKER_DEF_INTENSITY_NAME "Intensity" +#define ESP_RMAKER_DEF_CCT_NAME "CCT" +#define ESP_RMAKER_DEF_DIRECTION_NAME "Direction" +#define ESP_RMAKER_DEF_SPEED_NAME "Speed" +#define ESP_RMAKER_DEF_TEMPERATURE_NAME "Temperature" +#define ESP_RMAKER_DEF_OTA_STATUS_NAME "Status" +#define ESP_RMAKER_DEF_OTA_INFO_NAME "Info" +#define ESP_RMAKER_DEF_OTA_URL_NAME "URL" +#define ESP_RMAKER_DEF_TIMEZONE_NAME "TZ" +#define ESP_RMAKER_DEF_TIMEZONE_POSIX_NAME "TZ-POSIX" +#define ESP_RMAKER_DEF_SCHEDULE_NAME "Schedules" +#define ESP_RMAKER_DEF_SCENES_NAME "Scenes" +#define ESP_RMAKER_DEF_REBOOT_NAME "Reboot" +#define ESP_RMAKER_DEF_FACTORY_RESET_NAME "Factory-Reset" +#define ESP_RMAKER_DEF_WIFI_RESET_NAME "Wi-Fi-Reset" +#define ESP_RMAKER_DEF_LOCAL_CONTROL_POP "POP" +#define ESP_RMAKER_DEF_LOCAL_CONTROL_TYPE "Type" +#define ESP_RMAKER_DEF_ADD_ZIGBEE_DEVICE "Add_zigbee_device" + +/** + * Create standard name param + * + * This will create the standard name parameter. + * This should be added to all devices for which you want a user customisable name. + * The value should be same as the device name. + * + * All standard device creation APIs will add this internally. + * No application registered callback will be called for this parameter, + * and changes will be managed internally. + * + * @param[in] param_name Name of the parameter + * @param[in] val The device name + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_name_param_create(const char *param_name, const char *val); + +/** + * Create standard Power param + * + * This will create the standard power parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_power_param_create(const char *param_name, bool val); + +/** + * Create standard Brightness param + * + * This will create the standard brightness parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_brightness_param_create(const char *param_name, int val); + +/** + * Create standard Hue param + * + * This will create the standard hue parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_hue_param_create(const char *param_name, int val); + +/** + * Create standard Saturation param + * + * This will create the standard saturation parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_saturation_param_create(const char *param_name, int val); + +/** + * Create standard Intensity param + * + * This will create the standard intensity parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_intensity_param_create(const char *param_name, int val); + +/** + * Create standard CCT param + * + * This will create the standard cct parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_cct_param_create(const char *param_name, int val); + +/** + * Create standard Direction param + * + * This will create the standard direction parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_direction_param_create(const char *param_name, int val); + +/** + * Create standard Speed param + * + * This will create the standard speed parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_speed_param_create(const char *param_name, int val); + +/** + * Create standard Temperature param + * + * This will create the standard temperature parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_temperature_param_create(const char *param_name, float val); + +/** + * Create standard OTA Status param + * + * This will create the standard ota status parameter. Default value + * is set internally. + * + * @param[in] param_name Name of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_ota_status_param_create(const char *param_name); + +/** + * Create standard OTA Info param + * + * This will create the standard ota info parameter. Default value + * is set internally. + * + * @param[in] param_name Name of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_ota_info_param_create(const char *param_name); + +/** + * Create standard OTA URL param + * + * This will create the standard ota url parameter. Default value + * is set internally. + * + * @param[in] param_name Name of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_ota_url_param_create(const char *param_name); + +/** + * Create standard Timezone param + * + * This will create the standard timezone parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter (Eg. "Asia/Shanghai"). Can be kept NULL. + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_timezone_param_create(const char *param_name, const char *val); + +/** + * Create standard POSIX Timezone param + * + * This will create the standard posix timezone parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter (Eg. "CST-8"). Can be kept NULL. + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_timezone_posix_param_create(const char *param_name, const char *val); + +/** + * Create standard Schedules param + * + * This will create the standard schedules parameter. Default value + * is set internally. + * + * @param[in] param_name Name of the parameter + * @param[in] max_schedules Maximum number of schedules allowed + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_schedules_param_create(const char *param_name, int max_schedules); + +/** + * Create standard Scenes param + * + * This will create the standard scenes parameter. Default value + * is set internally. + * + * @param[in] param_name Name of the parameter + * @param[in] max_scenes Maximum number of scenes allowed + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_scenes_param_create(const char *param_name, int max_scenes); + +/** + * Create standard Reboot param + * + * This will create the standard reboot parameter. + * Set value to true (via write param) for the action to trigger. + * + * @param[in] param_name Name of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_reboot_param_create(const char *param_name); + +/** + * Create standard Factory Reset param + * + * This will create the standard factory reset parameter. + * Set value to true (via write param) for the action to trigger. + * + * @param[in] param_name Name of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_factory_reset_param_create(const char *param_name); + +/** + * Create standard Wi-Fi Reset param + * + * This will create the standard Wi-Fi Reset parameter. + * Set value to true (via write param) for the action to trigger. + * + * @param[in] param_name Name of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_wifi_reset_param_create(const char *param_name); + +/** + * Create standard Local Control POP param + * + * This will create the standard Local Control POP parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter (Eg. "abcd1234"). Can be kept NULL. + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_local_control_pop_param_create(const char *param_name, const char *val); + +/** + * Create standard Local Control Type param + * + * This will create the standard Local Control security type parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_local_control_type_param_create(const char *param_name, int val); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_standard_services.h b/components/esp_rainmaker/include/esp_rmaker_standard_services.h new file mode 100644 index 0000000..8e52e66 --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_standard_services.h @@ -0,0 +1,124 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Create a standard OTA service + * + * This creates an OTA service with the mandatory parameters. The default parameter names will be used. + * Refer \ref esp_rmaker_standard_params.h for default names. + * + * @param[in] serv_name The unique service name + * @param[in] priv_data (Optional) Private data associated with the service. This should stay + * allocated throughout the lifetime of the service. + * + * @return service_handle on success. + * @return NULL in case of any error. + */ +esp_rmaker_device_t *esp_rmaker_ota_service_create(const char *serv_name, void *priv_data); + +/** Create a standard Time service + * + * This creates a Time service with the mandatory parameters. The default parameter names will be used. + * Refer \ref esp_rmaker_standard_params.h for default names. + * + * @param[in] serv_name The unique service name + * @param[in] timezone Default value of timezone string (Eg. "Asia/Shanghai"). Can be kept NULL. + * @param[in] timezone_posix Default value of posix timezone string (Eg. "CST-8"). Can be kept NULL. + * @param[in] priv_data (Optional) Private data associated with the service. This should stay + * allocated throughout the lifetime of the service. + * + * @return service_handle on success. + * @return NULL in case of any error. + */ +esp_rmaker_device_t *esp_rmaker_time_service_create(const char *serv_name, const char *timezone, + const char *timezone_posix, void *priv_data); + +/** Create a standard Schedule service + * + * This creates a Schedule service with the mandatory parameters. The default parameter names will be used. + * Refer \ref esp_rmaker_standard_params.h for default names. + * + * @param[in] serv_name The unique service name + * @param[in] write_cb Write callback. + * @param[in] read_cb Read callback. + * @param[in] max_schedules Maximum number of schedules supported. + * @param[in] priv_data (Optional) Private data associated with the service. This should stay + * allocated throughout the lifetime of the service. + * + * @return service_handle on success. + * @return NULL in case of any error. + */ +esp_rmaker_device_t *esp_rmaker_create_schedule_service(const char *serv_name, esp_rmaker_device_write_cb_t write_cb, esp_rmaker_device_read_cb_t read_cb, int max_schedules, void *priv_data); + +/** Create a standard Scenes service + * + * This creates a Scenes service with the mandatory parameters. The default parameter names will be used. + * Refer \ref esp_rmaker_standard_params.h for default names. + * + * @param[in] serv_name The unique service name + * @param[in] write_cb Write callback. + * @param[in] read_cb Read callback. + * @param[in] max_scenes Maximum number of scenes supported. + * @param[in] deactivation_support Deactivation callback support. + * @param[in] priv_data (Optional) Private data associated with the service. This should stay + * allocated throughout the lifetime of the service. + * + * @return service_handle on success. + * @return NULL in case of any error. + */ +esp_rmaker_device_t *esp_rmaker_create_scenes_service(const char *serv_name, esp_rmaker_device_write_cb_t write_cb, esp_rmaker_device_read_cb_t read_cb, int max_scenes, bool deactivation_support, void *priv_data); + +/** Create a standard System service + * + * This creates an empty System service. Appropriate parameters should be added by the caller. + * + * @param[in] serv_name The unique service name + * @param[in] priv_data (Optional) Private data associated with the service. This should stay + * allocated throughout the lifetime of the service. + * + * @return service_handle on success. + * @return NULL in case of any error. + */ + +esp_rmaker_device_t *esp_rmaker_create_system_service(const char *serv_name, void *priv_data); + +/** Create a standard Local Control service + * + * This creates a Local Control service with the mandatory parameters. The default parameter names will be used. + * Refer \ref esp_rmaker_standard_params.h for default names. + * + * @param[in] serv_name The unique service name + * @param[in] pop Proof of possession + * @param[in] sec_type Security type + * @param[in] priv_data (Optional) Private data associated with the service. This should stay + * allocated throughout the lifetime of the service. + * + * @return service_handle on success. + * @return NULL in case of any error. + */ +esp_rmaker_device_t *esp_rmaker_create_local_control_service(const char *serv_name, const char *pop, int sec_type, void *priv_data); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_standard_types.h b/components/esp_rainmaker/include/esp_rmaker_standard_types.h new file mode 100644 index 0000000..e49381a --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_standard_types.h @@ -0,0 +1,102 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once + +#ifdef __cplusplus +extern "C" +{ +#endif + +/********** STANDARD UI TYPES **********/ + +#define ESP_RMAKER_UI_TOGGLE "esp.ui.toggle" +#define ESP_RMAKER_UI_SLIDER "esp.ui.slider" +#define ESP_RMAKER_UI_DROPDOWN "esp.ui.dropdown" +#define ESP_RMAKER_UI_TEXT "esp.ui.text" +#define ESP_RMAKER_UI_HUE_SLIDER "esp.ui.hue-slider" +#define ESP_RMAKER_UI_HUE_CIRCLE "esp.ui.hue-circle" +#define ESP_RMAKER_UI_PUSHBUTTON "esp.ui.push-btn-big" +#define ESP_RMAKER_UI_TRIGGER "esp.ui.trigger" +#define ESP_RMAKER_UI_HIDDEN "esp.ui.hidden" +#define ESP_RMAKER_UI_QR_SCAN "esp.ui.qr-scan" + +/********** STANDARD PARAM TYPES **********/ + +#define ESP_RMAKER_PARAM_NAME "esp.param.name" +#define ESP_RMAKER_PARAM_POWER "esp.param.power" +#define ESP_RMAKER_PARAM_BRIGHTNESS "esp.param.brightness" +#define ESP_RMAKER_PARAM_HUE "esp.param.hue" +#define ESP_RMAKER_PARAM_SATURATION "esp.param.saturation" +#define ESP_RMAKER_PARAM_INTENSITY "esp.param.intensity" +#define ESP_RMAKER_PARAM_CCT "esp.param.cct" +#define ESP_RMAKER_PARAM_SPEED "esp.param.speed" +#define ESP_RMAKER_PARAM_DIRECTION "esp.param.direction" +#define ESP_RMAKER_PARAM_TEMPERATURE "esp.param.temperature" +#define ESP_RMAKER_PARAM_OTA_STATUS "esp.param.ota_status" +#define ESP_RMAKER_PARAM_OTA_INFO "esp.param.ota_info" +#define ESP_RMAKER_PARAM_OTA_URL "esp.param.ota_url" +#define ESP_RMAKER_PARAM_TIMEZONE "esp.param.tz" +#define ESP_RMAKER_PARAM_TIMEZONE_POSIX "esp.param.tz_posix" +#define ESP_RMAKER_PARAM_SCHEDULES "esp.param.schedules" +#define ESP_RMAKER_PARAM_SCENES "esp.param.scenes" +#define ESP_RMAKER_PARAM_REBOOT "esp.param.reboot" +#define ESP_RMAKER_PARAM_FACTORY_RESET "esp.param.factory-reset" +#define ESP_RMAKER_PARAM_WIFI_RESET "esp.param.wifi-reset" +#define ESP_RMAKER_PARAM_LOCAL_CONTROL_POP "esp.param.local_control_pop" +#define ESP_RMAKER_PARAM_LOCAL_CONTROL_TYPE "esp.param.local_control_type" +#define ESP_RMAKER_PARAM_TOGGLE "esp.param.toggle" +#define ESP_RMAKER_PARAM_RANGE "esp.param.range" +#define ESP_RMAKER_PARAM_MODE "esp.param.mode" +#define ESP_RMAKER_PARAM_BLINDS_POSITION "esp.param.blinds-position" +#define ESP_RMAKER_PARAM_GARAGE_POSITION "esp.param.garage-position" +#define ESP_RMAKER_PARAM_LIGHT_MODE "esp.param.light-mode" +#define ESP_RMAKER_PARAM_AC_MODE "esp.param.ac-mode" +#define ESP_RMAKER_PARAM_ADD_ZIGBEE_DEVICE "esp.param.add_zigbee_device" + + +/********** STANDARD DEVICE TYPES **********/ + +#define ESP_RMAKER_DEVICE_SWITCH "esp.device.switch" +#define ESP_RMAKER_DEVICE_LIGHTBULB "esp.device.lightbulb" +#define ESP_RMAKER_DEVICE_FAN "esp.device.fan" +#define ESP_RMAKER_DEVICE_TEMP_SENSOR "esp.device.temperature-sensor" +#define ESP_RMAKER_DEVICE_LIGHT "esp.device.light" +#define ESP_RMAKER_DEVICE_OUTLET "esp.device.outlet" +#define ESP_RMAKER_DEVICE_PLUG "esp.device.plug" +#define ESP_RMAKER_DEVICE_SOCKET "esp.device.socket" +#define ESP_RMAKER_DEVICE_LOCK "esp.device.lock" +#define ESP_RMAKER_DEVICE_BLINDS_INTERNAL "esp.device.blinds-internal" +#define ESP_RMAKER_DEVICE_BLINDS_EXTERNAL "esp.device.blinds-external" +#define ESP_RMAKER_DEVICE_GARAGE_DOOR "esp.device.garage-door" +#define ESP_RMAKER_DEVICE_GARAGE_LOCK "esp.device.garage-door-lock" +#define ESP_RMAKER_DEVICE_SPEAKER "esp.device.speaker" +#define ESP_RMAKER_DEVICE_AIR_CONDITIONER "esp.device.air-conditioner" +#define ESP_RMAKER_DEVICE_THERMOSTAT "esp.device.thermostat" +#define ESP_RMAKER_DEVICE_TV "esp.device.tv" +#define ESP_RMAKER_DEVICE_WASHER "esp.device.washer" +#define ESP_RMAKER_DEVICE_OTHER "esp.device.other" +#define ESP_RMAKER_DEVICE_ZIGBEE_GATEWAY "esp.device.zigbee_gateway" +#define ESP_RMAKER_DEVICE_THREAD_BR "esp.device.thread-br" + +/********** STANDARD SERVICE TYPES **********/ +#define ESP_RMAKER_SERVICE_OTA "esp.service.ota" +#define ESP_RMAKER_SERVICE_TIME "esp.service.time" +#define ESP_RMAKER_SERVICE_SCHEDULE "esp.service.schedule" +#define ESP_RMAKER_SERVICE_SCENES "esp.service.scenes" +#define ESP_RMAKER_SERVICE_SYSTEM "esp.service.system" +#define ESP_RMAKER_SERVICE_LOCAL_CONTROL "esp.service.local_control" + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_thread_br.h b/components/esp_rainmaker/include/esp_rmaker_thread_br.h new file mode 100644 index 0000000..96fdfde --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_thread_br.h @@ -0,0 +1,44 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include +#include + +/** Enable Thread Border Router + * + * This API enables the Thread Border Router service for the node. For more information, + * check [here](https://openthread.io/guides/border-router/espressif-esp32) + * + * @note This API should be called after esp_rmaker_node_init() but before esp_rmaker_start(). + * + * @param[in] platform_config Platform config for OpenThread + * @param[in] rcp_update_config RCP update config + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_thread_br_enable(const esp_openthread_platform_config_t *platform_config, + const esp_rcp_update_config_t *rcp_update_config); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/include/esp_rmaker_user_mapping.h b/components/esp_rainmaker/include/esp_rmaker_user_mapping.h new file mode 100644 index 0000000..af90c1e --- /dev/null +++ b/components/esp_rainmaker/include/esp_rmaker_user_mapping.h @@ -0,0 +1,84 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** User-Node Mapping states */ +typedef enum { + /** Mapping does not exist or is not initialized */ + ESP_RMAKER_USER_MAPPING_RESET = 0, + /** Mapping has started */ + ESP_RMAKER_USER_MAPPING_STARTED, + /** Mapping request sent to cloud */ + ESP_RMAKER_USER_MAPPING_REQ_SENT, + /** Mapping is done */ + ESP_RMAKER_USER_MAPPING_DONE, +} esp_rmaker_user_mapping_state_t; + +/** + * Get User-Node mapping state + * + * This returns the current user-node mapping state. + * + * @return user mapping state + */ +esp_rmaker_user_mapping_state_t esp_rmaker_user_node_mapping_get_state(void); + +/** + * Create User Mapping Endpoint + * + * This will create a custom provisioning endpoint for user-node mapping. + * This should be called after network_prov_mgr_init()/wifi_prov_mgr_init() but before + * network_prov_mgr_start_provisioning()/wifi_prov_mgr_start_provisioning() + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_user_mapping_endpoint_create(void); + +/** + * Register User Mapping Endpoint + * + * This will register the callback for the custom provisioning endpoint + * for user-node mapping which was created with esp_rmaker_user_mapping_endpoint_create(). + * This should be called immediately after network_prov_mgr_start_provisioning()/wifi_prov_mgr_start_provisioning(). + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_user_mapping_endpoint_register(void); + +/** Add User-Node mapping + * + * This call will start the user-node mapping workflow on the node. + * This is automatically called if you have used esp_rmaker_user_mapping_endpoint_register(). + * Use this API only if you want to trigger the user-node mapping after the Wi-Fi provisioning + * has already been done. + * + * @param[in] user_id The User identifier received from the client (Phone app/CLI) + * @param[in] secret_key The Secret key received from the client (Phone app/CLI) + * + * @return ESP_OK if the workflow was successfully triggered. This does not guarantee success + * of the actual mapping. The mapping status needs to be checked separately by the clients. + * @return error on failure. + */ +esp_err_t esp_rmaker_start_user_node_mapping(char *user_id, char *secret_key); +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/sdkconfig.rename b/components/esp_rainmaker/sdkconfig.rename new file mode 100644 index 0000000..94c8c39 --- /dev/null +++ b/components/esp_rainmaker/sdkconfig.rename @@ -0,0 +1,4 @@ +# sdkconfig replacement configurations for deprecated options formatted as +# CONFIG_DEPRECATED_OPTION CONFIG_NEW_OPTION + +CONFIG_ESP_RMAKER_LOCAL_CTRL_ENABLE CONFIG_ESP_RMAKER_LOCAL_CTRL_AUTO_ENABLE diff --git a/components/esp_rainmaker/server_certs/rmaker_claim_service_server.crt b/components/esp_rainmaker/server_certs/rmaker_claim_service_server.crt new file mode 100644 index 0000000..a6f3e92 --- /dev/null +++ b/components/esp_rainmaker/server_certs/rmaker_claim_service_server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- diff --git a/components/esp_rainmaker/server_certs/rmaker_mqtt_server.crt b/components/esp_rainmaker/server_certs/rmaker_mqtt_server.crt new file mode 100644 index 0000000..a6f3e92 --- /dev/null +++ b/components/esp_rainmaker/server_certs/rmaker_mqtt_server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- diff --git a/components/esp_rainmaker/server_certs/rmaker_ota_server.crt b/components/esp_rainmaker/server_certs/rmaker_ota_server.crt new file mode 100644 index 0000000..ca4b0ae --- /dev/null +++ b/components/esp_rainmaker/server_certs/rmaker_ota_server.crt @@ -0,0 +1,54 @@ +ESP RainMaker OTA Upgrade Server Certificate. +Replace this if you choose to use any other Server. +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIQAYL4CY6i5ia5GjsnhB+5rzANBgkqhkiG9w0BAQsFADBa +MQswCQYDVQQGEwJJRTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJl +clRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTE1 +MTIwODEyMDUwN1oXDTI1MDUxMDEyMDAwMFowZDELMAkGA1UEBhMCVVMxFTATBgNV +BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEjMCEG +A1UEAxMaRGlnaUNlcnQgQmFsdGltb3JlIENBLTIgRzIwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQC75wD+AAFz75uI8FwIdfBccHMf/7V6H40II/3HwRM/ +sSEGvU3M2y24hxkx3tprDcFd0lHVsF5y1PBm1ITykRhBtQkmsgOWBGmVU/oHTz6+ +hjpDK7JZtavRuvRZQHJaZ7bN5lX8CSukmLK/zKkf1L+Hj4Il/UWAqeydjPl0kM8c ++GVQr834RavIL42ONh3e6onNslLZ5QnNNnEr2sbQm8b2pFtbObYfAB8ZpPvTvgzm ++4/dDoDmpOdaxMAvcu6R84Nnyc3KzkqwIIH95HKvCRjnT0LsTSdCTQeg3dUNdfc2 +YMwmVJihiDfwg/etKVkgz7sl4dWe5vOuwQHrtQaJ4gqPAgMBAAGjggEZMIIBFTAd +BgNVHQ4EFgQUwBKyKHRoRmfpcCV0GgBFWwZ9XEQwHwYDVR0jBBgwFoAU5Z1ZMIJH +WMys+ghUNoZ7OrUETfAwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC +AYYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp +Y2VydC5jb20wOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NybDMuZGlnaWNlcnQu +Y29tL09tbmlyb290MjAyNS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYB +BQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDQYJKoZIhvcNAQEL +BQADggEBAC/iN2bDGs+RVe4pFPpQEL6ZjeIo8XQWB2k7RDA99blJ9Wg2/rcwjang +B0lCY0ZStWnGm0nyGg9Xxva3vqt1jQ2iqzPkYoVDVKtjlAyjU6DqHeSmpqyVDmV4 +7DOMvpQ+2HCr6sfheM4zlbv7LFjgikCmbUHY2Nmz+S8CxRtwa+I6hXsdGLDRS5rB +bxcQKegOw+FUllSlkZUIII1pLJ4vP1C0LuVXH6+kc9KhJLsNkP5FEx2noSnYZgvD +0WyzT7QrhExHkOyL4kGJE7YHRndC/bseF/r/JUuOUFfrjsxOFT+xJd1BDKCcYm1v +upcHi9nzBhDFKdT3uhaQqNBU4UtJx5g= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEdTCCA12gAwIBAgIJAKcOSkw0grd/MA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV +BAYTAlVTMSUwIwYDVQQKExxTdGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTIw +MAYDVQQLEylTdGFyZmllbGQgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTAeFw0wOTA5MDIwMDAwMDBaFw0zNDA2MjgxNzM5MTZaMIGYMQswCQYDVQQGEwJV +UzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjE7MDkGA1UEAxMyU3RhcmZp +ZWxkIFNlcnZpY2VzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVDDrEKvlO4vW+GZdfjohTsR8/ +y8+fIBNtKTrID30892t2OGPZNmCom15cAICyL1l/9of5JUOG52kbUpqQ4XHj2C0N +Tm/2yEnZtvMaVq4rtnQU68/7JuMauh2WLmo7WJSJR1b/JaCTcFOD2oR0FMNnngRo +Ot+OQFodSk7PQ5E751bWAHDLUu57fa4657wx+UX2wmDPE1kCK4DMNEffud6QZW0C +zyyRpqbn3oUYSXxmTqM6bam17jQuug0DuDPfR+uxa40l2ZvOgdFFRjKWcIfeAg5J +Q4W2bHO7ZOphQazJ1FTfhy/HIrImzJ9ZVGif/L4qL8RVHHVAYBeFAlU5i38FAgMB +AAGjgfAwge0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0O +BBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMB8GA1UdIwQYMBaAFL9ft9HO3R+G9FtV +rNzXEMIOqYjnME8GCCsGAQUFBwEBBEMwQTAcBggrBgEFBQcwAYYQaHR0cDovL28u +c3MyLnVzLzAhBggrBgEFBQcwAoYVaHR0cDovL3guc3MyLnVzL3guY2VyMCYGA1Ud +HwQfMB0wG6AZoBeGFWh0dHA6Ly9zLnNzMi51cy9yLmNybDARBgNVHSAECjAIMAYG +BFUdIAAwDQYJKoZIhvcNAQELBQADggEBACMd44pXyn3pF3lM8R5V/cxTbj5HD9/G +VfKyBDbtgB9TxF00KGu+x1X8Z+rLP3+QsjPNG1gQggL4+C/1E2DUBc7xgQjB3ad1 +l08YuW3e95ORCLp+QCztweq7dp4zBncdDQh/U90bZKuCJ/Fp1U1ervShw3WnWEQt +8jxwmKy6abaVd38PMV4s/KCHOkdp8Hlf9BRUpJVeEXgSYCfOn8J3/yNTd126/+pZ +59vPr5KW7ySaNRB6nJHGDn2Z9j8Z3/VyVOEVqQdZe4O/Ui5GjLIAZHYcSNPYeehu +VsyuLAOQ1xk4meTKCRlb/weWsKh/NEnfVqn3sF/tM+2MR7cwA130A4w= +-----END CERTIFICATE----- diff --git a/components/esp_rainmaker/src/console/esp_rmaker_commands.c b/components/esp_rainmaker/src/console/esp_rmaker_commands.c new file mode 100644 index 0000000..a5a3a46 --- /dev/null +++ b/components/esp_rainmaker/src/console/esp_rmaker_commands.c @@ -0,0 +1,206 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#if RMAKER_USING_NETWORK_PROV +#include +#else +#include +#endif + +static const char *TAG = "esp_rmaker_commands"; + +static int user_node_mapping_handler(int argc, char** argv) +{ + if (argc == 3) { + printf("%s: Starting user-node mapping\n", TAG); + return esp_rmaker_start_user_node_mapping(argv[1], argv[2]); + } else { + printf("%s: Invalid Usage.\n", TAG); + return ESP_ERR_INVALID_ARG; + } + + return ESP_OK; +} + +static void register_user_node_mapping() +{ + const esp_console_cmd_t cmd = { + .command = "add-user", + .help = "Initiate the User-Node mapping from the node. Usage: add-user ", + .func = &user_node_mapping_handler, + }; + ESP_LOGI(TAG, "Registering command: %s", cmd.command); + esp_console_cmd_register(&cmd); +} + +static int get_node_id_handler(int argc, char** argv) +{ + printf("%s: Node ID: %s\n", TAG, esp_rmaker_get_node_id()); + return ESP_OK; +} + +static void register_get_node_id() +{ + const esp_console_cmd_t cmd = { + .command = "get-node-id", + .help = "Get the Node ID for this board", + .func = &get_node_id_handler, + }; + ESP_LOGI(TAG, "Registering command: %s", cmd.command); + esp_console_cmd_register(&cmd); +} + +static int wifi_prov_handler(int argc, char** argv) +{ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + if (argc < 2) { + printf("%s: Invalid Usage.\n", TAG); + return ESP_ERR_INVALID_ARG; + } + wifi_config_t wifi_config; + memset(&wifi_config, 0, sizeof(wifi_config)); + memcpy(wifi_config.sta.ssid, argv[1], strlen(argv[1])); + if (argc == 3) { + memcpy(wifi_config.sta.password, argv[2], strlen(argv[2])); + } + + /* If device is still provisioning, use network_prov_mgr_configure_wifi_sta/wifi_prov_mgr_configure_sta */ + bool provisioned = false; +#if RMAKER_USING_NETWORK_PROV + network_prov_mgr_is_wifi_provisioned(&provisioned); +#else + wifi_prov_mgr_is_provisioned(&provisioned); +#endif + if (!provisioned) { // provisioning in progress +#if RMAKER_USING_NETWORK_PROV + network_prov_mgr_configure_wifi_sta(&wifi_config); +#else + wifi_prov_mgr_configure_sta(&wifi_config); +#endif + return ESP_OK; + } + + /* If already provisioned, just set the new credentials */ + /* Stop the Wi-Fi */ + if (esp_wifi_stop() != ESP_OK) { + printf("%s: Failed to stop wifi\n", TAG); + } + /* Configure Wi-Fi station with provided host credentials */ + if (esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) != ESP_OK) { + printf("%s: Failed to set WiFi configuration\n", TAG); + return ESP_FAIL; + } + /* (Re)Start Wi-Fi */ + if (esp_wifi_start() != ESP_OK) { + printf("%s: Failed to start WiFi\n", TAG); + return ESP_FAIL; + } + /* Connect to AP */ + if (esp_wifi_connect() != ESP_OK) { + printf("%s: Failed to connect WiFi\n", TAG); + return ESP_FAIL; + } + return ESP_OK; +#else + return ESP_ERR_NOT_SUPPORTED; +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ +} + +static void register_wifi_prov() +{ + const esp_console_cmd_t cmd = { + .command = "wifi-prov", + .help = "Wi-Fi Provision the node. Usage: wifi-prov []", + .func = &wifi_prov_handler, + }; + ESP_LOGI(TAG, "Registering command: %s", cmd.command); + esp_console_cmd_register(&cmd); +} + +static int cmd_resp_cli_handler(int argc, char *argv[]) +{ + if (argc != 5) { + printf("Usage: cmd \n"); + return -1; + } + char *req_id = argv[1]; + uint8_t user_role = atoi(argv[2]); + uint16_t cmd = atoi(argv[3]); + esp_rmaker_cmd_resp_test_send(req_id, user_role, cmd, (void *)argv[4], strlen(argv[4]), esp_rmaker_test_cmd_resp, NULL); + return 0; +} + +static void register_cmd_resp_command() +{ + const esp_console_cmd_t cmd_resp_cmd = { + .command = "cmd", + .help = "Send command to command-response module. Usage cmd ", + .func = &cmd_resp_cli_handler, + }; + ESP_LOGI(TAG, "Registering command: %s", cmd_resp_cmd.command); + esp_console_cmd_register(&cmd_resp_cmd); +} + +static int sign_data_command(int argc, char *argv[]) +{ + if (argc != 2) { + printf("Usage: sign-data \n"); + return -1; + } + char *data = (char *)argv[1]; + size_t outlen = 0; + char *response = NULL; + esp_err_t err = esp_rmaker_node_auth_sign_msg((const void *)data, strlen(data), (void **)&response, &outlen); + if(err != ESP_OK) { + ESP_LOGE(TAG, "Failed to sign message"); + return -1; + } + ESP_LOGI(TAG, "hex signature(len %d): %s", outlen, response); + free(response); + return 0; +} + +static void register_sign_data_command() +{ + const esp_console_cmd_t cmd = { + .command = "sign-data", + .help = "sends some data and expects a rsa signed response", + .func = &sign_data_command, + }; + ESP_LOGI(TAG, "Registering command: %s", cmd.command); + esp_console_cmd_register(&cmd); +} + +void register_commands() +{ + register_user_node_mapping(); + register_get_node_id(); + register_wifi_prov(); + register_cmd_resp_command(); + register_sign_data_command(); +} diff --git a/components/esp_rainmaker/src/console/esp_rmaker_console.c b/components/esp_rainmaker/src/console/esp_rmaker_console.c new file mode 100644 index 0000000..a598e06 --- /dev/null +++ b/components/esp_rainmaker/src/console/esp_rmaker_console.c @@ -0,0 +1,24 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +esp_err_t esp_rmaker_console_init() +{ + esp_rmaker_common_console_init(); + register_commands(); + return ESP_OK; +} diff --git a/components/esp_rainmaker/src/console/esp_rmaker_console_internal.h b/components/esp_rainmaker/src/console/esp_rmaker_console_internal.h new file mode 100644 index 0000000..be2c7c9 --- /dev/null +++ b/components/esp_rainmaker/src/console/esp_rmaker_console_internal.h @@ -0,0 +1,17 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +void register_commands(void); diff --git a/components/esp_rainmaker/src/core/esp_rmaker_claim.c b/components/esp_rainmaker/src/core/esp_rmaker_claim.c new file mode 100644 index 0000000..ec1e99e --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_claim.c @@ -0,0 +1,1041 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +/* Keep forward-compatibility with Mbed TLS 3.x */ +#if (MBEDTLS_VERSION_NUMBER < 0x03000000) +#define MBEDTLS_2_X_COMPAT +#else /* !(MBEDTLS_VERSION_NUMBER < 0x03000000) */ +/* Macro wrapper for struct's private members */ +#ifndef MBEDTLS_ALLOW_PRIVATE_ACCESS +#define MBEDTLS_ALLOW_PRIVATE_ACCESS +#endif /* MBEDTLS_ALLOW_PRIVATE_ACCESS */ +#endif /* !(MBEDTLS_VERSION_NUMBER < 0x03000000) */ + +#include "mbedtls/platform.h" +#include "mbedtls/pk.h" +#include "mbedtls/rsa.h" +#include "mbedtls/entropy.h" +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/x509_csr.h" +#include "mbedtls/md.h" +#include "mbedtls/sha512.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "esp_rmaker_internal.h" +#include "esp_rmaker_client_data.h" +#include "esp_rmaker_claim.h" + +#if RMAKER_USING_NETWORK_PROV +#include +#else +#include +#endif + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) +// Features supported in 4.4+ + +#ifdef CONFIG_ESP_RMAKER_USE_CERT_BUNDLE +#define ESP_RMAKER_USE_CERT_BUNDLE +#include +#endif + +#else + +#ifdef CONFIG_ESP_RMAKER_USE_CERT_BUNDLE +#warning "Certificate Bundle not supported below IDF v4.4. Using provided certificate instead." +#endif + +#endif /* !IDF4.4 */ + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) +#include "esp_mac.h" +#endif + +static const char *TAG = "esp_claim"; + +#define ESP_RMAKER_RANDOM_NUMBER_LEN 64 + +#ifdef CONFIG_ESP_RMAKER_SELF_CLAIM +#include "soc/soc.h" +#include "soc/efuse_reg.h" +#include "esp_efuse.h" +#include "esp_efuse_table.h" + +#define CLAIM_BASE_URL CONFIG_ESP_RMAKER_CLAIM_SERVICE_BASE_URL +#define CLAIM_INIT_PATH "claim/initiate" +#define CLAIM_VERIFY_PATH "claim/verify" + +extern uint8_t claim_service_server_root_ca_pem_start[] asm("_binary_rmaker_claim_service_server_crt_start"); +extern uint8_t claim_service_server_root_ca_pem_end[] asm("_binary_rmaker_claim_service_server_crt_end"); +#endif /* CONFIG_ESP_RMAKER_SELF_CLAIM */ + +#define CLAIM_PK_SIZE 2048 + +static EventGroupHandle_t claim_event_group; +static const int CLAIM_TASK_BIT = BIT0; +static void escape_new_line(esp_rmaker_claim_data_t *data) +{ + char *str = (char *)data->csr; + memset(data->payload, 0, sizeof(data->payload)); + char *target_str = (char *)data->payload; + /* Hack to just avoid a "\r\n" at the end of string */ + if (str[strlen(str) - 1] == '\n') { + str[strlen(str) - 1] = '\0'; + } + while (*str) { + if (*str == '\n') { + *target_str++ = '\\'; + *target_str++ = 'n'; + str++; + continue; + } + *target_str++ = *str++; + } + *target_str = '\0'; + strcpy((char *)data->csr, (char *)data->payload); + ESP_LOGD(TAG, "Modified CSR : %s", data->csr); +} + +static void unescape_new_line(char *str) +{ + char *target_str = str; + while (*str) { + if (*str == '\\') { + str++; + if (*str == 'n') { + *target_str++ = '\n'; + str++; + } + } + *target_str++ = *str++; + } + *target_str = '\0'; +} + +static esp_err_t esp_rmaker_claim_generate_csr(esp_rmaker_claim_data_t *claim_data, const char *common_name) +{ + if (!claim_data || !common_name) { + ESP_LOGE(TAG, "claim_data or common_name cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + const char *pers = "gen_csr"; + mbedtls_x509write_csr csr; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_entropy_context entropy; + + /* Generating CSR from the private key */ + mbedtls_x509write_csr_init(&csr); + mbedtls_x509write_csr_set_md_alg(&csr, MBEDTLS_MD_SHA256); + mbedtls_ctr_drbg_init(&ctr_drbg); + + ESP_LOGD(TAG, "Seeding the random number generator."); + mbedtls_entropy_init(&entropy); + int ret = mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *) pers, strlen(pers)); + if (ret != 0) { + ESP_LOGE(TAG, "mbedtls_ctr_drbg_seed returned -0x%04x", -ret ); + goto exit; + } + char subject_name[50]; + snprintf(subject_name, sizeof(subject_name), "CN=%s", common_name); + ret = mbedtls_x509write_csr_set_subject_name(&csr, subject_name); + if (ret != 0) { + ESP_LOGE(TAG, "mbedtls_x509write_csr_set_subject_name returned %d", ret ); + goto exit; + } + + memset(claim_data->csr, 0, sizeof(claim_data->csr)); + mbedtls_x509write_csr_set_key(&csr, &claim_data->key); + ESP_LOGD(TAG, "Generating PEM"); + ret = mbedtls_x509write_csr_pem(&csr, claim_data->csr, sizeof(claim_data->csr), mbedtls_ctr_drbg_random, &ctr_drbg); + if (ret < 0) { + ESP_LOGE(TAG, "mbedtls_x509write_csr_pem returned -0x%04x", -ret ); + goto exit; + } + ESP_LOGD(TAG, "CSR generated."); + claim_data->state = RMAKER_CLAIM_STATE_CSR_GENERATED; +exit: + + mbedtls_x509write_csr_free(&csr); + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); + + return ret; +} + +static esp_err_t esp_rmaker_claim_generate_key(esp_rmaker_claim_data_t *claim_data) +{ + const char *pers = "gen_key"; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + + mbedtls_pk_free(&claim_data->key); + mbedtls_pk_init(&claim_data->key); + mbedtls_ctr_drbg_init(&ctr_drbg); + memset(claim_data->payload, 0, sizeof(claim_data->payload)); + + ESP_LOGD(TAG, "Seeding the random number generator."); + mbedtls_entropy_init(&entropy); + int ret = mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *) pers, strlen(pers)); + if (ret != 0) { + ESP_LOGE(TAG, "mbedtls_ctr_drbg_seed returned -0x%04x", -ret ); + mbedtls_pk_free(&claim_data->key); + goto exit; + } + + ESP_LOGW(TAG, "Generating the private key. This may take time." ); + ret = mbedtls_pk_setup(&claim_data->key, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)); + if (ret != 0) { + ESP_LOGE(TAG, "mbedtls_pk_setup returned -0x%04x", -ret ); + mbedtls_pk_free(&claim_data->key); + goto exit; + } + + ret = mbedtls_rsa_gen_key(mbedtls_pk_rsa(claim_data->key), mbedtls_ctr_drbg_random, &ctr_drbg, CLAIM_PK_SIZE, 65537); /* here, 65537 is the RSA exponent */ + if (ret != 0) { + ESP_LOGE(TAG, "mbedtls_rsa_gen_key returned -0x%04x", -ret ); + mbedtls_pk_free(&claim_data->key); + goto exit; + } + + claim_data->state = RMAKER_CLAIM_STATE_PK_GENERATED; + ESP_LOGD(TAG, "Converting Private Key to PEM..."); + ret = mbedtls_pk_write_key_pem(&claim_data->key, (unsigned char *)claim_data->payload, sizeof(claim_data->payload)); + if (ret != 0) { + ESP_LOGE(TAG, "mbedtls_pk_write_key_pem returned -0x%04x", -ret ); + mbedtls_pk_free(&claim_data->key); + } +exit: + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); + return ret; +} + +/* Parse the Claim Init response and generate Claim Verify request + * + * Claim Verify Response format: + * {"certificate":""} + */ +static esp_err_t handle_claim_verify_response(esp_rmaker_claim_data_t *claim_data) +{ + ESP_LOGD(TAG, "Claim Verify Response: %s", claim_data->payload); + jparse_ctx_t jctx; + if (json_parse_start(&jctx, claim_data->payload, strlen(claim_data->payload)) == 0) { + int required_len = 0; + if (json_obj_get_strlen(&jctx, "certificate", &required_len) == 0) { + required_len++; /* For NULL termination */ + char *value_buf = MEM_CALLOC_EXTRAM(1, required_len); + if (!value_buf) { + json_parse_end(&jctx); + ESP_LOGE(TAG, "Failed to allocate %d bytes for certificate.", required_len); + return ESP_ERR_NO_MEM; + } + /* Just using the certificate buffer itself (which is expected to be large enough) to + * check if the claiming service has also sent an MQTT Host, before going on to read + * the certificate itself. + */ + if (json_obj_get_string(&jctx, "mqtt_host", value_buf, required_len) == 0) { + ESP_LOGI(TAG, "Storing received MQTT Host: %s", value_buf); + esp_rmaker_factory_set(ESP_RMAKER_MQTT_HOST_NVS_KEY, value_buf, strlen(value_buf)); + memset(value_buf, 0, required_len); + } + json_obj_get_string(&jctx, "certificate", value_buf, required_len); + json_parse_end(&jctx); + unescape_new_line(value_buf); + esp_err_t err = esp_rmaker_factory_set(ESP_RMAKER_CLIENT_CERT_NVS_KEY, value_buf, strlen(value_buf)); + free(value_buf); + return err; + } else { + ESP_LOGE(TAG, "Claim Verify Response invalid."); + } + } + ESP_LOGE(TAG, "Failed to parse Claim Verify Response."); + return ESP_FAIL; +} + +static esp_err_t generate_claim_init_request(esp_rmaker_claim_data_t *claim_data) +{ + if (claim_data->state < RMAKER_CLAIM_STATE_PK_GENERATED) { + return ESP_ERR_INVALID_STATE; + } + uint8_t mac_addr[6]; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) + /* ESP_MAC_BASE was introduced in ESP-IDF v5.1. It is the same as the Wi-Fi Station MAC address + * for chips supporting Wi-Fi. We can use base MAC address to generate claim init request for both + * Wi-Fi and Thread devices + */ + esp_err_t err = esp_read_mac(mac_addr, ESP_MAC_BASE); +#else + /* Thread was officially supported in ESP-IDF v5.1. Use Wi-Fi Station MAC address to generate claim + * init request. + */ + esp_err_t err = esp_wifi_get_mac(WIFI_IF_STA, mac_addr); +#endif + if (err != ESP_OK) { + ESP_LOGE(TAG, "Could not fetch MAC address."); + return err; + } + + snprintf(claim_data->payload, sizeof(claim_data->payload), + "{\"mac_addr\":\"%02X%02X%02X%02X%02X%02X\",\"platform\":\"%s\"}", + mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5], CONFIG_IDF_TARGET); + claim_data->payload_len = strlen(claim_data->payload); + claim_data->payload_offset = 0; + return ESP_OK; +} + +void esp_rmaker_claim_data_free(esp_rmaker_claim_data_t *claim_data) +{ + if(claim_data) { + mbedtls_pk_free(&claim_data->key); + free(claim_data); + } +} + +#ifdef CONFIG_ESP_RMAKER_SELF_CLAIM + +static esp_err_t read_hmac_key(uint32_t *out_hmac_key, size_t hmac_key_size) +{ + /* ESP32-S2 HMAC Key programming scheme */ + if (hmac_key_size != 16) { + ESP_LOGE(TAG, "HMAC key size should be 16 bytes."); + } + esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, out_hmac_key, hmac_key_size * 8); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_efuse_read_field_blob failed!"); + } + return err; +} + +static esp_err_t hmac_challenge(const char* hmac_request, unsigned char *hmac_response, size_t len_hmac_response) +{ + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA512; + uint32_t hmac_key[4]; + + esp_err_t err = read_hmac_key(hmac_key, sizeof(hmac_key)); + if (err != ESP_OK) { + return err; + } + + mbedtls_md_init(&ctx); + int ret = mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type) ,1); + ret |= mbedtls_md_hmac_starts(&ctx, (const unsigned char *)hmac_key, sizeof(hmac_key)); + ret |= mbedtls_md_hmac_update(&ctx, (const unsigned char *)hmac_request, strlen(hmac_request)); + ret |= mbedtls_md_hmac_finish(&ctx, hmac_response); + mbedtls_md_free(&ctx); + + if(ret == 0) { + return ESP_OK; + } else { + return ret; + } +} + +/* Parse the Claim Init response and generate Claim Verify request + * + * Claim Init Response format: + * {"auth_id":"", "challenge":""} + * + * Claim Verify Request format + * {"auth_id":"", "challenge_response":"<64byte-response-in-hex>, "csr":""} + */ +static esp_err_t handle_self_claim_init_response(esp_rmaker_claim_data_t *claim_data) +{ + ESP_LOGD(TAG, "Claim Init Response: %s", claim_data->payload); + jparse_ctx_t jctx; + if (json_parse_start(&jctx, claim_data->payload, strlen(claim_data->payload)) == 0) { + char auth_id[64]; + char challenge[130]; + int ret = json_obj_get_string(&jctx, "auth_id", auth_id, sizeof(auth_id)); + ret |= json_obj_get_string(&jctx, "challenge", challenge, sizeof(challenge)); + json_parse_end(&jctx); + if (ret == 0) { + unsigned char response[64] = {0}; + if (hmac_challenge(challenge, response, sizeof(response)) == ESP_OK) { + json_gen_str_t jstr; + json_gen_str_start(&jstr, claim_data->payload, sizeof(claim_data->payload), NULL, NULL); + json_gen_start_object(&jstr); + json_gen_obj_set_string(&jstr, "auth_id", auth_id); + /* Add Challenge Response as a hex representation */ + json_gen_obj_start_long_string(&jstr, "challenge_response", NULL); + for(int i = 0 ; i < sizeof(response); i++) { + char hexstr[3]; + snprintf(hexstr, sizeof(hexstr), "%02X", response[i]); + json_gen_add_to_long_string(&jstr, hexstr); + } + json_gen_end_long_string(&jstr); + json_gen_obj_set_string(&jstr, "csr", (char *)claim_data->csr); + json_gen_end_object(&jstr); + json_gen_str_end(&jstr); + return ESP_OK; + } else { + ESP_LOGE(TAG, "HMAC Challenge failed."); + } + } else { + ESP_LOGE(TAG, "Claim Init Response invalid."); + } + } + ESP_LOGE(TAG, "Failed to parse Claim Init Response."); + return ESP_FAIL; +} +static esp_err_t esp_rmaker_claim_perform_common(esp_rmaker_claim_data_t *claim_data, const char *path) +{ + char url[100]; + snprintf(url, sizeof(url), "%s/%s", CLAIM_BASE_URL, path); + esp_http_client_config_t config = { + .url = url, + .transport_type = HTTP_TRANSPORT_OVER_SSL, + .buffer_size = 1024, +#ifdef ESP_RMAKER_USE_CERT_BUNDLE + .crt_bundle_attach = esp_crt_bundle_attach, +#else + .cert_pem = (const char *)claim_service_server_root_ca_pem_start, +#endif + .skip_cert_common_name_check = false + }; + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) { + ESP_LOGE(TAG, "Failed to initialise HTTP Client."); + return ESP_FAIL; + } + + ESP_LOGD(TAG, "Payload for %s: %s", url, claim_data->payload); + esp_http_client_set_method(client, HTTP_METHOD_POST); + esp_err_t err = esp_http_client_open(client, strlen(claim_data->payload)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open connection to %s", url); + esp_http_client_cleanup(client); + return ESP_FAIL; + } + int len = esp_http_client_write(client, claim_data->payload, strlen(claim_data->payload)); + if (len != strlen(claim_data->payload)) { + ESP_LOGE(TAG, "Failed to write Payload. Returned len = %d.", len); + esp_http_client_close(client); + esp_http_client_cleanup(client); + return ESP_FAIL; + } + ESP_LOGD(TAG, "Wrote %d of %d bytes.", len, strlen(claim_data->payload)); + len = esp_http_client_fetch_headers(client); + int status = esp_http_client_get_status_code(client); + if ((len > 0) && (status == 200)) { + len = esp_http_client_read_response(client, claim_data->payload, sizeof(claim_data->payload)); + claim_data->payload[len] = '\0'; + esp_http_client_close(client); + esp_http_client_cleanup(client); + return ESP_OK; + } else { + len = esp_http_client_read_response(client, claim_data->payload, sizeof(claim_data->payload)); + if (len >= 0) { + claim_data->payload[len] = 0; + } + ESP_LOGE(TAG, "Invalid response for %s", url); + ESP_LOGE(TAG, "Status = %d, Data = %s", status, len > 0 ? claim_data->payload : "None"); + } + esp_http_client_close(client); + esp_http_client_cleanup(client); + return ESP_FAIL; +} +static esp_err_t esp_rmaker_claim_perform_init(esp_rmaker_claim_data_t *claim_data) +{ + esp_err_t err = generate_claim_init_request(claim_data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to generate Claim init request"); + return err; + } + + err = esp_rmaker_claim_perform_common(claim_data, CLAIM_INIT_PATH); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Claim Init Request Failed."); + return err; + } + claim_data->state = RMAKER_CLAIM_STATE_INIT; + err = handle_self_claim_init_response(claim_data); + if (err == ESP_OK) { + claim_data->state = RMAKER_CLAIM_STATE_INIT_DONE; + } + return err; +} + +static esp_err_t esp_rmaker_claim_perform_verify(esp_rmaker_claim_data_t *claim_data) +{ + esp_err_t err = esp_rmaker_claim_perform_common(claim_data, CLAIM_VERIFY_PATH); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Claim Verify Failed."); + return err; + } + claim_data->state = RMAKER_CLAIM_STATE_VERIFY; + err = handle_claim_verify_response(claim_data); + if (err == ESP_OK) { + claim_data->state = RMAKER_CLAIM_STATE_VERIFY_DONE; + } + return err; +} + +esp_err_t esp_rmaker_self_claim_perform(esp_rmaker_claim_data_t *claim_data) +{ + ESP_LOGI(TAG, "Starting the Self Claim Process. This may take time."); + if (claim_data == NULL) { + ESP_LOGE(TAG, "Self claiming not initialised."); + return ESP_ERR_INVALID_STATE; + } + esp_err_t err = esp_rmaker_claim_perform_init(claim_data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Claim Init Sequence Failed."); + return err; + } + err = esp_rmaker_claim_perform_verify(claim_data); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Self Claiming was successful. Certificate received."); + } + esp_rmaker_claim_data_free(claim_data); + return err; +} +#endif /* CONFIG_ESP_RMAKER_SELF_CLAIM */ +#ifdef CONFIG_ESP_RMAKER_ASSISTED_CLAIM +static EventGroupHandle_t claim_csr_event_group; +static const int CLAIM_CSR_TASK_BIT = BIT1; +static void esp_rmaker_claim_csr_task(void *args) +{ + esp_rmaker_claim_data_t *claim_data = (esp_rmaker_claim_data_t *)args; + esp_rmaker_claim_generate_csr(claim_data, esp_rmaker_get_node_id()); + xEventGroupSetBits(claim_csr_event_group, CLAIM_CSR_TASK_BIT); + vTaskDelete(NULL); +} +static esp_err_t _esp_rmaker_claim_generate_csr(esp_rmaker_claim_data_t *claim_data) +{ + claim_csr_event_group = xEventGroupCreate(); + if (!claim_csr_event_group) { + ESP_LOGE(TAG, "Couldn't create CSR event group."); + return ESP_ERR_NO_MEM; + } + +#define ESP_RMAKER_CLAIM_CSR_TASK_STACK_SIZE (10 * 1024) + if (xTaskCreate(&esp_rmaker_claim_csr_task, "claim_csr_task", ESP_RMAKER_CLAIM_CSR_TASK_STACK_SIZE, + claim_data, (CONFIG_ESP_RMAKER_WORK_QUEUE_TASK_PRIORITY + 1), NULL) != pdPASS) { + ESP_LOGE(TAG, "Couldn't create CSR generation task"); + vEventGroupDelete(claim_csr_event_group); + return ESP_ERR_NO_MEM; + } + + /* Wait for claim init to complete */ + xEventGroupWaitBits(claim_csr_event_group, CLAIM_CSR_TASK_BIT, false, true, portMAX_DELAY); + if (claim_data->state == RMAKER_CLAIM_STATE_CSR_GENERATED) { + return ESP_OK; + } + return ESP_FAIL; +} +/* Parse the Claim Init response and generate Claim Verify request + * + * Claim Init Response format: + * {"node_id":""} + * + * Claim Verify Request format + * {"csr":""} + */ +static esp_err_t handle_assisted_claim_init_response(esp_rmaker_claim_data_t *claim_data) +{ + ESP_LOGD(TAG, "Claim Init Response: %s", claim_data->payload); + jparse_ctx_t jctx; + if (json_parse_start(&jctx, claim_data->payload, strlen(claim_data->payload)) == 0) { + char node_id[64]; + int ret = json_obj_get_string(&jctx, "node_id", node_id, sizeof(node_id)); + json_parse_end(&jctx); + if (ret == 0) { + esp_rmaker_factory_set("node_id", node_id, strlen(node_id)); + esp_rmaker_change_node_id(node_id, strlen(node_id)); + /* We use _esp_rmaker_claim_generate_csr instead of esp_rmaker_claim_generate_csr() + * because the thread in whose context this function is called doesn't have + * enough stack memory to generate CSR. A new thread with larger stack is spawned + * to generate the CSR by the below function. + */ + esp_err_t err = _esp_rmaker_claim_generate_csr(claim_data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to generate CSR."); + return err; + } + escape_new_line(claim_data); + + json_gen_str_t jstr; + json_gen_str_start(&jstr, claim_data->payload, sizeof(claim_data->payload), NULL, NULL); + json_gen_start_object(&jstr); + json_gen_obj_set_string(&jstr, "csr", (char *)claim_data->csr); + json_gen_obj_set_bool(&jstr, "send_mqtt_host", true); + json_gen_end_object(&jstr); + json_gen_str_end(&jstr); + claim_data->payload_len = strlen(claim_data->payload); + claim_data->payload_offset = 0; + return ESP_OK; + } else { + ESP_LOGE(TAG, "Claim Init Response invalid."); + } + } + ESP_LOGE(TAG, "Failed to parse Claim Init Response."); + return ESP_FAIL; +} +#include +#define CLAIM_FRAGMENT_SIZE 200 +esp_err_t esp_rmaker_assisted_claim_handle_start(RmakerClaim__RMakerClaimPayload *command, + RmakerClaim__RMakerClaimPayload *response, esp_rmaker_claim_data_t *claim_data) +{ + if (claim_data->state < RMAKER_CLAIM_STATE_PK_GENERATED) { + ESP_LOGE(TAG, "PK not created. Cannot proceed with Assisted Claiming."); + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidState; + return ESP_OK; + } + if (generate_claim_init_request(claim_data) != ESP_OK) { + return ESP_OK; + } + RmakerClaim__PayloadBuf *payload_buf = response->resppayload->buf; + payload_buf->offset = 0; + payload_buf->totallen = claim_data->payload_len; + payload_buf->payload.data = (uint8_t *)claim_data->payload; + payload_buf->payload.len = claim_data->payload_len; + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__Success; + claim_data->state = RMAKER_CLAIM_STATE_INIT; + ESP_LOGI(TAG, "Assisted Claiming Started."); + return ESP_OK; +} + +/* Return ESP_OK if this is just an application layer error. The response content will + * indicate error to the client. + */ +ProtobufCBinaryData *esp_rmaker_assisted_claim_validate_data(RmakerClaim__PayloadBuf *recv_payload, + RmakerClaim__RMakerClaimPayload *response, esp_rmaker_claim_data_t *claim_data) +{ + if (recv_payload == NULL) { + ESP_LOGE(TAG, "Empty response received. Cannot proceed."); + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidParam; + return NULL; + } + /* Read the command */ + ProtobufCBinaryData *recv_payload_buf = &recv_payload->payload; + if (!recv_payload_buf) { + ESP_LOGE(TAG, "No data received. Cannot proceed."); + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidParam; + return NULL; + } + if ((recv_payload_buf->len + recv_payload->offset) > recv_payload->totallen) { + ESP_LOGE(TAG, "Received data exceeds total length."); + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidParam; + return NULL; + } + if (recv_payload_buf->len >= sizeof(claim_data->payload)) { + ESP_LOGE(TAG, "Received data too long (%d bytes).", recv_payload_buf->len); + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__NoMemory; + return NULL; + } + return recv_payload_buf; +} + +/* Return ESP_OK if this is just an application layer error. The response content will + * indicate error to the client. + */ +esp_err_t esp_rmaker_assisted_claim_handle_init(RmakerClaim__RMakerClaimPayload *command, + RmakerClaim__RMakerClaimPayload *response, esp_rmaker_claim_data_t *claim_data) +{ + if (claim_data->state < RMAKER_CLAIM_STATE_INIT) { + ESP_LOGE(TAG, "Claiming hasn't started. Cannot proceed with init handling."); + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidState; + return ESP_OK; + } else if (claim_data->state != RMAKER_CLAIM_STATE_INIT_DONE) { + if (command->payload_case != RMAKER_CLAIM__RMAKER_CLAIM_PAYLOAD__PAYLOAD_CMD_PAYLOAD) { + ESP_LOGE(TAG, "Invalid response received for Claim Init. Cannot proceed."); + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidParam; + return ESP_OK; + } + RmakerClaim__PayloadBuf *recv_payload = command->cmdpayload; + ProtobufCBinaryData *recv_payload_buf = esp_rmaker_assisted_claim_validate_data + (recv_payload, response, claim_data); + if (!recv_payload_buf) { + ESP_LOGE(TAG, "Failed to get Claim Init Data."); + return ESP_OK; + } + if (recv_payload_buf->len != recv_payload->totallen) { + ESP_LOGE(TAG, "Claim Init Data length not equal to total length. Cannot proceed."); + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidParam; + return ESP_OK; + } + memset(claim_data->payload, 0, sizeof(claim_data->payload)); + memcpy(claim_data->payload, recv_payload_buf->data, recv_payload_buf->len); + if (handle_assisted_claim_init_response(claim_data) != ESP_OK) { + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidParam; + ESP_LOGE(TAG, "Error handling Claim Init response."); + return ESP_OK; + } else { + claim_data->state = RMAKER_CLAIM_STATE_INIT_DONE; + } + } + RmakerClaim__PayloadBuf *payload_buf = response->resppayload->buf; + payload_buf->totallen = claim_data->payload_len; + payload_buf->offset = claim_data->payload_offset; + + payload_buf->payload.data = (uint8_t *)claim_data->payload + claim_data->payload_offset; + payload_buf->payload.len = (claim_data->payload_len - claim_data->payload_offset) > CLAIM_FRAGMENT_SIZE ? + CLAIM_FRAGMENT_SIZE : (claim_data->payload_len - claim_data->payload_offset); + + claim_data->payload_offset += payload_buf->payload.len; + + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__Success; + + if (claim_data->payload_offset == claim_data->payload_len) { + ESP_LOGD(TAG, "Finished sending Claim Verify Payload."); + claim_data->state = RMAKER_CLAIM_STATE_VERIFY; + } + return ESP_OK; +} +/* Return ESP_OK if this is just an application layer error. The response content will + * indicate error to the client. + */ +esp_err_t esp_rmaker_assisted_claim_handle_verify(RmakerClaim__RMakerClaimPayload *command, + RmakerClaim__RMakerClaimPayload *response, esp_rmaker_claim_data_t *claim_data) +{ + if (claim_data->state < RMAKER_CLAIM_STATE_VERIFY) { + ESP_LOGE(TAG, "Invalid state. Cannot proceed with verify handling."); + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidState; + return ESP_OK; + } + if (command->payload_case != RMAKER_CLAIM__RMAKER_CLAIM_PAYLOAD__PAYLOAD_CMD_PAYLOAD) { + ESP_LOGE(TAG, "Invalid response received for Claim Verify. Cannot proceed."); + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidParam; + return ESP_OK; + } + RmakerClaim__PayloadBuf *recv_payload = command->cmdpayload; + ProtobufCBinaryData *recv_payload_buf = esp_rmaker_assisted_claim_validate_data + (recv_payload, response, claim_data); + if (!recv_payload_buf) { + ESP_LOGE(TAG, "Failed to get Claim Verify Data."); + return ESP_OK; + } + /* If offset is 0, this is the start of the fragmented data. */ + if (recv_payload->offset == 0) { + memset(claim_data->payload, 0, sizeof(claim_data->payload)); + claim_data->payload_offset = 0; + claim_data->payload_len = 0; + } + claim_data->payload_offset = recv_payload->offset; + memcpy(claim_data->payload + claim_data->payload_offset, recv_payload_buf->data, recv_payload_buf->len); + claim_data->payload_len += recv_payload_buf->len; + + if ((recv_payload->offset + recv_payload_buf->len) == recv_payload->totallen) { + ESP_LOGD(TAG, "Received complete response of len = %"PRIu32" bytes for Claim Verify", recv_payload->totallen); + if (handle_claim_verify_response(claim_data) == ESP_OK) { + ESP_LOGI(TAG,"Assisted Claiming was Successful."); + claim_data->state = RMAKER_CLAIM_STATE_VERIFY_DONE; + if (claim_event_group) { + xEventGroupSetBits(claim_event_group, CLAIM_TASK_BIT); + } + } else { + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidParam; + return ESP_OK; + } + } + response->resppayload->status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__Success; + return ESP_OK; +} + +esp_err_t esp_rmaker_claiming_handler(uint32_t session_id, const uint8_t *inbuf, ssize_t inlen, uint8_t **outbuf, ssize_t *outlen, void *priv_data) +{ + esp_rmaker_claim_data_t *claim_data = (esp_rmaker_claim_data_t *)priv_data; + if (!priv_data) { + ESP_LOGE(TAG, "Claim data cannot be NULL. Cannot proceed with Assisted Claiming."); + return ESP_ERR_INVALID_STATE; + } + RmakerClaim__RMakerClaimPayload *command; + command = rmaker_claim__rmaker_claim_payload__unpack(NULL, inlen, inbuf); + + if (!command) { + ESP_LOGE(TAG, "No Claim command received"); + return ESP_ERR_INVALID_ARG; + } + + /* Initialise the response objects */ + RmakerClaim__RMakerClaimPayload response; + rmaker_claim__rmaker_claim_payload__init(&response); + response.msg = command->msg + 1; + response.payload_case = RMAKER_CLAIM__RMAKER_CLAIM_PAYLOAD__PAYLOAD_RESP_PAYLOAD; + + RmakerClaim__RespPayload resppayload; + rmaker_claim__resp_payload__init(&resppayload); + resppayload.status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__Fail; + response.resppayload = &resppayload; + + RmakerClaim__PayloadBuf payload_buf; + rmaker_claim__payload_buf__init(&payload_buf); + resppayload.buf = &payload_buf; + + ESP_LOGD(TAG, "Received claim command: %d", command->msg); + + /* Handle the received command */ + switch (command->msg) { + case RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimStart: + esp_rmaker_assisted_claim_handle_start(command, &response, claim_data); + break; + case RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimInit: + esp_rmaker_assisted_claim_handle_init(command, &response, claim_data); + break; + case RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimVerify: + esp_rmaker_assisted_claim_handle_verify(command, &response, claim_data); + break; + case RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimAbort: + memset(claim_data->payload, 0, sizeof(claim_data->payload)); + claim_data->payload_len = 0; + claim_data->payload_offset = 0; + /* Go back to RMAKER_CLAIM_STATE_PK_GENERATED, so that claim can restart */ + claim_data->state = RMAKER_CLAIM_STATE_PK_GENERATED; + resppayload.status = RMAKER_CLAIM__RMAKER_CLAIM_STATUS__Success; + ESP_LOGW(TAG, "Assisted Claiming Aborted."); + break; + default: + break; + } + *outlen = rmaker_claim__rmaker_claim_payload__get_packed_size(&response); + *outbuf = (uint8_t *)MEM_ALLOC_EXTRAM(*outlen); + rmaker_claim__rmaker_claim_payload__pack(&response, *outbuf); + rmaker_claim__rmaker_claim_payload__free_unpacked(command, NULL); + return ESP_OK; +} +#define CLAIM_ENDPOINT "rmaker_claim" +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ +#if RMAKER_USING_NETWORK_PROV + if (event_base == NETWORK_PROV_EVENT) { + switch (event_id) { + case NETWORK_PROV_INIT: { + static const char *capabilities[] = {"claim"}; + network_prov_mgr_set_app_info("rmaker", "1.0", capabilities, 1); + if (network_prov_mgr_endpoint_create(CLAIM_ENDPOINT) != ESP_OK) { + ESP_LOGE(TAG, "Failed to create claim end point."); + } + break; + } + case NETWORK_PROV_START: + if (network_prov_mgr_endpoint_register(CLAIM_ENDPOINT, esp_rmaker_claiming_handler, arg) != ESP_OK) { + ESP_LOGE(TAG, "Failed to register claim end point."); + } + break; + default: + break; + } + } +#else + if (event_base == WIFI_PROV_EVENT) { + switch (event_id) { + case WIFI_PROV_INIT: { + static const char *capabilities[] = {"claim"}; + wifi_prov_mgr_set_app_info("rmaker", "1.0", capabilities, 1); + if (wifi_prov_mgr_endpoint_create(CLAIM_ENDPOINT) != ESP_OK) { + ESP_LOGE(TAG, "Failed to create claim end point."); + } + break; + } + case WIFI_PROV_START: + if (wifi_prov_mgr_endpoint_register(CLAIM_ENDPOINT, esp_rmaker_claiming_handler, arg) != ESP_OK) { + ESP_LOGE(TAG, "Failed to register claim end point."); + } + break; + default: + break; + } + } +#endif /* RMAKER_USING_NETWORK_PROV */ +} +#endif /* CONFIG_ESP_RMAKER_ASSISTED_CLAIM */ +esp_err_t __esp_rmaker_claim_init(esp_rmaker_claim_data_t *claim_data) +{ + esp_err_t err = ESP_OK; + + char *key = esp_rmaker_get_client_key(); + if (key) { + mbedtls_pk_free(&claim_data->key); + mbedtls_pk_init(&claim_data->key); +#ifdef MBEDTLS_2_X_COMPAT + int ret = mbedtls_pk_parse_key(&claim_data->key, (uint8_t *)key, strlen(key) + 1, NULL, 0); +#else + int ret = mbedtls_pk_parse_key(&claim_data->key, (uint8_t *)key, strlen(key) + 1, NULL, 0, mbedtls_ctr_drbg_random, NULL); +#endif + if (ret == 0) { + ESP_LOGI(TAG, "Private key already exists. No need to re-initialise it."); + claim_data->state = RMAKER_CLAIM_STATE_PK_GENERATED; + } + free(key); + } + if (claim_data->state != RMAKER_CLAIM_STATE_PK_GENERATED) { + /* Generate the Private Key */ + err = esp_rmaker_claim_generate_key(claim_data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to generate private key."); + return err; + } + err = esp_rmaker_factory_set(ESP_RMAKER_CLIENT_KEY_NVS_KEY, claim_data->payload, strlen((char *)claim_data->payload)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to save private key to storage."); + return err; + } + } + /* Check if the general purpose random bytes are already present in the storage */ + char *stored_random_bytes = esp_rmaker_factory_get(ESP_RMAKER_CLIENT_RANDOM_NVS_KEY); + if (stored_random_bytes == NULL) { + /* Generate random bytes for general purpose use */ + uint8_t random_bytes[ESP_RMAKER_RANDOM_NUMBER_LEN]; + esp_fill_random(&random_bytes, sizeof(random_bytes)); + + /* Store the random bytes in the factory storage */ + err = esp_rmaker_factory_set(ESP_RMAKER_CLIENT_RANDOM_NVS_KEY, random_bytes, sizeof(random_bytes)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to store random bytes to storage."); + return err; + } + } else { + /* Free the copy of the random bytes as it isn't required here. */ + free(stored_random_bytes); + } +#ifdef CONFIG_ESP_RMAKER_SELF_CLAIM + err = esp_rmaker_claim_generate_csr(claim_data, esp_rmaker_get_node_id()); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to generate CSR."); + return err; + } + /* New line characters from the CSR need to be removed and replaced with explicit \n for the claiming + * service to parse properly. Make that change here and store the CSR in storage. + */ + escape_new_line(claim_data); +#endif /* CONFIG_ESP_RMAKER_SELF_CLAIM */ + return err; +} + +void esp_rmaker_claim_task(void *args) +{ + if (!args) { + ESP_LOGE(TAG, "Arguments for claiming task cannot be NULL"); + return; + } + esp_rmaker_claim_data_t *claim_data = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_claim_data_t)); + if (!claim_data) { + ESP_LOGE(TAG, "Failed to allocate memory for claim data."); + return; + } + if (__esp_rmaker_claim_init(claim_data) != ESP_OK) { + esp_rmaker_claim_data_free(claim_data); + } else { + *((esp_rmaker_claim_data_t **)args) = claim_data; + } + xEventGroupSetBits(claim_event_group, CLAIM_TASK_BIT); + vTaskDelete(NULL); +} + +static esp_rmaker_claim_data_t *esp_rmaker_claim_init(void) +{ + static bool claim_init_done; + if (claim_init_done) { + ESP_LOGE(TAG, "Claim already initialised"); + return NULL; + } + claim_event_group = xEventGroupCreate(); + if (!claim_event_group) { + ESP_LOGE(TAG, "Couldn't create event group"); + return NULL; + } + esp_rmaker_claim_data_t *claim_data = NULL; + +#define ESP_RMAKER_CLAIM_TASK_STACK_SIZE (10 * 1024) + /* Using tskIDLE_PRIORITY so that the time consuming tasks, especially + * PK generation does not trigger task WatchDog timer. + */ + if (xTaskCreate(&esp_rmaker_claim_task, "claim_task", ESP_RMAKER_CLAIM_TASK_STACK_SIZE, + &claim_data, tskIDLE_PRIORITY, NULL) != pdPASS) { + ESP_LOGE(TAG, "Couldn't create Claim task"); + vEventGroupDelete(claim_event_group); + return NULL; + } + + /* Wait for claim init to complete */ + xEventGroupWaitBits(claim_event_group, CLAIM_TASK_BIT, false, true, portMAX_DELAY); + vEventGroupDelete(claim_event_group); + claim_event_group = NULL; + return claim_data; +} + +#ifdef CONFIG_ESP_RMAKER_SELF_CLAIM +esp_rmaker_claim_data_t *esp_rmaker_self_claim_init(void) +{ + ESP_LOGI(TAG, "Initialising Self Claiming. This may take time."); + return esp_rmaker_claim_init(); +} +#endif +#ifdef CONFIG_ESP_RMAKER_ASSISTED_CLAIM +esp_err_t esp_rmaker_assisted_claim_perform(esp_rmaker_claim_data_t *claim_data) +{ + if (claim_data == NULL) { + ESP_LOGE(TAG, "Assisted claiming not initialised."); + return ESP_ERR_INVALID_STATE; + } + claim_event_group = xEventGroupCreate(); + if (!claim_event_group) { + ESP_LOGE(TAG, "Couldn't create event group"); + return ESP_ERR_NO_MEM; + } + /* Wait for assisted claim to complete */ + ESP_LOGI(TAG, "Waiting for assisted claim to finish."); + xEventGroupWaitBits(claim_event_group, CLAIM_TASK_BIT, false, true, portMAX_DELAY); + esp_err_t err = ESP_FAIL; + if (claim_data->state == RMAKER_CLAIM_STATE_VERIFY_DONE) { + err = ESP_OK; + } +#if RMAKER_USING_NETWORK_PROV + esp_event_handler_unregister(NETWORK_PROV_EVENT, NETWORK_PROV_INIT, &event_handler); + esp_event_handler_unregister(NETWORK_PROV_EVENT, NETWORK_PROV_START, &event_handler); +#else + esp_event_handler_unregister(WIFI_PROV_EVENT, WIFI_PROV_INIT, &event_handler); + esp_event_handler_unregister(WIFI_PROV_EVENT, WIFI_PROV_START, &event_handler); +#endif + esp_rmaker_claim_data_free(claim_data); + vEventGroupDelete(claim_event_group); + return err; +} +esp_rmaker_claim_data_t *esp_rmaker_assisted_claim_init(void) +{ + ESP_LOGI(TAG, "Initialising Assisted Claiming. This may take time."); + esp_rmaker_claim_data_t *claim_data = esp_rmaker_claim_init(); + if (claim_data) { +#if RMAKER_USING_NETWORK_PROV + esp_event_handler_register(NETWORK_PROV_EVENT, NETWORK_PROV_INIT, &event_handler, claim_data); + esp_event_handler_register(NETWORK_PROV_EVENT, NETWORK_PROV_START, &event_handler, claim_data); +#else + esp_event_handler_register(WIFI_PROV_EVENT, WIFI_PROV_INIT, &event_handler, claim_data); + esp_event_handler_register(WIFI_PROV_EVENT, WIFI_PROV_START, &event_handler, claim_data); +#endif + } + return claim_data; +} +#endif diff --git a/components/esp_rainmaker/src/core/esp_rmaker_claim.h b/components/esp_rainmaker/src/core/esp_rmaker_claim.h new file mode 100644 index 0000000..c9aaf5a --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_claim.h @@ -0,0 +1,49 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#define MAX_CSR_SIZE 1024 +#define MAX_PAYLOAD_SIZE 3072 + +typedef enum { + RMAKER_CLAIM_STATE_PK_GENERATED = 1, + RMAKER_CLAIM_STATE_INIT, + RMAKER_CLAIM_STATE_INIT_DONE, + RMAKER_CLAIM_STATE_CSR_GENERATED, + RMAKER_CLAIM_STATE_VERIFY, + RMAKER_CLAIM_STATE_VERIFY_DONE, +} esp_rmaker_claim_state_t; + +typedef struct { + esp_rmaker_claim_state_t state; + unsigned char csr[MAX_CSR_SIZE]; + char payload[MAX_PAYLOAD_SIZE]; + size_t payload_offset; + size_t payload_len; + mbedtls_pk_context key; +} esp_rmaker_claim_data_t; + +#ifdef CONFIG_ESP_RMAKER_SELF_CLAIM +esp_rmaker_claim_data_t * esp_rmaker_self_claim_init(void); +esp_err_t esp_rmaker_self_claim_perform(esp_rmaker_claim_data_t *claim_data); +#endif +#ifdef CONFIG_ESP_RMAKER_ASSISTED_CLAIM +esp_rmaker_claim_data_t * esp_rmaker_assisted_claim_init(void); +esp_err_t esp_rmaker_assisted_claim_perform(esp_rmaker_claim_data_t *claim_data); +#endif + +void esp_rmaker_claim_data_free(esp_rmaker_claim_data_t *claim_data); + diff --git a/components/esp_rainmaker/src/core/esp_rmaker_claim.pb-c.c b/components/esp_rainmaker/src/core/esp_rmaker_claim.pb-c.c new file mode 100644 index 0000000..ea66430 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_claim.pb-c.c @@ -0,0 +1,398 @@ +/* Generated by the protocol buffer compiler. DO NOT EDIT! */ +/* Generated from: esp_rmaker_claim.proto */ + +/* Do not generate deprecated warnings for self */ +#ifndef PROTOBUF_C__NO_DEPRECATED +#define PROTOBUF_C__NO_DEPRECATED +#endif + +#include "esp_rmaker_claim.pb-c.h" +void rmaker_claim__payload_buf__init + (RmakerClaim__PayloadBuf *message) +{ + static const RmakerClaim__PayloadBuf init_value = RMAKER_CLAIM__PAYLOAD_BUF__INIT; + *message = init_value; +} +size_t rmaker_claim__payload_buf__get_packed_size + (const RmakerClaim__PayloadBuf *message) +{ + assert(message->base.descriptor == &rmaker_claim__payload_buf__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t rmaker_claim__payload_buf__pack + (const RmakerClaim__PayloadBuf *message, + uint8_t *out) +{ + assert(message->base.descriptor == &rmaker_claim__payload_buf__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t rmaker_claim__payload_buf__pack_to_buffer + (const RmakerClaim__PayloadBuf *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &rmaker_claim__payload_buf__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +RmakerClaim__PayloadBuf * + rmaker_claim__payload_buf__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (RmakerClaim__PayloadBuf *) + protobuf_c_message_unpack (&rmaker_claim__payload_buf__descriptor, + allocator, len, data); +} +void rmaker_claim__payload_buf__free_unpacked + (RmakerClaim__PayloadBuf *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &rmaker_claim__payload_buf__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void rmaker_claim__resp_payload__init + (RmakerClaim__RespPayload *message) +{ + static const RmakerClaim__RespPayload init_value = RMAKER_CLAIM__RESP_PAYLOAD__INIT; + *message = init_value; +} +size_t rmaker_claim__resp_payload__get_packed_size + (const RmakerClaim__RespPayload *message) +{ + assert(message->base.descriptor == &rmaker_claim__resp_payload__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t rmaker_claim__resp_payload__pack + (const RmakerClaim__RespPayload *message, + uint8_t *out) +{ + assert(message->base.descriptor == &rmaker_claim__resp_payload__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t rmaker_claim__resp_payload__pack_to_buffer + (const RmakerClaim__RespPayload *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &rmaker_claim__resp_payload__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +RmakerClaim__RespPayload * + rmaker_claim__resp_payload__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (RmakerClaim__RespPayload *) + protobuf_c_message_unpack (&rmaker_claim__resp_payload__descriptor, + allocator, len, data); +} +void rmaker_claim__resp_payload__free_unpacked + (RmakerClaim__RespPayload *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &rmaker_claim__resp_payload__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void rmaker_claim__rmaker_claim_payload__init + (RmakerClaim__RMakerClaimPayload *message) +{ + static const RmakerClaim__RMakerClaimPayload init_value = RMAKER_CLAIM__RMAKER_CLAIM_PAYLOAD__INIT; + *message = init_value; +} +size_t rmaker_claim__rmaker_claim_payload__get_packed_size + (const RmakerClaim__RMakerClaimPayload *message) +{ + assert(message->base.descriptor == &rmaker_claim__rmaker_claim_payload__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t rmaker_claim__rmaker_claim_payload__pack + (const RmakerClaim__RMakerClaimPayload *message, + uint8_t *out) +{ + assert(message->base.descriptor == &rmaker_claim__rmaker_claim_payload__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t rmaker_claim__rmaker_claim_payload__pack_to_buffer + (const RmakerClaim__RMakerClaimPayload *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &rmaker_claim__rmaker_claim_payload__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +RmakerClaim__RMakerClaimPayload * + rmaker_claim__rmaker_claim_payload__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (RmakerClaim__RMakerClaimPayload *) + protobuf_c_message_unpack (&rmaker_claim__rmaker_claim_payload__descriptor, + allocator, len, data); +} +void rmaker_claim__rmaker_claim_payload__free_unpacked + (RmakerClaim__RMakerClaimPayload *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &rmaker_claim__rmaker_claim_payload__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +static const ProtobufCFieldDescriptor rmaker_claim__payload_buf__field_descriptors[3] = +{ + { + "Offset", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_UINT32, + 0, /* quantifier_offset */ + offsetof(RmakerClaim__PayloadBuf, offset), + NULL, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "Payload", + 2, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_BYTES, + 0, /* quantifier_offset */ + offsetof(RmakerClaim__PayloadBuf, payload), + NULL, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "TotalLen", + 3, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_UINT32, + 0, /* quantifier_offset */ + offsetof(RmakerClaim__PayloadBuf, totallen), + NULL, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned rmaker_claim__payload_buf__field_indices_by_name[] = { + 0, /* field[0] = Offset */ + 1, /* field[1] = Payload */ + 2, /* field[2] = TotalLen */ +}; +static const ProtobufCIntRange rmaker_claim__payload_buf__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 3 } +}; +const ProtobufCMessageDescriptor rmaker_claim__payload_buf__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "rmaker_claim.PayloadBuf", + "PayloadBuf", + "RmakerClaim__PayloadBuf", + "rmaker_claim", + sizeof(RmakerClaim__PayloadBuf), + 3, + rmaker_claim__payload_buf__field_descriptors, + rmaker_claim__payload_buf__field_indices_by_name, + 1, rmaker_claim__payload_buf__number_ranges, + (ProtobufCMessageInit) rmaker_claim__payload_buf__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor rmaker_claim__resp_payload__field_descriptors[2] = +{ + { + "Status", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_ENUM, + 0, /* quantifier_offset */ + offsetof(RmakerClaim__RespPayload, status), + &rmaker_claim__rmaker_claim_status__descriptor, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "Buf", + 2, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + 0, /* quantifier_offset */ + offsetof(RmakerClaim__RespPayload, buf), + &rmaker_claim__payload_buf__descriptor, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned rmaker_claim__resp_payload__field_indices_by_name[] = { + 1, /* field[1] = Buf */ + 0, /* field[0] = Status */ +}; +static const ProtobufCIntRange rmaker_claim__resp_payload__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 2 } +}; +const ProtobufCMessageDescriptor rmaker_claim__resp_payload__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "rmaker_claim.RespPayload", + "RespPayload", + "RmakerClaim__RespPayload", + "rmaker_claim", + sizeof(RmakerClaim__RespPayload), + 2, + rmaker_claim__resp_payload__field_descriptors, + rmaker_claim__resp_payload__field_indices_by_name, + 1, rmaker_claim__resp_payload__number_ranges, + (ProtobufCMessageInit) rmaker_claim__resp_payload__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor rmaker_claim__rmaker_claim_payload__field_descriptors[3] = +{ + { + "msg", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_ENUM, + 0, /* quantifier_offset */ + offsetof(RmakerClaim__RMakerClaimPayload, msg), + &rmaker_claim__rmaker_claim_msg_type__descriptor, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "cmdPayload", + 10, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(RmakerClaim__RMakerClaimPayload, payload_case), + offsetof(RmakerClaim__RMakerClaimPayload, cmdpayload), + &rmaker_claim__payload_buf__descriptor, + NULL, + 0 | PROTOBUF_C_FIELD_FLAG_ONEOF, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "respPayload", + 11, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(RmakerClaim__RMakerClaimPayload, payload_case), + offsetof(RmakerClaim__RMakerClaimPayload, resppayload), + &rmaker_claim__resp_payload__descriptor, + NULL, + 0 | PROTOBUF_C_FIELD_FLAG_ONEOF, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned rmaker_claim__rmaker_claim_payload__field_indices_by_name[] = { + 1, /* field[1] = cmdPayload */ + 0, /* field[0] = msg */ + 2, /* field[2] = respPayload */ +}; +static const ProtobufCIntRange rmaker_claim__rmaker_claim_payload__number_ranges[2 + 1] = +{ + { 1, 0 }, + { 10, 1 }, + { 0, 3 } +}; +const ProtobufCMessageDescriptor rmaker_claim__rmaker_claim_payload__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "rmaker_claim.RMakerClaimPayload", + "RMakerClaimPayload", + "RmakerClaim__RMakerClaimPayload", + "rmaker_claim", + sizeof(RmakerClaim__RMakerClaimPayload), + 3, + rmaker_claim__rmaker_claim_payload__field_descriptors, + rmaker_claim__rmaker_claim_payload__field_indices_by_name, + 2, rmaker_claim__rmaker_claim_payload__number_ranges, + (ProtobufCMessageInit) rmaker_claim__rmaker_claim_payload__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCEnumValue rmaker_claim__rmaker_claim_status__enum_values_by_number[5] = +{ + { "Success", "RMAKER_CLAIM__RMAKER_CLAIM_STATUS__Success", 0 }, + { "Fail", "RMAKER_CLAIM__RMAKER_CLAIM_STATUS__Fail", 1 }, + { "InvalidParam", "RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidParam", 2 }, + { "InvalidState", "RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidState", 3 }, + { "NoMemory", "RMAKER_CLAIM__RMAKER_CLAIM_STATUS__NoMemory", 4 }, +}; +static const ProtobufCIntRange rmaker_claim__rmaker_claim_status__value_ranges[] = { +{0, 0},{0, 5} +}; +static const ProtobufCEnumValueIndex rmaker_claim__rmaker_claim_status__enum_values_by_name[5] = +{ + { "Fail", 1 }, + { "InvalidParam", 2 }, + { "InvalidState", 3 }, + { "NoMemory", 4 }, + { "Success", 0 }, +}; +const ProtobufCEnumDescriptor rmaker_claim__rmaker_claim_status__descriptor = +{ + PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, + "rmaker_claim.RMakerClaimStatus", + "RMakerClaimStatus", + "RmakerClaim__RMakerClaimStatus", + "rmaker_claim", + 5, + rmaker_claim__rmaker_claim_status__enum_values_by_number, + 5, + rmaker_claim__rmaker_claim_status__enum_values_by_name, + 1, + rmaker_claim__rmaker_claim_status__value_ranges, + NULL,NULL,NULL,NULL /* reserved[1234] */ +}; +static const ProtobufCEnumValue rmaker_claim__rmaker_claim_msg_type__enum_values_by_number[8] = +{ + { "TypeCmdClaimStart", "RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimStart", 0 }, + { "TypeRespClaimStart", "RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeRespClaimStart", 1 }, + { "TypeCmdClaimInit", "RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimInit", 2 }, + { "TypeRespClaimInit", "RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeRespClaimInit", 3 }, + { "TypeCmdClaimVerify", "RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimVerify", 4 }, + { "TypeRespClaimVerify", "RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeRespClaimVerify", 5 }, + { "TypeCmdClaimAbort", "RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimAbort", 6 }, + { "TypeRespClaimAbort", "RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeRespClaimAbort", 7 }, +}; +static const ProtobufCIntRange rmaker_claim__rmaker_claim_msg_type__value_ranges[] = { +{0, 0},{0, 8} +}; +static const ProtobufCEnumValueIndex rmaker_claim__rmaker_claim_msg_type__enum_values_by_name[8] = +{ + { "TypeCmdClaimAbort", 6 }, + { "TypeCmdClaimInit", 2 }, + { "TypeCmdClaimStart", 0 }, + { "TypeCmdClaimVerify", 4 }, + { "TypeRespClaimAbort", 7 }, + { "TypeRespClaimInit", 3 }, + { "TypeRespClaimStart", 1 }, + { "TypeRespClaimVerify", 5 }, +}; +const ProtobufCEnumDescriptor rmaker_claim__rmaker_claim_msg_type__descriptor = +{ + PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, + "rmaker_claim.RMakerClaimMsgType", + "RMakerClaimMsgType", + "RmakerClaim__RMakerClaimMsgType", + "rmaker_claim", + 8, + rmaker_claim__rmaker_claim_msg_type__enum_values_by_number, + 8, + rmaker_claim__rmaker_claim_msg_type__enum_values_by_name, + 1, + rmaker_claim__rmaker_claim_msg_type__value_ranges, + NULL,NULL,NULL,NULL /* reserved[1234] */ +}; diff --git a/components/esp_rainmaker/src/core/esp_rmaker_claim.pb-c.h b/components/esp_rainmaker/src/core/esp_rmaker_claim.pb-c.h new file mode 100644 index 0000000..c6fc37e --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_claim.pb-c.h @@ -0,0 +1,175 @@ +/* Generated by the protocol buffer compiler. DO NOT EDIT! */ +/* Generated from: esp_rmaker_claim.proto */ + +#ifndef PROTOBUF_C_esp_5frmaker_5fclaim_2eproto__INCLUDED +#define PROTOBUF_C_esp_5frmaker_5fclaim_2eproto__INCLUDED + +#include + +PROTOBUF_C__BEGIN_DECLS + +#if PROTOBUF_C_VERSION_NUMBER < 1003000 +# error This file was generated by a newer version of protoc-c which is incompatible with your libprotobuf-c headers. Please update your headers. +#elif 1003003 < PROTOBUF_C_MIN_COMPILER_VERSION +# error This file was generated by an older version of protoc-c which is incompatible with your libprotobuf-c headers. Please regenerate this file with a newer version of protoc-c. +#endif + + +typedef struct _RmakerClaim__PayloadBuf RmakerClaim__PayloadBuf; +typedef struct _RmakerClaim__RespPayload RmakerClaim__RespPayload; +typedef struct _RmakerClaim__RMakerClaimPayload RmakerClaim__RMakerClaimPayload; + + +/* --- enums --- */ + +typedef enum _RmakerClaim__RMakerClaimStatus { + RMAKER_CLAIM__RMAKER_CLAIM_STATUS__Success = 0, + RMAKER_CLAIM__RMAKER_CLAIM_STATUS__Fail = 1, + RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidParam = 2, + RMAKER_CLAIM__RMAKER_CLAIM_STATUS__InvalidState = 3, + RMAKER_CLAIM__RMAKER_CLAIM_STATUS__NoMemory = 4 + PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(RMAKER_CLAIM__RMAKER_CLAIM_STATUS) +} RmakerClaim__RMakerClaimStatus; +typedef enum _RmakerClaim__RMakerClaimMsgType { + RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimStart = 0, + RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeRespClaimStart = 1, + RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimInit = 2, + RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeRespClaimInit = 3, + RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimVerify = 4, + RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeRespClaimVerify = 5, + RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimAbort = 6, + RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeRespClaimAbort = 7 + PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE) +} RmakerClaim__RMakerClaimMsgType; + +/* --- messages --- */ + +struct _RmakerClaim__PayloadBuf +{ + ProtobufCMessage base; + uint32_t offset; + ProtobufCBinaryData payload; + uint32_t totallen; +}; +#define RMAKER_CLAIM__PAYLOAD_BUF__INIT \ + { PROTOBUF_C_MESSAGE_INIT (&rmaker_claim__payload_buf__descriptor) \ + , 0, {0,NULL}, 0 } + + +struct _RmakerClaim__RespPayload +{ + ProtobufCMessage base; + RmakerClaim__RMakerClaimStatus status; + RmakerClaim__PayloadBuf *buf; +}; +#define RMAKER_CLAIM__RESP_PAYLOAD__INIT \ + { PROTOBUF_C_MESSAGE_INIT (&rmaker_claim__resp_payload__descriptor) \ + , RMAKER_CLAIM__RMAKER_CLAIM_STATUS__Success, NULL } + + +typedef enum { + RMAKER_CLAIM__RMAKER_CLAIM_PAYLOAD__PAYLOAD__NOT_SET = 0, + RMAKER_CLAIM__RMAKER_CLAIM_PAYLOAD__PAYLOAD_CMD_PAYLOAD = 10, + RMAKER_CLAIM__RMAKER_CLAIM_PAYLOAD__PAYLOAD_RESP_PAYLOAD = 11 + PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(RMAKER_CLAIM__RMAKER_CLAIM_PAYLOAD__PAYLOAD) +} RmakerClaim__RMakerClaimPayload__PayloadCase; + +struct _RmakerClaim__RMakerClaimPayload +{ + ProtobufCMessage base; + RmakerClaim__RMakerClaimMsgType msg; + RmakerClaim__RMakerClaimPayload__PayloadCase payload_case; + union { + RmakerClaim__PayloadBuf *cmdpayload; + RmakerClaim__RespPayload *resppayload; + }; +}; +#define RMAKER_CLAIM__RMAKER_CLAIM_PAYLOAD__INIT \ + { PROTOBUF_C_MESSAGE_INIT (&rmaker_claim__rmaker_claim_payload__descriptor) \ + , RMAKER_CLAIM__RMAKER_CLAIM_MSG_TYPE__TypeCmdClaimStart, RMAKER_CLAIM__RMAKER_CLAIM_PAYLOAD__PAYLOAD__NOT_SET, {0} } + + +/* RmakerClaim__PayloadBuf methods */ +void rmaker_claim__payload_buf__init + (RmakerClaim__PayloadBuf *message); +size_t rmaker_claim__payload_buf__get_packed_size + (const RmakerClaim__PayloadBuf *message); +size_t rmaker_claim__payload_buf__pack + (const RmakerClaim__PayloadBuf *message, + uint8_t *out); +size_t rmaker_claim__payload_buf__pack_to_buffer + (const RmakerClaim__PayloadBuf *message, + ProtobufCBuffer *buffer); +RmakerClaim__PayloadBuf * + rmaker_claim__payload_buf__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data); +void rmaker_claim__payload_buf__free_unpacked + (RmakerClaim__PayloadBuf *message, + ProtobufCAllocator *allocator); +/* RmakerClaim__RespPayload methods */ +void rmaker_claim__resp_payload__init + (RmakerClaim__RespPayload *message); +size_t rmaker_claim__resp_payload__get_packed_size + (const RmakerClaim__RespPayload *message); +size_t rmaker_claim__resp_payload__pack + (const RmakerClaim__RespPayload *message, + uint8_t *out); +size_t rmaker_claim__resp_payload__pack_to_buffer + (const RmakerClaim__RespPayload *message, + ProtobufCBuffer *buffer); +RmakerClaim__RespPayload * + rmaker_claim__resp_payload__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data); +void rmaker_claim__resp_payload__free_unpacked + (RmakerClaim__RespPayload *message, + ProtobufCAllocator *allocator); +/* RmakerClaim__RMakerClaimPayload methods */ +void rmaker_claim__rmaker_claim_payload__init + (RmakerClaim__RMakerClaimPayload *message); +size_t rmaker_claim__rmaker_claim_payload__get_packed_size + (const RmakerClaim__RMakerClaimPayload *message); +size_t rmaker_claim__rmaker_claim_payload__pack + (const RmakerClaim__RMakerClaimPayload *message, + uint8_t *out); +size_t rmaker_claim__rmaker_claim_payload__pack_to_buffer + (const RmakerClaim__RMakerClaimPayload *message, + ProtobufCBuffer *buffer); +RmakerClaim__RMakerClaimPayload * + rmaker_claim__rmaker_claim_payload__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data); +void rmaker_claim__rmaker_claim_payload__free_unpacked + (RmakerClaim__RMakerClaimPayload *message, + ProtobufCAllocator *allocator); +/* --- per-message closures --- */ + +typedef void (*RmakerClaim__PayloadBuf_Closure) + (const RmakerClaim__PayloadBuf *message, + void *closure_data); +typedef void (*RmakerClaim__RespPayload_Closure) + (const RmakerClaim__RespPayload *message, + void *closure_data); +typedef void (*RmakerClaim__RMakerClaimPayload_Closure) + (const RmakerClaim__RMakerClaimPayload *message, + void *closure_data); + +/* --- services --- */ + + +/* --- descriptors --- */ + +extern const ProtobufCEnumDescriptor rmaker_claim__rmaker_claim_status__descriptor; +extern const ProtobufCEnumDescriptor rmaker_claim__rmaker_claim_msg_type__descriptor; +extern const ProtobufCMessageDescriptor rmaker_claim__payload_buf__descriptor; +extern const ProtobufCMessageDescriptor rmaker_claim__resp_payload__descriptor; +extern const ProtobufCMessageDescriptor rmaker_claim__rmaker_claim_payload__descriptor; + +PROTOBUF_C__END_DECLS + + +#endif /* PROTOBUF_C_esp_5frmaker_5fclaim_2eproto__INCLUDED */ diff --git a/components/esp_rainmaker/src/core/esp_rmaker_claim.proto b/components/esp_rainmaker/src/core/esp_rmaker_claim.proto new file mode 100644 index 0000000..93c21d0 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_claim.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package rmaker_claim; + +enum RMakerClaimStatus { + Success = 0; + Fail = 1; + InvalidParam = 2; + InvalidState = 3; + NoMemory = 4; +} + +message PayloadBuf { + uint32 Offset = 1; + bytes Payload = 2; + uint32 TotalLen = 3; +} + +message RespPayload { + RMakerClaimStatus Status = 1; + PayloadBuf Buf = 2; +} + +enum RMakerClaimMsgType { + TypeCmdClaimStart = 0; + TypeRespClaimStart = 1; + TypeCmdClaimInit = 2; + TypeRespClaimInit = 3; + TypeCmdClaimVerify = 4; + TypeRespClaimVerify = 5; + TypeCmdClaimAbort = 6; + TypeRespClaimAbort = 7; +} + +message RMakerClaimPayload { + RMakerClaimMsgType msg = 1; + oneof payload { + PayloadBuf cmdPayload = 10; + RespPayload respPayload = 11; + } +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_client_data.c b/components/esp_rainmaker/src/core/esp_rmaker_client_data.c new file mode 100644 index 0000000..53e0f59 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_client_data.c @@ -0,0 +1,178 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include + +#include +#include +#include + +#include "esp_rmaker_internal.h" +#include "esp_rmaker_client_data.h" + +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + #include "esp_secure_cert_read.h" + /* + * Since TAG is not used in any other place in this file at the moment, + * it has been placed inside this #ifdef to avoid -Werror=unused-variable. + */ + static const char *TAG = "esp_rmaker_client_data"; +#endif + +extern uint8_t mqtt_server_root_ca_pem_start[] asm("_binary_rmaker_mqtt_server_crt_start"); +extern uint8_t mqtt_server_root_ca_pem_end[] asm("_binary_rmaker_mqtt_server_crt_end"); + +char * esp_rmaker_get_mqtt_host() +{ +#ifdef CONFIG_ESP_RMAKER_READ_MQTT_HOST_FROM_CONFIG + return strdup(CONFIG_ESP_RMAKER_MQTT_HOST); +#else + char *host = esp_rmaker_factory_get(ESP_RMAKER_MQTT_HOST_NVS_KEY); +#if defined(CONFIG_ESP_RMAKER_SELF_CLAIM) || defined(CONFIG_ESP_RMAKER_ASSISTED_CLAIM) + if (!host) { + return strdup(CONFIG_ESP_RMAKER_MQTT_HOST); + } +#endif /* defined(CONFIG_ESP_RMAKER_SELF_CLAIM) || defined(CONFIG_ESP_RMAKER_ASSISTED_CLAIM) */ + return host; +#endif /* !CONFIG_ESP_RMAKER_READ_MQTT_HOST_FROM_CONFIG */ +} + +char * esp_rmaker_get_client_cert() +{ +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + uint32_t client_cert_len = 0; + char *client_cert_addr = NULL; + if (esp_secure_cert_get_device_cert(&client_cert_addr, &client_cert_len) == ESP_OK) { + return client_cert_addr; + } else { + ESP_LOGE(TAG, "Failed to obtain flash address of device cert"); + ESP_LOGI(TAG, "Attempting to fetch client certificate from NVS"); + } +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + return esp_rmaker_factory_get(ESP_RMAKER_CLIENT_CERT_NVS_KEY); +} + +size_t esp_rmaker_get_client_cert_len() +{ +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + uint32_t client_cert_len = 0; + char *client_cert_addr = NULL; + if (esp_secure_cert_get_device_cert(&client_cert_addr, &client_cert_len) == ESP_OK) { + return client_cert_len; + } else { + ESP_LOGE(TAG, "Failed to obtain flash address of device cert"); + ESP_LOGI(TAG, "Attempting to fetch client certificate from NVS"); + } +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + return esp_rmaker_factory_get_size(ESP_RMAKER_CLIENT_CERT_NVS_KEY) + 1; /* +1 for NULL terminating byte */ +} + +char * esp_rmaker_get_client_key() +{ +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + uint32_t client_key_len = 0; + char *client_key_addr = NULL; + if (esp_secure_cert_get_priv_key(&client_key_addr, &client_key_len) == ESP_OK) { + return client_key_addr; + } else { + ESP_LOGE(TAG, "Failed to obtain flash address of private_key"); + ESP_LOGI(TAG, "Attempting to fetch key from NVS"); + } +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + return esp_rmaker_factory_get(ESP_RMAKER_CLIENT_KEY_NVS_KEY); +} + +size_t esp_rmaker_get_client_key_len() +{ +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + uint32_t client_key_len = 0; + char *client_key_addr = NULL; + if (esp_secure_cert_get_priv_key(&client_key_addr, &client_key_len) == ESP_OK) { + return client_key_len; + } else { + ESP_LOGE(TAG, "Failed to obtain flash address of private_key"); + ESP_LOGI(TAG, "Attempting to fetch key from NVS"); + } +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + return esp_rmaker_factory_get_size(ESP_RMAKER_CLIENT_KEY_NVS_KEY) + 1; /* +1 for NULL terminating byte */ +} + +char * esp_rmaker_get_client_csr() +{ + return esp_rmaker_factory_get(ESP_RMAKER_CLIENT_CSR_NVS_KEY); +} + +esp_rmaker_mqtt_conn_params_t *esp_rmaker_get_mqtt_conn_params() +{ + esp_rmaker_mqtt_conn_params_t *mqtt_conn_params = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_mqtt_conn_params_t)); + +#if defined(CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR) && defined(CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL) + mqtt_conn_params->ds_data = esp_secure_cert_get_ds_ctx(); + if (mqtt_conn_params->ds_data == NULL) /* Get client key only if ds_data is NULL */ +#endif /* (defined(CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR) && defined(CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL)) */ + { + if ((mqtt_conn_params->client_key = esp_rmaker_get_client_key()) == NULL) { + goto init_err; + } + mqtt_conn_params->client_key_len = esp_rmaker_get_client_key_len(); + } + if ((mqtt_conn_params->client_cert = esp_rmaker_get_client_cert()) == NULL) { + goto init_err; + } + mqtt_conn_params->client_cert_len = esp_rmaker_get_client_cert_len(); + if ((mqtt_conn_params->mqtt_host = esp_rmaker_get_mqtt_host()) == NULL) { + goto init_err; + } + mqtt_conn_params->server_cert = (char *)mqtt_server_root_ca_pem_start; + mqtt_conn_params->client_id = esp_rmaker_get_node_id(); + return mqtt_conn_params; +init_err: + esp_rmaker_clean_mqtt_conn_params(mqtt_conn_params); + free(mqtt_conn_params); + return NULL; +} + +void esp_rmaker_clean_mqtt_conn_params(esp_rmaker_mqtt_conn_params_t *mqtt_conn_params) +{ + if (mqtt_conn_params) { + if (mqtt_conn_params->mqtt_host) { + free(mqtt_conn_params->mqtt_host); + } +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + if (mqtt_conn_params->client_cert) { + esp_secure_cert_free_device_cert(mqtt_conn_params->client_cert); + } +#ifdef CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL + if (mqtt_conn_params->ds_data) { + esp_secure_cert_free_ds_ctx(mqtt_conn_params->ds_data); + } +#else /* !CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL */ + if (mqtt_conn_params->client_key) { + esp_secure_cert_free_priv_key(mqtt_conn_params->client_key); + } +#endif /* CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL */ +#else /* !CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + if (mqtt_conn_params->client_cert) { + free(mqtt_conn_params->client_cert); + } + if (mqtt_conn_params->client_key) { + free(mqtt_conn_params->client_key); + } +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + } +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_client_data.h b/components/esp_rainmaker/src/core/esp_rmaker_client_data.h new file mode 100644 index 0000000..f89d573 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_client_data.h @@ -0,0 +1,31 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#include + +#define ESP_RMAKER_CLIENT_CERT_NVS_KEY "client_cert" +#define ESP_RMAKER_CLIENT_KEY_NVS_KEY "client_key" +#define ESP_RMAKER_MQTT_HOST_NVS_KEY "mqtt_host" +#define ESP_RMAKER_CLIENT_CSR_NVS_KEY "csr" +#define ESP_RMAKER_CLIENT_RANDOM_NVS_KEY "random" + +char *esp_rmaker_get_client_cert(); +size_t esp_rmaker_get_client_cert_len(); +char *esp_rmaker_get_client_key(); +size_t esp_rmaker_get_client_key_len(); +char *esp_rmaker_get_client_csr(); +char *esp_rmaker_get_mqtt_host(); +esp_rmaker_mqtt_conn_params_t *esp_rmaker_get_mqtt_conn_params(); +void esp_rmaker_clean_mqtt_conn_params(esp_rmaker_mqtt_conn_params_t *mqtt_conn_params); diff --git a/components/esp_rainmaker/src/core/esp_rmaker_cmd_resp_manager.c b/components/esp_rainmaker/src/core/esp_rmaker_cmd_resp_manager.c new file mode 100644 index 0000000..8a2d775 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_cmd_resp_manager.c @@ -0,0 +1,150 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include +#include +#include +#include +#include +#include "esp_rmaker_internal.h" +#include "esp_rmaker_mqtt_topics.h" + +static const char *TAG = "esp_rmaker_cmd_resp"; + +#ifdef CONFIG_ESP_RMAKER_CMD_RESP_TEST_ENABLE + +/* These are for testing purpose only */ +static void esp_rmaker_resp_callback(const char *topic, void *payload, size_t payload_len, void *priv_data) +{ + esp_rmaker_cmd_resp_parse_response(payload, payload_len, priv_data); + +} + +esp_err_t esp_rmaker_test_cmd_resp(const void *cmd, size_t cmd_len, void *priv_data) +{ + if (!cmd) { + ESP_LOGE(TAG, "No command data to send."); + return ESP_ERR_INVALID_ARG; + } + char publish_topic[MQTT_TOPIC_BUFFER_SIZE]; + snprintf(publish_topic, sizeof(publish_topic), "node/%s/%s", esp_rmaker_get_node_id(), TO_NODE_TOPIC_SUFFIX); + return esp_rmaker_mqtt_publish(publish_topic, (void *)cmd, cmd_len, RMAKER_MQTT_QOS1, NULL); +} + +static esp_err_t esp_rmaker_cmd_resp_test_enable(void) +{ + char subscribe_topic[100]; + snprintf(subscribe_topic, sizeof(subscribe_topic), "node/%s/%s", + esp_rmaker_get_node_id(), CMD_RESP_TOPIC_SUFFIX); + esp_err_t err = esp_rmaker_mqtt_subscribe(subscribe_topic, esp_rmaker_resp_callback, RMAKER_MQTT_QOS1, NULL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to subscribe to %s. Error %d", subscribe_topic, err); + return ESP_FAIL; + } + ESP_LOGI(TAG, "Command-Response test support enabled."); + return ESP_OK; +} + +#else +esp_err_t esp_rmaker_test_cmd_resp(const void *cmd, size_t cmd_len, void *priv_data) +{ + ESP_LOGE(TAG, "Please enable CONFIG_ESP_RMAKER_CMD_RESP_TEST_ENABLE to use this."); + return ESP_FAIL; +} +#endif /* !CONFIG_ESP_RMAKER_CMD_RESP_TEST_ENABLE */ + +static esp_err_t esp_rmaker_publish_response(void *output, size_t output_len) +{ + if (output) { + char publish_topic[MQTT_TOPIC_BUFFER_SIZE]; + esp_rmaker_create_mqtt_topic(publish_topic, sizeof(publish_topic), CMD_RESP_TOPIC_SUFFIX, CMD_RESP_TOPIC_RULE); + esp_err_t err = esp_rmaker_mqtt_publish(publish_topic, output, output_len, RMAKER_MQTT_QOS1, NULL); + free(output); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to publish reponse."); + return err; + } + } else { + ESP_LOGW(TAG, "No output generated by command-response handler."); + } + return ESP_OK; +} + +static void esp_rmaker_cmd_callback(const char *topic, void *payload, size_t payload_len, void *priv_data) +{ + void *output = NULL; + size_t output_len = 0; + /* Any command data received is directly sent to the command response framework and on success, + * the response (if any) is sent back to the MQTT Broker. + */ + if (esp_rmaker_cmd_response_handler(payload, payload_len, &output, &output_len) == ESP_OK) { + esp_rmaker_publish_response(output, output_len); + } +} + +/* Keeping the variable outside the function as there would subsequently also be a function + * to disable command response. + */ +static bool cmd_resp_topic_subscribed = false; +static esp_err_t esp_rmaker_cmd_resp_check_pending(void) +{ + ESP_LOGI(TAG, "Checking for pending commands."); + char subscribe_topic[100]; + snprintf(subscribe_topic, sizeof(subscribe_topic), "node/%s/%s", + esp_rmaker_get_node_id(), TO_NODE_TOPIC_SUFFIX); + if (!cmd_resp_topic_subscribed) { + /* Subscribing just once because any subsequent reconnect will automatically subscribe to the topic */ + esp_err_t err = esp_rmaker_mqtt_subscribe(subscribe_topic, esp_rmaker_cmd_callback, RMAKER_MQTT_QOS1, NULL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to subscribe to %s. Error %d", subscribe_topic, err); + return ESP_FAIL; + } + cmd_resp_topic_subscribed = true; + } + void *output = NULL; + size_t output_len = 0; + if (esp_rmaker_cmd_prepare_empty_response(&output, &output_len) == ESP_OK) { + return esp_rmaker_publish_response(output, output_len); + } + return ESP_FAIL; +} + +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + esp_rmaker_cmd_resp_check_pending(); +} + +esp_err_t esp_rmaker_cmd_response_enable(void) +{ + static bool enabled = false; + if (enabled == true) { + ESP_LOGI(TAG, "Command-response Module already enabled."); + return ESP_OK; + } + ESP_LOGI(TAG, "Enabling Command-Response Module."); + if (esp_rmaker_is_mqtt_connected()) { + esp_rmaker_cmd_resp_check_pending(); + } + if (esp_event_handler_register(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_CONNECTED, &event_handler, NULL) != ESP_OK) { + ESP_LOGE(TAG, "RMAKER_MQTT_EVENT_CONNECTED event subscription failed."); + return ESP_FAIL; + } +#ifdef CONFIG_ESP_RMAKER_CMD_RESP_TEST_ENABLE + esp_rmaker_cmd_resp_test_enable(); +#endif /* CONFIG_ESP_RMAKER_CMD_RESP_TEST_ENABLE */ + enabled = true; + return ESP_OK; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_core.c b/components/esp_rainmaker/src/core/esp_rmaker_core.c new file mode 100644 index 0000000..e8a517b --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_core.c @@ -0,0 +1,714 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include "esp_rmaker_internal.h" +#include "esp_rmaker_mqtt.h" +#include "esp_rmaker_claim.h" +#include "esp_rmaker_client_data.h" + +#ifdef CONFIG_OPENTHREAD_ENABLED +#include +#include +#include +#endif + +#if defined(CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI) +static const int WIFI_CONNECTED_EVENT = BIT0; +#elif defined(CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD) +static const int THREAD_SET_DNS_SEVER_EVENT = BIT0; +#endif + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) +#include "esp_mac.h" +#endif + +static const int MQTT_CONNECTED_EVENT = BIT1; +static EventGroupHandle_t rmaker_core_event_group; + +ESP_EVENT_DEFINE_BASE(RMAKER_EVENT); + +static const char *TAG = "esp_rmaker_core"; + + +#if defined(CONFIG_ESP_RMAKER_SELF_CLAIM) || defined(CONFIG_ESP_RMAKER_ASSISTED_CLAIM) +#define ESP_RMAKER_CLAIM_ENABLED +#endif + +#define ESP_RMAKER_CHECK_HANDLE(rval) \ +{ \ + if (!esp_rmaker_priv_data) {\ + ESP_LOGE(TAG, "ESP RainMaker not initialised"); \ + return rval; \ + } \ +} + +#define ESP_CLAIM_NODE_ID_SIZE 12 + +/* Handle to maintain internal information (will move to an internal file) */ +typedef struct { + char *node_id; + const esp_rmaker_node_t *node; + bool enable_time_sync; + esp_rmaker_state_t state; + bool mqtt_connected; + esp_rmaker_mqtt_conn_params_t *mqtt_conn_params; +#ifdef ESP_RMAKER_CLAIM_ENABLED + bool need_claim; + esp_rmaker_claim_data_t *claim_data; +#endif /* ESP_RMAKER_CLAIM_ENABLED */ +} esp_rmaker_priv_data_t; + +static esp_rmaker_priv_data_t *esp_rmaker_priv_data; + +bool esp_rmaker_is_mqtt_connected() +{ + if (esp_rmaker_priv_data) { + return esp_rmaker_priv_data->mqtt_connected; + } + return false; +} + +esp_rmaker_state_t esp_rmaker_get_state(void) +{ + if (esp_rmaker_priv_data) { + return esp_rmaker_priv_data->state; + } + return ESP_RMAKER_STATE_DEINIT; +} + +static void reset_event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + switch (event_id) { + case RMAKER_EVENT_WIFI_RESET: + esp_rmaker_mqtt_disconnect(); + break; + case RMAKER_EVENT_FACTORY_RESET: + esp_rmaker_reset_user_node_mapping(); + break; + default: + break; + } +} + +static void esp_rmaker_mqtt_event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + if (!esp_rmaker_priv_data) { + return; + } + if (event_base == RMAKER_COMMON_EVENT && event_id == RMAKER_MQTT_EVENT_CONNECTED) { + esp_rmaker_priv_data->mqtt_connected = true; + } else if (event_base == RMAKER_COMMON_EVENT && event_id == RMAKER_MQTT_EVENT_DISCONNECTED) { + esp_rmaker_priv_data->mqtt_connected = false; + } +} + +#ifdef CONFIG_ESP_RMAKER_READ_NODE_ID_FROM_CERT_CN + +#include "mbedtls/x509_crt.h" +#include "mbedtls/oid.h" + +static char* esp_rmaker_populate_node_id_from_cert() +{ + void *addr = NULL; + size_t len = 0; + addr = esp_rmaker_get_client_cert(); + if (addr) { + len = esp_rmaker_get_client_cert_len(); + } else { + ESP_LOGE(TAG, "Failed to get device certificate."); + return NULL; + } + mbedtls_x509_crt crt; + mbedtls_x509_crt_init(&crt); + char *node_id = NULL; + int ret = mbedtls_x509_crt_parse(&crt, addr, len); + if (ret != 0) { + ESP_LOGE(TAG, "Parsing of device certificate failed, returned %02X", ret); + } else { + mbedtls_asn1_named_data *cn_data; + cn_data = mbedtls_asn1_find_named_data(&crt.subject, MBEDTLS_OID_AT_CN, + MBEDTLS_OID_SIZE(MBEDTLS_OID_AT_CN)); + if (cn_data) { + node_id = MEM_CALLOC_EXTRAM(1, cn_data->val.len + 1); + memcpy(node_id, (const char *)cn_data->val.p, cn_data->val.len); + } + } + mbedtls_x509_crt_free(&crt); + return node_id; +} +#endif + +static char *esp_rmaker_populate_node_id(bool use_claiming) +{ +#ifdef CONFIG_ESP_RMAKER_READ_NODE_ID_FROM_CERT_CN + char *node_id = esp_rmaker_populate_node_id_from_cert(); +#else /* !CONFIG_ESP_RMAKER_READ_NODE_ID_FROM_CERT_CN */ + char *node_id = esp_rmaker_factory_get("node_id"); +#endif /* CONFIG_ESP_RMAKER_READ_NODE_ID_FROM_CERT_CN */ + +#ifdef ESP_RMAKER_CLAIM_ENABLED + if (!node_id && use_claiming) { + uint8_t mac_addr[6]; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) + /* ESP_MAC_BASE was introduced in ESP-IDF v5.1. It is the same as the Wi-Fi Station MAC address + * for chips supporting Wi-Fi. We can use base MAC address to generate claim init request for both + * Wi-Fi and Thread devices + */ + esp_err_t err = esp_read_mac(mac_addr, ESP_MAC_BASE); +#else + /* Thread was officially supported in ESP-IDF v5.1. Use Wi-Fi Station MAC address to generate claim + * init request. + */ + esp_err_t err = esp_wifi_get_mac(WIFI_IF_STA, mac_addr); +#endif + + if (err != ESP_OK) { + ESP_LOGE(TAG, "Could not fetch MAC address."); + return NULL; + } + node_id = MEM_CALLOC_EXTRAM(1, ESP_CLAIM_NODE_ID_SIZE + 1); /* +1 for NULL terminatation */ + snprintf(node_id, ESP_CLAIM_NODE_ID_SIZE + 1, "%02X%02X%02X%02X%02X%02X", + mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); + } +#endif /* ESP_RMAKER_CLAIM_ENABLED */ + return node_id; +} + +esp_err_t esp_rmaker_change_node_id(char *node_id, size_t len) +{ + if(esp_rmaker_priv_data) { + char *new_node_id = strndup(node_id, len); + if (!new_node_id) { + ESP_LOGE(TAG, "Failed to allocate %lu bytes for new node_id.", (unsigned long) len); + return ESP_ERR_NO_MEM; + } + if (esp_rmaker_priv_data->node_id) { + free(esp_rmaker_priv_data->node_id); + } + esp_rmaker_priv_data->node_id = new_node_id; + _esp_rmaker_node_t *node = (_esp_rmaker_node_t *)esp_rmaker_get_node(); + node->node_id = new_node_id; + ESP_LOGI(TAG, "New Node ID ----- %s", new_node_id); + return ESP_OK; + } + return ESP_ERR_INVALID_STATE; +} + + +/* Event handler for catching system events */ +static void esp_rmaker_event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ +#if defined(CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { +#ifdef CONFIG_ESP_RMAKER_ASSISTED_CLAIM + if (esp_rmaker_priv_data->claim_data) { + ESP_LOGE(TAG, "Node connected to Wi-Fi without Assisted claiming. Cannot proceed to MQTT connection."); + ESP_LOGE(TAG, "Please update your phone apps and repeat Wi-Fi provisioning with BLE transport."); + } +#endif + if (rmaker_core_event_group) { + /* Signal rmaker thread to continue execution */ + xEventGroupSetBits(rmaker_core_event_group, WIFI_CONNECTED_EVENT); + } + } else +#elif defined(CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD) /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + if (event_base == OPENTHREAD_EVENT && event_id == OPENTHREAD_EVENT_SET_DNS_SERVER) { +#ifdef CONFIG_ESP_RMAKER_ASSISTED_CLAIM + if (esp_rmaker_priv_data->claim_data) { + ESP_LOGE(TAG, "Node connected to Thread network without Assisted claiming. Cannot proceed to MQTT connection."); + ESP_LOGE(TAG, "Please update your phone apps and repeat Wi-Fi provisioning with BLE transport."); + } +#endif + if (rmaker_core_event_group) { + /* Signal rmaker thread to continue execution */ + xEventGroupSetBits(rmaker_core_event_group, THREAD_SET_DNS_SEVER_EVENT); + } + } else +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ + if (event_base == RMAKER_EVENT && + (event_id == RMAKER_EVENT_USER_NODE_MAPPING_DONE || + event_id == RMAKER_EVENT_USER_NODE_MAPPING_RESET)) { + esp_event_handler_unregister(RMAKER_EVENT, event_id, &esp_rmaker_event_handler); + esp_rmaker_params_mqtt_init(); + } else if (event_base == RMAKER_COMMON_EVENT && event_id == RMAKER_MQTT_EVENT_CONNECTED) { + if (rmaker_core_event_group) { + /* Signal rmaker thread to continue execution */ + xEventGroupSetBits(rmaker_core_event_group, MQTT_CONNECTED_EVENT); + } + } +} + +static esp_err_t esp_rmaker_deinit_priv_data(esp_rmaker_priv_data_t *rmaker_priv_data) +{ + if (!rmaker_priv_data) { + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_work_queue_deinit(); +#ifndef CONFIG_ESP_RMAKER_DISABLE_USER_MAPPING_PROV + esp_rmaker_user_mapping_prov_deinit(); +#endif +#ifdef ESP_RMAKER_CLAIM_ENABLED + if (rmaker_priv_data->claim_data) { + esp_rmaker_claim_data_free(rmaker_priv_data->claim_data); + } +#endif + if (rmaker_priv_data->mqtt_conn_params) { + esp_rmaker_clean_mqtt_conn_params(rmaker_priv_data->mqtt_conn_params); + free(rmaker_priv_data->mqtt_conn_params); + } + if (rmaker_priv_data->node_id) { + free(rmaker_priv_data->node_id); + } + free(rmaker_priv_data); + return ESP_OK; +} + +esp_err_t esp_rmaker_node_deinit(const esp_rmaker_node_t *node) +{ + if (!esp_rmaker_priv_data) { + ESP_LOGE(TAG, "ESP RainMaker already de-initialized."); + return ESP_ERR_INVALID_ARG; + } + + if (esp_rmaker_priv_data->state != ESP_RMAKER_STATE_INIT_DONE) { + ESP_LOGE(TAG, "ESP RainMaker is still running. Please stop it first."); + return ESP_ERR_INVALID_STATE; + } + esp_rmaker_node_delete(node); + esp_rmaker_priv_data->node = NULL; + esp_rmaker_deinit_priv_data(esp_rmaker_priv_data); + esp_rmaker_priv_data = NULL; + return ESP_OK; +} + +char *esp_rmaker_get_node_id(void) +{ + if (esp_rmaker_priv_data) { + return esp_rmaker_priv_data->node_id; + } + return NULL; +} + +static esp_err_t esp_rmaker_report_node_config_and_state() +{ + if (esp_rmaker_report_node_config() != ESP_OK) { + ESP_LOGE(TAG, "Report node config failed."); + return ESP_FAIL; + } + if (esp_rmaker_user_node_mapping_get_state() == ESP_RMAKER_USER_MAPPING_DONE) { + if (esp_rmaker_report_node_state() != ESP_OK) { + ESP_LOGE(TAG, "Report node state failed."); + return ESP_FAIL; + } + } + return ESP_OK; +} + +static void __esp_rmaker_report_node_config_and_state(void *data) +{ + esp_rmaker_report_node_config_and_state(); +} + +esp_err_t esp_rmaker_report_node_details() +{ + return esp_rmaker_work_queue_add_task(__esp_rmaker_report_node_config_and_state, NULL); +} + + +static void esp_rmaker_task(void *data) +{ + ESP_RMAKER_CHECK_HANDLE(); + esp_rmaker_priv_data->state = ESP_RMAKER_STATE_STARTING; + esp_err_t err = ESP_FAIL; +#if defined(CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI) + wifi_ap_record_t ap_info; +#elif defined(CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD) + ip6_addr_t nat64_prefix; +#endif + esp_rmaker_priv_data->mqtt_connected = false; + esp_event_handler_register(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_CONNECTED, &esp_rmaker_mqtt_event_handler, NULL); + esp_event_handler_register(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_DISCONNECTED, &esp_rmaker_mqtt_event_handler, NULL); + rmaker_core_event_group = xEventGroupCreate(); + if (!rmaker_core_event_group) { + ESP_LOGE(TAG, "Failed to create event group. Aborting"); + goto rmaker_end; + } +#if defined(CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI) + err = esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &esp_rmaker_event_handler, esp_rmaker_priv_data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register event handler. Error: %d. Aborting", err); + goto rmaker_end; + } +#elif defined(CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD) + err = esp_event_handler_register(OPENTHREAD_EVENT, OPENTHREAD_EVENT_SET_DNS_SERVER, &esp_rmaker_event_handler, esp_rmaker_priv_data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register event handler. Error: %d. Aborting", err); + goto rmaker_end; + } +#endif + err = esp_event_handler_register(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_CONNECTED, &esp_rmaker_event_handler, esp_rmaker_priv_data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register event handler. Error: %d. Aborting", err); + goto rmaker_end; + } + /* Assisted claiming needs to be done before Wi-Fi connection */ +#ifdef CONFIG_ESP_RMAKER_ASSISTED_CLAIM + if (esp_rmaker_priv_data->need_claim) { +#if defined(CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI) + if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { + ESP_LOGE(TAG, "Node connected to Wi-Fi without Assisted claiming. Cannot proceed to MQTT connection."); + ESP_LOGE(TAG, "Please update your phone apps and repeat Wi-Fi provisioning with BLE transport."); + esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &esp_rmaker_event_handler); +#elif defined(CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD) + if (esp_openthread_get_nat64_prefix(&nat64_prefix) == ESP_OK) { + ESP_LOGE(TAG, "Node connected to Thread without Assisted claiming. Cannot proceed to MQTT connection."); + ESP_LOGE(TAG, "Please update your phone apps and repeat Thread provisioning with BLE transport."); + esp_event_handler_unregister(OPENTHREAD_EVENT, OPENTHREAD_EVENT_SET_DNS_SERVER, &esp_rmaker_event_handler); +#endif + err = ESP_FAIL; + goto rmaker_end; + } + esp_rmaker_post_event(RMAKER_EVENT_CLAIM_STARTED, NULL, 0); + err = esp_rmaker_assisted_claim_perform(esp_rmaker_priv_data->claim_data); + if (err != ESP_OK) { + esp_rmaker_post_event(RMAKER_EVENT_CLAIM_FAILED, NULL, 0); + ESP_LOGE(TAG, "esp_rmaker_self_claim_perform() returned %d. Aborting", err); +#if defined(CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI) + esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &esp_rmaker_event_handler); +#elif defined(CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD) + esp_event_handler_unregister(OPENTHREAD_EVENT, OPENTHREAD_EVENT_SET_DNS_SERVER, &esp_rmaker_event_handler); +#endif + goto rmaker_end; + } + esp_rmaker_priv_data->claim_data = NULL; + esp_rmaker_post_event(RMAKER_EVENT_CLAIM_SUCCESSFUL, NULL, 0); + } +#endif /* CONFIG_ESP_RMAKER_ASSISTED_CLAIM */ +#if defined(CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI) + /* Check if already connected to Wi-Fi */ + if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK) { + /* Wait for Wi-Fi connection */ + xEventGroupWaitBits(rmaker_core_event_group, WIFI_CONNECTED_EVENT, false, true, portMAX_DELAY); + } + esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &esp_rmaker_event_handler); +#elif defined(CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD) /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + esp_openthread_lock_acquire(portMAX_DELAY); + err = esp_openthread_get_nat64_prefix(&nat64_prefix); + esp_openthread_lock_release(); + /* Check if already get nat64 prefix */ + if (err != ESP_OK) { + /* Wait for Thread connection */ + xEventGroupWaitBits(rmaker_core_event_group, THREAD_SET_DNS_SEVER_EVENT, false, true, portMAX_DELAY); + err = ESP_OK; + } + esp_event_handler_unregister(OPENTHREAD_EVENT, OPENTHREAD_EVENT_SET_DNS_SERVER, &esp_rmaker_event_handler); +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ + + if (esp_rmaker_priv_data->enable_time_sync) { +#ifdef CONFIG_MBEDTLS_HAVE_TIME_DATE + esp_rmaker_time_wait_for_sync(portMAX_DELAY); +#endif + } + /* Self claiming can be done only after Wi-Fi connection */ +#ifdef CONFIG_ESP_RMAKER_SELF_CLAIM + if (esp_rmaker_priv_data->need_claim) { + esp_rmaker_post_event(RMAKER_EVENT_CLAIM_STARTED, NULL, 0); + err = esp_rmaker_self_claim_perform(esp_rmaker_priv_data->claim_data); + if (err != ESP_OK) { + esp_rmaker_post_event(RMAKER_EVENT_CLAIM_FAILED, NULL, 0); + ESP_LOGE(TAG, "esp_rmaker_self_claim_perform() returned %d. Aborting", err); + goto rmaker_end; + } + esp_rmaker_priv_data->claim_data = NULL; + esp_rmaker_post_event(RMAKER_EVENT_CLAIM_SUCCESSFUL, NULL, 0); + } +#endif +#ifdef ESP_RMAKER_CLAIM_ENABLED + if (esp_rmaker_priv_data->need_claim) { + esp_rmaker_priv_data->mqtt_conn_params = esp_rmaker_get_mqtt_conn_params(); + if (!esp_rmaker_priv_data->mqtt_conn_params) { + ESP_LOGE(TAG, "Failed to initialise MQTT Config after claiming. Aborting"); + err = ESP_FAIL; + goto rmaker_end; + } + err = esp_rmaker_mqtt_init(esp_rmaker_priv_data->mqtt_conn_params); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_rmaker_mqtt_init() returned %d. Aborting", err); + goto rmaker_end; + } + esp_rmaker_priv_data->need_claim = false; + } +#endif /* ESP_RMAKER_CLAIM_ENABLED */ +#ifdef CONFIG_ESP_RMAKER_CMD_RESP_ENABLE + err = esp_rmaker_cmd_response_enable(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to enable Command - Response module. Aborting!!!"); + goto rmaker_end; + } + esp_rmaker_node_add_attribute(esp_rmaker_get_node(), "cmd-resp", "1"); +#else + ESP_LOGW(TAG, "Command-Response Module not enabled. Set CONFIG_ESP_RMAKER_CMD_RESP_ENABLE=y to use it."); +#endif /* !CONFIG_ESP_RMAKER_CMD_RESP_ENABLE */ +#ifdef CONFIG_ESP_RMAKER_LOCAL_CTRL_ENABLE + err = esp_rmaker_local_ctrl_enable(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start local control service. Aborting!!!"); + goto rmaker_end; + } +#endif /* CONFIG_ESP_RMAKER_LOCAL_CTRL_ENABLE */ + err = esp_rmaker_mqtt_connect(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_rmaker_mqtt_connect() returned %d. Aborting", err); + goto rmaker_end; + } + ESP_LOGI(TAG, "Waiting for MQTT connection"); + xEventGroupWaitBits(rmaker_core_event_group, MQTT_CONNECTED_EVENT, false, true, portMAX_DELAY); + esp_event_handler_unregister(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_CONNECTED, &esp_rmaker_event_handler); + esp_rmaker_priv_data->state = ESP_RMAKER_STATE_STARTED; + err = esp_rmaker_report_node_config(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Aborting!!!"); + goto rmaker_end; + } + if (esp_rmaker_user_node_mapping_get_state() == ESP_RMAKER_USER_MAPPING_DONE) { + err = esp_rmaker_params_mqtt_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Aborting!!!"); + goto rmaker_end; + } + } else { + /* If network is connected without even starting the user-node mapping workflow, + * it could mean that some incorrect app was used to provision the device. Even + * if the older user would not be able to update the params, it would be better + * to completely reset the user permissions by sending a dummy user node mapping + * request, so that the earlier user won't even see the connectivity and other + * status. + */ + if (esp_rmaker_user_node_mapping_get_state() != ESP_RMAKER_USER_MAPPING_STARTED) { + esp_rmaker_reset_user_node_mapping(); + /* Wait for user reset to finish. */ + err = esp_event_handler_register(RMAKER_EVENT, RMAKER_EVENT_USER_NODE_MAPPING_RESET, + &esp_rmaker_event_handler, NULL); + } else { + /* Wait for User Node mapping to finish. */ + err = esp_event_handler_register(RMAKER_EVENT, RMAKER_EVENT_USER_NODE_MAPPING_DONE, + &esp_rmaker_event_handler, NULL); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "Aborting!!!"); + goto rmaker_end; + } + ESP_LOGI(TAG, "Waiting for User Node Association."); + } + err = ESP_OK; + +rmaker_end: + if (rmaker_core_event_group) { + vEventGroupDelete(rmaker_core_event_group); + } + rmaker_core_event_group = NULL; + if (err == ESP_OK) { + return; + } + if (esp_rmaker_priv_data->mqtt_connected) { + esp_rmaker_mqtt_disconnect(); + } + esp_rmaker_priv_data->state = ESP_RMAKER_STATE_INIT_DONE; +} + + +static esp_err_t esp_rmaker_mqtt_conn_params_init(esp_rmaker_priv_data_t *rmaker_priv_data, bool use_claiming) +{ + rmaker_priv_data->mqtt_conn_params = esp_rmaker_get_mqtt_conn_params(); + if (rmaker_priv_data->mqtt_conn_params) { + return ESP_OK; + } +#ifdef ESP_RMAKER_CLAIM_ENABLED + if (use_claiming) { +#ifdef CONFIG_ESP_RMAKER_SELF_CLAIM + rmaker_priv_data->claim_data = esp_rmaker_self_claim_init(); +#endif +#ifdef CONFIG_ESP_RMAKER_ASSISTED_CLAIM + rmaker_priv_data->claim_data = esp_rmaker_assisted_claim_init(); +#endif + if (!rmaker_priv_data->claim_data) { + ESP_LOGE(TAG, "Failed to initialise Claiming."); + return ESP_FAIL; + } else { + rmaker_priv_data->need_claim = true; + return ESP_OK; + } + } +#endif /* ESP_RMAKER_CLAIM_ENABLED */ + return ESP_FAIL; +} +/* Initialize ESP RainMaker */ +static esp_err_t esp_rmaker_init(const esp_rmaker_config_t *config, bool use_claiming) +{ + if (esp_rmaker_priv_data) { + ESP_LOGE(TAG, "ESP RainMaker already initialised"); + return ESP_ERR_INVALID_STATE; + } + if (!config) { + ESP_LOGE(TAG, "RainMaker config missing. Cannot initialise"); + return ESP_ERR_INVALID_ARG; + } + if (esp_rmaker_factory_init() != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialise storage"); + return ESP_FAIL; + } + esp_rmaker_priv_data = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_priv_data_t)); + if (!esp_rmaker_priv_data) { + ESP_LOGE(TAG, "Failed to allocate memory"); + return ESP_ERR_NO_MEM; + } + + esp_rmaker_priv_data->node_id = esp_rmaker_populate_node_id(use_claiming); + if (!esp_rmaker_priv_data->node_id) { + esp_rmaker_deinit_priv_data(esp_rmaker_priv_data); + esp_rmaker_priv_data = NULL; + ESP_LOGE(TAG, "Failed to initialise Node Id. Please perform \"claiming\" using RainMaker CLI."); + return ESP_ERR_NO_MEM; + } + + if (esp_rmaker_work_queue_init() != ESP_OK) { + esp_rmaker_deinit_priv_data(esp_rmaker_priv_data); + esp_rmaker_priv_data = NULL; + ESP_LOGE(TAG, "ESP RainMaker Queue Creation Failed"); + return ESP_ERR_NO_MEM; + } +#ifndef CONFIG_ESP_RMAKER_DISABLE_USER_MAPPING_PROV + if (esp_rmaker_user_mapping_prov_init()) { + esp_rmaker_deinit_priv_data(esp_rmaker_priv_data); + esp_rmaker_priv_data = NULL; + ESP_LOGE(TAG, "Could not initialise User-Node mapping."); + return ESP_FAIL; + } +#endif /* !CONFIG_ESP_RMAKER_DISABLE_USER_MAPPING_PROV */ + if (esp_rmaker_mqtt_conn_params_init(esp_rmaker_priv_data, use_claiming) != ESP_OK) { + esp_rmaker_deinit_priv_data(esp_rmaker_priv_data); + esp_rmaker_priv_data = NULL; + ESP_LOGE(TAG, "Failed to initialise MQTT Params. Please perform \"claiming\" using RainMaker CLI."); + return ESP_FAIL; + } else { +#ifdef ESP_RMAKER_CLAIM_ENABLED + if (!esp_rmaker_priv_data->need_claim) +#endif /* ESP_RMAKER_CLAIM_ENABLED */ + { + if (esp_rmaker_mqtt_init(esp_rmaker_priv_data->mqtt_conn_params) != ESP_OK) { + esp_rmaker_deinit_priv_data(esp_rmaker_priv_data); + esp_rmaker_priv_data = NULL; + ESP_LOGE(TAG, "Failed to initialise MQTT"); + return ESP_FAIL; + } + } + } + esp_rmaker_user_node_mapping_init(); +#ifdef CONFIG_ESP_RMAKER_LOCAL_CTRL_ENABLE + esp_rmaker_init_local_ctrl_service(); +#endif + esp_rmaker_priv_data->enable_time_sync = config->enable_time_sync; + esp_rmaker_post_event(RMAKER_EVENT_INIT_DONE, NULL, 0); + esp_rmaker_priv_data->state = ESP_RMAKER_STATE_INIT_DONE; + + /* Adding the RainMaker Task to the queue so that it is will be the first function + * to be executed when the Work Queue task begins. + */ + esp_rmaker_work_queue_add_task(esp_rmaker_task, NULL); + return ESP_OK; +} + +static esp_err_t esp_rmaker_register_node(const esp_rmaker_node_t *node) +{ + ESP_RMAKER_CHECK_HANDLE(ESP_ERR_INVALID_STATE); + if (esp_rmaker_priv_data->node) { + ESP_LOGE(TAG, "A node has already been registered. Cannot register another."); + return ESP_ERR_INVALID_STATE; + } + if (!node) { + ESP_LOGE(TAG, "Node handle cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_priv_data->node = node; + return ESP_OK; +} + +esp_rmaker_node_t *esp_rmaker_node_init(const esp_rmaker_config_t *config, const char *name, const char *type) +{ + esp_err_t err = esp_rmaker_init(config, true); + if (err != ESP_OK) { + return NULL; + } + esp_rmaker_node_t *node = esp_rmaker_node_create(name, type); + if (!node) { + ESP_LOGE(TAG, "Failed to create node"); + return NULL; + } + err = esp_rmaker_register_node(node); + if (err != ESP_OK) { + free(node); + return NULL; + } + return node; +} + +const esp_rmaker_node_t *esp_rmaker_get_node() +{ + ESP_RMAKER_CHECK_HANDLE(NULL); + return esp_rmaker_priv_data->node; +} + +/* Start the ESP RainMaker Core Task */ +esp_err_t esp_rmaker_start(void) +{ + ESP_RMAKER_CHECK_HANDLE(ESP_ERR_INVALID_STATE); + if (esp_rmaker_priv_data->enable_time_sync) { + esp_rmaker_time_sync_init(NULL); + } + ESP_LOGI(TAG, "Starting RainMaker Work Queue task"); + if (esp_rmaker_work_queue_start() != ESP_OK) { + ESP_LOGE(TAG, "Couldn't create RainMaker Work Queue task"); + return ESP_FAIL; + } + ESP_ERROR_CHECK(esp_event_handler_register(RMAKER_COMMON_EVENT, ESP_EVENT_ANY_ID, &reset_event_handler, NULL)); + return ESP_OK; +} + +esp_err_t esp_rmaker_stop() +{ + ESP_RMAKER_CHECK_HANDLE(ESP_ERR_INVALID_STATE); + esp_rmaker_priv_data->state = ESP_RMAKER_STATE_STOP_REQUESTED; + return ESP_OK; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_device.c b/components/esp_rainmaker/src/core/esp_rmaker_device.c new file mode 100644 index 0000000..0c02922 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_device.c @@ -0,0 +1,379 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include + +#include +#include +#include + +#include "esp_rmaker_internal.h" + +static const char *TAG = "esp_rmaker_device"; + +esp_err_t esp_rmaker_device_delete(const esp_rmaker_device_t *device) +{ + _esp_rmaker_device_t *_device = (_esp_rmaker_device_t *)device; + if (_device) { + if (_device->parent) { + ESP_LOGE(TAG, "Cannot delete device as it is part of a node. Remove it from the node first."); + return ESP_ERR_INVALID_STATE; + } + esp_rmaker_attr_t *attr = _device->attributes; + while (attr) { + esp_rmaker_attr_t *next_attr = attr->next; + esp_rmaker_attribute_delete(attr); + attr = next_attr; + } + _esp_rmaker_param_t *param = _device->params; + while (param) { + _esp_rmaker_param_t *next_param = param->next; + esp_rmaker_param_delete((esp_rmaker_param_t *)param); + param = next_param; + } + if (_device->subtype) { + free(_device->subtype); + } + if (_device->model) { + free(_device->model); + } + if (_device->name) { + free(_device->name); + } + if (_device->type) { + free(_device->type); + } + free(_device); + return ESP_OK; + } + return ESP_ERR_INVALID_ARG; +} + +static esp_err_t esp_rmaker_default_bulk_write_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_write_req_t write_req[], + uint8_t count, void *priv_data, esp_rmaker_write_ctx_t *ctx) +{ + _esp_rmaker_device_t *_device = (_esp_rmaker_device_t *)device; + _esp_rmaker_param_t *param; + if (_device) { + for (int i = 0; i < count; i++) { + param = (_esp_rmaker_param_t *)(write_req[i].param); + if (param->type && (strcmp(param->type, ESP_RMAKER_PARAM_NAME) == 0)) { +#ifndef CONFIG_RMAKER_NAME_PARAM_CB + esp_rmaker_param_update(write_req[i].param, write_req[i].val); + continue; +#else + if (!_device->write_cb) { + esp_rmaker_param_update(write_req[i].param, write_req[i].val); + continue; + } +#endif + } + if (_device->write_cb) { + if (_device->write_cb(device, write_req[i].param, write_req[i].val, priv_data, ctx) != ESP_OK) { + ESP_LOGE(TAG, "Remote update to param %s - %s failed", _device->name, ((_esp_rmaker_param_t *)(write_req[i].param))->name); + } + } else { + ESP_LOGW(TAG, "No write callback for device %s", _device->name); + } + } + } + return ESP_OK; +} + +static esp_rmaker_device_t *__esp_rmaker_device_create(const char *name, const char *type, void *priv, bool is_service) +{ + if (!name) { + ESP_LOGE(TAG, "%s name is mandatory", is_service ? "Service":"Device"); + return NULL; + } + _esp_rmaker_device_t *_device = MEM_CALLOC_EXTRAM(1, sizeof(_esp_rmaker_device_t)); + if (!_device) { + ESP_LOGE(TAG, "Failed to allocate memory for %s %s", is_service ? "Service":"Device", name); + return NULL; + } + _device->name = strdup(name); + if (!_device->name) { + ESP_LOGE(TAG, "Failed to allocate memory for name for %s %s", is_service ? "Service":"Device", name); + goto device_create_err; + } + if (type) { + _device->type = strdup(type); + if (!_device->type) { + ESP_LOGE(TAG, "Failed to allocate memory for type for %s %s", is_service ? "Service":"Device", name); + goto device_create_err; + } + } + _device->priv_data = priv; + _device->is_service = is_service; + /* Adding a default bulk write callback for backward compatibility with application code using single param write callback */ + _device->bulk_write_cb = esp_rmaker_default_bulk_write_cb; + + return (esp_rmaker_device_t *)_device; + +device_create_err: + esp_rmaker_device_delete((esp_rmaker_device_t *)_device); + return NULL; +} + +esp_rmaker_device_t *esp_rmaker_device_create(const char *name, const char *type, void *priv) +{ + return __esp_rmaker_device_create(name, type, priv, false); +} +esp_rmaker_device_t *esp_rmaker_service_create(const char *name, const char *type, void *priv) +{ + return __esp_rmaker_device_create(name, type, priv, true); +} + +esp_err_t esp_rmaker_device_add_param(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param) +{ + if (!device || !param) { + ESP_LOGE(TAG, "Device or Param handle cannot be NULL"); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_device_t *_device = (_esp_rmaker_device_t *)device; + _esp_rmaker_param_t *_new_param = (_esp_rmaker_param_t *)param; + + _esp_rmaker_param_t *_param = _device->params; + while(_param) { + if (strcmp(_param->name, _new_param->name) == 0) { + ESP_LOGE(TAG, "Parameter with name %s already exists in Device %s", _new_param->name, _device->name); + return ESP_ERR_INVALID_ARG; + } + if (_param->next) { + _param = _param->next; + } else { + break; + } + } + _new_param->parent = _device; + if (_param) { + _param->next = _new_param; + } else { + _device->params = _new_param; + } + _device->param_count++; + /* We check the stored value here, and not during param creation, because a parameter + * in itself isn't unique. However, it is unique within a given device and hence can + * be uniquely represented in storage only when added to a device. + */ + esp_rmaker_param_val_t stored_val; + stored_val.type = _new_param->val.type; + if (_new_param->prop_flags & PROP_FLAG_PERSIST) { + if (esp_rmaker_param_get_stored_value(_new_param, &stored_val) == ESP_OK) { + if ((_new_param->val.type == RMAKER_VAL_TYPE_STRING) || (_new_param->val.type == RMAKER_VAL_TYPE_OBJECT) + || (_new_param->val.type == RMAKER_VAL_TYPE_ARRAY)) { + if (_new_param->val.val.s) { + free(_new_param->val.val.s); + } + } + _new_param->val = stored_val; + /* The device callback should be invoked once with the stored value, so + * that applications can do initialisations as required. + */ + if (_device->bulk_write_cb) { + /* However, the callback should be invoked, only if the parameter is not + * of type ESP_RMAKER_PARAM_NAME, as it has special handling internally. + */ + if (!(_new_param->type && strcmp(_new_param->type, ESP_RMAKER_PARAM_NAME) == 0)) { + esp_rmaker_write_ctx_t ctx = { + .src = ESP_RMAKER_REQ_SRC_INIT, + }; + esp_rmaker_param_write_req_t write_req = { + .param = (esp_rmaker_param_t *)param, + .val = stored_val, + }; + _device->bulk_write_cb(device, &write_req, 1, _device->priv_data, &ctx); + } + } + } else { + esp_rmaker_param_store_value(_new_param); + } + } + ESP_LOGD(TAG, "Param %s added in %s", _new_param->name, _device->name); + return ESP_OK; +} + +/* Add a new Device Attribute */ +esp_err_t esp_rmaker_device_add_attribute(const esp_rmaker_device_t *device, const char *attr_name, const char *val) +{ + if (!device || !attr_name || !val) { + ESP_LOGE(TAG, "Device handle, attribute name or value cannot be NULL"); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_device_t *_device = ( _esp_rmaker_device_t *)device; + esp_rmaker_attr_t *attr = _device->attributes; + while(attr) { + if (strcmp(attr_name, attr->name) == 0) { + ESP_LOGE(TAG, "Attribute with name %s already exists in Device %s", attr_name, _device->name); + return ESP_ERR_INVALID_ARG; + } + if (attr->next) { + attr = attr->next; + } else { + break; + } + } + esp_rmaker_attr_t *new_attr = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_attr_t)); + if (!new_attr) { + ESP_LOGE(TAG, "Failed to allocate memory for device attribute"); + return ESP_ERR_NO_MEM; + } + new_attr->name = strdup(attr_name); + new_attr->value = strdup(val); + if (!new_attr->name || !new_attr->value) { + ESP_LOGE(TAG, "Failed to allocate memory for device attribute name or value"); + esp_rmaker_attribute_delete(new_attr); + return ESP_ERR_NO_MEM; + } + if (attr) { + attr->next = new_attr; + } else { + _device->attributes = new_attr; + } + ESP_LOGD(TAG, "Device attribute %s.%s added", _device->name, attr_name); + return ESP_OK; +} + +/* Add a device subtype */ +esp_err_t esp_rmaker_device_add_subtype(const esp_rmaker_device_t *device, const char *subtype) +{ + if (!device || !subtype) { + ESP_LOGE(TAG, "Device handle or subtype cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_device_t *_device = (_esp_rmaker_device_t *)device; + if (_device->subtype) { + free(_device->subtype); + } + if ((_device->subtype = strdup(subtype)) != NULL ){ + return ESP_OK; + } else { + ESP_LOGE(TAG, "Failed to allocate memory for device subtype"); + return ESP_ERR_NO_MEM; + } +} + +/* Add a device model */ +esp_err_t esp_rmaker_device_add_model(const esp_rmaker_device_t *device, const char *model) +{ + if (!device || !model) { + ESP_LOGE(TAG, "Device handle or model cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_device_t *_device = (_esp_rmaker_device_t *)device; + if (_device->model) { + free(_device->model); + } + if ((_device->model = strdup(model)) != NULL ){ + return ESP_OK; + } else { + ESP_LOGE(TAG, "Failed to allocate memory for device model"); + return ESP_ERR_NO_MEM; + } +} + +esp_err_t esp_rmaker_device_assign_primary_param(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param) +{ + if (!device || !param) { + ESP_LOGE(TAG,"Device or Param handle cannot be NULL"); + return ESP_ERR_INVALID_ARG; + } + ((_esp_rmaker_device_t *)device)->primary = (_esp_rmaker_param_t *)param; + return ESP_OK; +} + +esp_err_t esp_rmaker_device_add_cb(const esp_rmaker_device_t *device, esp_rmaker_device_write_cb_t write_cb, esp_rmaker_device_read_cb_t read_cb) +{ + if (!device) { + ESP_LOGE(TAG, "Device handle cannot be NULL"); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_device_t *_device = (_esp_rmaker_device_t *)device; + _device->write_cb = write_cb; + _device->read_cb = read_cb; + return ESP_OK; +} + +esp_err_t esp_rmaker_device_add_bulk_cb(const esp_rmaker_device_t *device, esp_rmaker_device_bulk_write_cb_t write_cb, + esp_rmaker_device_bulk_read_cb_t read_cb) +{ + if (!device) { + ESP_LOGE(TAG, "Device handle cannot be NULL"); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_device_t *_device = (_esp_rmaker_device_t *)device; + _device->bulk_write_cb = write_cb; + _device->bulk_read_cb = read_cb; + return ESP_OK; +} + +char *esp_rmaker_device_get_name(const esp_rmaker_device_t *device) +{ + if (!device) { + ESP_LOGE(TAG, "Device handle cannot be NULL."); + return NULL; + } + return ((_esp_rmaker_device_t *)device)->name; +} + +void *esp_rmaker_device_get_priv_data(const esp_rmaker_device_t *device) +{ + if (!device) { + ESP_LOGE(TAG, "Device handle cannot be NULL."); + return NULL; + } + return ((_esp_rmaker_device_t *)device)->priv_data; +} + +char *esp_rmaker_device_get_type(const esp_rmaker_device_t *device) +{ + if (!device) { + ESP_LOGE(TAG, "Device handle cannot be NULL."); + return NULL; + } + return ((_esp_rmaker_device_t *)device)->type; +} + +esp_rmaker_param_t *esp_rmaker_device_get_param_by_type(const esp_rmaker_device_t *device, const char *param_type) +{ + if (!device || !param_type) { + ESP_LOGE(TAG, "Device handle or param type cannot be NULL"); + return NULL; + } + _esp_rmaker_param_t *param = ((_esp_rmaker_device_t *)device)->params; + while(param) { + if (strcmp(param->type, param_type) == 0) { + break; + } + param = param->next; + } + return (esp_rmaker_param_t *)param; +} + +esp_rmaker_param_t *esp_rmaker_device_get_param_by_name(const esp_rmaker_device_t *device, const char *param_name) +{ + if (!device || !param_name) { + ESP_LOGE(TAG, "Device handle or param name cannot be NULL"); + return NULL; + } + _esp_rmaker_param_t *param = ((_esp_rmaker_device_t *)device)->params; + while(param) { + if (strcmp(param->name, param_name) == 0) { + break; + } + param = param->next; + } + return (esp_rmaker_param_t *)param; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_internal.h b/components/esp_rainmaker/src/core/esp_rmaker_internal.h new file mode 100644 index 0000000..ff42c2c --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_internal.h @@ -0,0 +1,129 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#include +#include +#include +#include +#include + +#define RMAKER_PARAM_FLAG_VALUE_CHANGE (1 << 0) +#define RMAKER_PARAM_FLAG_VALUE_NOTIFY (1 << 1) +#define ESP_RMAKER_NVS_PART_NAME "nvs" + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) && defined(CONFIG_ESP_RMAKER_USING_NETWORK_PROV) +#define RMAKER_USING_NETWORK_PROV 1 +#else +#define RMAKER_USING_NETWORK_PROV 0 +#endif + +typedef enum { + ESP_RMAKER_STATE_DEINIT = 0, + ESP_RMAKER_STATE_INIT_DONE, + ESP_RMAKER_STATE_STARTING, + ESP_RMAKER_STATE_STARTED, + ESP_RMAKER_STATE_STOP_REQUESTED, +} esp_rmaker_state_t; + +typedef struct { + esp_rmaker_param_val_t min; + esp_rmaker_param_val_t max; + esp_rmaker_param_val_t step; +} esp_rmaker_param_bounds_t; + +typedef struct { + uint8_t str_list_cnt; + const char **str_list; +} esp_rmaker_param_valid_str_list_t; + +struct esp_rmaker_param { + char *name; + char *type; + uint8_t flags; + uint8_t prop_flags; + char *ui_type; + esp_rmaker_param_val_t val; + esp_rmaker_param_bounds_t *bounds; + esp_rmaker_param_valid_str_list_t *valid_str_list; + struct esp_rmaker_device *parent; + struct esp_rmaker_param * next; +}; +typedef struct esp_rmaker_param _esp_rmaker_param_t; + +struct esp_rmaker_attr { + char *name; + char *value; + struct esp_rmaker_attr *next; +}; +typedef struct esp_rmaker_attr esp_rmaker_attr_t; + + +struct esp_rmaker_device { + char *name; + char *type; + char *subtype; + char *model; + uint8_t param_count; + esp_rmaker_device_write_cb_t write_cb; + esp_rmaker_device_read_cb_t read_cb; + esp_rmaker_device_bulk_write_cb_t bulk_write_cb; + esp_rmaker_device_bulk_read_cb_t bulk_read_cb; + void *priv_data; + bool is_service; + esp_rmaker_attr_t *attributes; + _esp_rmaker_param_t *params; + _esp_rmaker_param_t *primary; + const esp_rmaker_node_t *parent; + struct esp_rmaker_device *next; +}; +typedef struct esp_rmaker_device _esp_rmaker_device_t; + +typedef struct { + char *node_id; + esp_rmaker_node_info_t *info; + esp_rmaker_attr_t *attributes; + _esp_rmaker_device_t *devices; +} _esp_rmaker_node_t; + +esp_rmaker_node_t *esp_rmaker_node_create(const char *name, const char *type); +esp_err_t esp_rmaker_change_node_id(char *node_id, size_t len); +esp_err_t esp_rmaker_report_value(const esp_rmaker_param_val_t *val, char *key, json_gen_str_t *jptr); +esp_err_t esp_rmaker_report_data_type(esp_rmaker_val_type_t type, char *data_type_key, json_gen_str_t *jptr); +esp_err_t esp_rmaker_report_node_config(void); +esp_err_t esp_rmaker_report_node_state(void); +_esp_rmaker_device_t *esp_rmaker_node_get_first_device(const esp_rmaker_node_t *node); +esp_rmaker_attr_t *esp_rmaker_node_get_first_attribute(const esp_rmaker_node_t *node); +esp_err_t esp_rmaker_params_mqtt_init(void); +esp_err_t esp_rmaker_param_get_stored_value(_esp_rmaker_param_t *param, esp_rmaker_param_val_t *val); +esp_err_t esp_rmaker_param_store_value(_esp_rmaker_param_t *param); +esp_err_t esp_rmaker_node_delete(const esp_rmaker_node_t *node); +esp_err_t esp_rmaker_param_delete(const esp_rmaker_param_t *param); +esp_err_t esp_rmaker_attribute_delete(esp_rmaker_attr_t *attr); +char *esp_rmaker_get_node_config(void); +char *esp_rmaker_get_node_params(void); +esp_err_t esp_rmaker_handle_set_params(char *data, size_t data_len, esp_rmaker_req_src_t src); +esp_err_t esp_rmaker_user_mapping_prov_init(void); +esp_err_t esp_rmaker_user_mapping_prov_deinit(void); +esp_err_t esp_rmaker_user_node_mapping_init(void); +esp_err_t esp_rmaker_user_node_mapping_deinit(void); +esp_err_t esp_rmaker_reset_user_node_mapping(void); +esp_err_t esp_rmaker_init_local_ctrl_service(void); +esp_err_t esp_rmaker_start_local_ctrl_service(const char *serv_name); +static inline esp_err_t esp_rmaker_post_event(esp_rmaker_event_t event_id, void* data, size_t data_size) +{ + return esp_event_post(RMAKER_EVENT, event_id, data, data_size, portMAX_DELAY); +} +esp_rmaker_state_t esp_rmaker_get_state(void); +esp_err_t esp_rmaker_cmd_response_enable(void); diff --git a/components/esp_rainmaker/src/core/esp_rmaker_local_ctrl.c b/components/esp_rainmaker/src/core/esp_rmaker_local_ctrl.c new file mode 100644 index 0000000..f60a7cf --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_local_ctrl.c @@ -0,0 +1,650 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +#include +#endif +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD +#include +#include +#include +#include +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ +#include + +#include + +#if RMAKER_USING_NETWORK_PROV +#include +#else +#include +#endif + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) +// Features supported in 4.2 + +#define ESP_RMAKER_LOCAL_CTRL_SECURITY_TYPE CONFIG_ESP_RMAKER_LOCAL_CTRL_SECURITY + +#else + +#if CONFIG_ESP_RMAKER_LOCAL_CTRL_SECURITY != 0 +#warning "Local control security type is not supported in idf versions below 4.2. Using sec0 by default." +#endif +#define ESP_RMAKER_LOCAL_CTRL_SECURITY_TYPE 0 + +#endif /* !IDF4.2 */ + +static const char * TAG = "esp_rmaker_local"; + +/* Random Port number that will be used by the local control http instance + * for internal control communication. + */ +#define ESP_RMAKER_LOCAL_CTRL_DEVICE_NAME "Local Control" +#define ESP_RMAKER_LOCAL_CTRL_HTTP_CTRL_PORT 12312 +#define ESP_RMAKER_NVS_PART_NAME "nvs" +#define ESP_RMAKER_NVS_LOCAL_CTRL_NAMESPACE "local_ctrl" +#define ESP_RMAKER_NVS_LOCAL_CTRL_POP "pop" +#define ESP_RMAKER_POP_LEN 9 + +/* Custom allowed property types */ +enum property_types { + PROP_TYPE_NODE_CONFIG = 1, + PROP_TYPE_NODE_PARAMS, +}; + +/* Custom flags that can be set for a property */ +enum property_flags { + PROP_FLAG_READONLY = (1 << 0) +}; + +static bool g_local_ctrl_is_started = false; + +static char *g_serv_name; +static bool wait_for_provisioning; +/********* Handler functions for responding to control requests / commands *********/ + +static esp_err_t get_property_values(size_t props_count, + const esp_local_ctrl_prop_t props[], + esp_local_ctrl_prop_val_t prop_values[], + void *usr_ctx) +{ + esp_err_t ret = ESP_OK; + uint32_t i; + for (i = 0; i < props_count && ret == ESP_OK ; i++) { + ESP_LOGD(TAG, "(%"PRIu32") Reading property : %s", i, props[i].name); + switch (props[i].type) { + case PROP_TYPE_NODE_CONFIG: { + char *node_config = esp_rmaker_get_node_config(); + if (!node_config) { + ESP_LOGE(TAG, "Failed to allocate memory for %s", props[i].name); + ret = ESP_ERR_NO_MEM; + } else { + prop_values[i].size = strlen(node_config); + prop_values[i].data = node_config; + prop_values[i].free_fn = free; + } + break; + } + case PROP_TYPE_NODE_PARAMS: { + char *node_params = esp_rmaker_get_node_params(); + if (!node_params) { + ESP_LOGE(TAG, "Failed to allocate memory for %s", props[i].name); + ret = ESP_ERR_NO_MEM; + } else { + prop_values[i].size = strlen(node_params); + prop_values[i].data = node_params; + prop_values[i].free_fn = free; + } + break; + } + default: + break; + } + } + if (ret != ESP_OK) { + for (uint32_t j = 0; j <= i; j++) { + if (prop_values[j].free_fn) { + ESP_LOGI(TAG, "Freeing memory for %s", props[j].name); + prop_values[j].free_fn(prop_values[j].data); + prop_values[j].free_fn = NULL; + prop_values[j].data = NULL; + prop_values[j].size = 0; + } + } + } + return ret; +} + +static esp_err_t set_property_values(size_t props_count, + const esp_local_ctrl_prop_t props[], + const esp_local_ctrl_prop_val_t prop_values[], + void *usr_ctx) +{ + esp_err_t ret = ESP_OK; + uint32_t i; + /* First check if any of the properties are read-only properties. If found, just abort */ + for (i = 0; i < props_count; i++) { + /* Cannot set the value of a read-only property */ + if (props[i].flags & PROP_FLAG_READONLY) { + ESP_LOGE(TAG, "%s is read-only", props[i].name); + return ESP_ERR_INVALID_ARG; + } + } + for (i = 0; i < props_count && ret == ESP_OK; i++) { + switch (props[i].type) { + case PROP_TYPE_NODE_PARAMS: + ret = esp_rmaker_handle_set_params((char *)prop_values[i].data, + prop_values[i].size, ESP_RMAKER_REQ_SRC_LOCAL); + break; + default: + break; + } + } + return ret; +} + +static char *__esp_rmaker_local_ctrl_get_nvs(const char *key) +{ + char *val = NULL; + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, ESP_RMAKER_NVS_LOCAL_CTRL_NAMESPACE, NVS_READONLY, &handle); + if (err != ESP_OK) { + return NULL; + } + size_t len = 0; + if ((err = nvs_get_blob(handle, key, NULL, &len)) == ESP_OK) { + val = MEM_CALLOC_EXTRAM(1, len + 1); /* +1 for NULL termination */ + if (val) { + nvs_get_blob(handle, key, val, &len); + } + } + nvs_close(handle); + return val; + +} + +static esp_err_t __esp_rmaker_local_ctrl_set_nvs(const char *key, const char *val) +{ + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, ESP_RMAKER_NVS_LOCAL_CTRL_NAMESPACE, NVS_READWRITE, &handle); + if (err != ESP_OK) { + return err; + } + err = nvs_set_blob(handle, key, val, strlen(val)); + nvs_commit(handle); + nvs_close(handle); + return err; +} + +static char *esp_rmaker_local_ctrl_get_pop() +{ + char *pop = __esp_rmaker_local_ctrl_get_nvs(ESP_RMAKER_NVS_LOCAL_CTRL_POP); + if (pop) { + return pop; + } + + ESP_LOGI(TAG, "Couldn't find POP in NVS. Generating a new one."); + pop = (char *)MEM_CALLOC_EXTRAM(1, ESP_RMAKER_POP_LEN); + if (!pop) { + ESP_LOGE(TAG, "Couldn't allocate POP"); + return NULL; + } + uint8_t random_bytes[ESP_RMAKER_POP_LEN] = {0}; + esp_fill_random(&random_bytes, sizeof(random_bytes)); + snprintf(pop, ESP_RMAKER_POP_LEN, "%02x%02x%02x%02x", random_bytes[0], random_bytes[1], random_bytes[2], random_bytes[3]); + + __esp_rmaker_local_ctrl_set_nvs(ESP_RMAKER_NVS_LOCAL_CTRL_POP, pop); + return pop; +} + +static int esp_rmaker_local_ctrl_get_security_type() +{ + return ESP_RMAKER_LOCAL_CTRL_SECURITY_TYPE; +} + +static esp_err_t esp_rmaker_local_ctrl_service_enable(void) +{ + char *pop_str = esp_rmaker_local_ctrl_get_pop(); + if (!pop_str) { + ESP_LOGE(TAG, "Get POP failed"); + return ESP_FAIL; + } + int sec_ver = esp_rmaker_local_ctrl_get_security_type(); + + esp_rmaker_device_t *local_ctrl_service = esp_rmaker_create_local_control_service(ESP_RMAKER_LOCAL_CTRL_DEVICE_NAME, pop_str, sec_ver, NULL);; + if (!local_ctrl_service) { + ESP_LOGE(TAG, "Failed to create Local Control Service."); + free(pop_str); + return ESP_FAIL; + } + free(pop_str); + + esp_err_t err = esp_rmaker_node_add_device(esp_rmaker_get_node(), local_ctrl_service); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to add Local Control Service to Node. err=0x%x", err); + return err; + } +#ifndef CONFIG_ESP_RMAKER_LOCAL_CTRL_ENABLE + /* Report node details only if the local control is not enabled via config option, + * but instead via an API call. + */ + err = esp_rmaker_report_node_details(); +#endif + ESP_LOGI(TAG, "Local Control Service Enabled"); + return err; +} + +static esp_err_t esp_rmaker_local_ctrl_service_disable(void) +{ + const esp_rmaker_node_t *node = esp_rmaker_get_node(); + esp_rmaker_device_t *local_ctrl_service = esp_rmaker_node_get_device_by_name(node, ESP_RMAKER_LOCAL_CTRL_DEVICE_NAME); + esp_err_t err = esp_rmaker_node_remove_device(node, local_ctrl_service); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to remove Local Control Service from Node. err=0x%x", err); + return err; + } + err = esp_rmaker_device_delete(local_ctrl_service); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to delete Local Control Service. err=0x%x", err); + return err; + } +#ifndef CONFIG_ESP_RMAKER_LOCAL_CTRL_ENABLE + /* Report node details only if the local control is not enabled via config option, + * but instead via an API call. + */ + err = esp_rmaker_report_node_details(); +#endif + return ESP_OK; +} + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD +#define SRP_MAX_HOST_NAME_LEN 32 +static char srp_host_name[SRP_MAX_HOST_NAME_LEN + 1]; + +static esp_err_t srp_client_set_host(const char *host_name) +{ + if (!host_name || strlen(host_name) > 15) { + return ESP_ERR_INVALID_ARG; + } + // Avoid adding the same host name multiple times + if (strcmp(srp_host_name, host_name) != 0) { + strncpy(srp_host_name, host_name, SRP_MAX_HOST_NAME_LEN); + srp_host_name[strnlen(host_name, SRP_MAX_HOST_NAME_LEN)] = 0; + esp_openthread_lock_acquire(portMAX_DELAY); + otInstance *instance = esp_openthread_get_instance(); + if (otSrpClientSetHostName(instance, srp_host_name) != OT_ERROR_NONE) { + esp_openthread_lock_release(); + return ESP_FAIL; + } + if (otSrpClientEnableAutoHostAddress(instance) != OT_ERROR_NONE) { + esp_openthread_lock_release(); + return ESP_FAIL; + } + esp_openthread_lock_release(); + } + return ESP_OK; +} + +static esp_err_t srp_client_add_local_ctrl_service(const char *serv_name) +{ + static uint8_t rainmaker_node_id_txt_value[30]; + char *rmaker_node_id = esp_rmaker_get_node_id(); + if (rmaker_node_id == NULL || strlen(rmaker_node_id) > sizeof(rainmaker_node_id_txt_value)) { + return ESP_ERR_INVALID_ARG; + } + memcpy(rainmaker_node_id_txt_value, rmaker_node_id, strlen(rmaker_node_id)); + const static uint8_t text_values[3][23] = { + {'/', 'e', 's', 'p', '_', 'l', 'o', 'c', 'a', 'l', '_', 'c', 't', 'r', 'l', '/', 'v', 'e', 'r', 's', 'i', 'o', 'n'}, + {'/', 'e', 's', 'p', '_', 'l', 'o', 'c', 'a', 'l', '_', 'c', 't', 'r', 'l', '/', 's', 'e', 's', 's', 'i', 'o', 'n'}, + {'/', 'e', 's', 'p', '_', 'l', 'o', 'c', 'a', 'l', '_', 'c', 't', 'r', 'l', '/', 'c', 'o', 'n', 't', 'r', 'o', 'l'}}; + static otDnsTxtEntry txt_entries[4] = { + { + .mKey = "version_endpoint", + .mValue = text_values[0], + .mValueLength = 23, + }, + { + .mKey = "session_endpoint", + .mValue = text_values[1], + .mValueLength = 23, + }, + { + .mKey = "control_endpoint", + .mValue = text_values[2], + .mValueLength = 23, + }, + { + .mKey = "node_id", + .mValue = rainmaker_node_id_txt_value, + .mValueLength = sizeof(rainmaker_node_id_txt_value), + } + }; + txt_entries[3].mValueLength = (uint16_t)strlen(rmaker_node_id); + static char s_serv_name[30]; + strncpy(s_serv_name, serv_name, strnlen(serv_name, sizeof(s_serv_name) - 1)); + s_serv_name[strnlen(serv_name, sizeof(s_serv_name) - 1)] = 0; + static otSrpClientService srp_client_service = { + .mName = "_esp_local_ctrl._tcp", + .mInstanceName = (const char*)s_serv_name, + .mTxtEntries = txt_entries, + .mPort = CONFIG_ESP_RMAKER_LOCAL_CTRL_HTTP_PORT, + .mNumTxtEntries = 4, + .mNext = NULL, + .mLease = 0, + .mKeyLease = 0, + }; + esp_openthread_lock_acquire(portMAX_DELAY); + otInstance *instance = esp_openthread_get_instance(); + // Try to remove the service registered before adding a new service. If the previous service is not removed, + // Adding service will fail with a duplicated instance error. This could happen when the device reboots, which + // might result in the wrong resolved IP addresss on the phone app side. + (void)otSrpClientRemoveService(instance, &srp_client_service); + if (otSrpClientAddService(instance, &srp_client_service) != OT_ERROR_NONE) { + esp_openthread_lock_release(); + return ESP_FAIL; + } + otSrpClientEnableAutoStartMode(instance, NULL, NULL); + esp_openthread_lock_release(); + return ESP_OK; +} + +static esp_err_t srp_client_clean_up() +{ + esp_err_t ret = ESP_OK; + esp_openthread_lock_acquire(portMAX_DELAY); + otInstance *instance = esp_openthread_get_instance(); + if (otSrpClientRemoveHostAndServices(instance, false, true) != OT_ERROR_NONE) { + ret = ESP_FAIL; + } + memset(srp_host_name, 0, sizeof(srp_host_name)); + esp_openthread_lock_release(); + return ret; +} + +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ + +static esp_err_t __esp_rmaker_start_local_ctrl_service(const char *serv_name) +{ + if (!serv_name) { + ESP_LOGE(TAG, "Service name cannot be empty."); + return ESP_ERR_INVALID_ARG; + } + + ESP_LOGI(TAG, "Starting ESP Local control with HTTP Transport and security version: %d", esp_rmaker_local_ctrl_get_security_type()); + /* Set the configuration */ + static httpd_ssl_config_t https_conf = HTTPD_SSL_CONFIG_DEFAULT(); + https_conf.transport_mode = HTTPD_SSL_TRANSPORT_INSECURE; + https_conf.port_insecure = CONFIG_ESP_RMAKER_LOCAL_CTRL_HTTP_PORT; + https_conf.httpd.ctrl_port = ESP_RMAKER_LOCAL_CTRL_HTTP_CTRL_PORT; + https_conf.httpd.stack_size = CONFIG_ESP_RMAKER_LOCAL_CTRL_STACK_SIZE; + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + mdns_init(); + mdns_hostname_set(serv_name); +#endif + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD + srp_client_set_host(serv_name); +#endif + + esp_local_ctrl_config_t config = { + .transport = ESP_LOCAL_CTRL_TRANSPORT_HTTPD, + .transport_config = { + .httpd = &https_conf + }, + .handlers = { + /* User defined handler functions */ + .get_prop_values = get_property_values, + .set_prop_values = set_property_values, + .usr_ctx = NULL, + .usr_ctx_free_fn = NULL + }, + /* Maximum number of properties that may be set */ + .max_properties = 10 + }; + + /* If sec1, add security type details to the config */ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) +#define PROTOCOMM_SEC_DATA protocomm_security1_params_t +#else +#define PROTOCOMM_SEC_DATA protocomm_security_pop_t +#endif /* ESP_IDF_VERSION */ + PROTOCOMM_SEC_DATA *pop = NULL; +#if ESP_RMAKER_LOCAL_CTRL_SECURITY_TYPE == 1 + char *pop_str = esp_rmaker_local_ctrl_get_pop(); + /* Note: pop_str shouldn't be freed. If it gets freed, the pointer which is internally copied in esp_local_ctrl_start() will become invalid which would cause corruption. */ + + int sec_ver = esp_rmaker_local_ctrl_get_security_type(); + + if (sec_ver != 0 && pop_str) { + pop = (PROTOCOMM_SEC_DATA *)MEM_CALLOC_EXTRAM(1, sizeof(PROTOCOMM_SEC_DATA)); + if (!pop) { + ESP_LOGE(TAG, "Failed to allocate pop"); + free(pop_str); + return ESP_ERR_NO_MEM; + } + pop->data = (uint8_t *)pop_str; + pop->len = strlen(pop_str); + } + + config.proto_sec.version = sec_ver; + config.proto_sec.custom_handle = NULL; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + config.proto_sec.sec_params = pop; +#else + config.proto_sec.pop = pop; +#endif /* ESP_IDF_VERSION */ +#endif + + /* Start esp_local_ctrl service */ + ESP_ERROR_CHECK(esp_local_ctrl_start(&config)); + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + /* The instance name of mdns service set by esp_local_ctrl_start is 'Local Control Service'. + * We should ensure that each end-device should have an unique instance name. + */ + mdns_service_instance_name_set("_esp_local_ctrl", "_tcp", serv_name); + /* Add node_id in mdns */ + mdns_service_txt_item_set("_esp_local_ctrl", "_tcp", "node_id", esp_rmaker_get_node_id()); +#endif +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD + srp_client_add_local_ctrl_service(serv_name); +#endif + + if (pop) { + free(pop); + } + + ESP_LOGI(TAG, "esp_local_ctrl service started with name : %s", serv_name); + + /* Create the Node Config property */ + esp_local_ctrl_prop_t node_config = { + .name = "config", + .type = PROP_TYPE_NODE_CONFIG, + .size = 0, + .flags = PROP_FLAG_READONLY, + .ctx = NULL, + .ctx_free_fn = NULL + }; + + /* Create the Node Params property */ + esp_local_ctrl_prop_t node_params = { + .name = "params", + .type = PROP_TYPE_NODE_PARAMS, + .size = 0, + .flags = 0, + .ctx = NULL, + .ctx_free_fn = NULL + }; + + /* Now register the properties */ + ESP_ERROR_CHECK(esp_local_ctrl_add_property(&node_config)); + ESP_ERROR_CHECK(esp_local_ctrl_add_property(&node_params)); + + /* update the global status */ + g_local_ctrl_is_started = true; + esp_rmaker_post_event(RMAKER_EVENT_LOCAL_CTRL_STARTED, (void *)serv_name, strlen(serv_name) + 1); + return ESP_OK; +} + +static void esp_rmaker_local_ctrl_prov_event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + ESP_LOGI(TAG, "Event %"PRIu32, event_id); +#if RMAKER_USING_NETWORK_PROV + if (event_base == NETWORK_PROV_EVENT) { +#else + if (event_base == WIFI_PROV_EVENT) { +#endif + switch (event_id) { +#if RMAKER_USING_NETWORK_PROV + case NETWORK_PROV_START: +#else + case WIFI_PROV_START: +#endif + wait_for_provisioning = true; + break; +#if RMAKER_USING_NETWORK_PROV + case NETWORK_PROV_DEINIT: +#else + case WIFI_PROV_DEINIT: +#endif + if (wait_for_provisioning == true) { + wait_for_provisioning = false; + if (g_serv_name) { + __esp_rmaker_start_local_ctrl_service(g_serv_name); + free(g_serv_name); + g_serv_name = NULL; + } + } +#if RMAKER_USING_NETWORK_PROV + esp_event_handler_unregister(NETWORK_PROV_EVENT, NETWORK_PROV_START, &esp_rmaker_local_ctrl_prov_event_handler); + esp_event_handler_unregister(NETWORK_PROV_EVENT, NETWORK_PROV_DEINIT, &esp_rmaker_local_ctrl_prov_event_handler); +#else + esp_event_handler_unregister(WIFI_PROV_EVENT, WIFI_PROV_START, &esp_rmaker_local_ctrl_prov_event_handler); + esp_event_handler_unregister(WIFI_PROV_EVENT, WIFI_PROV_DEINIT, &esp_rmaker_local_ctrl_prov_event_handler); +#endif + break; + default: + break; + } + } +} + +bool esp_rmaker_local_ctrl_service_started(void) +{ + return g_local_ctrl_is_started; +} + +esp_err_t esp_rmaker_init_local_ctrl_service(void) +{ + /* ESP Local Control uses protocomm_httpd, which is also used by SoftAP Provisioning. + * If local control is started before provisioning ends, it fails because only one protocomm_httpd + * instance is allowed at a time. + * So, we check for the NETWORK_PROV_START event, and if received, wait for the NETWORK_PROV_DEINIT + * event before starting local control. + * This would not be required in case of BLE Provisioning, but this code has no easy way of knowing + * what provisioning transport is being used and hence this logic will come into picture for both, + * SoftAP and BLE provisioning. + */ +#if RMAKER_USING_NETWORK_PROV + esp_event_handler_register(NETWORK_PROV_EVENT, NETWORK_PROV_START, &esp_rmaker_local_ctrl_prov_event_handler, NULL); + esp_event_handler_register(NETWORK_PROV_EVENT, NETWORK_PROV_DEINIT, &esp_rmaker_local_ctrl_prov_event_handler, NULL); +#else + esp_event_handler_register(WIFI_PROV_EVENT, WIFI_PROV_START, &esp_rmaker_local_ctrl_prov_event_handler, NULL); + esp_event_handler_register(WIFI_PROV_EVENT, WIFI_PROV_DEINIT, &esp_rmaker_local_ctrl_prov_event_handler, NULL); +#endif + return ESP_OK; +} + +esp_err_t esp_rmaker_start_local_ctrl_service(const char *serv_name) +{ + if (ESP_RMAKER_LOCAL_CTRL_SECURITY_TYPE == 1) { + esp_rmaker_local_ctrl_service_enable(); + } + + if (!wait_for_provisioning) { + return __esp_rmaker_start_local_ctrl_service(serv_name); + } + + ESP_LOGI(TAG, "Waiting for Wi-Fi provisioning to finish."); + g_serv_name = strdup(serv_name); + if (g_serv_name) { + return ESP_OK; + } + return ESP_ERR_NO_MEM; +} + +esp_err_t esp_rmaker_local_ctrl_enable(void) +{ + if (g_local_ctrl_is_started) { + return ESP_ERR_INVALID_STATE; + } + ESP_LOGI(TAG, "Enabling Local Control"); + esp_rmaker_init_local_ctrl_service(); + esp_err_t err; + if ((err = esp_rmaker_start_local_ctrl_service(esp_rmaker_get_node_id())) != ESP_OK) { + ESP_LOGE(TAG, "Failed to start Local Control Service. err=0x%x", err); + } + return err; +} + +esp_err_t esp_rmaker_local_ctrl_disable(void) +{ + ESP_LOGI(TAG, "Disabling Local Control"); + if (g_serv_name) { + free(g_serv_name); + g_serv_name = NULL; + } +#if RMAKER_USING_NETWORK_PROV + esp_event_handler_unregister(NETWORK_PROV_EVENT, NETWORK_PROV_START, &esp_rmaker_local_ctrl_prov_event_handler); + esp_event_handler_unregister(NETWORK_PROV_EVENT, NETWORK_PROV_DEINIT, &esp_rmaker_local_ctrl_prov_event_handler); +#else + esp_event_handler_unregister(WIFI_PROV_EVENT, WIFI_PROV_START, &esp_rmaker_local_ctrl_prov_event_handler); + esp_event_handler_unregister(WIFI_PROV_EVENT, WIFI_PROV_DEINIT, &esp_rmaker_local_ctrl_prov_event_handler); +#endif + if (!g_local_ctrl_is_started) { + return ESP_OK; + } +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + mdns_free(); +#endif +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD + srp_client_clean_up(); +#endif + esp_err_t err = esp_local_ctrl_stop(); + if (err != ESP_OK) { + return err; + } + if (ESP_RMAKER_LOCAL_CTRL_SECURITY_TYPE == 1) { + err = esp_rmaker_local_ctrl_service_disable(); + if (err != ESP_OK) { + return err; + } + } + /* update the global status */ + g_local_ctrl_is_started = false; + esp_rmaker_post_event(RMAKER_EVENT_LOCAL_CTRL_STOPPED, NULL, 0); + return err; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_mqtt_topics.h b/components/esp_rainmaker/src/core/esp_rmaker_mqtt_topics.h new file mode 100644 index 0000000..50117b5 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_mqtt_topics.h @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#define NODE_CONFIG_TOPIC_RULE "esp_node_config" +#define NODE_PARAMS_LOCAL_TOPIC_RULE "esp_set_params" +#define NODE_PARAMS_LOCAL_INIT_RULE "esp_init_params" +#define NODE_PARAMS_ALERT_TOPIC_RULE "esp_node_alert" +#define USER_MAPPING_TOPIC_RULE "esp_user_node_mapping" +#define OTAFETCH_TOPIC_RULE "esp_node_otafetch" +#define OTASTATUS_TOPIC_RULE "esp_node_otastatus" +#define TIME_SERIES_DATA_TOPIC_RULE "esp_ts_ingest" +#define SIMPLE_TS_DATA_TOPIC_RULE "esp_simple_ts_ingest" +#define CMD_RESP_TOPIC_RULE "esp_cmd_resp" + + +#define USER_MAPPING_TOPIC_SUFFIX "user/mapping" +#define NODE_PARAMS_LOCAL_TOPIC_SUFFIX "params/local" +#define NODE_PARAMS_LOCAL_INIT_TOPIC_SUFFIX "params/local/init" +#define NODE_PARAMS_REMOTE_TOPIC_SUFFIX "params/remote" +#define TIME_SERIES_DATA_TOPIC_SUFFIX "tsdata" +#define SIMPLE_TS_DATA_TOPIC_SUFFIX "simple_tsdata" +#define NODE_PARAMS_ALERT_TOPIC_SUFFIX "alert" +#define NODE_CONFIG_TOPIC_SUFFIX "config" +#define OTAURL_TOPIC_SUFFIX "otaurl" +#define OTAFETCH_TOPIC_SUFFIX "otafetch" +#define OTASTATUS_TOPIC_SUFFIX "otastatus" +#define CMD_RESP_TOPIC_SUFFIX "from-node" +#define TO_NODE_TOPIC_SUFFIX "to-node" +#define INSIGHTS_TOPIC_SUFFIX "diagnostics/from-node" + +#define MQTT_TOPIC_BUFFER_SIZE 150 diff --git a/components/esp_rainmaker/src/core/esp_rmaker_node.c b/components/esp_rainmaker/src/core/esp_rmaker_node.c new file mode 100644 index 0000000..04a5131 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_node.c @@ -0,0 +1,375 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include "esp_idf_version.h" +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) +#include +#else +#include +#endif +#include +#include +#include + +#include "esp_rmaker_internal.h" + +static const char *TAG = "esp_rmaker_node"; + +static void esp_rmaker_node_info_free(esp_rmaker_node_info_t *info) +{ + if (info) { + if (info->name) { + free(info->name); + } + if (info->type) { + free(info->type); + } + if (info->model) { + free(info->model); + } + if (info->fw_version) { + free(info->fw_version); + } + if (info->subtype) { + free(info->subtype); + } + if (info->secure_boot_digest) { + esp_rmaker_secure_boot_digest_free(info->secure_boot_digest); + info->secure_boot_digest = NULL; + } + free(info); + } +} + +esp_err_t esp_rmaker_attribute_delete(esp_rmaker_attr_t *attr) +{ + if (attr) { + if (attr->name) { + free(attr->name); + } + if (attr->value) { + free(attr->value); + } + free(attr); + return ESP_OK; + } + return ESP_ERR_INVALID_ARG; +} + +esp_err_t esp_rmaker_node_delete(const esp_rmaker_node_t *node) +{ + _esp_rmaker_node_t *_node = (_esp_rmaker_node_t *)node; + if (_node) { + esp_rmaker_attr_t *attr = _node->attributes; + while (attr) { + esp_rmaker_attr_t *next_attr = attr->next; + esp_rmaker_attribute_delete(attr); + attr = next_attr; + } + _esp_rmaker_device_t *device = _node->devices; + while (device) { + _esp_rmaker_device_t *next_device = device->next; + device->parent = NULL; + esp_rmaker_device_delete((esp_rmaker_device_t *)device); + device = next_device; + } + /* Node ID is created in the context of esp_rmaker_init and just assigned + * here. So, we would not free it here. + */ + if (_node->node_id) { + _node->node_id = NULL; + } + if (_node->info) { + esp_rmaker_node_info_free(_node->info); + } + return ESP_OK; + } + return ESP_ERR_INVALID_ARG; +} + +esp_rmaker_node_t *esp_rmaker_node_create(const char *name, const char *type) +{ + static bool node_created; + if (node_created) { + ESP_LOGE(TAG, "Node has already been created. Cannot create another"); + return NULL; + } + if (!name || !type) { + ESP_LOGE(TAG, "Node Name and Type are mandatory."); + return NULL; + } + _esp_rmaker_node_t *node = MEM_CALLOC_EXTRAM(1, sizeof(_esp_rmaker_node_t)); + if (!node) { + ESP_LOGE(TAG, "Failed to allocate memory for node."); + return NULL; + } + node->node_id = esp_rmaker_get_node_id(); + if (!node->node_id) { + ESP_LOGE(TAG, "Failed to initialise Node Id. Please perform \"claiming\" using RainMaker CLI."); + goto node_create_err; + } + ESP_LOGI(TAG, "Node ID ----- %s", node->node_id); + + node->info = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_node_info_t)); + if (!node->info) { + ESP_LOGE(TAG, "Failed to allocate memory for node info."); + goto node_create_err; + } + node->info->name = strdup(name); + node->info->type = strdup(type); + const esp_app_desc_t *app_desc; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + app_desc = esp_app_get_description(); +#else + app_desc = esp_ota_get_app_description(); +#endif + node->info->fw_version = strdup(app_desc->version); + node->info->model = strdup(app_desc->project_name); + if (esp_secure_boot_enabled()) { + node->info->secure_boot_digest = esp_rmaker_get_secure_boot_digest(); + if (!node->info->secure_boot_digest) { + goto node_create_err; + } + } + if (!node->info->name || !node->info->type + || !node->info->fw_version || !node->info->model) { + ESP_LOGE(TAG, "Failed to allocate memory for node info."); + goto node_create_err; + } + node_created = true; + return (esp_rmaker_node_t *)node; +node_create_err: + esp_rmaker_node_delete((esp_rmaker_node_t *)node); + return NULL; +} + +esp_err_t esp_rmaker_node_add_fw_version(const esp_rmaker_node_t *node, const char *fw_version) +{ + if (!node || !fw_version) { + ESP_LOGE(TAG, "Node handle or fw version cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_node_info_t *info = esp_rmaker_node_get_info(node); + if (!info) { + ESP_LOGE(TAG, "Failed to get Node Info."); + return ESP_ERR_INVALID_ARG; + } + if (info->fw_version) { + free(info->fw_version); + } + info->fw_version = strdup(fw_version); + if (!info->fw_version) { + ESP_LOGE(TAG, "Failed to allocate memory for fw version."); + return ESP_ERR_NO_MEM; + } + return ESP_OK; +} + +esp_err_t esp_rmaker_node_add_model(const esp_rmaker_node_t *node, const char *model) +{ + if (!node || !model) { + ESP_LOGE(TAG, "Node handle or model cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_node_info_t *info = esp_rmaker_node_get_info(node); + if (!info) { + ESP_LOGE(TAG, "Failed to get Node Info."); + return ESP_ERR_INVALID_ARG; + } + if (info->model) { + free(info->model); + } + info->model = strdup(model); + if (!info->model) { + ESP_LOGE(TAG, "Failed to allocate memory for node model."); + return ESP_ERR_NO_MEM; + } + return ESP_OK; +} + +esp_err_t esp_rmaker_node_add_subtype(const esp_rmaker_node_t *node, const char *subtype) +{ + if (!node || !subtype) { + ESP_LOGE(TAG, "Node handle or subtype cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_node_info_t *info = esp_rmaker_node_get_info(node); + if (!info) { + ESP_LOGE(TAG, "Failed to get Node Info."); + return ESP_ERR_INVALID_ARG; + } + if (info->subtype) { + free(info->subtype); + } + info->subtype = strdup(subtype); + if (!info->subtype) { + ESP_LOGE(TAG, "Failed to allocate memory for node subtype."); + return ESP_ERR_NO_MEM; + } + return ESP_OK; +} + +esp_err_t esp_rmaker_node_add_attribute(const esp_rmaker_node_t *node, const char *attr_name, const char *value) +{ + if (!node || !attr_name || !value) { + ESP_LOGE(TAG, "Node handle, attribute name or value cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_attr_t *attr = ((_esp_rmaker_node_t *)node)->attributes; + while(attr && attr->next) { + if (strcmp(attr->name, attr_name) == 0) { + ESP_LOGE(TAG, "Node attribute with name %s already exists.", attr_name); + return ESP_FAIL; + } + attr = attr->next; + } + esp_rmaker_attr_t *new_attr = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_attr_t)); + if (!new_attr) { + ESP_LOGE(TAG, "Failed to create node attribute %s.", attr_name); + return ESP_ERR_NO_MEM; + } + new_attr->name = strdup(attr_name); + new_attr->value = strdup(value); + if (!new_attr->name || !new_attr->value) { + ESP_LOGE(TAG, "Failed to allocate memory for name/value for attribute %s.", attr_name); + esp_rmaker_attribute_delete(new_attr); + return ESP_ERR_NO_MEM; + } + + if (attr) { + attr->next = new_attr; + } else { + ((_esp_rmaker_node_t *)node)->attributes = new_attr; + } + ESP_LOGI(TAG, "Node attribute %s created", attr_name); + return ESP_OK; +} + +esp_err_t esp_rmaker_node_add_device(const esp_rmaker_node_t *node, const esp_rmaker_device_t *device) +{ + if (!node || !device) { + ESP_LOGE(TAG, "Node or Device/Service handle cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_node_t *_node = (_esp_rmaker_node_t *)node; + _esp_rmaker_device_t *_new_device = (_esp_rmaker_device_t *)device; + _esp_rmaker_device_t *_device = _node->devices; + while(_device) { + if (strcmp(_device->name, _new_device->name) == 0) { + ESP_LOGE(TAG, "%s with name %s already exists", _new_device->is_service ? "Service":"Device", _new_device->name); + return ESP_ERR_INVALID_ARG; + } + if (_device->next) { + _device = _device->next; + } else { + break; + } + } + if (_device) { + _device->next = _new_device; + } else { + _node->devices = _new_device; + } + _new_device->parent = node; + return ESP_OK; +} + +esp_err_t esp_rmaker_node_remove_device(const esp_rmaker_node_t *node, const esp_rmaker_device_t *device) +{ + if (!node || !device) { + ESP_LOGE(TAG, "Node or Device/Service handle cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_node_t *_node = (_esp_rmaker_node_t *)node; + _esp_rmaker_device_t *_device = (_esp_rmaker_device_t *)device; + + _esp_rmaker_device_t *tmp_device = _node->devices; + _esp_rmaker_device_t *prev_device = NULL; + while(tmp_device) { + if (tmp_device == _device) { + break; + } + prev_device = tmp_device; + tmp_device = tmp_device->next; + } + if (!tmp_device) { + ESP_LOGE(TAG, "Device %s not found in node %s", _device->name, _node->info->name); + return ESP_ERR_INVALID_ARG; + } + if (tmp_device == _node->devices) { + _node->devices = tmp_device->next; + } else { + prev_device->next = tmp_device->next; + } + tmp_device->parent = NULL; + return ESP_OK; +} + +esp_rmaker_device_t *esp_rmaker_node_get_device_by_name(const esp_rmaker_node_t *node, const char *device_name) +{ + if (!node || !device_name) { + ESP_LOGE(TAG, "Node handle or device name cannot be NULL"); + return NULL; + } + _esp_rmaker_device_t *device = ((_esp_rmaker_node_t *)node)->devices; + while(device) { + if (strcmp(device->name, device_name) == 0) { + break; + } + device = device->next; + } + return (esp_rmaker_device_t *)device; +} + +_esp_rmaker_device_t *esp_rmaker_node_get_first_device(const esp_rmaker_node_t *node) +{ + _esp_rmaker_node_t *_node = (_esp_rmaker_node_t *)node; + if (!_node) { + ESP_LOGE(TAG, "Node handle cannot be NULL."); + return NULL; + } + return _node->devices; +} + +esp_rmaker_node_info_t *esp_rmaker_node_get_info(const esp_rmaker_node_t *node) +{ + _esp_rmaker_node_t *_node = (_esp_rmaker_node_t *)node; + if (!_node) { + ESP_LOGE(TAG, "Node handle cannot be NULL."); + return NULL; + } + return _node->info; +} + +esp_rmaker_attr_t *esp_rmaker_node_get_first_attribute(const esp_rmaker_node_t *node) +{ + _esp_rmaker_node_t *_node = (_esp_rmaker_node_t *)node; + if (!_node) { + ESP_LOGE(TAG, "Node handle cannot be NULL."); + return NULL; + } + return _node->attributes; +} + +char *esp_rmaker_node_get_id(const esp_rmaker_node_t *node) +{ + _esp_rmaker_node_t *_node = (_esp_rmaker_node_t *)node; + if (!_node) { + ESP_LOGE(TAG, "Node handle cannot be NULL."); + return NULL; + } + return _node->node_id; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_node_auth.c b/components/esp_rainmaker/src/core/esp_rmaker_node_auth.c new file mode 100644 index 0000000..ab2c90c --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_node_auth.c @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) +#include +#include +#include "mbedtls/platform.h" +#include "mbedtls/pk.h" +#include "mbedtls/sha256.h" +#include "mbedtls/ecdsa.h" +#include "sha/sha_parallel_engine.h" + +#include "esp_secure_cert_read.h" +#include "esp_rmaker_utils.h" +#include "esp_rmaker_client_data.h" +#include "mbedtls/x509_crt.h" + +static const char *TAG = "esp_rmaker_user_node_auth"; + +static int myrand(void *rng_state, unsigned char *output, size_t len) +{ + esp_fill_random(output, len); + return 0; +} + +static inline uint8_t to_hex_digit(unsigned val) +{ + return (val < 10) ? ('0' + val) : ('a' + val - 10); +} + +static void bytes_to_hex(uint8_t *src, uint8_t *dst, int in_len) +{ + for (int i = 0; i < in_len; i++) { + dst[2 * i] = to_hex_digit(src[i] >> 4); + dst[2 * i + 1] = to_hex_digit(src[i] & 0xf); + } + dst[2 * in_len] = 0; +} + +esp_err_t esp_rmaker_node_auth_sign_msg(const void *challenge, size_t inlen, void **response, size_t *outlen) +{ + if (!challenge || (inlen == 0)) { + ESP_LOGE(TAG, "function arguments challenge and inlen cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + + /* Get private key */ + char *priv_key = NULL; + size_t priv_key_len = 0; +#if CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + esp_secure_cert_key_type_t key_type; + esp_err_t err = esp_secure_cert_get_priv_key_type(&key_type); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to get the type of private key from secure cert partition, err:%d", err); + return err; + } + if (key_type == ESP_SECURE_CERT_INVALID_KEY) { + ESP_LOGE(TAG, "Private key type in secure cert partition is invalid"); + return ESP_FAIL; + } + /* This flow is for devices supporting ECDSA peripheral */ + if (key_type == ESP_SECURE_CERT_ECDSA_PERIPHERAL_KEY) { +#if CONFIG_USE_ESP32_ECDSA_PERIPHERAL + /* TODO: code for signing the challenge on devices that have a DS peripheral. */ + return ESP_FAIL; +#else /* !CONFIG_USE_ESP32_ECDSA_PERIPHERAL */ + return ESP_ERR_INVALID_STATE; +#endif /* CONFIG_USE_ESP32_ECDSA_PERIPHERAL */ + } else +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + { + /* This flow is for devices which do not support ECDSA peripheral */ +#if !CONFIG_USE_ESP32_ECDSA_PERIPHERAL + priv_key = esp_rmaker_get_client_key(); + priv_key_len = esp_rmaker_get_client_key_len(); +#else /* CONFIG_USE_ESP32_ECDSA_PERIPHERAL */ + return ESP_ERR_INVALID_STATE; +#endif /* !CONFIG_USE_ESP32_ECDSA_PERIPHERAL */ + } + if (!priv_key) { + ESP_LOGE(TAG, "Error getting private key"); + return ESP_FAIL; + } + /* Calculate SHA of challenge */ + uint8_t hash[32]; + esp_sha(SHA2_256,(const unsigned char *)challenge, inlen, hash); + + /* Sign the hash using RSA or ECDSA */ + mbedtls_pk_context pk_ctx; + mbedtls_pk_init(&pk_ctx); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + int ret = mbedtls_pk_parse_key(&pk_ctx, (uint8_t *)priv_key, priv_key_len, NULL, 0, NULL, 0); +#else /* !(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) */ + int ret = mbedtls_pk_parse_key(&pk_ctx, (uint8_t *)priv_key, priv_key_len, NULL, 0); +#endif /* ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) */ + uint8_t *signature = NULL; + if (mbedtls_pk_get_type(&pk_ctx) == MBEDTLS_PK_RSA) { + ESP_LOGI(TAG, "RSA key found"); + signature = (uint8_t *)MEM_CALLOC_EXTRAM(1, 256); // TODO: replace magic number 256 + } else if (mbedtls_pk_get_type(&pk_ctx) == MBEDTLS_PK_ECKEY) { + ESP_LOGI(TAG, "ECDSA key found"); + signature = (uint8_t *)MEM_CALLOC_EXTRAM(1, MBEDTLS_ECDSA_MAX_LEN); + } else { + ESP_LOGE(TAG, "found different key: %d", mbedtls_pk_get_type(&pk_ctx)); + } + if (!signature) { + ESP_LOGE(TAG, "Failed to allocate memory to signature."); + mbedtls_pk_free(&pk_ctx); + return ESP_ERR_NO_MEM; + } + size_t slen = 0; + if (mbedtls_pk_get_type(&pk_ctx) == MBEDTLS_PK_ECKEY) { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + ret = mbedtls_ecdsa_write_signature(mbedtls_pk_ec(pk_ctx), MBEDTLS_MD_SHA256, hash, sizeof(hash), signature, MBEDTLS_ECDSA_MAX_LEN, &slen, myrand, NULL); +#else /* !(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) */ + ret = mbedtls_ecdsa_write_signature(mbedtls_pk_ec(pk_ctx), MBEDTLS_MD_SHA256, hash, sizeof(hash), signature, &slen, myrand, NULL); +#endif /* (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) */ + if (ret != 0) { + ESP_LOGE(TAG, "Error in writing signature. err = %d", ret); + free(signature); + mbedtls_pk_free(&pk_ctx); + return ESP_FAIL; + } + } else if (mbedtls_pk_get_type(&pk_ctx) == MBEDTLS_PK_RSA) { + mbedtls_rsa_context *rsa_ctx = mbedtls_pk_rsa(pk_ctx); + // rsa_ctx->MBEDTLS_PRIVATE(len) = 256; + mbedtls_rsa_set_padding(rsa_ctx, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256); + ret = mbedtls_rsa_rsassa_pss_sign(rsa_ctx, + myrand, + NULL, + MBEDTLS_MD_SHA256, + sizeof(hash), + hash, + signature); + if (ret != 0) { + ESP_LOGE(TAG, "Error in writing signature. err = %d", ret); + free(signature); + mbedtls_pk_free(&pk_ctx); + return ESP_FAIL; + } + slen = mbedtls_rsa_get_len(rsa_ctx); + ESP_LOGI(TAG, "signature length %d", slen); + } + +#if TEST_SIGNATURE_VERIFICATION + char *cert = esp_rmaker_get_client_cert(); + mbedtls_x509_crt crt; + mbedtls_x509_crt_init(&crt); + mbedtls_x509_crt_parse(&crt, (const unsigned char *)cert, strlen(cert) + 1); + mbedtls_pk_context *pk = &crt.pk; + + if (mbedtls_pk_get_type(&pk_ctx) == MBEDTLS_PK_ECKEY) { + ret = mbedtls_pk_verify(pk, MBEDTLS_MD_SHA256, hash, sizeof(hash), signature, sizeof(signature)); + } else if (mbedtls_pk_get_type(&pk_ctx) == MBEDTLS_PK_RSA) { + mbedtls_pk_rsassa_pss_options opt = { + .mgf1_hash_id = MBEDTLS_MD_SHA256, + .expected_salt_len = MBEDTLS_RSA_SALT_LEN_ANY + }; + ret = mbedtls_pk_verify_ext(MBEDTLS_PK_RSASSA_PSS, (const void *)&opt, pk, MBEDTLS_MD_SHA256, hash, sizeof(hash), signature, slen); + } + + mbedtls_x509_crt_free(&crt); + free(cert); + if (ret == 0) { + ESP_LOGI(TAG, "Signature is valid"); + } else { + ESP_LOGE(TAG, "Signature verification failed %d", ret); + } +#endif + + /* Convert hex stream to bytes */ +#define BYTE_ENCODED_SIGNATURE_LEN ((2 * slen) + 1) /* +1 for null character */ + char *char_signature = (char *)MEM_ALLOC_EXTRAM(BYTE_ENCODED_SIGNATURE_LEN); + if (!char_signature) { + ESP_LOGE(TAG, "Error in allocating memory for challenge response."); + free(signature); + mbedtls_pk_free(&pk_ctx); + return ESP_ERR_NO_MEM; + } + bytes_to_hex(signature, (uint8_t *)char_signature, slen); + mbedtls_pk_free(&pk_ctx); + free(signature); + /* Set output variables */ + *(char **)response = char_signature; + *outlen = 2 * slen; /* hex encoding takes 2 bytes per input byte */ + return ESP_OK; +} +#endif /* ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 3, 0) */ diff --git a/components/esp_rainmaker/src/core/esp_rmaker_node_config.c b/components/esp_rainmaker/src/core/esp_rmaker_node_config.c new file mode 100644 index 0000000..df279d1 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_node_config.c @@ -0,0 +1,311 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include +#include +#include +#include +#include "esp_rmaker_internal.h" +#include "esp_rmaker_mqtt.h" +#include "esp_rmaker_mqtt_topics.h" +#include + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) +#include +#else +#include +#endif + +#define NODE_CONFIG_TOPIC_SUFFIX "config" + +static const char *TAG = "esp_rmaker_node_config"; +static esp_err_t esp_rmaker_report_info(json_gen_str_t *jptr) +{ + /* TODO: Error handling */ + esp_rmaker_node_info_t *info = esp_rmaker_node_get_info(esp_rmaker_get_node()); + json_gen_obj_set_string(jptr, "node_id", esp_rmaker_get_node_id()); + json_gen_obj_set_string(jptr, "config_version", ESP_RMAKER_CONFIG_VERSION); + json_gen_push_object(jptr, "info"); + json_gen_obj_set_string(jptr, "name", info->name); + json_gen_obj_set_string(jptr, "fw_version", info->fw_version); + json_gen_obj_set_string(jptr, "type", info->type); + if (info->subtype) { + json_gen_obj_set_string(jptr, "subtype", info->subtype); + } + json_gen_obj_set_string(jptr, "model", info->model); + const esp_app_desc_t *app_desc; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + app_desc = esp_app_get_description(); +#else + app_desc = esp_ota_get_app_description(); +#endif + json_gen_obj_set_string(jptr, "project_name", (char *)app_desc->project_name); + json_gen_obj_set_string(jptr, "platform", CONFIG_IDF_TARGET); +#ifdef CONFIG_SECURE_BOOT_V2_ENABLED + json_gen_push_object(jptr, "secure_boot_digest"); + for (int i = 0; i < SECURE_BOOT_NUM_BLOCKS; i++) { + char key_name[3]; + snprintf(key_name, sizeof(key_name), "k%d", i); + key_name[2] = '\0'; + if (info->secure_boot_digest[i] == NULL) { + json_gen_obj_set_null(jptr, key_name); + continue; + } + json_gen_obj_set_string(jptr, key_name, info->secure_boot_digest[i]); + } + json_gen_pop_object(jptr); +#endif + json_gen_pop_object(jptr); + return ESP_OK; +} + +static void esp_rmaker_report_attribute(esp_rmaker_attr_t *attr, json_gen_str_t *jptr) +{ + json_gen_start_object(jptr); + json_gen_obj_set_string(jptr, "name", attr->name); + json_gen_obj_set_string(jptr, "value", attr->value); + json_gen_end_object(jptr); +} + +static esp_err_t esp_rmaker_report_node_attributes(json_gen_str_t *jptr) +{ + esp_rmaker_attr_t *attr = esp_rmaker_node_get_first_attribute(esp_rmaker_get_node()); + if (!attr) { + return ESP_OK; + } + json_gen_push_array(jptr, "attributes"); + while (attr) { + esp_rmaker_report_attribute(attr, jptr); + attr = attr->next; + } + json_gen_pop_array(jptr); + return ESP_OK; +} + +esp_err_t esp_rmaker_report_value(const esp_rmaker_param_val_t *val, char *key, json_gen_str_t *jptr) +{ + if (!key || !jptr) { + return ESP_FAIL; + } + if (!val) { + json_gen_obj_set_null(jptr, key); + return ESP_OK; + } + switch (val->type) { + case RMAKER_VAL_TYPE_BOOLEAN: + json_gen_obj_set_bool(jptr, key, val->val.b); + break; + case RMAKER_VAL_TYPE_INTEGER: + json_gen_obj_set_int(jptr, key, val->val.i); + break; + case RMAKER_VAL_TYPE_FLOAT: + json_gen_obj_set_float(jptr, key, val->val.f); + break; + case RMAKER_VAL_TYPE_STRING: + json_gen_obj_set_string(jptr, key, val->val.s); + break; + case RMAKER_VAL_TYPE_OBJECT: + json_gen_push_object_str(jptr, key, val->val.s); + break; + case RMAKER_VAL_TYPE_ARRAY: + json_gen_push_array_str(jptr, key, val->val.s); + break; + default: + break; + } + return ESP_OK; +} + +esp_err_t esp_rmaker_report_data_type(esp_rmaker_val_type_t type, char *data_type_key, json_gen_str_t *jptr) +{ + switch (type) { + case RMAKER_VAL_TYPE_BOOLEAN: + json_gen_obj_set_string(jptr, data_type_key, "bool"); + break; + case RMAKER_VAL_TYPE_INTEGER: + json_gen_obj_set_string(jptr, data_type_key, "int"); + break; + case RMAKER_VAL_TYPE_FLOAT: + json_gen_obj_set_string(jptr, data_type_key, "float"); + break; + case RMAKER_VAL_TYPE_STRING: + json_gen_obj_set_string(jptr, data_type_key, "string"); + break; + case RMAKER_VAL_TYPE_OBJECT: + json_gen_obj_set_string(jptr, data_type_key, "object"); + break; + case RMAKER_VAL_TYPE_ARRAY: + json_gen_obj_set_string(jptr, data_type_key, "array"); + break; + default: + json_gen_obj_set_string(jptr, data_type_key, "invalid"); + break; + } + return ESP_OK; +} + +static esp_err_t esp_rmaker_report_param_config(_esp_rmaker_param_t *param, json_gen_str_t *jptr) +{ + json_gen_start_object(jptr); + if (param->name) { + json_gen_obj_set_string(jptr, "name", param->name); + } + if (param->type) { + json_gen_obj_set_string(jptr, "type", param->type); + } + esp_rmaker_report_data_type(param->val.type, "data_type", jptr); + json_gen_push_array(jptr, "properties"); + if (param->prop_flags & PROP_FLAG_READ) { + json_gen_arr_set_string(jptr, "read"); + } + if (param->prop_flags & PROP_FLAG_WRITE) { + json_gen_arr_set_string(jptr, "write"); + } + /* A parameter cannot have both, PROP_FLAG_TIME_SERIES and PROP_FLAG_SIMPLE_TIME_SERIES */ + if (param->prop_flags & PROP_FLAG_TIME_SERIES) { + json_gen_arr_set_string(jptr, "time_series"); + } else if (param->prop_flags & PROP_FLAG_SIMPLE_TIME_SERIES) { + json_gen_arr_set_string(jptr, "simple_ts"); + } + json_gen_pop_array(jptr); + if (param->bounds) { + json_gen_push_object(jptr, "bounds"); + esp_rmaker_report_value(¶m->bounds->min, "min", jptr); + esp_rmaker_report_value(¶m->bounds->max, "max", jptr); + if (param->bounds->step.val.i) { + esp_rmaker_report_value(¶m->bounds->step, "step", jptr); + } + json_gen_pop_object(jptr); + } + if (param->valid_str_list) { + json_gen_push_array(jptr, "valid_strs"); + for (int i = 0; i < param->valid_str_list->str_list_cnt; i++) { + json_gen_arr_set_string(jptr, (char *)param->valid_str_list->str_list[i]); + } + json_gen_pop_array(jptr); + } + if (param->ui_type) { + json_gen_obj_set_string(jptr, "ui_type", param->ui_type); + } + json_gen_end_object(jptr); + return ESP_OK; +} + +static esp_err_t esp_rmaker_report_devices_or_services(json_gen_str_t *jptr, char *key) +{ + _esp_rmaker_device_t *device = esp_rmaker_node_get_first_device(esp_rmaker_get_node()); + if (!device) { + return ESP_OK; + } + bool is_service = false; + if (strcmp(key, "services") == 0) { + is_service = true; + } + json_gen_push_array(jptr, key); + while (device) { + if (device->is_service == is_service) { + json_gen_start_object(jptr); + json_gen_obj_set_string(jptr, "name", device->name); + if (device->type) { + json_gen_obj_set_string(jptr, "type", device->type); + } + if (device->subtype) { + json_gen_obj_set_string(jptr, "subtype", device->subtype); + } + if (device->model) { + json_gen_obj_set_string(jptr, "model", device->model); + } + if (device->attributes) { + json_gen_push_array(jptr, "attributes"); + esp_rmaker_attr_t *attr = device->attributes; + while (attr) { + esp_rmaker_report_attribute(attr, jptr); + attr = attr->next; + } + json_gen_pop_array(jptr); + } + if (device->primary) { + json_gen_obj_set_string(jptr, "primary", device->primary->name); + } + if (device->params) { + json_gen_push_array(jptr, "params"); + _esp_rmaker_param_t *param = device->params; + while (param) { + esp_rmaker_report_param_config(param, jptr); + param = param->next; + } + json_gen_pop_array(jptr); + } + json_gen_end_object(jptr); + } + device = device->next; + } + json_gen_pop_array(jptr); + return ESP_OK; +} + +int __esp_rmaker_get_node_config(char *buf, size_t buf_size) +{ + json_gen_str_t jstr; + json_gen_str_start(&jstr, buf, buf_size, NULL, NULL); + json_gen_start_object(&jstr); + esp_rmaker_report_info(&jstr); + esp_rmaker_report_node_attributes(&jstr); + esp_rmaker_report_devices_or_services(&jstr, "devices"); + esp_rmaker_report_devices_or_services(&jstr, "services"); + if (json_gen_end_object(&jstr) < 0) { + return -1; + } + return json_gen_str_end(&jstr); +} + +char *esp_rmaker_get_node_config(void) +{ + /* Setting buffer to NULL and size to 0 just to get the required buffer size */ + int req_size = __esp_rmaker_get_node_config(NULL, 0); + if (req_size < 0) { + ESP_LOGE(TAG, "Failed to get required size for Node config JSON."); + return NULL; + } + char *node_config = MEM_CALLOC_EXTRAM(1, req_size); + if (!node_config) { + ESP_LOGE(TAG, "Failed to allocate %d bytes for node config", req_size); + return NULL; + } + if (__esp_rmaker_get_node_config(node_config, req_size) < 0) { + free(node_config); + ESP_LOGE(TAG, "Failed to generate Node config JSON."); + return NULL; + } + ESP_LOGI(TAG, "Generated Node config of length %d", req_size); + return node_config; +} + +esp_err_t esp_rmaker_report_node_config() +{ + char *publish_payload = esp_rmaker_get_node_config(); + if (!publish_payload) { + ESP_LOGE(TAG, "Could not get node configuration for reporting to cloud"); + return ESP_FAIL; + } + char publish_topic[MQTT_TOPIC_BUFFER_SIZE]; + esp_rmaker_create_mqtt_topic(publish_topic, MQTT_TOPIC_BUFFER_SIZE, NODE_CONFIG_TOPIC_SUFFIX, NODE_CONFIG_TOPIC_RULE); + ESP_LOGD(TAG, "Reporting Node Configuration of length %lu bytes.", (unsigned long) strlen(publish_payload)); + ESP_LOGD(TAG, "%s", publish_payload); + esp_err_t ret = esp_rmaker_mqtt_publish(publish_topic, publish_payload, strlen(publish_payload), + RMAKER_MQTT_QOS1, NULL); + free(publish_payload); + return ret; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_param.c b/components/esp_rainmaker/src/core/esp_rmaker_param.c new file mode 100644 index 0000000..518685c --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_param.c @@ -0,0 +1,1001 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include "esp_rmaker_mqtt_topics.h" +#include "esp_rmaker_internal.h" + +#define TS_DATA_VERSION "2021-09-13" + +#define ESP_RMAKER_ALERT_KEY "esp.alert.str" + +#define RMAKER_PARAMS_SIZE_MARGIN 50 /* To accommodate for changes in param values while creating JSON */ +#define RMAKER_ALERT_STR_MARGIN 25 /* To accommodate rest of the alert payload {"esp.alert.str":""} */ +#define MAX_TS_DATA_PARAM_NAME 66 /* Time series data param name is of the format . */ + +static size_t max_node_params_size = CONFIG_ESP_RMAKER_MAX_PARAM_DATA_SIZE; +/* This buffer will be allocated once and will be reused for all param updates. + * It may be reallocated if the params size becomes too large */ + +static char publish_topic[MQTT_TOPIC_BUFFER_SIZE]; +static bool esp_rmaker_params_mqtt_init_done; + +static const char *TAG = "esp_rmaker_param"; + + +static const char *cb_srcs[ESP_RMAKER_REQ_SRC_MAX] = { + [ESP_RMAKER_REQ_SRC_INIT] = "Init", + [ESP_RMAKER_REQ_SRC_CLOUD] = "Cloud", + [ESP_RMAKER_REQ_SRC_SCHEDULE] = "Schedule", + [ESP_RMAKER_REQ_SRC_SCENE_ACTIVATE] = "Scene Activate", + [ESP_RMAKER_REQ_SRC_SCENE_DEACTIVATE] = "Scene Deactivate", + [ESP_RMAKER_REQ_SRC_LOCAL] = "Local", +}; + +const char *esp_rmaker_device_cb_src_to_str(esp_rmaker_req_src_t src) +{ + if ((src >= 0) && (src < ESP_RMAKER_REQ_SRC_MAX)) { + return cb_srcs[src]; + } + return NULL; +} + +esp_rmaker_param_val_t esp_rmaker_bool(bool val) +{ + esp_rmaker_param_val_t param_val = { + .type = RMAKER_VAL_TYPE_BOOLEAN, + .val.b = val + }; + return param_val; +} + +esp_rmaker_param_val_t esp_rmaker_int(int val) +{ + esp_rmaker_param_val_t param_val = { + .type = RMAKER_VAL_TYPE_INTEGER, + .val.i = val + }; + return param_val; +} + +esp_rmaker_param_val_t esp_rmaker_float(float val) +{ + esp_rmaker_param_val_t param_val = { + .type = RMAKER_VAL_TYPE_FLOAT, + .val.f = val + }; + return param_val; +} + +esp_rmaker_param_val_t esp_rmaker_str(const char *val) +{ + esp_rmaker_param_val_t param_val = { + .type = RMAKER_VAL_TYPE_STRING, + .val.s = (char *)val + }; + return param_val; +} + +esp_rmaker_param_val_t esp_rmaker_obj(const char *val) +{ + esp_rmaker_param_val_t param_val = { + .type = RMAKER_VAL_TYPE_OBJECT, + .val.s = (char *)val + }; + return param_val; +} + +esp_rmaker_param_val_t esp_rmaker_array(const char *val) +{ + esp_rmaker_param_val_t param_val = { + .type = RMAKER_VAL_TYPE_ARRAY, + .val.s = (char *)val + }; + return param_val; +} + +static esp_err_t esp_rmaker_populate_params(char *buf, size_t *buf_len, uint8_t flags, bool reset_flags) +{ + esp_err_t err = ESP_OK; + json_gen_str_t jstr; + json_gen_str_start(&jstr, buf, *buf_len, NULL, NULL); + json_gen_start_object(&jstr); + _esp_rmaker_device_t *device = esp_rmaker_node_get_first_device(esp_rmaker_get_node()); + while (device) { + bool device_added = false; + _esp_rmaker_param_t *param = device->params; + while (param) { + if (!flags || (param->flags & flags)) { + if (!device_added) { + json_gen_push_object(&jstr, device->name); + device_added = true; + } + esp_rmaker_report_value(¶m->val, param->name, &jstr); + } + param = param->next; + } + if (device_added) { + json_gen_pop_object(&jstr); + } + device = device->next; + } + if (json_gen_end_object(&jstr) < 0) { + err = ESP_ERR_NO_MEM; + } + /* Resetting the flags after creating the JSON in order to handle cases wherein + * memory has been insufficient and this same function would have to be called + * again with a larger buffer. + */ + if (err == ESP_OK) { + device = esp_rmaker_node_get_first_device(esp_rmaker_get_node()); + while (device) { + _esp_rmaker_param_t *param = device->params; + while (param) { + if (reset_flags) { + param->flags &= ~flags; + } + param = param->next; + } + device = device->next; + } + } + *buf_len = json_gen_str_end(&jstr); + return err; +} + +/* This function does not use the node_params_buf since this is for external use + * and we do not want __esp_rmaker_allocate_and_populate_params to overwrite + * the buffer. + */ +char *esp_rmaker_get_node_params(void) +{ + size_t req_size = 0; + /* Passing NULL pointer to find the required buffer size */ + esp_err_t err = esp_rmaker_populate_params(NULL, &req_size, 0, false); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to get required size for Node params JSON."); + return NULL; + } + /* Keeping some margin just in case some param value changes in between */ + req_size += RMAKER_PARAMS_SIZE_MARGIN; + char *node_params = MEM_CALLOC_EXTRAM(1, req_size); + if (!node_params) { + ESP_LOGE(TAG, "Failed to allocate %lu bytes for Node params.", (unsigned long) req_size); + return NULL; + } + err = esp_rmaker_populate_params(node_params, &req_size, 0, false); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to generate Node params JSON."); + free(node_params); + return NULL; + } + return node_params; +} + +static char * esp_rmaker_param_get_buf(size_t size) +{ + static char *s_node_params_buf; + static size_t s_param_buf_size; + /* If received size is 0, we will just return the pointer to the buffer */ + if (size == 0) { + return s_node_params_buf; + } + /* If s_param_buf_size is not 0, it means that a buffer was already allocated. + * If the requested "size" is not the same as the existing s_param_buf_size, + * we need to free existing buffer. + */ + if ((s_param_buf_size != 0) && (s_param_buf_size != size)) { + ESP_LOGD(TAG, "Freeing s_node_params_buf of size %lu", (unsigned long) s_param_buf_size); + free(s_node_params_buf); + s_node_params_buf = NULL; + } + if (!s_node_params_buf) { + ESP_LOGD(TAG, "Allocating s_node_params_buf for size %lu.", (unsigned long) size); + s_node_params_buf = MEM_CALLOC_EXTRAM(1, size); + if (!s_node_params_buf) { + ESP_LOGE(TAG, "Failed to allocate %lu bytes for Node params.", (unsigned long) size); + s_param_buf_size = 0; + return NULL; + } + s_param_buf_size = size; + } + return s_node_params_buf; +} + +static esp_err_t esp_rmaker_allocate_and_populate_params(uint8_t flags, bool reset_flags) +{ + char *node_params_buf = esp_rmaker_param_get_buf(max_node_params_size); + if (!node_params_buf) { + return ESP_ERR_NO_MEM; + } + /* Typically, max_node_params_size should be sufficient for the parameters */ + size_t req_size = max_node_params_size; + esp_err_t err = esp_rmaker_populate_params(node_params_buf, &req_size, flags, reset_flags); + /* If the max_node_params_size was insufficient, we will re-allocate new buffer */ + if (err == ESP_ERR_NO_MEM) { + ESP_LOGW(TAG, "%lu bytes not sufficient for Node params. Reallocating %lu bytes.", + (unsigned long) max_node_params_size, (unsigned long) req_size + RMAKER_PARAMS_SIZE_MARGIN); + max_node_params_size = req_size + RMAKER_PARAMS_SIZE_MARGIN; /* Keeping some margin since paramater value size can change */ + node_params_buf = esp_rmaker_param_get_buf(max_node_params_size); + if (!node_params_buf) { + return ESP_ERR_NO_MEM; + } + req_size = max_node_params_size; + err = esp_rmaker_populate_params(node_params_buf, &req_size, flags, reset_flags); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to populate node parameters."); + } + } + return err; +} + +static esp_err_t esp_rmaker_report_param_internal(uint8_t flags) +{ + esp_err_t err = esp_rmaker_allocate_and_populate_params(flags, true); + if (err == ESP_OK) { + /* Just checking if there are indeed any params to report by comparing with a decent enough + * length as even the smallest possible data, Eg. '{"d":{"p":0}}' will be > 10 bytes. + */ + char *node_params_buf = esp_rmaker_param_get_buf(0); + if (strlen(node_params_buf) > 10) { + if (flags == RMAKER_PARAM_FLAG_VALUE_CHANGE) { + esp_rmaker_create_mqtt_topic(publish_topic, sizeof(publish_topic), NODE_PARAMS_LOCAL_TOPIC_SUFFIX, NODE_PARAMS_LOCAL_TOPIC_RULE); + ESP_LOGI(TAG, "Reporting params: %s", node_params_buf); + } else if (flags == RMAKER_PARAM_FLAG_VALUE_NOTIFY) { + esp_rmaker_create_mqtt_topic(publish_topic, sizeof(publish_topic), NODE_PARAMS_ALERT_TOPIC_SUFFIX, NODE_PARAMS_ALERT_TOPIC_RULE); + ESP_LOGI(TAG, "Notifying params: %s", node_params_buf); + } else { + return ESP_FAIL; + } + if (esp_rmaker_params_mqtt_init_done) { + esp_rmaker_mqtt_publish(publish_topic, node_params_buf, strlen(node_params_buf), RMAKER_MQTT_QOS1, NULL); + } else { + ESP_LOGW(TAG, "Not reporting params since params mqtt not initialized yet."); + } + } + return ESP_OK; + } + return err; +} + +static esp_err_t esp_rmaker_report_updated_params(void) +{ + return esp_rmaker_report_param_internal(RMAKER_PARAM_FLAG_VALUE_CHANGE); +} + +static esp_err_t esp_rmaker_device_set_params(_esp_rmaker_device_t *device, jparse_ctx_t *jptr, esp_rmaker_req_src_t src) +{ + _esp_rmaker_param_t *param = device->params; + esp_rmaker_param_write_req_t *write_req = MEM_CALLOC_EXTRAM(device->param_count, sizeof(esp_rmaker_param_write_req_t)); + if (!write_req) { + ESP_LOGE(TAG, "Could not allocate memory for set params."); + return ESP_ERR_NO_MEM; + } + esp_err_t err = ESP_OK; + + uint8_t num_param = 0; + while (param) { + bool param_found = false; + switch(param->val.type) { + case RMAKER_VAL_TYPE_BOOLEAN: + if (json_obj_get_bool(jptr, param->name, &write_req[num_param].val.val.b) == 0) { + write_req[num_param].param = (esp_rmaker_param_t *)param; + write_req[num_param].val.type = RMAKER_VAL_TYPE_BOOLEAN; + param_found = true; + } + break; + case RMAKER_VAL_TYPE_INTEGER: + if (json_obj_get_int(jptr, param->name, &write_req[num_param].val.val.i) == 0) { + write_req[num_param].param = (esp_rmaker_param_t *)param; + write_req[num_param].val.type = RMAKER_VAL_TYPE_INTEGER; + param_found = true; + } + break; + case RMAKER_VAL_TYPE_FLOAT: + if (json_obj_get_float(jptr, param->name, &write_req[num_param].val.val.f) == 0) { + write_req[num_param].param = (esp_rmaker_param_t *)param; + write_req[num_param].val.type = RMAKER_VAL_TYPE_FLOAT; + param_found = true; + } + break; + case RMAKER_VAL_TYPE_STRING: { + int val_size = 0; + if (json_obj_get_strlen(jptr, param->name, &val_size) == 0) { + val_size++; /* For NULL termination */ + write_req[num_param].val.val.s = MEM_CALLOC_EXTRAM(1, val_size); + if (!write_req[num_param].val.val.s) { + err = ESP_ERR_NO_MEM; + goto set_params_free; + } + json_obj_get_string(jptr, param->name, write_req[num_param].val.val.s, val_size); + write_req[num_param].param = (esp_rmaker_param_t *)param; + write_req[num_param].val.type = RMAKER_VAL_TYPE_STRING; + param_found = true; + } + break; + } + case RMAKER_VAL_TYPE_OBJECT: { + int val_size = 0; + if (json_obj_get_object_strlen(jptr, param->name, &val_size) == 0) { + val_size++; /* For NULL termination */ + write_req[num_param].val.val.s = MEM_CALLOC_EXTRAM(1, val_size); + if (!write_req[num_param].val.val.s) { + err = ESP_ERR_NO_MEM; + goto set_params_free; + } + json_obj_get_object_str(jptr, param->name, write_req[num_param].val.val.s, val_size); + write_req[num_param].param = (esp_rmaker_param_t *)param; + write_req[num_param].val.type = RMAKER_VAL_TYPE_OBJECT; + param_found = true; + } + break; + } + case RMAKER_VAL_TYPE_ARRAY: { + int val_size = 0; + if (json_obj_get_array_strlen(jptr, param->name, &val_size) == 0) { + val_size++; /* For NULL termination */ + write_req[num_param].val.val.s = MEM_CALLOC_EXTRAM(1, val_size); + if (!write_req[num_param].val.val.s) { + err = ESP_ERR_NO_MEM; + goto set_params_free; + } + json_obj_get_array_str(jptr, param->name, write_req[num_param].val.val.s, val_size); + write_req[num_param].param = (esp_rmaker_param_t *)param; + write_req[num_param].val.type = RMAKER_VAL_TYPE_ARRAY; + param_found = true; + } + break; + } + default: + break; + } + if (param_found) { + num_param++; + } + param = param->next; + } + ESP_LOGI(TAG, "Found %d params in write request for %s", num_param, device->name); + if (device->bulk_write_cb) { + esp_rmaker_write_ctx_t ctx = { + .src = src, + }; + if (device->bulk_write_cb((esp_rmaker_device_t *)device, (const esp_rmaker_param_write_req_t *)write_req, + num_param, device->priv_data, &ctx) != ESP_OK) { + ESP_LOGE(TAG, "Remote update for device %s failed", device->name); + } else { + esp_rmaker_report_updated_params(); + } + } +set_params_free: + /* Free all values which are allocated on heap */ + for (int i = 0; i < num_param; i++) { + if ((write_req[i].val.type == RMAKER_VAL_TYPE_STRING) || (write_req[i].val.type == RMAKER_VAL_TYPE_OBJECT || + (write_req[i].val.type == RMAKER_VAL_TYPE_ARRAY))) { + if (write_req[i].val.val.s) { + free(write_req[i].val.val.s); + } + } + } + if (write_req) { + free(write_req); + } + return err; +} + +esp_err_t esp_rmaker_handle_set_params(char *data, size_t data_len, esp_rmaker_req_src_t src) +{ + ESP_LOGI(TAG, "Received params: %.*s", (int) data_len, data); + jparse_ctx_t jctx; + if (json_parse_start(&jctx, data, data_len) != 0) { + return ESP_FAIL; + } + _esp_rmaker_device_t *device = esp_rmaker_node_get_first_device(esp_rmaker_get_node()); + while (device) { + if (json_obj_get_object(&jctx, device->name) == 0) { + esp_rmaker_device_set_params(device, &jctx, src); + json_obj_leave_object(&jctx); + } + device = device->next; + } + json_parse_end(&jctx); + return ESP_OK; +} + +static void esp_rmaker_set_params_callback(const char *topic, void *payload, size_t payload_len, void *priv_data) +{ + esp_rmaker_handle_set_params((char *)payload, payload_len, ESP_RMAKER_REQ_SRC_CLOUD); +} + +static esp_err_t esp_rmaker_register_for_set_params(void) +{ + char subscribe_topic[MQTT_TOPIC_BUFFER_SIZE]; + snprintf(subscribe_topic, sizeof(subscribe_topic), "node/%s/%s", + esp_rmaker_get_node_id(), NODE_PARAMS_REMOTE_TOPIC_SUFFIX); + esp_err_t err = esp_rmaker_mqtt_subscribe(subscribe_topic, esp_rmaker_set_params_callback, RMAKER_MQTT_QOS1, NULL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to subscribe to %s. Error %d", subscribe_topic, err); + return ESP_FAIL; + } + return ESP_OK; +} + +esp_err_t esp_rmaker_param_get_stored_value(_esp_rmaker_param_t *param, esp_rmaker_param_val_t *val) +{ + if (!param || !param->parent || !val) { + return ESP_FAIL; + } + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, param->parent->name, NVS_READONLY, &handle); + if (err != ESP_OK) { + return err; + } + if ((param->val.type == RMAKER_VAL_TYPE_STRING) || (param->val.type == RMAKER_VAL_TYPE_OBJECT) || + (param->val.type == RMAKER_VAL_TYPE_ARRAY)) { + size_t len = 0; + if ((err = nvs_get_blob(handle, param->name, NULL, &len)) == ESP_OK) { + char *s_val = MEM_CALLOC_EXTRAM(1, len + 1); + if (!s_val) { + err = ESP_ERR_NO_MEM; + } else { + nvs_get_blob(handle, param->name, s_val, &len); + s_val[len] = '\0'; + val->type = param->val.type; + val->val.s = s_val; + } + } else if ((err = nvs_get_str(handle, param->name, NULL, &len)) == ESP_OK) { + /* In order to be compatible with the previous nvs_set_str() */ + char *s_val = MEM_CALLOC_EXTRAM(1, len); + if (!s_val) { + err = ESP_ERR_NO_MEM; + } else { + nvs_get_str(handle, param->name, s_val, &len); + val->type = param->val.type; + val->val.s = s_val; + } + } + } else { + size_t len = sizeof(esp_rmaker_param_val_t); + err = nvs_get_blob(handle, param->name, val, &len); + } + nvs_close(handle); + return err; +} + +esp_err_t esp_rmaker_param_store_value(_esp_rmaker_param_t *param) +{ + if (!param || !param->parent) { + return ESP_FAIL; + } + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, param->parent->name, NVS_READWRITE, &handle); + if (err != ESP_OK) { + return err; + } + if ((param->val.type == RMAKER_VAL_TYPE_STRING) || (param->val.type == RMAKER_VAL_TYPE_OBJECT) || + (param->val.type == RMAKER_VAL_TYPE_ARRAY)) { + /* Store only if value is not NULL */ + if (param->val.val.s) { + err = nvs_set_blob(handle, param->name, param->val.val.s, strlen(param->val.val.s)); + nvs_commit(handle); + } else { + err = ESP_OK; + } + } else { + err = nvs_set_blob(handle, param->name, ¶m->val, sizeof(esp_rmaker_param_val_t)); + nvs_commit(handle); + } + nvs_close(handle); + return err; +} + +esp_rmaker_param_val_t *esp_rmaker_param_get_val(esp_rmaker_param_t *param) +{ + if (!param) { + ESP_LOGE(TAG, "Param handle cannot be NULL."); + return NULL; + } + return &((_esp_rmaker_param_t *)param)->val; +} + +esp_err_t esp_rmaker_param_delete(const esp_rmaker_param_t *param) +{ + _esp_rmaker_param_t *_param = (_esp_rmaker_param_t *)param; + if (_param) { + if (_param->name) { + free(_param->name); + } + if (_param->type) { + free(_param->type); + } + if (_param->ui_type) { + free(_param->ui_type); + } + if ((_param->val.type == RMAKER_VAL_TYPE_STRING) || (_param->val.type == RMAKER_VAL_TYPE_OBJECT) || + (_param->val.type == RMAKER_VAL_TYPE_ARRAY)) { + if (_param->val.val.s) { + free(_param->val.val.s); + } + } + free(_param); + return ESP_OK; + } + return ESP_ERR_INVALID_ARG; +} + +esp_rmaker_param_t *esp_rmaker_param_create(const char *param_name, const char *type, + esp_rmaker_param_val_t val, uint8_t properties) +{ + if (!param_name) { + ESP_LOGE(TAG, "Param name is mandatory"); + return NULL; + } + if ((properties & PROP_FLAG_TIME_SERIES) && (properties & PROP_FLAG_SIMPLE_TIME_SERIES)) { + ESP_LOGE(TAG, "A paramater cannot have both, PROP_FLAG_TIME_SERIES and PROP_FLAG_SIMPLE_TIME_SERIES."); + return NULL; + } + if ((properties & PROP_FLAG_TIME_SERIES) || (properties & PROP_FLAG_SIMPLE_TIME_SERIES)) { + if ((val.type == RMAKER_VAL_TYPE_ARRAY) || (val.type == RMAKER_VAL_TYPE_OBJECT)) { + ESP_LOGE(TAG, "PROP_FLAG_TIME_SERIES/PROP_FLAG_SIMPLE_TIME_SERIES not allowed for array/object param types."); + return NULL; + } + } + _esp_rmaker_param_t *param = MEM_CALLOC_EXTRAM(1, sizeof(_esp_rmaker_param_t)); + if (!param) { + ESP_LOGE(TAG, "Failed to allocate memory for param %s", param_name); + return NULL; + } + param->name = strdup(param_name); + if (!param->name) { + ESP_LOGE(TAG, "Failed to allocate memory for name for param %s.", param_name); + goto param_create_err; + } + if (type) { + param->type = strdup(type); + if (!param->type) { + ESP_LOGE(TAG, "Failed to allocate memory for type for param %s.", param_name); + goto param_create_err; + } + } + param->val.type = val.type; + param->prop_flags = properties; + if ((val.type == RMAKER_VAL_TYPE_STRING) || (val.type == RMAKER_VAL_TYPE_OBJECT) || + (val.type == RMAKER_VAL_TYPE_ARRAY)) { + if (val.val.s) { + param->val.val.s = strdup(val.val.s); + if (!param->val.val.s) { + ESP_LOGE(TAG, "Failed to allocate memory for the value of param %s.", param_name); + } + } + } else { + param->val.val = val.val; + } + if ((properties & PROP_FLAG_TIME_SERIES) || (properties & PROP_FLAG_SIMPLE_TIME_SERIES)) { + /* Time series params will require time sync */ + esp_rmaker_time_sync_init(NULL); + } + return (esp_rmaker_param_t *)param; + +param_create_err: + esp_rmaker_param_delete((esp_rmaker_param_t *)param); + return NULL; +} + +esp_err_t esp_rmaker_param_add_bounds(const esp_rmaker_param_t *param, + esp_rmaker_param_val_t min, esp_rmaker_param_val_t max, esp_rmaker_param_val_t step) +{ + if (!param) { + ESP_LOGE(TAG, "Param handle cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_param_t *_param = (_esp_rmaker_param_t *)param; + if ((_param->val.type != RMAKER_VAL_TYPE_INTEGER) && (_param->val.type != RMAKER_VAL_TYPE_FLOAT)) { + ESP_LOGE(TAG, "Only integer and float params can have bounds."); + return ESP_ERR_INVALID_ARG; + } + if ((min.type != _param->val.type) || (max.type != _param->val.type) || (step.type != _param->val.type)) { + ESP_LOGE(TAG, "Cannot set bounds for %s because of value type mismatch.", _param->name); + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_param_bounds_t *bounds = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_param_bounds_t)); + if (!bounds) { + ESP_LOGE(TAG, "Failed to allocate memory for parameter bounds."); + return ESP_ERR_NO_MEM; + } + bounds->min = min; + bounds->max = max; + bounds->step = step; + if (_param->bounds) { + free(_param->bounds); + } + _param->bounds = bounds; + return ESP_OK; +} + +esp_err_t esp_rmaker_param_add_valid_str_list(const esp_rmaker_param_t *param, const char *strs[], uint8_t count) +{ + if (!param) { + ESP_LOGE(TAG, "Param handle cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_param_t *_param = (_esp_rmaker_param_t *)param; + if (_param->val.type != RMAKER_VAL_TYPE_STRING) { + ESP_LOGE(TAG, "Only string params can have valid strings array."); + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_param_valid_str_list_t *valid_str_list = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_param_valid_str_list_t)); + if (!valid_str_list) { + ESP_LOGE(TAG, "Failed to allocate memory for valid strings array."); + return ESP_ERR_NO_MEM; + } + valid_str_list->str_list = strs; + valid_str_list->str_list_cnt = count; + if (_param->valid_str_list) { + free(_param->valid_str_list); + } + _param->valid_str_list = valid_str_list; + return ESP_OK; +} + +esp_err_t esp_rmaker_param_add_array_max_count(const esp_rmaker_param_t *param, int count) +{ + if (!param) { + ESP_LOGE(TAG, "Param handle cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_param_t *_param = (_esp_rmaker_param_t *)param; + if (_param->val.type != RMAKER_VAL_TYPE_ARRAY) { + ESP_LOGE(TAG, "Only array params can have max count."); + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_param_bounds_t *bounds = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_param_bounds_t)); + if (!bounds) { + ESP_LOGE(TAG, "Failed to allocate memory for parameter bounds."); + return ESP_ERR_NO_MEM; + } + bounds->max = esp_rmaker_int(count); + if (_param->bounds) { + free(_param->bounds); + } + _param->bounds = bounds; + return ESP_OK; +} + +esp_err_t esp_rmaker_param_add_ui_type(const esp_rmaker_param_t *param, const char *ui_type) +{ + if (!param || !ui_type) { + ESP_LOGE(TAG, "Param handle or UI type cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_param_t *_param = (_esp_rmaker_param_t *)param; + if (_param->ui_type) { + free(_param->ui_type); + } + if ((_param->ui_type = strdup(ui_type)) != NULL ) { + return ESP_OK; + } else { + return ESP_ERR_NO_MEM; + } +} + +esp_err_t esp_rmaker_param_update(const esp_rmaker_param_t *param, esp_rmaker_param_val_t val) +{ + if (!param) { + ESP_LOGE(TAG, "Param handle cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_param_t *_param = (_esp_rmaker_param_t *)param; + if (_param->val.type != val.type) { + ESP_LOGE(TAG, "New param value type not same as the existing one."); + return ESP_ERR_INVALID_ARG; + } + switch (_param->val.type) { + case RMAKER_VAL_TYPE_STRING: + case RMAKER_VAL_TYPE_OBJECT: + case RMAKER_VAL_TYPE_ARRAY: { + char *new_val = NULL; + if (val.val.s) { + new_val = strdup(val.val.s); + if (!new_val) { + return ESP_FAIL; + } + } + if (_param->val.val.s) { + free(_param->val.val.s); + } + _param->val.val.s = new_val; + break; + } + case RMAKER_VAL_TYPE_BOOLEAN: + case RMAKER_VAL_TYPE_INTEGER: + case RMAKER_VAL_TYPE_FLOAT: + _param->val.val = val.val; + break; + default: + return ESP_ERR_INVALID_ARG; + } + _param->flags |= RMAKER_PARAM_FLAG_VALUE_CHANGE; + if (_param->prop_flags & PROP_FLAG_PERSIST) { + esp_rmaker_param_store_value(_param); + } + return ESP_OK; +} + +static esp_err_t __esp_rmaker_param_report_time_series_records(json_gen_str_t *jptr, const _esp_rmaker_param_t *param) +{ + json_gen_start_object(jptr); + time_t current_timestamp = 0; + time(¤t_timestamp); + json_gen_obj_set_int(jptr, "t", (int)current_timestamp); + esp_rmaker_report_value(¶m->val, "v", jptr); + json_gen_end_object(jptr); + return ESP_OK; +} + + +static esp_err_t __esp_rmaker_param_report_time_series(json_gen_str_t *jptr, const esp_rmaker_param_t *param) +{ + json_gen_start_object(jptr); + char param_name[MAX_TS_DATA_PARAM_NAME]; + if (!param) { + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_param_t *_param = (_esp_rmaker_param_t *)param; + _esp_rmaker_device_t *device = _param->parent; + if (!device) { + return ESP_FAIL; + } + snprintf(param_name, sizeof(param_name), "%s.%s", device->name, _param->name); + json_gen_obj_set_string(jptr, "name", param_name); + if (_param->type) { + json_gen_obj_set_string(jptr, "type", _param->type); + } + esp_rmaker_report_data_type( _param->val.type, "dt", jptr); + json_gen_push_array(jptr, "records"); + __esp_rmaker_param_report_time_series_records(jptr, _param); + json_gen_pop_array(jptr); + json_gen_end_object(jptr); + return ESP_OK; +} + +static esp_err_t esp_rmaker_param_report_time_series(const esp_rmaker_param_t *param) +{ + if (!param) { + ESP_LOGE(TAG, "Param handle cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + if (!((_esp_rmaker_param_t *)param)->parent) { + ESP_LOGE(TAG, "Param \"%s\" has not been added to any device.", ((_esp_rmaker_param_t *)param)->name); + return ESP_FAIL; + } + if (esp_rmaker_time_check() != true) { + ESP_LOGE(TAG, "Current time not yet available. Cannot report time series data."); + return ESP_ERR_INVALID_STATE; + } + /* node_params_buf will be NULL during the first publish */ + char * node_params_buf = esp_rmaker_param_get_buf(max_node_params_size); + if (!node_params_buf) { + return ESP_ERR_NO_MEM; + } + esp_err_t err; + json_gen_str_t jstr; + int buf_len = max_node_params_size; + json_gen_str_start(&jstr, node_params_buf, buf_len, NULL, NULL); + json_gen_start_object(&jstr); + json_gen_obj_set_string(&jstr, "ts_data_version", TS_DATA_VERSION); + json_gen_push_array(&jstr, "ts_data"); + if ((err = __esp_rmaker_param_report_time_series(&jstr, param)) != ESP_OK) { + return err; + } + json_gen_pop_array(&jstr); + json_gen_end_object(&jstr); + json_gen_str_end(&jstr); + esp_rmaker_create_mqtt_topic(publish_topic, sizeof(publish_topic), TIME_SERIES_DATA_TOPIC_SUFFIX, TIME_SERIES_DATA_TOPIC_RULE); + if (esp_rmaker_params_mqtt_init_done) { + _esp_rmaker_param_t *_param = (_esp_rmaker_param_t *)param; + _esp_rmaker_device_t *_device = _param->parent; + ESP_LOGI(TAG, "Reporting Time Series Data for %s.%s", _device->name, _param->name); + esp_rmaker_mqtt_publish(publish_topic, node_params_buf, strlen(node_params_buf), RMAKER_MQTT_QOS1, NULL); + } + return ESP_OK; +} + +static esp_err_t esp_rmaker_param_report_simple_time_series(const esp_rmaker_param_t *param) +{ + if (!param) { + ESP_LOGE(TAG, "Param handle cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + _esp_rmaker_param_t *_param = (_esp_rmaker_param_t *)param; + _esp_rmaker_device_t *_device = _param->parent; + if (!_device) { + ESP_LOGE(TAG, "Param \"%s\" has not been added to any device.", _param->name); + return ESP_FAIL; + } + if (esp_rmaker_time_check() != true) { + ESP_LOGE(TAG, "Current time not yet available. Cannot report time series data."); + return ESP_ERR_INVALID_STATE; + } + /* node_params_buf will be NULL during the first publish */ + char * node_params_buf = esp_rmaker_param_get_buf(max_node_params_size); + if (!node_params_buf) { + return ESP_ERR_NO_MEM; + } + + json_gen_str_t jstr; + int buf_len = max_node_params_size; + json_gen_str_start(&jstr, node_params_buf, buf_len, NULL, NULL); + json_gen_start_object(&jstr); + char param_name[MAX_TS_DATA_PARAM_NAME]; + snprintf(param_name, sizeof(param_name), "%s.%s", _device->name, _param->name); + json_gen_obj_set_string(&jstr, "name", param_name); + if (_param->type) { + json_gen_obj_set_string(&jstr, "type", _param->type); + } + esp_rmaker_report_data_type(_param->val.type, "dt", &jstr); + time_t current_timestamp = 0; + time(¤t_timestamp); + json_gen_obj_set_int(&jstr, "t", (int)current_timestamp); + esp_rmaker_report_value(&_param->val, "v", &jstr); + json_gen_end_object(&jstr); + json_gen_str_end(&jstr); + + esp_rmaker_create_mqtt_topic(publish_topic, sizeof(publish_topic), SIMPLE_TS_DATA_TOPIC_SUFFIX, SIMPLE_TS_DATA_TOPIC_RULE); + if (esp_rmaker_params_mqtt_init_done) { + _esp_rmaker_param_t *_param = (_esp_rmaker_param_t *)param; + _esp_rmaker_device_t *_device = _param->parent; + ESP_LOGI(TAG, "Reporting Simple Time Series Data for %s.%s", _device->name, _param->name); + esp_rmaker_mqtt_publish(publish_topic, node_params_buf, strlen(node_params_buf), RMAKER_MQTT_QOS1, NULL); + } + return ESP_OK; +} + +esp_err_t esp_rmaker_param_notify(const esp_rmaker_param_t *param) +{ + if (!param) { + ESP_LOGE(TAG, "Param handle cannot be NULL."); + return ESP_ERR_INVALID_ARG; + } + ((_esp_rmaker_param_t *)param)->flags |= (RMAKER_PARAM_FLAG_VALUE_CHANGE | RMAKER_PARAM_FLAG_VALUE_NOTIFY); + esp_err_t err = esp_rmaker_report_param_internal(RMAKER_PARAM_FLAG_VALUE_NOTIFY); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to report parameter"); + } + return esp_rmaker_report_param_internal(RMAKER_PARAM_FLAG_VALUE_CHANGE); +} + +esp_err_t esp_rmaker_param_update_and_report(const esp_rmaker_param_t *param, esp_rmaker_param_val_t val) +{ + esp_err_t err = esp_rmaker_param_update(param, val); + /** Report parameter only if the RainMaker has started */ + if ((err == ESP_OK) && (esp_rmaker_get_state() == ESP_RMAKER_STATE_STARTED)) { + if (((_esp_rmaker_param_t *)param)->prop_flags & PROP_FLAG_TIME_SERIES) { + esp_rmaker_param_report_time_series(param); + } else if (((_esp_rmaker_param_t *)param)->prop_flags & PROP_FLAG_SIMPLE_TIME_SERIES) { + esp_rmaker_param_report_simple_time_series(param); + } + err = esp_rmaker_report_updated_params(); + } + return err; +} + +esp_err_t esp_rmaker_param_update_and_notify(const esp_rmaker_param_t *param, esp_rmaker_param_val_t val) +{ + esp_err_t err = esp_rmaker_param_update(param, val); + /** Report parameter only if the RainMaker has started */ + if ((err == ESP_OK) && (esp_rmaker_get_state() == ESP_RMAKER_STATE_STARTED)) { + if (((_esp_rmaker_param_t *)param)->prop_flags & PROP_FLAG_TIME_SERIES) { + esp_rmaker_param_report_time_series(param); + } else if (((_esp_rmaker_param_t *)param)->prop_flags & PROP_FLAG_SIMPLE_TIME_SERIES) { + esp_rmaker_param_report_simple_time_series(param); + } + err = esp_rmaker_param_notify(param); + } + return err; +} + +static esp_err_t esp_rmaker_report_all_ts_params(void) +{ + _esp_rmaker_device_t *device = esp_rmaker_node_get_first_device(esp_rmaker_get_node()); + while (device) { + _esp_rmaker_param_t *param = device->params; + while (param) { + if (param->prop_flags & PROP_FLAG_TIME_SERIES) { + esp_rmaker_param_report_time_series((esp_rmaker_param_t *)param); + } else if (param->prop_flags & PROP_FLAG_SIMPLE_TIME_SERIES) { + esp_rmaker_param_report_simple_time_series((esp_rmaker_param_t *)param); + } + param = param->next; + } + device = device->next; + } + return ESP_OK; +} + + +esp_err_t esp_rmaker_report_node_state(void) +{ + esp_err_t err = esp_rmaker_allocate_and_populate_params(0, false); + if (err == ESP_OK) { + /* Just checking if there are indeed any params to report by comparing with a decent enough + * length as even the smallest possible data, Eg. '{"d":{"p":0}}' will be > 10 bytes. + */ + char *node_params_buf = esp_rmaker_param_get_buf(0); + if (strlen(node_params_buf) > 10) { + esp_rmaker_create_mqtt_topic(publish_topic, sizeof(publish_topic), NODE_PARAMS_LOCAL_INIT_TOPIC_SUFFIX, NODE_PARAMS_LOCAL_INIT_RULE); + ESP_LOGI(TAG, "Reporting params (init): %s", node_params_buf); + if (esp_rmaker_params_mqtt_init_done) { + esp_rmaker_mqtt_publish(publish_topic, node_params_buf, strlen(node_params_buf), RMAKER_MQTT_QOS1, NULL); + } else { + ESP_LOGW(TAG, "Not reporting params since params mqtt not initialized yet."); + } + } + /* Report all Time Series Params separately */ + return esp_rmaker_report_all_ts_params(); + } + return err; +} + +esp_err_t esp_rmaker_params_mqtt_init(void) +{ + /* Subscribe for parameter update requests */ + esp_err_t err = esp_rmaker_register_for_set_params(); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Params MQTT Init done."); + esp_rmaker_params_mqtt_init_done = true; + /* Report the current node state i.e. values of all the node parameters */ + esp_rmaker_report_node_state(); + } + return err; +} + +char *esp_rmaker_param_get_name(const esp_rmaker_param_t *param) +{ + if (!param) { + ESP_LOGE(TAG, "Param handle cannot be NULL."); + return NULL; + } + return ((_esp_rmaker_param_t *)param)->name; +} + +char *esp_rmaker_param_get_type(const esp_rmaker_param_t *param) +{ + if (!param) { + ESP_LOGE(TAG, "Param handle cannot be NULL."); + return NULL; + } + return ((_esp_rmaker_param_t *)param)->type; +} + +esp_err_t esp_rmaker_raise_alert(const char *alert_str) +{ + char msg[ESP_RMAKER_MAX_ALERT_LEN + 1]; /* + 1 for NULL terminattion */ + strlcpy(msg, alert_str, sizeof(msg)); + char buf[ESP_RMAKER_MAX_ALERT_LEN + RMAKER_ALERT_STR_MARGIN]; + snprintf(buf, sizeof(buf), "{\"%s\":\"%s\"}", ESP_RMAKER_ALERT_KEY, msg); + esp_rmaker_create_mqtt_topic(publish_topic, sizeof(publish_topic), NODE_PARAMS_ALERT_TOPIC_SUFFIX, NODE_PARAMS_ALERT_TOPIC_RULE); + ESP_LOGI(TAG, "Reporting alert: %s", buf); + return esp_rmaker_mqtt_publish(publish_topic, buf, strlen(buf), RMAKER_MQTT_QOS1, NULL); +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_scenes.c b/components/esp_rainmaker/src/core/esp_rmaker_scenes.c new file mode 100644 index 0000000..5441823 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_scenes.c @@ -0,0 +1,548 @@ +// Copyright 2022 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#define MAX_ID_LEN 8 +#define MAX_NAME_LEN 32 +#define MAX_INFO_LEN 100 +#define MAX_OPERATION_LEN 10 +#define MAX_SCENES CONFIG_ESP_RMAKER_SCENES_MAX_SCENES + +static const char *TAG = "esp_rmaker_scenes"; + +typedef struct esp_rmaker_scene_action { + void *data; + size_t data_len; +} esp_rmaker_scene_action_t; + +typedef struct esp_rmaker_scene { + char name[MAX_NAME_LEN + 1]; /* +1 for NULL termination */ + char id[MAX_ID_LEN + 1]; /* +1 for NULL termination */ + /* Info is used to store additional information, it is limited to MAX_INFO_LEN bytes. */ + char *info; + /* Flags can be used to identify the scene. */ + uint32_t flags; + esp_rmaker_scene_action_t action; + struct esp_rmaker_scene *next; +} esp_rmaker_scene_t; + +typedef enum scenes_operation { + OPERATION_INVALID, + OPERATION_ADD, + OPERATION_EDIT, + OPERATION_REMOVE, + OPERATION_ACTIVATE, + OPERATION_DEACTIVATE, +} scenes_operation_t; + +typedef struct { + esp_rmaker_scene_t *scenes_list; + int total_scenes; + bool deactivate_support; + esp_rmaker_device_t *scenes_service; +} esp_rmaker_scenes_priv_data_t; + +static esp_rmaker_scenes_priv_data_t *scenes_priv_data; + +static void esp_rmaker_scenes_free(esp_rmaker_scene_t *scene) +{ + if (!scene) { + return; + } + if (scene->action.data) { + free(scene->action.data); + } + if (scene->info) { + free(scene->info); + } + free(scene); +} + +static esp_rmaker_scene_t *esp_rmaker_scenes_get_scene_from_id(const char *id) +{ + if (!id) { + return NULL; + } + esp_rmaker_scene_t *scene = scenes_priv_data->scenes_list; + while(scene) { + if (strncmp(id, scene->id, sizeof(scene->id)) == 0) { + ESP_LOGD(TAG, "Scene with id %s found in list for get.", id); + return scene; + } + scene = scene->next; + } + ESP_LOGD(TAG, "Scene with id %s not found in list for get.", id); + return NULL; +} + +static esp_err_t esp_rmaker_scenes_add_to_list(esp_rmaker_scene_t *scene) +{ + if (!scene) { + ESP_LOGE(TAG, "Scene is NULL. Not adding to list."); + return ESP_ERR_INVALID_ARG; + } + + if (esp_rmaker_scenes_get_scene_from_id(scene->id) != NULL) { + ESP_LOGI(TAG, "Scene with id %s already added to list. Not adding again.", scene->id); + return ESP_FAIL; + } + /* Parse list */ + esp_rmaker_scene_t *prev_scene = scenes_priv_data->scenes_list; + while(prev_scene) { + if (prev_scene->next) { + prev_scene = prev_scene->next; + } else { + break; + } + } + + /* Add to list */ + if (prev_scene) { + prev_scene->next = scene; + } else { + scenes_priv_data->scenes_list = scene; + } + ESP_LOGD(TAG, "Scene with id %s added to list.", scene->id); + scenes_priv_data->total_scenes++; + return ESP_OK; +} + +static esp_err_t esp_rmaker_scenes_remove_from_list(esp_rmaker_scene_t *scene) +{ + if (!scene) { + ESP_LOGE(TAG, "Scene is NULL. Not removing from list."); + return ESP_ERR_INVALID_ARG; + } + /* Parse list */ + esp_rmaker_scene_t *curr_scene = scenes_priv_data->scenes_list; + esp_rmaker_scene_t *prev_scene = curr_scene; + while(curr_scene) { + if (strncmp(scene->id, curr_scene->id, sizeof(scene->id)) == 0) { + ESP_LOGD(TAG, "Scene with id %s found in list for removing", scene->id); + break; + } + prev_scene = curr_scene; + curr_scene = curr_scene->next; + } + if (!curr_scene) { + ESP_LOGE(TAG, "Scene with id %s not found in list. Not removing.", scene->id); + return ESP_ERR_NOT_FOUND; + } + + /* Remove from list */ + if (curr_scene == scenes_priv_data->scenes_list) { + scenes_priv_data->scenes_list = curr_scene->next; + } else { + prev_scene->next = curr_scene->next; + } + scenes_priv_data->total_scenes--; + ESP_LOGD(TAG, "Scene with id %s removed from list.", scene->id); + return ESP_OK; +} + +scenes_operation_t esp_rmaker_scenes_get_operation_from_str(char *operation) +{ + if (!operation) { + return OPERATION_INVALID; + } + if (strncmp(operation, "add", strlen(operation)) == 0) { + return OPERATION_ADD; + } else if (strncmp(operation, "edit", strlen(operation)) == 0) { + return OPERATION_EDIT; + } else if (strncmp(operation, "remove", strlen(operation)) == 0) { + return OPERATION_REMOVE; + } else if (strncmp(operation, "activate", strlen(operation)) == 0) { + return OPERATION_ACTIVATE; + } else if (strncmp(operation, "deactivate", strlen(operation)) == 0) { + return OPERATION_DEACTIVATE; + } + return OPERATION_INVALID; +} + +static scenes_operation_t esp_rmaker_scenes_parse_operation(jparse_ctx_t *jctx, char *id) +{ + char operation_str[MAX_OPERATION_LEN + 1] = {0}; /* +1 for NULL termination */ + scenes_operation_t operation = OPERATION_INVALID; + json_obj_get_string(jctx, "operation", operation_str, sizeof(operation_str)); + if (strlen(operation_str) <= 0) { + ESP_LOGE(TAG, "Operation not found in scene with id: %s", id); + return operation; + } + operation = esp_rmaker_scenes_get_operation_from_str(operation_str); + if (operation == OPERATION_EDIT) { + /* Get scene temporarily */ + if (esp_rmaker_scenes_get_scene_from_id(id) == NULL) { + /* Operation is edit, but scene not present already. Consider this as add. */ + ESP_LOGD(TAG, "Operation is edit, but scene with id %s not found. Changing the operation to add.", id); + operation = OPERATION_ADD; + } + } else if (operation == OPERATION_INVALID) { + ESP_LOGE(TAG, "Invalid scene operation found: %s", operation_str); + } + return operation; +} + +static esp_err_t esp_rmaker_scenes_parse_info_and_flags(jparse_ctx_t *jctx, char **info, uint32_t *flags) +{ + char _info[MAX_INFO_LEN + 1] = {0}; /* +1 for NULL termination */ + int _flags = 0; + + int err_code = json_obj_get_string(jctx, "info", _info, sizeof(_info)); + if (err_code == OS_SUCCESS) { + if (*info) { + free(*info); + *info = NULL; + } + + int len = strlen(_info); + if (len > 0) { + /* +1 for NULL termination */ + *info = (char *)MEM_CALLOC_EXTRAM(1, len + 1); + if (*info) { + memcpy(*info, _info, len + 1); + } + } + } + + err_code = json_obj_get_int(jctx, "flags", &_flags); + if (err_code == OS_SUCCESS) { + if (flags) { + *flags = _flags; + } + } + + return ESP_OK; +} + +static esp_err_t esp_rmaker_scenes_parse_action(jparse_ctx_t *jctx, esp_rmaker_scene_action_t *action) +{ + int data_len = 0; + json_obj_get_object_strlen(jctx, "action", &data_len); + if (data_len <= 0) { + ESP_LOGD(TAG, "Action not found in JSON"); + return ESP_OK; + } + action->data_len = data_len + 1; + + if (action->data) { + free(action->data); + } + action->data = (void *)MEM_CALLOC_EXTRAM(1, action->data_len); + if (!action->data) { + ESP_LOGE(TAG, "Could not allocate action"); + return ESP_ERR_NO_MEM; + } + json_obj_get_object_str(jctx, "action", action->data, action->data_len); + return ESP_OK; +} + +static esp_rmaker_scene_t *esp_rmaker_scenes_find_or_create(jparse_ctx_t *jctx, char *id, scenes_operation_t operation) +{ + char name[MAX_NAME_LEN + 1] = {0}; /* +1 for NULL termination */ + esp_rmaker_scene_t *scene = NULL; + if (operation == OPERATION_ADD) { + /* Checking if scene with same id already exists. */ + scene = esp_rmaker_scenes_get_scene_from_id(id); + if (scene) { + ESP_LOGE(TAG, "Scene with id %s already exists. Not adding it again.", id); + return NULL; + } + + /* Get name */ + json_obj_get_string(jctx, "name", name, sizeof(name)); + if (strlen(name) <= 0) { + ESP_LOGE(TAG, "Name not found for scene with id: %s", id); + return NULL; + } + + /* This is a new scene. Fill it. */ + scene = (esp_rmaker_scene_t *)MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_scene_t)); + if (!scene) { + ESP_LOGE(TAG, "Couldn't allocate scene with id: %s", id); + return NULL; + } + strlcpy(scene->id, id, sizeof(scene->id)); + strlcpy(scene->name, name, sizeof(scene->name)); + } else { + /* This scene should already be present */ + scene = esp_rmaker_scenes_get_scene_from_id(id); + if (!scene) { + ESP_LOGE(TAG, "Scene with id %s not found", id); + return NULL; + } + + /* Get name */ + if (operation == OPERATION_EDIT) { + json_obj_get_string(jctx, "name", name, sizeof(name)); + if (strlen(name) > 0) { + /* If there is name in the request, replace the name in the scene with this new one */ + memset(scene->name, 0, sizeof(scene->name)); + strlcpy(scene->name, name, sizeof(scene->name)); + } + } + } + return scene; +} + +static esp_err_t esp_rmaker_scenes_perform_operation(esp_rmaker_scene_t *scene, scenes_operation_t operation) +{ + esp_err_t err = ESP_OK; + switch (operation) { + case OPERATION_ADD: + if (scenes_priv_data->total_scenes < MAX_SCENES) { + err = esp_rmaker_scenes_add_to_list(scene); + } else { + ESP_LOGE(TAG, "Max sceness (%d) reached. Not adding this scene with id %s", MAX_SCENES, + scene->id); + err = ESP_FAIL; + } + break; + + case OPERATION_EDIT: + /* Nothing to do here. name, info, action have already been handled */ + break; + + case OPERATION_REMOVE: + err = esp_rmaker_scenes_remove_from_list(scene); + if (err == ESP_OK) { + esp_rmaker_scenes_free(scene); + } + break; + + case OPERATION_ACTIVATE: + err = esp_rmaker_handle_set_params(scene->action.data, scene->action.data_len, ESP_RMAKER_REQ_SRC_SCENE_ACTIVATE); + break; + + case OPERATION_DEACTIVATE: + if (scenes_priv_data->deactivate_support) { + err = esp_rmaker_handle_set_params(scene->action.data, scene->action.data_len, ESP_RMAKER_REQ_SRC_SCENE_DEACTIVATE); + } else { + ESP_LOGW(TAG, "Deactivate operation not supported."); + err = ESP_ERR_NOT_SUPPORTED; + } + break; + + default: + ESP_LOGE(TAG, "Invalid Operation: %d", operation); + err = ESP_FAIL; + break; + } + return err; +} + +static esp_err_t esp_rmaker_scenes_parse_json(void *data, size_t data_len, esp_rmaker_req_src_t src, + bool *report_params) +{ + char id[MAX_ID_LEN + 1] = {0}; /* +1 for NULL termination */ + scenes_operation_t operation = OPERATION_INVALID; + int current_scene = 0; + esp_rmaker_scene_t *scene = NULL; + + /* Get details from JSON */ + jparse_ctx_t jctx; + if (json_parse_start(&jctx, (char *)data, data_len) != 0) { + ESP_LOGE(TAG, "Json parse start failed"); + return ESP_FAIL; + } + + /* Parse all scenes */ + while(json_arr_get_object(&jctx, current_scene) == 0) { + /* Get ID */ + json_obj_get_string(&jctx, "id", id, sizeof(id)); + if (strlen(id) <= 0) { + ESP_LOGE(TAG, "ID not found in scene JSON"); + goto cleanup; + } + + /* Get operation */ + if (src == ESP_RMAKER_REQ_SRC_INIT) { + /* Scene loaded from NVS. Add it */ + operation = OPERATION_ADD; + } else { + operation = esp_rmaker_scenes_parse_operation(&jctx, id); + if (operation == OPERATION_INVALID) { + ESP_LOGE(TAG, "Error getting operation"); + goto cleanup; + } + } + + /* Find/Create new scene */ + scene = esp_rmaker_scenes_find_or_create(&jctx, id, operation); + if (!scene) { + goto cleanup; + } + + /* Get other scene details */ + if (operation == OPERATION_ADD || operation == OPERATION_EDIT) { + /* Get info and flags */ + esp_rmaker_scenes_parse_info_and_flags(&jctx, &scene->info, &scene->flags); + + /* Get action */ + esp_rmaker_scenes_parse_action(&jctx, &scene->action); + } + + /* Set report_params */ + if (operation == OPERATION_ADD || operation == OPERATION_EDIT || operation == OPERATION_REMOVE) { + *report_params = true; + } else { + *report_params = false; + } + + /* Perform operation */ + esp_rmaker_scenes_perform_operation(scene, operation); + +cleanup: + json_arr_leave_object(&jctx); + current_scene++; + } + json_parse_end(&jctx); + return ESP_OK; +} + +static esp_err_t __esp_rmaker_scenes_get_params(char *buf, size_t *buf_size) +{ + esp_err_t err = ESP_OK; + esp_rmaker_scene_t *scene = scenes_priv_data->scenes_list; + json_gen_str_t jstr; + json_gen_str_start(&jstr, buf, *buf_size, NULL, NULL); + json_gen_start_array(&jstr); + + while (scene) { + json_gen_start_object(&jstr); + + /* Add details */ + json_gen_obj_set_string(&jstr, "name", scene->name); + json_gen_obj_set_string(&jstr, "id", scene->id); + /* If info and flags is not zero, add it. */ + if (scene->info != NULL) { + json_gen_obj_set_string(&jstr, "info", scene->info); + } + if (scene->flags != 0) { + json_gen_obj_set_int(&jstr, "flags", scene->flags); + } + + /* Add action */ + json_gen_push_object_str(&jstr, "action", scene->action.data); + + json_gen_end_object(&jstr); + + /* Go to next scene */ + scene = scene->next; + } + + if (json_gen_end_array(&jstr) < 0) { + ESP_LOGE(TAG, "Buffer size %lu not sufficient for reporting Scenes Params.", (unsigned long) *buf_size); + err = ESP_ERR_NO_MEM; + } + *buf_size = json_gen_str_end(&jstr); + return err; +} + +static char *esp_rmaker_scenes_get_params(void) +{ + size_t req_size = 0; + esp_err_t err = __esp_rmaker_scenes_get_params(NULL, &req_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to get required size for scenes JSON."); + return NULL; + } + char *data = MEM_CALLOC_EXTRAM(1, req_size); + if (!data) { + ESP_LOGE(TAG, "Failed to allocate %lu bytes for scenes.", (unsigned long) req_size); + return NULL; + } + err = __esp_rmaker_scenes_get_params(data, &req_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error occured while trying to populate sceness JSON."); + free(data); + return NULL; + } + return data; +} + +static esp_err_t esp_rmaker_scenes_report_params(void) +{ + char *data = esp_rmaker_scenes_get_params(); + esp_rmaker_param_val_t val = { + .type = RMAKER_VAL_TYPE_ARRAY, + .val.s = data, + }; + esp_rmaker_param_t *param = esp_rmaker_device_get_param_by_type(scenes_priv_data->scenes_service, ESP_RMAKER_PARAM_SCENES); + esp_rmaker_param_update_and_report(param, val); + + free(data); + return ESP_OK; +} + +static esp_err_t write_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx) +{ + if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_SCENES) != 0) { + ESP_LOGE(TAG, "Got callback for invalid param with name %s and type %s", esp_rmaker_param_get_name(param), esp_rmaker_param_get_type(param)); + return ESP_ERR_INVALID_ARG; + } + if (strlen(val.val.s) <= 0) { + ESP_LOGI(TAG, "Invalid length for params: %lu", (unsigned long) strlen(val.val.s)); + return ESP_ERR_INVALID_ARG; + } + bool report_params = false; + esp_rmaker_scenes_parse_json(val.val.s, strlen(val.val.s), ctx->src, &report_params); + if (ctx->src != ESP_RMAKER_REQ_SRC_INIT) { + /* Since this is a persisting param, we get a write_cb while booting up. We need not report the param when the source is 'init' as this will get reported when the device first reports all the params. */ + if (report_params) { + /* report_params is only set for add, edit, remove operations. The scenes params are not changed for + activate, deactivate operations. So need to report the params in that case. */ + esp_rmaker_scenes_report_params(); + } + } + return ESP_OK; +} + +esp_err_t esp_rmaker_scenes_enable(void) +{ + scenes_priv_data = (esp_rmaker_scenes_priv_data_t *)MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_scenes_priv_data_t)); + if (!scenes_priv_data) { + ESP_LOGE(TAG, "Couldn't allocate scenes_priv_data"); + return ESP_ERR_NO_MEM; + } + +#ifdef CONFIG_ESP_RMAKER_SCENES_DEACTIVATE_SUPPORT + scenes_priv_data->deactivate_support = CONFIG_ESP_RMAKER_SCENES_DEACTIVATE_SUPPORT; +#endif + + scenes_priv_data->scenes_service = esp_rmaker_create_scenes_service("Scenes", write_cb, NULL, MAX_SCENES, scenes_priv_data->deactivate_support, NULL); + if (!scenes_priv_data->scenes_service) { + ESP_LOGE(TAG, "Failed to create Scenes Service"); + return ESP_FAIL; + } + + esp_err_t err = esp_rmaker_node_add_device(esp_rmaker_get_node(), scenes_priv_data->scenes_service); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to add Scenes Service"); + return err; + } + ESP_LOGD(TAG, "Scenes Service Enabled"); + return err; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_schedule.c b/components/esp_rainmaker/src/core/esp_rmaker_schedule.c new file mode 100644 index 0000000..d141dff --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_schedule.c @@ -0,0 +1,983 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_ID_LEN 8 +#define MAX_NAME_LEN 32 +#define MAX_INFO_LEN 128 +#define MAX_OPERATION_LEN 10 +#define TIME_SYNC_DELAY 10 /* 10 seconds */ +#define MAX_SCHEDULES CONFIG_ESP_RMAKER_SCHEDULING_MAX_SCHEDULES + +static const char *TAG = "esp_rmaker_schedule"; + +typedef enum trigger_type { + TRIGGER_TYPE_INVALID = 0, + TRIGGER_TYPE_DAYS_OF_WEEK, + TRIGGER_TYPE_DATE, + TRIGGER_TYPE_RELATIVE, +} trigger_type_t; + +typedef struct esp_rmaker_schedule_trigger { + trigger_type_t type; + /* Relative Seconds */ + int relative_seconds; + /* Minutes from 12am */ + uint16_t minutes; + struct { + /* 'OR' list of days or the week. Eg. Monday = 0b1, Tuesday = 0b10 */ + uint8_t repeat_days; + } day; + struct { + /* Day of the month */ + uint8_t day; + /* 'OR' list of months of the year. Eg. January = 0b1, February = 0b10. + 0 for next date (either this month or next). */ + uint16_t repeat_months; + uint16_t year; + bool repeat_every_year; + } date; + /* Used for non repeating schedules */ + int64_t next_timestamp; +} esp_rmaker_schedule_trigger_t; + +typedef struct esp_rmaker_schedule_action { + void *data; + size_t data_len; +} esp_rmaker_schedule_action_t; + +typedef struct esp_rmaker_schedule { + char name[MAX_NAME_LEN + 1]; /* +1 for NULL termination */ + char id[MAX_ID_LEN + 1]; /* +1 for NULL termination */ + /* Info is used to store additional information, it is limited to 128 bytes. */ + char *info; + /* Index is used in the callback to get back the schedule. */ + long index; + /* Flags are used to identify the schedule. Eg. timing, countdown */ + uint32_t flags; + bool enabled; + esp_schedule_handle_t handle; + esp_rmaker_schedule_action_t action; + esp_rmaker_schedule_trigger_t trigger; + esp_schedule_validity_t validity; + struct esp_rmaker_schedule *next; +} esp_rmaker_schedule_t; + +enum time_sync_state { + TIME_SYNC_NOT_STARTED, + TIME_SYNC_STARTED, + TIME_SYNC_DONE, +}; + +typedef enum schedule_operation { + OPERATION_INVALID, + OPERATION_ADD, + OPERATION_EDIT, + OPERATION_REMOVE, + OPERATION_ENABLE, + OPERATION_DISABLE, +} schedule_operation_t; + +typedef struct { + esp_rmaker_schedule_t *schedule_list; + int total_schedules; + /* This index just increases. This makes sure it is unique for the given schedules */ + int32_t index; + esp_rmaker_device_t *schedule_service; + TimerHandle_t time_sync_timer; + enum time_sync_state time_sync_state;; +} esp_rmaker_schedule_priv_data_t; + +static esp_rmaker_schedule_priv_data_t *schedule_priv_data; + +static esp_err_t esp_rmaker_schedule_operation_enable(esp_rmaker_schedule_t *schedule); +static esp_err_t esp_rmaker_schedule_operation_disable(esp_rmaker_schedule_t *schedule); +static esp_err_t esp_rmaker_schedule_report_params(void); +static esp_err_t esp_rmaker_schedule_timesync_timer_deinit(void); +static esp_err_t esp_rmaker_schedule_timesync_timer_start(void); + +static void esp_rmaker_schedule_free(esp_rmaker_schedule_t *schedule) +{ + if (!schedule) { + return; + } + if (schedule->action.data) { + free(schedule->action.data); + } + if (schedule->info) { + free(schedule->info); + } + free(schedule); +} + +static esp_rmaker_schedule_t *esp_rmaker_schedule_get_schedule_from_id(const char *id) +{ + if (!id) { + return NULL; + } + esp_rmaker_schedule_t *schedule = schedule_priv_data->schedule_list; + while(schedule) { + if (strncmp(id, schedule->id, sizeof(schedule->id)) == 0) { + ESP_LOGD(TAG, "Schedule with id %s found in list for get.", id); + return schedule; + } + schedule = schedule->next; + } + ESP_LOGD(TAG, "Schedule with id %s not found in list for get.", id); + return NULL; +} + +static esp_rmaker_schedule_t *esp_rmaker_schedule_get_schedule_from_index(long index) +{ + esp_rmaker_schedule_t *schedule = schedule_priv_data->schedule_list; + while(schedule) { + if (schedule->index == index) { + ESP_LOGD(TAG, "Schedule with index %ld found in list for get.", index); + return schedule; + } + schedule = schedule->next; + } + ESP_LOGD(TAG, "Schedule with index %ld not found in list for get.", index); + return NULL; +} + +static esp_err_t esp_rmaker_schedule_add_to_list(esp_rmaker_schedule_t *schedule) +{ + if (!schedule) { + ESP_LOGE(TAG, "Schedule is NULL. Not adding to list."); + return ESP_ERR_INVALID_ARG; + } + + if (esp_rmaker_schedule_get_schedule_from_id(schedule->id) != NULL) { + ESP_LOGI(TAG, "Schedule with id %s already added to list. Not adding again.", schedule->id); + return ESP_FAIL; + } + /* Parse list */ + esp_rmaker_schedule_t *prev_schedule = schedule_priv_data->schedule_list; + while(prev_schedule) { + if (prev_schedule->next) { + prev_schedule = prev_schedule->next; + } else { + break; + } + } + + /* Add to list */ + if (prev_schedule) { + prev_schedule->next = schedule; + } else { + schedule_priv_data->schedule_list = schedule; + } + ESP_LOGD(TAG, "Schedule with id %s added to list.", schedule->id); + schedule_priv_data->total_schedules++; + return ESP_OK; +} + +static esp_err_t esp_rmaker_schedule_remove_from_list(esp_rmaker_schedule_t *schedule) +{ + if (!schedule) { + ESP_LOGE(TAG, "Schedule is NULL. Not removing from list."); + return ESP_ERR_INVALID_ARG; + } + /* Parse list */ + esp_rmaker_schedule_t *curr_schedule = schedule_priv_data->schedule_list; + esp_rmaker_schedule_t *prev_schedule = curr_schedule; + while(curr_schedule) { + if (strncmp(schedule->id, curr_schedule->id, sizeof(schedule->id)) == 0) { + ESP_LOGD(TAG, "Schedule with id %s found in list for removing", schedule->id); + break; + } + prev_schedule = curr_schedule; + curr_schedule = curr_schedule->next; + } + if (!curr_schedule) { + ESP_LOGE(TAG, "Schedule with id %s not found in list. Not removing.", schedule->id); + return ESP_ERR_NOT_FOUND; + } + + /* Remove from list */ + if (curr_schedule == schedule_priv_data->schedule_list) { + schedule_priv_data->schedule_list = curr_schedule->next; + } else { + prev_schedule->next = curr_schedule->next; + } + schedule_priv_data->total_schedules--; + ESP_LOGD(TAG, "Schedule with id %s removed from list.", schedule->id); + return ESP_OK; +} + +static bool esp_rmaker_schedule_is_expired(esp_rmaker_schedule_t *schedule) +{ + time_t current_timestamp = 0; + struct tm current_time = {0}; + time(¤t_timestamp); + localtime_r(¤t_timestamp, ¤t_time); + + if (schedule->trigger.type == TRIGGER_TYPE_RELATIVE) { + if (schedule->trigger.next_timestamp > 0 && schedule->trigger.next_timestamp <= current_timestamp) { + /* Relative seconds based schedule has expired */ + return true; + } + } else if (schedule->trigger.type == TRIGGER_TYPE_DAYS_OF_WEEK) { + if (schedule->trigger.day.repeat_days == 0) { + if (schedule->trigger.next_timestamp > 0 && schedule->trigger.next_timestamp <= current_timestamp) { + /* One time schedule has expired */ + return true; + } + } + } else if (schedule->trigger.type == TRIGGER_TYPE_DATE) { + if (schedule->trigger.date.repeat_months == 0) { + if (schedule->trigger.next_timestamp > 0 && schedule->trigger.next_timestamp <= current_timestamp) { + /* One time schedule has expired */ + return true; + } else { + return false; + } + } + if (schedule->trigger.date.repeat_every_year == true) { + return false; + } + + struct tm schedule_time = {0}; + localtime_r(¤t_timestamp, &schedule_time); + schedule_time.tm_sec = 0; + schedule_time.tm_min = schedule->trigger.minutes; + schedule_time.tm_hour = 0; + schedule_time.tm_mday = schedule->trigger.date.day; + /* For expiry, just check the last month of the repeat_months. */ + /* '-1' because struct tm has months starting from 0 and we have months starting from 1. */ + schedule_time.tm_mon = fls(schedule->trigger.date.repeat_months) - 1; + /* '-1900' because struct tm has number of years after 1900 */ + schedule_time.tm_year = schedule->trigger.date.year - 1900; + time_t schedule_timestamp = mktime(&schedule_time); + + if (schedule_timestamp < current_timestamp) { + return true; + } + } + return false; +} + +static esp_err_t esp_rmaker_schedule_process_action(esp_rmaker_schedule_action_t *action) +{ + return esp_rmaker_handle_set_params(action->data, action->data_len, ESP_RMAKER_REQ_SRC_SCHEDULE); +} + +static void esp_rmaker_schedule_trigger_work_cb(void *priv_data) +{ + long index = (long)priv_data; + esp_rmaker_schedule_t *schedule = esp_rmaker_schedule_get_schedule_from_index(index); + if (!schedule) { + ESP_LOGE(TAG, "Schedule with index %ld not found for trigger work callback", index); + return; + } + esp_rmaker_schedule_process_action(&schedule->action); + if (esp_rmaker_schedule_is_expired(schedule)) { + /* This schedule does not repeat anymore. Disable it and report the params. */ + esp_rmaker_schedule_operation_disable(schedule); + esp_rmaker_schedule_report_params(); + } +} + +static void esp_rmaker_schedule_trigger_common_cb(esp_schedule_handle_t handle, void *priv_data) +{ + /* Adding to work queue to change the context from timer's task. */ + esp_rmaker_work_queue_add_task(esp_rmaker_schedule_trigger_work_cb, priv_data); +} + +static void esp_rmaker_schedule_timestamp_common_cb(esp_schedule_handle_t handle, uint32_t next_timestamp, void *priv_data) +{ + long index = (long)priv_data; + esp_rmaker_schedule_t *schedule = esp_rmaker_schedule_get_schedule_from_index(index); + if (!schedule) { + ESP_LOGE(TAG, "Schedule with index %ld not found for timestamp callback", index); + return; + } + schedule->trigger.next_timestamp = next_timestamp; +} + +static esp_err_t esp_rmaker_schedule_prepare_config(esp_rmaker_schedule_t *schedule, esp_schedule_config_t *schedule_config) +{ + if (!schedule || !schedule_config) { + ESP_LOGE(TAG, "schedule or schedule_config is NULL."); + return ESP_ERR_INVALID_ARG; + } + + if (schedule->trigger.type == TRIGGER_TYPE_RELATIVE) { + schedule_config->trigger.next_scheduled_time_utc = (time_t)schedule->trigger.next_timestamp; + schedule_config->trigger.relative_seconds = schedule->trigger.relative_seconds; + schedule_config->trigger.type = ESP_SCHEDULE_TYPE_RELATIVE; + schedule_config->trigger_cb = esp_rmaker_schedule_trigger_common_cb; + schedule_config->timestamp_cb = esp_rmaker_schedule_timestamp_common_cb; + } else { + int hours = schedule->trigger.minutes / 60; + int minutes = schedule->trigger.minutes % 60; + schedule_config->trigger.hours = hours; + schedule_config->trigger.minutes = minutes; + schedule_config->trigger_cb = esp_rmaker_schedule_trigger_common_cb; + + if (schedule->trigger.type == TRIGGER_TYPE_DAYS_OF_WEEK) { + schedule_config->trigger.type = ESP_SCHEDULE_TYPE_DAYS_OF_WEEK; + schedule_config->trigger.day.repeat_days = schedule->trigger.day.repeat_days; + if (schedule->trigger.day.repeat_days == 0) { + schedule_config->timestamp_cb = esp_rmaker_schedule_timestamp_common_cb; + } + } else if (schedule->trigger.type == TRIGGER_TYPE_DATE) { + schedule_config->trigger.type = ESP_SCHEDULE_TYPE_DATE; + schedule_config->trigger.date.day = schedule->trigger.date.day; + schedule_config->trigger.date.repeat_months = schedule->trigger.date.repeat_months; + schedule_config->trigger.date.year = schedule->trigger.date.year; + schedule_config->trigger.date.repeat_every_year = schedule->trigger.date.repeat_every_year; + if (schedule->trigger.date.repeat_months == 0) { + schedule_config->timestamp_cb = esp_rmaker_schedule_timestamp_common_cb; + } + } + } + + /* In esp_schedule, name should be unique and is used as the primary key. + We are setting the id in esp_rmaker_schedule as the name in esp_schedule */ + strlcpy(schedule_config->name, schedule->id, sizeof(schedule_config->name)); + /* Just passing the schedule pointer as priv_data could create a race condition between the schedule getting a + callback and the schedule getting removed. Using this unique index as the priv_data solves it to some extent. */ + schedule_config->priv_data = (void *)schedule->index; + schedule_config->validity = schedule->validity; + return ESP_OK; +} + +static esp_err_t esp_rmaker_schedule_add(esp_rmaker_schedule_t *schedule) +{ + esp_schedule_config_t schedule_config = {0}; + esp_rmaker_schedule_prepare_config(schedule, &schedule_config); + + schedule->handle = esp_schedule_create(&schedule_config); + if (schedule->handle == NULL) { + return ESP_FAIL; + } + return ESP_OK; +} + +static esp_err_t esp_rmaker_schedule_operation_add(esp_rmaker_schedule_t *schedule) +{ + esp_err_t ret = esp_rmaker_schedule_add(schedule); + if (ret != ESP_OK) { + return ret; + } + ret = esp_rmaker_schedule_add_to_list(schedule); + return ret; +} + +static esp_err_t esp_rmaker_schedule_operation_edit(esp_rmaker_schedule_t *schedule) +{ + esp_schedule_config_t schedule_config = {0}; + esp_rmaker_schedule_prepare_config(schedule, &schedule_config); + + esp_err_t ret = esp_schedule_edit(schedule->handle, &schedule_config); + if (schedule->enabled == true) { + /* If the schedule is already enabled, disable it and enable it again so that the new changes after the + edit are reflected. */ + esp_rmaker_schedule_operation_disable(schedule); + esp_rmaker_schedule_operation_enable(schedule); + } + return ret; +} + +static esp_err_t esp_rmaker_schedule_remove(esp_rmaker_schedule_t *schedule) +{ + return esp_schedule_delete(schedule->handle); +} + +static esp_err_t esp_rmaker_schedule_operation_remove(esp_rmaker_schedule_t *schedule) +{ + esp_err_t ret = esp_rmaker_schedule_remove_from_list(schedule); + if (ret != ESP_OK) { + return ret; + } + ret = esp_rmaker_schedule_remove(schedule); + return ret; +} + +static void esp_rmaker_schedule_timesync_timer_work_cb(void *priv_data) +{ + if (esp_rmaker_time_check() != true) { + esp_rmaker_schedule_timesync_timer_start(); + return; + } + esp_rmaker_schedule_timesync_timer_deinit(); + schedule_priv_data->time_sync_state = TIME_SYNC_DONE; + ESP_LOGI(TAG, "Time is synchronised now. Enabling the schedules."); + esp_rmaker_schedule_t *schedule = schedule_priv_data->schedule_list; + while (schedule) { + if (schedule->enabled == true) { + esp_rmaker_schedule_operation_enable(schedule); + } + schedule = schedule->next; + } +} + +static void esp_rmaker_schedule_timesync_timer_cb(TimerHandle_t timer) +{ + esp_rmaker_work_queue_add_task(esp_rmaker_schedule_timesync_timer_work_cb, NULL); +} + +static esp_err_t esp_rmaker_schedule_timesync_timer_init(void) +{ + schedule_priv_data->time_sync_timer = xTimerCreate("esp_rmaker_schedule", (TIME_SYNC_DELAY * 1000) / portTICK_PERIOD_MS, pdFALSE, NULL, esp_rmaker_schedule_timesync_timer_cb); + if (schedule_priv_data->time_sync_timer == NULL) { + ESP_LOGE(TAG, "Failed to create timer for time sync"); + return ESP_FAIL; + } + return ESP_OK; +} + +static esp_err_t esp_rmaker_schedule_timesync_timer_deinit(void) +{ + xTimerDelete(schedule_priv_data->time_sync_timer, portMAX_DELAY); + schedule_priv_data->time_sync_timer = NULL; + return ESP_OK; +} + +static esp_err_t esp_rmaker_schedule_timesync_timer_start(void) +{ + xTimerStart(schedule_priv_data->time_sync_timer, portMAX_DELAY); + return ESP_OK; +} + +static esp_err_t esp_rmaker_schedule_operation_enable(esp_rmaker_schedule_t *schedule) +{ + /* Setting enabled to true even if time is not synced yet. This reports the correct enabled state when reporting the schedules.*/ + schedule->enabled = true; + + /* Check for time sync */ + if (schedule_priv_data->time_sync_state == TIME_SYNC_NOT_STARTED) { + if (esp_rmaker_time_check() != true) { + ESP_LOGI(TAG, "Time is not synchronised yet. The schedule will actually be enabled when time is synchronised. This may take time."); + esp_rmaker_schedule_timesync_timer_init(); + esp_rmaker_schedule_timesync_timer_start(); + schedule_priv_data->time_sync_state = TIME_SYNC_STARTED; + return ESP_FAIL; + } + schedule_priv_data->time_sync_state = TIME_SYNC_DONE; + } else if (schedule_priv_data->time_sync_state == TIME_SYNC_STARTED) { + return ESP_FAIL; + } + + if (esp_rmaker_schedule_is_expired(schedule)) { + /* This schedule does not repeat anymore. Disable it. */ + /* While time sync is happening, it might be possible that this schedule will be shown as enabled, but actually it is disabled. */ + ESP_LOGI(TAG, "Schedule with id %s does not repeat anymore. Disabling it.", schedule->id); + esp_rmaker_schedule_operation_disable(schedule); + /* Since the enabled state has been changed, report this */ + esp_rmaker_schedule_report_params(); + return ESP_OK; + } + + /* Time is synced. Enable the schedule */ + return esp_schedule_enable(schedule->handle); +} + +static esp_err_t esp_rmaker_schedule_operation_disable(esp_rmaker_schedule_t *schedule) +{ + esp_err_t ret = esp_schedule_disable(schedule->handle); + schedule->trigger.next_timestamp = 0; + schedule->enabled = false; + return ret; +} + +schedule_operation_t esp_rmaker_schedule_get_operation_from_str(char *operation) +{ + if (!operation) { + return OPERATION_INVALID; + } + if (strncmp(operation, "add", strlen(operation)) == 0) { + return OPERATION_ADD; + } else if (strncmp(operation, "edit", strlen(operation)) == 0) { + return OPERATION_EDIT; + } else if (strncmp(operation, "remove", strlen(operation)) == 0) { + return OPERATION_REMOVE; + } else if (strncmp(operation, "enable", strlen(operation)) == 0) { + return OPERATION_ENABLE; + } else if (strncmp(operation, "disable", strlen(operation)) == 0) { + return OPERATION_DISABLE; + } + return OPERATION_INVALID; +} + +static schedule_operation_t esp_rmaker_schedule_parse_operation(jparse_ctx_t *jctx, char *id) +{ + char operation_str[MAX_OPERATION_LEN + 1] = {0}; /* +1 for NULL termination */ + schedule_operation_t operation = OPERATION_INVALID; + json_obj_get_string(jctx, "operation", operation_str, sizeof(operation_str)); + if (strlen(operation_str) <= 0) { + ESP_LOGE(TAG, "Operation not found in schedule with id: %s", id); + return operation; + } + operation = esp_rmaker_schedule_get_operation_from_str(operation_str); + if (operation == OPERATION_EDIT) { + /* Get schedule temporarily */ + if (esp_rmaker_schedule_get_schedule_from_id(id) == NULL) { + /* Operation is edit, but schedule not present already. Consider this as add. */ + ESP_LOGD(TAG, "Operation is edit, but schedule with id %s not found. Changing the operation to add.", id); + operation = OPERATION_ADD; + } + } else if (operation == OPERATION_INVALID) { + ESP_LOGE(TAG, "Invalid schedule operation found: %s", operation_str); + } + return operation; +} + +static esp_err_t esp_rmaker_schedule_parse_action(jparse_ctx_t *jctx, esp_rmaker_schedule_action_t *action) +{ + int data_len = 0; + json_obj_get_object_strlen(jctx, "action", &data_len); + if (data_len <= 0) { + ESP_LOGD(TAG, "Action not found in JSON"); + return ESP_OK; + } + action->data_len = data_len + 1; + + if (action->data) { + free(action->data); + } + action->data = (void *)MEM_CALLOC_EXTRAM(1, action->data_len); + if (!action->data) { + ESP_LOGE(TAG, "Could not allocate action"); + return ESP_ERR_NO_MEM; + } + json_obj_get_object_str(jctx, "action", action->data, action->data_len); + return ESP_OK; +} + +static esp_err_t esp_rmaker_schedule_parse_trigger(jparse_ctx_t *jctx, esp_rmaker_schedule_trigger_t *trigger) +{ + int total_triggers = 0; + int relative_seconds = 0, minutes = 0, repeat_days = 0, day = 0, repeat_months = 0, year = 0; + bool repeat_every_year = false; + int64_t timestamp = 0; + trigger_type_t type = TRIGGER_TYPE_INVALID; + if(json_obj_get_array(jctx, "triggers", &total_triggers) != 0) { + ESP_LOGD(TAG, "Trigger not found in JSON"); + return ESP_OK; + } + if (total_triggers <= 0) { + ESP_LOGD(TAG, "No triggers found in trigger array"); + json_obj_leave_array(jctx); + return ESP_OK; + } + if(json_arr_get_object(jctx, 0) == 0) { + json_obj_get_int64(jctx, "ts", ×tamp); + if (json_obj_get_int(jctx, "rsec", &relative_seconds) == 0) { + type = TRIGGER_TYPE_RELATIVE; + } else { + json_obj_get_int(jctx, "m", &minutes); + /* Check if it is of type day */ + if (json_obj_get_int(jctx, "d", &repeat_days) == 0) { + type = TRIGGER_TYPE_DAYS_OF_WEEK; + } + if (json_obj_get_int(jctx, "dd", &day) == 0) { + type = TRIGGER_TYPE_DATE; + json_obj_get_int(jctx, "mm", &repeat_months); + json_obj_get_int(jctx, "yy", &year); + json_obj_get_bool(jctx, "r", &repeat_every_year); + } + } + json_arr_leave_object(jctx); + } + json_obj_leave_array(jctx); + + trigger->type = type; + trigger->relative_seconds = relative_seconds; + trigger->minutes = minutes; + trigger->day.repeat_days = repeat_days; + trigger->date.day = day; + trigger->date.repeat_months = repeat_months; + trigger->date.year = year; + trigger->date.repeat_every_year = repeat_every_year; + trigger->next_timestamp = timestamp; + return ESP_OK; +} + +static esp_err_t esp_rmaker_schedule_parse_info_and_flags(jparse_ctx_t *jctx, char **info, uint32_t *flags) +{ + char _info[MAX_INFO_LEN + 1] = {0}; /* +1 for NULL termination */ + int _flags = 0; + + int err_code = json_obj_get_string(jctx, "info", _info, sizeof(_info)); + if (err_code == OS_SUCCESS) { + if (*info) { + free(*info); + *info = NULL; + } + + int len = strlen(_info); + if (len > 0) { + /* +1 for NULL termination */ + *info = (char *)MEM_CALLOC_EXTRAM(1, len + 1); + if (*info) { + memcpy(*info, _info, len + 1); + } + } + } + + err_code = json_obj_get_int(jctx, "flags", &_flags); + if (err_code == OS_SUCCESS) { + if (flags) { + *flags = _flags; + } + } + + return ESP_OK; +} + +static esp_err_t esp_rmaker_schedule_parse_validity(jparse_ctx_t *jctx, esp_schedule_validity_t *validity) +{ + if (json_obj_get_object(jctx, "validity") == OS_SUCCESS) { + json_obj_get_int64(jctx, "start", (int64_t *)&validity->start_time); + json_obj_get_int64(jctx, "end", (int64_t *)&validity->end_time); + json_obj_leave_object(jctx); + } + return ESP_OK; +} + +static esp_rmaker_schedule_t *esp_rmaker_schedule_find_or_create(jparse_ctx_t *jctx, char *id, schedule_operation_t operation) +{ + char name[MAX_NAME_LEN + 1] = {0}; /* +1 for NULL termination */ + esp_rmaker_schedule_t *schedule = NULL; + if (operation == OPERATION_ADD) { + /* Checking if schedule with same id already exists. */ + schedule = esp_rmaker_schedule_get_schedule_from_id(id); + if (schedule) { + ESP_LOGE(TAG, "Schedule with id %s already exists. Not adding it again.", id); + return NULL; + } + + /* Get name */ + json_obj_get_string(jctx, "name", name, sizeof(name)); + if (strlen(name) <= 0) { + ESP_LOGE(TAG, "Name not found for schedule with id: %s", id); + return NULL; + } + + /* This is a new schedule. Fill it. */ + schedule = (esp_rmaker_schedule_t *)MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_schedule_t)); + if (!schedule) { + ESP_LOGE(TAG, "Couldn't allocate schedule with id: %s", id); + return NULL; + } + strlcpy(schedule->id, id, sizeof(schedule->id)); + strlcpy(schedule->name, name, sizeof(schedule->name)); + schedule->index = schedule_priv_data->index++; + } else { + /* This schedule should already be present */ + schedule = esp_rmaker_schedule_get_schedule_from_id(id); + if (!schedule) { + ESP_LOGE(TAG, "Schedule with id %s not found", id); + return NULL; + } + + /* Get name */ + if (operation == OPERATION_EDIT) { + json_obj_get_string(jctx, "name", name, sizeof(name)); + if (strlen(name) > 0) { + /* If there is name in the request, replace the name in the schedule with this new one */ + memset(schedule->name, 0, sizeof(schedule->name)); + strlcpy(schedule->name, name, sizeof(schedule->name)); + } + } + } + return schedule; +} + +static esp_err_t esp_rmaker_schedule_perform_operation(esp_rmaker_schedule_t *schedule, schedule_operation_t operation, bool enabled) +{ + esp_err_t err = ESP_OK; + switch (operation) { + case OPERATION_ADD: + if (schedule_priv_data->total_schedules < MAX_SCHEDULES) { + esp_rmaker_schedule_operation_add(schedule); + if (enabled == true) { + esp_rmaker_schedule_operation_enable(schedule); + } + } else { + ESP_LOGE(TAG, "Max schedules (%d) reached. Not adding this schedule with id %s", MAX_SCHEDULES, + schedule->id); + err = ESP_FAIL; + } + break; + + case OPERATION_EDIT: + esp_rmaker_schedule_operation_edit(schedule); + break; + + case OPERATION_REMOVE: + esp_rmaker_schedule_operation_remove(schedule); + esp_rmaker_schedule_free(schedule); + break; + + case OPERATION_ENABLE: + esp_rmaker_schedule_operation_enable(schedule); + break; + + case OPERATION_DISABLE: + esp_rmaker_schedule_operation_disable(schedule); + break; + + default: + ESP_LOGE(TAG, "Invalid Operation: %d", operation); + err = ESP_FAIL; + break; + } + return err; +} + +static esp_err_t esp_rmaker_schedule_parse_json(void *data, size_t data_len, esp_rmaker_req_src_t src) +{ + char id[MAX_ID_LEN + 1] = {0}; /* +1 for NULL termination */ + schedule_operation_t operation = OPERATION_INVALID; + bool enabled = true; + int current_schedule = 0; + esp_rmaker_schedule_t *schedule = NULL; + + /* Get details from JSON */ + jparse_ctx_t jctx; + if (json_parse_start(&jctx, (char *)data, data_len) != 0) { + ESP_LOGE(TAG, "Json parse start failed"); + return ESP_FAIL; + } + + /* Parse all schedules */ + while(json_arr_get_object(&jctx, current_schedule) == 0) { + /* Get ID */ + json_obj_get_string(&jctx, "id", id, sizeof(id)); + if (strlen(id) <= 0) { + ESP_LOGE(TAG, "ID not found in schedule JSON"); + goto cleanup; + } + + /* Get operation */ + if (src == ESP_RMAKER_REQ_SRC_INIT) { + /* Schedule loaded from NVS. Add it */ + operation = OPERATION_ADD; + } else { + operation = esp_rmaker_schedule_parse_operation(&jctx, id); + if (operation == OPERATION_INVALID) { + ESP_LOGE(TAG, "Error getting operation"); + goto cleanup; + } + } + + /* Find/Create new schedule */ + schedule = esp_rmaker_schedule_find_or_create(&jctx, id, operation); + if (!schedule) { + goto cleanup; + } + + /* Get other schedule details */ + if (operation == OPERATION_ADD || operation == OPERATION_EDIT) { + /* Get enabled state */ + if (operation == OPERATION_ADD) { + /* If loaded from NVS, check for previous enabled state. If new schedule, enable it */ + if (src == ESP_RMAKER_REQ_SRC_INIT) { + json_obj_get_bool(&jctx, "enabled", &enabled); + } else { + enabled = true; + } + } + + /* Get action */ + esp_rmaker_schedule_parse_action(&jctx, &schedule->action); + + /* Get trigger */ + /* There is only one trigger for now. If more triggers are added, then they should be parsed here in a loop */ + esp_rmaker_schedule_parse_trigger(&jctx, &schedule->trigger); + + /* Get info and flags */ + esp_rmaker_schedule_parse_info_and_flags(&jctx, &schedule->info, &schedule->flags); + + /* Get validity */ + esp_rmaker_schedule_parse_validity(&jctx, &schedule->validity); + } + + /* Perform operation */ + esp_rmaker_schedule_perform_operation(schedule, operation, enabled); + +cleanup: + json_arr_leave_object(&jctx); + current_schedule++; + } + json_parse_end(&jctx); + return ESP_OK; +} + +static esp_err_t __esp_rmaker_schedule_get_params(char *buf, size_t *buf_size) +{ + esp_err_t err = ESP_OK; + esp_rmaker_schedule_t *schedule = schedule_priv_data->schedule_list; + json_gen_str_t jstr; + json_gen_str_start(&jstr, buf, *buf_size, NULL, NULL); + json_gen_start_array(&jstr); + while (schedule) { + json_gen_start_object(&jstr); + + /* Add details */ + json_gen_obj_set_string(&jstr, "name", schedule->name); + json_gen_obj_set_string(&jstr, "id", schedule->id); + json_gen_obj_set_bool(&jstr, "enabled", schedule->enabled); + /* If info and flags is not zero, add it. */ + if (schedule->info != NULL) { + json_gen_obj_set_string(&jstr, "info", schedule->info); + } + if (schedule->flags != 0) { + json_gen_obj_set_int(&jstr, "flags", schedule->flags); + } + /* Add validity */ + if (schedule->validity.start_time != 0 || schedule->validity.end_time != 0) { + json_gen_push_object(&jstr, "validity"); + if (schedule->validity.start_time != 0) { + json_gen_obj_set_int(&jstr, "start", schedule->validity.start_time); + } + if (schedule->validity.end_time != 0) { + json_gen_obj_set_int(&jstr, "end", schedule->validity.end_time); + } + json_gen_pop_object(&jstr); + } + /* Add action */ + json_gen_push_object_str(&jstr, "action", schedule->action.data); + + /* Add trigger */ + json_gen_push_array(&jstr, "triggers"); + json_gen_start_object(&jstr); + if (schedule->trigger.type == TRIGGER_TYPE_RELATIVE) { + json_gen_obj_set_int(&jstr, "rsec", schedule->trigger.relative_seconds); + json_gen_obj_set_int(&jstr, "ts", schedule->trigger.next_timestamp); + } else { + json_gen_obj_set_int(&jstr, "m", schedule->trigger.minutes); + if (schedule->trigger.type == TRIGGER_TYPE_DAYS_OF_WEEK) { + json_gen_obj_set_int(&jstr, "d", schedule->trigger.day.repeat_days); + if (schedule->trigger.day.repeat_days == 0) { + json_gen_obj_set_int(&jstr, "ts", schedule->trigger.next_timestamp); + } + } else if (schedule->trigger.type == TRIGGER_TYPE_DATE) { + json_gen_obj_set_int(&jstr, "dd", schedule->trigger.date.day); + json_gen_obj_set_int(&jstr, "mm", schedule->trigger.date.repeat_months); + json_gen_obj_set_int(&jstr, "yy", schedule->trigger.date.year); + json_gen_obj_set_int(&jstr, "r", schedule->trigger.date.repeat_every_year); + if (schedule->trigger.date.repeat_months == 0) { + json_gen_obj_set_int(&jstr, "ts", schedule->trigger.next_timestamp); + } + } + } + json_gen_end_object(&jstr); + json_gen_pop_array(&jstr); + + json_gen_end_object(&jstr); + + /* Go to next schedule */ + schedule = schedule->next; + } + if (json_gen_end_array(&jstr) < 0) { + ESP_LOGE(TAG, "Buffer size %lu not sufficient for reporting Schedule Params.", (unsigned long) *buf_size); + err = ESP_ERR_NO_MEM; + } + *buf_size = json_gen_str_end(&jstr); + return err; +} + +static char *esp_rmaker_schedule_get_params(void) +{ + size_t req_size = 0; + esp_err_t err = __esp_rmaker_schedule_get_params(NULL, &req_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to get required size for schedules JSON."); + return NULL; + } + char *data = MEM_CALLOC_EXTRAM(1, req_size); + if (!data) { + ESP_LOGE(TAG, "Failed to allocate %lu bytes for schedule.", (unsigned long) req_size); + return NULL; + } + err = __esp_rmaker_schedule_get_params(data, &req_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error occured while trying to populate schedules JSON."); + free(data); + return NULL; + } + return data; +} + +static esp_err_t esp_rmaker_schedule_report_params(void) +{ + char *data = esp_rmaker_schedule_get_params(); + esp_rmaker_param_val_t val = { + .type = RMAKER_VAL_TYPE_ARRAY, + .val.s = data, + }; + esp_rmaker_param_t *param = esp_rmaker_device_get_param_by_type(schedule_priv_data->schedule_service, ESP_RMAKER_PARAM_SCHEDULES); + esp_rmaker_param_update_and_report(param, val); + + free(data); + return ESP_OK; +} + +static esp_err_t write_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx) +{ + if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_SCHEDULES) != 0) { + ESP_LOGE(TAG, "Got callback for invalid param with name %s and type %s", esp_rmaker_param_get_name(param), esp_rmaker_param_get_type(param)); + return ESP_ERR_INVALID_ARG; + } + if (strlen(val.val.s) <= 0) { + ESP_LOGI(TAG, "Invalid length for params: %lu", (unsigned long) strlen(val.val.s)); + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_schedule_parse_json(val.val.s, strlen(val.val.s), ctx->src); + if (ctx->src != ESP_RMAKER_REQ_SRC_INIT) { + /* Since this is a persisting param, we get a write_cb while booting up. We need not report the param when the source is 'init' as this will get reported when the device first reports all the params. */ + esp_rmaker_schedule_report_params(); + } + return ESP_OK; +} + +esp_err_t esp_rmaker_schedule_enable(void) +{ + schedule_priv_data = (esp_rmaker_schedule_priv_data_t *)MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_schedule_priv_data_t)); + if (!schedule_priv_data) { + ESP_LOGE(TAG, "Couldn't allocate schedule_priv_data"); + return ESP_ERR_NO_MEM; + } + esp_rmaker_time_sync_init(NULL); + + esp_schedule_init(false, NULL, NULL); + + schedule_priv_data->schedule_service = esp_rmaker_create_schedule_service("Schedule", write_cb, NULL, MAX_SCHEDULES, NULL); + if (!schedule_priv_data->schedule_service) { + ESP_LOGE(TAG, "Failed to create Schedule Service"); + return ESP_FAIL; + } + esp_err_t err = esp_rmaker_node_add_device(esp_rmaker_get_node(), schedule_priv_data->schedule_service); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to add service Service"); + return err; + } + ESP_LOGD(TAG, "Scheduling Service Enabled"); + return err; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_secure_boot_digest.c b/components/esp_rainmaker/src/core/esp_rmaker_secure_boot_digest.c new file mode 100644 index 0000000..bb0ed95 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_secure_boot_digest.c @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include "esp_rmaker_secure_boot_digest.h" + +static const char *TAG = "rmaker_secure_boot"; + +#ifdef CONFIG_SECURE_BOOT_V2_ENABLED + +static inline uint8_t to_hex_digit(unsigned val) +{ + return (val < 10) ? ('0' + val) : ('a' + val - 10); +} + +static void bytes_to_hex(uint8_t *src, uint8_t *dst, int in_len) +{ + for (int i = 0; i < in_len; i++) { + dst[2 * i] = to_hex_digit(src[i] >> 4); + dst[2 * i + 1] = to_hex_digit(src[i] & 0xf); + } + dst[2 * in_len] = 0; +} + +// Hex representation of secure boot digest. +1 for NULL termination +#define SECURE_BOOT_DIGEST_LEN (ESP_SECURE_BOOT_DIGEST_LEN * 2 + 1) + +esp_err_t esp_rmaker_secure_boot_digest_free(char **digest) +{ + // cleanup + for(int i = 0; i < SECURE_BOOT_NUM_BLOCKS; i++) { + if (digest[i]) { + free(digest[i]); + } + } + free(digest); + return ESP_OK; +} + +char** esp_rmaker_get_secure_boot_digest() +{ + char **secure_boot_digest = NULL; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + esp_secure_boot_key_digests_t trusted_keys; +#else + ets_secure_boot_key_digests_t trusted_keys; +#endif + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + esp_err_t ret = esp_secure_boot_read_key_digests(&trusted_keys); +#else + esp_err_t ret = ets_secure_boot_read_key_digests(&trusted_keys); +#endif + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Could not read the secure boot key digests from efuse."); + return NULL; + } + secure_boot_digest = MEM_ALLOC_EXTRAM(SECURE_BOOT_NUM_BLOCKS * sizeof(char *)); + if (!secure_boot_digest) { + ESP_LOGE(TAG, "Failed to allocate memory for secure boot digest"); + return NULL; + } + for(int i = 0; i < SECURE_BOOT_NUM_BLOCKS; i++) { + secure_boot_digest[i] = NULL; + if (trusted_keys.key_digests[i] != NULL) { + secure_boot_digest[i] = MEM_ALLOC_EXTRAM(SECURE_BOOT_DIGEST_LEN); + if (!secure_boot_digest[i]) { + ESP_LOGE(TAG, "Failed to allocate memory for secure boot digest"); + for (int k = 0; k < i; k++) { + free(secure_boot_digest[k]); + secure_boot_digest[k] = NULL; + } + free(secure_boot_digest); + secure_boot_digest = NULL; + break; + } + bytes_to_hex((uint8_t *)trusted_keys.key_digests[i], (uint8_t *)secure_boot_digest[i], ESP_SECURE_BOOT_DIGEST_LEN); + } + } + return secure_boot_digest; +} + +#else /* CONFIG_SECURE_BOOT_V2_ENABLED */ + +esp_err_t esp_rmaker_secure_boot_digest_free(char **digest) +{ + (void) digest; + return ESP_OK; +} + +char** esp_rmaker_get_secure_boot_digest() +{ + ESP_LOGI(TAG, "Secure boot is not enabled. Could not get digest."); + return NULL; +} + +#endif diff --git a/components/esp_rainmaker/src/core/esp_rmaker_secure_boot_digest.h b/components/esp_rainmaker/src/core/esp_rmaker_secure_boot_digest.h new file mode 100644 index 0000000..4827cd2 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_secure_boot_digest.h @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +#if CONFIG_IDF_TARGET_ESP32 +#include "esp32/rom/secure_boot.h" +#elif CONFIG_IDF_TARGET_ESP32S2 +#include "esp32s2/rom/secure_boot.h" +#elif CONFIG_IDF_TARGET_ESP32C3 +#include "esp32c3/rom/secure_boot.h" +#elif CONFIG_IDF_TARGET_ESP32S3 +#include "esp32s3/rom/secure_boot.h" +#elif CONFIG_IDF_TARGET_ESP32C2 +#include "esp32c2/rom/secure_boot.h" +#elif CONFIG_IDF_TARGET_ESP32C6 +#include "esp32c6/rom/secure_boot.h" +#elif CONFIG_IDF_TARGET_ESP32H2 +#include "esp32h2/rom/secure_boot.h" +#endif + +/** + * @brief Get secure boot digest + * + * @return 2D pointer with secure boot digest array + * @note the memory allocated gets freed with \ref `esp_rmaker_secure_boot_digest_free` API + */ +char** esp_rmaker_get_secure_boot_digest(); + +/** + * @brief free secure boot digest buffer + */ +esp_err_t esp_rmaker_secure_boot_digest_free(char **digest); diff --git a/components/esp_rainmaker/src/core/esp_rmaker_system_service.c b/components/esp_rainmaker/src/core/esp_rmaker_system_service.c new file mode 100644 index 0000000..dcdb848 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_system_service.c @@ -0,0 +1,90 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include +#include +#include +#include +#include + +static const char *TAG = "esp_rmaker_system_service"; + +#define ESP_RMAKER_SYSTEM_SERV_NAME "System" + +static esp_err_t esp_rmaker_system_serv_write_cb(const esp_rmaker_device_t *device, + const esp_rmaker_param_t *param, const esp_rmaker_param_val_t val, + void *priv_data, esp_rmaker_write_ctx_t *ctx) +{ + esp_err_t err = ESP_OK; + esp_rmaker_system_serv_config_t *config = (esp_rmaker_system_serv_config_t *)priv_data; + if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_REBOOT) == 0) { + if (val.val.b == true) { + err = esp_rmaker_reboot(config->reboot_seconds); + } + } else if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_FACTORY_RESET) == 0) { + if (val.val.b == true) { + err = esp_rmaker_factory_reset(config->reset_seconds, config->reset_reboot_seconds); + } + } else if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_WIFI_RESET) == 0) { + if (val.val.b == true) { + err = esp_rmaker_wifi_reset(config->reset_seconds, config->reset_reboot_seconds); + } + } else { + return ESP_FAIL; + } + + if (err == ESP_OK) { + esp_rmaker_param_update_and_report(param, val); + } + return err; +} + +esp_err_t esp_rmaker_system_service_enable(esp_rmaker_system_serv_config_t *config) +{ + if ((config->flags & SYSTEM_SERV_FLAGS_ALL) == 0) { + ESP_LOGE(TAG, "Atleast one flag should be set for system service."); + return ESP_ERR_INVALID_ARG; + } + esp_rmaker_system_serv_config_t *priv_config = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_system_serv_config_t)); + if (!priv_config) { + ESP_LOGE(TAG, "Failed to allocate data for system service config."); + return ESP_ERR_NO_MEM; + } + *priv_config = *config; + esp_rmaker_device_t *service = esp_rmaker_create_system_service(ESP_RMAKER_SYSTEM_SERV_NAME, (void *)priv_config); + if (service) { + esp_rmaker_device_add_cb(service, esp_rmaker_system_serv_write_cb, NULL); + if (priv_config->flags & SYSTEM_SERV_FLAG_REBOOT) { + esp_rmaker_device_add_param(service, esp_rmaker_reboot_param_create(ESP_RMAKER_DEF_REBOOT_NAME)); + } + if (priv_config->flags & SYSTEM_SERV_FLAG_FACTORY_RESET) { + esp_rmaker_device_add_param(service, esp_rmaker_factory_reset_param_create(ESP_RMAKER_DEF_FACTORY_RESET_NAME)); + } + if (priv_config->flags & SYSTEM_SERV_FLAG_WIFI_RESET) { + esp_rmaker_device_add_param(service, esp_rmaker_wifi_reset_param_create(ESP_RMAKER_DEF_WIFI_RESET_NAME)); + } + esp_err_t err = esp_rmaker_node_add_device(esp_rmaker_get_node(), service); + if (err == ESP_OK) { + ESP_LOGI(TAG, "System service enabled."); + } else { + esp_rmaker_device_delete(service); + } + return err; + } else { + free(priv_config); + ESP_LOGE(TAG, "Failed to create System service."); + } + return ESP_ERR_NO_MEM; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_time_service.c b/components/esp_rainmaker/src/core/esp_rmaker_time_service.c new file mode 100644 index 0000000..d6f8ebe --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_time_service.c @@ -0,0 +1,82 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include +#include +#include +#include + +static const char *TAG = "esp_rmaker_time_service"; + +#define ESP_RMAKER_TIME_SERV_NAME "Time" + +static esp_err_t esp_rmaker_time_service_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx) +{ + esp_err_t err = ESP_FAIL; + if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_TIMEZONE) == 0) { + ESP_LOGI(TAG, "Received value = %s for %s - %s", + val.val.s, esp_rmaker_device_get_name(device), esp_rmaker_param_get_name(param)); + err = esp_rmaker_time_set_timezone(val.val.s); + if (err == ESP_OK) { + char *tz_posix = esp_rmaker_time_get_timezone_posix(); + if (tz_posix) { + esp_rmaker_param_t *tz_posix_param = esp_rmaker_device_get_param_by_type( + device, ESP_RMAKER_PARAM_TIMEZONE_POSIX); + esp_rmaker_param_update_and_report(tz_posix_param, esp_rmaker_str(tz_posix)); + free(tz_posix); + } + } + } else if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_TIMEZONE_POSIX) == 0) { + ESP_LOGI(TAG, "Received value = %s for %s - %s", + val.val.s, esp_rmaker_device_get_name(device), esp_rmaker_param_get_name(param)); + err = esp_rmaker_time_set_timezone_posix(val.val.s); + } + if (err == ESP_OK) { + esp_rmaker_param_update_and_report(param, val); + } + return err; +} + +static esp_err_t esp_rmaker_time_add_service(const char *tz, const char *tz_posix) +{ + esp_rmaker_device_t *service = esp_rmaker_time_service_create(ESP_RMAKER_TIME_SERV_NAME, tz, tz_posix, NULL); + if (!service) { + ESP_LOGE(TAG, "Failed to create Time Service"); + return ESP_FAIL; + } + esp_rmaker_device_add_cb(service, esp_rmaker_time_service_cb, NULL); + esp_err_t err = esp_rmaker_node_add_device(esp_rmaker_get_node(), service); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Time service enabled"); + } else { + esp_rmaker_device_delete(service); + } + return err; +} + +esp_err_t esp_rmaker_timezone_service_enable(void) +{ + char *tz_posix = esp_rmaker_time_get_timezone_posix(); + char *tz = esp_rmaker_time_get_timezone(); + esp_err_t err = esp_rmaker_time_add_service(tz, tz_posix); + if (tz_posix) { + free(tz_posix); + } + if (tz) { + free(tz); + } + return err; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.c b/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.c new file mode 100644 index 0000000..fbe2d83 --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.c @@ -0,0 +1,414 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "esp_rmaker_user_mapping.pb-c.h" +#include "esp_rmaker_internal.h" +#include "esp_rmaker_mqtt_topics.h" +#if RMAKER_USING_NETWORK_PROV +#include +#else +#include +#endif + +static const char *TAG = "esp_rmaker_user_mapping"; + +#define USER_MAPPING_ENDPOINT "cloud_user_assoc" +#define USER_MAPPING_NVS_NAMESPACE "user_mapping" +#define USER_ID_NVS_NAME "user_id" +#define USER_RESET_ID "esp-rmaker" +#define USER_RESET_KEY "failed" + +/* A delay large enough to allow the tasks to get the semaphore, but small + * enough to prevent tasks getting blocked for long. + */ +#define SEMAPHORE_DELAY_MSEC 5000 + +typedef struct { + char *user_id; + char *secret_key; + int mqtt_msg_id; + bool sent; +} esp_rmaker_user_mapping_data_t; + +static esp_rmaker_user_mapping_data_t *rmaker_user_mapping_data; +esp_rmaker_user_mapping_state_t rmaker_user_mapping_state; +SemaphoreHandle_t esp_rmaker_user_mapping_lock = NULL; + +static void esp_rmaker_user_mapping_cleanup_data(void) +{ + if (rmaker_user_mapping_data) { + if (rmaker_user_mapping_data->user_id) { + free(rmaker_user_mapping_data->user_id); + } + if (rmaker_user_mapping_data->secret_key) { + free(rmaker_user_mapping_data->secret_key); + } + free(rmaker_user_mapping_data); + rmaker_user_mapping_data = NULL; + } +} + +static void esp_rmaker_user_mapping_event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ +#if RMAKER_USING_NETWORK_PROV + if (event_base == NETWORK_PROV_EVENT) { + switch (event_id) { + case NETWORK_PROV_INIT: { + if (esp_rmaker_user_mapping_endpoint_create() != ESP_OK) { + ESP_LOGE(TAG, "Failed to create user mapping end point."); + } + break; + } + case NETWORK_PROV_START: + if (esp_rmaker_user_mapping_endpoint_register() != ESP_OK) { + ESP_LOGE(TAG, "Failed to register user mapping end point."); + } + break; + default: + break; + } +#else + if (event_base == WIFI_PROV_EVENT) { + switch (event_id) { + case WIFI_PROV_INIT: { + if (esp_rmaker_user_mapping_endpoint_create() != ESP_OK) { + ESP_LOGE(TAG, "Failed to create user mapping end point."); + } + break; + } + case WIFI_PROV_START: + if (esp_rmaker_user_mapping_endpoint_register() != ESP_OK) { + ESP_LOGE(TAG, "Failed to register user mapping end point."); + } + break; + default: + break; + } +#endif + } else if ((event_base == RMAKER_COMMON_EVENT) && (event_id == RMAKER_MQTT_EVENT_PUBLISHED)) { + /* Checking for the PUBACK for the user node association message to be sure that the message + * has indeed reached the RainMaker cloud. + */ + int msg_id = *((int *)event_data); + if (xSemaphoreTake(esp_rmaker_user_mapping_lock, SEMAPHORE_DELAY_MSEC/portTICK_PERIOD_MS) != pdTRUE) { + ESP_LOGE(TAG, "Failed to take semaphore."); + return; + } + if ((rmaker_user_mapping_data != NULL) && (msg_id == rmaker_user_mapping_data->mqtt_msg_id)) { + ESP_LOGI(TAG, "User Node association message published successfully."); + if (strcmp(rmaker_user_mapping_data->user_id, USER_RESET_ID) == 0) { + rmaker_user_mapping_state = ESP_RMAKER_USER_MAPPING_RESET; + esp_rmaker_post_event(RMAKER_EVENT_USER_NODE_MAPPING_RESET, NULL, 0); + } else { + rmaker_user_mapping_state = ESP_RMAKER_USER_MAPPING_DONE; + esp_rmaker_post_event(RMAKER_EVENT_USER_NODE_MAPPING_DONE, rmaker_user_mapping_data->user_id, + strlen(rmaker_user_mapping_data->user_id) + 1); + } +#ifdef CONFIG_ESP_RMAKER_USER_ID_CHECK + /* Store User Id in NVS since acknowledgement of the user-node association message is received */ + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, USER_MAPPING_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err == ESP_OK) { + nvs_set_blob(handle, USER_ID_NVS_NAME, rmaker_user_mapping_data->user_id, strlen(rmaker_user_mapping_data->user_id)); + nvs_close(handle); + } +#endif + esp_rmaker_user_mapping_cleanup_data(); + esp_event_handler_unregister(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_PUBLISHED, + &esp_rmaker_user_mapping_event_handler); + } + xSemaphoreGive(esp_rmaker_user_mapping_lock); + } +} + +static void esp_rmaker_user_mapping_cb(void *priv_data) +{ + if (xSemaphoreTake(esp_rmaker_user_mapping_lock, SEMAPHORE_DELAY_MSEC/portTICK_PERIOD_MS) != pdTRUE) { + ESP_LOGE(TAG, "Failed to take semaphore."); + return; + } + /* If there is no user node mapping data, or if the data is already sent, just return */ + if (rmaker_user_mapping_data == NULL || rmaker_user_mapping_data->sent == true) { + xSemaphoreGive(esp_rmaker_user_mapping_lock); + return; + } + esp_event_handler_register(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_PUBLISHED, + &esp_rmaker_user_mapping_event_handler, NULL); + char publish_payload[200]; + json_gen_str_t jstr; + json_gen_str_start(&jstr, publish_payload, sizeof(publish_payload), NULL, NULL); + json_gen_start_object(&jstr); + char *node_id = esp_rmaker_get_node_id(); + json_gen_obj_set_string(&jstr, "node_id", node_id); + json_gen_obj_set_string(&jstr, "user_id", rmaker_user_mapping_data->user_id); + json_gen_obj_set_string(&jstr, "secret_key", rmaker_user_mapping_data->secret_key); + if (esp_rmaker_user_node_mapping_get_state() != ESP_RMAKER_USER_MAPPING_DONE) { + json_gen_obj_set_bool(&jstr, "reset", true); + } + json_gen_end_object(&jstr); + json_gen_str_end(&jstr); + char publish_topic[MQTT_TOPIC_BUFFER_SIZE]; + esp_rmaker_create_mqtt_topic(publish_topic, sizeof(publish_topic), USER_MAPPING_TOPIC_SUFFIX, USER_MAPPING_TOPIC_RULE); + esp_err_t err = esp_rmaker_mqtt_publish(publish_topic, publish_payload, strlen(publish_payload), RMAKER_MQTT_QOS1, &rmaker_user_mapping_data->mqtt_msg_id); + ESP_LOGI(TAG, "MQTT Publish: %s", publish_payload); + if (err != ESP_OK) { + ESP_LOGE(TAG, "MQTT Publish Error %d", err); + } else { + rmaker_user_mapping_state = ESP_RMAKER_USER_MAPPING_REQ_SENT; + rmaker_user_mapping_data->sent = true; + } + xSemaphoreGive(esp_rmaker_user_mapping_lock); + return; +} + +static bool esp_rmaker_user_mapping_detect_reset(const char *user_id) +{ +#ifdef CONFIG_ESP_RMAKER_USER_ID_CHECK + bool reset_state = true; + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, USER_MAPPING_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err != ESP_OK) { + return true; + } + char *nvs_user_id = NULL; + size_t len = 0; + if ((err = nvs_get_blob(handle, USER_ID_NVS_NAME, NULL, &len)) == ESP_OK) { + nvs_user_id = MEM_CALLOC_EXTRAM(1, len + 1); /* +1 for NULL termination */ + if (nvs_user_id) { + nvs_get_blob(handle, USER_ID_NVS_NAME, nvs_user_id, &len); + /* If existing user id and new user id are same, this is not a reset state */ + if (strcmp(nvs_user_id, user_id) == 0) { + reset_state = false; + } else { + /* Deleting the key in case of a mismatch. It will be stored only after the user node association + * message is acknowledged from the cloud. + */ + nvs_erase_key(handle, USER_ID_NVS_NAME); + } + free(nvs_user_id); + } + } + nvs_close(handle); + return reset_state; +#else + return false; +#endif +} + +esp_err_t esp_rmaker_start_user_node_mapping(char *user_id, char *secret_key) +{ + if (esp_rmaker_user_mapping_lock == NULL) { + ESP_LOGE(TAG, "User Node mapping not initialised."); + return ESP_FAIL; + } + if (xSemaphoreTake(esp_rmaker_user_mapping_lock, SEMAPHORE_DELAY_MSEC/portTICK_PERIOD_MS) != pdTRUE) { + ESP_LOGE(TAG, "Failed to take semaphore."); + return ESP_FAIL; + } + if (rmaker_user_mapping_data) { + esp_rmaker_user_mapping_cleanup_data(); + } + + rmaker_user_mapping_data = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_user_mapping_data_t)); + if (!rmaker_user_mapping_data) { + ESP_LOGE(TAG, "Failed to allocate memory for rmaker_user_mapping_data."); + xSemaphoreGive(esp_rmaker_user_mapping_lock); + return ESP_ERR_NO_MEM; + } + rmaker_user_mapping_data->user_id = strdup(user_id); + if (!rmaker_user_mapping_data->user_id) { + ESP_LOGE(TAG, "Failed to allocate memory for user_id."); + goto user_mapping_error; + } + rmaker_user_mapping_data->secret_key = strdup(secret_key); + if (!rmaker_user_mapping_data->secret_key) { + ESP_LOGE(TAG, "Failed to allocate memory for secret_key."); + goto user_mapping_error; + } + if (esp_rmaker_user_mapping_detect_reset(user_id)) { + ESP_LOGI(TAG, "User Node mapping reset detected."); + rmaker_user_mapping_state = ESP_RMAKER_USER_MAPPING_STARTED; + } else { + rmaker_user_mapping_state = ESP_RMAKER_USER_MAPPING_DONE; + } + if (esp_rmaker_work_queue_add_task(esp_rmaker_user_mapping_cb, NULL) != ESP_OK) { + ESP_LOGE(TAG, "Failed to queue user mapping task."); + goto user_mapping_error; + } + esp_rmaker_user_mapping_prov_deinit(); + xSemaphoreGive(esp_rmaker_user_mapping_lock); + return ESP_OK; + +user_mapping_error: + esp_rmaker_user_mapping_cleanup_data(); + xSemaphoreGive(esp_rmaker_user_mapping_lock); + return ESP_FAIL; +} + +esp_err_t esp_rmaker_reset_user_node_mapping(void) +{ + return esp_rmaker_start_user_node_mapping(USER_RESET_ID, USER_RESET_KEY); +} + +int esp_rmaker_user_mapping_handler(uint32_t session_id, const uint8_t *inbuf, ssize_t inlen, uint8_t **outbuf, ssize_t *outlen, void *priv_data) +{ + Rainmaker__RMakerConfigPayload *data; + data = rainmaker__rmaker_config_payload__unpack(NULL, inlen, inbuf); + + if (!data) { + ESP_LOGE(TAG, "No Data Received"); + return ESP_FAIL; + } + switch (data->msg) { + case RAINMAKER__RMAKER_CONFIG_MSG_TYPE__TypeCmdSetUserMapping: { + ESP_LOGI(TAG, "Received request for node details"); + Rainmaker__RMakerConfigPayload resp; + Rainmaker__RespSetUserMapping payload; + rainmaker__rmaker_config_payload__init(&resp); + rainmaker__resp_set_user_mapping__init(&payload); + + if (data->payload_case != RAINMAKER__RMAKER_CONFIG_PAYLOAD__PAYLOAD_CMD_SET_USER_MAPPING) { + ESP_LOGE(TAG, "Invalid payload type in the message: %d", data->payload_case); + payload.status = RAINMAKER__RMAKER_CONFIG_STATUS__InvalidParam; + } else { + ESP_LOGI(TAG, "Got user_id = %s, secret_key = %s", data->cmd_set_user_mapping->userid, data->cmd_set_user_mapping->secretkey); + if (esp_rmaker_start_user_node_mapping(data->cmd_set_user_mapping->userid, + data->cmd_set_user_mapping->secretkey) != ESP_OK) { + ESP_LOGI(TAG, "Sending status Invalid Param"); + payload.status = RAINMAKER__RMAKER_CONFIG_STATUS__InvalidParam; + } else { + ESP_LOGI(TAG, "Sending status SUCCESS"); + payload.status = RAINMAKER__RMAKER_CONFIG_STATUS__Success; + payload.nodeid = esp_rmaker_get_node_id(); + } + } + resp.msg = RAINMAKER__RMAKER_CONFIG_MSG_TYPE__TypeRespSetUserMapping; + resp.payload_case = RAINMAKER__RMAKER_CONFIG_PAYLOAD__PAYLOAD_RESP_SET_USER_MAPPING; + resp.resp_set_user_mapping = &payload; + + *outlen = rainmaker__rmaker_config_payload__get_packed_size(&resp); + *outbuf = (uint8_t *)MEM_ALLOC_EXTRAM(*outlen); + rainmaker__rmaker_config_payload__pack(&resp, *outbuf); + break; + } + default: + ESP_LOGE(TAG, "Received invalid message type: %d", data->msg); + break; + } + rainmaker__rmaker_config_payload__free_unpacked(data, NULL); + return ESP_OK; +} +esp_err_t esp_rmaker_user_mapping_endpoint_create(void) +{ +#if RMAKER_USING_NETWORK_PROV + esp_err_t err = network_prov_mgr_endpoint_create(USER_MAPPING_ENDPOINT); +#else + esp_err_t err = wifi_prov_mgr_endpoint_create(USER_MAPPING_ENDPOINT); +#endif + return err; +} + +esp_err_t esp_rmaker_user_mapping_endpoint_register(void) +{ +#if RMAKER_USING_NETWORK_PROV + return network_prov_mgr_endpoint_register(USER_MAPPING_ENDPOINT, esp_rmaker_user_mapping_handler, NULL); +#else + return wifi_prov_mgr_endpoint_register(USER_MAPPING_ENDPOINT, esp_rmaker_user_mapping_handler, NULL); +#endif +} + +esp_err_t esp_rmaker_user_mapping_prov_init(void) +{ + int ret = ESP_OK; +#if RMAKER_USING_NETWORK_PROV + ret = esp_event_handler_register(NETWORK_PROV_EVENT, NETWORK_PROV_INIT, &esp_rmaker_user_mapping_event_handler, NULL); +#else + ret = esp_event_handler_register(WIFI_PROV_EVENT, WIFI_PROV_INIT,&esp_rmaker_user_mapping_event_handler, NULL); +#endif + if (ret != ESP_OK) { + return ret; + } +#if RMAKER_USING_NETWORK_PROV + ret = esp_event_handler_register(NETWORK_PROV_EVENT, NETWORK_PROV_START, &esp_rmaker_user_mapping_event_handler, NULL); +#else + ret = esp_event_handler_register(WIFI_PROV_EVENT, WIFI_PROV_START,&esp_rmaker_user_mapping_event_handler, NULL); +#endif + return ret; +} + +esp_err_t esp_rmaker_user_mapping_prov_deinit(void) +{ +#if RMAKER_USING_NETWORK_PROV + esp_event_handler_unregister(NETWORK_PROV_EVENT, NETWORK_PROV_INIT, &esp_rmaker_user_mapping_event_handler); + esp_event_handler_unregister(NETWORK_PROV_EVENT, NETWORK_PROV_START, &esp_rmaker_user_mapping_event_handler); +#else + esp_event_handler_unregister(WIFI_PROV_EVENT, WIFI_PROV_INIT, &esp_rmaker_user_mapping_event_handler); + esp_event_handler_unregister(WIFI_PROV_EVENT, WIFI_PROV_START, &esp_rmaker_user_mapping_event_handler); +#endif + return ESP_OK; +} + +esp_rmaker_user_mapping_state_t esp_rmaker_user_node_mapping_get_state(void) +{ + return rmaker_user_mapping_state; +} + +esp_err_t esp_rmaker_user_node_mapping_init(void) +{ +#ifdef CONFIG_ESP_RMAKER_USER_ID_CHECK + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, USER_MAPPING_NVS_NAMESPACE, NVS_READONLY, &handle); + if (err == ESP_OK) { + size_t len = 0; + if ((err = nvs_get_blob(handle, USER_ID_NVS_NAME, NULL, &len)) == ESP_OK) { + /* Some User Id found, which means that user node association is already done */ + rmaker_user_mapping_state = ESP_RMAKER_USER_MAPPING_DONE; + } + nvs_close(handle); + } +#else + rmaker_user_mapping_state = ESP_RMAKER_USER_MAPPING_DONE; +#endif + if (!esp_rmaker_user_mapping_lock) { + esp_rmaker_user_mapping_lock = xSemaphoreCreateMutex(); + if (!esp_rmaker_user_mapping_lock) { + ESP_LOGE(TAG, "Failed to create Mutex"); + return ESP_FAIL; + } + } + return ESP_OK; +} + +esp_err_t esp_rmaker_user_node_mapping_deinit(void) +{ + if (esp_rmaker_user_mapping_lock) { + vSemaphoreDelete(esp_rmaker_user_mapping_lock); + esp_rmaker_user_mapping_lock = NULL; + } + return ESP_OK; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.pb-c.c b/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.pb-c.c new file mode 100644 index 0000000..3a0ae5b --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.pb-c.c @@ -0,0 +1,369 @@ +/* Generated by the protocol buffer compiler. DO NOT EDIT! */ +/* Generated from: esp_rmaker_user_mapping.proto */ + +/* Do not generate deprecated warnings for self */ +#ifndef PROTOBUF_C__NO_DEPRECATED +#define PROTOBUF_C__NO_DEPRECATED +#endif + +#include "esp_rmaker_user_mapping.pb-c.h" +void rainmaker__cmd_set_user_mapping__init + (Rainmaker__CmdSetUserMapping *message) +{ + static const Rainmaker__CmdSetUserMapping init_value = RAINMAKER__CMD_SET_USER_MAPPING__INIT; + *message = init_value; +} +size_t rainmaker__cmd_set_user_mapping__get_packed_size + (const Rainmaker__CmdSetUserMapping *message) +{ + assert(message->base.descriptor == &rainmaker__cmd_set_user_mapping__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t rainmaker__cmd_set_user_mapping__pack + (const Rainmaker__CmdSetUserMapping *message, + uint8_t *out) +{ + assert(message->base.descriptor == &rainmaker__cmd_set_user_mapping__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t rainmaker__cmd_set_user_mapping__pack_to_buffer + (const Rainmaker__CmdSetUserMapping *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &rainmaker__cmd_set_user_mapping__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +Rainmaker__CmdSetUserMapping * + rainmaker__cmd_set_user_mapping__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (Rainmaker__CmdSetUserMapping *) + protobuf_c_message_unpack (&rainmaker__cmd_set_user_mapping__descriptor, + allocator, len, data); +} +void rainmaker__cmd_set_user_mapping__free_unpacked + (Rainmaker__CmdSetUserMapping *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &rainmaker__cmd_set_user_mapping__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void rainmaker__resp_set_user_mapping__init + (Rainmaker__RespSetUserMapping *message) +{ + static const Rainmaker__RespSetUserMapping init_value = RAINMAKER__RESP_SET_USER_MAPPING__INIT; + *message = init_value; +} +size_t rainmaker__resp_set_user_mapping__get_packed_size + (const Rainmaker__RespSetUserMapping *message) +{ + assert(message->base.descriptor == &rainmaker__resp_set_user_mapping__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t rainmaker__resp_set_user_mapping__pack + (const Rainmaker__RespSetUserMapping *message, + uint8_t *out) +{ + assert(message->base.descriptor == &rainmaker__resp_set_user_mapping__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t rainmaker__resp_set_user_mapping__pack_to_buffer + (const Rainmaker__RespSetUserMapping *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &rainmaker__resp_set_user_mapping__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +Rainmaker__RespSetUserMapping * + rainmaker__resp_set_user_mapping__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (Rainmaker__RespSetUserMapping *) + protobuf_c_message_unpack (&rainmaker__resp_set_user_mapping__descriptor, + allocator, len, data); +} +void rainmaker__resp_set_user_mapping__free_unpacked + (Rainmaker__RespSetUserMapping *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &rainmaker__resp_set_user_mapping__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void rainmaker__rmaker_config_payload__init + (Rainmaker__RMakerConfigPayload *message) +{ + static const Rainmaker__RMakerConfigPayload init_value = RAINMAKER__RMAKER_CONFIG_PAYLOAD__INIT; + *message = init_value; +} +size_t rainmaker__rmaker_config_payload__get_packed_size + (const Rainmaker__RMakerConfigPayload *message) +{ + assert(message->base.descriptor == &rainmaker__rmaker_config_payload__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t rainmaker__rmaker_config_payload__pack + (const Rainmaker__RMakerConfigPayload *message, + uint8_t *out) +{ + assert(message->base.descriptor == &rainmaker__rmaker_config_payload__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t rainmaker__rmaker_config_payload__pack_to_buffer + (const Rainmaker__RMakerConfigPayload *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &rainmaker__rmaker_config_payload__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +Rainmaker__RMakerConfigPayload * + rainmaker__rmaker_config_payload__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (Rainmaker__RMakerConfigPayload *) + protobuf_c_message_unpack (&rainmaker__rmaker_config_payload__descriptor, + allocator, len, data); +} +void rainmaker__rmaker_config_payload__free_unpacked + (Rainmaker__RMakerConfigPayload *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &rainmaker__rmaker_config_payload__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +static const ProtobufCFieldDescriptor rainmaker__cmd_set_user_mapping__field_descriptors[2] = +{ + { + "UserID", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_STRING, + 0, /* quantifier_offset */ + offsetof(Rainmaker__CmdSetUserMapping, userid), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "SecretKey", + 2, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_STRING, + 0, /* quantifier_offset */ + offsetof(Rainmaker__CmdSetUserMapping, secretkey), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned rainmaker__cmd_set_user_mapping__field_indices_by_name[] = { + 1, /* field[1] = SecretKey */ + 0, /* field[0] = UserID */ +}; +static const ProtobufCIntRange rainmaker__cmd_set_user_mapping__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 2 } +}; +const ProtobufCMessageDescriptor rainmaker__cmd_set_user_mapping__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "rainmaker.CmdSetUserMapping", + "CmdSetUserMapping", + "Rainmaker__CmdSetUserMapping", + "rainmaker", + sizeof(Rainmaker__CmdSetUserMapping), + 2, + rainmaker__cmd_set_user_mapping__field_descriptors, + rainmaker__cmd_set_user_mapping__field_indices_by_name, + 1, rainmaker__cmd_set_user_mapping__number_ranges, + (ProtobufCMessageInit) rainmaker__cmd_set_user_mapping__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor rainmaker__resp_set_user_mapping__field_descriptors[2] = +{ + { + "Status", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_ENUM, + 0, /* quantifier_offset */ + offsetof(Rainmaker__RespSetUserMapping, status), + &rainmaker__rmaker_config_status__descriptor, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "NodeId", + 2, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_STRING, + 0, /* quantifier_offset */ + offsetof(Rainmaker__RespSetUserMapping, nodeid), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned rainmaker__resp_set_user_mapping__field_indices_by_name[] = { + 1, /* field[1] = NodeId */ + 0, /* field[0] = Status */ +}; +static const ProtobufCIntRange rainmaker__resp_set_user_mapping__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 2 } +}; +const ProtobufCMessageDescriptor rainmaker__resp_set_user_mapping__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "rainmaker.RespSetUserMapping", + "RespSetUserMapping", + "Rainmaker__RespSetUserMapping", + "rainmaker", + sizeof(Rainmaker__RespSetUserMapping), + 2, + rainmaker__resp_set_user_mapping__field_descriptors, + rainmaker__resp_set_user_mapping__field_indices_by_name, + 1, rainmaker__resp_set_user_mapping__number_ranges, + (ProtobufCMessageInit) rainmaker__resp_set_user_mapping__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor rainmaker__rmaker_config_payload__field_descriptors[3] = +{ + { + "msg", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_ENUM, + 0, /* quantifier_offset */ + offsetof(Rainmaker__RMakerConfigPayload, msg), + &rainmaker__rmaker_config_msg_type__descriptor, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "cmd_set_user_mapping", + 10, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(Rainmaker__RMakerConfigPayload, payload_case), + offsetof(Rainmaker__RMakerConfigPayload, cmd_set_user_mapping), + &rainmaker__cmd_set_user_mapping__descriptor, + NULL, + 0 | PROTOBUF_C_FIELD_FLAG_ONEOF, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "resp_set_user_mapping", + 11, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(Rainmaker__RMakerConfigPayload, payload_case), + offsetof(Rainmaker__RMakerConfigPayload, resp_set_user_mapping), + &rainmaker__resp_set_user_mapping__descriptor, + NULL, + 0 | PROTOBUF_C_FIELD_FLAG_ONEOF, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned rainmaker__rmaker_config_payload__field_indices_by_name[] = { + 1, /* field[1] = cmd_set_user_mapping */ + 0, /* field[0] = msg */ + 2, /* field[2] = resp_set_user_mapping */ +}; +static const ProtobufCIntRange rainmaker__rmaker_config_payload__number_ranges[2 + 1] = +{ + { 1, 0 }, + { 10, 1 }, + { 0, 3 } +}; +const ProtobufCMessageDescriptor rainmaker__rmaker_config_payload__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "rainmaker.RMakerConfigPayload", + "RMakerConfigPayload", + "Rainmaker__RMakerConfigPayload", + "rainmaker", + sizeof(Rainmaker__RMakerConfigPayload), + 3, + rainmaker__rmaker_config_payload__field_descriptors, + rainmaker__rmaker_config_payload__field_indices_by_name, + 2, rainmaker__rmaker_config_payload__number_ranges, + (ProtobufCMessageInit) rainmaker__rmaker_config_payload__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCEnumValue rainmaker__rmaker_config_status__enum_values_by_number[3] = +{ + { "Success", "RAINMAKER__RMAKER_CONFIG_STATUS__Success", 0 }, + { "InvalidParam", "RAINMAKER__RMAKER_CONFIG_STATUS__InvalidParam", 1 }, + { "InvalidState", "RAINMAKER__RMAKER_CONFIG_STATUS__InvalidState", 2 }, +}; +static const ProtobufCIntRange rainmaker__rmaker_config_status__value_ranges[] = { +{0, 0},{0, 3} +}; +static const ProtobufCEnumValueIndex rainmaker__rmaker_config_status__enum_values_by_name[3] = +{ + { "InvalidParam", 1 }, + { "InvalidState", 2 }, + { "Success", 0 }, +}; +const ProtobufCEnumDescriptor rainmaker__rmaker_config_status__descriptor = +{ + PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, + "rainmaker.RMakerConfigStatus", + "RMakerConfigStatus", + "Rainmaker__RMakerConfigStatus", + "rainmaker", + 3, + rainmaker__rmaker_config_status__enum_values_by_number, + 3, + rainmaker__rmaker_config_status__enum_values_by_name, + 1, + rainmaker__rmaker_config_status__value_ranges, + NULL,NULL,NULL,NULL /* reserved[1234] */ +}; +static const ProtobufCEnumValue rainmaker__rmaker_config_msg_type__enum_values_by_number[2] = +{ + { "TypeCmdSetUserMapping", "RAINMAKER__RMAKER_CONFIG_MSG_TYPE__TypeCmdSetUserMapping", 0 }, + { "TypeRespSetUserMapping", "RAINMAKER__RMAKER_CONFIG_MSG_TYPE__TypeRespSetUserMapping", 1 }, +}; +static const ProtobufCIntRange rainmaker__rmaker_config_msg_type__value_ranges[] = { +{0, 0},{0, 2} +}; +static const ProtobufCEnumValueIndex rainmaker__rmaker_config_msg_type__enum_values_by_name[2] = +{ + { "TypeCmdSetUserMapping", 0 }, + { "TypeRespSetUserMapping", 1 }, +}; +const ProtobufCEnumDescriptor rainmaker__rmaker_config_msg_type__descriptor = +{ + PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, + "rainmaker.RMakerConfigMsgType", + "RMakerConfigMsgType", + "Rainmaker__RMakerConfigMsgType", + "rainmaker", + 2, + rainmaker__rmaker_config_msg_type__enum_values_by_number, + 2, + rainmaker__rmaker_config_msg_type__enum_values_by_name, + 1, + rainmaker__rmaker_config_msg_type__value_ranges, + NULL,NULL,NULL,NULL /* reserved[1234] */ +}; diff --git a/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.pb-c.h b/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.pb-c.h new file mode 100644 index 0000000..acc3b1e --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.pb-c.h @@ -0,0 +1,166 @@ +/* Generated by the protocol buffer compiler. DO NOT EDIT! */ +/* Generated from: esp_rmaker_user_mapping.proto */ + +#ifndef PROTOBUF_C_esp_5frmaker_5fuser_5fmapping_2eproto__INCLUDED +#define PROTOBUF_C_esp_5frmaker_5fuser_5fmapping_2eproto__INCLUDED + +#include + +PROTOBUF_C__BEGIN_DECLS + +#if PROTOBUF_C_VERSION_NUMBER < 1003000 +# error This file was generated by a newer version of protoc-c which is incompatible with your libprotobuf-c headers. Please update your headers. +#elif 1003003 < PROTOBUF_C_MIN_COMPILER_VERSION +# error This file was generated by an older version of protoc-c which is incompatible with your libprotobuf-c headers. Please regenerate this file with a newer version of protoc-c. +#endif + + +typedef struct _Rainmaker__CmdSetUserMapping Rainmaker__CmdSetUserMapping; +typedef struct _Rainmaker__RespSetUserMapping Rainmaker__RespSetUserMapping; +typedef struct _Rainmaker__RMakerConfigPayload Rainmaker__RMakerConfigPayload; + + +/* --- enums --- */ + +typedef enum _Rainmaker__RMakerConfigStatus { + RAINMAKER__RMAKER_CONFIG_STATUS__Success = 0, + RAINMAKER__RMAKER_CONFIG_STATUS__InvalidParam = 1, + RAINMAKER__RMAKER_CONFIG_STATUS__InvalidState = 2 + PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(RAINMAKER__RMAKER_CONFIG_STATUS) +} Rainmaker__RMakerConfigStatus; +typedef enum _Rainmaker__RMakerConfigMsgType { + RAINMAKER__RMAKER_CONFIG_MSG_TYPE__TypeCmdSetUserMapping = 0, + RAINMAKER__RMAKER_CONFIG_MSG_TYPE__TypeRespSetUserMapping = 1 + PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(RAINMAKER__RMAKER_CONFIG_MSG_TYPE) +} Rainmaker__RMakerConfigMsgType; + +/* --- messages --- */ + +struct _Rainmaker__CmdSetUserMapping +{ + ProtobufCMessage base; + char *userid; + char *secretkey; +}; +#define RAINMAKER__CMD_SET_USER_MAPPING__INIT \ + { PROTOBUF_C_MESSAGE_INIT (&rainmaker__cmd_set_user_mapping__descriptor) \ + , (char *)protobuf_c_empty_string, (char *)protobuf_c_empty_string } + + +struct _Rainmaker__RespSetUserMapping +{ + ProtobufCMessage base; + Rainmaker__RMakerConfigStatus status; + char *nodeid; +}; +#define RAINMAKER__RESP_SET_USER_MAPPING__INIT \ + { PROTOBUF_C_MESSAGE_INIT (&rainmaker__resp_set_user_mapping__descriptor) \ + , RAINMAKER__RMAKER_CONFIG_STATUS__Success, (char *)protobuf_c_empty_string } + + +typedef enum { + RAINMAKER__RMAKER_CONFIG_PAYLOAD__PAYLOAD__NOT_SET = 0, + RAINMAKER__RMAKER_CONFIG_PAYLOAD__PAYLOAD_CMD_SET_USER_MAPPING = 10, + RAINMAKER__RMAKER_CONFIG_PAYLOAD__PAYLOAD_RESP_SET_USER_MAPPING = 11 + PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(RAINMAKER__RMAKER_CONFIG_PAYLOAD__PAYLOAD) +} Rainmaker__RMakerConfigPayload__PayloadCase; + +struct _Rainmaker__RMakerConfigPayload +{ + ProtobufCMessage base; + Rainmaker__RMakerConfigMsgType msg; + Rainmaker__RMakerConfigPayload__PayloadCase payload_case; + union { + Rainmaker__CmdSetUserMapping *cmd_set_user_mapping; + Rainmaker__RespSetUserMapping *resp_set_user_mapping; + }; +}; +#define RAINMAKER__RMAKER_CONFIG_PAYLOAD__INIT \ + { PROTOBUF_C_MESSAGE_INIT (&rainmaker__rmaker_config_payload__descriptor) \ + , RAINMAKER__RMAKER_CONFIG_MSG_TYPE__TypeCmdSetUserMapping, RAINMAKER__RMAKER_CONFIG_PAYLOAD__PAYLOAD__NOT_SET, {0} } + + +/* Rainmaker__CmdSetUserMapping methods */ +void rainmaker__cmd_set_user_mapping__init + (Rainmaker__CmdSetUserMapping *message); +size_t rainmaker__cmd_set_user_mapping__get_packed_size + (const Rainmaker__CmdSetUserMapping *message); +size_t rainmaker__cmd_set_user_mapping__pack + (const Rainmaker__CmdSetUserMapping *message, + uint8_t *out); +size_t rainmaker__cmd_set_user_mapping__pack_to_buffer + (const Rainmaker__CmdSetUserMapping *message, + ProtobufCBuffer *buffer); +Rainmaker__CmdSetUserMapping * + rainmaker__cmd_set_user_mapping__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data); +void rainmaker__cmd_set_user_mapping__free_unpacked + (Rainmaker__CmdSetUserMapping *message, + ProtobufCAllocator *allocator); +/* Rainmaker__RespSetUserMapping methods */ +void rainmaker__resp_set_user_mapping__init + (Rainmaker__RespSetUserMapping *message); +size_t rainmaker__resp_set_user_mapping__get_packed_size + (const Rainmaker__RespSetUserMapping *message); +size_t rainmaker__resp_set_user_mapping__pack + (const Rainmaker__RespSetUserMapping *message, + uint8_t *out); +size_t rainmaker__resp_set_user_mapping__pack_to_buffer + (const Rainmaker__RespSetUserMapping *message, + ProtobufCBuffer *buffer); +Rainmaker__RespSetUserMapping * + rainmaker__resp_set_user_mapping__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data); +void rainmaker__resp_set_user_mapping__free_unpacked + (Rainmaker__RespSetUserMapping *message, + ProtobufCAllocator *allocator); +/* Rainmaker__RMakerConfigPayload methods */ +void rainmaker__rmaker_config_payload__init + (Rainmaker__RMakerConfigPayload *message); +size_t rainmaker__rmaker_config_payload__get_packed_size + (const Rainmaker__RMakerConfigPayload *message); +size_t rainmaker__rmaker_config_payload__pack + (const Rainmaker__RMakerConfigPayload *message, + uint8_t *out); +size_t rainmaker__rmaker_config_payload__pack_to_buffer + (const Rainmaker__RMakerConfigPayload *message, + ProtobufCBuffer *buffer); +Rainmaker__RMakerConfigPayload * + rainmaker__rmaker_config_payload__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data); +void rainmaker__rmaker_config_payload__free_unpacked + (Rainmaker__RMakerConfigPayload *message, + ProtobufCAllocator *allocator); +/* --- per-message closures --- */ + +typedef void (*Rainmaker__CmdSetUserMapping_Closure) + (const Rainmaker__CmdSetUserMapping *message, + void *closure_data); +typedef void (*Rainmaker__RespSetUserMapping_Closure) + (const Rainmaker__RespSetUserMapping *message, + void *closure_data); +typedef void (*Rainmaker__RMakerConfigPayload_Closure) + (const Rainmaker__RMakerConfigPayload *message, + void *closure_data); + +/* --- services --- */ + + +/* --- descriptors --- */ + +extern const ProtobufCEnumDescriptor rainmaker__rmaker_config_status__descriptor; +extern const ProtobufCEnumDescriptor rainmaker__rmaker_config_msg_type__descriptor; +extern const ProtobufCMessageDescriptor rainmaker__cmd_set_user_mapping__descriptor; +extern const ProtobufCMessageDescriptor rainmaker__resp_set_user_mapping__descriptor; +extern const ProtobufCMessageDescriptor rainmaker__rmaker_config_payload__descriptor; + +PROTOBUF_C__END_DECLS + + +#endif /* PROTOBUF_C_esp_5frmaker_5fuser_5fmapping_2eproto__INCLUDED */ diff --git a/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.proto b/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.proto new file mode 100644 index 0000000..c8e83fb --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_user_mapping.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package rainmaker; + +enum RMakerConfigStatus { + Success = 0; + InvalidParam = 1; + InvalidState = 2; +} + +message CmdSetUserMapping { + string UserID = 1; + string SecretKey = 2; +} +message RespSetUserMapping { + RMakerConfigStatus Status = 1; + string NodeId = 2; +} + +enum RMakerConfigMsgType { + TypeCmdSetUserMapping = 0; + TypeRespSetUserMapping = 1; +} + +message RMakerConfigPayload { + RMakerConfigMsgType msg = 1; + oneof payload { + CmdSetUserMapping cmd_set_user_mapping = 10; + RespSetUserMapping resp_set_user_mapping = 11; + } +} diff --git a/components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt.c b/components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt.c new file mode 100644 index 0000000..a40318d --- /dev/null +++ b/components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt.c @@ -0,0 +1,135 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +#include "esp_rmaker_mqtt.h" +#include "esp_rmaker_mqtt_budget.h" + +static const char *TAG = "esp_rmaker_mqtt"; +static esp_rmaker_mqtt_config_t g_mqtt_config; + +esp_rmaker_mqtt_conn_params_t *esp_rmaker_mqtt_get_conn_params(void) +{ + if (g_mqtt_config.get_conn_params) { + return g_mqtt_config.get_conn_params(); + } else { + return esp_rmaker_get_mqtt_conn_params(); + } +} + +esp_err_t esp_rmaker_mqtt_setup(esp_rmaker_mqtt_config_t mqtt_config) +{ + g_mqtt_config = mqtt_config; + g_mqtt_config.setup_done = true; + return ESP_OK; +} + +esp_err_t esp_rmaker_mqtt_init(esp_rmaker_mqtt_conn_params_t *conn_params) +{ + if (!g_mqtt_config.setup_done) { + esp_rmaker_mqtt_glue_setup(&g_mqtt_config); + } + if (g_mqtt_config.init) { + esp_err_t err = g_mqtt_config.init(conn_params); + if (err == ESP_OK) { + if (esp_rmaker_mqtt_budgeting_init() != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialise MQTT Budgeting."); + } + } + return err; + } + ESP_LOGW(TAG, "esp_rmaker_mqtt_init not registered"); + return ESP_OK; +} + +void esp_rmaker_mqtt_deinit(void) +{ + esp_rmaker_mqtt_budgeting_deinit(); + if (g_mqtt_config.deinit) { + return g_mqtt_config.deinit(); + } + ESP_LOGW(TAG, "esp_rmaker_mqtt_deinit not registered"); +} + +esp_err_t esp_rmaker_mqtt_connect(void) +{ + if (g_mqtt_config.connect) { + esp_err_t err = g_mqtt_config.connect(); + if (err == ESP_OK) { + esp_rmaker_mqtt_budgeting_start(); + } + return err; + } + ESP_LOGW(TAG, "esp_rmaker_mqtt_connect not registered"); + return ESP_OK; +} + + +esp_err_t esp_rmaker_mqtt_disconnect(void) +{ + esp_rmaker_mqtt_budgeting_stop(); + if (g_mqtt_config.disconnect) { + return g_mqtt_config.disconnect(); + } + ESP_LOGW(TAG, "esp_rmaker_mqtt_disconnect not registered"); + return ESP_OK; +} + +esp_err_t esp_rmaker_mqtt_subscribe(const char *topic, esp_rmaker_mqtt_subscribe_cb_t cb, uint8_t qos, void *priv_data) +{ + if (g_mqtt_config.subscribe) { + return g_mqtt_config.subscribe(topic, cb, qos, priv_data); + } + ESP_LOGW(TAG, "esp_rmaker_mqtt_subscribe not registered"); + return ESP_OK; +} + +esp_err_t esp_rmaker_mqtt_unsubscribe(const char *topic) +{ + if (g_mqtt_config.unsubscribe) { + return g_mqtt_config.unsubscribe(topic); + } + ESP_LOGW(TAG, "esp_rmaker_mqtt_unsubscribe not registered"); + return ESP_OK; +} + +esp_err_t esp_rmaker_mqtt_publish(const char *topic, void *data, size_t data_len, uint8_t qos, int *msg_id) +{ + if (esp_rmaker_mqtt_is_budget_available() != true) { + ESP_LOGE(TAG, "Out of MQTT Budget. Dropping publish message."); + return ESP_FAIL; + } + if (g_mqtt_config.publish) { + esp_err_t err = g_mqtt_config.publish(topic, data, data_len, qos, msg_id); + if (err == ESP_OK) { + esp_rmaker_mqtt_decrease_budget(1); + } + return err; + } + ESP_LOGW(TAG, "esp_rmaker_mqtt_publish not registered"); + return ESP_OK; +} + +void esp_rmaker_create_mqtt_topic(char *buf, size_t buf_size, const char *topic_suffix, const char *rule) +{ +#ifdef CONFIG_ESP_RMAKER_MQTT_USE_BASIC_INGEST_TOPICS + snprintf(buf, buf_size, "$aws/rules/%s/node/%s/%s", rule, esp_rmaker_get_node_id(), topic_suffix); +#else + snprintf(buf, buf_size, "node/%s/%s", esp_rmaker_get_node_id(), topic_suffix); +#endif +} diff --git a/components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt_budget.c b/components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt_budget.c new file mode 100644 index 0000000..bbdbc29 --- /dev/null +++ b/components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt_budget.c @@ -0,0 +1,183 @@ + +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +static const char *TAG = "esp_rmaker_mqtt_budget"; + +#ifdef CONFIG_ESP_RMAKER_MQTT_ENABLE_BUDGETING + +#include +#include +#include + +#define DEFAULT_BUDGET CONFIG_ESP_RMAKER_MQTT_DEFAULT_BUDGET +#define MAX_BUDGET CONFIG_ESP_RMAKER_MQTT_MAX_BUDGET +#define BUDGET_REVIVE_COUNT CONFIG_ESP_RMAKER_MQTT_BUDGET_REVIVE_COUNT +#define BUDGET_REVIVE_PERIOD CONFIG_ESP_RMAKER_MQTT_BUDGET_REVIVE_PERIOD + +static int16_t mqtt_budget = DEFAULT_BUDGET; +static TimerHandle_t mqtt_budget_timer; +static SemaphoreHandle_t mqtt_budget_lock; +#define SEMAPHORE_DELAY_MSEC 500 + +bool esp_rmaker_mqtt_is_budget_available(void) +{ + if (mqtt_budget_lock == NULL) { + ESP_LOGW(TAG, "MQTT budgeting not started yet. Allowing publish."); + return true; + } + if (xSemaphoreTake(mqtt_budget_lock, SEMAPHORE_DELAY_MSEC/portTICK_PERIOD_MS) != pdTRUE) { + ESP_LOGW(TAG, "Could not acquire MQTT budget lock. Allowing publish."); + return true; + } + int16_t budget = mqtt_budget; + xSemaphoreGive(mqtt_budget_lock); + return budget ? true : false; +} + +esp_err_t esp_rmaker_mqtt_increase_budget(uint8_t budget) +{ + if (mqtt_budget_lock == NULL) { + ESP_LOGW(TAG, "MQTT budgeting not started. Not increasing the budget."); + return ESP_FAIL; + } + if (xSemaphoreTake(mqtt_budget_lock, SEMAPHORE_DELAY_MSEC/portTICK_PERIOD_MS) != pdTRUE) { + ESP_LOGE(TAG, "Failed to increase MQTT budget."); + return ESP_FAIL; + } + mqtt_budget += budget; + if (mqtt_budget > MAX_BUDGET) { + mqtt_budget = MAX_BUDGET; + } + xSemaphoreGive(mqtt_budget_lock); + ESP_LOGD(TAG, "MQTT budget increased to %d", mqtt_budget); + return ESP_OK; +} + +esp_err_t esp_rmaker_mqtt_decrease_budget(uint8_t budget) +{ + if (mqtt_budget_lock == NULL) { + ESP_LOGW(TAG, "MQTT budgeting not started. Not decreasing the budget."); + return ESP_FAIL; + } + if (xSemaphoreTake(mqtt_budget_lock, SEMAPHORE_DELAY_MSEC/portTICK_PERIOD_MS) != pdTRUE) { + ESP_LOGE(TAG, "Failed to decrease MQTT budget."); + return ESP_FAIL; + } + mqtt_budget -= budget; + if (mqtt_budget < 0) { + mqtt_budget = 0; + } + xSemaphoreGive(mqtt_budget_lock); + ESP_LOGD(TAG, "MQTT budget decreased to %d.", mqtt_budget); + return ESP_OK; +} + +static void esp_rmaker_mqtt_revive_budget(TimerHandle_t handle) +{ + esp_rmaker_mqtt_increase_budget(BUDGET_REVIVE_COUNT); +} + +esp_err_t esp_rmaker_mqtt_budgeting_start(void) +{ + if (mqtt_budget_timer) { + xTimerStart(mqtt_budget_timer, 0); + return ESP_OK; + } + return ESP_FAIL; +} + +esp_err_t esp_rmaker_mqtt_budgeting_stop(void) +{ + if (mqtt_budget_timer) { + xTimerStop(mqtt_budget_timer, 100); + return ESP_OK; + } + return ESP_FAIL; +} + +esp_err_t esp_rmaker_mqtt_budgeting_deinit(void) +{ + if (mqtt_budget_timer) { + esp_rmaker_mqtt_budgeting_stop(); + xTimerDelete(mqtt_budget_timer, 100); + mqtt_budget_timer = NULL; + } + if (mqtt_budget_lock) { + vSemaphoreDelete(mqtt_budget_lock); + mqtt_budget_lock = NULL; + } + return ESP_OK; +} + +esp_err_t esp_rmaker_mqtt_budgeting_init(void) +{ + if (mqtt_budget_timer) { + ESP_LOGI(TAG, "MQTT budgeting already initialised."); + return ESP_OK; + } + + mqtt_budget_lock = xSemaphoreCreateMutex(); + if (!mqtt_budget_lock) { + return ESP_FAIL; + } + + mqtt_budget_timer = xTimerCreate("mqtt_budget_tm", (BUDGET_REVIVE_PERIOD * 1000) / portTICK_PERIOD_MS, + pdTRUE, NULL, esp_rmaker_mqtt_revive_budget); + if (mqtt_budget_timer) { + ESP_LOGI(TAG, "MQTT Budgeting initialised. Default: %d, Max: %d, Revive count: %d, Revive period: %d", + DEFAULT_BUDGET, MAX_BUDGET, BUDGET_REVIVE_COUNT, BUDGET_REVIVE_PERIOD); + return ESP_OK; + } + return ESP_FAIL; +} + +#else /* ! CONFIG_ESP_RMAKER_MQTT_ENABLE_BUDGETING */ + +esp_err_t esp_rmaker_mqtt_budgeting_init(void) +{ + /* Adding a print only here, because this is always going to be the first function + * to be invoked since it is called from MQTT init. Else, MQTT itself is going to fail. + */ + ESP_LOGW(TAG, "MQTT Budgeting is not enabled."); + return ESP_OK; +} + +esp_err_t esp_rmaker_mqtt_budgeting_deinit(void) +{ + return ESP_FAIL; +} + +esp_err_t esp_rmaker_mqtt_budgeting_stop(void) +{ + return ESP_FAIL; +} + +esp_err_t esp_rmaker_mqtt_budgeting_start(void) +{ + return ESP_FAIL; +} + +esp_err_t esp_rmaker_mqtt_increase_budget(uint8_t budget) +{ + return ESP_OK; +} + +esp_err_t esp_rmaker_mqtt_decrease_budget(uint8_t budget) +{ + return ESP_OK; +} + +bool esp_rmaker_mqtt_is_budget_available(void) +{ + return true; +} + +#endif /* ! CONFIG_ESP_RMAKER_MQTT_ENABLE_BUDGETING */ diff --git a/components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt_budget.h b/components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt_budget.h new file mode 100644 index 0000000..e6ec584 --- /dev/null +++ b/components/esp_rainmaker/src/mqtt/esp_rmaker_mqtt_budget.h @@ -0,0 +1,17 @@ + +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +esp_err_t esp_rmaker_mqtt_budgeting_init(void); +esp_err_t esp_rmaker_mqtt_budgeting_deinit(void); +esp_err_t esp_rmaker_mqtt_budgeting_stop(void); +esp_err_t esp_rmaker_mqtt_budgeting_start(void); +esp_err_t esp_rmaker_mqtt_increase_budget(uint8_t budget); +esp_err_t esp_rmaker_mqtt_decrease_budget(uint8_t budget); diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota.c b/components/esp_rainmaker/src/ota/esp_rmaker_ota.c new file mode 100644 index 0000000..d816bc0 --- /dev/null +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota.c @@ -0,0 +1,726 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if CONFIG_BT_ENABLED +#include +#endif /* CONFIG_BT_ENABLED */ + +#include +#include +#include +#include "esp_rmaker_internal.h" +#include "esp_rmaker_ota_internal.h" + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) +// Features supported in 4.4+ + +#ifdef CONFIG_ESP_RMAKER_USE_CERT_BUNDLE +#define ESP_RMAKER_USE_CERT_BUNDLE +#include +#endif + +#else + +#ifdef CONFIG_ESP_RMAKER_USE_CERT_BUNDLE +#warning "Certificate Bundle not supported below IDF v4.4. Using provided certificate instead." +#endif + +#endif /* !IDF4.4 */ +static const char *TAG = "esp_rmaker_ota"; + +#define OTA_REBOOT_TIMER_SEC 10 +#define DEF_HTTP_TX_BUFFER_SIZE 1024 +#define DEF_HTTP_RX_BUFFER_SIZE CONFIG_ESP_RMAKER_OTA_HTTP_RX_BUFFER_SIZE +#define RMAKER_OTA_ROLLBACK_WAIT_PERIOD CONFIG_ESP_RMAKER_OTA_ROLLBACK_WAIT_PERIOD +extern const char esp_rmaker_ota_def_cert[] asm("_binary_rmaker_ota_server_crt_start"); +const char *ESP_RMAKER_OTA_DEFAULT_SERVER_CERT = esp_rmaker_ota_def_cert; +ESP_EVENT_DEFINE_BASE(RMAKER_OTA_EVENT); + +typedef enum { + OTA_OK = 0, + OTA_ERR, + OTA_DELAYED +} esp_rmaker_ota_action_t; + +static esp_rmaker_ota_t *g_ota_priv; + +char *esp_rmaker_ota_status_to_string(ota_status_t status) +{ + switch (status) { + case OTA_STATUS_IN_PROGRESS: + return "in-progress"; + case OTA_STATUS_SUCCESS: + return "success"; + case OTA_STATUS_FAILED: + return "failed"; + case OTA_STATUS_DELAYED: + return "delayed"; + case OTA_STATUS_REJECTED: + return "rejected"; + default: + return "invalid"; + } + return "invalid"; +} + +esp_rmaker_ota_event_t esp_rmaker_ota_status_to_event(ota_status_t status) +{ + switch (status) { + case OTA_STATUS_IN_PROGRESS: + return RMAKER_OTA_EVENT_IN_PROGRESS; + case OTA_STATUS_SUCCESS: + return RMAKER_OTA_EVENT_SUCCESSFUL; + case OTA_STATUS_FAILED: + return RMAKER_OTA_EVENT_FAILED; + case OTA_STATUS_DELAYED: + return RMAKER_OTA_EVENT_DELAYED; + case OTA_STATUS_REJECTED: + return RMAKER_OTA_EVENT_REJECTED; + default: + ESP_LOGD(TAG, "No Rmaker OTA Event for given status: %d: %s", + status, esp_rmaker_ota_status_to_string(status)); + } + return RMAKER_OTA_EVENT_INVALID; +} + +static inline esp_err_t esp_rmaker_ota_post_event(esp_rmaker_event_t event_id, void* data, size_t data_size) +{ + return esp_event_post(RMAKER_OTA_EVENT, event_id, data, data_size, portMAX_DELAY); +} + +esp_err_t esp_rmaker_ota_report_status(esp_rmaker_ota_handle_t ota_handle, ota_status_t status, char *additional_info) +{ + ESP_LOGI(TAG, "Reporting %s: %s", esp_rmaker_ota_status_to_string(status), additional_info); + + if (!ota_handle) { + return ESP_FAIL; + } + esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)ota_handle; + esp_err_t err = ESP_FAIL; + if (ota->type == OTA_USING_PARAMS) { + err = esp_rmaker_ota_report_status_using_params(ota_handle, status, additional_info); + } else if (ota->type == OTA_USING_TOPICS) { + err = esp_rmaker_ota_report_status_using_topics(ota_handle, status, additional_info); + } + if (err == ESP_OK) { + esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)ota_handle; + ota->last_reported_status = status; + } + esp_rmaker_ota_post_event(esp_rmaker_ota_status_to_event(status), additional_info, strlen(additional_info) + 1); + return err; +} + +void esp_rmaker_ota_common_cb(void *priv) +{ + if (!priv) { + return; + } + esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)priv; + if (!ota->url) { + goto ota_finish; + } + esp_rmaker_ota_data_t ota_data = { + .url = ota->url, + .filesize = ota->filesize, + .fw_version = ota->fw_version, + .ota_job_id = (char *)ota->transient_priv, + .server_cert = ota->server_cert, + .priv = ota->priv, + .metadata = ota->metadata + }; + ota->ota_cb((esp_rmaker_ota_handle_t) ota, &ota_data); +ota_finish: + if (ota->type == OTA_USING_PARAMS) { + esp_rmaker_ota_finish_using_params(ota); + } else if (ota->type == OTA_USING_TOPICS) { + esp_rmaker_ota_finish_using_topics(ota); + } +} + +static esp_err_t validate_image_header(esp_rmaker_ota_handle_t ota_handle, + esp_app_desc_t *new_app_info) +{ + if (new_app_info == NULL) { + return ESP_ERR_INVALID_ARG; + } + + const esp_partition_t *running = esp_ota_get_running_partition(); + esp_app_desc_t running_app_info; + if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK) { + ESP_LOGD(TAG, "Running firmware version: %s", running_app_info.version); + } + +#ifndef CONFIG_ESP_RMAKER_SKIP_PROJECT_NAME_CHECK + if (memcmp(new_app_info->project_name, running_app_info.project_name, sizeof(new_app_info->project_name)) != 0) { + ESP_LOGW(TAG, "OTA Image built for Project: %s. Expected: %s", + new_app_info->project_name, running_app_info.project_name); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_REJECTED, "Project Name mismatch"); + return ESP_FAIL; + } +#endif + +#ifndef CONFIG_ESP_RMAKER_SKIP_VERSION_CHECK + if (memcmp(new_app_info->version, running_app_info.version, sizeof(new_app_info->version)) == 0) { + ESP_LOGW(TAG, "Current running version is same as the new. We will not continue the update."); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_REJECTED, "Same version received"); + return ESP_FAIL; + } +#endif + +#ifndef CONFIG_ESP_RMAKER_SKIP_SECURE_VERSION_CHECK + if (esp_efuse_check_secure_version(new_app_info->secure_version) == false) { + ESP_LOGW(TAG, "New secure version is lower than stored in efuse. We will not continue the update."); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_REJECTED, "Lower secure version received"); + return ESP_FAIL; + } +#endif + + return ESP_OK; +} + +#ifdef CONFIG_ESP_RMAKER_OTA_TIME_SUPPORT + +/* Retry delay for cases wherein time info itself is not available */ +#define OTA_FETCH_RETRY_DELAY 30 +#define MINUTES_IN_DAY (24 * 60) +#define OTA_DELAY_TIME_BUFFER 5 + +/* Check if time data is available in the metadata. Format + * {"download_window":{"end":1155,"start":1080},"validity":{"end":1665426600,"start":1665081000}} + */ +esp_rmaker_ota_action_t esp_rmaker_ota_handle_time(jparse_ctx_t *jptr, esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data) +{ + bool time_info = false; + int start_min = -1, end_min = -1, start_date = -1, end_date = -1; + if (json_obj_get_object(jptr, "download_window") == 0) { + /* Download window means specific time of day. Eg, Between 02:00am and 05:00am only */ + time_info = true; + json_obj_get_int(jptr, "start", &start_min); + json_obj_get_int(jptr, "end", &end_min); + json_obj_leave_object(jptr); + ESP_LOGI(TAG, "Download Window : %d %d", start_min, end_min); + } + if (json_obj_get_object(jptr, "validity") == 0) { + /* Validity indicates start and end epoch time, typicaly useful if OTA is to be performed between some dates */ + time_info = true; + json_obj_get_int(jptr, "start", &start_date); + json_obj_get_int(jptr, "end", &end_date); + json_obj_leave_object(jptr); + ESP_LOGI(TAG, "Validity : %d %d", start_date, end_date); + } + if (time_info) { + /* If time info is present, but time is not yet synchronised, we will re-fetch OTA after some time */ + if (esp_rmaker_time_check() != true) { + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_DELAYED, "No time information available yet."); + esp_rmaker_ota_fetch_with_delay(OTA_FETCH_RETRY_DELAY); + return OTA_DELAYED; + } + time_t current_timestamp = 0; + struct tm current_time = {0}; + time(¤t_timestamp); + localtime_r(¤t_timestamp, ¤t_time); + + /* Check for date validity first */ + if ((start_date != -1) && (current_timestamp < start_date)) { + int delay_time = start_date - current_timestamp; + /* The delay logic here can include the start_min and end_min as well, but it makes the logic quite complex, + * just for a minor optimisation. + */ + ESP_LOGI(TAG, "Delaying OTA by %d seconds (%d min) as it is not valid yet.", delay_time, delay_time / 60); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_DELAYED, "Not within valid window."); + esp_rmaker_ota_fetch_with_delay(delay_time + OTA_DELAY_TIME_BUFFER); + return OTA_DELAYED; + } else if ((end_date != -1) && (current_timestamp > end_date)) { + ESP_LOGE(TAG, "OTA download window lapsed"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "OTA download window lapsed."); + return OTA_ERR; + } + + /* Check for download window */ + if (start_min != -1) { + /* end_min is required if start_min is provided */ + if (end_min == -1) { + ESP_LOGE(TAG, "Download window should have an end time if start time is specified."); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Invalid download window specified."); + return OTA_ERR; + } + int cur_min = current_time.tm_hour * 60 + current_time.tm_min; + if (start_min > end_min) { + /* This means that the window is across midnight (Eg. 23:00 to 02:00 i.e. 1380 to 120). + * We are just moving the window here such that start_min becomes 0 and the comparisons are simplified. + * For this example, diff_min will be 1440 - 1380 = 60. + * Effective end_min: 180 + * If cur_time is 18:00, effective cur_time = 1080 + 60 = 1140 + * If cur_time is 23:30, effective cur_time = 1410 + 60 = 1470 ( > MINUTES_IN_DAY) + * So, cur_time = 1470 - 1440 = 30 + * */ + int diff_min = MINUTES_IN_DAY - start_min; + start_min = 0; + end_min += diff_min; + cur_min += diff_min; + if (cur_min >= MINUTES_IN_DAY) { + cur_min -= MINUTES_IN_DAY; + } + } + /* Current time is within OTA download window */ + if ((cur_min >= start_min) && (cur_min <= end_min)) { + ESP_LOGI(TAG, "OTA received within download window."); + return OTA_OK; + } else { + /* Delay the OTA if it is not in the download window. Even if it later goes outside the valid date range, + * that will be handled in subsequent ota fetch. Reporting failure here itself would mark the OTA job + * as failed and the node will no more get the OTA even if it tries to fetch it again due to a reboot or + * other action within the download window. + */ + int delay_min = start_min - cur_min; + if (delay_min < 0) { + delay_min += MINUTES_IN_DAY; + } + ESP_LOGI(TAG, "Delaying OTA by %d seconds (%d min) as it is not within download window.", delay_min * 60, delay_min); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_DELAYED, "Not within download window."); + esp_rmaker_ota_fetch_with_delay(delay_min * 60 + OTA_DELAY_TIME_BUFFER); + return OTA_DELAYED; + } + } else { + ESP_LOGI(TAG, "OTA received within validity period."); + } + } + return OTA_OK; +} + +#endif /* CONFIG_ESP_RMAKER_OTA_TIME_SUPPORT */ + +esp_rmaker_ota_action_t esp_rmaker_ota_handle_metadata(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data) +{ + if (!ota_data->metadata) { + return ESP_OK; + } + esp_rmaker_ota_action_t ota_action = OTA_OK; + jparse_ctx_t jctx; + if (json_parse_start(&jctx, ota_data->metadata, strlen(ota_data->metadata)) == 0) { +#ifdef CONFIG_ESP_RMAKER_OTA_TIME_SUPPORT + /* Handle OTA timing data, if any */ + ota_action = esp_rmaker_ota_handle_time(&jctx, ota_handle, ota_data); +#endif /* CONFIG_ESP_RMAKER_OTA_TIME_SUPPORT */ + json_parse_end(&jctx); + } + return ota_action; +} + +esp_err_t esp_rmaker_ota_default_cb(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data) +{ + if (!ota_data->url) { + return ESP_FAIL; + } + /* Handle OTA metadata, if any */ + if (ota_data->metadata) { + if (esp_rmaker_ota_handle_metadata(ota_handle, ota_data) != OTA_OK) { + ESP_LOGW(TAG, "Cannot proceed with the OTA as per the metadata received."); + return ESP_FAIL; + } + } + esp_rmaker_ota_post_event(RMAKER_OTA_EVENT_STARTING, NULL, 0); + int buffer_size_tx = DEF_HTTP_TX_BUFFER_SIZE; + /* In case received url is longer, we will increase the tx buffer size + * to accomodate the longer url and other headers. + */ + if (strlen(ota_data->url) > buffer_size_tx) { + buffer_size_tx = strlen(ota_data->url) + 128; + } + esp_err_t ota_finish_err = ESP_OK; + esp_http_client_config_t config = { + .url = ota_data->url, +#ifdef ESP_RMAKER_USE_CERT_BUNDLE + .crt_bundle_attach = esp_crt_bundle_attach, +#else + .cert_pem = ota_data->server_cert, +#endif + .timeout_ms = 5000, + .buffer_size = DEF_HTTP_RX_BUFFER_SIZE, + .buffer_size_tx = buffer_size_tx, + .keep_alive_enable = true + }; +#ifdef CONFIG_ESP_RMAKER_SKIP_COMMON_NAME_CHECK + config.skip_cert_common_name_check = true; +#endif + + esp_https_ota_config_t ota_config = { + .http_config = &config, + }; + + if (ota_data->filesize) { + ESP_LOGD(TAG, "Received file size: %d", ota_data->filesize); + } + + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, "Starting OTA Upgrade"); + + /* Using a warning just to highlight the message */ + ESP_LOGW(TAG, "Starting OTA. This may take time."); + esp_https_ota_handle_t https_ota_handle = NULL; + esp_err_t err = esp_https_ota_begin(&ota_config, &https_ota_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "ESP HTTPS OTA Begin failed"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "ESP HTTPS OTA Begin failed"); + return ESP_FAIL; + } + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +/* Get the current Wi-Fi power save type. In case OTA fails and we need this + * to restore power saving. + */ + wifi_ps_type_t ps_type; + esp_wifi_get_ps(&ps_type); +/* Disable Wi-Fi power save to speed up OTA, iff BT is controller is idle/disabled. + * Co-ex requirement, device panics otherwise.*/ +#if CONFIG_BT_ENABLED + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + esp_wifi_set_ps(WIFI_PS_NONE); + } +#else + esp_wifi_set_ps(WIFI_PS_NONE); +#endif /* CONFIG_BT_ENABLED */ +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + + esp_app_desc_t app_desc; + err = esp_https_ota_get_img_desc(https_ota_handle, &app_desc); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_https_ota_read_img_desc failed"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Failed to read image decription"); + goto ota_end; + } + err = validate_image_header(ota_handle, &app_desc); + if (err != ESP_OK) { + ESP_LOGE(TAG, "image header verification failed"); + goto ota_end; + } + + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, "Downloading Firmware Image"); + int count = 0; + while (1) { + err = esp_https_ota_perform(https_ota_handle); + if (err == ESP_ERR_INVALID_VERSION) { + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_REJECTED, "Chip revision mismatch"); + goto ota_end; + } + if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) { + break; + } + /* esp_https_ota_perform returns after every read operation which gives user the ability to + * monitor the status of OTA upgrade by calling esp_https_ota_get_image_len_read, which gives length of image + * data read so far. + * We are using a counter just to reduce the number of prints + */ + + count++; + if (count == 50) { + ESP_LOGI(TAG, "Image bytes read: %d", esp_https_ota_get_image_len_read(https_ota_handle)); + count = 0; + } + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "ESP_HTTPS_OTA upgrade failed %s", esp_err_to_name(err)); + char description[40]; + snprintf(description, sizeof(description), "OTA failed: Error %s", esp_err_to_name(err)); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, description); + } + + if (esp_https_ota_is_complete_data_received(https_ota_handle) != true) { + // the OTA image was not completely received and user can customise the response to this situation. + ESP_LOGE(TAG, "Complete data was not received."); + } else { + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, "Firmware Image download complete"); + } + +ota_end: +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +#ifdef CONFIG_BT_ENABLED + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + esp_wifi_set_ps(ps_type); + } +#else + esp_wifi_set_ps(ps_type); +#endif /* CONFIG_BT_ENABLED */ +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + ota_finish_err = esp_https_ota_finish(https_ota_handle); + if ((err == ESP_OK) && (ota_finish_err == ESP_OK)) { +#ifdef CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, RMAKER_OTA_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err == ESP_OK) { + uint8_t ota_update = 1; + nvs_set_blob(handle, RMAKER_OTA_UPDATE_FLAG_NVS_NAME, &ota_update, sizeof(ota_update)); + nvs_close(handle); + } + /* Success will be reported after a reboot since Rollback is enabled */ + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, "Rebooting into new firmware"); +#else + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_SUCCESS, "OTA Upgrade finished successfully"); +#endif +#ifndef CONFIG_ESP_RMAKER_OTA_DISABLE_AUTO_REBOOT + ESP_LOGI(TAG, "OTA upgrade successful. Rebooting in %d seconds...", OTA_REBOOT_TIMER_SEC); + esp_rmaker_reboot(OTA_REBOOT_TIMER_SEC); +#else + ESP_LOGI(TAG, "OTA upgrade successful. Auto reboot is disabled. Requesting a Reboot via Event handler."); + esp_rmaker_ota_post_event(RMAKER_OTA_EVENT_REQ_FOR_REBOOT, NULL, 0); +#endif + return ESP_OK; + } else { + if (ota_finish_err == ESP_ERR_OTA_VALIDATE_FAILED) { + ESP_LOGE(TAG, "Image validation failed, image is corrupted"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Image validation failed"); + } else { + /* Not reporting status here, because relevant error will already be reported + * in some earlier step + */ + ESP_LOGE(TAG, "ESP_HTTPS_OTA upgrade failed %d", ota_finish_err); + } + } + return ESP_FAIL; +} + +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)arg; + esp_rmaker_ota_diag_status_t diag_status = OTA_DIAG_STATUS_SUCCESS; + if (ota->ota_diag) { + esp_rmaker_ota_diag_priv_t ota_diag_priv = { + .state = OTA_DIAG_STATE_POST_MQTT, + .rmaker_ota = ota->validation_in_progress + }; + diag_status = ota->ota_diag(&ota_diag_priv, ota->priv); + } + if (diag_status == OTA_DIAG_STATUS_SUCCESS) { + esp_rmaker_ota_mark_valid(); + } else if (diag_status == OTA_DIAG_STATUS_FAIL) { + ESP_LOGE(TAG, "Diagnostics failed! Start rollback to the previous version ..."); + esp_rmaker_ota_mark_invalid(); + } else { + ESP_LOGW(TAG, "Waiting for application to validate OTA."); + } +} + +static esp_err_t esp_rmaker_erase_rollback_flag(void) +{ + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, RMAKER_OTA_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err == ESP_OK) { + nvs_erase_key(handle, RMAKER_OTA_UPDATE_FLAG_NVS_NAME); + nvs_commit(handle); + nvs_close(handle); + } + return ESP_OK; +} + +esp_err_t esp_rmaker_ota_mark_valid(void) +{ + const esp_partition_t *running = esp_ota_get_running_partition(); + esp_ota_img_states_t ota_state; + if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) { + if (ota_state != ESP_OTA_IMG_PENDING_VERIFY) { + ESP_LOGW(TAG, "OTA Already marked as valid"); + return ESP_ERR_INVALID_STATE; + } + } + + esp_event_handler_unregister(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_CONNECTED, &event_handler); + esp_rmaker_ota_t *ota = g_ota_priv; + if (!ota) { + return ESP_ERR_INVALID_ARG; + } + esp_ota_mark_app_valid_cancel_rollback(); + esp_rmaker_erase_rollback_flag(); + ota->ota_in_progress = false; + if (ota->rollback_timer) { + xTimerStop(ota->rollback_timer, portMAX_DELAY); + xTimerDelete(ota->rollback_timer, portMAX_DELAY); + ota->rollback_timer = NULL; + } + esp_rmaker_ota_report_status((esp_rmaker_ota_handle_t )ota, OTA_STATUS_SUCCESS, "OTA Upgrade finished and verified successfully"); + if (ota->type == OTA_USING_TOPICS) { + if (esp_rmaker_ota_fetch_with_delay(RMAKER_OTA_FETCH_DELAY) != ESP_OK) { + ESP_LOGE(TAG, "Failed to create OTA Fetch timer."); + } + } + return ESP_OK; +} + +esp_err_t esp_rmaker_ota_mark_invalid(void) +{ + const esp_partition_t *running = esp_ota_get_running_partition(); + esp_ota_img_states_t ota_state; + if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) { + if (ota_state != ESP_OTA_IMG_PENDING_VERIFY) { + ESP_LOGE(TAG, "Cannot rollback due to invalid OTA state."); + return ESP_ERR_INVALID_STATE; + } + } + esp_ota_mark_app_invalid_rollback_and_reboot(); + return ESP_OK; +} + + +static void esp_ota_rollback(TimerHandle_t handle) +{ + ESP_LOGE(TAG, "Could not verify firmware even after %d seconds since boot-up. Rolling back.", + RMAKER_OTA_ROLLBACK_WAIT_PERIOD); + esp_rmaker_ota_mark_invalid(); +} + +static esp_err_t esp_ota_check_for_mqtt(esp_rmaker_ota_t *ota) +{ + ota->rollback_timer = xTimerCreate("ota_rollback_tm", (RMAKER_OTA_ROLLBACK_WAIT_PERIOD * 1000) / portTICK_PERIOD_MS, + pdTRUE, NULL, esp_ota_rollback); + if (ota->rollback_timer) { + xTimerStart(ota->rollback_timer, 0); + } else { + ESP_LOGW(TAG, "Could not create rollback timer. Will require manual reboot if firmware verification fails"); + } + + return esp_event_handler_register(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_CONNECTED, &event_handler, ota); +} + +static void esp_rmaker_ota_manage_rollback(esp_rmaker_ota_t *ota) +{ + /* If rollback is enabled, and the ota update flag is found, it means that the OTA validation is pending + */ + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, RMAKER_OTA_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err == ESP_OK) { + uint8_t ota_update = 0; + size_t len = sizeof(ota_update); + if ((err = nvs_get_blob(handle, RMAKER_OTA_UPDATE_FLAG_NVS_NAME, &ota_update, &len)) == ESP_OK) { + ota->validation_in_progress = true; + } + nvs_close(handle); + } + const esp_partition_t *running = esp_ota_get_running_partition(); + esp_ota_img_states_t ota_state; + if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) { + ESP_LOGI(TAG, "OTA state = %d", ota_state); + /* Not checking for CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE here because the firmware may have + * it disabled, but bootloader may have it enabled, in which case, we will have to + * handle this state. + */ + if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) { + ESP_LOGI(TAG, "First Boot after an OTA"); + /* Run diagnostic function */ + esp_rmaker_ota_diag_status_t diag_status = OTA_DIAG_STATUS_SUCCESS; + if (ota->ota_diag) { + esp_rmaker_ota_diag_priv_t ota_diag_priv = { + .state = OTA_DIAG_STATE_INIT, + .rmaker_ota = ota->validation_in_progress + }; + diag_status = ota->ota_diag(&ota_diag_priv, ota->priv); + } + if (diag_status != OTA_DIAG_STATUS_FAIL) { + ESP_LOGI(TAG, "Diagnostics completed successfully! Continuing execution ..."); + /* Will not mark the image valid here immediately, but instead will wait for + * MQTT connection. The below flag will tell the OTA functions that the earlier + * OTA is still in progress. + */ + ota->ota_in_progress = true; + esp_ota_check_for_mqtt(ota); + } else { + ESP_LOGE(TAG, "Diagnostics failed! Start rollback to the previous version ..."); + esp_rmaker_ota_mark_invalid(); + } + } else { + /* If rollback is enabled, and the ota update flag is found, it means that the firmware was rolled back + */ + if (ota->validation_in_progress) { + ota->rolled_back = true; + esp_rmaker_erase_rollback_flag(); + if (ota->type == OTA_USING_PARAMS) { + /* Calling this only for OTA_USING_PARAMS, because for OTA_USING_TOPICS, + * the work queue function will manage the status reporting later. + */ + esp_rmaker_ota_report_status((esp_rmaker_ota_handle_t )ota, + OTA_STATUS_REJECTED, "Firmware rolled back"); + } + } + } + } +} + +static const esp_rmaker_ota_config_t ota_default_config = { + .server_cert = esp_rmaker_ota_def_cert, +}; +/* Enable the ESP RainMaker specific OTA */ +esp_err_t esp_rmaker_ota_enable(esp_rmaker_ota_config_t *ota_config, esp_rmaker_ota_type_t type) +{ + if (ota_config == NULL) { + ota_config = (esp_rmaker_ota_config_t *)&ota_default_config; + } + if ((type != OTA_USING_PARAMS) && (type != OTA_USING_TOPICS)) { + ESP_LOGE(TAG,"Invalid arguments for esp_rmaker_ota_enable()"); + return ESP_ERR_INVALID_ARG; + } + static bool ota_init_done; + if (ota_init_done) { + ESP_LOGE(TAG, "OTA already initialised"); + return ESP_FAIL; + } + esp_rmaker_ota_t *ota = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_ota_t)); + if (!ota) { + ESP_LOGE(TAG, "Failed to allocate memory for esp_rmaker_ota_t"); + return ESP_ERR_NO_MEM; + } + if (ota_config->ota_cb) { + ota->ota_cb = ota_config->ota_cb; + } else { + ota->ota_cb = esp_rmaker_ota_default_cb; + } + ota->ota_diag = ota_config->ota_diag; + ota->priv = ota_config->priv; + ota->server_cert = ota_config->server_cert; + esp_err_t err = ESP_FAIL; + ota->type = type; + if (type == OTA_USING_PARAMS) { + err = esp_rmaker_ota_enable_using_params(ota); + } else if (type == OTA_USING_TOPICS) { + err = esp_rmaker_ota_enable_using_topics(ota); + } + if (err == ESP_OK) { + esp_rmaker_ota_manage_rollback(ota); + ota_init_done = true; + g_ota_priv = ota; + } else { + free(ota); + ESP_LOGE(TAG, "Failed to enable OTA"); + } +#ifdef CONFIG_ESP_RMAKER_OTA_TIME_SUPPORT + esp_rmaker_time_sync_init(NULL); +#endif + return err; +} + +esp_err_t esp_rmaker_ota_enable_default(void) +{ + return esp_rmaker_ota_enable(NULL, OTA_USING_TOPICS); +} diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h b/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h new file mode 100644 index 0000000..218cba7 --- /dev/null +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h @@ -0,0 +1,55 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include + +#define RMAKER_OTA_NVS_NAMESPACE "rmaker_ota" +#define RMAKER_OTA_JOB_ID_NVS_NAME "rmaker_ota_id" +#define RMAKER_OTA_UPDATE_FLAG_NVS_NAME "ota_update" +#define RMAKER_OTA_FETCH_DELAY 5 + +typedef struct { + esp_rmaker_ota_type_t type; + esp_rmaker_ota_cb_t ota_cb; + void *priv; + esp_rmaker_post_ota_diag_t ota_diag; + TimerHandle_t rollback_timer; + const char *server_cert; + char *url; + char *fw_version; + int filesize; + bool ota_in_progress; + bool validation_in_progress; + bool rolled_back; + ota_status_t last_reported_status; + void *transient_priv; + char *metadata; +} esp_rmaker_ota_t; + +char *esp_rmaker_ota_status_to_string(ota_status_t status); +void esp_rmaker_ota_common_cb(void *priv); +void esp_rmaker_ota_finish_using_params(esp_rmaker_ota_t *ota); +void esp_rmaker_ota_finish_using_topics(esp_rmaker_ota_t *ota); +esp_err_t esp_rmaker_ota_enable_using_params(esp_rmaker_ota_t *ota); +esp_err_t esp_rmaker_ota_report_status_using_params(esp_rmaker_ota_handle_t ota_handle, + ota_status_t status, char *additional_info); +esp_err_t esp_rmaker_ota_enable_using_topics(esp_rmaker_ota_t *ota); +esp_err_t esp_rmaker_ota_report_status_using_topics(esp_rmaker_ota_handle_t ota_handle, + ota_status_t status, char *additional_info); diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_params.c b/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_params.c new file mode 100644 index 0000000..3c2cece --- /dev/null +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_params.c @@ -0,0 +1,114 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "esp_rmaker_ota_internal.h" +#include "esp_rmaker_mqtt.h" + +static const char *TAG = "esp_rmaker_ota_using_params"; + +#define ESP_RMAKER_OTA_SERV_NAME "OTA" + +void esp_rmaker_ota_finish_using_params(esp_rmaker_ota_t *ota) +{ + if (ota->url) { + free(ota->url); + ota->url = NULL; + } + ota->filesize = 0; + if (ota->transient_priv) { + ota->transient_priv = NULL; + } + if (ota->metadata) { + free(ota->metadata); + ota->metadata = NULL; + } + ota->ota_in_progress = false; +} +static esp_err_t esp_rmaker_ota_service_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx) +{ + esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)priv_data; + if (!ota) { + ESP_LOGE(TAG, "No OTA specific data received in callback"); + return ESP_FAIL; + } + if (ota->ota_in_progress) { + ESP_LOGE(TAG, "OTA already in progress. Please try later."); + return ESP_FAIL; + } + if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_OTA_URL) == 0) { + ESP_LOGI(TAG, "Received value = %s for %s - %s", + val.val.s, esp_rmaker_device_get_name(device), esp_rmaker_param_get_name(param)); + if (ota->url) { + free(ota->url); + ota->url = NULL; + } + ota->url = strdup(val.val.s); + if (ota->url) { + ota->filesize = 0; + ota->ota_in_progress = true; + ota->transient_priv = (void *)device; + ota->metadata = NULL; + if (esp_rmaker_work_queue_add_task(esp_rmaker_ota_common_cb, ota) != ESP_OK) { + esp_rmaker_ota_finish_using_params(ota); + } else { + return ESP_OK; + } + } + + } + return ESP_FAIL; +} + +esp_err_t esp_rmaker_ota_report_status_using_params(esp_rmaker_ota_handle_t ota_handle, ota_status_t status, char *additional_info) +{ + const esp_rmaker_device_t *device = esp_rmaker_node_get_device_by_name(esp_rmaker_get_node(), ESP_RMAKER_OTA_SERV_NAME); + if (!device) { + return ESP_FAIL; + } + esp_rmaker_param_t *info_param = esp_rmaker_device_get_param_by_type(device, ESP_RMAKER_PARAM_OTA_INFO); + esp_rmaker_param_t *status_param = esp_rmaker_device_get_param_by_type(device, ESP_RMAKER_PARAM_OTA_STATUS); + + esp_rmaker_param_update_and_report(info_param, esp_rmaker_str(additional_info)); + esp_rmaker_param_update_and_report(status_param, esp_rmaker_str(esp_rmaker_ota_status_to_string(status))); + + return ESP_OK; +} + +/* Enable the ESP RainMaker specific OTA */ +esp_err_t esp_rmaker_ota_enable_using_params(esp_rmaker_ota_t *ota) +{ + esp_rmaker_device_t *service = esp_rmaker_ota_service_create(ESP_RMAKER_OTA_SERV_NAME, ota); + if (!service) { + ESP_LOGE(TAG, "Failed to create OTA Service"); + return ESP_FAIL; + } + esp_rmaker_device_add_cb(service, esp_rmaker_ota_service_cb, NULL); + esp_err_t err = esp_rmaker_node_add_device(esp_rmaker_get_node(), service); + if (err == ESP_OK) { + ESP_LOGI(TAG, "OTA enabled with Params"); + } + return err; +} diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_topics.c b/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_topics.c new file mode 100644 index 0000000..1a3887e --- /dev/null +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_topics.c @@ -0,0 +1,346 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "esp_rmaker_internal.h" +#include "esp_rmaker_ota_internal.h" +#include "esp_rmaker_mqtt.h" +#include "esp_rmaker_mqtt_topics.h" + +#ifdef CONFIG_ESP_RMAKER_OTA_AUTOFETCH +#include +static esp_timer_handle_t ota_autofetch_timer; +/* Autofetch period in hours */ +#define OTA_AUTOFETCH_PERIOD CONFIG_ESP_RMAKER_OTA_AUTOFETCH_PERIOD +/* Autofetch period in micro-seconds */ +static uint64_t ota_autofetch_period = (OTA_AUTOFETCH_PERIOD * 60 * 60 * 1000000LL); +#endif /* CONFIG_ESP_RMAKER_OTA_AUTOFETCH */ + +static const char *TAG = "esp_rmaker_ota_using_topics"; + +esp_err_t esp_rmaker_ota_report_status_using_topics(esp_rmaker_ota_handle_t ota_handle, ota_status_t status, char *additional_info) +{ + if (!ota_handle) { + return ESP_FAIL; + } + esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)ota_handle; + + char publish_payload[200]; + json_gen_str_t jstr; + json_gen_str_start(&jstr, publish_payload, sizeof(publish_payload), NULL, NULL); + json_gen_start_object(&jstr); + if (ota->transient_priv) { + json_gen_obj_set_string(&jstr, "ota_job_id", (char *)ota->transient_priv); + } else { + /* This will get executed only when the OTA status is being reported after a reboot, either to + * indicate successful verification of new firmware, or to indicate that firmware was rolled back + */ + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, RMAKER_OTA_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err == ESP_OK) { + char job_id[64] = {0}; + size_t len = sizeof(job_id); + if ((err = nvs_get_blob(handle, RMAKER_OTA_JOB_ID_NVS_NAME, job_id, &len)) == ESP_OK) { + json_gen_obj_set_string(&jstr, "ota_job_id", job_id); + nvs_erase_key(handle, RMAKER_OTA_JOB_ID_NVS_NAME); + } + nvs_close(handle); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Not reporting any status, since there is no Job ID available"); + return ESP_ERR_INVALID_STATE; + } + } + } + json_gen_obj_set_string(&jstr, "status", esp_rmaker_ota_status_to_string(status)); + json_gen_obj_set_string(&jstr, "additional_info", additional_info); + json_gen_end_object(&jstr); + json_gen_str_end(&jstr); + + char publish_topic[MQTT_TOPIC_BUFFER_SIZE]; + esp_rmaker_create_mqtt_topic(publish_topic, sizeof(publish_topic), OTASTATUS_TOPIC_SUFFIX, OTASTATUS_TOPIC_RULE); + ESP_LOGI(TAG, "%s",publish_payload); + esp_err_t err = esp_rmaker_mqtt_publish(publish_topic, publish_payload, strlen(publish_payload), + RMAKER_MQTT_QOS1, NULL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_rmaker_mqtt_publish_data returned error %d",err); + return ESP_FAIL; + } + return ESP_OK; +} + +void esp_rmaker_ota_finish_using_topics(esp_rmaker_ota_t *ota) +{ + if (ota->url) { + free(ota->url); + ota->url = NULL; + } + ota->filesize = 0; + if (ota->transient_priv) { + free(ota->transient_priv); + ota->transient_priv = NULL; + } + if (ota->metadata) { + free(ota->metadata); + ota->metadata = NULL; + } + if (ota->fw_version) { + free(ota->fw_version); + ota->fw_version = NULL; + } + ota->ota_in_progress = false; +} +static void ota_url_handler(const char *topic, void *payload, size_t payload_len, void *priv_data) +{ + if (!priv_data) { + return; + } + esp_rmaker_ota_handle_t ota_handle = priv_data; + esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)ota_handle; + if (ota->ota_in_progress) { + ESP_LOGE(TAG, "OTA already in progress. Please try later."); + return; + } + ota->ota_in_progress = true; + /* Starting Firmware Upgrades */ + ESP_LOGI(TAG, "Upgrade Handler got:%.*s on %s topic\n", (int) payload_len, (char *)payload, topic); + /* + { + "ota_job_id": "", + "url": "", + "fw_version": "", + "filesize": + } + */ + jparse_ctx_t jctx; + char *url = NULL, *ota_job_id = NULL, *fw_version = NULL; + int ret = json_parse_start(&jctx, (char *)payload, (int) payload_len); + if (ret != 0) { + ESP_LOGE(TAG, "Invalid JSON received: %s", (char *)payload); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Aborted. JSON Payload error"); + ota->ota_in_progress = false; + return; + } + int len = 0; + ret = json_obj_get_strlen(&jctx, "ota_job_id", &len); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Aborted. OTA Job ID not found in JSON"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Aborted. OTA Updated ID not found in JSON"); + goto end; + } + len++; /* Increment for NULL character */ + ota_job_id = MEM_CALLOC_EXTRAM(1, len); + if (!ota_job_id) { + ESP_LOGE(TAG, "Aborted. OTA Job ID memory allocation failed"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Aborted. OTA Updated ID memory allocation failed"); + goto end; + } + json_obj_get_string(&jctx, "ota_job_id", ota_job_id, len); + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, RMAKER_OTA_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err == ESP_OK) { + nvs_set_blob(handle, RMAKER_OTA_JOB_ID_NVS_NAME, ota_job_id, strlen(ota_job_id)); + nvs_close(handle); + } + ESP_LOGI(TAG, "OTA Job ID: %s", ota_job_id); + ota->transient_priv = ota_job_id; + len = 0; + ret = json_obj_get_strlen(&jctx, "url", &len); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Aborted. URL not found in JSON"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Aborted. URL not found in JSON"); + goto end; + } + len++; /* Increment for NULL character */ + url = MEM_CALLOC_EXTRAM(1, len); + if (!url) { + ESP_LOGE(TAG, "Aborted. URL memory allocation failed"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Aborted. URL memory allocation failed"); + goto end; + } + json_obj_get_string(&jctx, "url", url, len); + ESP_LOGI(TAG, "URL: %s", url); + + int filesize = 0; + json_obj_get_int(&jctx, "file_size", &filesize); + ESP_LOGI(TAG, "File Size: %d", filesize); + + len = 0; + ret = json_obj_get_strlen(&jctx, "fw_version", &len); + if (ret == ESP_OK && len > 0) { + len++; /* Increment for NULL character */ + fw_version = MEM_CALLOC_EXTRAM(1, len); + if (!fw_version) { + ESP_LOGE(TAG, "Aborted. Firmware version memory allocation failed"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Aborted. Firmware version memory allocation failed"); + goto end; + } + json_obj_get_string(&jctx, "fw_version", fw_version, len); + ESP_LOGI(TAG, "Firmware version: %s", fw_version); + } + + int metadata_size = 0; + char *metadata = NULL; + ret = json_obj_get_object_strlen(&jctx, "metadata", &metadata_size); + if (ret == ESP_OK && metadata_size > 0) { + metadata_size++; /* Increment for NULL character */ + metadata = MEM_CALLOC_EXTRAM(1, metadata_size); + if (!metadata) { + ESP_LOGE(TAG, "Aborted. OTA metadata memory allocation failed"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Aborted. OTA metadata memory allocation failed"); + goto end; + } + json_obj_get_object_str(&jctx, "metadata", metadata, metadata_size); + ota->metadata = metadata; + } + + json_parse_end(&jctx); + if (ota->url) { + free(ota->url); + } + ota->url = url; + ota->fw_version = fw_version; + ota->filesize = filesize; + ota->ota_in_progress = true; + if (esp_rmaker_work_queue_add_task(esp_rmaker_ota_common_cb, ota) != ESP_OK) { + esp_rmaker_ota_finish_using_topics(ota); + } + return; +end: + if (url) { + free(url); + } + if (fw_version) { + free(fw_version); + } + esp_rmaker_ota_finish_using_topics(ota); + json_parse_end(&jctx); + return; +} + +esp_err_t esp_rmaker_ota_fetch(void) +{ + ESP_LOGI(TAG, "Fetching OTA details, if any."); + esp_rmaker_node_info_t *info = esp_rmaker_node_get_info(esp_rmaker_get_node()); + if (!info) { + ESP_LOGE(TAG, "Node info not found. Cant send otafetch request"); + return ESP_FAIL; + } + char publish_payload[150]; + json_gen_str_t jstr; + json_gen_str_start(&jstr, publish_payload, sizeof(publish_payload), NULL, NULL); + json_gen_start_object(&jstr); + json_gen_obj_set_string(&jstr, "node_id", esp_rmaker_get_node_id()); + json_gen_obj_set_string(&jstr, "fw_version", info->fw_version); + json_gen_end_object(&jstr); + json_gen_str_end(&jstr); + char publish_topic[MQTT_TOPIC_BUFFER_SIZE]; + esp_rmaker_create_mqtt_topic(publish_topic, sizeof(publish_topic), OTAFETCH_TOPIC_SUFFIX, OTAFETCH_TOPIC_RULE); + esp_err_t err = esp_rmaker_mqtt_publish(publish_topic, publish_payload, strlen(publish_payload), + RMAKER_MQTT_QOS1, NULL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "OTA Fetch Publish Error %d", err); + } + return err; +} + +void esp_rmaker_ota_autofetch_timer_cb(void *priv) +{ + esp_rmaker_ota_fetch(); +} + +static esp_err_t esp_rmaker_ota_subscribe(void *priv_data) +{ + char subscribe_topic[MQTT_TOPIC_BUFFER_SIZE]; + + snprintf(subscribe_topic, sizeof(subscribe_topic),"node/%s/%s", esp_rmaker_get_node_id(), OTAURL_TOPIC_SUFFIX); + + ESP_LOGI(TAG, "Subscribing to: %s", subscribe_topic); + /* First unsubscribe, in case there is a stale subscription */ + esp_rmaker_mqtt_unsubscribe(subscribe_topic); + esp_err_t err = esp_rmaker_mqtt_subscribe(subscribe_topic, ota_url_handler, RMAKER_MQTT_QOS1, priv_data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "OTA URL Subscription Error %d", err); + } + return err; +} + +static void esp_rmaker_ota_work_fn(void *priv_data) +{ + esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)priv_data; + /* If the firmware was rolled back, indicate that first */ + if (ota->rolled_back) { + esp_rmaker_ota_report_status((esp_rmaker_ota_handle_t )ota, OTA_STATUS_REJECTED, "Firmware rolled back"); + ota->rolled_back = false; + } + esp_rmaker_ota_subscribe(priv_data); +#ifdef CONFIG_ESP_RMAKER_OTA_AUTOFETCH + if (ota->ota_in_progress != true) { + if (esp_rmaker_ota_fetch_with_delay(RMAKER_OTA_FETCH_DELAY) != ESP_OK) { + ESP_LOGE(TAG, "Failed to create OTA Fetch timer."); + } + } + if (ota_autofetch_period > 0) { + esp_timer_create_args_t autofetch_timer_conf = { + .callback = esp_rmaker_ota_autofetch_timer_cb, + .arg = priv_data, + .dispatch_method = ESP_TIMER_TASK, + .name = "ota_autofetch_tm" + }; + if (esp_timer_create(&autofetch_timer_conf, &ota_autofetch_timer) == ESP_OK) { + esp_timer_start_periodic(ota_autofetch_timer, ota_autofetch_period); + } else { + ESP_LOGE(TAG, "Failed to create OTA Autofetch timer"); + } + } +#endif /* CONFIG_ESP_RMAKER_OTA_AUTOFETCH */ +} + +/* Enable the ESP RainMaker specific OTA */ +esp_err_t esp_rmaker_ota_enable_using_topics(esp_rmaker_ota_t *ota) +{ + esp_err_t err = esp_rmaker_work_queue_add_task(esp_rmaker_ota_work_fn, ota); + if (err == ESP_OK) { + ESP_LOGI(TAG, "OTA enabled with Topics"); + } + return err; +} + +static void esp_rmaker_ota_fetch_timer_cb(TimerHandle_t xTimer) +{ + esp_rmaker_ota_fetch(); + xTimerDelete(xTimer, 0); +} + +esp_err_t esp_rmaker_ota_fetch_with_delay(int time) +{ + TimerHandle_t timer = xTimerCreate(NULL, (time * 1000) / portTICK_PERIOD_MS, pdFALSE, NULL, esp_rmaker_ota_fetch_timer_cb); + if (timer == NULL) { + return ESP_ERR_NO_MEM; + } else { + xTimerStart(timer, 0); + } + return ESP_OK; +} \ No newline at end of file diff --git a/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_devices.c b/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_devices.c new file mode 100644 index 0000000..a693d6d --- /dev/null +++ b/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_devices.c @@ -0,0 +1,69 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#include +#include + +esp_rmaker_device_t *esp_rmaker_switch_device_create(const char *dev_name, + void *priv_data, bool power) +{ + esp_rmaker_device_t *device = esp_rmaker_device_create(dev_name, ESP_RMAKER_DEVICE_SWITCH, priv_data); + if (device) { + esp_rmaker_device_add_param(device, esp_rmaker_name_param_create(ESP_RMAKER_DEF_NAME_PARAM, dev_name)); + esp_rmaker_param_t *primary = esp_rmaker_power_param_create(ESP_RMAKER_DEF_POWER_NAME, power); + esp_rmaker_device_add_param(device, primary); + esp_rmaker_device_assign_primary_param(device, primary); + } + return device; +} + +esp_rmaker_device_t *esp_rmaker_lightbulb_device_create(const char *dev_name, + void *priv_data, bool power) +{ + esp_rmaker_device_t *device = esp_rmaker_device_create(dev_name, ESP_RMAKER_DEVICE_LIGHTBULB, priv_data); + if (device) { + esp_rmaker_device_add_param(device, esp_rmaker_name_param_create(ESP_RMAKER_DEF_NAME_PARAM, dev_name)); + esp_rmaker_param_t *primary = esp_rmaker_power_param_create(ESP_RMAKER_DEF_POWER_NAME, power); + esp_rmaker_device_add_param(device, primary); + esp_rmaker_device_assign_primary_param(device, primary); + } + return device; +} + +esp_rmaker_device_t *esp_rmaker_fan_device_create(const char *dev_name, + void *priv_data, bool power) +{ + esp_rmaker_device_t *device = esp_rmaker_device_create(dev_name, ESP_RMAKER_DEVICE_FAN, priv_data); + if (device) { + esp_rmaker_device_add_param(device, esp_rmaker_name_param_create(ESP_RMAKER_DEF_NAME_PARAM, dev_name)); + esp_rmaker_param_t *primary = esp_rmaker_power_param_create(ESP_RMAKER_DEF_POWER_NAME, power); + esp_rmaker_device_add_param(device, primary); + esp_rmaker_device_assign_primary_param(device, primary); + } + return device; +} + +esp_rmaker_device_t *esp_rmaker_temp_sensor_device_create(const char *dev_name, + void *priv_data, float temperature) +{ + esp_rmaker_device_t *device = esp_rmaker_device_create(dev_name, ESP_RMAKER_DEVICE_TEMP_SENSOR, priv_data); + if (device) { + esp_rmaker_device_add_param(device, esp_rmaker_name_param_create(ESP_RMAKER_DEF_NAME_PARAM, dev_name)); + esp_rmaker_param_t *primary = esp_rmaker_temperature_param_create(ESP_RMAKER_DEF_TEMPERATURE_NAME, temperature); + esp_rmaker_device_add_param(device, primary); + esp_rmaker_device_assign_primary_param(device, primary); + } + return device; +} diff --git a/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_params.c b/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_params.c new file mode 100644 index 0000000..59e16ed --- /dev/null +++ b/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_params.c @@ -0,0 +1,208 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +esp_rmaker_param_t *esp_rmaker_name_param_create(const char *param_name, const char *val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_NAME, + esp_rmaker_str(val), PROP_FLAG_READ | PROP_FLAG_WRITE | PROP_FLAG_PERSIST); + return param; +} + +esp_rmaker_param_t *esp_rmaker_power_param_create(const char *param_name, bool val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_POWER, + esp_rmaker_bool(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + if (param) { + esp_rmaker_param_add_ui_type(param, ESP_RMAKER_UI_TOGGLE); + } + return param; +} + +esp_rmaker_param_t *esp_rmaker_brightness_param_create(const char *param_name, int val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_BRIGHTNESS, + esp_rmaker_int(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + if (param) { + esp_rmaker_param_add_ui_type(param, ESP_RMAKER_UI_SLIDER); + esp_rmaker_param_add_bounds(param, esp_rmaker_int(0), esp_rmaker_int(100), esp_rmaker_int(1)); + } + return param; +} + +esp_rmaker_param_t *esp_rmaker_hue_param_create(const char *param_name, int val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_HUE, + esp_rmaker_int(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + if (param) { + esp_rmaker_param_add_ui_type(param, ESP_RMAKER_UI_HUE_SLIDER); + esp_rmaker_param_add_bounds(param, esp_rmaker_int(0), esp_rmaker_int(360), esp_rmaker_int(1)); + } + return param; +} + +esp_rmaker_param_t *esp_rmaker_saturation_param_create(const char *param_name, int val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_SATURATION, + esp_rmaker_int(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + if (param) { + esp_rmaker_param_add_ui_type(param, ESP_RMAKER_UI_SLIDER); + esp_rmaker_param_add_bounds(param, esp_rmaker_int(0), esp_rmaker_int(100), esp_rmaker_int(1)); + } + return param; +} + +esp_rmaker_param_t *esp_rmaker_intensity_param_create(const char *param_name, int val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_INTENSITY, + esp_rmaker_int(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + if (param) { + esp_rmaker_param_add_ui_type(param, ESP_RMAKER_UI_SLIDER); + esp_rmaker_param_add_bounds(param, esp_rmaker_int(0), esp_rmaker_int(100), esp_rmaker_int(1)); + } + return param; +} + +esp_rmaker_param_t *esp_rmaker_cct_param_create(const char *param_name, int val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_CCT, + esp_rmaker_int(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + if (param) { + esp_rmaker_param_add_ui_type(param, ESP_RMAKER_UI_SLIDER); + esp_rmaker_param_add_bounds(param, esp_rmaker_int(2700), esp_rmaker_int(6500), esp_rmaker_int(100)); + } + return param; +} + +esp_rmaker_param_t *esp_rmaker_direction_param_create(const char *param_name, int val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_DIRECTION, + esp_rmaker_int(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + if (param) { + esp_rmaker_param_add_ui_type(param, ESP_RMAKER_UI_DROPDOWN); + esp_rmaker_param_add_bounds(param, esp_rmaker_int(0), esp_rmaker_int(1), esp_rmaker_int(1)); + } + return param; +} + +esp_rmaker_param_t *esp_rmaker_speed_param_create(const char *param_name, int val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_SPEED, + esp_rmaker_int(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + if (param) { + esp_rmaker_param_add_ui_type(param, ESP_RMAKER_UI_SLIDER); + esp_rmaker_param_add_bounds(param, esp_rmaker_int(0), esp_rmaker_int(5), esp_rmaker_int(1)); + } + return param; +} + +esp_rmaker_param_t *esp_rmaker_temperature_param_create(const char *param_name, float val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_TEMPERATURE, + esp_rmaker_float(val), PROP_FLAG_READ | PROP_FLAG_TIME_SERIES); + if (param) { + esp_rmaker_param_add_ui_type(param, ESP_RMAKER_UI_TEXT); + } + return param; +} + +esp_rmaker_param_t *esp_rmaker_ota_status_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_OTA_STATUS, + esp_rmaker_str(""), PROP_FLAG_READ); + return param; +} + +esp_rmaker_param_t *esp_rmaker_ota_info_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_OTA_INFO, + esp_rmaker_str(""), PROP_FLAG_READ); + return param; +} + +esp_rmaker_param_t *esp_rmaker_ota_url_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_OTA_URL, + esp_rmaker_str(""), PROP_FLAG_WRITE); + return param; +} + +esp_rmaker_param_t *esp_rmaker_timezone_param_create(const char *param_name, const char *val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_TIMEZONE, + esp_rmaker_str(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + return param; +} + +esp_rmaker_param_t *esp_rmaker_timezone_posix_param_create(const char *param_name, const char *val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_TIMEZONE_POSIX, + esp_rmaker_str(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + return param; +} + +esp_rmaker_param_t *esp_rmaker_schedules_param_create(const char *param_name, int max_schedules) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_SCHEDULES, + esp_rmaker_array("[]"), PROP_FLAG_READ | PROP_FLAG_WRITE | PROP_FLAG_PERSIST); + esp_rmaker_param_add_array_max_count(param, max_schedules); + return param; +} + +esp_rmaker_param_t *esp_rmaker_scenes_param_create(const char *param_name, int max_scenes) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_SCENES, + esp_rmaker_array("[]"), PROP_FLAG_READ | PROP_FLAG_WRITE | PROP_FLAG_PERSIST); + esp_rmaker_param_add_array_max_count(param, max_scenes); + return param; +} + +esp_rmaker_param_t *esp_rmaker_reboot_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_REBOOT, + esp_rmaker_bool(false), PROP_FLAG_READ | PROP_FLAG_WRITE); + return param; +} + +esp_rmaker_param_t *esp_rmaker_factory_reset_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_FACTORY_RESET, + esp_rmaker_bool(false), PROP_FLAG_READ | PROP_FLAG_WRITE); + return param; +} + +esp_rmaker_param_t *esp_rmaker_wifi_reset_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_WIFI_RESET, + esp_rmaker_bool(false), PROP_FLAG_READ | PROP_FLAG_WRITE); + return param; +} + +esp_rmaker_param_t *esp_rmaker_local_control_pop_param_create(const char *param_name, const char *val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_LOCAL_CONTROL_POP, + esp_rmaker_str(val), PROP_FLAG_READ); + return param; +} + +esp_rmaker_param_t *esp_rmaker_local_control_type_param_create(const char *param_name, int val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_LOCAL_CONTROL_TYPE, + esp_rmaker_int(val), PROP_FLAG_READ); + return param; +} diff --git a/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_services.c b/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_services.c new file mode 100644 index 0000000..cd6c3a0 --- /dev/null +++ b/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_services.c @@ -0,0 +1,79 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#include +#include + +esp_rmaker_device_t *esp_rmaker_ota_service_create(const char *serv_name, void *priv_data) +{ + esp_rmaker_device_t *service = esp_rmaker_service_create(serv_name, ESP_RMAKER_SERVICE_OTA, priv_data); + if (service) { + esp_rmaker_device_add_param(service, esp_rmaker_ota_status_param_create(ESP_RMAKER_DEF_OTA_STATUS_NAME)); + esp_rmaker_device_add_param(service, esp_rmaker_ota_info_param_create(ESP_RMAKER_DEF_OTA_INFO_NAME)); + esp_rmaker_device_add_param(service, esp_rmaker_ota_url_param_create(ESP_RMAKER_DEF_OTA_URL_NAME)); + } + return service; +} + +esp_rmaker_device_t *esp_rmaker_time_service_create(const char *serv_name, const char *timezone, + const char *timezone_posix, void *priv_data) +{ + esp_rmaker_device_t *service = esp_rmaker_service_create(serv_name, ESP_RMAKER_SERVICE_TIME, priv_data); + if (service) { + esp_rmaker_device_add_param(service, esp_rmaker_timezone_param_create( + ESP_RMAKER_DEF_TIMEZONE_NAME, timezone)); + esp_rmaker_device_add_param(service, esp_rmaker_timezone_posix_param_create( + ESP_RMAKER_DEF_TIMEZONE_POSIX_NAME, timezone_posix)); + } + return service; +} + +esp_rmaker_device_t *esp_rmaker_create_schedule_service(const char *serv_name, esp_rmaker_device_write_cb_t write_cb, + esp_rmaker_device_read_cb_t read_cb, int max_schedules, void *priv_data) +{ + esp_rmaker_device_t *service = esp_rmaker_service_create(serv_name, ESP_RMAKER_SERVICE_SCHEDULE, priv_data); + if (service) { + esp_rmaker_device_add_cb(service, write_cb, read_cb); + esp_rmaker_device_add_param(service, esp_rmaker_schedules_param_create(ESP_RMAKER_DEF_SCHEDULE_NAME, max_schedules)); + } + return service; +} + +esp_rmaker_device_t *esp_rmaker_create_scenes_service(const char *serv_name, esp_rmaker_device_write_cb_t write_cb, + esp_rmaker_device_read_cb_t read_cb, int max_scenes, bool deactivation_support, void *priv_data) +{ + esp_rmaker_device_t *service = esp_rmaker_service_create(serv_name, ESP_RMAKER_SERVICE_SCENES, priv_data); + if (service) { + esp_rmaker_device_add_cb(service, write_cb, read_cb); + esp_rmaker_device_add_param(service, esp_rmaker_scenes_param_create(ESP_RMAKER_DEF_SCENES_NAME, max_scenes)); + esp_rmaker_device_add_attribute(service, "deactivation_support", deactivation_support ? "yes" : "no"); + } + return service; +} + +esp_rmaker_device_t *esp_rmaker_create_system_service(const char *serv_name, void *priv_data) +{ + return esp_rmaker_service_create(serv_name, ESP_RMAKER_SERVICE_SYSTEM, priv_data); +} + +esp_rmaker_device_t *esp_rmaker_create_local_control_service(const char *serv_name, const char *pop, int sec_type, void *priv_data) +{ + esp_rmaker_device_t *service = esp_rmaker_service_create(serv_name, ESP_RMAKER_SERVICE_LOCAL_CONTROL, priv_data); + if (service) { + esp_rmaker_device_add_param(service, esp_rmaker_local_control_pop_param_create(ESP_RMAKER_DEF_LOCAL_CONTROL_POP, pop)); + esp_rmaker_device_add_param(service, esp_rmaker_local_control_type_param_create(ESP_RMAKER_DEF_LOCAL_CONTROL_TYPE, sec_type)); + } + return service; +} diff --git a/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br.c b/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br.c new file mode 100644 index 0000000..3d785cd --- /dev/null +++ b/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br.c @@ -0,0 +1,196 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "esp_rmaker_thread_br_priv.h" + +static const char *TAG = "thread_br"; +static const esp_rmaker_device_t *thread_br_service = NULL; + +static int char_to_num(char ch) +{ + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } else if (ch >= 'A' && ch <= 'F') { + return ch - 'A' + 10; + } else if (ch >= 'a' && ch <= 'f') { + return ch - 'a' + 10; + } + return -1; +} + +static int parse_bytes(uint8_t *buffer, size_t buffer_size, const char *str) +{ + if (!str || strlen(str) % 2 != 0 || strlen(str) / 2 > buffer_size) { + return -1; + } + for (size_t i = 0; i < strlen(str) / 2; ++i) { + int byte_h = char_to_num(str[2 * i]); + int byte_l = char_to_num(str[2 * i + 1]); + if (byte_h < 0 || byte_l < 0) { + return -1; + } + buffer[i] = (byte_h << 4) + byte_l; + } + return strlen(str) / 2; +} + +static bool convert_bytes_to_str(const uint8_t *bytes, size_t bytes_size, char *str, size_t str_size) +{ + if (str_size < bytes_size * 2 + 1) { + return false; + } + for (size_t i = 0; i < bytes_size; ++i) { + uint8_t byte_h = bytes[i] >> 4; + uint8_t byte_l = bytes[i] & 0xF; + str[2 * i] = byte_h > 9 ? (byte_h - 0xA + 'A') : (byte_h + '0'); + str[2 * i + 1] = byte_l > 9 ? (byte_l - 0xA + 'A') : (byte_l + '0'); + } + str[2 * bytes_size] = 0; + return true; +} + +static esp_err_t write_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx) +{ + if (ctx->src == ESP_RMAKER_REQ_SRC_INIT) { + return ESP_OK; + } + if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_TBR_ACTIVE_DATASET) == 0) { + if (val.type != RMAKER_VAL_TYPE_STRING || !val.val.s) { + return ESP_ERR_INVALID_ARG; + } + otOperationalDatasetTlvs active_dataset; + otOperationalDatasetTlvs current_active_dataset; + int len = parse_bytes(active_dataset.mTlvs, sizeof(active_dataset.mTlvs), val.val.s); + if (len < 0) { + return ESP_ERR_INVALID_ARG; + } + active_dataset.mLength = len; + if (thread_br_get_active_dataset_tlvs(¤t_active_dataset) != ESP_OK || + current_active_dataset.mLength != active_dataset.mLength || + memcmp(current_active_dataset.mTlvs, active_dataset.mTlvs, len)) { + ESP_RETURN_ON_ERROR(thread_br_set_thread_enabled(false), TAG, "Failed to disable Thread"); + ESP_RETURN_ON_ERROR(thread_br_set_active_dataset_tlvs(&active_dataset), TAG, "Failed to set active dataset"); + } + ESP_RETURN_ON_ERROR(thread_br_set_thread_enabled(true), TAG, "Failed to enable Thread"); + ESP_RETURN_ON_ERROR(esp_rmaker_param_update_and_report(param, val), TAG, + "Failed to update and report active dataset"); + } else if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_TBR_PENDING_DATASET) == 0) { + if (val.type != RMAKER_VAL_TYPE_STRING || !val.val.s) { + return ESP_ERR_INVALID_ARG; + } + otOperationalDatasetTlvs pending_dataset; + int len = parse_bytes(pending_dataset.mTlvs, sizeof(pending_dataset.mTlvs), val.val.s); + if (len < 0) { + return ESP_ERR_INVALID_ARG; + } + pending_dataset.mLength = len; + ESP_RETURN_ON_ERROR(thread_br_set_pending_dataset_tlvs(&pending_dataset), TAG, "Failed to set pending dataset"); + ESP_RETURN_ON_ERROR(esp_rmaker_param_update_and_report(param, val), TAG, + "Failed to update and report active dataset"); + } else if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_TBR_CMD) == 0) { + if (val.type != RMAKER_VAL_TYPE_INTEGER) { + return ESP_ERR_INVALID_ARG; + } + if (val.val.i == ESP_RMAKER_THREAD_BR_CMD_GEN_DATASET) { + ESP_RETURN_ON_ERROR(thread_br_set_thread_enabled(false), TAG, "Failed to disable Thread"); + ESP_RETURN_ON_ERROR(thread_br_generate_and_commit_new_dataset(), TAG, "Failed to generate a new dataset"); + ESP_RETURN_ON_ERROR(thread_br_set_thread_enabled(true), TAG, "Failed to enable Thread"); + // We need to report the generated active dataset. + otOperationalDatasetTlvs active_dataset; + ESP_RETURN_ON_ERROR(thread_br_get_active_dataset_tlvs(&active_dataset), TAG, "Failed to get active dataset"); + char *dataset_str = (char *)MEM_CALLOC_EXTRAM(2 * active_dataset.mLength + 1, 1); + if (!dataset_str) { + return ESP_ERR_NO_MEM; + } + convert_bytes_to_str(active_dataset.mTlvs, active_dataset.mLength, dataset_str, 2 * active_dataset.mLength + 1); + esp_rmaker_param_val_t report_val = esp_rmaker_str(dataset_str); + esp_rmaker_param_t *report_param = esp_rmaker_device_get_param_by_type(device, ESP_RMAKER_PARAM_TBR_ACTIVE_DATASET); + esp_err_t err = esp_rmaker_param_update_and_report(report_param, report_val); + free(dataset_str); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to update and report active dataset"); + return err; + } + } + } + return ESP_OK; +} + +esp_err_t esp_rmaker_thread_br_enable(const esp_openthread_platform_config_t *platform_config, + const esp_rcp_update_config_t *rcp_update_config) +{ + if (thread_br_service) { + return ESP_ERR_INVALID_STATE; + } + thread_br_service = esp_rmaker_thread_br_service_create(ESP_RMAKER_DEF_THREAD_BR_SERVICE, write_cb, NULL, NULL); + if (!thread_br_service) { + ESP_LOGE(TAG, "Failed to create thread br service"); + return ESP_FAIL; + } + esp_err_t err = esp_rmaker_node_add_device(esp_rmaker_get_node(), thread_br_service); + if (err != ESP_OK) { + esp_rmaker_device_delete(thread_br_service); + thread_br_service = NULL; + } + thread_border_router_start(platform_config, rcp_update_config); + return ESP_OK; +} + +esp_err_t esp_rmaker_thread_br_report_device_role(void) +{ + if (!thread_br_service) { + return ESP_ERR_INVALID_STATE; + } + esp_rmaker_param_t *param = esp_rmaker_device_get_param_by_type(thread_br_service, + ESP_RMAKER_PARAM_TBR_DEVICE_ROLE); + if (!param) { + return ESP_ERR_INVALID_STATE; + } + otDeviceRole role = thread_br_get_device_role(); + esp_rmaker_param_val_t val = esp_rmaker_int(role); + ESP_RETURN_ON_ERROR(esp_rmaker_param_update_and_report(param, val), TAG, + "Failed to update and report Thread device role"); + return ESP_OK; +} + +esp_err_t esp_rmaker_thread_br_report_border_agent_id(void) +{ + if (!thread_br_service) { + return ESP_ERR_INVALID_STATE; + } + esp_rmaker_param_t *param = esp_rmaker_device_get_param_by_type(thread_br_service, + ESP_RMAKER_PARAM_TBR_BORDER_AGENT_ID); + if (!param) { + return ESP_ERR_INVALID_STATE; + } + otBorderAgentId border_agent_id; + ESP_RETURN_ON_ERROR(thread_br_get_border_agent_id(&border_agent_id), TAG, "Failed to get Border Agent ID"); + char border_agent_id_str[sizeof(border_agent_id.mId) * 2 + 1]; + convert_bytes_to_str(border_agent_id.mId, sizeof(border_agent_id.mId), border_agent_id_str, + sizeof(border_agent_id_str)); + esp_rmaker_param_val_t report_val = esp_rmaker_str(border_agent_id_str); + ESP_RETURN_ON_ERROR(esp_rmaker_param_update_and_report(param, report_val), TAG, + "Failed to update and report border agent id"); + return ESP_OK; +} diff --git a/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_internal.c b/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_internal.c new file mode 100644 index 0000000..df8a813 --- /dev/null +++ b/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_internal.c @@ -0,0 +1,185 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "esp_rmaker_thread_br_priv.h" + +static const char* TAG = "thread_br"; + +esp_err_t thread_br_set_thread_enabled(bool enabled) +{ + otInstance *instance = esp_openthread_get_instance(); + if (!instance) { + ESP_LOGE(TAG, "Thread not initialized"); + return ESP_ERR_INVALID_STATE; + } + esp_openthread_lock_acquire(portMAX_DELAY); + bool is_enabled = (otThreadGetDeviceRole(instance) != OT_DEVICE_ROLE_DISABLED); + bool is_ip6_enabled = otIp6IsEnabled(instance); + if (enabled && !is_ip6_enabled) { + if (otIp6SetEnabled(instance, enabled) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to %s netif", enabled ? "enable" : "disable"); + esp_openthread_lock_release(); + return ESP_FAIL; + } + } + if (enabled != is_enabled) { + if (otThreadSetEnabled(instance, enabled) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to %s thread", enabled ? "enable" : "disable"); + esp_openthread_lock_release(); + return ESP_FAIL; + } + } + if (!enabled && is_ip6_enabled) { + if (otIp6SetEnabled(instance, enabled) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to %s netif", enabled ? "enable" : "disable"); + esp_openthread_lock_release(); + return ESP_FAIL; + } + } + esp_openthread_lock_release(); + return ESP_OK; +} + +esp_err_t thread_br_set_active_dataset_tlvs(otOperationalDatasetTlvs *dataset_tlvs) +{ + if (!dataset_tlvs) { + return ESP_ERR_INVALID_ARG; + } + otInstance *instance = esp_openthread_get_instance(); + if (!instance) { + ESP_LOGE(TAG, "Thread not initialized"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t err = ESP_OK; + esp_openthread_lock_acquire(portMAX_DELAY); + if (otDatasetSetActiveTlvs(instance, dataset_tlvs) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set active Thread DatasetTlvs"); + err = ESP_FAIL; + } + esp_openthread_lock_release(); + return err; +} + +esp_err_t thread_br_get_active_dataset_tlvs(otOperationalDatasetTlvs *dataset_tlvs) +{ + if (!dataset_tlvs) { + return ESP_ERR_INVALID_ARG; + } + otInstance *instance = esp_openthread_get_instance(); + if (!instance) { + ESP_LOGE(TAG, "Thread not initialized"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t err = ESP_OK; + esp_openthread_lock_acquire(portMAX_DELAY); + if (otDatasetGetActiveTlvs(instance, dataset_tlvs) != OT_ERROR_NONE) { + ESP_LOGI(TAG, "Active Dataset not set"); + err = ESP_FAIL; + } + esp_openthread_lock_release(); + return err; +} + +esp_err_t thread_br_set_pending_dataset_tlvs(otOperationalDatasetTlvs *dataset_tlvs) +{ + if (!dataset_tlvs) { + return ESP_ERR_INVALID_ARG; + } + otInstance *instance = esp_openthread_get_instance(); + if (!instance) { + ESP_LOGE(TAG, "Thread not initialized"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t err = ESP_OK; + esp_openthread_lock_acquire(portMAX_DELAY); + if (otDatasetSetPendingTlvs(instance, dataset_tlvs) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set pending Thread DatasetTlvs"); + err = ESP_FAIL; + } + esp_openthread_lock_release(); + return err; +} + +esp_err_t thread_br_get_border_agent_id(otBorderAgentId *border_agent_id) +{ + if (!border_agent_id) { + return ESP_ERR_INVALID_ARG; + } + otInstance *instance = esp_openthread_get_instance(); + if (!instance) { + ESP_LOGE(TAG, "Thread not initialized"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t err = ESP_OK; + esp_openthread_lock_acquire(portMAX_DELAY); + if (otBorderAgentGetId(instance, border_agent_id) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to get Border Agent Id"); + err = ESP_FAIL; + } + esp_openthread_lock_release(); + return err; +} + +otDeviceRole thread_br_get_device_role() +{ + if (!esp_openthread_get_instance()) { + return OT_DEVICE_ROLE_DISABLED; + } + esp_openthread_lock_acquire(portMAX_DELAY); + otDeviceRole role = otThreadGetDeviceRole(esp_openthread_get_instance()); + esp_openthread_lock_release(); + return role; +} + +esp_err_t thread_br_generate_and_commit_new_dataset() +{ + otInstance *instance = esp_openthread_get_instance(); + if (!instance) { + ESP_LOGE(TAG, "Thread not initialized"); + return ESP_ERR_INVALID_STATE; + } + if (thread_br_get_device_role() != OT_DEVICE_ROLE_DISABLED) { + ESP_LOGE(TAG, "The device role should be disabled when calling generate_and_commit_new_dataset"); + return ESP_ERR_INVALID_STATE; + } + esp_openthread_lock_acquire(portMAX_DELAY); + otOperationalDataset dataset; + if (otDatasetCreateNewNetwork(instance, &dataset) != OT_ERROR_NONE) { + esp_openthread_lock_release(); + return ESP_FAIL; + } + if (otDatasetSetActive(instance, &dataset) != OT_ERROR_NONE) { + esp_openthread_lock_release(); + return ESP_FAIL; + } + esp_openthread_lock_release(); + return ESP_OK; +} diff --git a/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_launcher.c b/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_launcher.c new file mode 100644 index 0000000..14a5e31 --- /dev/null +++ b/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_launcher.c @@ -0,0 +1,191 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef CONFIG_AUTO_UPDATE_RCP +#include +#endif + +#include "esp_rmaker_thread_br_priv.h" + +static const char *TAG = "thread_br"; + +static esp_openthread_platform_config_t s_platform_config; +#ifdef CONFIG_AUTO_UPDATE_RCP +static bool s_rcp_update = false; +#endif + +#ifdef CONFIG_AUTO_UPDATE_RCP +#define RCP_VERSION_MAX_SIZE 100 +static void update_rcp(void) +{ + // Deinit uart to transfer UART to the serial loader + esp_openthread_rcp_deinit(); + if (esp_rcp_update() == ESP_OK) { + esp_rcp_mark_image_verified(true); + } else { + esp_rcp_mark_image_verified(false); + } + esp_restart(); +} + +static void try_update_ot_rcp(const esp_openthread_platform_config_t *config) +{ + char internal_rcp_version[RCP_VERSION_MAX_SIZE]; + const char *running_rcp_version = otPlatRadioGetVersionString(esp_openthread_get_instance()); + + if (esp_rcp_load_version_in_storage(internal_rcp_version, sizeof(internal_rcp_version)) == ESP_OK) { + ESP_LOGI(TAG, "Internal RCP Version: %s", internal_rcp_version); + ESP_LOGI(TAG, "Running RCP Version: %s", running_rcp_version); + if (strcmp(internal_rcp_version, running_rcp_version) == 0) { + esp_rcp_mark_image_verified(true); + } else { + update_rcp(); + } + } else { + ESP_LOGI(TAG, "RCP firmware not found in storage, will reboot to try next image"); + esp_rcp_mark_image_verified(false); + esp_restart(); + } +} +#endif // CONFIG_AUTO_UPDATE_RCP + +static void rcp_failure_handler(void) +{ +#ifdef CONFIG_AUTO_UPDATE_RCP + esp_rcp_mark_image_unusable(); + char internal_rcp_version[RCP_VERSION_MAX_SIZE]; + if (esp_rcp_load_version_in_storage(internal_rcp_version, sizeof(internal_rcp_version)) == ESP_OK) { + ESP_LOGI(TAG, "Internal RCP Version: %s", internal_rcp_version); + update_rcp(); + } else { + ESP_LOGI(TAG, "RCP firmware not found in storage, will reboot to try next image"); + esp_rcp_mark_image_verified(false); + esp_restart(); + } +#endif // CONFIG_AUTO_UPDATE_RCP +} + +static void ot_task_worker(void *aContext) +{ + esp_netif_config_t cfg = ESP_NETIF_DEFAULT_OPENTHREAD(); + esp_netif_t *openthread_netif = esp_netif_new(&cfg); + assert(openthread_netif != NULL); + + esp_openthread_register_rcp_failure_handler(rcp_failure_handler); + // Initialize the OpenThread stack + ESP_ERROR_CHECK(esp_openthread_init(&s_platform_config)); +#ifdef CONFIG_AUTO_UPDATE_RCP + try_update_ot_rcp(&s_platform_config); +#endif + // Initialize border routing features + esp_openthread_lock_acquire(portMAX_DELAY); + ESP_ERROR_CHECK(esp_netif_attach(openthread_netif, esp_openthread_netif_glue_init(&s_platform_config))); +#if CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC + (void)otLoggingSetLevel(CONFIG_LOG_DEFAULT_LEVEL); +#endif + otInstance *instance = esp_openthread_get_instance(); + if (otDatasetIsCommissioned(instance)) { + (void)otIp6SetEnabled(instance, true); + (void)otThreadSetEnabled(instance, true); + } + esp_openthread_lock_release(); + + esp_openthread_launch_mainloop(); + // Clean up + esp_netif_destroy(openthread_netif); + esp_openthread_netif_glue_deinit(); + esp_vfs_eventfd_unregister(); + vTaskDelete(NULL); +} + +static void thread_br_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, + void* event_data) +{ + if (event_base == OPENTHREAD_EVENT && event_id == OPENTHREAD_EVENT_ROLE_CHANGED) { + esp_openthread_role_changed_event_t *role_changed = (esp_openthread_role_changed_event_t *)event_data; + if (!role_changed) { + return; + } + esp_rmaker_thread_br_report_device_role(); + if (role_changed->current_role == OT_DEVICE_ROLE_ROUTER || + role_changed->current_role == OT_DEVICE_ROLE_LEADER) { + esp_rmaker_thread_br_report_border_agent_id(); + } + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) { + esp_openthread_lock_acquire(portMAX_DELAY); + // Create link local address for Wi-Fi Station interface + esp_netif_create_ip6_linklocal(esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")); + static bool border_router_initialized = false; + if (!border_router_initialized) { + ESP_ERROR_CHECK(esp_openthread_border_router_init()); + border_router_initialized = true; + } + esp_openthread_lock_release(); + } +} + +esp_err_t thread_border_router_start(const esp_openthread_platform_config_t *platform_config, + const esp_rcp_update_config_t *rcp_update_config) +{ + static bool thread_br_started = false; + if (thread_br_started) { + return ESP_OK; + } + if (!platform_config) { + return ESP_ERR_INVALID_ARG; + } + memcpy(&s_platform_config, platform_config, sizeof(esp_openthread_platform_config_t)); + esp_vfs_eventfd_config_t eventfd_config = { + .max_fds = 4, + }; + ESP_RETURN_ON_ERROR(esp_vfs_eventfd_register(&eventfd_config), TAG, "Failed to register eventfd"); + esp_openthread_set_backbone_netif(esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")); + ESP_ERROR_CHECK(mdns_init()); +#define MDNS_MAX_NAME_LEN 64 + char hostname[MDNS_MAX_NAME_LEN]; + if (mdns_hostname_get(hostname) != ESP_OK) { + // if hostname is not set we will set it with the rainmaker node id. + ESP_ERROR_CHECK(mdns_hostname_set(esp_rmaker_get_node_id())); + } +#ifdef CONFIG_AUTO_UPDATE_RCP + if (rcp_update_config) { + esp_rcp_update_init(rcp_update_config); + s_rcp_update = true; + } +#endif +#define THREAD_TASK_STACK_SIZE 8192 + if (xTaskCreate(ot_task_worker, "ot_br", THREAD_TASK_STACK_SIZE, NULL, 5, NULL) != pdTRUE) { + ESP_LOGE(TAG, "Failed to start openthread task for thread br"); + return ESP_FAIL; + } + esp_event_handler_register(OPENTHREAD_EVENT, OPENTHREAD_EVENT_ROLE_CHANGED, thread_br_event_handler, NULL); + esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, thread_br_event_handler, NULL); + thread_br_started = true; + return ESP_OK; +} diff --git a/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_priv.h b/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_priv.h new file mode 100644 index 0000000..4a6f94c --- /dev/null +++ b/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_priv.h @@ -0,0 +1,67 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "esp_rcp_update.h" +#include +#include +#include + +/********** THREAD BR PARAMS NAME ***********/ +#define ESP_RMAKER_DEF_BORDER_AGENT_ID "BorderAgentId" +#define ESP_RMAKER_DEF_ACTIVE_DATASET "ActiveDataset" +#define ESP_RMAKER_DEF_PENDING_DATASET "PendingDataset" +#define ESP_RMAKER_DEF_DEVICE_ROLE "DeviceRole" +#define ESP_RMAKER_DEF_THREAD_BR_CMD "ThreadCmd" + +/********** THREAD BR SERVICE NAME ***********/ +#define ESP_RMAKER_DEF_THREAD_BR_SERVICE "TBRService" + +/********** THREAD BR PARAM TYPES ***********/ +#define ESP_RMAKER_PARAM_TBR_BORDER_AGENT_ID "esp.param.tbr-border-agent-id" +#define ESP_RMAKER_PARAM_TBR_ACTIVE_DATASET "esp.param.tbr-active-dataset" +#define ESP_RMAKER_PARAM_TBR_PENDING_DATASET "esp.param.tbr-pending-dataset" +#define ESP_RMAKER_PARAM_TBR_DEVICE_ROLE "esp.param.tbr-device-role" +#define ESP_RMAKER_PARAM_TBR_CMD "esp.param.tbr-cmd" + +/********** THREAD BR SERVICE TYPE ***********/ +#define ESP_RMAKER_SERVICE_THREAD_BR "esp.service.thread-br" + +/********** THREAD BR CMD TYPE ***********/ +#define ESP_RMAKER_THREAD_BR_CMD_GEN_DATASET 1 + +esp_rmaker_device_t *esp_rmaker_thread_br_service_create(const char *serv_name, esp_rmaker_device_write_cb_t write_cb, + esp_rmaker_device_read_cb_t read_cb, void *priv_data); + +esp_err_t esp_rmaker_thread_br_report_device_role(void); + +esp_err_t esp_rmaker_thread_br_report_border_agent_id(void); + +esp_err_t thread_br_set_thread_enabled(bool enabled); + +esp_err_t thread_br_set_active_dataset_tlvs(otOperationalDatasetTlvs *dataset_tlvs); + +esp_err_t thread_br_get_active_dataset_tlvs(otOperationalDatasetTlvs *dataset_tlvs); + +esp_err_t thread_br_set_pending_dataset_tlvs(otOperationalDatasetTlvs *dataset_tlvs); + +esp_err_t thread_br_get_border_agent_id(otBorderAgentId *border_agent_id); + +otDeviceRole thread_br_get_device_role(void); + +esp_err_t thread_br_generate_and_commit_new_dataset(void); + +esp_err_t thread_border_router_start(const esp_openthread_platform_config_t *platform_config, + const esp_rcp_update_config_t *rcp_update_config); diff --git a/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_service.c b/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_service.c new file mode 100644 index 0000000..bc18c1d --- /dev/null +++ b/components/esp_rainmaker/src/thread_br/esp_rmaker_thread_br_service.c @@ -0,0 +1,74 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#include +#include +#include +#include + +static esp_rmaker_param_t *esp_rmaker_thread_br_border_agent_id_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_TBR_BORDER_AGENT_ID, + esp_rmaker_str(""), PROP_FLAG_READ); + return param; +} + +static esp_rmaker_param_t *esp_rmaker_thread_br_active_dataset_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_TBR_ACTIVE_DATASET, + esp_rmaker_str(""), + PROP_FLAG_READ | PROP_FLAG_WRITE | PROP_FLAG_PERSIST); + return param; +} + +static esp_rmaker_param_t *esp_rmaker_thread_br_pending_dataset_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_TBR_PENDING_DATASET, + esp_rmaker_str(""), + PROP_FLAG_READ | PROP_FLAG_WRITE | PROP_FLAG_PERSIST); + return param; +} + +static esp_rmaker_param_t *esp_rmaker_thread_br_thread_role_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_TBR_DEVICE_ROLE, + esp_rmaker_int(0), PROP_FLAG_READ); + return param; +} + +static esp_rmaker_param_t *esp_rmaker_thread_br_cmd_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_TBR_CMD, + esp_rmaker_int(0), PROP_FLAG_WRITE); + return param; +} + +esp_rmaker_device_t *esp_rmaker_thread_br_service_create(const char *serv_name, esp_rmaker_device_write_cb_t write_cb, + esp_rmaker_device_read_cb_t read_cb, void *priv_data) +{ + esp_rmaker_device_t *service = esp_rmaker_service_create(serv_name, ESP_RMAKER_SERVICE_THREAD_BR, priv_data); + if (service) { + esp_rmaker_device_add_cb(service, write_cb, read_cb); + esp_rmaker_device_add_param( + service, esp_rmaker_thread_br_border_agent_id_param_create(ESP_RMAKER_DEF_BORDER_AGENT_ID)); + esp_rmaker_device_add_param( + service, esp_rmaker_thread_br_active_dataset_param_create(ESP_RMAKER_DEF_ACTIVE_DATASET)); + esp_rmaker_device_add_param( + service, esp_rmaker_thread_br_pending_dataset_param_create(ESP_RMAKER_DEF_PENDING_DATASET)); + esp_rmaker_device_add_param(service, esp_rmaker_thread_br_thread_role_param_create(ESP_RMAKER_DEF_DEVICE_ROLE)); + esp_rmaker_device_add_param(service, esp_rmaker_thread_br_cmd_param_create(ESP_RMAKER_DEF_THREAD_BR_CMD)); + } + return service; +} diff --git a/components/esp_schedule/CMakeLists.txt b/components/esp_schedule/CMakeLists.txt new file mode 100644 index 0000000..02eb952 --- /dev/null +++ b/components/esp_schedule/CMakeLists.txt @@ -0,0 +1,10 @@ +set(component_srcs "src/esp_schedule.c" + "src/esp_schedule_nvs.c") + +idf_component_register(SRCS "${component_srcs}" + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "src" + PRIV_REQUIRES "rmaker_common" + REQUIRES "nvs_flash") + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-function") diff --git a/components/esp_schedule/LICENSE b/components/esp_schedule/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/components/esp_schedule/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/components/esp_schedule/README.md b/components/esp_schedule/README.md new file mode 100644 index 0000000..3646ec0 --- /dev/null +++ b/components/esp_schedule/README.md @@ -0,0 +1,50 @@ +# ESP Scheduling + +[![Component Registry](https://components.espressif.com/components/espressif/esp_schedule/badge.svg)](https://components.espressif.com/components/espressif/esp_schedule) + +This component is used internally by ESP RainMaker to implement schedules. + +> Note: By default, the time is w.r.t. UTC. If the timezone has been set, then the time is w.r.t. the specified timezone. + +## Test code: + +``` +#include + +void app_schedule_trigger_cb(esp_schedule_handle_t handle, void *priv_data) +{ + printf("priv_data: %.*s\n", (char *)priv_data); +} + +static char *priv_data_global = "from app"; + +void app_schedule_set() +{ + esp_schedule_config_t schedule_config = { + .name = "test", + .trigger.type = ESP_SCHEDULE_TYPE_DAYS_OF_WEEK, + .trigger.hours = 13, + .trigger.minutes = 30, + .trigger.day.repeat_days = ESP_SCHEDULE_DAY_MONDAY | ESP_SCHEDULE_DAY_THURSDAY, + .trigger_cb = app_schedule_trigger_cb, + .priv_data = priv_data_global, + }; + esp_schedule_create(&schedule_config); +} + +void app_schedule_init() +{ + uint8_t schedule_count; + esp_schedule_handle_t *schedule_list = esp_schedule_init(true, NULL, &schedule_count); + if (schedule_count > 0 && schedule_list != NULL) { + ESP_LOGI(TAG, "Found %d schedule(s) in NVS.", schedule_count); + for (size_t i = 0; i < schedule_count; i++) { + esp_schedule_config_t schedule_config; + esp_schedule_get(schedule_list[i], &schedule_config); + schedule_config.trigger_cb = app_schedule_trigger_cb; + schedule_config.priv_data = priv_data_global; + esp_schedule_edit(schedule_list[i], &schedule_config); + } + } +} +``` diff --git a/components/esp_schedule/component.mk b/components/esp_schedule/component.mk new file mode 100644 index 0000000..49844ec --- /dev/null +++ b/components/esp_schedule/component.mk @@ -0,0 +1,5 @@ +COMPONENT_ADD_INCLUDEDIRS := include + +COMPONENT_SRCDIRS := src + +CFLAGS += -Wno-unused-function diff --git a/components/esp_schedule/idf_component.yml b/components/esp_schedule/idf_component.yml new file mode 100644 index 0000000..5e875ac --- /dev/null +++ b/components/esp_schedule/idf_component.yml @@ -0,0 +1,9 @@ +## IDF Component Manager Manifest File +version: "1.2.0" +description: ESP Schedules, used in RainMaker +url: https://github.com/espressif/esp-rainmaker/tree/master/components/esp_schedule +repository: https://github.com/espressif/esp-rainmaker.git +issues: https://github.com/espressif/esp-rainmaker/issues +dependencies: + espressif/rmaker_common: + version: "~1.4.2" diff --git a/components/esp_schedule/include/esp_schedule.h b/components/esp_schedule/include/esp_schedule.h new file mode 100644 index 0000000..4c75ebf --- /dev/null +++ b/components/esp_schedule/include/esp_schedule.h @@ -0,0 +1,236 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** Schedule Handle */ +typedef void *esp_schedule_handle_t; + +/** Maximum length of the schedule name allowed. This value cannot be more than 16 as it is used for NVS key. */ +#define MAX_SCHEDULE_NAME_LEN 16 + +/** Callback for schedule trigger + * + * This callback is called when the schedule is triggered. + * + * @param[in] handle Schedule handle. + * @param[in] priv_data Pointer to the private data passed while creating/editing the schedule. + */ +typedef void (*esp_schedule_trigger_cb_t)(esp_schedule_handle_t handle, void *priv_data); + +/** Callback for schedule timestamp + * + * This callback is called when the next trigger timestamp of the schedule is changed. This might be useful to check if + * one time schedules have already passed while the device was powered off. + * + * @param[in] handle Schedule handle. + * @param[in] next_timestamp timestamp at which the schedule will trigger next. + * @param[in] priv_data Pointer to the user data passed while creating/editing the schedule. + */ +typedef void (*esp_schedule_timestamp_cb_t)(esp_schedule_handle_t handle, uint32_t next_timestamp, void *priv_data); + +/** Schedule type */ +typedef enum esp_schedule_type { + ESP_SCHEDULE_TYPE_INVALID = 0, + ESP_SCHEDULE_TYPE_DAYS_OF_WEEK, + ESP_SCHEDULE_TYPE_DATE, + ESP_SCHEDULE_TYPE_RELATIVE, +} esp_schedule_type_t; + +/** Schedule days. Used for ESP_SCHEDULE_TYPE_DAYS_OF_WEEK. */ +typedef enum esp_schedule_days { + ESP_SCHEDULE_DAY_ONCE = 0, + ESP_SCHEDULE_DAY_EVERYDAY = 0b1111111, + ESP_SCHEDULE_DAY_MONDAY = 1 << 0, + ESP_SCHEDULE_DAY_TUESDAY = 1 << 1, + ESP_SCHEDULE_DAY_WEDNESDAY = 1 << 2, + ESP_SCHEDULE_DAY_THURSDAY = 1 << 3, + ESP_SCHEDULE_DAY_FRIDAY = 1 << 4, + ESP_SCHEDULE_DAY_SATURDAY = 1 << 5, + ESP_SCHEDULE_DAY_SUNDAY = 1 << 6, +} esp_schedule_days_t; + +/** Schedule months. Used for ESP_SCHEDULE_TYPE_DATE. */ +typedef enum esp_schedule_months { + ESP_SCHEDULE_MONTH_ONCE = 0, + ESP_SCHEDULE_MONTH_ALL = 0b1111111, + ESP_SCHEDULE_MONTH_JANUARY = 1 << 0, + ESP_SCHEDULE_MONTH_FEBRUARY = 1 << 1, + ESP_SCHEDULE_MONTH_MARCH = 1 << 2, + ESP_SCHEDULE_MONTH_APRIL = 1 << 3, + ESP_SCHEDULE_MONTH_MAY = 1 << 4, + ESP_SCHEDULE_MONTH_JUNE = 1 << 5, + ESP_SCHEDULE_MONTH_JULY = 1 << 6, + ESP_SCHEDULE_MONTH_AUGUST = 1 << 7, + ESP_SCHEDULE_MONTH_SEPTEMBER = 1 << 8, + ESP_SCHEDULE_MONTH_OCTOBER = 1 << 9, + ESP_SCHEDULE_MONTH_NOVEMBER = 1 << 10, + ESP_SCHEDULE_MONTH_DECEMBER = 1 << 11, +} esp_schedule_months_t; + +/** Trigger details of the schedule */ +typedef struct esp_schedule_trigger { + /** Type of schedule */ + esp_schedule_type_t type; + /** Hours in 24 hour format. Accepted values: 0-23 */ + uint8_t hours; + /** Minutes in the given hour. Accepted values: 0-59. */ + uint8_t minutes; + /** For type ESP_SCHEDULE_TYPE_DAYS_OF_WEEK */ + struct { + /** 'OR' list of esp_schedule_days_t */ + uint8_t repeat_days; + } day; + /** For type ESP_SCHEDULE_TYPE_DATE */ + struct { + /** Day of the month. Accepted values: 1-31. */ + uint8_t day; + /* 'OR' list of esp_schedule_months_t */ + uint16_t repeat_months; + /** Year */ + uint16_t year; + /** If the schedule is to be repeated every year. */ + bool repeat_every_year; + } date; + /** For type ESP_SCHEDULE_TYPE_SECONDS */ + int relative_seconds; + /** Used for passing the next schedule timestamp for + * ESP_SCHEDULE_TYPE_RELATIVE */ + time_t next_scheduled_time_utc; +} esp_schedule_trigger_t; + +/** Schedule Validity + * Start and end time within which the schedule will be applicable. + */ +typedef struct esp_schedule_validity { + /* Start time as UTC timestamp */ + time_t start_time; + /* End time as UTC timestamp */ + time_t end_time; +} esp_schedule_validity_t; + +/** Schedule config */ +typedef struct esp_schedule_config { + /** Name of the schedule. This is like a primary key for the schedule. This is required. +1 for NULL termination. */ + char name[MAX_SCHEDULE_NAME_LEN + 1]; + /** Trigger details */ + esp_schedule_trigger_t trigger; + /** Trigger callback */ + esp_schedule_trigger_cb_t trigger_cb; + /** Timestamp callback */ + esp_schedule_timestamp_cb_t timestamp_cb; + /** Private data associated with the schedule. This will be passed to callbacks. */ + void *priv_data; + /** Validity of schedules. */ + esp_schedule_validity_t validity; +} esp_schedule_config_t; + +/** Initialize ESP Schedule + * + * This initializes ESP Schedule. This must be called first before calling any of the other APIs. + * This API also gets all the schedules from NVS (if it has been enabled). + * + * Note: After calling this API, the pointers to the callbacks should be updated for all the schedules by calling + * esp_schedule_get() followed by esp_schedule_edit() with the correct callbacks. + * + * @param[in] enable_nvs If NVS is to be enabled or not. + * @param[in] nvs_partition (Optional) The NVS partition to be used. If NULL is passed, the default partition is used. + * @param[out] schedule_count Number of active schedules found in NVS. + * + * @return Array of schedule handles if any schedules have been found. + * @return NULL if no schedule is found in NVS (or if NVS is not enabled). + */ +esp_schedule_handle_t *esp_schedule_init(bool enable_nvs, char *nvs_partition, uint8_t *schedule_count); + +/** Create Schedule + * + * This API can be used to create a new schedule. The schedule still needs to be enabled using + * esp_schedule_enable(). + * + * @param[in] schedule_config Configuration of the schedule to be created. + * + * @return Schedule handle if successfully created. + * @return NULL in case of error. + */ +esp_schedule_handle_t esp_schedule_create(esp_schedule_config_t *schedule_config); + +/** Remove Schedule + * + * This API can be used to remove an existing schedule. + * + * @param[in] handle Schedule handle for the schedule to be removed. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_delete(esp_schedule_handle_t handle); + +/** Edit Schedule + * + * This API can be used to edit an existing schedule. + * The schedule name should be same as when the schedule was created. The complete config must be provided + * or the previously stored config might be over-written. + * + * Note: If a schedule is edited when it is on-going, the new changes will not be reflected. + * You will need to disable the schedule, edit it, and then enable it again. + * + * @param[in] handle Schedule handle for the schedule to be edited. + * @param[in] schedule_config Configuration of the schedule to be edited. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_edit(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config); + +/** Enable Schedule + * + * This API can be used to enable an existing schedule. + * It can be used to enable a schedule after it has been created using esp_schedule_create() + * or if the schedule has been disabled using esp_schedule_disable(). + * + * @param[in] handle Schedule handle for the schedule to be enabled. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_enable(esp_schedule_handle_t handle); + +/** Disable Schedule + * + * This API can be used to disable an on-going schedule. + * It does not remove the schedule, just stops it. The schedule can be enabled again using + * esp_schedule_enable(). + * + * @param[in] handle Schedule handle for the schedule to be disabled. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_disable(esp_schedule_handle_t handle); + +/** Get Schedule + * + * This API can be used to get details of an existing schedule. + * The schedule_config is populated with the schedule details. + * + * @param[in] handle Schedule handle. + * @param[out] schedule_config Details of the schedule whose handle is passed. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_get(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_schedule/src/esp_schedule.c b/components/esp_schedule/src/esp_schedule.c new file mode 100644 index 0000000..017ecb3 --- /dev/null +++ b/components/esp_schedule/src/esp_schedule.c @@ -0,0 +1,600 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include "esp_schedule_internal.h" + +static const char *TAG = "esp_schedule"; + +#define SECONDS_TILL_2020 ((2020 - 1970) * 365 * 24 * 3600) +#define SECONDS_IN_DAY (60 * 60 * 24) + +static bool init_done = false; + +static int esp_schedule_get_no_of_days(esp_schedule_trigger_t *trigger, struct tm *current_time, struct tm *schedule_time) +{ + /* for day, monday = 0, sunday = 6. */ + int next_day = 0; + /* struct tm has tm_wday with sunday as 0. Whereas we have monday as 0. Converting struct tm to our format */ + int today = ((current_time->tm_wday + 7 - 1) % 7); + + esp_schedule_days_t today_bit = 1 << today; + uint8_t repeat_days = trigger->day.repeat_days; + int current_seconds = (current_time->tm_hour * 60 + current_time->tm_min) * 60 + current_time->tm_sec; + int schedule_seconds = (schedule_time->tm_hour * 60 + schedule_time->tm_min) * 60; + + /* Handling for one time schedule */ + if (repeat_days == ESP_SCHEDULE_DAY_ONCE) { + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off */ + return 0; + } else { + /* The schedule is tomorrow */ + return 1; + } + } + + /* Handling for repeating schedules */ + /* Check if it is today */ + if ((repeat_days & today_bit)) { + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off. */ + return 0; + } + } + /* Check if it is this week or next week */ + if ((repeat_days & (today_bit ^ 0xFF)) > today_bit) { + /* Next schedule is yet to come in this week */ + next_day = ffs(repeat_days & (0xFF << (today + 1))) - 1; + return (next_day - today); + } else { + /* First scheduled day of the next week */ + next_day = ffs(repeat_days) - 1; + if (next_day == today) { + /* Same day, next week */ + return 7; + } + return (7 - today + next_day); + } + + ESP_LOGE(TAG, "No of days could not be found. This should not happen."); + return 0; +} + +static uint8_t esp_schedule_get_next_month(esp_schedule_trigger_t *trigger, struct tm *current_time, struct tm *schedule_time) +{ + int current_seconds = (current_time->tm_hour * 60 + current_time->tm_min) * 60 + current_time->tm_sec; + int schedule_seconds = (schedule_time->tm_hour * 60 + schedule_time->tm_min) * 60; + /* +1 is because struct tm has months starting from 0, whereas we have them starting from 1 */ + uint8_t current_month = current_time->tm_mon + 1; + /* -1 because month_bit starts from 0b1. So for January, it should be 1 << 0. And current_month starts from 1. */ + uint16_t current_month_bit = 1 << (current_month - 1); + uint8_t next_schedule_month = 0; + uint16_t repeat_months = trigger->date.repeat_months; + + /* Check if month is not specified */ + if (repeat_months == ESP_SCHEDULE_MONTH_ONCE) { + if (trigger->date.day == current_time->tm_mday) { + /* The schedule day is same. Check if time has already passed */ + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off */ + return current_month; + } else { + /* Today's time has passed */ + return (current_month + 1); + } + } else if (trigger->date.day > current_time->tm_mday) { + /* The day is yet to come in this month */ + return current_month; + } else { + /* The day has passed in the current month */ + return (current_month + 1); + } + } + + /* Check if schedule is not this year itself, it is in future. */ + if (trigger->date.year > (current_time->tm_year + 1900)) { + /* Find first schedule month of next year */ + next_schedule_month = ffs(repeat_months); + /* Year will be handled by the caller. So no need to add any additional months */ + return next_schedule_month; + } + + /* Check if schedule is this month and is yet to come */ + if (current_month_bit & repeat_months) { + if (trigger->date.day == current_time->tm_mday) { + /* The schedule day is same. Check if time has already passed */ + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off */ + return current_month; + } + } + if (trigger->date.day > current_time->tm_mday) { + /* The day is yet to come in this month */ + return current_month; + } + } + + /* Check if schedule is this year */ + if ((repeat_months & (current_month_bit ^ 0xFFFF)) > current_month_bit) { + /* Next schedule month is yet to come in this year */ + next_schedule_month = ffs(repeat_months & (0xFFFF << (current_month))); + return next_schedule_month; + } + + /* Check if schedule is for this year and does not repeat */ + if (!trigger->date.repeat_every_year) { + if (trigger->date.year <= (current_time->tm_year + 1900)) { + ESP_LOGE(TAG, "Schedule does not repeat next year, but get_next_month has been called."); + return 0; + } + } + + /* Schedule is not this year */ + /* Find first schedule month of next year */ + next_schedule_month = ffs(repeat_months); + /* +12 because the schedule is next year */ + return (next_schedule_month + 12); +} + +static uint16_t esp_schedule_get_next_year(esp_schedule_trigger_t *trigger, struct tm *current_time, struct tm *schedule_time) +{ + uint16_t current_year = current_time->tm_year + 1900; + uint16_t schedule_year = trigger->date.year; + if (schedule_year > current_year) { + return schedule_year; + } + /* If the schedule is set to repeat_every_year, we return the current year */ + /* If the schedule has already passed in this year, we still return current year, as the additional months will be handled in get_next_month */ + return current_year; +} + +static uint32_t esp_schedule_get_next_schedule_time_diff(const char *schedule_name, esp_schedule_trigger_t *trigger) +{ + struct tm current_time, schedule_time; + time_t now; + char time_str[64]; + int32_t time_diff; + + /* Get current time */ + time(&now); + /* Handling ESP_SCHEDULE_TYPE_RELATIVE first since it doesn't require any + * computation based on days, hours, minutes, etc. + */ + if (trigger->type == ESP_SCHEDULE_TYPE_RELATIVE) { + /* If next scheduled time is already set, just compute the difference + * between current time and next scheduled time and return that diff. + */ + time_t target; + if (trigger->next_scheduled_time_utc > 0) { + target = (time_t)trigger->next_scheduled_time_utc; + time_diff = difftime(target, now); + } else { + target = now + (time_t)trigger->relative_seconds; + time_diff = trigger->relative_seconds; + } + localtime_r(&target, &schedule_time); + trigger->next_scheduled_time_utc = mktime(&schedule_time); + /* Print schedule time */ + memset(time_str, 0, sizeof(time_str)); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &schedule_time); + ESP_LOGI(TAG, "Schedule %s will be active on: %s. DST: %s", schedule_name, time_str, schedule_time.tm_isdst ? "Yes" : "No"); + return time_diff; + } + localtime_r(&now, ¤t_time); + + /* Get schedule time */ + localtime_r(&now, &schedule_time); + schedule_time.tm_sec = 0; + schedule_time.tm_min = trigger->minutes; + schedule_time.tm_hour = trigger->hours; + mktime(&schedule_time); + + /* Adjust schedule day */ + if (trigger->type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + int no_of_days = 0; + no_of_days = esp_schedule_get_no_of_days(trigger, ¤t_time, &schedule_time); + schedule_time.tm_sec += no_of_days * SECONDS_IN_DAY; + } + if (trigger->type == ESP_SCHEDULE_TYPE_DATE) { + schedule_time.tm_mday = trigger->date.day; + schedule_time.tm_mon = esp_schedule_get_next_month(trigger, ¤t_time, &schedule_time) - 1; + schedule_time.tm_year = esp_schedule_get_next_year(trigger, ¤t_time, &schedule_time) - 1900; + if (schedule_time.tm_mon < 0) { + ESP_LOGE(TAG, "Invalid month found: %d. Setting it to next month.", schedule_time.tm_mon); + schedule_time.tm_mon = current_time.tm_mon + 1; + } + if (schedule_time.tm_mon >= 12) { + schedule_time.tm_year += schedule_time.tm_mon / 12; + schedule_time.tm_mon = schedule_time.tm_mon % 12; + } + } + mktime(&schedule_time); + + /* Adjust time according to DST */ + time_t dst_adjust = 0; + if (!current_time.tm_isdst && schedule_time.tm_isdst) { + dst_adjust = -3600; + } else if (current_time.tm_isdst && !schedule_time.tm_isdst ) { + dst_adjust = 3600; + } + ESP_LOGD(TAG, "DST adjust seconds: %lld", (long long) dst_adjust); + schedule_time.tm_sec += dst_adjust; + mktime(&schedule_time); + + /* Print schedule time */ + memset(time_str, 0, sizeof(time_str)); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &schedule_time); + ESP_LOGI(TAG, "Schedule %s will be active on: %s. DST: %s", schedule_name, time_str, schedule_time.tm_isdst ? "Yes" : "No"); + + /* Calculate difference */ + time_diff = difftime((mktime(&schedule_time)), mktime(¤t_time)); + + /* For one time schedules to check for expiry after a reboot. If NVS is enabled, this should be stored in NVS. */ + trigger->next_scheduled_time_utc = mktime(&schedule_time); + + return time_diff; +} + +static bool esp_schedule_is_expired(esp_schedule_trigger_t *trigger) +{ + time_t current_timestamp = 0; + struct tm current_time = {0}; + time(¤t_timestamp); + localtime_r(¤t_timestamp, ¤t_time); + + if (trigger->type == ESP_SCHEDULE_TYPE_RELATIVE) { + if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) { + /* Relative seconds based schedule has expired */ + return true; + } else if (trigger->next_scheduled_time_utc == 0) { + /* Schedule has been disabled , so it is as good as expired. */ + return true; + } + } else if (trigger->type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + if (trigger->day.repeat_days == ESP_SCHEDULE_DAY_ONCE) { + if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) { + /* One time schedule has expired */ + return true; + } else if (trigger->next_scheduled_time_utc == 0) { + /* Schedule has been disabled , so it is as good as expired. */ + return true; + } + } + } else if (trigger->type == ESP_SCHEDULE_TYPE_DATE) { + if (trigger->date.repeat_months == 0) { + if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) { + /* One time schedule has expired */ + return true; + } else { + return false; + } + } + if (trigger->date.repeat_every_year == true) { + return false; + } + + struct tm schedule_time = {0}; + localtime_r(¤t_timestamp, &schedule_time); + schedule_time.tm_sec = 0; + schedule_time.tm_min = trigger->minutes; + schedule_time.tm_hour = trigger->hours; + schedule_time.tm_mday = trigger->date.day; + /* For expiry, just check the last month of the repeat_months. */ + /* '-1' because struct tm has months starting from 0 and we have months starting from 1. */ + schedule_time.tm_mon = fls(trigger->date.repeat_months) - 1; + /* '-1900' because struct tm has number of years after 1900 */ + schedule_time.tm_year = trigger->date.year - 1900; + time_t schedule_timestamp = mktime(&schedule_time); + + if (schedule_timestamp < current_timestamp) { + return true; + } + } else { + /* Invalid type. Mark as expired */ + return true; + } + return false; +} + +static void esp_schedule_stop_timer(esp_schedule_t *schedule) +{ + xTimerStop(schedule->timer, portMAX_DELAY); +} + +static void esp_schedule_start_timer(esp_schedule_t *schedule) +{ + time_t current_time = 0; + time(¤t_time); + if (current_time < SECONDS_TILL_2020) { + ESP_LOGE(TAG, "Time is not updated"); + return; + } + + schedule->next_scheduled_time_diff = esp_schedule_get_next_schedule_time_diff(schedule->name, &schedule->trigger); + ESP_LOGI(TAG, "Starting a timer for %"PRIu32" seconds for schedule %s", schedule->next_scheduled_time_diff, schedule->name); + + if (schedule->timestamp_cb) { + schedule->timestamp_cb((esp_schedule_handle_t)schedule, schedule->trigger.next_scheduled_time_utc, schedule->priv_data); + } + + xTimerStop(schedule->timer, portMAX_DELAY); + xTimerChangePeriod(schedule->timer, (schedule->next_scheduled_time_diff * 1000) / portTICK_PERIOD_MS, portMAX_DELAY); +} + +static void esp_schedule_common_timer_cb(TimerHandle_t timer) +{ + void *priv_data = pvTimerGetTimerID(timer); + if (priv_data == NULL) { + return; + } + esp_schedule_t *schedule = (esp_schedule_t *)priv_data; + time_t now; + time(&now); + struct tm validity_time; + char time_str[64] = {0}; + if (schedule->validity.start_time != 0) { + if (now < schedule->validity.start_time) { + memset(time_str, 0, sizeof(time_str)); + localtime_r(&schedule->validity.start_time, &validity_time); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &validity_time); + ESP_LOGW(TAG, "Schedule %s skipped. It will be active only after: %s. DST: %s.", schedule->name, time_str, validity_time.tm_isdst ? "Yes" : "No"); + /* TODO: Start the timer such that the next time it triggeres, it will be within the valid window. + * Currently, it will just keep triggering and then get skipped if not in valid range. + */ + goto restart_schedule; + } + } + if (schedule->validity.end_time != 0) { + if (now > schedule->validity.end_time) { + localtime_r(&schedule->validity.end_time, &validity_time); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &validity_time); + ESP_LOGW(TAG, "Schedule %s skipped. It can't be active after: %s. DST: %s.", schedule->name, time_str, validity_time.tm_isdst ? "Yes" : "No"); + /* Return from here will ensure that the timer does not start again for this schedule */ + return; + } + } + ESP_LOGI(TAG, "Schedule %s triggered", schedule->name); + if (schedule->trigger_cb) { + schedule->trigger_cb((esp_schedule_handle_t)schedule, schedule->priv_data); + } + +restart_schedule: + + if (esp_schedule_is_expired(&schedule->trigger)) { + /* Not deleting the schedule here. Just not starting it again. */ + return; + } + esp_schedule_start_timer(schedule); +} + +static void esp_schedule_delete_timer(esp_schedule_t *schedule) +{ + xTimerDelete(schedule->timer, portMAX_DELAY); +} + +static void esp_schedule_create_timer(esp_schedule_t *schedule) +{ + if (esp_schedule_nvs_is_enabled()) { + /* This is just used for calculating next_scheduled_time_utc for ESP_SCHEDULE_DAY_ONCE (in case of ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) or for ESP_SCHEDULE_MONTH_ONCE (in case of ESP_SCHEDULE_TYPE_DATE), and only used when NVS is enabled. And if NVS is enabled, time will already be synced and the time will be correctly calculated. */ + schedule->next_scheduled_time_diff = esp_schedule_get_next_schedule_time_diff(schedule->name, &schedule->trigger); + } + + /* Temporarily setting the timer for 1 (anything greater than 0) tick. This will get changed when xTimerChangePeriod() is called. */ + schedule->timer = xTimerCreate("schedule", 1, pdFALSE, (void *)schedule, esp_schedule_common_timer_cb); +} + +esp_err_t esp_schedule_get(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config) +{ + if (schedule_config == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + + strcpy(schedule_config->name, schedule->name); + schedule_config->trigger.type = schedule->trigger.type; + schedule_config->trigger.hours = schedule->trigger.hours; + schedule_config->trigger.minutes = schedule->trigger.minutes; + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + schedule_config->trigger.day.repeat_days = schedule->trigger.day.repeat_days; + } else if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DATE) { + schedule_config->trigger.date.day = schedule->trigger.date.day; + schedule_config->trigger.date.repeat_months = schedule->trigger.date.repeat_months; + schedule_config->trigger.date.year = schedule->trigger.date.year; + schedule_config->trigger.date.repeat_every_year = schedule->trigger.date.repeat_every_year; + } + + schedule_config->trigger_cb = schedule->trigger_cb; + schedule_config->timestamp_cb = schedule->timestamp_cb; + schedule_config->priv_data = schedule->priv_data; + schedule_config->validity = schedule->validity; + return ESP_OK; +} + +esp_err_t esp_schedule_enable(esp_schedule_handle_t handle) +{ + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + esp_schedule_start_timer(schedule); + return ESP_OK; +} + +esp_err_t esp_schedule_disable(esp_schedule_handle_t handle) +{ + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + esp_schedule_stop_timer(schedule); + /* Disabling a schedule should also reset the next_scheduled_time. + * It would be re-computed after enabling. + */ + schedule->trigger.next_scheduled_time_utc = 0; + return ESP_OK; +} + +static esp_err_t esp_schedule_set(esp_schedule_t *schedule, esp_schedule_config_t *schedule_config) +{ + /* Setting everything apart from name. */ + schedule->trigger.type = schedule_config->trigger.type; + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_RELATIVE) { + schedule->trigger.relative_seconds = schedule_config->trigger.relative_seconds; + schedule->trigger.next_scheduled_time_utc = schedule_config->trigger.next_scheduled_time_utc; + } else { + schedule->trigger.hours = schedule_config->trigger.hours; + schedule->trigger.minutes = schedule_config->trigger.minutes; + + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + schedule->trigger.day.repeat_days = schedule_config->trigger.day.repeat_days; + } else if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DATE) { + schedule->trigger.date.day = schedule_config->trigger.date.day; + schedule->trigger.date.repeat_months = schedule_config->trigger.date.repeat_months; + schedule->trigger.date.year = schedule_config->trigger.date.year; + schedule->trigger.date.repeat_every_year = schedule_config->trigger.date.repeat_every_year; + } + } + + schedule->trigger_cb = schedule_config->trigger_cb; + schedule->timestamp_cb = schedule_config->timestamp_cb; + schedule->priv_data = schedule_config->priv_data; + schedule->validity = schedule_config->validity; + esp_schedule_nvs_add(schedule); + return ESP_OK; +} + +esp_err_t esp_schedule_edit(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config) +{ + if (handle == NULL || schedule_config == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + if (strncmp(schedule->name, schedule_config->name, sizeof(schedule->name)) != 0) { + ESP_LOGE(TAG, "Schedule name mismatch. Expected: %s, Passed: %s", schedule->name, schedule_config->name); + return ESP_FAIL; + } + + /* Editing a schedule with relative time should also reset it. */ + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_RELATIVE) { + schedule->trigger.next_scheduled_time_utc = 0; + } + esp_schedule_set(schedule, schedule_config); + ESP_LOGD(TAG, "Schedule %s edited", schedule->name); + return ESP_OK; +} + +esp_err_t esp_schedule_delete(esp_schedule_handle_t handle) +{ + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + ESP_LOGI(TAG, "Deleting schedule %s", schedule->name); + if (schedule->timer) { + esp_schedule_stop_timer(schedule); + esp_schedule_delete_timer(schedule); + } + esp_schedule_nvs_remove(schedule); + free(schedule); + return ESP_OK; +} + +esp_schedule_handle_t esp_schedule_create(esp_schedule_config_t *schedule_config) +{ + if (schedule_config == NULL) { + return NULL; + } + if (strlen(schedule_config->name) <= 0) { + ESP_LOGE(TAG, "Set schedule failed. Please enter a unique valid name for the schedule."); + return NULL; + } + + if (schedule_config->trigger.type == ESP_SCHEDULE_TYPE_INVALID) { + ESP_LOGE(TAG, "Schedule type is invalid."); + return NULL; + } + + esp_schedule_t *schedule = (esp_schedule_t *)MEM_CALLOC_EXTRAM(1, sizeof(esp_schedule_t)); + if (schedule == NULL) { + ESP_LOGE(TAG, "Could not allocate handle"); + return NULL; + } + strlcpy(schedule->name, schedule_config->name, sizeof(schedule->name)); + + esp_schedule_set(schedule, schedule_config); + + esp_schedule_create_timer(schedule); + ESP_LOGD(TAG, "Schedule %s created", schedule->name); + return (esp_schedule_handle_t)schedule; +} + +esp_schedule_handle_t *esp_schedule_init(bool enable_nvs, char *nvs_partition, uint8_t *schedule_count) +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) + if (!esp_sntp_enabled()) { + ESP_LOGI(TAG, "Initializing SNTP"); + esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); + esp_sntp_setservername(0, "pool.ntp.org"); + esp_sntp_init(); + } +#else + if (!sntp_enabled()) { + ESP_LOGI(TAG, "Initializing SNTP"); + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_setservername(0, "pool.ntp.org"); + sntp_init(); + } +#endif + + if (!enable_nvs) { + return NULL; + } + + /* Wait for time to be updated here */ + + + /* Below this is initialising schedules from NVS */ + esp_schedule_nvs_init(nvs_partition); + + /* Get handle list from NVS */ + esp_schedule_handle_t *handle_list = NULL; + *schedule_count = 0; + handle_list = esp_schedule_nvs_get_all(schedule_count); + if (handle_list == NULL) { + ESP_LOGI(TAG, "No schedules found in NVS"); + return NULL; + } + ESP_LOGI(TAG, "Schedules found in NVS: %"PRIu8, *schedule_count); + /* Start/Delete the schedules */ + esp_schedule_t *schedule = NULL; + for (size_t handle_count = 0; handle_count < *schedule_count; handle_count++) { + schedule = (esp_schedule_t *)handle_list[handle_count]; + schedule->trigger_cb = NULL; + schedule->timer = NULL; + /* Check for ONCE and expired schedules and delete them. */ + if (esp_schedule_is_expired(&schedule->trigger)) { + /* This schedule has already expired. */ + ESP_LOGI(TAG, "Schedule %s does not repeat and has already expired. Deleting it.", schedule->name); + esp_schedule_delete((esp_schedule_handle_t)schedule); + /* Removing the schedule from the list */ + handle_list[handle_count] = handle_list[*schedule_count - 1]; + (*schedule_count)--; + handle_count--; + continue; + } + esp_schedule_create_timer(schedule); + esp_schedule_start_timer(schedule); + } + init_done = true; + return handle_list; +} diff --git a/components/esp_schedule/src/esp_schedule_internal.h b/components/esp_schedule/src/esp_schedule_internal.h new file mode 100644 index 0000000..64c3f8a --- /dev/null +++ b/components/esp_schedule/src/esp_schedule_internal.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +typedef struct esp_schedule { + char name[MAX_SCHEDULE_NAME_LEN + 1]; + esp_schedule_trigger_t trigger; + uint32_t next_scheduled_time_diff; + TimerHandle_t timer; + esp_schedule_trigger_cb_t trigger_cb; + esp_schedule_timestamp_cb_t timestamp_cb; + void *priv_data; + esp_schedule_validity_t validity; +} esp_schedule_t; + +esp_err_t esp_schedule_nvs_add(esp_schedule_t *schedule); +esp_err_t esp_schedule_nvs_remove(esp_schedule_t *schedule); +esp_schedule_handle_t *esp_schedule_nvs_get_all(uint8_t *schedule_count); +bool esp_schedule_nvs_is_enabled(void); +esp_err_t esp_schedule_nvs_init(char *nvs_partition); diff --git a/components/esp_schedule/src/esp_schedule_nvs.c b/components/esp_schedule/src/esp_schedule_nvs.c new file mode 100644 index 0000000..acff405 --- /dev/null +++ b/components/esp_schedule/src/esp_schedule_nvs.c @@ -0,0 +1,296 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include "esp_schedule_internal.h" + +static const char *TAG = "esp_schedule_nvs"; + +#define ESP_SCHEDULE_NVS_NAMESPACE "schd" +#define ESP_SCHEDULE_COUNT_KEY "schd_count" + +static char *esp_schedule_nvs_partition = NULL; +static bool nvs_enabled = false; + +esp_err_t esp_schedule_nvs_add(esp_schedule_t *schedule) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not adding to NVS."); + return ESP_ERR_INVALID_STATE; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return err; + } + + /* Check if this is new schedule or editing an existing schedule */ + size_t buf_size; + bool editing_schedule = true; + err = nvs_get_blob(nvs_handle, schedule->name, NULL, &buf_size); + if (err != ESP_OK) { + if (err == ESP_ERR_NVS_NOT_FOUND) { + editing_schedule = false; + } else { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + } else { + ESP_LOGI(TAG, "Updating the existing schedule %s", schedule->name); + } + + err = nvs_set_blob(nvs_handle, schedule->name, schedule, sizeof(esp_schedule_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + if (editing_schedule == false) { + uint8_t schedule_count; + err = nvs_get_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, &schedule_count); + if (err != ESP_OK) { + if (err == ESP_ERR_NVS_NOT_FOUND) { + schedule_count = 0; + } else { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + } + schedule_count++; + err = nvs_set_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return err; + } + } + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedule %s added in NVS", schedule->name); + return ESP_OK; +} + +esp_err_t esp_schedule_nvs_remove_all(void) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not removing from NVS."); + return ESP_ERR_INVALID_STATE; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return err; + } + err = nvs_erase_all(nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS erase all keys failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + ESP_LOGI(TAG, "All schedules removed from NVS"); + return ESP_OK; +} + +esp_err_t esp_schedule_nvs_remove(esp_schedule_t *schedule) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not removing from NVS."); + return ESP_ERR_INVALID_STATE; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return err; + } + err = nvs_erase_key(nvs_handle, schedule->name); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS erase key failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + uint8_t schedule_count; + err = nvs_get_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, &schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return err; + } + schedule_count--; + err = nvs_set_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return err; + } + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedule %s removed from NVS", schedule->name); + return ESP_OK; +} + +static uint8_t esp_schedule_nvs_get_count(void) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not getting count from NVS."); + return 0; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READONLY, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return 0; + } + uint8_t schedule_count; + err = nvs_get_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, &schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return 0; + } + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedules in NVS: %d", schedule_count); + return schedule_count; +} + +static esp_schedule_handle_t esp_schedule_nvs_get(char *nvs_key) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not getting from NVS."); + return NULL; + } + size_t buf_size; + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READONLY, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return NULL; + } + err = nvs_get_blob(nvs_handle, nvs_key, NULL, &buf_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + return NULL; + } + esp_schedule_t *schedule = (esp_schedule_t *)malloc(buf_size); + if (schedule == NULL) { + ESP_LOGE(TAG, "Could not allocate handle"); + nvs_close(nvs_handle); + return NULL; + } + err = nvs_get_blob(nvs_handle, nvs_key, schedule, &buf_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + free(schedule); + return NULL; + } + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedule %s found in NVS", schedule->name); + return (esp_schedule_handle_t) schedule; +} + +esp_schedule_handle_t *esp_schedule_nvs_get_all(uint8_t *schedule_count) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not Initialising NVS."); + return NULL; + } + + *schedule_count = esp_schedule_nvs_get_count(); + if (*schedule_count == 0) { + ESP_LOGI(TAG, "No Entries found in NVS"); + return NULL; + } + esp_schedule_handle_t *handle_list = (esp_schedule_handle_t *)malloc(sizeof(esp_schedule_handle_t) * (*schedule_count)); + if (handle_list == NULL) { + ESP_LOGE(TAG, "Could not allocate schedule list"); + *schedule_count = 0; + return NULL; + } + int handle_count = 0; + + nvs_entry_info_t nvs_entry; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + nvs_iterator_t nvs_iterator = NULL; + esp_err_t err = nvs_entry_find(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_TYPE_BLOB, &nvs_iterator); + if (err != ESP_OK) { + ESP_LOGE(TAG, "No entry found in NVS"); + return NULL;; + } + while (err == ESP_OK) { + nvs_entry_info(nvs_iterator, &nvs_entry); + ESP_LOGI(TAG, "Found schedule in NVS with key: %s", nvs_entry.key); + handle_list[handle_count] = esp_schedule_nvs_get(nvs_entry.key); + if (handle_list[handle_count] != NULL) { + /* Increase count only if nvs_get was successful */ + handle_count++; + } + err = nvs_entry_next(&nvs_iterator); + } + nvs_release_iterator(nvs_iterator); +#else + nvs_iterator_t nvs_iterator = nvs_entry_find(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_TYPE_BLOB); + if (nvs_iterator == NULL) { + ESP_LOGE(TAG, "No entry found in NVS"); + return NULL;; + } + while (nvs_iterator != NULL) { + nvs_entry_info(nvs_iterator, &nvs_entry); + ESP_LOGI(TAG, "Found schedule in NVS with key: %s", nvs_entry.key); + handle_list[handle_count] = esp_schedule_nvs_get(nvs_entry.key); + if (handle_list[handle_count] != NULL) { + /* Increase count only if nvs_get was successful */ + handle_count++; + } + nvs_iterator = nvs_entry_next(nvs_iterator); + } +#endif + *schedule_count = handle_count; + ESP_LOGI(TAG, "Found %d schedules in NVS", *schedule_count); + return handle_list; +} + +bool esp_schedule_nvs_is_enabled(void) +{ + return nvs_enabled; +} + +esp_err_t esp_schedule_nvs_init(char *nvs_partition) +{ + if (nvs_enabled) { + ESP_LOGI(TAG, "NVS already enabled"); + return ESP_OK; + } + if (nvs_partition) { + esp_schedule_nvs_partition = strndup(nvs_partition, strlen(nvs_partition)); + } else { + esp_schedule_nvs_partition = strndup("nvs", strlen("nvs")); + } + if (esp_schedule_nvs_partition == NULL) { + ESP_LOGE(TAG, "Could not allocate nvs_partition"); + return ESP_ERR_NO_MEM; + } + nvs_enabled = true; + return ESP_OK; +} diff --git a/components/rmaker_common/.github/workflows/upload_components.yml b/components/rmaker_common/.github/workflows/upload_components.yml new file mode 100644 index 0000000..6674000 --- /dev/null +++ b/components/rmaker_common/.github/workflows/upload_components.yml @@ -0,0 +1,21 @@ +name: Push components to Espressif Component Registry + +on: + push: + branches: + - master + +jobs: + upload_components: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - name: Upload components to component registry + uses: espressif/upload-components-ci-action@v1 + with: + namespace: "espressif" + name: "rmaker_common" + api_token: ${{ secrets.IDF_COMPONENT_API_TOKEN }} diff --git a/components/rmaker_common/CMakeLists.txt b/components/rmaker_common/CMakeLists.txt new file mode 100644 index 0000000..7212fb0 --- /dev/null +++ b/components/rmaker_common/CMakeLists.txt @@ -0,0 +1,23 @@ +set(srcs "src/work_queue.c" "src/factory.c" "src/time.c" "src/timezone.c" "src/utils.c" + "src/cmd_resp.c" "src/console/rmaker_common_cmds.c" "src/console/rmaker_console.c") + +set(priv_req mqtt nvs_flash console nvs_flash esp_wifi driver) +set(requires esp_event) + +# esp_timer component was introduced in v4.2 +if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER "4.1") + list(APPEND priv_req esp_timer) +endif() + +#if(CONFIG_ESP_RMAKER_LIB_ESP_MQTT) + list(APPEND srcs "src/esp-mqtt/esp-mqtt-glue.c") +#endif() +if(CONFIG_ESP_RMAKER_MQTT_SEND_USERNAME) + list(APPEND srcs "src/create_APN3_PPI_string.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS + REQUIRES ${requires} + PRIV_REQUIRES ${priv_req}) diff --git a/components/rmaker_common/Kconfig b/components/rmaker_common/Kconfig new file mode 100644 index 0000000..9ab478a --- /dev/null +++ b/components/rmaker_common/Kconfig @@ -0,0 +1,181 @@ +menu "ESP RainMaker Common" + + choice ESP_RMAKER_MQTT_GLUE_LIB + bool "MQTT Library" + default ESP_RMAKER_LIB_ESP_MQTT + help + MQTT Library to be used + + config ESP_RMAKER_LIB_ESP_MQTT + bool "ESP-MQTT" + config ESP_RMAKER_LIB_AWS_IOT + bool "AWS-IOT" + endchoice + + config ESP_RMAKER_MQTT_GLUE_LIB + int + default 1 if ESP_RMAKER_LIB_ESP_MQTT + default 2 if ESP_RMAKER_LIB_AWS_IOT + + choice ESP_RMAKER_MQTT_PORT + bool "MQTT Port" + default ESP_RMAKER_MQTT_PORT_443 + help + ESP RainMaker MQTT Broker can be connected to either on Port 8883 or port 443. + Port 443 is recommended as it is generally not blocked by any firewalls, + since it is standard HTTPS port. + + config ESP_RMAKER_MQTT_PORT_443 + bool "443" + config ESP_RMAKER_MQTT_PORT_8883 + bool "8883" + endchoice + + config ESP_RMAKER_MQTT_PORT + int + default 1 if ESP_RMAKER_MQTT_PORT_443 + default 2 if ESP_RMAKER_MQTT_PORT_8883 + + config ESP_RMAKER_MQTT_PERSISTENT_SESSION + bool "Use Persisent MQTT sessions" + default n + help + Use persistent MQTT sessions. This improves reliability as QOS1 messages missed + out due to some network issue are received after the MQTT reconnects. The broker + caches messages for a period of upto 1 hour. However, a side-effect of this is that + messages can be received at unexpected time. Enable this option only if it suits + your use case. Please read MQTT specs to understand more about persistent sessions + and the cleanSession flag. + + config ESP_RMAKER_MQTT_SEND_USERNAME + bool "Send MQTT Username" + default y + help + Send a Username during MQTT Connect. This is generally required only for tracking + the MQTT client types, platform, SDK, etc. in AWS. + + config ESP_RMAKER_MQTT_PRODUCT_NAME + string "Product Name" + depends on ESP_RMAKER_MQTT_SEND_USERNAME + default "RMDev" + help + Approved AWS product name. Please get in touch with your Espressif/AWS representative for more info. + + config ESP_RMAKER_MQTT_PRODUCT_VERSION + string "Product Version" + depends on ESP_RMAKER_MQTT_SEND_USERNAME + default "1x0" + help + Approved AWS product version. Please get in touch with your Espressif/AWS representative for more info. + + config ESP_RMAKER_MQTT_PRODUCT_SKU + string "Product SKU" + depends on ESP_RMAKER_MQTT_SEND_USERNAME + default "EX00" + help + Product SKU. Please get in touch with your Espressif/AWS representative for more info. + + config ESP_RMAKER_MQTT_USE_CERT_BUNDLE + bool "Use Certificate Bundle" + default y + help + Use Certificate Bundle for server authentication. Enabling this is recommended to safeguard + against any changes in the server certificates in future. This has an impact on the binary + size as well as heap requirement. + + config ESP_RMAKER_MAX_MQTT_SUBSCRIPTIONS + int "Maximum number of MQTT Subscriptions" + default 10 + help + This value controls the maximum number of topics that the device can subscribe to. + + config ESP_RMAKER_MQTT_KEEP_ALIVE_INTERVAL + int "MQTT Keep Alive Internal" + default 120 + range 30 1200 + help + MQTT Keep Alive Interval. Note that it can take upto 1.5x of keep alive interval for a device + to be reported by offline by the MQTT Broker. Change this only if required. + + choice ESP_RMAKER_NETWORK_PROTOCOL_TYPE + prompt "ESP RainMaker Network Type" + default ESP_RMAKER_NETWORK_OVER_WIFI + help + Network protocol type over which the ESP RainMaker will run. + + config ESP_RMAKER_NETWORK_OVER_WIFI + # ESP_WIFI_ENABLED was introduced in IDF v5.0 + depends on IDF_TARGET_ESP32 || IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 || ESP_WIFI_ENABLED + bool "ESP RainMaker over Wi-Fi" + + config ESP_RMAKER_NETWORK_OVER_THREAD + depends on OPENTHREAD_ENABLED + bool "ESP RainMaker over Thread" + endchoice + + config ESP_RMAKER_WORK_QUEUE_TASK_STACK + int "ESP RainMaker Work Queue Task stack" + default 4096 + help + Stack size for the ESP RainMaker Work Queue Task. + + config ESP_RMAKER_WORK_QUEUE_TASK_PRIORITY + int "ESP RainMaker Work Queue Task priority" + default 5 + help + Priority for the ESP RainMaker Work Queue Task. Not recommended to be changed + unless you really need it. + + config ESP_RMAKER_FACTORY_PARTITION_NAME + string "ESP RainMaker Factory Partition Name" + default "fctry" + help + Factory NVS Partition name which will have the MQTT connectivity credentials. + + config ESP_RMAKER_FACTORY_NAMESPACE + string "ESP RainMaker Factory Namespace" + default "rmaker_creds" + help + Namespace in the Factory NVS Partition name which will have the MQTT + connectivity credentials. + + config ESP_RMAKER_ENCRYPT_FACTORY_PARTITION + bool "Encrypt Rainmaker Factory partition" + default false + depends on NVS_ENCRYPTION + help + Enable this option if the factory partition is pre-encrypted before flashing and the encryption keys + are flashed in partition with subtype nvs_keys specified by CONFIG_ESP_RMAKER_FACTORY_NVS_KEYS_PARTITION_NAME. + If an unencrypted factory partition is flashed, the device would not be able to read its data and + the partition would be considered empty. + If nvs encryption keys are not flashed onto device, they would be auto-generated and any previous data + in nvs/factory partition would become invalid. + + config ESP_RMAKER_FACTORY_NVS_KEYS_PARTITION_NAME + string "ESP Rainmaker Factory NVS keys partition label" + default "nvs_key" + depends on ESP_RMAKER_ENCRYPT_FACTORY_PARTITION + help + Label of the partition of subtype "nvs_keys" used for encrypting/decrypting Rainmaker factory partition. + + config ESP_RMAKER_DEF_TIMEZONE + string "Default Timezone" + default "Asia/Shanghai" + help + Default Timezone to use. Eg. "Asia/Shanghai", "America/Los_Angeles". + Check documentation for complete list of valid values. This value + will be used only if no timezone is set using the C APIs. + + config ESP_RMAKER_SNTP_SERVER_NAME + string "ESP RainMaker SNTP Server Name" + default "pool.ntp.org" + help + Default SNTP Server which is used for time synchronization. + + config ESP_RMAKER_MAX_COMMANDS + int "Maximum commands supported for command-response" + default 10 + help + Maximum number of commands supported by the command-response framework + +endmenu diff --git a/components/rmaker_common/LICENSE b/components/rmaker_common/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/components/rmaker_common/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/components/rmaker_common/README.md b/components/rmaker_common/README.md new file mode 100644 index 0000000..0db26c8 --- /dev/null +++ b/components/rmaker_common/README.md @@ -0,0 +1,9 @@ +# ESP RainMaker Common modules + +This component consists of some common modules used by ESP RainMaker and ESP RainMaker Diagnostics repos. +Currently, it consists of +- MQTT glue layer +- Timing APIs (SNTP helpers, timezone, etc.) +- Factory NVS helpers +- Work Queue + diff --git a/components/rmaker_common/component.mk b/components/rmaker_common/component.mk new file mode 100644 index 0000000..978b48b --- /dev/null +++ b/components/rmaker_common/component.mk @@ -0,0 +1,5 @@ +COMPONENT_ADD_INCLUDEDIRS := include +COMPONENT_SRCDIRS := src +ifdef CONFIG_ESP_RMAKER_LIB_ESP_MQTT +COMPONENT_SRCDIRS += src/esp-mqtt +endif diff --git a/components/rmaker_common/idf_component.yml b/components/rmaker_common/idf_component.yml new file mode 100644 index 0000000..366e98c --- /dev/null +++ b/components/rmaker_common/idf_component.yml @@ -0,0 +1,3 @@ +version: "1.4.6" +description: ESP RainMaker firmware agent Common component +url: https://github.com/espressif/esp-rainmaker-common diff --git a/components/rmaker_common/include/esp_rmaker_cmd_resp.h b/components/rmaker_common/include/esp_rmaker_cmd_resp.h new file mode 100644 index 0000000..74e937d --- /dev/null +++ b/components/rmaker_common/include/esp_rmaker_cmd_resp.h @@ -0,0 +1,184 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Super Admin User Flag*/ +#define ESP_RMAKER_USER_ROLE_SUPER_ADMIN (1 << 0) + +/** Primary User Flag */ +#define ESP_RMAKER_USER_ROLE_PRIMARY_USER (1 << 1) + +/** Secondary User Flag */ +#define ESP_RMAKER_USER_ROLE_SECONDARY_USER (1 << 2) + + +/** RainMaker Command Response TLV8 Types */ +typedef enum { + /** Request Id : Variable length string, max 32 characters*/ + ESP_RMAKER_TLV_TYPE_REQ_ID = 1, + /** User Role : 1 byte */ + ESP_RMAKER_TLV_TYPE_USER_ROLE, + /** Status : 1 byte */ + ESP_RMAKER_TLV_TYPE_STATUS, + /** Timestamp : TBD */ + ESP_RMAKER_TLV_TYPE_TIMESTAMP, + /** Command : 2 bytes*/ + ESP_RMAKER_TLV_TYPE_CMD, + /** Data : Variable length */ + ESP_RMAKER_TLV_TYPE_DATA +} esp_rmaker_tlv_type_t; + +/* RainMaker Command Response Status */ +typedef enum { + /** Success */ + ESP_RMAKER_CMD_STATUS_SUCCESS = 0, + /** Generic Failure */ + ESP_RMAKER_CMD_STATUS_FAILED, + /** Invalid Command */ + ESP_RMAKER_CMD_STATUS_CMD_INVALID, + /** Authentication Failed */ + ESP_RMAKER_CMD_STATUS_AUTH_FAIL, + /** Command not found */ + ESP_RMAKER_CMD_STATUS_NOT_FOUND, + /** Last status value */ + ESP_RMAKER_CMD_STATUS_MAX, +} esp_rmaker_cmd_status_t; + +#define REQ_ID_LEN 32 +typedef struct { + /** Command id */ + uint16_t cmd; + /** Request id */ + char req_id[REQ_ID_LEN]; + /** User Role */ + uint8_t user_role; +} esp_rmaker_cmd_ctx_t; + +typedef enum { + /** Standard command: Set Parameters */ + ESP_RMAKER_CMD_TYPE_SET_PARAMS = 1, + /** Last Standard command */ + ESP_RMAKER_CMD_STANDARD_LAST = 0xfff, + /** Custom commands can start from here */ + ESP_RMAKER_CMD_CUSTOM_START = 0x1000 +} esp_rmaker_cmd_t; + +/** Command Response Handler + * + * If any command data is received from any of the supported transports (which are outside the scope of this core framework), + * this function should be called to handle it and fill in the response. + * + * @param[in] input Pointer to input data. + * @param[in] input_len data len. + * @param[in] output Pointer to output data which should be set by the handler. + * @param[out] output_len Length of output generated. + * + * @return ESP_OK on success. + * @return error on failure. + */ +esp_err_t esp_rmaker_cmd_response_handler(const void *input, size_t input_len, void **output, size_t *output_len); + +/** Prototype for Command Handler + * + * The handler to be invoked when a given command is received. + * + * @param[in] in_data Pointer to input data. + * @param[in] in_len data len. + * @param[in] out_data Pointer to output data which should be set by the handler. + * @param[out] out_len Length of output generated. + * @param[in] ctx Command Context. + * @param[in] priv Private data, if specified while registering command. + * + * @return ESP_OK on success. + * @return error on failure. + */ +typedef esp_err_t (*esp_rmaker_cmd_handler_t)(const void *in_data, size_t in_len, void **out_data, size_t *out_len, esp_rmaker_cmd_ctx_t *ctx, void *priv); + +/** Register a new command + * + * @param[in] cmd Command Identifier. Custom commands should start beyond ESP_RMAKER_CMD_STANDARD_LAST + * @param[in] access User Access for the command. Can be an OR of the various user role flags like ESP_RMAKER_USER_ROLE_SUPER_ADMIN, + * ESP_RMAKER_USER_ROLE_PRIMARY_USER and ESP_RMAKER_USER_ROLE_SECONDARY_USER + * @param[in] handler The handler to be invoked when the given command is received. + * @param[in] free_on_return Flag to indicate of the framework should free the output after it has been sent as response. + * @param[in] priv Optional private data to be passed to the handler. + * + * @return ESP_OK on success. + * @return error on failure. + */ +esp_err_t esp_rmaker_cmd_register(uint16_t cmd, uint8_t access, esp_rmaker_cmd_handler_t handler, bool free_on_return, void *priv); + +/** De-register a command + * + * @param[in] cmd Command Identifier. Custom commands should start beyond ESP_RMAKER_CMD_STANDARD_LAST + * + * @return ESP_OK on success. + * @return error on failure. + */ +esp_err_t esp_rmaker_cmd_deregister(uint16_t cmd); + +/* Prepare an empty command response + * + * This can be used to populate the request to be sent to get all pending commands + * + * @param[in] out_data Pointer to output data. This function will allocate memory and set this pointer + * accordingly. + * @param[out] out_len Length of output generated. + * + * @return ESP_OK on success. + * @return error on failure. + */ + esp_err_t esp_rmaker_cmd_prepare_empty_response(void **output, size_t *output_len); + +/** Prototype for Command sending function (TESTING only) + * + * @param[in] data Pointer to the data to be sent. + * @param[in] data_len Size of data to be sent. + * @param[in] priv Private data, if applicable. + * + * @return ESP_OK on success. + * @return error on failure. + */ +typedef esp_err_t (*esp_rmaker_cmd_send_t)(const void *data, size_t data_len, void *priv); + +/** Send Test command (TESTING only) + * + * @param[in] req_id NULL terminated request id of max 32 characters. + * @param[in] role User Role flag. + * @param[in] cmd Command Identifier. + * @param[in] data Pointer to data for the command. + * @param[in] data_size Size of the data. + * @param[in] cmd_send Transport specific function to send the command data. + * @param[in] priv Private data (if any) to be sent to cmd_send. + * + * @return ESP_OK on success. + * @return error on failure. + */ +esp_err_t esp_rmaker_cmd_resp_test_send(const char *req_id, uint8_t role, uint16_t cmd, const void *data, size_t data_size, esp_rmaker_cmd_send_t cmd_send, void *priv); + +/** Parse response (TESTING only) + * + * @param[in] response Pointer to the response received + * @param[in] response_len Length of the response + * @param[in] priv Private data, if any. Can be NULL. + * + * @return ESP_OK on success. + * @return error on failure. + */ +esp_err_t esp_rmaker_cmd_resp_parse_response(const void *response, size_t response_len, void *priv); +#ifdef __cplusplus +} +#endif diff --git a/components/rmaker_common/include/esp_rmaker_common_console.h b/components/rmaker_common/include/esp_rmaker_common_console.h new file mode 100644 index 0000000..55825a8 --- /dev/null +++ b/components/rmaker_common/include/esp_rmaker_common_console.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** + * @brief Initialize console + * + * Initializes serial console and adds basic commands. + * + * @return ESP_OK on success. + * @return error in case of failures. + */ +esp_err_t esp_rmaker_common_console_init(void); + +/* Reference for adding custom console commands: +#include + +static int command_console_handler(int argc, char *argv[]) +{ + // Command code here +} + +static void register_console_command() +{ + const esp_console_cmd_t cmd = { + .command = "", + .help = "", + .func = &command_console_handler, + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&cmd)); +} +*/ + +#ifdef __cplusplus +} +#endif diff --git a/components/rmaker_common/include/esp_rmaker_common_events.h b/components/rmaker_common/include/esp_rmaker_common_events.h new file mode 100644 index 0000000..b77f8a3 --- /dev/null +++ b/components/rmaker_common/include/esp_rmaker_common_events.h @@ -0,0 +1,60 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#include +#ifdef __cplusplus +extern "C" +{ +#endif + +/** ESP RainMaker Common Event Base */ +ESP_EVENT_DECLARE_BASE(RMAKER_COMMON_EVENT); + +typedef enum { + /** Node reboot has been triggered. The associated event data is the time in seconds + * (type: uint8_t) after which the node will reboot. Note that this time may not be + * accurate as the events are received asynchronously.*/ + RMAKER_EVENT_REBOOT, + /** Wi-Fi credentials reset. Triggered after calling esp_rmaker_wifi_reset() */ + RMAKER_EVENT_WIFI_RESET, + /** Node reset to factory defaults. Triggered after calling esp_rmaker_factory_reset() */ + RMAKER_EVENT_FACTORY_RESET, + /** Connected to MQTT Broker */ + RMAKER_MQTT_EVENT_CONNECTED, + /** Disconnected from MQTT Broker */ + RMAKER_MQTT_EVENT_DISCONNECTED, + /** MQTT message published successfully. + * Event data will contain the message ID (integer) of published message. + */ + RMAKER_MQTT_EVENT_PUBLISHED, + /** POSIX Timezone Changed. Associated data would be NULL terminated POSIX Timezone + * Eg. "PST8PDT,M3.2.0,M11.1.0" */ + RMAKER_EVENT_TZ_POSIX_CHANGED, + /** Timezone Changed. Associated data would be NULL terminated Timezone. + * Eg. "America/Los_Angeles" + * Note that whenever this event is received, the RMAKER_EVENT_TZ_POSIX_CHANGED event + * will also be received, but not necessarily vice versa. + */ + RMAKER_EVENT_TZ_CHANGED, + /** + * MQTT message deleted from the outbox if the message couldn't have been sent and acknowledged. + * Event data will contain the message ID (integer) of deleted message. + * Valid only if CONFIG_MQTT_REPORT_DELETED_MESSAGES is enabled. + */ + RMAKER_MQTT_EVENT_MSG_DELETED, +} esp_rmaker_common_event_t; +#ifdef __cplusplus +} +#endif diff --git a/components/rmaker_common/include/esp_rmaker_factory.h b/components/rmaker_common/include/esp_rmaker_factory.h new file mode 100644 index 0000000..cea7489 --- /dev/null +++ b/components/rmaker_common/include/esp_rmaker_factory.h @@ -0,0 +1,73 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#include +#include +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Initialize Factory NVS + * + * This initializes the Factory NVS partition which will store data + * that should not be cleared even after a reset to factory. + * + * @return ESP_OK on success. + * @return error on failure. + */ +esp_err_t esp_rmaker_factory_init(void); + +/** Get value from factory NVS + * + * This will search for the specified key in the Factory NVS partition, + * allocate the required memory to hold it, copy the value and return + * the pointer to it. It is responsibility of the caller to free the + * memory when the value is no more required. + * + * @param[in] key The key of the value to be read from factory NVS. + * + * @return pointer to the value on success. + * @return NULL on failure. + */ +void *esp_rmaker_factory_get(const char *key); + +/** Get size of value from factory NVS + * + * This will search for the specified key in the Factory NVS partition, + * and return the size of the value associated with the key. + * + * @param[in] key The key of the value to be read from factory NVS. + * + * @return size of the value on success. + * @return 0 on failure. + */ +size_t esp_rmaker_factory_get_size(const char *key); + +/** Set a value in factory NVS + * + * This will write the value for the specified key into factory NVS. + * + * @param[in] key The key for the value to be set in factory NVS. + * @param[in] value Pointer to the value. + * @param[in] len Length of the value. + * + * @return ESP_OK on success. + * @return error on failure. + */ +esp_err_t esp_rmaker_factory_set(const char *key, void *value, size_t len); +#ifdef __cplusplus +} +#endif diff --git a/components/rmaker_common/include/esp_rmaker_mqtt_glue.h b/components/rmaker_common/include/esp_rmaker_mqtt_glue.h new file mode 100644 index 0000000..20f1a9a --- /dev/null +++ b/components/rmaker_common/include/esp_rmaker_mqtt_glue.h @@ -0,0 +1,177 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define RMAKER_MQTT_QOS0 0 +#define RMAKER_MQTT_QOS1 1 + +/** MQTT Connection parameters */ +typedef struct { + /** MQTT Host */ + char *mqtt_host; + /** Client ID */ + char *client_id; + /** Client Certificate in DER format or NULL-terminated PEM format */ + char *client_cert; + /** Client Certificate length */ + size_t client_cert_len; + /** Client Key in DER format or NULL-terminated PEM format */ + char *client_key; + /** Client Key length */ + size_t client_key_len; + /** Server Certificate in DER format or NULL-terminated PEM format */ + char *server_cert; + /** Server Certificate length */ + size_t server_cert_len; + /** Pointer for digital signature peripheral context */ + void *ds_data; +} esp_rmaker_mqtt_conn_params_t; + +/** MQTT Get Connection Parameters function prototype + * + * @return Pointer to \ref esp_rmaker_mqtt_conn_params_t on success. + * @return NULL on failure. + */ +typedef esp_rmaker_mqtt_conn_params_t *(*esp_rmaker_mqtt_get_conn_params_t)(void); + +/** MQTT Subscribe callback prototype + * + * @param[in] topic Topic on which the message was received + * @param[in] payload Data received in the message + * @param[in] payload_len Length of the data + * @param[in] priv_data The private data passed during subscription + */ +typedef void (*esp_rmaker_mqtt_subscribe_cb_t)(const char *topic, void *payload, size_t payload_len, void *priv_data); + +/** MQTT Init function prototype + * + * @param[in] conn_params The MQTT connection parameters. If NULL is passed, it should internally use the + * \ref esp_rmaker_mqtt_get_conn_params call if registered. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +typedef esp_err_t (*esp_rmaker_mqtt_init_t)(esp_rmaker_mqtt_conn_params_t *conn_params); + +/** MQTT Deinit function prototype + * + * Call this function after MQTT has disconnected. + */ +typedef void (*esp_rmaker_mqtt_deinit_t)(void); + +/** MQTT Connect function prototype + * + * Starts the connection attempts to the MQTT broker. + * This should ideally be called after successful network connection. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +typedef esp_err_t (*esp_rmaker_mqtt_connect_t)(void); + +/** MQTT Disconnect function prototype + * + * Disconnects from the MQTT broker. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +typedef esp_err_t (*esp_rmaker_mqtt_disconnect_t)(void); + +/** MQTT Publish Message function prototype + * + * @param[in] topic The MQTT topic on which the message should be published. + * @param[in] data Data to be published. + * @param[in] data_len Length of the data. + * @param[in] qos Quality of service for the message. + * @param[out] msg_id If a non NULL pointer is passed, the id of the published message will be returned in this. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +typedef esp_err_t (*esp_rmaker_mqtt_publish_t)(const char *topic, void *data, size_t data_len, uint8_t qos, int *msg_id); + +/** MQTT Subscribe function prototype + * + * @param[in] topic The topic to be subscribed to. + * @param[in] cb The callback to be invoked when a message is received on the given topic. + * @param[in] qos Quality of service for the subscription. + * @param[in] priv_data Optional private data to be passed to the callback. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +typedef esp_err_t (*esp_rmaker_mqtt_subscribe_t)(const char *topic, esp_rmaker_mqtt_subscribe_cb_t cb, uint8_t qos, void *priv_data); + +/** MQTT Unsubscribe function prototype + * + * @param[in] topic Topic from which to unsubscribe. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +typedef esp_err_t (*esp_rmaker_mqtt_unsubscribe_t)(const char *topic); + +/** MQTT configuration */ +typedef struct { + /** Flag to indicate if the MQTT config setup is done */ + bool setup_done; + /** Pointer to the Get MQTT params function. */ + esp_rmaker_mqtt_get_conn_params_t get_conn_params; + /** Pointer to MQTT Init function. */ + esp_rmaker_mqtt_init_t init; + /** Pointer to MQTT Deinit function. */ + esp_rmaker_mqtt_deinit_t deinit; + /** Pointer to MQTT Connect function. */ + esp_rmaker_mqtt_connect_t connect; + /** Pointer to MQTQ Disconnect function */ + esp_rmaker_mqtt_disconnect_t disconnect; + /** Pointer to MQTT Publish function */ + esp_rmaker_mqtt_publish_t publish; + /** Pointer to MQTT Subscribe function */ + esp_rmaker_mqtt_subscribe_t subscribe; + /** Pointer to MQTT Unsubscribe function */ + esp_rmaker_mqtt_unsubscribe_t unsubscribe; +} esp_rmaker_mqtt_config_t; + +/** Setup MQTT Glue + * + * This function initializes MQTT glue layer with all the default functions. + * + * @param[out] mqtt_config Pointer to an allocated MQTT configuration structure. + * + * @return ESP_OK on success. + * @return error in case of any error. + */ +esp_err_t esp_rmaker_mqtt_glue_setup(esp_rmaker_mqtt_config_t *mqtt_config); + +/* Get the ESP AWS PPI String + * + * @return pointer to a NULL terminated PPI string on success. + * @return NULL in case of any error. + */ +const char *esp_get_aws_ppi(void); +#ifdef __cplusplus +} +#endif diff --git a/components/rmaker_common/include/esp_rmaker_utils.h b/components/rmaker_common/include/esp_rmaker_utils.h new file mode 100644 index 0000000..21f7994 --- /dev/null +++ b/components/rmaker_common/include/esp_rmaker_utils.h @@ -0,0 +1,211 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) +#include +#else +#include +#endif + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if (CONFIG_SPIRAM_SUPPORT && (CONFIG_SPIRAM_USE_CAPS_ALLOC || CONFIG_SPIRAM_USE_MALLOC)) +#define MEM_ALLOC_EXTRAM(size) heap_caps_malloc_prefer(size, 2, MALLOC_CAP_DEFAULT | MALLOC_CAP_SPIRAM, MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL) +#define MEM_CALLOC_EXTRAM(num, size) heap_caps_calloc_prefer(num, size, 2, MALLOC_CAP_DEFAULT | MALLOC_CAP_SPIRAM, MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL) +#define MEM_REALLOC_EXTRAM(ptr, size) heap_caps_realloc_prefer(ptr, size, 2, MALLOC_CAP_DEFAULT | MALLOC_CAP_SPIRAM, MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL) +#else +#define MEM_ALLOC_EXTRAM(size) malloc(size) +#define MEM_CALLOC_EXTRAM(num, size) calloc(num, size) +#define MEM_REALLOC_EXTRAM(ptr, size) realloc(ptr, size) +#endif + +typedef struct esp_rmaker_time_config { + /** If not specified, then 'CONFIG_ESP_RMAKER_SNTP_SERVER_NAME' is used as the SNTP server. */ + char *sntp_server_name; + /** Optional callback to invoke, whenever time is synchronised. This will be called + * periodically as per the SNTP polling interval (which is 60min by default). + * If kept NULL, the default callback will be invoked, which will just print the + * current local time. + */ + sntp_sync_time_cb_t sync_time_cb; +} esp_rmaker_time_config_t; + +/** Reboot the device after a delay + * + * This API just starts a reboot timer and returns immediately. + * The actual reboot is trigerred asynchronously in the timer callback. + * This is useful if you want to reboot after a delay, to allow other tasks to finish + * their operations (Eg. MQTT publish to indicate OTA success). The \ref RMAKER_EVENT_REBOOT + * event is triggered when the reboot timer is started. + * + * @param[in] seconds Time in seconds after which the device should reboot. + * + * @return ESP_OK on success. + * @return error on failure. + */ +esp_err_t esp_rmaker_reboot(int8_t seconds); + +/** Reset Wi-Fi credentials and (optionally) reboot + * + * This will reset just the Wi-Fi credentials and (optionally) trigger a reboot. + * This is useful when you want to keep all the entries in NVS memory + * intact, but just change the Wi-Fi credentials. The \ref RMAKER_EVENT_WIFI_RESET + * event is triggered when this API is called. The actual reset will happen after a + * delay if reset_seconds is not zero. + * + * @note This reset and reboot operations will happen asynchronously depending + * on the values passed to the API. + * + * @param[in] reset_seconds Time in seconds after which the reset should get triggered. + * This will help other modules take some actions before the device actually resets. + * If set to zero, the operation would be performed immediately. + * @param[in] reboot_seconds Time in seconds after which the device should reboot. If set + * to negative value, the device will not reboot at all. + * + * @return ESP_OK on success. + * @return error on failure. + */ +esp_err_t esp_rmaker_wifi_reset(int8_t reset_seconds, int8_t reboot_seconds); + +/** Reset to factory defaults and reboot + * + * This will clear entire NVS partition and (optionally) trigger a reboot. + * The \ref RMAKER_EVENT_FACTORY_RESET event is triggered when this API is called. + * The actual reset will happen after a delay if reset_seconds is not zero. + * + * @note This reset and reboot operations will happen asynchronously depending + * on the values passed to the API. + * + * @param[in] reset_seconds Time in seconds after which the reset should get triggered. + * This will help other modules take some actions before the device actually resets. + * If set to zero, the operation would be performed immediately. + * @param[in] reboot_seconds Time in seconds after which the device should reboot. If set + * to negative value, the device will not reboot at all. + * + * @return ESP_OK on success. + * @return error on failure. + */ +esp_err_t esp_rmaker_factory_reset(int8_t reset_seconds, int8_t reboot_seconds); + +/** Initialize time synchronization + * + * This API initializes SNTP for time synchronization. + * + * @param[in] config Configuration to be used for SNTP time synchronization. The default configuration is used if NULL is passed. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_time_sync_init(esp_rmaker_time_config_t *config); + +/** Check if current time is updated + * + * This API checks if the current system time is updated against the reference time of 1-Jan-2019. + * + * @return true if time is updated + * @return false if time is not updated + */ +bool esp_rmaker_time_check(void); + +/** Wait for time synchronization + * + * This API waits for the system time to be updated against the reference time of 1-Jan-2019. + * This is a blocking call. + * + * @param[in] ticks_to_wait Number of ticks to wait for time synchronization. Accepted values: 0 to portMAX_DELAY. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_time_wait_for_sync(uint32_t ticks_to_wait); + +/** Set POSIX timezone + * + * Set the timezone (TZ environment variable) as per the POSIX format + * specified in the [GNU libc documentation](https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html). + * Eg. For China: "CST-8" + * For US Pacific Time (including daylight saving information): "PST8PDT,M3.2.0,M11.1.0" + * + * @param[in] tz_posix NULL terminated TZ POSIX string + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_time_set_timezone_posix(const char *tz_posix); + +/** Set timezone location string + * + * Set the timezone as a user friendly location string. + * Check [here](https://rainmaker.espressif.com/docs/time-service.html) for a list of valid values. + * + * Eg. For China: "Asia/Shanghai" + * For US Pacific Time: "America/Los_Angeles" + * + * @note Setting timezone using this API internally also sets the POSIX timezone string. + * + * @param[in] tz NULL terminated Timezone location string + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_time_set_timezone(const char *tz); + +/** Get the current POSIX timezone + * + * This fetches the current timezone in POSIX format, read from NVS. + * + * @return Pointer to a NULL terminated POSIX timezone string on success. + * Freeing this is the responsibility of the caller. + * @return NULL on failure. + */ +char *esp_rmaker_time_get_timezone_posix(void); + +/** Get the current timezone + * + * This fetches the current timezone in POSIX format, read from NVS. + * + * @return Pointer to a NULL terminated timezone string on success. + * Freeing this is the responsibility of the caller. + * @return NULL on failure. + */ +char *esp_rmaker_time_get_timezone(void); + +/** Get printable local time string + * + * Get a printable local time string, with information of timezone and Daylight Saving. + * Eg. "Tue Sep 1 09:04:38 2020 -0400[EDT], DST: Yes" + * "Tue Sep 1 21:04:04 2020 +0800[CST], DST: No" + * + * + * @param[out] buf Pointer to a pre-allocated buffer into which the time string will + * be populated. + * @param[in] buf_len Length of the above buffer. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_get_local_time_str(char *buf, size_t buf_len); + +#ifdef __cplusplus +} +#endif diff --git a/components/rmaker_common/include/esp_rmaker_work_queue.h b/components/rmaker_common/include/esp_rmaker_work_queue.h new file mode 100644 index 0000000..0e696d4 --- /dev/null +++ b/components/rmaker_common/include/esp_rmaker_work_queue.h @@ -0,0 +1,82 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#include +#include +#ifdef __cplusplus +extern "C" +{ +#endif +/** Prototype for ESP RainMaker Work Queue Function + * + * @param[in] priv_data The private data associated with the work function. + */ +typedef void (*esp_rmaker_work_fn_t)(void *priv_data); + +/** Initializes the Work Queue + * + * This initializes the work queue, which is basically a mechanism to run + * tasks in the context of a dedicated thread. You can start queueing tasks + * after this, but they will get executed only after calling + * esp_rmaker_work_queue_start(). + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_work_queue_init(void); + +/** De-initialize the Work Queue + * + * This de-initializes the work queue. Note that the work queue needs to + * be stopped using esp_rmaker_work_queue_stop() before calling this. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_work_queue_deinit(void); + +/** Start the Work Queue + * + * This starts the Work Queue thread which then starts executing the tasks queued. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_work_queue_start(void); + +/** Stop the Work Queue + * + * This stops a running Work Queue. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_work_queue_stop(void); + +/** Queue execution of a function in the Work Queue's context + * + * This API queues a work function for execution in the Work Queue Task's context. + * + * @param[in] work_fn The Work function to be queued. + * @param[in] priv_data Private data to be passed to the work function. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_rmaker_work_queue_add_task(esp_rmaker_work_fn_t work_fn, void *priv_data); + +#ifdef __cplusplus +} +#endif diff --git a/components/rmaker_common/src/cmd_resp.c b/components/rmaker_common/src/cmd_resp.c new file mode 100644 index 0000000..997b3ad --- /dev/null +++ b/components/rmaker_common/src/cmd_resp.c @@ -0,0 +1,429 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#define RMAKER_MAX_CMD CONFIG_ESP_RMAKER_MAX_COMMANDS + +static const char *TAG = "esp_rmaker_common_cmd_resp"; + +typedef struct { + uint16_t cmd; + uint8_t access; + bool free_on_return; + esp_rmaker_cmd_handler_t handler; + void *priv; +} esp_rmaker_cmd_info_t; + +typedef struct { + uint8_t *bufptr; + int bufsize; + int curlen; +} esp_rmaker_tlv_data_t; + +static esp_rmaker_cmd_info_t *esp_rmaker_cmd_list[RMAKER_MAX_CMD]; + +/* Get uint16 from Little Endian data buffer */ +static uint16_t get_u16_le(const void *val_ptr) +{ + const uint8_t *p = (const uint8_t *) val_ptr; + uint16_t val; + + val = (uint16_t)p[0]; + val |= (uint16_t)p[1] << 8; + + return val; +} + +/* Put uint16 in Little Endian ordering into data buffer */ +static void put_u16_le(void *val_ptr, const uint16_t val) +{ + uint8_t *p = (uint8_t *) val_ptr; + + p[0] = (uint8_t)val & 0xff; + p[1] = (uint8_t)(val >> 8) & 0xff; +} + +/* Initialise TLV Data */ +static void esp_rmaker_tlv_data_init(esp_rmaker_tlv_data_t *tlv_data, uint8_t *buf, int buf_size) +{ + tlv_data->bufptr = buf; + tlv_data->bufsize = buf_size; + tlv_data->curlen = 0; +} + +/* Get length of data, for given type. + * + * Returns length of data on success and -1 if the TLV was not found + */ +static int esp_rmaker_get_tlv_length(const uint8_t *buf, int buflen, uint8_t type) +{ + if (!buf ) { + return -1; + } + int curlen = 0; + int val_len = 0; + bool found = false; + while (buflen > 0) { + if (buf[curlen] == type) { + uint8_t len = buf[curlen + 1]; + if ((buflen - len) < 2) { + return -1; + } + val_len += len; + if (len < 255) { + return val_len; + } else { + found = true; + } + + } else if (found) { + return val_len; + } + + /* buf[curlen +1] will give the Length */ + buflen -= (2 + buf[curlen + 1]); + curlen += (2 + buf[curlen + 1]); + } + if (found) { + return val_len; + } + return -1; +} + +/* Get the value for the given type. + * + * Returns length of data on success and -1 if the TLV was not found + */ +static int esp_rmaker_get_value_from_tlv(const uint8_t *buf, int buflen, uint8_t type, void *val, int val_size) +{ + if (!buf || !val) { + return -1; + } + int curlen = 0; + int val_len = 0; + bool found = false; + while (buflen > 0) { + if (buf[curlen] == type) { + uint8_t len = buf[curlen + 1]; + if ((val_size < len) || ((buflen - len) < 2)) { + return -1; + } + memcpy(val + val_len, &buf[curlen + 2], len); + val_len += len; + val_size -= len; + if (len < 255) { + return val_len; + } else { + found = true; + } + + } else if (found) { + return val_len; + } + + /* buf[curlen +1] will give the Length */ + buflen -= (2 + buf[curlen + 1]); + curlen += (2 + buf[curlen + 1]); + } + if (found) { + return val_len; + } + return -1; +} + +/* Add a TLV to the TLV buffer */ +static int esp_rmaker_add_tlv(esp_rmaker_tlv_data_t *tlv_data, uint8_t type, int len, const void *val) +{ + if (!tlv_data->bufptr || ((len + 2) > (tlv_data->bufsize - tlv_data->curlen))) { + return -1; + } + if (len > 0 && val == NULL) { + return -1; + } + uint8_t *buf_ptr = (uint8_t *)val; + int orig_len = tlv_data->curlen; + do { + tlv_data->bufptr[tlv_data->curlen++] = type; + int tmp_len; + if (len > 255) { + tmp_len = 255; + } else { + tmp_len = len; + } + tlv_data->bufptr[tlv_data->curlen++] = tmp_len; + memcpy(&tlv_data->bufptr[tlv_data->curlen], buf_ptr, tmp_len); + tlv_data->curlen += tmp_len; + buf_ptr += tmp_len; + len -= tmp_len; + } while (len); + return tlv_data->curlen - orig_len; +} + +/* Get user role string from flag. Useful for printing */ +const char *esp_rmaker_get_user_role_string(uint8_t user_role) +{ + switch (user_role) { + case ESP_RMAKER_USER_ROLE_SUPER_ADMIN: + return "Admin"; + case ESP_RMAKER_USER_ROLE_PRIMARY_USER: + return "Primary"; + case ESP_RMAKER_USER_ROLE_SECONDARY_USER: + return "Secondary"; + default: + return "Invalid Role"; + } +} + +/* Prepare the response TLV8 which includes + * + * Request Id + * Status + * Command Id + * Response Data + */ +static esp_err_t esp_rmaker_cmd_prepare_response(esp_rmaker_cmd_ctx_t *cmd_ctx, uint8_t status, void *response, size_t response_size, void **output, size_t *output_len) +{ + size_t publish_size = response_size + 100; /* +100 for rest of metadata. TODO: Do exact calculation */ + void *publish_data = MEM_CALLOC_EXTRAM(1, publish_size); + if (!publish_data) { + ESP_LOGE(TAG, "Failed to allocate buffer of size %d for response.", response_size + 100); + return ESP_ERR_NO_MEM; + } + esp_rmaker_tlv_data_t tlv_data; + esp_rmaker_tlv_data_init(&tlv_data, publish_data, publish_size); + if (strlen(cmd_ctx->req_id)) { + esp_rmaker_add_tlv(&tlv_data, ESP_RMAKER_TLV_TYPE_REQ_ID, strlen(cmd_ctx->req_id), cmd_ctx->req_id); + } + esp_rmaker_add_tlv(&tlv_data, ESP_RMAKER_TLV_TYPE_STATUS, sizeof(status), &status); + uint8_t cmd_buf[2]; + put_u16_le(cmd_buf, cmd_ctx->cmd); + esp_rmaker_add_tlv(&tlv_data, ESP_RMAKER_TLV_TYPE_CMD, sizeof(cmd_buf), cmd_buf); + if (response != NULL && response_size != 0) { + esp_rmaker_add_tlv(&tlv_data, ESP_RMAKER_TLV_TYPE_DATA, response_size, response); + } + ESP_LOGD(TAG, "Generated response of size %d for cmd %d", tlv_data.curlen, cmd_ctx->cmd); + *output = publish_data; + *output_len = tlv_data.curlen; + return ESP_OK; +} + +esp_err_t esp_rmaker_cmd_prepare_empty_response(void **output, size_t *output_len) +{ + size_t publish_size = 6; /* unit16 cmd = 0 (4 bytes in TLV), req_id = empty (2 bytes) */ + void *publish_data = MEM_CALLOC_EXTRAM(1, publish_size); + if (!publish_data) { + return ESP_ERR_NO_MEM; + } + esp_rmaker_tlv_data_t tlv_data; + esp_rmaker_tlv_data_init(&tlv_data, publish_data, publish_size); + esp_rmaker_add_tlv(&tlv_data, ESP_RMAKER_TLV_TYPE_REQ_ID, 0, NULL); + uint8_t cmd_buf[2]; + put_u16_le(cmd_buf, 0); + esp_rmaker_add_tlv(&tlv_data, ESP_RMAKER_TLV_TYPE_CMD, sizeof(cmd_buf), cmd_buf); + *output = publish_data; + *output_len = tlv_data.curlen; + ESP_LOGD(TAG, "Generated empty response for requesting pending commands."); + return ESP_OK; +} + +/* Register a new command with its handler + */ +esp_err_t esp_rmaker_cmd_register(uint16_t cmd, uint8_t access, esp_rmaker_cmd_handler_t handler, bool free_on_return, void *priv) +{ + int i; + for (i = 0; i < RMAKER_MAX_CMD; i++) { + if (esp_rmaker_cmd_list[i] && (esp_rmaker_cmd_list[i]->cmd == cmd)) { + ESP_LOGE(TAG, "Handler for command %d already exists.", cmd); + return ESP_FAIL; + } + } + for (i = 0; i < RMAKER_MAX_CMD; i++) { + if (!esp_rmaker_cmd_list[i]) { + esp_rmaker_cmd_info_t *cmd_info = calloc(1, sizeof(esp_rmaker_cmd_info_t)); + if (!cmd_info) { + ESP_LOGE(TAG, "Could not allocate memory for cmd %d", cmd); + return ESP_ERR_NO_MEM; + } + cmd_info->cmd = cmd; + cmd_info->access = access; + cmd_info->free_on_return = free_on_return; + cmd_info->handler = handler; + cmd_info->priv = priv; + esp_rmaker_cmd_list[i] = cmd_info; + ESP_LOGI(TAG, "Registered command %d", cmd); + return ESP_OK; + } + } + ESP_LOGE(TAG, "No space to add command %d", cmd); + return ESP_ERR_NO_MEM; +} + +/* Find the command infor for given command + * + * Returns pointer to the info if found and NULL on error + */ +static esp_rmaker_cmd_info_t *esp_rmaker_get_cmd_info(uint16_t cmd) +{ + int i; + for (i = 0; i < RMAKER_MAX_CMD; i++) { + if (esp_rmaker_cmd_list[i] && (esp_rmaker_cmd_list[i]->cmd == cmd)) { + ESP_LOGI(TAG, "Handler found for command %d.", cmd); + return esp_rmaker_cmd_list[i]; + } + } + ESP_LOGE(TAG, "No handler found for command %d.", cmd); + return NULL; +} + +/* De-register given command */ +esp_err_t esp_rmaker_cmd_deregister(uint16_t cmd) +{ + int i; + for (i = 0; i < RMAKER_MAX_CMD; i++) { + if (esp_rmaker_cmd_list[i] && (esp_rmaker_cmd_list[i]->cmd == cmd)) { + free(esp_rmaker_cmd_list[i]); + esp_rmaker_cmd_list[i] = NULL; + return ESP_OK; + } + } + ESP_LOGE(TAG, "Cannot unregister command %d as it wasn't registered.", cmd); + return ESP_ERR_INVALID_ARG; +} + +/* Main command response handling function. + * + * It parses the rceived data to find the command and other metadata and + * prepares the response to be sent + */ +esp_err_t esp_rmaker_cmd_response_handler(const void *input, size_t input_len, void **output, size_t *output_len) +{ + esp_rmaker_cmd_ctx_t cmd_ctx = {0}; + + /* Read request id, user role and command, since these are mandatory fields */ + esp_rmaker_get_value_from_tlv(input, input_len, ESP_RMAKER_TLV_TYPE_REQ_ID, &cmd_ctx.req_id, sizeof(cmd_ctx.req_id)); + esp_rmaker_get_value_from_tlv(input, input_len, ESP_RMAKER_TLV_TYPE_USER_ROLE, &cmd_ctx.user_role, sizeof(cmd_ctx.user_role)); + uint8_t cmd_buf[2] = {0}; + esp_rmaker_get_value_from_tlv(input, input_len, ESP_RMAKER_TLV_TYPE_CMD, cmd_buf, sizeof(cmd_buf)); + cmd_ctx.cmd = get_u16_le(cmd_buf); + + if (strlen(cmd_ctx.req_id) == 0 || cmd_ctx.user_role == 0 || cmd_ctx.cmd == 0) { + ESP_LOGE(TAG, "Request id, user role or command id cannot be 0"); + return esp_rmaker_cmd_prepare_response(&cmd_ctx, ESP_RMAKER_CMD_STATUS_CMD_INVALID, NULL, 0, output, output_len); + } + ESP_LOGI(TAG, "Got Req. Id: %s, Role = %s, Cmd = %d", cmd_ctx.req_id, + esp_rmaker_get_user_role_string(cmd_ctx.user_role), cmd_ctx.cmd); + + /* Search for the command info and handle it if found */ + esp_rmaker_cmd_info_t *cmd_info = esp_rmaker_get_cmd_info(cmd_ctx.cmd); + if (cmd_info) { + if (cmd_info->access & cmd_ctx.user_role) { + void *data = NULL; + int data_size = esp_rmaker_get_tlv_length(input, input_len, ESP_RMAKER_TLV_TYPE_DATA); + if (data_size > 0) { + /* TODO: If data size < 255, can just use the pointer to input */ + data = MEM_CALLOC_EXTRAM(1, data_size); + if (!data) { + ESP_LOGE(TAG, "Failed to allocate buffer of size %d for data.", data_size); + return ESP_ERR_NO_MEM; + } + esp_rmaker_get_value_from_tlv(input, input_len, ESP_RMAKER_TLV_TYPE_DATA, data, data_size); + } else { + /* It is not mandatory to have data for a given command. So, just throwing a warning */ + ESP_LOGW(TAG, "No data received for the command."); + data_size = 0; + } + void *response; + size_t response_size = 0; + esp_err_t err = cmd_info->handler(data, data_size, &response, &response_size, &cmd_ctx, cmd_info->priv); + if (err == ESP_OK) { + err = esp_rmaker_cmd_prepare_response(&cmd_ctx, ESP_RMAKER_CMD_STATUS_SUCCESS, response, response_size, output, output_len); + } else { + err = esp_rmaker_cmd_prepare_response(&cmd_ctx, ESP_RMAKER_CMD_STATUS_FAILED, NULL, 0, output, output_len); + } + if (response && cmd_info->free_on_return) { + ESP_LOGI(TAG, "Freeing response buffer."); + free(response); + } + return err; + } else { + return esp_rmaker_cmd_prepare_response(&cmd_ctx, ESP_RMAKER_CMD_STATUS_AUTH_FAIL, NULL, 0, output, output_len); + } + } + return esp_rmaker_cmd_prepare_response(&cmd_ctx, ESP_RMAKER_CMD_STATUS_NOT_FOUND, NULL, 0, output, output_len); +} + +/****************************************** Testing Functions ******************************************/ +static const char *cmd_status[] = { + [ESP_RMAKER_CMD_STATUS_SUCCESS] = "Success", + [ESP_RMAKER_CMD_STATUS_FAILED] = "Fail", + [ESP_RMAKER_CMD_STATUS_CMD_INVALID] = "Invalid command data", + [ESP_RMAKER_CMD_STATUS_AUTH_FAIL] = "Auth fail", + [ESP_RMAKER_CMD_STATUS_NOT_FOUND] = "Command not found", +}; + +/* Send test command */ +esp_err_t esp_rmaker_cmd_resp_test_send(const char *req_id, uint8_t role, uint16_t cmd, const void *data, + size_t data_size, esp_rmaker_cmd_send_t cmd_send, void *priv_data) +{ + if (!cmd_send) { + ESP_LOGE(TAG, "No callback to trigger the command."); + return ESP_ERR_INVALID_ARG; + } + uint8_t cmd_data[200]; + esp_rmaker_tlv_data_t tlv_data; + esp_rmaker_tlv_data_init(&tlv_data, cmd_data, sizeof(cmd_data)); + if (req_id) { + esp_rmaker_add_tlv(&tlv_data, ESP_RMAKER_TLV_TYPE_REQ_ID, strlen(req_id), req_id); + } + esp_rmaker_add_tlv(&tlv_data, ESP_RMAKER_TLV_TYPE_USER_ROLE, sizeof(role), &role); + uint8_t cmd_buf[2]; + put_u16_le(cmd_buf, cmd); + esp_rmaker_add_tlv(&tlv_data, ESP_RMAKER_TLV_TYPE_CMD, sizeof(cmd_buf), cmd_buf); + if (data != NULL && data_size != 0) { + esp_rmaker_add_tlv(&tlv_data, ESP_RMAKER_TLV_TYPE_DATA, data_size, data); + } + ESP_LOGI(TAG, "Sending command of size %d for cmd %d", tlv_data.curlen, cmd); + return cmd_send(cmd_data, tlv_data.curlen, priv_data); +} + +/* Parse response */ +esp_err_t esp_rmaker_cmd_resp_parse_response(const void *response, size_t response_len, void *priv_data) +{ + if (!response) { + ESP_LOGE(TAG, "NULL response. Cannot parse."); + return ESP_ERR_INVALID_ARG; + } + char req_id[REQ_ID_LEN] = {0}; + if (esp_rmaker_get_value_from_tlv(response, response_len, ESP_RMAKER_TLV_TYPE_REQ_ID, req_id, sizeof(req_id)) > 0) { + ESP_LOGI(TAG, "RESP: Request Id: %s", req_id); + } + + uint16_t cmd; + uint8_t cmd_buf[2]; + if (esp_rmaker_get_value_from_tlv(response, response_len, ESP_RMAKER_TLV_TYPE_CMD, cmd_buf, sizeof(cmd_buf)) > 0) { + cmd = get_u16_le(cmd_buf); + ESP_LOGI(TAG, "RESP: Command: %" PRIu16, cmd); + } + + uint8_t status; + if (esp_rmaker_get_value_from_tlv(response, response_len, ESP_RMAKER_TLV_TYPE_STATUS, &status, sizeof(status)) > 0) { + ESP_LOGI(TAG, "RESP: Status: %" PRIu8 ": %s", status, cmd_status[status]); + } + + char resp_data[200]; + int resp_size = esp_rmaker_get_value_from_tlv(response, response_len, ESP_RMAKER_TLV_TYPE_DATA, resp_data, sizeof(resp_data)); + if (resp_size > 0) { + resp_data[resp_size] = 0; + ESP_LOGI(TAG, "RESP: Data: %s", resp_data); + } + return ESP_OK; +} diff --git a/components/rmaker_common/src/console/rmaker_common_cmds.c b/components/rmaker_common/src/console/rmaker_common_cmds.c new file mode 100644 index 0000000..ee538fc --- /dev/null +++ b/components/rmaker_common/src/console/rmaker_common_cmds.c @@ -0,0 +1,359 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef CONFIG_HEAP_TRACING +#include +#endif + +#include + + +#include "rmaker_console_internal.h" + +static const char *TAG = "esp_rmaker_commands"; + +static int reboot_cli_handler(int argc, char *argv[]) +{ + /* Just to go to the next line */ + printf("\n"); + esp_restart(); + return 0; +} + +static int up_time_cli_handler(int argc, char *argv[]) +{ + printf("%s: Uptime of the device: %lld milliseconds\n", TAG, esp_timer_get_time() / 1000); + return 0; +} + +static int task_dump_cli_handler(int argc, char *argv[]) +{ +#ifndef CONFIG_FREERTOS_USE_TRACE_FACILITY + printf("%s: To use this utility enable: Component config --> FreeRTOS --> Enable FreeRTOS trace facility\n", TAG); +#else + int num_of_tasks = uxTaskGetNumberOfTasks(); + TaskStatus_t *task_array = MEM_CALLOC_EXTRAM(num_of_tasks, sizeof(TaskStatus_t)); + if (!task_array) { + ESP_LOGE(TAG, "Memory allocation for task list failed."); + return -1; + } + num_of_tasks = uxTaskGetSystemState(task_array, num_of_tasks, NULL); + printf("%s: \tName\tNumber\tPriority\tStackWaterMark\n", TAG); + for (int i = 0; i < num_of_tasks; i++) { + printf("%16s\t%d\t%d\t%" PRIu32 "\n", + task_array[i].pcTaskName, + task_array[i].xTaskNumber, + task_array[i].uxCurrentPriority, + task_array[i].usStackHighWaterMark); + } + free(task_array); +#endif + return 0; +} + +static int cpu_dump_cli_handler(int argc, char *argv[]) +{ +#ifndef CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS + printf("%s: To use this utility enable: Component config --> FreeRTOS --> Enable FreeRTOS to collect run time stats\n", TAG); +#else + char *buf = MEM_CALLOC_EXTRAM(1, 2 * 1024); + if (!buf) { + ESP_LOGE(TAG, "Memory allocation for cpu dump failed."); + return -1; + } + vTaskGetRunTimeStats(buf); + printf("%s: Run Time Stats:\n%s\n", TAG, buf); + free(buf); +#endif + return 0; +} + +static int mem_dump_cli_handler(int argc, char *argv[]) +{ + printf("\tDescription\tInternal\tSPIRAM\n"); + printf("Current Free Memory\t%d\t\t%d\n", + heap_caps_get_free_size(MALLOC_CAP_8BIT) - heap_caps_get_free_size(MALLOC_CAP_SPIRAM), + heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + printf("Largest Free Block\t%d\t\t%d\n", + heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL), + heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)); + printf("Min. Ever Free Size\t%d\t\t%d\n", + heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL), + heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM)); + return 0; +} + +static int sock_dump_cli_handler(int argc, char *argv[]) +{ +#if LWIP_IPV4 + int i, ret, used_sockets = 0; + + struct sockaddr_in local_sock, peer_sock; + socklen_t local_sock_len = sizeof(struct sockaddr_in), peer_sock_len = sizeof(struct sockaddr_in); + char local_ip_addr[16], peer_ip_addr[16]; + unsigned int local_port, peer_port; + + int sock_type; + socklen_t sock_type_len; + +#define TOTAL_NUM_SOCKETS MEMP_NUM_NETCONN + printf("sock_fd\tprotocol\tlocal_addr\t\tpeer_addr\n"); + for (i = LWIP_SOCKET_OFFSET; i < LWIP_SOCKET_OFFSET + TOTAL_NUM_SOCKETS; i++) { + memset(&local_sock, 0, sizeof(struct sockaddr_in)); + memset(&peer_sock, 0, sizeof(struct sockaddr_in)); + local_sock_len = sizeof(struct sockaddr); + peer_sock_len = sizeof(struct sockaddr); + memset(local_ip_addr, 0, sizeof(local_ip_addr)); + memset(peer_ip_addr, 0, sizeof(peer_ip_addr)); + local_port = 0; + peer_port = 0; + sock_type = 0; + sock_type_len = sizeof(int); + + ret = getsockname(i, (struct sockaddr *)&local_sock, &local_sock_len); + if (ret >= 0) { + used_sockets++; + inet_ntop(AF_INET, &local_sock.sin_addr, local_ip_addr, sizeof(local_ip_addr)); + local_port = ntohs(local_sock.sin_port); + getsockopt(i, SOL_SOCKET, SO_TYPE, &sock_type, &sock_type_len); + printf("%d\t%d:%s\t%16s:%d", i, sock_type, sock_type == SOCK_STREAM ? "tcp" : sock_type == SOCK_DGRAM ? "udp" : "raw", local_ip_addr, local_port); + + ret = getpeername(i, (struct sockaddr *)&peer_sock, &peer_sock_len); + if (ret >= 0) { + inet_ntop(AF_INET, &peer_sock.sin_addr, peer_ip_addr, sizeof(peer_ip_addr)); + peer_port = ntohs(peer_sock.sin_port); + printf("\t%16s:%d", peer_ip_addr, peer_port); + } + printf("\n"); + } + } + printf("Remaining sockets: %d\n", TOTAL_NUM_SOCKETS - used_sockets); +#else + printf("%s: To use this utility enable: Component config --> LWIP --> Enable IPv4 \n", TAG); +#endif /* LWIP_IPV4 */ + return 0; +} + +#ifdef CONFIG_HEAP_TRACING +static int heap_trace_records; +static heap_trace_record_t *heap_trace_records_buf; +static int cli_heap_trace_start() +{ + if (!heap_trace_records_buf) { + // this buffer is required to be in internal + heap_trace_records_buf = malloc(heap_trace_records * sizeof(heap_trace_record_t)); + if (!heap_trace_records_buf) { + printf("%s: Failed to allocate records buffer\n", TAG); + return -1; + } + if (heap_trace_init_standalone(heap_trace_records_buf, heap_trace_records) != ESP_OK) { + printf("%s: Failed to initialise tracing\n", TAG); + goto error1; + } + } + if (heap_trace_start(HEAP_TRACE_LEAKS) != ESP_OK) { + printf("%s: Failed to start heap trace\n", TAG); + goto error2; + } + return 0; +error2: + heap_trace_init_standalone(NULL, 0); +error1: + free(heap_trace_records_buf); + heap_trace_records_buf = NULL; + return -1; +} + +static int cli_heap_trace_stop() +{ + if (!heap_trace_records_buf) { + printf("%s: Tracing not started?\n", TAG); + return 0; + } + heap_trace_stop(); + heap_trace_dump(); + heap_trace_init_standalone(NULL, 0); + free(heap_trace_records_buf); + heap_trace_records_buf = NULL; + return 0; +} +#endif + +static int heap_trace_cli_handler(int argc, char *argv[]) +{ + int ret = 0; +#ifndef CONFIG_HEAP_TRACING + printf("%s: To use this utility enable: Component config --> Heap memory debugging --> Enable heap tracing\n", TAG); +#else + if (argc < 2) { + printf("%s: Incorrect arguments\n", TAG); + return -1; + } + if (strcmp(argv[1], "start") == 0) { +#define DEFAULT_HEAP_TRACE_RECORDS 200 + if (argc != 3) { + heap_trace_records = DEFAULT_HEAP_TRACE_RECORDS; + } else { + heap_trace_records = atoi(argv[2]); + } + printf("%s: Using a buffer to trace %d records\n", TAG, heap_trace_records); + ret = cli_heap_trace_start(); + } else if (strcmp(argv[1], "stop") == 0) { + ret = cli_heap_trace_stop(); + } else { + printf("%s: Invalid argument:%s:\n", TAG, argv[1]); + ret = -1; + } +#endif + return ret; +} + +static int register_generic_debug_commands() +{ + const esp_console_cmd_t debug_commands[] = { + { + .command = "reboot", + .help = "", + .func = reboot_cli_handler, + }, + { + .command = "up-time", + .help = "Get the device up time in milliseconds.", + .func = up_time_cli_handler, + }, + { + .command = "mem-dump", + .help = "Get the available memory.", + .func = mem_dump_cli_handler, + }, + { + .command = "task-dump", + .help = "Get the list of all the running tasks.", + .func = task_dump_cli_handler, + }, + { + .command = "cpu-dump", + .help = "Get the CPU utilisation by all the runninng tasks.", + .func = cpu_dump_cli_handler, + }, + { + .command = "sock-dump", + .help = "Get the list of all the active sockets.", + .func = sock_dump_cli_handler, + }, + { + .command = "heap-trace", + .help = "Start or stop heap tracing. Usage: heap-trace ", + .func = heap_trace_cli_handler, + }, + }; + + int cmds_num = sizeof(debug_commands) / sizeof(esp_console_cmd_t); + int i; + for (i = 0; i < cmds_num; i++) { + ESP_LOGI(TAG, "Registering command: %s", debug_commands[i].command); + esp_console_cmd_register(&debug_commands[i]); + } + return 0; +} + +static int reset_to_factory_handler(int argc, char** argv) +{ +#define RESET_DELAY 2 +#define REBOOT_DELAY 2 + printf("%s: Resetting to Factory Defaults...\n", TAG); + esp_rmaker_factory_reset(RESET_DELAY, REBOOT_DELAY); + return ESP_OK; +} + +static void register_reset_to_factory() +{ + const esp_console_cmd_t cmd = { + .command = "reset-to-factory", + .help = "Reset the board to factory defaults", + .func = &reset_to_factory_handler, + }; + ESP_LOGI(TAG, "Registering command: %s", cmd.command); + esp_console_cmd_register(&cmd); +} + +static int local_time_cli_handler(int argc, char *argv[]) +{ + char local_time[64]; + if (esp_rmaker_get_local_time_str(local_time, sizeof(local_time)) == ESP_OK) { + printf("%s: Current local time: %s\n", TAG, local_time); + } else { + printf("%s: Current local time (truncated): %s\n", TAG, local_time); + } + return ESP_OK; +} + +static int tz_set_cli_handler(int argc, char *argv[]) +{ + if (argc < 2) { + printf("%s: Invalid Usage.\n", TAG); + return ESP_ERR_INVALID_ARG; + } + if (strcmp(argv[1], "posix") == 0) { + if (argv[2]) { + esp_rmaker_time_set_timezone_posix(argv[2]); + } else { + printf("%s: Invalid Usage.\n", TAG); + return ESP_ERR_INVALID_ARG; + } + } else { + esp_rmaker_time_set_timezone(argv[1]); + } + return ESP_OK; +} + +static void register_time_commands() +{ + const esp_console_cmd_t local_time_cmd = { + .command = "local-time", + .help = "Get the local time of device.", + .func = &local_time_cli_handler, + }; + ESP_LOGI(TAG, "Registering command: %s", local_time_cmd.command); + esp_console_cmd_register(&local_time_cmd); + + const esp_console_cmd_t tz_set_cmd = { + .command = "tz-set", + .help = "Set Timezone. Usage: tz-set [posix] .", + .func = &tz_set_cli_handler, + }; + ESP_LOGI(TAG, "Registering command: %s", tz_set_cmd.command); + esp_console_cmd_register(&tz_set_cmd); +} + +void esp_rmaker_common_register_commands() +{ + static bool registered = false; + if (registered) { + ESP_LOGI(TAG, "Skipping already registered commands"); + return; + } + register_generic_debug_commands(); + register_reset_to_factory(); + register_time_commands(); + registered = true; +} diff --git a/components/rmaker_common/src/console/rmaker_console.c b/components/rmaker_common/src/console/rmaker_console.c new file mode 100644 index 0000000..b51f81b --- /dev/null +++ b/components/rmaker_common/src/console/rmaker_console.c @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "rmaker_console_internal.h" + +static const char *TAG = "esp_rmaker_console"; +static int stop; + +static void scli_task(void *arg) +{ + int uart_num = 0; + uint8_t linebuf[256]; + int i, cmd_ret; + esp_err_t ret; + QueueHandle_t uart_queue; + uart_event_t event; + bool first_done = false; + + ESP_LOGI(TAG, "Initialising UART on port %d", uart_num); + uart_driver_install(uart_num, 256, 0, 8, &uart_queue, 0); + + /* Initialize the console */ + esp_console_config_t console_config = { + .max_cmdline_args = 8, + .max_cmdline_length = 256, + }; + esp_console_init(&console_config); + esp_console_register_help_command(); + + while (!stop) { + if (first_done) { + uart_write_bytes(uart_num, "\n>> ", 4); + } else { + first_done = true; + } + memset(linebuf, 0, sizeof(linebuf)); + i = 0; + do { + ret = xQueueReceive(uart_queue, (void * )&event, (TickType_t)portMAX_DELAY); + if (ret != pdPASS) { + if(stop == 1) { + break; + } else { + continue; + } + } + if (event.type == UART_DATA) { + while (uart_read_bytes(uart_num, (uint8_t *) &linebuf[i], 1, 0)) { + if (linebuf[i] == '\r') { + uart_write_bytes(uart_num, "\r\n", 2); + } else { + uart_write_bytes(uart_num, (char *) &linebuf[i], 1); + } + i++; + } + } + } while ((i < 255) && linebuf[i-1] != '\r'); + if (stop) { + break; + } + /* Remove the truncating \r\n */ + linebuf[strlen((char *)linebuf) - 1] = '\0'; + /* Just to go to the next line */ + printf("\n"); + ret = esp_console_run((char *) linebuf, &cmd_ret); + if (cmd_ret != 0) { + printf("%s: Console command failed with error: %d\n", TAG, cmd_ret); + cmd_ret = 0; + } + if (ret < 0) { + printf("%s: Console dispatcher error\n", TAG); + break; + } + } + ESP_LOGE(TAG, "Stopped CLI"); + vTaskDelete(NULL); +} + +static esp_err_t scli_init() +{ + static bool cli_started = false; + if (cli_started) { + return ESP_OK; + } +#define SCLI_STACK_SIZE 4096 + if (xTaskCreate(&scli_task, "console_task", SCLI_STACK_SIZE, NULL, 3, NULL) != pdPASS) { + ESP_LOGE(TAG, "Couldn't create thread"); + return ESP_ERR_NO_MEM; + } + cli_started = true; + return ESP_OK; +} + +esp_err_t esp_rmaker_common_console_init() +{ + esp_err_t ret = scli_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Couldn't initialise console"); + return ret; + } + + esp_rmaker_common_register_commands(); + return ESP_OK; +} diff --git a/components/rmaker_common/src/console/rmaker_console_internal.h b/components/rmaker_common/src/console/rmaker_console_internal.h new file mode 100644 index 0000000..942a7e0 --- /dev/null +++ b/components/rmaker_common/src/console/rmaker_console_internal.h @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +void esp_rmaker_common_register_commands(void); diff --git a/components/rmaker_common/src/create_APN3_PPI_string.c b/components/rmaker_common/src/create_APN3_PPI_string.c new file mode 100644 index 0000000..ca2074a --- /dev/null +++ b/components/rmaker_common/src/create_APN3_PPI_string.c @@ -0,0 +1,246 @@ +/* + * APN3 PPI string generator - sample code + * + * NOTE: All code is provided as sample code for informational purposes only, and should not be used for any testing or production workloads. + * All code is provided “as is” and AWS expressly disclaims all warranties, including, without limitation: any implied warranties of + * noninfringement, merchantability, or fitness for a particular purpose; any warranty that operation of the code will be error-free + * or free of harmful components; or any warranty arising out of any course of dealing or usage of trade. In no event shall AWS or + * any of its affiliates be liable for any damages arising out of the use of this code, including, without limitation, any direct, + * indirect, special, incidental or consequential damages. + */ +#include +#include +#include +#include +#include +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD +#include +#endif + +static const char *TAG = "aws_ppi"; + +#define PPI_STRING_GEN_VERSION 0.4 +#define MAX_PPI_STRING_LEN 64 +#define MAX_LEN_FIELDS_1_2_3 37 /* max length of fields ProductName + ProductUID + ProductVersion */ + +#define PPI_PREFIX_1 "?" +#define PPI_PREFIX_1_ALT "&" + +#define PPI_PREFIX_2 "Platform=APN3|" +#define PPI_RESERVED "A0|" /* reserved for AWS use */ + +#define PPI_PLATFORM_AWS "AZ|" +#define PPI_PLATFORM_RAINMAKER "RM|" +#define PPI_PLATFORM PPI_PLATFORM_RAINMAKER + +#define PPI_SILICON_SKU_CODE CONFIG_ESP_RMAKER_MQTT_PRODUCT_SKU +#define PPI_PRODUCT_NAME CONFIG_ESP_RMAKER_MQTT_PRODUCT_NAME +#define PPI_PRODUCT_VERSION CONFIG_ESP_RMAKER_MQTT_PRODUCT_VERSION + +#define PPI_SEPARATOR_CHAR '|' +#define PPI_SEPARATOR "|" + +#define PPI_FIXED_PART PPI_PREFIX_1 PPI_PREFIX_2 PPI_RESERVED PPI_PLATFORM PPI_SILICON_SKU_CODE PPI_SEPARATOR PPI_PRODUCT_NAME PPI_SEPARATOR PPI_PRODUCT_VERSION PPI_SEPARATOR + +#define VALID 1 +#define INVALID 0 + +/* + * platform_get_product_name is a stub function to be customized for each platform. + * It must return a pointer to the product name string. + * Can use a #define instead of a function in most cases. + */ +char __attribute__((weak)) *platform_get_product_name() +{ + return(PPI_PRODUCT_NAME); +} + +/* + * platform_get_product_UID is a stub function to be customized for each platform. + * It must return a pointer to the product UID string. + */ +char __attribute__((weak)) *platform_get_product_UID() +{ + static char mac_str[13]; +#if defined(CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI) + uint8_t eth_mac[6]; + esp_err_t err = esp_wifi_get_mac(WIFI_IF_STA, eth_mac); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Could not fetch MAC address. Please initialise Wi-Fi first"); + return NULL; + } + snprintf(mac_str, sizeof(mac_str), "%02X%02X%02X%02X%02X%02X", + eth_mac[0], eth_mac[1], eth_mac[2], eth_mac[3], eth_mac[4], eth_mac[5]); +#elif defined(CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD) /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + uint8_t base_mac[6]; + esp_err_t err = esp_read_mac(base_mac, ESP_MAC_BASE); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Could not fetch base mac"); + return NULL; + } + snprintf(mac_str, sizeof(mac_str), "%02X%02X%02X%02X%02X%02X", + base_mac[0], base_mac[1], base_mac[2], base_mac[3], base_mac[4], base_mac[5]); +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ + return mac_str; +} + +/* + * platform_get_product_version is a stub function to be customized for each platform. + * It must return a pointer to the product version string. + * Can use a #define instead of a function in most cases. + */ +char __attribute__((weak)) *platform_get_product_version() +{ + return(PPI_PRODUCT_VERSION); +} + +/* + * platform_get_silicom_sku_code is a stub function to be customized for each platform. + * It must return a pointer to a valid Silicon SKU code. + * Can use a #define instead of a function in most cases. + */ +char __attribute__((weak)) *platform_get_silicon_sku_code() +{ + return(PPI_SILICON_SKU_CODE); +} + +/* + * validate_sku_code takes as input the 4 character SKU code and validates it. + * + * Returns + * VALID if valid, else INVALID + */ +int validate_sku_code(char *silicon_sku_code) +{ + int retval = INVALID; + + typedef struct sku_item_t { + /* entry in sku code lookup table */ + char *skucode; /* code corresponding to sku name */ + } sku_item; + + static sku_item skutable[] = + { + /* + * codes for partner 1 + */ + { "ES00" }, /* Older Default */ + { "EX00" }, /* Default */ + { "EX01" }, /* ESP32-C3, including 8685 */ + { "EX02" }, /* ESP32-C2, including 8684 */ + { "EX03" }, /* ESP32-S3 */ + { "EX04" }, /* ESP32 */ + { "EX05" }, /* ESP32-C5 */ + { "EX06" }, /* ESP32-S2 */ + /* + * add new entries above in any order + */ + { NULL } /* this must be the last entry */ + }; + + /* + * linear search for a matching silicon sku code entry + */ + for (sku_item *p = skutable; p->skucode != NULL; p++) + { + if (strcmp(p->skucode, silicon_sku_code) == 0) + { /* found match */ + retval = VALID; + break; + } + } + return (retval); +} + +/* + * validate_platform_inputs + * + * Returns + * VALID if inputs are valid, INVALID otherwise + */ +int validate_platform_inputs(char *product_name, char *product_uid, char *product_version, char *silicon_sku_code) +{ + int retval = INVALID; + + if ( (strlen(product_name) + strlen(product_uid) + strlen(product_version)) <= MAX_LEN_FIELDS_1_2_3) + { /* field 1,2,3 length check passed */ + + if (validate_sku_code(silicon_sku_code) == VALID) + { + retval = VALID; + } + else + { /* invalid sku code */ + ESP_LOGE(TAG, "Error: Invalid sku code"); + } + } + else + { /* invalid string length */ + ESP_LOGE(TAG, "Error: Invalid field(s) length"); + } + return(retval); +} + +/* + * create_APN_PPI_string creates the string that must be passed in the UserName field of the MQTT Connect + * packet as required by the IAP Module Incentive program. Details are given in the APN3 IoT Acceleration + * Module Incentive Program Guide-Tech- document. + * + * Inputs: + * product_name = pointer to a string holding the product name. + * product_uid = pointer to a string holding the product UID (unique id) + * product_version = version number of the product. + * silicon_sku_code = pointer to a string holding a valid SKU code as specified in the guide. + * + * Return values + * NULL if there is an error and the PPI string could not be generated, otherwise the PPI string is returned. + */ +char * create_APN_PPI_string(char *product_name, char *product_uid, char *product_version, char *silicon_sku_code) +{ + char *retval = NULL; + static char ppistring[MAX_PPI_STRING_LEN + 1]; + + + ESP_LOGD(TAG, "PPI string generator v%2.1f", PPI_STRING_GEN_VERSION); + + /* + * Validate inputs + */ + if (validate_platform_inputs(product_name, product_uid, product_version, silicon_sku_code)) + { /* valid inputs - build the string */ + snprintf(ppistring, sizeof(ppistring),"%s%s%s%s%s%s%s%s%s%s%s", + PPI_PREFIX_1, PPI_PREFIX_2, PPI_RESERVED, PPI_PLATFORM, silicon_sku_code, + PPI_SEPARATOR, product_name, PPI_SEPARATOR, product_version, PPI_SEPARATOR, + product_uid); + retval = ppistring; + } + else + { /* error - some inputs are not valid */ + ESP_LOGE(TAG, "Error: Some inputs are not valid"); + } + return(retval); +} + + +/* + * main - setup values for your platform and invoke string generator. + */ +const char __attribute__((weak)) *esp_get_aws_ppi(void) +{ + char *ppistring = NULL; + + ppistring = create_APN_PPI_string( platform_get_product_name(), + platform_get_product_UID(), + platform_get_product_version(), + platform_get_silicon_sku_code() ); + + if (ppistring != NULL) + { + ESP_LOGD(TAG, "PPI inputs pass all checks. String value is shown below:\n\n%s", ppistring); + } + else + { /* error */ + ESP_LOGE(TAG, "Error creating PPI string"); + } + return ppistring; +} diff --git a/components/rmaker_common/src/esp-mqtt/esp-mqtt-glue.c b/components/rmaker_common/src/esp-mqtt/esp-mqtt-glue.c new file mode 100644 index 0000000..5e17598 --- /dev/null +++ b/components/rmaker_common/src/esp-mqtt/esp-mqtt-glue.c @@ -0,0 +1,477 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0) +// Features supported in 4.1+ + +#ifdef CONFIG_ESP_RMAKER_MQTT_PORT_443 +#define ESP_RMAKER_MQTT_USE_PORT_443 +#endif + +#else + +#ifdef CONFIG_ESP_RMAKER_MQTT_PORT_443 +#warning "Certificate Bundle not supported below IDF v4.4. Using provided certificate instead." +#endif + +#endif /* !IDF4.1 */ + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) +// Features supported in 4.4+ + +#ifdef CONFIG_ESP_RMAKER_MQTT_USE_CERT_BUNDLE +#define ESP_RMAKER_MQTT_USE_CERT_BUNDLE +#include +#endif + +#else + +#ifdef CONFIG_ESP_RMAKER_MQTT_USE_CERT_BUNDLE +#warning "Certificate Bundle not supported below 4.4. Using provided certificate instead." +#endif + +#endif /* !IDF4.4 */ + +static const char *TAG = "esp_mqtt_glue"; + +#define MAX_MQTT_SUBSCRIPTIONS CONFIG_ESP_RMAKER_MAX_MQTT_SUBSCRIPTIONS + +typedef struct { + char *topic; + esp_rmaker_mqtt_subscribe_cb_t cb; + void *priv; +} esp_mqtt_glue_subscription_t; + +typedef struct { + esp_mqtt_client_handle_t mqtt_client; + esp_rmaker_mqtt_conn_params_t *conn_params; + esp_mqtt_glue_subscription_t *subscriptions[MAX_MQTT_SUBSCRIPTIONS]; +} esp_mqtt_glue_data_t; +esp_mqtt_glue_data_t *mqtt_data; + +typedef struct { + char *data; + char *topic; +} esp_mqtt_glue_long_data_t; + +static void esp_mqtt_glue_deinit(void); + +static void esp_mqtt_glue_subscribe_callback(const char *topic, int topic_len, const char *data, int data_len) +{ + esp_mqtt_glue_subscription_t **subscriptions = mqtt_data->subscriptions; + int i; + for (i = 0; i < MAX_MQTT_SUBSCRIPTIONS; i++) { + if (subscriptions[i]) { + if ((strncmp(topic, subscriptions[i]->topic, topic_len) == 0) + && (topic_len == strlen(subscriptions[i]->topic))) { + subscriptions[i]->cb(subscriptions[i]->topic, (void *)data, data_len, subscriptions[i]->priv); + } + } + } +} + +static esp_err_t esp_mqtt_glue_subscribe(const char *topic, esp_rmaker_mqtt_subscribe_cb_t cb, uint8_t qos, void *priv_data) +{ + if ( !mqtt_data || !topic || !cb) { + return ESP_FAIL; + } + int i; + for (i = 0; i < MAX_MQTT_SUBSCRIPTIONS; i++) { + if (!mqtt_data->subscriptions[i]) { + esp_mqtt_glue_subscription_t *subscription = calloc(1, sizeof(esp_mqtt_glue_subscription_t)); + if (!subscription) { + return ESP_FAIL; + } + subscription->topic = strdup(topic); + if (!subscription->topic) { + free(subscription); + return ESP_FAIL; + } + int ret = esp_mqtt_client_subscribe(mqtt_data->mqtt_client, subscription->topic, qos); + if (ret < 0) { + free(subscription->topic); + free(subscription); + return ESP_FAIL; + } + subscription->priv = priv_data; + subscription->cb = cb; + mqtt_data->subscriptions[i] = subscription; + ESP_LOGD(TAG, "Subscribed to topic: %s", topic); + return ESP_OK; + } + } + return ESP_FAIL; +} + +static void unsubscribe_helper(esp_mqtt_glue_subscription_t **subscription) +{ + if (subscription && *subscription) { + if (esp_mqtt_client_unsubscribe(mqtt_data->mqtt_client, (*subscription)->topic) < 0) { + ESP_LOGW(TAG, "Could not unsubscribe from topic: %s", (*subscription)->topic); + } + free((*subscription)->topic); + free(*subscription); + *subscription = NULL; + } +} + +static esp_err_t esp_mqtt_glue_unsubscribe(const char *topic) +{ + if (!mqtt_data || !topic) { + return ESP_FAIL; + } + esp_mqtt_glue_subscription_t **subscriptions = mqtt_data->subscriptions; + int i; + for (i = 0; i < MAX_MQTT_SUBSCRIPTIONS; i++) { + if (subscriptions[i]) { + if (strncmp(topic, subscriptions[i]->topic, strlen(topic)) == 0) { + unsubscribe_helper(&subscriptions[i]); + return ESP_OK; + } + } + } + return ESP_FAIL; +} + +static esp_err_t esp_mqtt_glue_publish(const char *topic, void *data, size_t data_len, uint8_t qos, int *msg_id) +{ + if (!mqtt_data || !topic || !data) { + return ESP_FAIL; + } + ESP_LOGD(TAG, "Publishing to %s", topic); + int ret = esp_mqtt_client_publish(mqtt_data->mqtt_client, topic, data, data_len, qos, 0); + if (ret < 0) { + ESP_LOGE(TAG, "MQTT Publish failed"); + return ESP_FAIL; + } + if (msg_id) { + *msg_id = ret; + } + return ESP_OK; +} + +static esp_mqtt_glue_long_data_t *esp_mqtt_glue_free_long_data(esp_mqtt_glue_long_data_t *long_data) +{ + if (long_data) { + if (long_data->topic) { + free(long_data->topic); + } + if (long_data->data) { + free(long_data->data); + } + free(long_data); + } + return NULL; +} + +static esp_mqtt_glue_long_data_t *esp_mqtt_glue_manage_long_data(esp_mqtt_glue_long_data_t *long_data, + esp_mqtt_event_handle_t event) +{ + if (event->topic) { + /* This is new data. Free any earlier data, if present. */ + esp_mqtt_glue_free_long_data(long_data); + long_data = calloc(1, sizeof(esp_mqtt_glue_long_data_t)); + if (!long_data) { + ESP_LOGE(TAG, "Could not allocate memory for esp_mqtt_glue_long_data_t"); + return NULL; + } + long_data->data = MEM_CALLOC_EXTRAM(1, event->total_data_len); + if (!long_data->data) { + ESP_LOGE(TAG, "Could not allocate %d bytes for received data.", event->total_data_len); + return esp_mqtt_glue_free_long_data(long_data); + } + long_data->topic = strndup(event->topic, event->topic_len); + if (!long_data->topic) { + ESP_LOGE(TAG, "Could not allocate %d bytes for received topic.", event->topic_len); + return esp_mqtt_glue_free_long_data(long_data); + } + } + if (long_data) { + memcpy(long_data->data + event->current_data_offset, event->data, event->data_len); + + if ((event->current_data_offset + event->data_len) == event->total_data_len) { + esp_mqtt_glue_subscribe_callback(long_data->topic, strlen(long_data->topic), + long_data->data, event->total_data_len); + return esp_mqtt_glue_free_long_data(long_data); + } + } + return long_data; +} + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +#else +static esp_err_t mqtt_event_handler(esp_mqtt_event_handle_t event) +#endif +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + esp_mqtt_event_handle_t event = event_data; +#else + uint32_t event_id = event->event_id; +#endif + + switch (event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT Connected"); + /* Resubscribe to all topics after reconnection */ + for (int i = 0; i < MAX_MQTT_SUBSCRIPTIONS; i++) { + if (mqtt_data->subscriptions[i]) { + esp_mqtt_client_subscribe(event->client, mqtt_data->subscriptions[i]->topic, 1); + } + } + esp_event_post(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_CONNECTED, NULL, 0, portMAX_DELAY); + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGW(TAG, "MQTT Disconnected. Will try reconnecting in a while..."); + esp_event_post(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_DISCONNECTED, NULL, 0, portMAX_DELAY); + break; + + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGD(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGD(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_PUBLISHED: + ESP_LOGD(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); + esp_event_post(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_PUBLISHED, &event->msg_id, sizeof(event->msg_id), portMAX_DELAY); + break; +#ifdef CONFIG_MQTT_REPORT_DELETED_MESSAGES + case MQTT_EVENT_DELETED: + ESP_LOGD(TAG, "MQTT_EVENT_DELETED, msg_id=%d", event->msg_id); + esp_event_post(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_MSG_DELETED, &event->msg_id, sizeof(event->msg_id), portMAX_DELAY); + break; +#endif /* CONFIG_MQTT_REPORT_DELETED_MESSAGES */ + case MQTT_EVENT_DATA: { + ESP_LOGD(TAG, "MQTT_EVENT_DATA"); + static esp_mqtt_glue_long_data_t *long_data; + /* Topic can be NULL, for data longer than the MQTT buffer */ + if (event->topic) { + ESP_LOGD(TAG, "TOPIC=%.*s\r\n", event->topic_len, event->topic); + } + ESP_LOGD(TAG, "DATA=%.*s\r\n", event->data_len, event->data); + if (event->data_len == event->total_data_len) { + /* If long_data still exists, it means there was some issue getting the + * long data, and so, it needs to be freed up. + */ + if (long_data) { + long_data = esp_mqtt_glue_free_long_data(long_data); + } + esp_mqtt_glue_subscribe_callback(event->topic, event->topic_len, event->data, event->data_len); + } else { + long_data = esp_mqtt_glue_manage_long_data(long_data, event); + } + break; + } + case MQTT_EVENT_ERROR: + ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); + break; + default: + ESP_LOGD(TAG, "Other event id:%d", event->event_id); + break; + } +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + return ESP_OK; +#endif +} + +static esp_err_t esp_mqtt_glue_connect(void) +{ + if (!mqtt_data) { + return ESP_FAIL; + } + ESP_LOGI(TAG, "Connecting to %s", mqtt_data->conn_params->mqtt_host); + esp_err_t ret = esp_mqtt_client_start(mqtt_data->mqtt_client); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "esp_mqtt_client_start() failed with err = %d", ret); + return ret; + } + return ESP_OK; +} + +static void esp_mqtt_glue_unsubscribe_all(void) +{ + if (!mqtt_data) { + return; + } + int i; + for (i = 0; i < MAX_MQTT_SUBSCRIPTIONS; i++) { + if (mqtt_data->subscriptions[i]) { + unsubscribe_helper(&(mqtt_data->subscriptions[i])); + } + } +} + +static esp_err_t esp_mqtt_glue_disconnect(void) +{ + if (!mqtt_data) { + return ESP_FAIL; + } + esp_mqtt_glue_unsubscribe_all(); + esp_err_t err = esp_mqtt_client_stop(mqtt_data->mqtt_client); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to disconnect from MQTT"); + } else { + ESP_LOGI(TAG, "MQTT Disconnected."); + } + return err; +} +#ifdef ESP_RMAKER_MQTT_USE_PORT_443 +static const char *alpn_protocols[] = { "x-amzn-mqtt-ca", NULL }; +#endif /* ESP_RMAKER_MQTT_USE_PORT_443 */ +static esp_err_t esp_mqtt_glue_init(esp_rmaker_mqtt_conn_params_t *conn_params) +{ +#ifdef CONFIG_ESP_RMAKER_MQTT_SEND_USERNAME + const char *username = esp_get_aws_ppi(); + ESP_LOGI(TAG, "AWS PPI: %s", username); +#endif + if (mqtt_data) { + ESP_LOGE(TAG, "MQTT already initialized"); + return ESP_OK; + } + if (!conn_params) { + ESP_LOGE(TAG, "Connection params are mandatory for esp_mqtt_glue_init"); + return ESP_FAIL; + } + ESP_LOGI(TAG, "Initialising MQTT"); + mqtt_data = calloc(1, sizeof(esp_mqtt_glue_data_t)); + if (!mqtt_data) { + ESP_LOGE(TAG, "Failed to allocate memory for esp_mqtt_glue_data_t"); + return ESP_ERR_NO_MEM; + } + mqtt_data->conn_params = conn_params; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + const esp_mqtt_client_config_t mqtt_client_cfg = { + .broker = { + .address = { + .hostname = conn_params->mqtt_host, +#ifdef ESP_RMAKER_MQTT_USE_PORT_443 + .port = 443, +#else + .port = 8883, +#endif + .transport = MQTT_TRANSPORT_OVER_SSL, + }, + .verification = { +#ifdef ESP_RMAKER_MQTT_USE_PORT_443 + .alpn_protos = alpn_protocols, +#endif +#ifdef ESP_RMAKER_MQTT_USE_CERT_BUNDLE + .crt_bundle_attach = esp_crt_bundle_attach, +#else + .certificate = (const char *)conn_params->server_cert, + .certificate_len = conn_params->server_cert_len, +#endif + } + }, + .credentials = { +#ifdef CONFIG_ESP_RMAKER_MQTT_SEND_USERNAME + .username = username, +#endif + .client_id = (const char *)conn_params->client_id, + .authentication = { + .certificate = (const char *)conn_params->client_cert, + .certificate_len = conn_params->client_cert_len, + .key = (const char *)conn_params->client_key, + .key_len = conn_params->client_key_len, + .ds_data = conn_params->ds_data + }, + }, + .session = { + .keepalive = CONFIG_ESP_RMAKER_MQTT_KEEP_ALIVE_INTERVAL, +#ifdef CONFIG_ESP_RMAKER_MQTT_PERSISTENT_SESSION + .disable_clean_session = 1, +#endif /* CONFIG_ESP_RMAKER_MQTT_PERSISTENT_SESSION */ + }, + }; +#else + const esp_mqtt_client_config_t mqtt_client_cfg = { + .host = conn_params->mqtt_host, +#ifdef ESP_RMAKER_MQTT_USE_PORT_443 + .port = 443, + .alpn_protos = alpn_protocols, +#else + .port = 8883, +#endif /* !ESP_RMAKER_MQTT_USE_PORT_443 */ +#ifdef ESP_RMAKER_MQTT_USE_CERT_BUNDLE + .crt_bundle_attach = esp_crt_bundle_attach, +#else + .cert_pem = (const char *)conn_params->server_cert, + .cert_len = conn_params->server_cert_len, +#endif + .client_cert_pem = (const char *)conn_params->client_cert, + .client_cert_len = conn_params->client_cert_len, + .client_key_pem = (const char *)conn_params->client_key, + .client_key_len = conn_params->client_key_len, + .client_id = (const char *)conn_params->client_id, + .keepalive = CONFIG_ESP_RMAKER_MQTT_KEEP_ALIVE_INTERVAL, + .event_handle = mqtt_event_handler, + .transport = MQTT_TRANSPORT_OVER_SSL, +#ifdef CONFIG_ESP_RMAKER_MQTT_PERSISTENT_SESSION + .disable_clean_session = 1, +#endif /* CONFIG_ESP_RMAKER_MQTT_PERSISTENT_SESSION */ +#ifdef CONFIG_ESP_RMAKER_MQTT_SEND_USERNAME + .username = username, +#endif + }; +#endif + mqtt_data->mqtt_client = esp_mqtt_client_init(&mqtt_client_cfg); + if (!mqtt_data->mqtt_client) { + ESP_LOGE(TAG, "esp_mqtt_client_init failed"); + esp_mqtt_glue_deinit(); + return ESP_FAIL; + } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + esp_mqtt_client_register_event(mqtt_data->mqtt_client , ESP_EVENT_ANY_ID, mqtt_event_handler, NULL); +#endif + return ESP_OK; +} + +static void esp_mqtt_glue_deinit(void) +{ + esp_mqtt_glue_unsubscribe_all(); + if (mqtt_data && mqtt_data->mqtt_client) { + esp_mqtt_client_destroy(mqtt_data->mqtt_client); + } + if (mqtt_data) { + free(mqtt_data); + mqtt_data = NULL; + } +} + +esp_err_t esp_rmaker_mqtt_glue_setup(esp_rmaker_mqtt_config_t *mqtt_config) +{ + mqtt_config->init = esp_mqtt_glue_init; + mqtt_config->deinit = esp_mqtt_glue_deinit; + mqtt_config->connect = esp_mqtt_glue_connect; + mqtt_config->disconnect = esp_mqtt_glue_disconnect; + mqtt_config->publish = esp_mqtt_glue_publish; + mqtt_config->subscribe = esp_mqtt_glue_subscribe; + mqtt_config->unsubscribe = esp_mqtt_glue_unsubscribe; + mqtt_config->setup_done = true; + return ESP_OK; +} diff --git a/components/rmaker_common/src/factory.c b/components/rmaker_common/src/factory.c new file mode 100644 index 0000000..c7a3ba3 --- /dev/null +++ b/components/rmaker_common/src/factory.c @@ -0,0 +1,120 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include +#include +#include +#include + +static const char *TAG = "esp_rmaker_fctry"; + +#define RMAKER_FACTORY_NAMESPACE CONFIG_ESP_RMAKER_FACTORY_NAMESPACE +#define RMAKER_FACTORY_PART CONFIG_ESP_RMAKER_FACTORY_PARTITION_NAME + +esp_err_t esp_rmaker_factory_init(void) +{ + static bool esp_rmaker_storage_init_done; + if (esp_rmaker_storage_init_done) { + ESP_LOGW(TAG, "ESP RainMaker Storage already initialized"); + return ESP_OK; + } + esp_err_t err = ESP_OK; +#ifdef CONFIG_ESP_RMAKER_ENCRYPT_FACTORY_PARTITION + const char *nvs_keys_partition_name = CONFIG_ESP_RMAKER_FACTORY_NVS_KEYS_PARTITION_NAME; + if (strlen(nvs_keys_partition_name) == 0) { + nvs_keys_partition_name = NULL; + } + ESP_LOGI(TAG, "Initialising factory partition in secure mode."); + const esp_partition_t* key_part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_NVS_KEYS, nvs_keys_partition_name); + if (!key_part) { + ESP_LOGE(TAG, "Partition with subtype nvs_keys not found"); + return ESP_FAIL; + } + nvs_sec_cfg_t cfg = {}; + err = nvs_flash_read_security_cfg(key_part, &cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to read NVS security cfg: 0x%x", err); + return err; + } + err = nvs_flash_secure_init_partition(RMAKER_FACTORY_PART, &cfg); +#else + err = nvs_flash_init_partition(RMAKER_FACTORY_PART); +#endif + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS Flash init failed"); + } else { + esp_rmaker_storage_init_done = true; + } + return err; +} + +void *esp_rmaker_factory_get(const char *key) +{ + nvs_handle handle; + esp_err_t err; + if ((err = nvs_open_from_partition(RMAKER_FACTORY_PART, RMAKER_FACTORY_NAMESPACE, + NVS_READONLY, &handle)) != ESP_OK) { + ESP_LOGD(TAG, "NVS open for %s %s %s failed with error %d", RMAKER_FACTORY_PART, RMAKER_FACTORY_NAMESPACE, key, err); + return NULL; + } + size_t required_size = 0; + if ((err = nvs_get_blob(handle, key, NULL, &required_size)) != ESP_OK) { + ESP_LOGD(TAG, "Failed to read key %s with error %d size %d", key, err, required_size); + nvs_close(handle); + return NULL; + } + void *value = MEM_CALLOC_EXTRAM(required_size + 1, 1); /* + 1 for NULL termination */ + if (value) { + nvs_get_blob(handle, key, value, &required_size); + } + nvs_close(handle); + return value; +} + +size_t esp_rmaker_factory_get_size(const char *key) +{ + nvs_handle handle; + esp_err_t err; + if ((err = nvs_open_from_partition(RMAKER_FACTORY_PART, RMAKER_FACTORY_NAMESPACE, + NVS_READONLY, &handle)) != ESP_OK) { + ESP_LOGD(TAG, "NVS open for %s %s %s failed with error %d", RMAKER_FACTORY_PART, RMAKER_FACTORY_NAMESPACE, key, err); + return 0; + } + size_t required_size = 0; + if ((err = nvs_get_blob(handle, key, NULL, &required_size)) != ESP_OK) { + ESP_LOGD(TAG, "Failed to read key %s with error %d size %d", key, err, required_size); + } + nvs_close(handle); + return required_size; +} + +esp_err_t esp_rmaker_factory_set(const char *key, void *value, size_t len) +{ + nvs_handle handle; + esp_err_t err; + if ((err = nvs_open_from_partition(RMAKER_FACTORY_PART, RMAKER_FACTORY_NAMESPACE, + NVS_READWRITE, &handle)) != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return ESP_FAIL; + } + if ((err = nvs_set_blob(handle, key, value, len)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to write key %s with error %d size %d", key, err, len); + nvs_close(handle); + return ESP_FAIL; + } + nvs_commit(handle); + nvs_close(handle); + return ESP_OK; +} diff --git a/components/rmaker_common/src/time.c b/components/rmaker_common/src/time.c new file mode 100644 index 0000000..363a50c --- /dev/null +++ b/components/rmaker_common/src/time.c @@ -0,0 +1,277 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) +#include +#else +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +static const char *TAG = "esp_rmaker_time"; + +#define ESP_RMAKER_NVS_PART_NAME "nvs" + +#define ESP_RMAKER_NVS_TIME_NAMESPACE "rmaker_time" +#define ESP_RMAKER_TZ_POSIX_NVS_NAME "tz_posix" +#define ESP_RMAKER_TZ_NVS_NAME "tz" + +#define REF_TIME 1546300800 /* 01-Jan-2019 00:00:00 */ +static bool init_done = false; +extern const char *esp_rmaker_tz_db_get_posix_str(const char *name); + +#define ESP_RMAKER_DEF_TZ CONFIG_ESP_RMAKER_DEF_TIMEZONE + +int esp_setenv(const char *name, const char *value, int rewrite) +{ +/* IDF version lower than v4.4.3 not support parse bracket POSIX TZ in newlib. + Wrap setenv function to convert bracket POSIX TZ, such as <+08>-8 to TZ-08 */ +#if ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 3) +#define ESP_TZNAME_MIN 3 +#define ESP_TZNAME_MAX 10 + if (value) { + const char *tzenv = value; + if (*tzenv == '<') { + ++ tzenv; + char tzname[ESP_TZNAME_MIN + 1] = {0}; + char real_value[6] = {0}; + int n = 0; + if (sscanf(tzenv, "%10[-+0-9A-Za-z]%n", tzname, &n) <= 0 || n < ESP_TZNAME_MIN || n > ESP_TZNAME_MAX || '>' != tzenv[n]) { + ESP_LOGW(TAG, "Failed to convert Posix TZ %s", value); + goto exit; + } + tzname[0] = (tzname[0] == '-') ? '+' : '-'; + sprintf(real_value, "TZ%s", tzname); + ESP_LOGI(TAG, "Real Posix TZ is %s", real_value); + return setenv(name, real_value, rewrite); + } + } +exit: +#endif + return setenv(name, value, rewrite); +} + +esp_err_t esp_rmaker_get_local_time_str(char *buf, size_t buf_len) +{ + struct tm timeinfo; + char strftime_buf[64]; + time_t now; + time(&now); + localtime_r(&now, &timeinfo); + strftime(strftime_buf, sizeof(strftime_buf), "%c %z[%Z]", &timeinfo); + size_t print_size = snprintf(buf, buf_len, "%s, DST: %s", strftime_buf, timeinfo.tm_isdst ? "Yes" : "No"); + if (print_size >= buf_len) { + ESP_LOGE(TAG, "Buffer size %d insufficient for localtime string. Required size: %d", buf_len, print_size); + return ESP_ERR_INVALID_ARG; + } + return ESP_OK; +} + +static esp_err_t esp_rmaker_print_current_time(void) +{ + char local_time[64]; + if (esp_rmaker_get_local_time_str(local_time, sizeof(local_time)) == ESP_OK) { + if (esp_rmaker_time_check() == false) { + ESP_LOGI(TAG, "Time not synchronised yet."); + } + ESP_LOGI(TAG, "The current time is: %s.", local_time); + return ESP_OK; + } + return ESP_FAIL; +} + +static char *__esp_rmaker_time_get_nvs(const char *key) +{ + char *val = NULL; + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, ESP_RMAKER_NVS_TIME_NAMESPACE, NVS_READONLY, &handle); + if (err != ESP_OK) { + return NULL; + } + size_t len = 0; + if ((err = nvs_get_blob(handle, key, NULL, &len)) == ESP_OK) { + val = MEM_CALLOC_EXTRAM(1, len + 1); /* +1 for NULL termination */ + if (val) { + nvs_get_blob(handle, key, val, &len); + } + } + nvs_close(handle); + return val; + +} + +static esp_err_t __esp_rmaker_time_set_nvs(const char *key, const char *val) +{ + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, ESP_RMAKER_NVS_TIME_NAMESPACE, NVS_READWRITE, &handle); + if (err != ESP_OK) { + return err; + } + err = nvs_set_blob(handle, key, val, strlen(val)); + nvs_commit(handle); + nvs_close(handle); + return err; +} + +char *esp_rmaker_time_get_timezone_posix(void) +{ + return __esp_rmaker_time_get_nvs(ESP_RMAKER_TZ_POSIX_NVS_NAME); +} + +char *esp_rmaker_time_get_timezone(void) +{ + return __esp_rmaker_time_get_nvs(ESP_RMAKER_TZ_NVS_NAME); +} + +esp_err_t esp_rmaker_time_set_timezone_posix(const char *tz_posix) +{ + esp_err_t err = __esp_rmaker_time_set_nvs(ESP_RMAKER_TZ_POSIX_NVS_NAME, tz_posix); + if (err == ESP_OK) { + esp_setenv("TZ", tz_posix, 1); + tzset(); + esp_event_post(RMAKER_COMMON_EVENT, RMAKER_EVENT_TZ_POSIX_CHANGED, + (void *)tz_posix, strlen(tz_posix) + 1, portMAX_DELAY); + esp_rmaker_print_current_time(); + } + return err; +} + +esp_err_t esp_rmaker_time_set_timezone(const char *tz) +{ + const char *tz_posix = esp_rmaker_tz_db_get_posix_str(tz); + if (!tz_posix) { + return ESP_ERR_INVALID_ARG; + } + esp_err_t err = esp_rmaker_time_set_timezone_posix(tz_posix); + if (err == ESP_OK) { + esp_event_post(RMAKER_COMMON_EVENT, RMAKER_EVENT_TZ_CHANGED, (void *)tz, + strlen(tz) + 1, portMAX_DELAY); + err = __esp_rmaker_time_set_nvs(ESP_RMAKER_TZ_NVS_NAME, tz); + } + return err; +} + +esp_err_t esp_rmaker_timezone_enable(void) +{ + char *tz_posix = esp_rmaker_time_get_timezone_posix(); + if (tz_posix) { + esp_setenv("TZ", tz_posix, 1); + tzset(); + free(tz_posix); + } else { + if (strlen(ESP_RMAKER_DEF_TZ) > 0) { + const char *tz_def = esp_rmaker_tz_db_get_posix_str(ESP_RMAKER_DEF_TZ); + if (tz_def) { + esp_setenv("TZ", tz_def, 1); + tzset(); + return ESP_OK; + } else { + ESP_LOGE(TAG, "Invalid Timezone %s specified.", ESP_RMAKER_DEF_TZ); + return ESP_ERR_INVALID_ARG; + } + } + } + return ESP_OK; +} +static void esp_rmaker_time_sync_cb(struct timeval *tv) +{ + ESP_LOGI(TAG, "SNTP Synchronised."); + esp_rmaker_print_current_time(); +} + +esp_err_t esp_rmaker_time_sync_init(esp_rmaker_time_config_t *config) +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) + if (esp_sntp_enabled()) { +#else + if (sntp_enabled()) { +#endif + ESP_LOGI(TAG, "SNTP already initialized."); + init_done = true; + return ESP_OK; + } + char *sntp_server_name; + if (!config || !config->sntp_server_name) { + sntp_server_name = CONFIG_ESP_RMAKER_SNTP_SERVER_NAME; + } else { + sntp_server_name = config->sntp_server_name; + } + ESP_LOGI(TAG, "Initializing SNTP. Using the SNTP server: %s", sntp_server_name); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) + esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); + esp_sntp_setservername(0, sntp_server_name); + esp_sntp_init(); +#else + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_setservername(0, sntp_server_name); + sntp_init(); +#endif + if (config && config->sync_time_cb) { + sntp_set_time_sync_notification_cb(config->sync_time_cb); + } else { + sntp_set_time_sync_notification_cb(esp_rmaker_time_sync_cb); + } + esp_rmaker_timezone_enable(); + init_done = true; + return ESP_OK; +} + +bool esp_rmaker_time_check(void) +{ + time_t now; + time(&now); + if (now > REF_TIME) { + return true; + } + return false; +} + +#define DEFAULT_TICKS (2000 / portTICK_PERIOD_MS) /* 2 seconds in ticks */ + +esp_err_t esp_rmaker_time_wait_for_sync(uint32_t ticks_to_wait) +{ + if (!init_done) { + ESP_LOGW(TAG, "Time sync not initialized using 'esp_rmaker_time_sync_init'"); + } + ESP_LOGW(TAG, "Waiting for time to be synchronized. This may take time."); + uint32_t ticks_remaining = ticks_to_wait; + uint32_t ticks = DEFAULT_TICKS; + while (ticks_remaining > 0) { + if (esp_rmaker_time_check() == true) { + break; + } + ESP_LOGD(TAG, "Time not synchronized yet. Retrying..."); + ticks = ticks_remaining < DEFAULT_TICKS ? ticks_remaining : DEFAULT_TICKS; + ticks_remaining -= ticks; + vTaskDelay(ticks); + } + + /* Check if ticks_to_wait expired and time is not synchronized yet. */ + if (esp_rmaker_time_check() == false) { + ESP_LOGE(TAG, "Time not synchronized within the provided ticks: %" PRIu32, ticks_to_wait); + return ESP_FAIL; + } + + /* Get current time */ + esp_rmaker_print_current_time(); + return ESP_OK; +} diff --git a/components/rmaker_common/src/timezone.c b/components/rmaker_common/src/timezone.c new file mode 100644 index 0000000..e69eed1 --- /dev/null +++ b/components/rmaker_common/src/timezone.c @@ -0,0 +1,516 @@ +// MIT License +// +// Copyright (c) 2020 Nayar Systems +// Copyright (c) 2020 Jacob Lambert +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// 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. +// +// Original code taken from: https://github.com/jdlambert/micro_tz_db +// which was forked from https://github.com/nayarsystems/posix_tz_db + +#include + +typedef struct { + const char *name; + const char *posix_str; +} esp_rmaker_tz_db_pair_t; + +static const esp_rmaker_tz_db_pair_t esp_rmaker_tz_db_tzs[] = { + {"Africa/Abidjan", "GMT0"}, + {"Africa/Accra", "GMT0"}, + {"Africa/Addis_Ababa", "EAT-3"}, + {"Africa/Algiers", "CET-1"}, + {"Africa/Asmara", "EAT-3"}, + {"Africa/Bamako", "GMT0"}, + {"Africa/Bangui", "WAT-1"}, + {"Africa/Banjul", "GMT0"}, + {"Africa/Bissau", "GMT0"}, + {"Africa/Blantyre", "CAT-2"}, + {"Africa/Brazzaville", "WAT-1"}, + {"Africa/Bujumbura", "CAT-2"}, + {"Africa/Cairo", "EET-2"}, + {"Africa/Casablanca", "<+01>-1"}, + {"Africa/Ceuta", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Africa/Conakry", "GMT0"}, + {"Africa/Dakar", "GMT0"}, + {"Africa/Dar_es_Salaam", "EAT-3"}, + {"Africa/Djibouti", "EAT-3"}, + {"Africa/Douala", "WAT-1"}, + {"Africa/El_Aaiun", "<+01>-1"}, + {"Africa/Freetown", "GMT0"}, + {"Africa/Gaborone", "CAT-2"}, + {"Africa/Harare", "CAT-2"}, + {"Africa/Johannesburg", "SAST-2"}, + {"Africa/Juba", "EAT-3"}, + {"Africa/Kampala", "EAT-3"}, + {"Africa/Khartoum", "CAT-2"}, + {"Africa/Kigali", "CAT-2"}, + {"Africa/Kinshasa", "WAT-1"}, + {"Africa/Lagos", "WAT-1"}, + {"Africa/Libreville", "WAT-1"}, + {"Africa/Lome", "GMT0"}, + {"Africa/Luanda", "WAT-1"}, + {"Africa/Lubumbashi", "CAT-2"}, + {"Africa/Lusaka", "CAT-2"}, + {"Africa/Malabo", "WAT-1"}, + {"Africa/Maputo", "CAT-2"}, + {"Africa/Maseru", "SAST-2"}, + {"Africa/Mbabane", "SAST-2"}, + {"Africa/Mogadishu", "EAT-3"}, + {"Africa/Monrovia", "GMT0"}, + {"Africa/Nairobi", "EAT-3"}, + {"Africa/Ndjamena", "WAT-1"}, + {"Africa/Niamey", "WAT-1"}, + {"Africa/Nouakchott", "GMT0"}, + {"Africa/Ouagadougou", "GMT0"}, + {"Africa/Porto-Novo", "WAT-1"}, + {"Africa/Sao_Tome", "GMT0"}, + {"Africa/Tripoli", "EET-2"}, + {"Africa/Tunis", "CET-1"}, + {"Africa/Windhoek", "CAT-2"}, + {"America/Adak", "HST10HDT,M3.2.0,M11.1.0"}, + {"America/Anchorage", "AKST9AKDT,M3.2.0,M11.1.0"}, + {"America/Anguilla", "AST4"}, + {"America/Antigua", "AST4"}, + {"America/Araguaina", "<-03>3"}, + {"America/Argentina/Buenos_Aires", "<-03>3"}, + {"America/Argentina/Catamarca", "<-03>3"}, + {"America/Argentina/Cordoba", "<-03>3"}, + {"America/Argentina/Jujuy", "<-03>3"}, + {"America/Argentina/La_Rioja", "<-03>3"}, + {"America/Argentina/Mendoza", "<-03>3"}, + {"America/Argentina/Rio_Gallegos", "<-03>3"}, + {"America/Argentina/Salta", "<-03>3"}, + {"America/Argentina/San_Juan", "<-03>3"}, + {"America/Argentina/San_Luis", "<-03>3"}, + {"America/Argentina/Tucuman", "<-03>3"}, + {"America/Argentina/Ushuaia", "<-03>3"}, + {"America/Aruba", "AST4"}, + {"America/Asuncion", "<-04>4<-03>,M10.1.0/0,M3.4.0/0"}, + {"America/Atikokan", "EST5"}, + {"America/Bahia", "<-03>3"}, + {"America/Bahia_Banderas", "CST6CDT,M4.1.0,M10.5.0"}, + {"America/Barbados", "AST4"}, + {"America/Belem", "<-03>3"}, + {"America/Belize", "CST6"}, + {"America/Blanc-Sablon", "AST4"}, + {"America/Boa_Vista", "<-04>4"}, + {"America/Bogota", "<-05>5"}, + {"America/Boise", "MST7MDT,M3.2.0,M11.1.0"}, + {"America/Cambridge_Bay", "MST7MDT,M3.2.0,M11.1.0"}, + {"America/Campo_Grande", "<-04>4"}, + {"America/Cancun", "EST5"}, + {"America/Caracas", "<-04>4"}, + {"America/Cayenne", "<-03>3"}, + {"America/Cayman", "EST5"}, + {"America/Chicago", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Chihuahua", "MST7MDT,M4.1.0,M10.5.0"}, + {"America/Costa_Rica", "CST6"}, + {"America/Creston", "MST7"}, + {"America/Cuiaba", "<-04>4"}, + {"America/Curacao", "AST4"}, + {"America/Danmarkshavn", "GMT0"}, + {"America/Dawson", "MST7"}, + {"America/Dawson_Creek", "MST7"}, + {"America/Denver", "MST7MDT,M3.2.0,M11.1.0"}, + {"America/Detroit", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Dominica", "AST4"}, + {"America/Edmonton", "MST7MDT,M3.2.0,M11.1.0"}, + {"America/Eirunepe", "<-05>5"}, + {"America/El_Salvador", "CST6"}, + {"America/Fortaleza", "<-03>3"}, + {"America/Fort_Nelson", "MST7"}, + {"America/Glace_Bay", "AST4ADT,M3.2.0,M11.1.0"}, + {"America/Godthab", "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1"}, + {"America/Goose_Bay", "AST4ADT,M3.2.0,M11.1.0"}, + {"America/Grand_Turk", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Grenada", "AST4"}, + {"America/Guadeloupe", "AST4"}, + {"America/Guatemala", "CST6"}, + {"America/Guayaquil", "<-05>5"}, + {"America/Guyana", "<-04>4"}, + {"America/Halifax", "AST4ADT,M3.2.0,M11.1.0"}, + {"America/Havana", "CST5CDT,M3.2.0/0,M11.1.0/1"}, + {"America/Hermosillo", "MST7"}, + {"America/Indiana/Indianapolis", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Indiana/Knox", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Indiana/Marengo", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Indiana/Petersburg", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Indiana/Tell_City", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Indiana/Vevay", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Indiana/Vincennes", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Indiana/Winamac", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Inuvik", "MST7MDT,M3.2.0,M11.1.0"}, + {"America/Iqaluit", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Jamaica", "EST5"}, + {"America/Juneau", "AKST9AKDT,M3.2.0,M11.1.0"}, + {"America/Kentucky/Louisville", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Kentucky/Monticello", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Kralendijk", "AST4"}, + {"America/La_Paz", "<-04>4"}, + {"America/Lima", "<-05>5"}, + {"America/Los_Angeles", "PST8PDT,M3.2.0,M11.1.0"}, + {"America/Lower_Princes", "AST4"}, + {"America/Maceio", "<-03>3"}, + {"America/Managua", "CST6"}, + {"America/Manaus", "<-04>4"}, + {"America/Marigot", "AST4"}, + {"America/Martinique", "AST4"}, + {"America/Matamoros", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Mazatlan", "MST7MDT,M4.1.0,M10.5.0"}, + {"America/Menominee", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Merida", "CST6CDT,M4.1.0,M10.5.0"}, + {"America/Metlakatla", "AKST9AKDT,M3.2.0,M11.1.0"}, + {"America/Mexico_City", "CST6CDT,M4.1.0,M10.5.0"}, + {"America/Miquelon", "<-03>3<-02>,M3.2.0,M11.1.0"}, + {"America/Moncton", "AST4ADT,M3.2.0,M11.1.0"}, + {"America/Monterrey", "CST6CDT,M4.1.0,M10.5.0"}, + {"America/Montevideo", "<-03>3"}, + {"America/Montreal", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Montserrat", "AST4"}, + {"America/Nassau", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/New_York", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Nipigon", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Nome", "AKST9AKDT,M3.2.0,M11.1.0"}, + {"America/Noronha", "<-02>2"}, + {"America/North_Dakota/Beulah", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/North_Dakota/Center", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/North_Dakota/New_Salem", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Ojinaga", "MST7MDT,M3.2.0,M11.1.0"}, + {"America/Panama", "EST5"}, + {"America/Pangnirtung", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Paramaribo", "<-03>3"}, + {"America/Phoenix", "MST7"}, + {"America/Port-au-Prince", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Port_of_Spain", "AST4"}, + {"America/Porto_Velho", "<-04>4"}, + {"America/Puerto_Rico", "AST4"}, + {"America/Punta_Arenas", "<-03>3"}, + {"America/Rainy_River", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Rankin_Inlet", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Recife", "<-03>3"}, + {"America/Regina", "CST6"}, + {"America/Resolute", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Rio_Branco", "<-05>5"}, + {"America/Santarem", "<-03>3"}, + {"America/Santiago", "<-04>4<-03>,M9.1.6/24,M4.1.6/24"}, + {"America/Santo_Domingo", "AST4"}, + {"America/Sao_Paulo", "<-03>3"}, + {"America/Scoresbysund", "<-01>1<+00>,M3.5.0/0,M10.5.0/1"}, + {"America/Sitka", "AKST9AKDT,M3.2.0,M11.1.0"}, + {"America/St_Barthelemy", "AST4"}, + {"America/St_Johns", "NST3:30NDT,M3.2.0,M11.1.0"}, + {"America/St_Kitts", "AST4"}, + {"America/St_Lucia", "AST4"}, + {"America/St_Thomas", "AST4"}, + {"America/St_Vincent", "AST4"}, + {"America/Swift_Current", "CST6"}, + {"America/Tegucigalpa", "CST6"}, + {"America/Thule", "AST4ADT,M3.2.0,M11.1.0"}, + {"America/Thunder_Bay", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Tijuana", "PST8PDT,M3.2.0,M11.1.0"}, + {"America/Toronto", "EST5EDT,M3.2.0,M11.1.0"}, + {"America/Tortola", "AST4"}, + {"America/Vancouver", "PST8PDT,M3.2.0,M11.1.0"}, + {"America/Whitehorse", "MST7"}, + {"America/Winnipeg", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Yakutat", "AKST9AKDT,M3.2.0,M11.1.0"}, + {"America/Yellowknife", "MST7MDT,M3.2.0,M11.1.0"}, + {"Antarctica/Casey", "<+08>-8"}, + {"Antarctica/Davis", "<+07>-7"}, + {"Antarctica/DumontDUrville", "<+10>-10"}, + {"Antarctica/Macquarie", "<+11>-11"}, + {"Antarctica/Mawson", "<+05>-5"}, + {"Antarctica/McMurdo", "NZST-12NZDT,M9.5.0,M4.1.0/3"}, + {"Antarctica/Palmer", "<-03>3"}, + {"Antarctica/Rothera", "<-03>3"}, + {"Antarctica/Syowa", "<+03>-3"}, + {"Antarctica/Troll", "<+00>0<+02>-2,M3.5.0/1,M10.5.0/3"}, + {"Antarctica/Vostok", "<+06>-6"}, + {"Arctic/Longyearbyen", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Asia/Aden", "<+03>-3"}, + {"Asia/Almaty", "<+06>-6"}, + {"Asia/Amman", "EET-2EEST,M3.5.4/24,M10.5.5/1"}, + {"Asia/Anadyr", "<+12>-12"}, + {"Asia/Aqtau", "<+05>-5"}, + {"Asia/Aqtobe", "<+05>-5"}, + {"Asia/Ashgabat", "<+05>-5"}, + {"Asia/Atyrau", "<+05>-5"}, + {"Asia/Baghdad", "<+03>-3"}, + {"Asia/Bahrain", "<+03>-3"}, + {"Asia/Baku", "<+04>-4"}, + {"Asia/Bangkok", "<+07>-7"}, + {"Asia/Barnaul", "<+07>-7"}, + {"Asia/Beirut", "EET-2EEST,M3.5.0/0,M10.5.0/0"}, + {"Asia/Bishkek", "<+06>-6"}, + {"Asia/Brunei", "<+08>-8"}, + {"Asia/Chita", "<+09>-9"}, + {"Asia/Choibalsan", "<+08>-8"}, + {"Asia/Colombo", "<+0530>-5:30"}, + {"Asia/Damascus", "EET-2EEST,M3.5.5/0,M10.5.5/0"}, + {"Asia/Dhaka", "<+06>-6"}, + {"Asia/Dili", "<+09>-9"}, + {"Asia/Dubai", "<+04>-4"}, + {"Asia/Dushanbe", "<+05>-5"}, + {"Asia/Famagusta", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Asia/Gaza", "EET-2EEST,M3.5.5/0,M10.5.6/1"}, + {"Asia/Hebron", "EET-2EEST,M3.5.5/0,M10.5.6/1"}, + {"Asia/Ho_Chi_Minh", "<+07>-7"}, + {"Asia/Hong_Kong", "HKT-8"}, + {"Asia/Hovd", "<+07>-7"}, + {"Asia/Irkutsk", "<+08>-8"}, + {"Asia/Jakarta", "WIB-7"}, + {"Asia/Jayapura", "WIT-9"}, + {"Asia/Jerusalem", "IST-2IDT,M3.4.4/26,M10.5.0"}, + {"Asia/Kabul", "<+0430>-4:30"}, + {"Asia/Kamchatka", "<+12>-12"}, + {"Asia/Karachi", "PKT-5"}, + {"Asia/Kathmandu", "<+0545>-5:45"}, + {"Asia/Khandyga", "<+09>-9"}, + {"Asia/Kolkata", "IST-5:30"}, + {"Asia/Krasnoyarsk", "<+07>-7"}, + {"Asia/Kuala_Lumpur", "<+08>-8"}, + {"Asia/Kuching", "<+08>-8"}, + {"Asia/Kuwait", "<+03>-3"}, + {"Asia/Macau", "CST-8"}, + {"Asia/Magadan", "<+11>-11"}, + {"Asia/Makassar", "WITA-8"}, + {"Asia/Manila", "PST-8"}, + {"Asia/Muscat", "<+04>-4"}, + {"Asia/Nicosia", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Asia/Novokuznetsk", "<+07>-7"}, + {"Asia/Novosibirsk", "<+07>-7"}, + {"Asia/Omsk", "<+06>-6"}, + {"Asia/Oral", "<+05>-5"}, + {"Asia/Phnom_Penh", "<+07>-7"}, + {"Asia/Pontianak", "WIB-7"}, + {"Asia/Pyongyang", "KST-9"}, + {"Asia/Qatar", "<+03>-3"}, + {"Asia/Qyzylorda", "<+05>-5"}, + {"Asia/Riyadh", "<+03>-3"}, + {"Asia/Sakhalin", "<+11>-11"}, + {"Asia/Samarkand", "<+05>-5"}, + {"Asia/Seoul", "KST-9"}, + {"Asia/Shanghai", "CST-8"}, + {"Asia/Singapore", "<+08>-8"}, + {"Asia/Srednekolymsk", "<+11>-11"}, + {"Asia/Taipei", "CST-8"}, + {"Asia/Tashkent", "<+05>-5"}, + {"Asia/Tbilisi", "<+04>-4"}, + {"Asia/Tehran", "<+0330>-3:30<+0430>,J79/24,J263/24"}, + {"Asia/Thimphu", "<+06>-6"}, + {"Asia/Tokyo", "JST-9"}, + {"Asia/Tomsk", "<+07>-7"}, + {"Asia/Ulaanbaatar", "<+08>-8"}, + {"Asia/Urumqi", "<+06>-6"}, + {"Asia/Ust-Nera", "<+10>-10"}, + {"Asia/Vientiane", "<+07>-7"}, + {"Asia/Vladivostok", "<+10>-10"}, + {"Asia/Yakutsk", "<+09>-9"}, + {"Asia/Yangon", "<+0630>-6:30"}, + {"Asia/Yekaterinburg", "<+05>-5"}, + {"Asia/Yerevan", "<+04>-4"}, + {"Atlantic/Azores", "<-01>1<+00>,M3.5.0/0,M10.5.0/1"}, + {"Atlantic/Bermuda", "AST4ADT,M3.2.0,M11.1.0"}, + {"Atlantic/Canary", "WET0WEST,M3.5.0/1,M10.5.0"}, + {"Atlantic/Cape_Verde", "<-01>1"}, + {"Atlantic/Faroe", "WET0WEST,M3.5.0/1,M10.5.0"}, + {"Atlantic/Madeira", "WET0WEST,M3.5.0/1,M10.5.0"}, + {"Atlantic/Reykjavik", "GMT0"}, + {"Atlantic/South_Georgia", "<-02>2"}, + {"Atlantic/Stanley", "<-03>3"}, + {"Atlantic/St_Helena", "GMT0"}, + {"Australia/Adelaide", "ACST-9:30ACDT,M10.1.0,M4.1.0/3"}, + {"Australia/Brisbane", "AEST-10"}, + {"Australia/Broken_Hill", "ACST-9:30ACDT,M10.1.0,M4.1.0/3"}, + {"Australia/Currie", "AEST-10AEDT,M10.1.0,M4.1.0/3"}, + {"Australia/Darwin", "ACST-9:30"}, + {"Australia/Eucla", "<+0845>-8:45"}, + {"Australia/Hobart", "AEST-10AEDT,M10.1.0,M4.1.0/3"}, + {"Australia/Lindeman", "AEST-10"}, + {"Australia/Lord_Howe", "<+1030>-10:30<+11>-11,M10.1.0,M4.1.0"}, + {"Australia/Melbourne", "AEST-10AEDT,M10.1.0,M4.1.0/3"}, + {"Australia/Perth", "AWST-8"}, + {"Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3"}, + {"Europe/Amsterdam", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Andorra", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Astrakhan", "<+04>-4"}, + {"Europe/Athens", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Belgrade", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Berlin", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Bratislava", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Brussels", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Bucharest", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Budapest", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Busingen", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Chisinau", "EET-2EEST,M3.5.0,M10.5.0/3"}, + {"Europe/Copenhagen", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Dublin", "IST-1GMT0,M10.5.0,M3.5.0/1"}, + {"Europe/Gibraltar", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Guernsey", "GMT0BST,M3.5.0/1,M10.5.0"}, + {"Europe/Helsinki", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Isle_of_Man", "GMT0BST,M3.5.0/1,M10.5.0"}, + {"Europe/Istanbul", "<+03>-3"}, + {"Europe/Jersey", "GMT0BST,M3.5.0/1,M10.5.0"}, + {"Europe/Kaliningrad", "EET-2"}, + {"Europe/Kiev", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Kirov", "<+03>-3"}, + {"Europe/Lisbon", "WET0WEST,M3.5.0/1,M10.5.0"}, + {"Europe/Ljubljana", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/London", "GMT0BST,M3.5.0/1,M10.5.0"}, + {"Europe/Luxembourg", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Madrid", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Malta", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Mariehamn", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Minsk", "<+03>-3"}, + {"Europe/Monaco", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Moscow", "MSK-3"}, + {"Europe/Oslo", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Paris", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Podgorica", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Prague", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Riga", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Rome", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Samara", "<+04>-4"}, + {"Europe/San_Marino", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Sarajevo", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Saratov", "<+04>-4"}, + {"Europe/Simferopol", "MSK-3"}, + {"Europe/Skopje", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Sofia", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Stockholm", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Tallinn", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Tirane", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Ulyanovsk", "<+04>-4"}, + {"Europe/Uzhgorod", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Vaduz", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Vatican", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Vienna", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Vilnius", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Volgograd", "<+04>-4"}, + {"Europe/Warsaw", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Zagreb", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Europe/Zaporozhye", "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Europe/Zurich", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Indian/Antananarivo", "EAT-3"}, + {"Indian/Chagos", "<+06>-6"}, + {"Indian/Christmas", "<+07>-7"}, + {"Indian/Cocos", "<+0630>-6:30"}, + {"Indian/Comoro", "EAT-3"}, + {"Indian/Kerguelen", "<+05>-5"}, + {"Indian/Mahe", "<+04>-4"}, + {"Indian/Maldives", "<+05>-5"}, + {"Indian/Mauritius", "<+04>-4"}, + {"Indian/Mayotte", "EAT-3"}, + {"Indian/Reunion", "<+04>-4"}, + {"Pacific/Apia", "<+13>-13<+14>,M9.5.0/3,M4.1.0/4"}, + {"Pacific/Auckland", "NZST-12NZDT,M9.5.0,M4.1.0/3"}, + {"Pacific/Bougainville", "<+11>-11"}, + {"Pacific/Chatham", "<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45"}, + {"Pacific/Chuuk", "<+10>-10"}, + {"Pacific/Easter", "<-06>6<-05>,M9.1.6/22,M4.1.6/22"}, + {"Pacific/Efate", "<+11>-11"}, + {"Pacific/Enderbury", "<+13>-13"}, + {"Pacific/Fakaofo", "<+13>-13"}, + {"Pacific/Fiji", "<+12>-12<+13>,M11.2.0,M1.2.3/99"}, + {"Pacific/Funafuti", "<+12>-12"}, + {"Pacific/Galapagos", "<-06>6"}, + {"Pacific/Gambier", "<-09>9"}, + {"Pacific/Guadalcanal", "<+11>-11"}, + {"Pacific/Guam", "ChST-10"}, + {"Pacific/Honolulu", "HST10"}, + {"Pacific/Kiritimati", "<+14>-14"}, + {"Pacific/Kosrae", "<+11>-11"}, + {"Pacific/Kwajalein", "<+12>-12"}, + {"Pacific/Majuro", "<+12>-12"}, + {"Pacific/Marquesas", "<-0930>9:30"}, + {"Pacific/Midway", "SST11"}, + {"Pacific/Nauru", "<+12>-12"}, + {"Pacific/Niue", "<-11>11"}, + {"Pacific/Norfolk", "<+11>-11<+12>,M10.1.0,M4.1.0/3"}, + {"Pacific/Noumea", "<+11>-11"}, + {"Pacific/Pago_Pago", "SST11"}, + {"Pacific/Palau", "<+09>-9"}, + {"Pacific/Pitcairn", "<-08>8"}, + {"Pacific/Pohnpei", "<+11>-11"}, + {"Pacific/Port_Moresby", "<+10>-10"}, + {"Pacific/Rarotonga", "<-10>10"}, + {"Pacific/Saipan", "ChST-10"}, + {"Pacific/Tahiti", "<-10>10"}, + {"Pacific/Tarawa", "<+12>-12"}, + {"Pacific/Tongatapu", "<+13>-13"}, + {"Pacific/Wake", "<+12>-12"}, + {"Pacific/Wallis", "<+12>-12"} +}; + +static char lower(char start) { + if ('A' <= start && start <= 'Z') { + return start - 'A' + 'a'; + } + return start; +} + +/** + * Basically strcmp, but accounting for spaces that have become underscores + * @param[in] target - the 0-terminated string on the left hand side of the comparison + * @param[in] other - the 0-terminated string on the right hand side of the comparison + * @return > 0 if target comes before other alphabetically, + * ==0 if they're the same, + * < 0 if other comes before target alphabetically + * (we don't expect NULL arguments, but, -1 if either is NULL) + **/ +static int tz_name_cmp(const char * target, const char * other) +{ + if (!target || !other) { + return -1; + } + + while (*target) { + if (lower(*target) != lower(*other)) { + break; + } + do { + target++; + } while (*target == '_'); + do { + other++; + } while (*other == '_'); + } + + return lower(*target) - lower(*other); +} + +const char *esp_rmaker_tz_db_get_posix_str(const char *name) +{ + int lo = 0, hi = sizeof(esp_rmaker_tz_db_tzs) / sizeof(esp_rmaker_tz_db_pair_t); + while (lo < hi) { + int mid = (lo + hi) / 2; + esp_rmaker_tz_db_pair_t mid_pair = esp_rmaker_tz_db_tzs[mid]; + int comparison = tz_name_cmp(name, mid_pair.name); + if (comparison == 0) { + return mid_pair.posix_str; + } else if (comparison < 0) { + hi = mid; + } else { + lo = mid + 1; + } + } + return NULL; +} diff --git a/components/rmaker_common/src/utils.c b/components/rmaker_common/src/utils.c new file mode 100644 index 0000000..36c33ba --- /dev/null +++ b/components/rmaker_common/src/utils.c @@ -0,0 +1,174 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include +#include +#include +#include +#include + +ESP_EVENT_DEFINE_BASE(RMAKER_COMMON_EVENT); + +static TimerHandle_t reboot_timer; +static TimerHandle_t reset_timer; + +static void esp_rmaker_reboot_cb(TimerHandle_t handle) +{ + xTimerDelete(reboot_timer, 10); + reboot_timer = NULL; + esp_restart(); +} + +esp_err_t esp_rmaker_reboot(int8_t seconds) +{ + /* If specified time is 0, reboot immediately */ + if (seconds == 0) { + esp_event_post(RMAKER_COMMON_EVENT, RMAKER_EVENT_REBOOT, &seconds, sizeof(seconds), portMAX_DELAY); + esp_restart(); + return ESP_OK; + } else if (reboot_timer) { + /* If reboot timer already exists, it means that a reboot operation is already in progress. + * So, just return an error from here. + */ + return ESP_FAIL; + } + reboot_timer = xTimerCreate("rmaker_reboot_tm", (seconds * 1000) / portTICK_PERIOD_MS, + pdFALSE, NULL, esp_rmaker_reboot_cb); + if (reboot_timer) { + if (xTimerStart(reboot_timer, 10) != pdTRUE) { + xTimerDelete(reboot_timer, 10); + reboot_timer = NULL; + return ESP_FAIL; + } + } else { + return ESP_ERR_NO_MEM; + } + esp_event_post(RMAKER_COMMON_EVENT, RMAKER_EVENT_REBOOT, &seconds, sizeof(seconds), portMAX_DELAY); + return ESP_OK; +} + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +static esp_err_t __esp_rmaker_wifi_reset(int8_t reboot_seconds) +{ + esp_wifi_restore(); + if (reboot_seconds >= 0) { + esp_rmaker_reboot(reboot_seconds); + } + return ESP_OK; +} +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + +static esp_err_t __esp_rmaker_factory_reset(int8_t reboot_seconds) +{ + nvs_flash_deinit(); + nvs_flash_erase(); + if (reboot_seconds >= 0) { + esp_rmaker_reboot(reboot_seconds); + } + return ESP_OK; +} + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +static void esp_rmaker_wifi_reset_cb(TimerHandle_t handle) +{ + /* (Hack) Using the timer id as reboot seconds */ + int8_t *reboot_seconds = (int8_t *)pvTimerGetTimerID(handle); + if (reboot_seconds) { + __esp_rmaker_wifi_reset((int8_t)*reboot_seconds); + free(reboot_seconds); + } else { + __esp_rmaker_wifi_reset(0); + } + xTimerDelete(reset_timer, 10); + reset_timer = NULL; +} +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + +static void esp_rmaker_factory_reset_cb(TimerHandle_t handle) +{ + /* (Hack) Using the timer id as reboot seconds */ + int8_t *reboot_seconds = (int8_t *)pvTimerGetTimerID(handle); + if (reboot_seconds) { + __esp_rmaker_factory_reset((int8_t)*reboot_seconds); + free(reboot_seconds); + } else { + __esp_rmaker_factory_reset(0); + } + xTimerDelete(reset_timer, 10); + reset_timer = NULL; +} + +static esp_err_t esp_rmaker_start_reset_timer(int8_t reset_seconds, int8_t reboot_seconds, TimerCallbackFunction_t cb) +{ + /* If reset timer already exists, it means that a reset operation is already in progress. + * So, just return an error from here. + */ + if (reset_timer) { + return ESP_FAIL; + } + /* (Hack) Passing the reboot delay as timer id */ + int8_t *reboot_delay = calloc(1, sizeof(int8_t)); + if (reboot_delay) { + *reboot_delay = reboot_seconds; + } + reset_timer = xTimerCreate("rmaker_reset_tm", (reset_seconds * 1000) / portTICK_PERIOD_MS, + pdFALSE, (void *)reboot_delay, cb); + if (reset_timer) { + if (xTimerStart(reset_timer, 10) != pdTRUE) { + xTimerDelete(reset_timer, 10); + reset_timer = NULL; + return ESP_FAIL; + } + } else { + return ESP_ERR_NO_MEM; + } + return ESP_OK; +} + +esp_err_t esp_rmaker_wifi_reset(int8_t reset_seconds, int8_t reboot_seconds) +{ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + esp_err_t err = ESP_FAIL; + /* If reset time is 0, just do it right away */ + if (reset_seconds == 0) { + err = __esp_rmaker_wifi_reset(reboot_seconds); + } else { + /* Else start a timer so that the task gets performed after the specified delay */ + err = esp_rmaker_start_reset_timer(reset_seconds, reboot_seconds, esp_rmaker_wifi_reset_cb); + } + if (err == ESP_OK) { + esp_event_post(RMAKER_COMMON_EVENT, RMAKER_EVENT_WIFI_RESET, NULL, 0, portMAX_DELAY); + } + return ESP_OK; +#else + return ESP_ERR_NOT_SUPPORTED; +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ +} + +esp_err_t esp_rmaker_factory_reset(int8_t reset_seconds, int8_t reboot_seconds) +{ + esp_err_t err = ESP_FAIL; + /* If reset time is 0, just do it right away */ + if (reset_seconds == 0) { + err = __esp_rmaker_factory_reset(reboot_seconds); + } else { + /* Else start a timer so that the task gets performed after the specified delay */ + err = esp_rmaker_start_reset_timer(reset_seconds, reboot_seconds, esp_rmaker_factory_reset_cb); + } + if (err == ESP_OK) { + esp_event_post(RMAKER_COMMON_EVENT, RMAKER_EVENT_FACTORY_RESET, NULL, 0, portMAX_DELAY); + } + return ESP_OK; +} diff --git a/components/rmaker_common/src/work_queue.c b/components/rmaker_common/src/work_queue.c new file mode 100644 index 0000000..f00a7cb --- /dev/null +++ b/components/rmaker_common/src/work_queue.c @@ -0,0 +1,148 @@ +// Copyright 2021 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include + +#include + +#define ESP_RMAKER_TASK_QUEUE_SIZE 8 +#define ESP_RMAKER_TASK_STACK CONFIG_ESP_RMAKER_WORK_QUEUE_TASK_STACK +#define ESP_RMAKER_TASK_PRIORITY CONFIG_ESP_RMAKER_WORK_QUEUE_TASK_PRIORITY + +static const char *TAG = "esp_rmaker_work_queue"; + +typedef enum { + WORK_QUEUE_STATE_DEINIT = 0, + WORK_QUEUE_STATE_INIT_DONE, + WORK_QUEUE_STATE_RUNNING, + WORK_QUEUE_STATE_STOP_REQUESTED, +} esp_rmaker_work_queue_state_t; + +typedef struct { + esp_rmaker_work_fn_t work_fn; + void *priv_data; +} esp_rmaker_work_queue_entry_t; + +static QueueHandle_t work_queue; +static esp_rmaker_work_queue_state_t queue_state; + +static void esp_rmaker_handle_work_queue(void) +{ + esp_rmaker_work_queue_entry_t work_queue_entry; + /* 2 sec delay to prevent spinning */ + BaseType_t ret = xQueueReceive(work_queue, &work_queue_entry, 2000 / portTICK_PERIOD_MS); + while (ret == pdTRUE) { + work_queue_entry.work_fn(work_queue_entry.priv_data); + ret = xQueueReceive(work_queue, &work_queue_entry, 0); + } +} + +static void esp_rmaker_work_queue_task(void *param) +{ + ESP_LOGI(TAG, "RainMaker Work Queue task started."); + while (queue_state != WORK_QUEUE_STATE_STOP_REQUESTED) { + esp_rmaker_handle_work_queue(); + } + ESP_LOGI(TAG, "Stopping Work Queue task"); + queue_state = WORK_QUEUE_STATE_INIT_DONE; + vTaskDelete(NULL); +} + +esp_err_t esp_rmaker_work_queue_add_task(esp_rmaker_work_fn_t work_fn, void *priv_data) +{ + if (!work_queue) { + ESP_LOGE(TAG, "Cannot enqueue function as Work Queue hasn't been created."); + return ESP_ERR_INVALID_STATE; + } + esp_rmaker_work_queue_entry_t work_queue_entry = { + .work_fn = work_fn, + .priv_data = priv_data, + }; + if (xQueueSend(work_queue, &work_queue_entry, 0) == pdTRUE) { + return ESP_OK; + } + return ESP_FAIL; +} + +esp_err_t esp_rmaker_work_queue_init(void) +{ + if (queue_state != WORK_QUEUE_STATE_DEINIT) { + ESP_LOGW(TAG, "Work Queue already initialiased/started."); + return ESP_OK; + } + work_queue = xQueueCreate(ESP_RMAKER_TASK_QUEUE_SIZE, sizeof(esp_rmaker_work_queue_entry_t)); + if (!work_queue) { + ESP_LOGE(TAG, "Failed to create Work Queue."); + return ESP_FAIL; + } + ESP_LOGI(TAG, "Work Queue created."); + queue_state = WORK_QUEUE_STATE_INIT_DONE; + return ESP_OK; +} + +esp_err_t esp_rmaker_work_queue_deinit(void) +{ + if (queue_state != WORK_QUEUE_STATE_STOP_REQUESTED) { + esp_rmaker_work_queue_stop(); + } + + while (queue_state == WORK_QUEUE_STATE_STOP_REQUESTED) { + ESP_LOGI(TAG, "Waiting for esp_rmaker_work_queue being stopped..."); + vTaskDelay(2000 / portTICK_PERIOD_MS); + } + + if (queue_state == WORK_QUEUE_STATE_DEINIT) { + return ESP_OK; + } else if (queue_state != WORK_QUEUE_STATE_INIT_DONE) { + ESP_LOGE(TAG, "Cannot deinitialize Work Queue as the task is still running."); + return ESP_ERR_INVALID_STATE; + } else { + vQueueDelete(work_queue); + work_queue = NULL; + queue_state = WORK_QUEUE_STATE_DEINIT; + } + ESP_LOGI(TAG, "esp_rmaker_work_queue was successfully deinitialized"); + return ESP_OK; +} + +esp_err_t esp_rmaker_work_queue_start(void) +{ + if (queue_state == WORK_QUEUE_STATE_RUNNING) { + ESP_LOGW(TAG, "Work Queue already started."); + return ESP_OK; + } + if (queue_state != WORK_QUEUE_STATE_INIT_DONE) { + ESP_LOGE(TAG, "Failed to start Work Queue as it wasn't initialized."); + return ESP_ERR_INVALID_STATE; + } + if (xTaskCreate(&esp_rmaker_work_queue_task, "rmaker_queue_task", ESP_RMAKER_TASK_STACK, + NULL, ESP_RMAKER_TASK_PRIORITY, NULL) != pdPASS) { + ESP_LOGE(TAG, "Couldn't create RainMaker work queue task"); + return ESP_FAIL; + } + queue_state = WORK_QUEUE_STATE_RUNNING; + return ESP_OK; +} + +esp_err_t esp_rmaker_work_queue_stop(void) +{ + if (queue_state == WORK_QUEUE_STATE_RUNNING) { + queue_state = WORK_QUEUE_STATE_STOP_REQUESTED; + } + return ESP_OK; +} diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 0000000..3a7ae81 --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,77 @@ +# This is Doxygen configuration file +# +# Doxygen provides over 260 configuration statements +# To make this file easier to follow, +# it contains only statements that are non-default +# +# NOTE: +# It is recommended not to change defaults unless specifically required +# Test any changes how they affect generated documentation +# Make sure that correct warnings are generated to flag issues with documented code +# +# For the complete list of configuration statements see: +# https://www.stack.nl/~dimitri/doxygen/manual/config.html + + +PROJECT_NAME = "ESP RainMaker Programming Guide" + +## The 'INPUT' statement below is used as input by script 'gen-df-input.py' +## to automatically generate API reference list files heder_file.inc +## These files are placed in '_inc' directory +## and used to include in API reference documentation + +INPUT = \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_core.h \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_user_mapping.h \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_schedule.h \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_scenes.h \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_standard_types.h \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_standard_params.h \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_standard_devices.h \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_standard_services.h \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_ota.h \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_mqtt.h \ + $(PROJECT_PATH)/components/esp_rainmaker/include/esp_rmaker_console.h \ + $(PROJECT_PATH)/components/rmaker_common/include/esp_rmaker_common_events.h \ + $(PROJECT_PATH)/components/rmaker_common/include/esp_rmaker_factory.h \ + $(PROJECT_PATH)/components/rmaker_common/include/esp_rmaker_work_queue.h \ + $(PROJECT_PATH)/components/rmaker_common/include/esp_rmaker_utils.h \ + $(PROJECT_PATH)/components/rmaker_common/include/esp_rmaker_cmd_resp.h \ + $(PROJECT_PATH)/components/rmaker_common/include/esp_rmaker_mqtt_glue.h + +## Get warnings for functions that have no documentation for their parameters or return value +## +WARN_NO_PARAMDOC = YES + +## Enable preprocessing and remove __attribute__(...) expressions from the INPUT files +## +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +PREDEFINED = \ + $(ENV_DOXYGEN_DEFINES) + +## Extract all +EXTRACT_ALL = YES + +## Do not complain about not having dot +## +HAVE_DOT = NO + +## Generate XML that is required for Breathe +## +GENERATE_XML = YES +XML_OUTPUT = xml + +GENERATE_HTML = NO +HAVE_DOT = NO +GENERATE_LATEX = NO +GENERATE_MAN = YES +GENERATE_RTF = NO + +## Skip distracting progress messages +## +QUIET = YES +## Log warnings in a file for further review +## +WARN_LOGFILE = "doxygen-warning-log.txt" diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..b50c24d --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= -W +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..260e60c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ +# Documentation Source Folder + +This folder contains source files of **ESP RainMaker API documentation**. + +# Hosted Documentation + +* Check the following link for the documentation: https://docs.espressif.com/projects/esp-rainmaker/en/latest/ + +The above URL is for the master branch latest version. + +# Building Documentation + +* ESP RainMaker uses esp-docs for building the docs. +* Change to the docs directory and run `build-docs -l en -t esp32` +* To understand more about ESP-Docs, please follow https://docs.espressif.com/projects/esp-docs. diff --git a/docs/_static/esp-rainmaker-logo.png b/docs/_static/esp-rainmaker-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a55ba3a4e152dd1c03bd0f05dcb81700c10acf1d GIT binary patch literal 68568 zcmeEu`9IX_|Gw%-he}bY6ostCu_fyiDOt-dF=fr3Ez4L^S;m^Ojb$WT)(IiY&@%R6 zWE;C0`))7>!}#9QruTpF{o&Iu=iwap{kpH`a$VQ`e7!pG+nP5IF`j0mqoX@?OXa#Y z9UT)Z9o=4|1N*_hd3Xt)2mZ0gRr{vmp1!7w{@{myAE_9*{`@T_);)BI$tUUdF!|oP zeofaid2Z<61QVNUd|L*C3T)Rc@7!@_e;{*fUXQFVIG~X$I3yS;!zK3QZ8-loF~Zb0 z20?tlsXb>sw>}~Pi8EL#Rse1q%>;L}zUj+Uy0{<6*|F0r&6(v(_GnNwWYBbEhrn#Tx6;HrS($OIUH#T( znhpMtQ#-Y+#9I0GldyBt%`XjQ;R_%9SEl&PsS8sR(GdAcn^iIa@1>-^vQ!^E$=Uzk zR#(>wjl6&Uc~bMpZKbi-H|eJTR&U(b@-t51zEj|7L)lq;ZP?Z>4ux{`a1Cz>X{}F4 zyNcJ`x$gk z3SCFyzBZXhd>v6Ja-WOT=^VY;rml6bvH8;^s`({7fh7#Hfa~jyqQL+^dTan`8r6p{RDfS{+_%KX% znF*5k_};TMaD?nAkQfR;loWjVpugJ8_MY86?wxfCN6&#kF{dJOQm*(Ld3_iO!>Eme z7@FUdirSk0uJJ`g4ej)21>HMA@I(;>@Wga$Ydf|$L1rNqD1@)rzBRPYoQ+lkQ{N-F zMKsZ=yK5dPfi<`*V9h?)l2UQzVVOR^M+lZ8E+Q!mV=&npS0sK@D#GVmd;NFojA85D zg$W+jqGcOnWke5fytA;SvZghkV{q3?EFu#DVHHj*Q$^fKK_k8oN0%nLD9nG>T}_o9 zK1u0eHEtMjv9cn33#{8)#N{lYOb84&X$CcNm}B_LR-6)hP^~1%Yd0EKnPK;5d*gHl z>US2l);7N|o0@Rx^9Z9=wyutTJu@J-xdigaNlV0KlGk5(WcC8fJdjA_dNxye52sP_ z^?a4&_bNVH#b?5#D{aPJ1z$c4ks^8R+dUwgY?1p`MRqnHb-a?^Oqrm19FA)Hs=+OL z*y#*)x@+?2pI@aj83XBIOGO|)kkbZu2bcv|JS?NvoB57MamZCWkHzNTgm!;fke18% zw$`pS;kc0F@TyuBAN!Vc+9B4zM|^Qo_eJi)y?vgqyfr?A8R9muS;pn;bOIjba0(p* zIyhOn+IihSLt$}uV-;FAZ6Xl;d~B+iP|Wsio;H`Ax-)$eE=%n}-#OKNId%hbyI7}_ zb+PiD?{W$FO(UKfxnQZoJM(FWzCaP1$^ZP>^!0sE_tmR&Ynw$k)i7us9tCj|WBwh- zq=%{1hFE#OqI*fPR-k|S6)J5qd@VX3>A!a1Og?SP83JMAn3lJ=Iz>jP_JunsvEc_z9J%0NF*%{}JvmN&=0drLgJ{A0JYMQc0HAxFXbV zRrl=~d(Z$3&s^rw(^I<0VeBg4VR;HI=3yFbd_v4cjnav4N(nRCDrNI`dYnIJHGI-kYm zyw)|6&yyrCu>LJ5Z4KGCF1hzLpg(C;wNeH7DlVBcAA9hfB>6sRP}2A6Qztyv5Yl?JS}!Rz)C$EQ=pi1G%l zB1t>LO#y44a?&>VBn&cD`oEpk?`jCcZ1rl)8O}>5P!5Zsv+td@Z{Mqp-Y9<5%tf^H8UwXko~eM_4dI98 zLG^j=)Qals$!}GB%w|0)jrGyA6m2NZ6}G5hnlVx)yrzwxE9Y_#y(gnMZ8`fSUKXqP z2mrkPGv9)~^JOYb?ryg^oHI=vXmdbu-ZJ6=r0G**QR%V$&4)<(%FjA^hqp$qN<`!Y zA3uw=bn$I|1>Z+smcn#WcmBkS&Ubco6*J3G?XWB^5Q+h6Ho^3kk{#4vHM?)6Pml99 zukZ);>JN|mWEK!-9&#q!vn%6W zpe_esGy7KV@D}PLJe>fJa$+7+|{4AV(A6_W2-)uWO_}^AX7k*c`uHt0m<%wT_ zR{6Zk=OQY?J?+!3%SpdGVo6YqhqH-2%F#_e?iVJ^!dVmZeeKhsFHUN{DZ5BklRGqS_#E&&4st7_-^k$&#eHk4TK*Q$m22vC|4Wu zy3a;uNGu#yrCv=TaI`^kJk3Rw2})so|M_r{+;@ZB?ml5<=MS}rUI)Cp7H~)@J`~j6 zwgGQjvDCse=!-X@n6Qc$o8G&$J9T=p3{O6x$D<%`k|YvrYRaY0yD%a~G%Mc5F@B^! z24`Do1ku#e@O9WNP~HAO<5^ebV{n`UsvPjY-69n7-Xhu%@u9W_6Go}>w|!XiU$QG8 zR0g)?vpFcyKiHnAdQX)#-3;YtN$r(UfR15l2L$aQ$ZAbVNnz^S(7p;H0zq!x-POIf zF~Pz@M<>A%#ZBxuhvdX|-uG?rvkI|}TnE*&SmJs?XlLRcPh?Z1Y?q2}>tDzGOl$GW zI4o?O1jl8Qtoh7At+oApYJKudpqO*q_KMcAwoH}k z1f?7@)1U2LOMvFyd1(;6-c?3iON+qAh*uS<`#f&I^g}*)n9aK`o`q7}bIANosHF)JqQ=?I^&yxcFM7`o*U?{2BT8&g*JAD9E2pKIURud;H2@ zaS?%~05$N1X*yDjC>t9RR$^{8^vXjsAEqG^1CJMCS*-P(TesUePpaH)n*3hi6jwMa z9hRfJPr$Y8+5O+E+}jo-LK@_RNQvaI6xP|xbNK}@|MEDvQ<+Nj;#Wl?yADcv_wgDu zgDliGBID&!s!U)gi!?FfDRe}v+pCLDgz2EXApJ(h-YO!9N>3iU20uZyGcUpFVKK#lZb~j;h4AVED{UW4t3Oq^oTAjj)!a=UNh;#WU6k z39;a!9=I$;uftN~y(4y6v+xmUm$YvAXwM>*dwpU=2D18L^$b&}zLkN)H)FG5<8Cn` zLAvTC=raQTO0u`_?owfCAa|dhA`c|U+t7lQS&^7tP3!7I+tM1L&_P;3nLl8irxEKU=HIQ+OY-{zX(v4J@|<%i|KX!1CmjhIqD6P<_@- z^`~mv~Dg{xXVe-LYxkN~SC3WzzW%LK&49InI3ZzQy@F7wFDr)25aUgGj zgd}j4jFjS-4a~tG|FglA4iM5Er)*^cV=R0G=`AnNu(0_tIM23Ev ziyM_j%BU^EgO|M~tiaVhn{OfvbNmu(tuV>^Wj%89BPRyc-A z&(1Q{`g7vfTFC>ZJG0~7r!-VB8@LtLSdeWmX3v=z(p`VB>8 znyT%tWCkn!hg*Rw9J%YnX&0_vs=l=emP*d$Fjwc=S_0~nr&cBb{;OGEBpQmSqT~K7 zz8WQ4>+6DRJq9lU^A4aqUKybv&{?K8b1iXWA7^v*!MXiwZu^YVD$MY3w&~43YhMQg zP1qR&57yGqx_AtpXTVFnI^&*^xTb!%ItM&=Zg2!5`N5v%sxBWyi;-k@iC2CGT+a)^ znQ*N={opF3U!=a3)@7$sI*&Nx1M6G-`Ec^-IlQm|&#)eQW4$*&>GWTx9-Fh@rk4T@Eh#Sw(jQ55+DFSw{kRK0{F)LSX!({N5EBy zIE|tF&-yxGU~$HUK+ANhvQ+K`h!L9-;NZ+oAl6rox)!Sfb7b^s!&{kfQMN>pxcMnn z4ej?QRafI>%F%w4fFk}%)tfH6n@)WI@*5gtXf0(wSPbK3Sj#k({j>5lQwjo+fPR#o z%`zg>N4s-1aXAyji1Ru4$YF5*`!=r!EGixwuudv8py3{6(njrisdh`6@pc9^fIz_4 zwvFS2S@r4j?zaRN_0Y{x_u;Wx9Q)=DCVpCch0H#3r2bv3)FJ%oWE{4Q+t82Vi&oWt>SZjBA~z= z5SNq=ihbL1Wb+eeo;Ko2Rb;Wbh#t?dvnZIACeQcu-*%pKDo8cTRq=6zp?&9afAA-=OqB&7 zj%-LS08f^!*9OT3!pB_i@AqKFeK%f$wvJ_MOlqJtlLs&R0mHP{lHieLD!w@YAd8^D zTD7(JV|hSmLFp`2cG>L?Idr=~TUEsi0wU>(^AU8WZ^oGC;18~$IJd5YIQRJv7_l3^ zQLipTSJgN|!7TVS%Eu3DQ14@oy^jq_a0=U?CUC+x5D8XTTz&O_6BVK(XT@2?Pn8Hy zB$**kt|(_lp7{mx^$0Lj^_<`$9}SLT#L+8bS|>(K(m|cFpgk^!79I=e&J5@`m^rwP z`xwti?t9HA$7^+Ng2AkqZ=~Ezfy31a8=c@PI-l%%d4g5c|Hw-7yW0wdbv|C7TU73@UtMAtAn@ntMX$yzd>)(mu|W zo_9;DV@^%3(VHqWv+oC13`7^1neZJ>b=3+JdZG}{l~**y0szmzUex zH>f*~)9Jznri+JCb=E!#+!G&jbzFUUgMWcKb7@@TnZheyk(@@{em6eY2z0r3(W>v6 zRb6rFNkh!+)+59#!vL<4fsFo~#ID&pnOnE8iw#@k1n9p^{yocY60Cy*A=Z5-Px$4| z=saCKwJ(1dI36~WUQ>UX)(>U^+f=ANmxIx5rjnGjBBixK^dF<=a|^28fIHHaHVsg0 zsxN$BKMWf@7xD4U9J4Qybp>LZu-uaV*>+SB`Fg= z`f}7y2{{RT7fTFQgRE5i4?lKYU}U1FLL)Y!-`&3A*P(iuPG2Bug`Algl~`?INzf$M zayDbU?(sSC!@eCWJS77WxogQ>c|YRz+&0Ux@3s3UFBzO>+?d*4(&delFC|8%wY2F2LD>k6eQ*{>t>U+V$tNKp~#3Nv`KF$_0`do@J9(LqR4@Nji5$ zMItwXl6u)cAejTV+M1$YHfSn{jH&_ZIP=ZONJgSf;7n6}jJL_Az~Uh$|G1fK<-Oap zgZA5FFF`Y#{%Gd+pqYmaX*}VpCYRzliK)b8mHLdHU{l zb-kTU`FLT%$U!Fm1WL?>wO2ECL93H@YO{`?Kz5Z|RcJhIk-Izwl1Z!#j08u~T zRj@?{KL#Y8PbDvzb{wc1A=_WF6I%_`dQQL@RknyL)lhb@F2Oj#apzk6MOgdASp!8X zYPv7=4kQnI%kA|PSt`fsZkl(oXr^hrUu!A`P_J>^(1jy;TT&1?+x^{^gjZo8Gx}i2k%MP+tf%Ir z)#hs>)@QO=M_fgL_n~^sls``B5<9csa-zDg}a zhnK>M6Q*p)GjA(Fk<~QDHX#*=LsGp!B)`7cktT0vwmp&KXB9ymU#`_(tM_?;c=h^7 zYx_n;yCAHc-tM`}f-?uS!)MCDiFrI3*+GX|W45D~y+lm;RUWDyr1?8cy&S#{Oc53zmT+T!o{KbKWr|1-@=oAziJ@SzWVSYBzIEPRRpv3W0ec*1J}( z>$PCYL7#j$1;+EpW3Kr$e+%-1;%Bw)lagLf08Bu?{F4cSPzRVGE*D{91~ZcL!E^A9 zz66mr_W7yUXZ%Go)+Ec6#lx4W1ei5oZoJO~7=0cGh?B zEwz`NImS?c2%HuG5wyv-TGOBi2?fSF7nZdo9;x^sa?JJcput~g?6y?+Cr&%YT?v+p z{U10|k=ml|?3!9yK9l3hgq8B10dV^UH^+6HHtKjj29(`ME@&g!w&RwBqSAR_{~@U$ zKUaUY(E=GQntE^=U(C6ZlfWiit89Wg-hnDTmQ)KI(p5bPrA=2wN8!Fs^W)1Dj9*WJ#dfSM%nsvE?6au1ekT7*~gL=;_+H<+Pu*P3v8G ztv{@582BqILKgncl_7RyNlQ&GYfCilR%ypC%5H2`28nS598HFr0uuxVX|>fV_fo`& zVZV_K(C(#$b-)Fw(JZCizMX`>ppf)LYmauI(k{xyF6B8^o0t>;C`B=^=92D-2XuiF zt2igr(qM)x$C!2ugl;PJ)SVG>5*)iq6QJ93k}Gk`S&D6d54N3R<)@xXDteJcIYq2h zS6_qHNy(mnIdOfmn1bNKJu|I^&OG)ST$F6#kchW{%%_LUt{t+}d_!khgTLke2T$|M zZLgO4%*(8=JKEZMqAs5J;d5!`u`dCoh1P$k&5#wO0L+*38i@@!%g12UsD{?OeeS2m zdNd=?xLK>z`Yk^!&Q@}&{hH-o1Q?-4+j01`_-U0)P-XjyD=qx5u4Urq_|bxtunOf= z9a4vtQZ}G#JYyYHe8iyNzEMZqt%m%oYsef{4gw`_@LTTuWVuY)D{Py_9AsE)F+*$h zoJfe=$>my8ClOZ`dVv+RX6`Hfw@7>S9K5nVl1Ws=$PBV4cNPrhtWShtCd)+RVm~~! zBtTTqn(s~n95fT@90$gOUwo(>p;-KnGNcRg{-*ua+NK1D8USX*ba=c~K4`sCW`d?#n z&A=^G_ndf#xoYywJI#KzuWU1p=kl@Z%(zE)Po5PWdDL3CSGN^h*`R1c@OIqW`%SK; z8Rx-iRe;Me#|+^)GWN|UdVQfta;2}r-*kf_@-hKXu$5XW27=GZ9#$%0XeQp!fMn?ZspYf5} zkE&b#pQM{h6;3ouvnlBJJU1-**)^3ojiA8A?Q!1}@B*ahLR-3uW$nl<$={WxVqL*E zamv8y;?>M#1j6%ErWg@Sw887A+&Fk@vUkD#(!=V9d^3QVnh^sK5Mr&VtcwH2?r4FGA~N|oB*+;MQ4Y`ol5Wma>cJxufYa$EXjtOTD- z`j7r!tn|{q<;ukg#0FX3%;EOwblMi>1jIW&mY=d^A1{7MW+~(L!p2gDOI97@FF)XG zf5pn9ac6Vu>Qn)PLxv~r)#`6w!;@MqGvT`T>hacA&8-EqXivwi$2ltZio}Q`8K!(w z81N1ouLqMVbE<4C=a^6Q7KFK~XO zry_@${OL#qqUjI5`^r*s1abc|X<^%tR7#$xKZomo8MQa!nipPo_1Im0?t)A67X+20 z@`DzJ-Cd!Na2*kV1qdEOZxmjMRh?( zE386%%-@0Pi!;e z1q1QkDX-6Y`Mi5MtjVoAz!|h^$CEP2S2)}&+zHz3b}ryRC*ys}wn08?xt#I}7pu#6 zEU->>TT0k_D*i(j8<~VGjV7u!|IXHQX0{AT@JEqW;9Cb;5GSM`f+DT@S)?`#23cwe z>_Z|HB37VHD|cR(pX_r*ukF2I+T8YsPP9Xu{FK6KOoIR64QPC7h>K zguFl$zE^)by)Xy`M;u~;M2xQ`LFS*y45xN4}Ih4v)I+gr_&N6LH4*evQJg&fIgJXu^K+XuP8oN0=YOp`}uQJbxYgG zT!UA<@6eVTQ77Xnj@ic42^S+OEd!<`{6NET<5p^PQ6O! z1{|aClVi>gQCjUnQ#nj#oX-z=^$9=l%SWy}RqmrWXT$ozjl-89D(cU&T)RK2Zgn$N zWU@%}@4l=Frf zo3yNYzELEzqu^GCN*U(fp%Ahg8EJ)QItx^Kh;R7pvcHJ}+X@=kr)asJ+&gQpr*C;x z*O#esx|3ly_$P7}@L>R08%PqTwK8EH2j)e`1zDxEQLJO%KX$cGg2{QZrGdl!8ujWb zg{6SMOHT&qmA8tQ*W)P;;Ahbpn_j->L_nsH?_@0FLSqLi)z;V^$@VRd%r23u3iH54 z8Qk0#6vi#E25b;z{Tv|8qd%oNqsp{cE_>-zM?`UlVe`sM+f9opd#Eh>Ce?zdV?kd` z9@RvVXf?O5Z!(SW4 z5mjh2B3rX?Z$|Rz-d}9@!c<>h6@B<6Z?KZb!@yO$3q!9A{*mcExG_h+v0g{>uvfA7 zZAu=LmIbw~d6DZzw{(LWHgwZUGFX3Hq*loSkbCPFjUue3e4LDbjFDbYy-)FbzjTX@ z3S6D!>0aq???Y0%>PF`lmad>Kxy9vQU-KKBE*+*Jo5L%Kq8da5Tv?94Xh{5@Txn>9 zc)9Yg4U;{wiN(DC{I>|c4O9ZsbpgB(6JV|7Bb~m61geDwLl^MYz;3o5e1JeYB<+2N zgEK#)yz_ufH*jgIv*)Il2huh{N6Fms$UHT59q=CW*rcra!ofNd@tU1GzKcvL^F*^x z-|b}NG~=?5N#D``S%p-Nadj;Ig!~=-d{Bi0KdaEKjDvH2iEQm<{Pk6a-5`q$Bvo_r z(ZSqF#VLEO+6jWQ%%uxZ{{O`|)19`0HWU;AF8FwKL*@T~bigq)F&`2nkS@0@#!4N3 z!VeQ9P9xxg@o*^OSMrd0;LU6eNol_72LAoxh}`CL&|uOnKe_#U3K&LBUa05Uj!!+@ z-2;?arBVcSAYUKAWEc~oe63hK0C0*3I|=5uPXhM#AQX?HtG<|T-@U_{$^9#Zd`4UJ zkux-MFa0T6DFqE89_tjg6)$?+tA$74TjYDqqtgkxEpAixkP+X$B3>CeeG0)-MFa*} za1%)ad1nAjMc!jDsdu3*O?BC@>PFwQEWUB#=>b9CF0_*amI4x8WrWlj)_qd**gI=2 z1GJvYfM;wA1_x;HMK+LSnj5ihsBm@DxLkG@2wU>_Pep#wCWI2_oyPeXye$NLuh5a= z<$YfgbRH@DB!n%r!W0bVW?3dbwZa}u**ig8x{1iX??etki#RvcAA~|3wh)DAFWoaj zfGXAcS*7iL-dphr=4Uy$Tl_D2B6Ua16%|lbU!>1Gvz>k~v4YKuBu5{PB2yKunp7?S zu%)@_>1D5>8X1%jNam}C&HqUzrwCJBHI=G{HHf>y{2H2qL_P%LR#xM9VcH8bxn%E@ zAKl0S?FRRnss?-+S>rJ6J?AdadJC+zqnS?!q>oMfNNx_eY(}|RD;!_x4u}Ott0$Ep zCv?oUwK^Sp>rtn|S(yQR{5|(-;5^5Pnvv4RnCsIaKNMjb-DK8QCzshfUA&j(rGJu6 zS_#S10gHO<8GbRC3CY(TKh)sn&9QCMm>r|;?&CLQFH!>gj;okS@JhBzg? zdtksgmUYrrNKip1K>$0Z;q>LdNmWGI7uTyLurJiZ+ndk;-k8lZ|3~g}ZiaHds*2J@ zV8zMFc5%hY@y^FzSuK@K&&Uk4NL_TsqGp#8=EFw03U(n9)4>RGJ5Q&3(I8$R%H2Lj z{-L)v2db!`k(J$j){>mU6$r)9g1E*Lbpp=p_o zzU`H5OXye%65;`s>tP#zbTN0du)WXJv8K&=#~`=?Erd5JK1~x7yf@8hKAFpIdtAAo zHZK2ZvQ3@CuJ3P0202cYAFUO1-+y&ST3!b>3BFhbTqOS>5a|XS*$}Nkp)do7q10?s z?na|uOgrxFf^9ofe+iw$95iDCIG1ZEr6Qr}V=Jv@Q(U&(8g~*mQpET^9+IcU-+Jg< zuZNqbz5ku`yFUan1$F0fIC_p7{+E0O@yVVA~oolKqTq6z3r-YB`ZV?7=j@r?@T}&bp0YzP;N4NMvuh+%p&lzU zkLorDPAabb)Sh$Un26&s<%}KU1Bx{|Uwyk)czMi8UU^Y>Ti+)>Byr>&D(HE4x4~9C z?`YS`V-_+4F3RZaa>vfB-&`@zQ{qE`GO3K%V+ySNz9vfYsb%yLqCBsp+ec5ngjg+2 zS1zgZnI7US^=>^f0Ew&EhUt&+95|2&XV+nYaF<8i(cd}RtB(?1k$!mlUnMbDZazlB zxU*xb!%*8`Tqj;Wf)&%ki%FGQxM}1Bvw_Y2YtM}NbPAI?RX+TAl4f}6uv#9Pf3pqFGcj)m^Os?KW{rg_khPS|8=$A3y#Gy@BS|*&7xtW z%k=s;4K>mYsP}oA4D+;%Q>@(${xM(6tST737=!rdLLly+W~J<-Z=aq=R0jxWznUX1 zE16L4(+wsiT#=M>C216X?!nG`)V@D-gyA8tqx!9_@aFh3l2?EOsoUD;&}rSzF9ru2 z1D|?&C68De2aA?>3*8o}xwa+US)Q|fY45;3=Z^*E>9cro`n5fuX`nYxIdsAaeowZJ zH6@QyX7+z(uc2^9+od_2wt!3Bv1n|#Fij1xLq`-nTY6Qm+FGm-U#Nuf=RTYQWY zn9PhNud5pm&jj2!`LgNsRB>FlJC8@coOx;N%z>-Wd-4^MgK3}r3N04W?|cz!-`9$X z5P+xN>9dBh{cDdh5INE__~PWt6n5#`M?|8SDE2Mrqxyv4R?7M*!H{F{($YNK+GUnr zjsv)ZgzlwgMoMo$MbtvP&(r@ttcaHT-579t`T<3i*A%mJhYs0DYCE6FIT!8l7hHD3 zp=&O7q|%>&zmBa{0l>quZa3)G?_ZmIi#B)UEu&Oj06!n#b~evEzmHz$n)l$1?~{o! z!sRXyYQLdGT2EikOp88QXWc&IeV1*5g~~xy#?+g~5h3nkr0TPCf_eUTw7C7IsuibU z|D1lH!fW>$r~49$zk2fUdDHpaVPpT~3eGdL#3xpS`WOuE?OcgGeroCY$ICGHil}SP zRR6$FB@YZ6VdtfXCLSc8(_G`D`c!0%T$Vc8%X@&C9Q=qA^&!^SLeGo8@1g-I-15sW}? zKQb`M+-wc4K=e|RU`p2iJo%4BO8>MNvYwf6ZS#g`%!JNhYP%OJY28&p$@)UtVq<-$ z7%1j9q2bv?ikZWV*FMt+qzieq+Uo~PpO>>TN1R6_UppDeS7AQRktgXs|13U1t-9a0 zh~PI^+9Eblvvly%;db@GgqQ25Vz1FbV?D4~qTYqC+G( zz86=r$=%%gI>#EKPqUn2`_tog?P0K{(5_0~^Lyy7mRw96EQ{=h6+AYQ$eGvC=QG~6 z_?}T#Ybbxe`>dC^bF9|2y)Vvyz|0?U6(_RFhWm2;DT`5?t#ceakWcKYlXuZ-GAFg{ z9Y{=!sizE>@`w48HH2c0DTE1v_@SDN^QjFp2lI>te6ni(gyxFhO_zoiYV+_l92@fo z_kmpab_#9~zi51h?*;v@mXQAV*k9n?P>pxg*z0Q-4sIaW1$019wCwMlSVRouf03cL zIE`aF_-@F9$)4@`_1H0Z&wPhLKl|=kOZ-#trGXPZi1@g`P{b{Z zPU0$|UR9~SbN$+x@U$h@@O0S%g>%Tz!wbv|S~}|orTY!!VkZOyd=Beb$VEVSs$OC_ zx6O|YYsy^-vJl(<{n5(6_BG8nkp==VhU?a>C@TYridqDATUyVzg?)+Pr%0r`JefaS z?yY`*0R1`pD@k?)W^$YGshAGRQy@NknmJi|Hx#CkB`ew^a>wn*~a8S zOHxN`o>tUSj89+qM)=)5bmZlLS(OR7VS|Ajdei#EC zrun77JiB4*JDIOD%@F?1_Ya5BRs>}jrRSJj4~DQQ>pn@liq5F_foSr zF`*lp-&QKqY%7)Lz;|(Sbld3!ZHN>9_;XV#z zGZwv%&g}kl%$j7uQ&-Md*pPIDa9s_aXV-HeEq)uL@{_OjABPcPlEGp1Vn_P>3wbiEUk*0$2RQa^@F{HlKFKs%5{gz2A*~@6YN?ZN=CH74D-`Fl?8$ZB3M9t*!1^eLJsEb;1`n z!>zx_Xu^D(3788B&=#D}t%VpNwRgW->N~BzxT$jaeP#4#>w(Zn;)|Q&fMuT1Sca2( z5HeQB^(P#z0!>}Tu~J?EJ>UFVUw79#!6#OZURq#p{4F_W%qR;5@lgln(hZ)W`;Ir7 zH!cpUL<3u~<^E#c@$;Y~Dy!wt@#`2C0xrVlnLRB0W7)MUA4=^=!5GM~Ua9uFl&A_x z@%}9C39gsYPGtO@#Al6ka4eHU=Q%Z=o?h)C$tm2XK65m-U#ypTgN{ymsjG{qQ=nP2 zISB$XBx2VrlxrkTv{M3qj|Y?|4h=X7`8246Us*mL!a%Alc5qvvVJ0bI>7 zKR?Mlh}SKy!$CT@B#NT-V16O_h4Lh$QhA;wO`Na{F z%C-XnHNSU&;X!YTr#pq+e54aWS@3Iuw14>>fN@2FHQdMYMI-tzo(NdyZOXso`aWVG zo!rVu&gcjcz`toc^oqqJ#{)D|HX%*ju%7; zzaz*J{;e(B8#p6(0GlJr@KXAckyM_5!soXt($7%|G`h5UTy$6Sd&%U~C?O{!>O8{! znrF|uO{u0XS1r&67<39;C*5D1?9cDIb7kSn(_daA{~ag%?rOJ68%|qQOWiZlVMA}o8Z`8L<)8b0@{!c%yT{}6b#&E@5Hm1UVVOzINL?h&=hr$HMEYOwb z$!cPgGgKKp*CVISNBjO|vG$2ZkM*pPX(YM+tyVT-q-|zyNAqm$2Pan1M7V(s@9-73 zl%MDwco*_&jnCQymOJec{%tuvyK)DnLR+VHHsI%F4!@=$@2uvz6X9|ID%#|E+Mf}d zwX{*5t0p%VEfZu>Oz-@NtEp?HrN7>;aWA2Jif4i2!!pl zWIWqm3f<-zjp{@bo^KC$I+e(3qjWMf17QjhecdO*q7AoAQfj?s5pw+y<8F_i4Bu9< z@|9NWlcIalQk)`l*|e;4J$>lr&X#yJ$KzD$gEd8~Bh zrU#ct(W=i1999coY#_t`UHhjx)S{>H=%F=nWsq;qg(NDOW%-VnuN=?VK8YY>A-;46 z8J3#sVzbWv+q$kt5L|bH|Kj=m=B_V|ppNaA57Rx2w(OMYftKks6zQ+Tlbn8mC#KmQ zcybJYk0{!N+i@)%PlOxW@-CQAILc+}7@gkj)BDr$CzQTV>gx^7@jacAj{R&s5?;Rn zPdjg@*7-n+5gM$&dXw`356JnwYjP@*@6k#Fo)O=ACd3sLdfs^_PmiBGFY;RYH@9ib zIoajW@1@Ljry8GDbPP(}1!dgAhj+RqA_0Q$7%#FcBFhk2=TZ~R-`gGV+Bn#$CKU+F(w7?CNH7)x5!gE@e@3c2V5|m9-e2cD7JB{C-!%|ApkfvA z=`QnCtekv-c?yU_nn$!Y{hWV>fat; z?_}t}UfFZb_D>0nCmr*1)ay2ow>%>2RJ17-h^x&#yvnMi^e!$-*;Zp2eR`fd{5qH* z2!t>!Oa{P8^U8>cOVif#b%pyHB+m*U%!X$xTp8s zhS~hXMU}9=_mK;+isGW#2+!1ZN&44%6Iy2^9sDNWwoEX$Uk|r3`)WlPCJ}A�zc& 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
FSK1#!Dl%zS?SUmZ5X31#Pvh*4?l=qbKv6fJ zy}zp4*TY?!m30^(oIV<2nqM_D0YF@s?F0ile4YwR>Zz2K-1{GnWMLK zsXeo+q#oGEIZ98%zOXqO6^3kZ=0JwIBN8zX#X;9)lmOe9B9%;x0fKUP?$vj3FH z^N;JmAGyB#JNLPK}Sc&%TV%Rm-5! z`e1uLyU)`IaFwuOYEQ*=5R*6}tGHtWU(RO5P@Ly;i%##z8AFIyqwa*!8o=A*P9ya8 ztc2d`_g&a2!e&CgqD&#uIy3HA8FU`h^zMa8QTM2v*XQQfmevdFQkI|P*3I=tXscz2 zI!+=!()-`?F1DI#$X=lYb+40CSZXe&T$d$xI?s0voSScD)FP%=+{MiWF+jT*dY|=? zsDe<7D@kr%)E*!rcpHkTT_!WZD(S?ogOmxSoX!D+ie?2eO5jJimelyZWXQrmMNgE) z`eaY@_#^pM@TIFBMf{wolUC>#{TalPY}UIpMJ$K)_CrX-3%n>d0NHY-u-;ro*xc=s zN36YszAye*ewF=rPuN`7eO9F5J!)*!9D$D+k|m%c1+J%jpb?9Sjuf86@NgJ8! zuE|2Iy}{0T0dP>mzptOb0C!miI%-bZlp(Lru@Fs7+UeO5iAM{lN7!)Ld0t-dA}(1* zg*EuAyCpk*dDCOdR}iW2r}+0XfrT~pGvQ%#Qw&c;AapCr(;6O9ED{MGux=QpQRM|S zKxCnhBXAu}Ho@ym2VFIcrPOkdfiuKfPd_#rkjM4F>c^btgPiBf3^d!a`e5~1&K!9` zrMR>%rKjG;$EMybY>w26_WQHwebCRN9xr&G?tii_Wtbo?KvBrY`$I4%5D!@gH#_ZS zN|q2_r^7cC{0s*o^|LcM2Pxe!CH(HO zl@ZAfLm3eS-5NH~Q0w{zkr7d6HghNZMq^9cWx&+Ctn5J*glZ4kbPot>Tm5(ete=&M zF_o^0(#ZWQwLstB-GvSmhauhTl%3U9LsHQ@5e+soV|6y809Z|n)V{B$`p(^u>|tF& zr=_%E5ADo_ActU`Ln85$bs~e-fpg|aZ!gIb-zD446oxs;L(ePp9-#I#_o0OmO%{HF zU=86$Y42=tU!JQXt|ZFC-ihI031Ud(4R=FGkG7}xqVtb$^#Rn`C+pvCc$E^w%l;}d zT}~;1|9qs?L~7ahg(j!L4V$fXFqSiOQD$=IE-Pe~ZSBO^-gz(9nf>e)%Noqsu4j-` z_Y@6nBx>_?K9{{V&B~cG@*s%3(+XL=R0zcC z=tt)Z90IzcBxP9!#mr#m?c7zFtQXxGaX@ny3Pr&(GKdceVs9$C33!4`qe5n8sRIPD z!)&Ood}B|6eTbojOm^mgTA2dks>fDt89}J`VB3D^xE0zHx&;6XDrvWc?#GHCngVo) zAo7+gK^#I5Am$df)oNe9a^eYm5E+|t?3I;^=kY@pjgFG!bXdq^%>`WJMo zegs&p+1WTm;CB`_ + +.. _C API Reference: c-api-reference/index.html + +.. toctree:: + :hidden: + + c-api-reference/index diff --git a/docs/local_util.py b/docs/local_util.py new file mode 100644 index 0000000..d0fa1ce --- /dev/null +++ b/docs/local_util.py @@ -0,0 +1,77 @@ +# Utility functions used in conf.py +# +# Copyright 2017 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import unicode_literals +from io import open +import os +import shutil + +try: + import urllib.request + _urlretrieve = urllib.request.urlretrieve +except ImportError: + # Python 2 fallback + import urllib + _urlretrieve = urllib.urlretrieve + + +def run_cmd_get_output(cmd): + return os.popen(cmd).read().strip() + + +def files_equal(path_1, path_2): + if not os.path.exists(path_1) or not os.path.exists(path_2): + return False + file_1_contents = '' + with open(path_1, "r", encoding='utf-8') as f_1: + file_1_contents = f_1.read() + file_2_contents = '' + with open(path_2, "r", encoding='utf-8') as f_2: + file_2_contents = f_2.read() + return file_1_contents == file_2_contents + + +def copy_file_if_modified(src_file_path, dst_file_path): + if not files_equal(src_file_path, dst_file_path): + dst_dir_name = os.path.dirname(dst_file_path) + if not os.path.isdir(dst_dir_name): + os.makedirs(dst_dir_name) + shutil.copy(src_file_path, dst_file_path) + + +def copy_if_modified(src_path, dst_path): + if os.path.isfile(src_path): + copy_file_if_modified(src_path, dst_path) + return + + src_path_len = len(src_path) + for root, dirs, files in os.walk(src_path): + for src_file_name in files: + src_file_path = os.path.join(root, src_file_name) + dst_file_path = os.path.join(dst_path + root[src_path_len:], src_file_name) + copy_file_if_modified(src_file_path, dst_file_path) + + +def download_file_if_missing(from_url, to_path): + filename_with_path = to_path + "/" + os.path.basename(from_url) + exists = os.path.isfile(filename_with_path) + if exists: + print("The file '%s' already exists" % (filename_with_path)) + else: + tmp_file, header = _urlretrieve(from_url) + with open(filename_with_path, 'wb') as fobj: + with open(tmp_file, 'rb') as tmp: + fobj.write(tmp.read()) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..2119f51 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..498bb28 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +# This is a list of python packages used to generate documentation. This file is used with pip: +# pip install --user -r requirements.txt +# +esp-docs==0.2.4 diff --git a/docs/utils.sh b/docs/utils.sh new file mode 100644 index 0000000..84f3748 --- /dev/null +++ b/docs/utils.sh @@ -0,0 +1,18 @@ +# Bash helper functions for adding SSH keys + +function add_ssh_keys() { + local key_string="${1}" + mkdir -p ~/.ssh + chmod 700 ~/.ssh + echo -n "${key_string}" >~/.ssh/id_rsa_base64 + base64 --decode --ignore-garbage ~/.ssh/id_rsa_base64 >~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa +} + +function add_doc_server_ssh_keys() { + local key_string="${1}" + local server_url="${2}" + local server_user="${3}" + add_ssh_keys "${key_string}" + echo -e "Host ${server_url}\n\tStrictHostKeyChecking no\n\tUser ${server_user}\n" >>~/.ssh/config +} diff --git a/examples/common/app_insights/CMakeLists.txt b/examples/common/app_insights/CMakeLists.txt new file mode 100644 index 0000000..ac7b763 --- /dev/null +++ b/examples/common/app_insights/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "app_insights.c" + INCLUDE_DIRS "." + PRIV_REQUIRES esp_insights esp_diagnostics esp_rainmaker) diff --git a/examples/common/app_insights/Kconfig b/examples/common/app_insights/Kconfig new file mode 100644 index 0000000..19d6a56 --- /dev/null +++ b/examples/common/app_insights/Kconfig @@ -0,0 +1,11 @@ +menu "App Insights" +visible if ESP_INSIGHTS_ENABLED + + config APP_INSIGHTS_ENABLE_LOG_TYPE_ALL + bool "Enable all diagnostics log type" + default n + help + By default only error logs are enabled. + This config option enables the capture of all log types (errors/warnings/events). + +endmenu diff --git a/examples/common/app_insights/app_insights.c b/examples/common/app_insights/app_insights.c new file mode 100644 index 0000000..195ea21 --- /dev/null +++ b/examples/common/app_insights/app_insights.c @@ -0,0 +1,122 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include +#include +#ifdef CONFIG_ESP_INSIGHTS_ENABLED +#include +#include +#include +#include +#include + +#if CONFIG_APP_INSIGHTS_ENABLE_LOG_TYPE_ALL +#define APP_INSIGHTS_LOG_TYPE ESP_DIAG_LOG_TYPE_ERROR \ + | ESP_DIAG_LOG_TYPE_WARNING \ + | ESP_DIAG_LOG_TYPE_EVENT +#else +#define APP_INSIGHTS_LOG_TYPE ESP_DIAG_LOG_TYPE_ERROR +#endif /* CONFIG_APP_INSIGHTS_ENABLE_LOG_TYPE_ALL */ + +#define INSIGHTS_TOPIC_SUFFIX "diagnostics/from-node" +#define INSIGHTS_TOPIC_RULE "insights_message_delivery" + +static int app_insights_data_send(void *data, size_t len) +{ + char topic[128]; + int msg_id = -1; + if (data == NULL) { + return 0; + } + char *node_id = esp_rmaker_get_node_id(); + if (!node_id) { + return -1; + } + if (esp_rmaker_mqtt_is_budget_available() == false) { + /* the API `esp_rmaker_mqtt_publish` already checks if the budget is available. + This also raises an error message, which we do not want for esp-insights. + silently return with error */ + return ESP_FAIL; + } + esp_rmaker_create_mqtt_topic(topic, sizeof(topic), INSIGHTS_TOPIC_SUFFIX, INSIGHTS_TOPIC_RULE); + esp_rmaker_mqtt_publish(topic, data, len, RMAKER_MQTT_QOS1, &msg_id); + return msg_id; +} + +static void rmaker_common_event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + if (event_base != RMAKER_COMMON_EVENT) { + return; + } + esp_insights_transport_event_data_t data; + switch(event_id) { + case RMAKER_MQTT_EVENT_PUBLISHED: + memset(&data, 0, sizeof(data)); + data.msg_id = *(int *)event_data; + esp_event_post(INSIGHTS_EVENT, INSIGHTS_EVENT_TRANSPORT_SEND_SUCCESS, &data, sizeof(data), portMAX_DELAY); + break; +#ifdef CONFIG_MQTT_REPORT_DELETED_MESSAGES + case RMAKER_MQTT_EVENT_MSG_DELETED: + memset(&data, 0, sizeof(data)); + data.msg_id = *(int *)event_data; + esp_event_post(INSIGHTS_EVENT, INSIGHTS_EVENT_TRANSPORT_SEND_FAILED, &data, sizeof(data), portMAX_DELAY); + break; +#endif /* CONFIG_MQTT_REPORT_DELETED_MESSAGES */ + default: + break; + } +} +#endif /* CONFIG_ESP_INSIGHTS_ENABLED */ + +#define TAG "app_insights" + +esp_err_t app_insights_enable(void) +{ +#ifdef CONFIG_ESP_INSIGHTS_ENABLED +#ifndef CONFIG_ESP_INSIGHTS_TRANSPORT_MQTT + ESP_LOGE(TAG, "Please select the CONFIG_ESP_INSIGHTS_TRANSPORT_MQTT option from menuconfig"); +#endif + /* Initialize the event loop, if not done already. */ + esp_err_t err = esp_event_loop_create_default(); + /* If the default event loop is already initialized, we get ESP_ERR_INVALID_STATE */ + if (err != ESP_OK) { + if (err == ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, "Event loop creation failed with ESP_ERR_INVALID_STATE. Proceeding since it must have been created elsewhere."); + } else { + ESP_LOGE(TAG, "Failed to create default event loop, err = %x", err); + return err; + } + } +#ifdef CONFIG_ESP_RMAKER_SELF_CLAIM + ESP_LOGW(TAG, "Nodes with Self Claiming may not be accessible for Insights."); +#endif + char *node_id = esp_rmaker_get_node_id(); + + esp_insights_transport_config_t transport = { + .callbacks.data_send = app_insights_data_send, + }; + esp_insights_transport_register(&transport); + + esp_event_handler_register(RMAKER_COMMON_EVENT, ESP_EVENT_ANY_ID, rmaker_common_event_handler, NULL); + + esp_insights_config_t config = { + .log_type = APP_INSIGHTS_LOG_TYPE, + .node_id = node_id, + .alloc_ext_ram = true, + }; + + esp_insights_enable(&config); + + if (esp_insights_cmd_resp_enable()!= ESP_OK) { + ESP_LOGE(TAG, "Failed to enabled insights command response"); + } +#else + ESP_LOGI(TAG, "Enable CONFIG_ESP_INSIGHTS_ENABLED to get Insights."); +#endif /* ! CONFIG_ESP_INSIGHTS_ENABLED */ + return ESP_OK; +} diff --git a/examples/common/app_insights/app_insights.h b/examples/common/app_insights/app_insights.h new file mode 100644 index 0000000..4efddf1 --- /dev/null +++ b/examples/common/app_insights/app_insights.h @@ -0,0 +1,25 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Enable ESP Insights in the application + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t app_insights_enable(void); + +#ifdef __cplusplus +} +#endif diff --git a/examples/common/app_insights/component.mk b/examples/common/app_insights/component.mk new file mode 100644 index 0000000..2ec0e78 --- /dev/null +++ b/examples/common/app_insights/component.mk @@ -0,0 +1,2 @@ +COMPONENT_ADD_INCLUDEDIRS := . +COMPONENT_SRCDIRS := . diff --git a/examples/common/app_insights/idf_component.yml b/examples/common/app_insights/idf_component.yml new file mode 100644 index 0000000..b9f9039 --- /dev/null +++ b/examples/common/app_insights/idf_component.yml @@ -0,0 +1,7 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/esp_rainmaker: + version: ">=1.0" + override_path: '../../../components/esp_rainmaker/' + espressif/esp_insights: + version: "~1.2.2" diff --git a/examples/common/app_network/CMakeLists.txt b/examples/common/app_network/CMakeLists.txt new file mode 100644 index 0000000..d69da0b --- /dev/null +++ b/examples/common/app_network/CMakeLists.txt @@ -0,0 +1,14 @@ +set(priv_req qrcode nvs_flash esp_event rmaker_common vfs wifi_provisioning) + +if ("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.1") + list(APPEND priv_req network_provisioning openthread) +endif() + +idf_component_register(SRCS "app_wifi_internal.c" "app_network.c" "app_thread_internal.c" + INCLUDE_DIRS "." + PRIV_INCLUDE_DIRS "private_include" + PRIV_REQUIRES ${priv_req}) + +if(CONFIG_APP_WIFI_SHOW_DEMO_INTRO_TEXT) + target_compile_definitions(${COMPONENT_TARGET} PRIVATE "-D RMAKER_DEMO_PROJECT_NAME=\"${CMAKE_PROJECT_NAME}\"") +endif() diff --git a/examples/common/app_network/Kconfig.projbuild b/examples/common/app_network/Kconfig.projbuild new file mode 100644 index 0000000..3edf412 --- /dev/null +++ b/examples/common/app_network/Kconfig.projbuild @@ -0,0 +1,85 @@ +menu "ESP RainMaker App Wi-Fi Provisioning" + + config APP_NETWORK_PROV_SHOW_QR + bool "Show provisioning QR code" + default y + help + Show the QR code for provisioning. + + config APP_NETWORK_PROV_MAX_POP_MISMATCH + int + default 5 + range 0 20 + prompt "Max wrong pop attempts allowed" + help + Set the maximum wrong pop attempts allowed before stopping provisioning. + Set 0 for the feature to be disabled. + This safeguards the device from brute-force attempt by limiting the wrong pop allowed. + Needs IDF version >= 5.1.3 + + choice APP_NETWORK_PROV_TRANSPORT + bool "Provisioning Transport method" + default APP_NETWORK_PROV_TRANSPORT_BLE + help + Wi-Fi/Network provisioning component offers both, SoftAP and BLE transports. Choose any one. + + config APP_NETWORK_PROV_TRANSPORT_SOFTAP + bool "Soft AP" + depends on !IDF_TARGET_ESP32H2 + config APP_NETWORK_PROV_TRANSPORT_BLE + bool "BLE" + select BT_ENABLED + depends on !IDF_TARGET_ESP32S2 + endchoice + + config APP_NETWORK_PROV_TRANSPORT + int + default 1 if APP_NETWORK_PROV_TRANSPORT_SOFTAP + default 2 if APP_NETWORK_PROV_TRANSPORT_BLE + + config APP_NETWORK_RESET_PROV_ON_FAILURE + bool + default y + prompt "Reset provisioned credentials and state machine after session failure" + help + Enable reseting provisioned credentials and state machine after session failure. + This will restart the provisioning service after retries are exhausted. + + config APP_NETWORK_PROV_MAX_RETRY_CNT + int + default 5 + prompt "Max retries before reseting provisioning state machine" + depends on APP_NETWORK_RESET_PROV_ON_FAILURE + help + Set the Maximum retry to avoid reconnecting to an inexistent network or if credentials + are misconfigured. Provisioned credentials are erased and internal state machine + is reset after this threshold is reached. + + config APP_NETWORK_SHOW_DEMO_INTRO_TEXT + bool "Show intro text for demos" + default n + help + Show some intro text for demos in order to help users understand more about ESP RainMaker. + + config APP_NETWORK_PROV_TIMEOUT_PERIOD + int "Provisioning Timeout" + default 30 + help + Timeout (in minutes) after which the provisioning will auto stop. A reboot will be required + to restart provisioning. It is always recommended to set this to some non zero value, especially + if you are not using PoP. Set to 0 if you do not want provisioning to auto stop. + + config APP_NETWORK_PROV_NAME_PREFIX + string "Provisioning Name Prefix" + default "PROV" + help + Provisioning Name Prefix. + + config APP_WIFI_PROV_COMPAT + bool "Stay compatible with App Wi-Fi component" + depends on ESP_RMAKER_NETWORK_OVER_WIFI + default y + help + Stay compatible with Previous App Wi-Fi component + +endmenu diff --git a/examples/common/app_network/app_network.c b/examples/common/app_network/app_network.c new file mode 100644 index 0000000..55daabb --- /dev/null +++ b/examples/common/app_network/app_network.c @@ -0,0 +1,495 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +#include +#include +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD +#include +#include +#endif + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) +#include +#else +#include +#endif + +#if RMAKER_USING_NETWORK_PROV +#include +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE +#include +#else /* CONFIG_APP_NETOWRK_PROV_TRANSPORT_SOFTAP */ +#include +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ +#else +#include +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE +#include +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ +#include +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ +#endif + +#ifdef CONFIG_APP_NETWORK_PROV_SHOW_QR +#include +#endif + +#include +#include +#include +#include + +ESP_EVENT_DEFINE_BASE(APP_NETWORK_EVENT); +static const char *TAG = "app_network"; +static const int NETWORK_CONNECTED_EVENT = BIT0; +static EventGroupHandle_t network_event_group; + +#define PROV_QR_VERSION "v1" + +#define PROV_TRANSPORT_SOFTAP "softap" +#define PROV_TRANSPORT_BLE "ble" +#define QRCODE_BASE_URL "https://rainmaker.espressif.com/qrcode.html" + +#define CREDENTIALS_NAMESPACE "rmaker_creds" +#define RANDOM_NVS_KEY "random" + +#define POP_STR_SIZE 9 +static esp_timer_handle_t prov_stop_timer; +/* Timeout period in minutes */ +#define APP_NETWORK_PROV_TIMEOUT_PERIOD CONFIG_APP_NETWORK_PROV_TIMEOUT_PERIOD +/* Autofetch period in micro-seconds */ +static uint64_t prov_timeout_period = (APP_NETWORK_PROV_TIMEOUT_PERIOD * 60 * 1000000LL); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 3) +#define APP_PROV_STOP_ON_CREDS_MISMATCH +#elif (CONFIG_APP_NETWOKR_PROV_MAX_RETRY_CNT > 0) +#warning "Provisioning window stop on max credentials failures, needs IDF version >= 5.1.3" +#endif + +#ifdef CONFIG_APP_NETWORK_SHOW_DEMO_INTRO_TEXT + +#define ESP_RAINMAKER_GITHUB_EXAMPLES_PATH "https://github.com/espressif/esp-rainmaker/blob/master/examples" +#define ESP_RAINMAKER_INTRO_LINK "https://rainmaker.espressif.com" +#define ESP_RMAKER_PHONE_APP_LINK "http://bit.ly/esp-rmaker" +char esp_rainmaker_ascii_art[] = \ +" ______ _____ _____ _____ _____ _ _ __ __ _ ________ _____\n"\ +" | ____|/ ____| __ \\ | __ \\ /\\ |_ _| \\ | | \\/ | /\\ | |/ / ____| __ \\\n"\ +" | |__ | (___ | |__) | | |__) | / \\ | | | \\| | \\ / | / \\ | ' /| |__ | |__) |\n"\ +" | __| \\___ \\| ___/ | _ / / /\\ \\ | | | . ` | |\\/| | / /\\ \\ | < | __| | _ /\n"\ +" | |____ ____) | | | | \\ \\ / ____ \\ _| |_| |\\ | | | |/ ____ \\| . \\| |____| | \\ \\\n"\ +" |______|_____/|_| |_| \\_\\/_/ \\_\\_____|_| \\_|_| |_/_/ \\_\\_|\\_\\______|_| \\_\\\n"; + +static void intro_print(bool provisioned) +{ + printf("####################################################################################################\n"); + printf("%s\n", esp_rainmaker_ascii_art); + printf("Welcome to ESP RainMaker %s demo application!\n", RMAKER_DEMO_PROJECT_NAME); + if (!provisioned) { + printf("Follow these steps to get started:\n"); + printf("1. Download the ESP RainMaker phone app by visiting this link from your phone's browser:\n\n"); + printf(" %s\n\n", ESP_RMAKER_PHONE_APP_LINK); + printf("2. Sign up and follow the steps on screen to add the device to your Wi-Fi/Thread network.\n"); + printf("3. You are now ready to use the device and control it locally as well as remotely.\n"); + printf(" You can also use the Boot button on the board to control your device.\n"); + } + printf("\nIf you want to reset network credentials, or reset to factory, press and hold the Boot button.\n"); + printf("\nThis application uses ESP RainMaker, which is based on ESP IDF.\n"); + printf("Check out the source code for this application here:\n %s/%s\n", + ESP_RAINMAKER_GITHUB_EXAMPLES_PATH, RMAKER_DEMO_PROJECT_NAME); + printf("\nPlease visit %s for additional information.\n\n", ESP_RAINMAKER_INTRO_LINK); + printf("####################################################################################################\n"); +} + +#else + +static void intro_print(bool provisioned) +{ + /* Do nothing */ +} + +#endif /* !APP_NETWORK_SHOW_DEMO_INTRO_TEXT */ + +#ifdef CONFIG_APP_NETWORK_PROV_SHOW_QR +static esp_err_t qrcode_display(const char *text) +{ +#define MAX_QRCODE_VERSION 5 + esp_qrcode_config_t cfg = ESP_QRCODE_CONFIG_DEFAULT(); + cfg.max_qrcode_version = MAX_QRCODE_VERSION; + return esp_qrcode_generate(&cfg, text); +} +#endif + +static uint8_t *custom_mfg_data = NULL; +static size_t custom_mfg_data_len = 0; + +esp_err_t app_network_set_custom_mfg_data(uint16_t device_type, uint8_t device_subtype) +{ + int8_t mfg_data[] = {MFG_DATA_HEADER, MGF_DATA_APP_ID, MFG_DATA_VERSION, MFG_DATA_CUSTOMER_ID}; + size_t mfg_data_len = sizeof(mfg_data) + 4; // 4 bytes of device type, subtype, and extra-code + custom_mfg_data = (uint8_t *)MEM_ALLOC_EXTRAM(mfg_data_len); + if (custom_mfg_data == NULL) { + ESP_LOGE(TAG, "Failed to allocate memory to custom mfg data"); + return ESP_ERR_NO_MEM; + } + memcpy(custom_mfg_data, mfg_data, sizeof(mfg_data)); + custom_mfg_data[8] = 0xff & (device_type >> 8); + custom_mfg_data[9] = 0xff & device_type; + custom_mfg_data[10] = device_subtype; + custom_mfg_data[11] = 0; + custom_mfg_data_len = mfg_data_len; + ESP_LOG_BUFFER_HEXDUMP("tag", custom_mfg_data, mfg_data_len, 3); + return ESP_OK; +} + +static void app_network_print_qr(const char *name, const char *pop, const char *transport) +{ + if (!name || !transport) { + ESP_LOGW(TAG, "Cannot generate QR code payload. Data missing."); + return; + } + char payload[150]; + if (pop) { + snprintf(payload, sizeof(payload), "{\"ver\":\"%s\",\"name\":\"%s\"" \ + ",\"pop\":\"%s\",\"transport\":\"%s\"}", + PROV_QR_VERSION, name, pop, transport); + } else { + snprintf(payload, sizeof(payload), "{\"ver\":\"%s\",\"name\":\"%s\"" \ + ",\"transport\":\"%s\"}", + PROV_QR_VERSION, name, transport); + } +#ifdef CONFIG_APP_NETWORK_PROV_SHOW_QR + ESP_LOGI(TAG, "Scan this QR code from the ESP RainMaker phone app for Provisioning."); + qrcode_display(payload); +#endif /* CONFIG_APP_NETWORK_PROV_SHOW_QR */ + ESP_LOGI(TAG, "If QR code is not visible, copy paste the below URL in a browser.\n%s?data=%s", QRCODE_BASE_URL, payload); + esp_event_post(APP_NETWORK_EVENT, APP_NETWORK_EVENT_QR_DISPLAY, payload, strlen(payload) + 1, portMAX_DELAY); +} + +/* Free random_bytes after use only if function returns ESP_OK */ +static esp_err_t read_random_bytes_from_nvs(uint8_t **random_bytes, size_t *len) +{ + nvs_handle handle; + esp_err_t err; + *len = 0; + + if ((err = nvs_open_from_partition(CONFIG_ESP_RMAKER_FACTORY_PARTITION_NAME, CREDENTIALS_NAMESPACE, + NVS_READONLY, &handle)) != ESP_OK) { + ESP_LOGD(TAG, "NVS open for %s %s %s failed with error %d", CONFIG_ESP_RMAKER_FACTORY_PARTITION_NAME, CREDENTIALS_NAMESPACE, RANDOM_NVS_KEY, err); + return ESP_FAIL; + } + + if ((err = nvs_get_blob(handle, RANDOM_NVS_KEY, NULL, len)) != ESP_OK) { + ESP_LOGD(TAG, "Error %d. Failed to read key %s.", err, RANDOM_NVS_KEY); + nvs_close(handle); + return ESP_ERR_NOT_FOUND; + } + + *random_bytes = calloc(*len, 1); + if (*random_bytes) { + nvs_get_blob(handle, RANDOM_NVS_KEY, *random_bytes, len); + nvs_close(handle); + return ESP_OK; + } + nvs_close(handle); + return ESP_ERR_NO_MEM; +} + +static char *custom_pop; +esp_err_t app_network_set_custom_pop(const char *pop) +{ + /* NULL PoP is not allowed here. Use POP_TYPE_NONE instead. */ + if (!pop) { + return ESP_ERR_INVALID_ARG; + } + + /* Freeing up the PoP in case it is already allocated */ + if (custom_pop) { + free(custom_pop); + custom_pop = NULL; + } + + custom_pop = strdup(pop); + if (!custom_pop) { + return ESP_ERR_NO_MEM; + } + return ESP_OK; +} + +static esp_err_t get_device_service_name(char *service_name, size_t max) +{ + uint8_t *nvs_random = NULL; + const char *ssid_prefix = CONFIG_APP_NETWORK_PROV_NAME_PREFIX; + size_t nvs_random_size = 0; + if ((read_random_bytes_from_nvs(&nvs_random, &nvs_random_size) != ESP_OK) || nvs_random_size < 3) { + uint8_t mac_addr[6]; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) + esp_read_mac(mac_addr, ESP_MAC_BASE); +#else + esp_wifi_get_mac(WIFI_IF_STA, mac_addr); +#endif + snprintf(service_name, max, "%s_%02x%02x%02x", ssid_prefix, mac_addr[3], mac_addr[4], mac_addr[5]); + } else { + snprintf(service_name, max, "%s_%02x%02x%02x", ssid_prefix, nvs_random[nvs_random_size - 3], + nvs_random[nvs_random_size - 2], nvs_random[nvs_random_size - 1]); + } + if (nvs_random) { + free(nvs_random); + } + return ESP_OK; +} + + +static char *get_device_pop(app_network_pop_type_t pop_type) +{ + if (pop_type == POP_TYPE_NONE) { + return NULL; + } else if (pop_type == POP_TYPE_CUSTOM) { + if (!custom_pop) { + ESP_LOGE(TAG, "Custom PoP not set. Please use app_wifi_set_custom_pop()."); + return NULL; + } + return strdup(custom_pop); + } + char *pop = calloc(1, POP_STR_SIZE); + if (!pop) { + ESP_LOGE(TAG, "Failed to allocate memory for PoP."); + return NULL; + } + + if (pop_type == POP_TYPE_MAC) { + uint8_t mac_addr[6]; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) + esp_err_t err = esp_read_mac(mac_addr, ESP_MAC_BASE); +#else + esp_err_t err = esp_wifi_get_mac(WIFI_IF_STA, mac_addr); +#endif + if (err == ESP_OK) { + snprintf(pop, POP_STR_SIZE, "%02x%02x%02x%02x", mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); + return pop; + } else { + ESP_LOGE(TAG, "Failed to get MAC address to generate PoP."); + goto pop_err; + } + } else if (pop_type == POP_TYPE_RANDOM) { + uint8_t *nvs_random = NULL; + size_t nvs_random_size = 0; + if ((read_random_bytes_from_nvs(&nvs_random, &nvs_random_size) != ESP_OK) || nvs_random_size < 4) { + ESP_LOGE(TAG, "Failed to read random bytes from NVS to generate PoP."); + if (nvs_random) { + free(nvs_random); + } + goto pop_err; + } else { + snprintf(pop, POP_STR_SIZE, "%02x%02x%02x%02x", nvs_random[0], nvs_random[1], nvs_random[2], nvs_random[3]); + free(nvs_random); + return pop; + } + } +pop_err: + free(pop); + return NULL; +} + +static void network_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) +{ + +#ifdef APP_PROV_STOP_ON_CREDS_MISMATCH + static int failed_cnt = 0; +#endif +#ifdef APP_PROV_STOP_ON_CREDS_MISMATCH + if (event_base == PROTOCOMM_SECURITY_SESSION_EVENT) { + switch (event_id) { + case PROTOCOMM_SECURITY_SESSION_SETUP_OK: + ESP_LOGI(TAG, "Secured session established!"); + break; + case PROTOCOMM_SECURITY_SESSION_INVALID_SECURITY_PARAMS: + /* fall-through */ + case PROTOCOMM_SECURITY_SESSION_CREDENTIALS_MISMATCH: + ESP_LOGE(TAG, "Received incorrect PoP or invalid security params! event: %d", (int) event_id); + if (CONFIG_APP_NETWORK_PROV_MAX_POP_MISMATCH && + (++failed_cnt >= CONFIG_APP_NETWORK_PROV_MAX_POP_MISMATCH)) { + /* stop provisioning for security reasons */ +#if RMAKER_USING_NETWORK_PROV + network_prov_mgr_stop_provisioning(); +#else + wifi_prov_mgr_stop_provisioning(); +#endif + ESP_LOGW(TAG, "Max PoP attempts reached! Provisioning disabled for security reasons. Please reboot device to restart provisioning"); + esp_event_post(APP_NETWORK_EVENT, APP_NETWORK_EVENT_PROV_CRED_MISMATCH, NULL, 0, portMAX_DELAY); + } + break; + default: + break; + } + } +#endif /* APP_PROV_STOP_ON_CREDS_MISMATCH */ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + ESP_LOGI(TAG, "Connected with IP Address:" IPSTR, IP2STR(&event->ip_info.ip)); + /* Signal main application to continue execution */ + xEventGroupSetBits(network_event_group, NETWORK_CONNECTED_EVENT); + } +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD + if (event_base == OPENTHREAD_EVENT && event_id == OPENTHREAD_EVENT_ATTACHED) { + /* Signal main application to continue execution */ + xEventGroupSetBits(network_event_group, NETWORK_CONNECTED_EVENT); + } +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ +#if RMAKER_USING_NETWORK_PROV + if (event_base == NETWORK_PROV_EVENT && event_id == NETWORK_PROV_END) { +#else + if (event_base == WIFI_PROV_EVENT && event_id == WIFI_PROV_END) { +#endif + if (prov_stop_timer) { + esp_timer_stop(prov_stop_timer); + esp_timer_delete(prov_stop_timer); + prov_stop_timer = NULL; + } +#if RMAKER_USING_NETWORK_PROV + network_prov_mgr_deinit(); +#else + wifi_prov_mgr_deinit(); +#endif + } +} + +void app_network_init() +{ + /* Initialize the event loop, if not done already. */ + esp_err_t err = esp_event_loop_create_default(); + /* If the default event loop is already initialized, we get ESP_ERR_INVALID_STATE */ + if (err != ESP_OK) { + if (err == ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, "Event loop creation failed with ESP_ERR_INVALID_STATE. Proceeding since it must have been created elsewhere."); + } else { + ESP_LOGE(TAG, "Failed to create default event loop, err = %x", err); + return; + } + } +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + ESP_ERROR_CHECK(wifi_init()); +#endif +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD + ESP_ERROR_CHECK(thread_init()); +#endif + network_event_group = xEventGroupCreate(); +#ifdef APP_PROV_STOP_ON_CREDS_MISMATCH + ESP_ERROR_CHECK(esp_event_handler_register(PROTOCOMM_SECURITY_SESSION_EVENT, ESP_EVENT_ANY_ID, &network_event_handler, NULL)); +#endif +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &network_event_handler, NULL)); +#endif +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD + ESP_ERROR_CHECK(esp_event_handler_register(OPENTHREAD_EVENT, ESP_EVENT_ANY_ID, &network_event_handler, NULL)); +#endif +#if RMAKER_USING_NETWORK_PROV + ESP_ERROR_CHECK(esp_event_handler_register(NETWORK_PROV_EVENT, NETWORK_PROV_END, &network_event_handler, NULL)); +#else + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_PROV_EVENT, WIFI_PROV_END, &network_event_handler, NULL)); +#endif +} + +static void app_network_prov_stop(void *priv) +{ + ESP_LOGW(TAG, "Provisioning timed out. Please reboot device to restart provisioning."); +#if RMAKER_USING_NETWORK_PROV + network_prov_mgr_stop_provisioning(); +#else + wifi_prov_mgr_stop_provisioning(); +#endif + esp_event_post(APP_NETWORK_EVENT, APP_NETWORK_EVENT_PROV_TIMEOUT, NULL, 0, portMAX_DELAY); +} + +esp_err_t app_network_start_timer(void) +{ + if (prov_timeout_period == 0) { + return ESP_OK; + } + esp_timer_create_args_t prov_stop_timer_conf = { + .callback = app_network_prov_stop, + .arg = NULL, + .dispatch_method = ESP_TIMER_TASK, + .name = "app_wifi_prov_stop_tm" + }; + if (esp_timer_create(&prov_stop_timer_conf, &prov_stop_timer) == ESP_OK) { + esp_timer_start_once(prov_stop_timer, prov_timeout_period); + ESP_LOGI(TAG, "Provisioning will auto stop after %d minute(s).", + APP_NETWORK_PROV_TIMEOUT_PERIOD); + return ESP_OK; + } else { + ESP_LOGE(TAG, "Failed to create Provisioning auto stop timer."); + } + return ESP_FAIL; +} + +esp_err_t app_network_start(app_network_pop_type_t pop_type) +{ + /* Do we want a proof-of-possession (ignored if Security 0 is selected): + * - this should be a string with length > 0 + * - NULL if not used + */ + char *pop = get_device_pop(pop_type); + if ((pop_type != POP_TYPE_NONE) && (pop == NULL)) { + return ESP_ERR_NO_MEM; + } + /* What is the Device Service Name that we want + * This translates to : + * - device name when scheme is network_prov_scheme_ble/wifi_prov_scheme_ble + */ + char service_name[12]; + get_device_service_name(service_name, sizeof(service_name)); + /* What is the service key (Wi-Fi password) + * NULL = Open network + * This is ignored when scheme is network_prov_scheme_ble/wifi_prov_scheme_ble + */ + const char *service_key = NULL; + esp_err_t err = ESP_OK; + bool provisioned = false; +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + err = wifi_start(pop, service_name, service_key, custom_mfg_data, custom_mfg_data_len, &provisioned); +#endif +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD + err = thread_start(pop, service_name, service_key, custom_mfg_data, custom_mfg_data_len, &provisioned); +#endif + if (err != ESP_OK) { + free(pop); + return err; + } + if (!provisioned) { +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE + app_network_print_qr(service_name, pop, PROV_TRANSPORT_BLE); +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ + app_network_print_qr(service_name, pop, PROV_TRANSPORT_SOFTAP); +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + app_network_start_timer(); + } + free(pop); + intro_print(provisioned); + if (custom_mfg_data) { + free(custom_mfg_data); + custom_mfg_data = NULL; + custom_mfg_data_len = 0; + } + /* Wait for Network connection */ + xEventGroupWaitBits(network_event_group, NETWORK_CONNECTED_EVENT, false, true, portMAX_DELAY); + return err; +} diff --git a/examples/common/app_network/app_network.h b/examples/common/app_network/app_network.h new file mode 100644 index 0000000..6e2692a --- /dev/null +++ b/examples/common/app_network/app_network.h @@ -0,0 +1,120 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define MFG_DATA_HEADER 0xe5, 0x02 +#define MGF_DATA_APP_ID 'N', 'o', 'v' +#define MFG_DATA_VERSION 'a' +#define MFG_DATA_CUSTOMER_ID 0x00, 0x01 + +#define MGF_DATA_DEVICE_TYPE_LIGHT 0x0005 +#define MGF_DATA_DEVICE_TYPE_SWITCH 0x0080 + +#define MFG_DATA_DEVICE_SUBTYPE_SWITCH 0x01 +#define MFG_DATA_DEVICE_SUBTYPE_LIGHT 0x01 + +#define MFG_DATA_DEVICE_EXTRA_CODE 0x00 + +/** ESP RainMaker Event Base */ +ESP_EVENT_DECLARE_BASE(APP_NETWORK_EVENT); + +/** App Network Events */ +typedef enum { + /** QR code available for display. Associated data is the NULL terminated QR payload. */ + APP_NETWORK_EVENT_QR_DISPLAY = 1, + /** Provisioning timed out */ + APP_NETWORK_EVENT_PROV_TIMEOUT, + /** Provisioning has restarted due to failures (Invalid SSID/Passphrase) */ + APP_NETWORK_EVENT_PROV_RESTART, + /** Provisioning closed due to invalid credentials */ + APP_NETWORK_EVENT_PROV_CRED_MISMATCH, +} app_network_event_t; + +/** Types of Proof of Possession */ +typedef enum { + /** Use MAC address to generate PoP */ + POP_TYPE_MAC, + /** Use random stream generated and stored in fctry partition during claiming process as PoP */ + POP_TYPE_RANDOM, + /** Do not use any PoP. + * Use this option with caution. Consider using `CONFIG_APP_NETWORK_PROV_TIMEOUT_PERIOD` with this. + */ + POP_TYPE_NONE, + /** Use a custom PoP. + * Set a custom PoP using app_network_set_custom_pop() first. + */ + POP_TYPE_CUSTOM +} app_network_pop_type_t; + +/** Initialize Wi-Fi/Thread + * + * This initializes Wi-Fi/Thread stack and the network provisioning manager + */ +void app_network_init(); + +/** Start Wi-Fi/Thread + * + * This will start provisioning if the node is not provisioned and will connect to any network + * if node is provisioned. Function will return successfully only after network is connected + * + * @param[in] pop_type The type for Proof of Possession (PoP) pin + * + * @return ESP_OK on success (Network connected). + * @return error in case of failure. + */ +esp_err_t app_network_start(app_network_pop_type_t pop_type); + +/** Set custom manufacturing data + * + * This can be used to add some custom manufacturing data in BLE advertisements during + * provisioning. This can be used by apps to filter the scanned BLE devices and show + * only the relevant one. Supported by Nova Home app for light and switch + * + * @param[in] device_type Type of the device, like light or switch + * @param[in] device_subtype Sub Type of the device (application specific) + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t app_network_set_custom_mfg_data(uint16_t device_type, uint8_t device_subtype); + +/** Set custom PoP + * + * This can be used to set a custom Proof of Possession (PoP) pin for provisioning. + * Applicable only if POP_TYPE_CUSTOM is used for app_network_start(). + * + * @param[in] pop A NULL terminated PoP string (typically 8 characters alphanumeric) + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t app_network_set_custom_pop(const char *pop); + +#if CONFIG_APP_WIFI_PROV_COMPAT +#define APP_WIFI_EVENT APP_NETWORK_EVENT +typedef app_network_event_t app_wifi_event_t; +#define APP_WIFI_EVENT_QR_DISPLAY APP_NETWORK_EVENT_QR_DISPLAY +#define APP_WIFI_EVENT_PROV_TIMEOUT APP_NETWORK_EVENT_PROV_TIMEOUT +#define APP_WIFI_EVENT_PROV_RESTART APP_NETWORK_EVENT_PROV_RESTART +#define APP_WIFI_EVENT_PROV_CRED_MISMATCH APP_NETWORK_EVENT_PROV_CRED_MISMATCH +typedef app_network_pop_type_t app_wifi_pop_type_t; +#define app_wifi_init() app_network_init() +#define app_wifi_start(pop_type) app_network_start(pop_type) +#define app_wifi_set_custom_mfg_data(device_type, device_subtype) app_network_set_custom_mfg_data(device_type, device_subtype) +#define app_wifi_set_custom_pop(pop) app_network_set_custom_pop(pop) +#endif /* !CONFIG_APP_WIFI_PROV_COMPAT */ + +#ifdef __cplusplus +} +#endif diff --git a/examples/common/app_network/app_thread_internal.c b/examples/common/app_network/app_thread_internal.c new file mode 100644 index 0000000..6227c91 --- /dev/null +++ b/examples/common/app_network/app_thread_internal.c @@ -0,0 +1,220 @@ +/* + * This example code is in the Public Domain (or CC0 licensed, at your option.) + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + * */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + + + +static const char* TAG = "app_thread"; +/* Event handler for catching system events */ +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ +#ifdef CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + static int retries = 0; +#endif + if (event_base == NETWORK_PROV_EVENT) { + switch (event_id) { + case NETWORK_PROV_START: + ESP_LOGI(TAG, "Provisioning started"); + break; + case NETWORK_PROV_THREAD_DATASET_RECV: { + break; + } + case NETWORK_PROV_THREAD_DATASET_FAIL: { +#ifdef CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + retries++; + if (retries >= CONFIG_APP_NETWORK_PROV_MAX_RETRY_CNT) { + ESP_LOGI(TAG, "Failed to connect with provisioned network, reseting provisioned dataset"); + network_prov_mgr_reset_thread_sm_state_on_failure(); + esp_event_post(APP_NETWORK_EVENT, APP_NETWORK_EVENT_PROV_RESTART, NULL, 0, portMAX_DELAY); + retries = 0; + } +#endif + break; + } + case NETWORK_PROV_THREAD_DATASET_SUCCESS: + ESP_LOGI(TAG, "Provisioning successful"); +#ifdef CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + retries = 0; +#endif + break; + default: + break; + } + } +} + +static esp_netif_t* init_openthread_netif(const esp_openthread_platform_config_t* config) +{ + esp_netif_config_t cfg = ESP_NETIF_DEFAULT_OPENTHREAD(); + esp_netif_t* netif = esp_netif_new(&cfg); + assert(netif != NULL); + ESP_ERROR_CHECK(esp_netif_attach(netif, esp_openthread_netif_glue_init(config))); + + return netif; +} + +static void ot_task_worker(void* aContext) +{ + esp_openthread_platform_config_t config = { + .radio_config = ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG(), + .host_config = ESP_OPENTHREAD_DEFAULT_HOST_CONFIG(), + .port_config = ESP_OPENTHREAD_DEFAULT_PORT_CONFIG(), + }; + /* Initialize the OpenThread stack */ + ESP_ERROR_CHECK(esp_openthread_init(&config)); +#if CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC + /* The OpenThread log level directly matches ESP log level */ + (void)otLoggingSetLevel(CONFIG_LOG_DEFAULT_LEVEL); +#endif + esp_netif_t *openthread_netif = init_openthread_netif(&config); + /* Initialize the esp_netif bindings */ + esp_netif_set_default_netif(openthread_netif); + + /* Run the main loop */ + esp_openthread_launch_mainloop(); + /* Clean up */ + esp_netif_destroy(openthread_netif); + esp_openthread_netif_glue_deinit(); + + esp_vfs_eventfd_unregister(); + vTaskDelete(NULL); +} +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ + +esp_err_t thread_init() +{ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD + /* Initialize TCP/IP */ + esp_netif_init(); + + /* Register our event handler for OpenThread and Provisioning related events */ + ESP_ERROR_CHECK(esp_event_handler_register(NETWORK_PROV_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); + + esp_vfs_eventfd_config_t eventfd_config = { + .max_fds = 3, + }; + ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config)); + xTaskCreate(ot_task_worker, "ot_task", 6144, xTaskGetCurrentTaskHandle(), 5, NULL); + return ESP_OK; +#else + return ESP_ERR_NOT_SUPPORTED; +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ +} + +esp_err_t thread_start(const char *pop, const char *service_name, const char *service_key, uint8_t *mfg_data, + size_t mfg_data_len, bool *provisioned) +{ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD + /* Configuration for the provisioning manager */ + network_prov_mgr_config_t config = { + .scheme = network_prov_scheme_ble, + + /* Any default scheme specific event handler that you would + * like to choose. Since our example application requires + * neither BT nor BLE, we can choose to release the associated + * memory once provisioning is complete, or not needed + * (in case when device is already provisioned). Choosing + * appropriate scheme specific event handler allows the manager + * to take care of this automatically.*/ + .scheme_event_handler = NETWORK_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BTDM + }; + + /* Initialize provisioning manager with the + * configuration parameters set above */ + ESP_ERROR_CHECK(network_prov_mgr_init(config)); + + /* Let's find out if the device is provisioned */ + ESP_ERROR_CHECK(network_prov_mgr_is_thread_provisioned(provisioned)); + /* If device is not yet provisioned start provisioning service */ + if (!(*provisioned)) { + ESP_LOGI(TAG, "Starting provisioning"); + + /* What is the security level that we want (0 or 1): + * - NETWORK_PROV_SECURITY_0 is simply plain text communication. + * - NETWORK_PROV_SECURITY_1 is secure communication which consists of secure handshake + * using X25519 key exchange and proof of possession (pop) and AES-CTR + * for encryption/decryption of messages. + */ + network_prov_security_t security = NETWORK_PROV_SECURITY_1; + + /* This step is only useful when scheme is network_prov_scheme_ble. This will + * set a custom 128 bit UUID which will be included in the BLE advertisement + * and will correspond to the primary GATT service that provides provisioning + * endpoints as GATT characteristics. Each GATT characteristic will be + * formed using the primary service UUID as base, with different auto assigned + * 12th and 13th bytes (assume counting starts from 0th byte). The client side + * applications must identify the endpoints by reading the User Characteristic + * Description descriptor (0x2901) for each characteristic, which contains the + * endpoint name of the characteristic */ + uint8_t custom_service_uuid[] = { + /* This is a random uuid. This can be modified if you want to change the BLE uuid. */ + /* 12th and 13th bit will be replaced by internal bits. */ + 0xb4, 0xdf, 0x5a, 0x1c, 0x3f, 0x6b, 0xf4, 0xbf, + 0xea, 0x4a,0x82, 0x03, 0x04, 0x90, 0x1a, 0x02, + }; + esp_err_t err = network_prov_scheme_ble_set_service_uuid(custom_service_uuid); + if (err != ESP_OK) { + ESP_LOGE(TAG, "thread_prov_scheme_ble_set_service_uuid failed %d", err); + return err; + } + + if (mfg_data) { + err = network_prov_scheme_ble_set_mfg_data(mfg_data, mfg_data_len); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to set mfg data, err=0x%x", err); + return err; + } + } + + /* Start provisioning service */ + ESP_ERROR_CHECK(network_prov_mgr_start_provisioning(security, pop, service_name, service_key)); + } else { + ESP_LOGI(TAG, "Already provisioned, enabling netif and starting Thread"); + /* We don't need the manager as device is already provisioned, + * so let's release it's resources */ + network_prov_mgr_deinit(); + + esp_openthread_lock_acquire(portMAX_DELAY); + otInstance* instance = esp_openthread_get_instance(); + (void)otIp6SetEnabled(instance, true); + (void)otThreadSetEnabled(instance, true); + esp_openthread_lock_release(); + } + return ESP_OK; +#else + return ESP_ERR_NOT_SUPPORTED; +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ +} diff --git a/examples/common/app_network/app_wifi.h b/examples/common/app_network/app_wifi.h new file mode 100644 index 0000000..1bc8109 --- /dev/null +++ b/examples/common/app_network/app_wifi.h @@ -0,0 +1,32 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if CONFIG_APP_WIFI_PROV_COMPAT +#define APP_WIFI_EVENT APP_NETWORK_EVENT +typedef app_network_event_t app_wifi_event_t; +#define APP_WIFI_EVENT_QR_DISPLAY APP_NETWORK_EVENT_QR_DISPLAY +#define APP_WIFI_EVENT_PROV_TIMEOUT APP_NETWORK_EVENT_PROV_TIMEOUT +#define APP_WIFI_EVENT_PROV_RESTART APP_NETWORK_EVENT_PROV_RESTART +#define APP_WIFI_EVENT_PROV_CRED_MISMATCH APP_NETWORK_EVENT_PROV_CRED_MISMATCH +typedef app_network_pop_type_t app_wifi_pop_type_t; +#define app_wifi_init() app_network_init() +#define app_wifi_start(pop_type) app_network_start(pop_type) +#define app_wifi_set_custom_mfg_data(device_type, device_subtype) app_network_set_custom_mfg_data(device_type, device_subtype) +#define app_wifi_set_custom_pop(pop) app_network_set_custom_pop(pop) +#endif /* !CONFIG_APP_WIFI_PROV_COMPAT */ + +#ifdef __cplusplus +} +#endif diff --git a/examples/common/app_network/app_wifi_internal.c b/examples/common/app_network/app_wifi_internal.c new file mode 100644 index 0000000..af1968b --- /dev/null +++ b/examples/common/app_network/app_wifi_internal.c @@ -0,0 +1,334 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0) +// Features supported in 4.1+ +#define ESP_NETIF_SUPPORTED +#endif + +#ifdef ESP_NETIF_SUPPORTED +#include +#else +#include +#endif + +#if RMAKER_USING_NETWORK_PROV +#include +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE +#include +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ +#include +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ +#else +#include +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE +#include +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ +#include +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + +#endif + +#include +#include + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 3) +#define APP_PROV_STOP_ON_CREDS_MISMATCH +#elif (CONFIG_APP_NETWORK_PROV_MAX_RETRY_CNT > 0) +#warning "Provisioning window stop on max credentials failures, needs IDF version >= 5.1.3" +#endif + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +static const char* TAG = "app_wifi"; +/* Event handler for catching system events */ +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ +#ifdef CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + static int retries = 0; +#endif + +#if RMAKER_USING_NETWORK_PROV + if (event_base == NETWORK_PROV_EVENT) { +#else + if (event_base == WIFI_PROV_EVENT) { +#endif + switch (event_id) { +#if RMAKER_USING_NETWORK_PROV + case NETWORK_PROV_START: +#else + case WIFI_PROV_START: +#endif + ESP_LOGI(TAG, "Provisioning started"); + break; +#if RMAKER_USING_NETWORK_PROV + case NETWORK_PROV_WIFI_CRED_RECV: { +#else + case WIFI_PROV_CRED_RECV: { +#endif + wifi_sta_config_t *wifi_sta_cfg = (wifi_sta_config_t *)event_data; + ESP_LOGI(TAG, "Received Wi-Fi credentials" + "\n\tSSID : %s\n\tPassword : %s", + (const char *) wifi_sta_cfg->ssid, + (const char *) wifi_sta_cfg->password); + break; + } +#if RMAKER_USING_NETWORK_PROV + case NETWORK_PROV_WIFI_CRED_FAIL: { + network_prov_wifi_sta_fail_reason_t *reason = (network_prov_wifi_sta_fail_reason_t *)event_data; +#else + case WIFI_PROV_CRED_FAIL: { + wifi_prov_sta_fail_reason_t *reason = (wifi_prov_sta_fail_reason_t *)event_data; +#endif + ESP_LOGE(TAG, "Provisioning failed!\n\tReason : %s" + "\n\tPlease reset to factory and retry provisioning", +#if RMAKER_USING_NETWORK_PROV + (*reason == NETWORK_PROV_WIFI_STA_AUTH_ERROR) ? +#else + (*reason == WIFI_PROV_STA_AUTH_ERROR) ? +#endif + "Wi-Fi station authentication failed" : "Wi-Fi access-point not found"); +#ifdef CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + retries++; + if (retries >= CONFIG_APP_NETWORK_PROV_MAX_RETRY_CNT) { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 3, 1) + ESP_LOGI(TAG, "Failed to connect with provisioned AP, reseting provisioned credentials"); +#if RMAKER_USING_NETWORK_PROV + network_prov_mgr_reset_wifi_sm_state_on_failure(); +#else + wifi_prov_mgr_reset_sm_state_on_failure(); +#endif // RMAKER_USING_NETWORK_PROV + esp_event_post(APP_NETWORK_EVENT, APP_NETWORK_EVENT_PROV_RESTART, NULL, 0, portMAX_DELAY); +#else + ESP_LOGW(TAG, "Failed to connect with provisioned AP, please reset to provisioning manually"); +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 3, 1) + retries = 0; + } +#endif // CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + break; + } +#if RMAKER_USING_NETWORK_PROV + case NETWORK_PROV_WIFI_CRED_SUCCESS: +#else + case WIFI_PROV_CRED_SUCCESS: +#endif + ESP_LOGI(TAG, "Provisioning successful"); +#ifdef CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + retries = 0; +#endif + break; + default: + break; + } + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + ESP_LOGI(TAG, "Disconnected. Connecting to the AP again..."); + esp_wifi_connect(); + } +} + +static void wifi_init_sta() +{ + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); +} +#endif // CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + +esp_err_t wifi_init(void) +{ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + /* Initialize TCP/IP */ +#ifdef ESP_NETIF_SUPPORTED + esp_netif_init(); +#else + tcpip_adapter_init(); +#endif + /* Register our event handler for Wi-Fi, IP and Provisioning related events */ +#if RMAKER_USING_NETWORK_PROV + ESP_ERROR_CHECK(esp_event_handler_register(NETWORK_PROV_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); +#else + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_PROV_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); +#endif + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); + + /* Initialize Wi-Fi including netif with default config */ +#ifdef ESP_NETIF_SUPPORTED + esp_netif_create_default_wifi_sta(); +#endif + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + return ESP_OK; +#else /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ + return ESP_ERR_NOT_SUPPORTED; +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ +} + +esp_err_t wifi_start(const char *pop, const char *service_name, const char *service_key, uint8_t *mfg_data, + size_t mfg_data_len, bool *provisioned) +{ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +#if RMAKER_USING_NETWORK_PROV + /* Configuration for the provisioning manager */ + network_prov_mgr_config_t config = { + /* What is the Provisioning Scheme that we want ? + * network_prov_scheme_softap or network_prov_scheme_ble */ +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE + .scheme = network_prov_scheme_ble, +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ + .scheme = network_prov_scheme_softap, +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + + /* Any default scheme specific event handler that you would + * like to choose. Since our example application requires + * neither BT nor BLE, we can choose to release the associated + * memory once provisioning is complete, or not needed + * (in case when device is already provisioned). Choosing + * appropriate scheme specific event handler allows the manager + * to take care of this automatically. This can be set to + * NETWORK_PROV_EVENT_HANDLER_NONE when using network_prov_scheme_softap*/ +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE + .scheme_event_handler = NETWORK_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BTDM +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ + .scheme_event_handler = NETWORK_PROV_EVENT_HANDLER_NONE, +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + }; + + /* Initialize provisioning manager with the + * configuration parameters set above */ + ESP_ERROR_CHECK(network_prov_mgr_init(config)); +#else // RMAKER_USING_NETWORK_PROV + /* Configuration for the provisioning manager */ + wifi_prov_mgr_config_t config = { + /* What is the Provisioning Scheme that we want ? + * wifi_prov_scheme_softap or wifi_prov_scheme_ble */ +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE + .scheme = wifi_prov_scheme_ble, +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ + .scheme = wifi_prov_scheme_softap, +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + + /* Any default scheme specific event handler that you would + * like to choose. Since our example application requires + * neither BT nor BLE, we can choose to release the associated + * memory once provisioning is complete, or not needed + * (in case when device is already provisioned). Choosing + * appropriate scheme specific event handler allows the manager + * to take care of this automatically. This can be set to + * WIFI_PROV_EVENT_HANDLER_NONE when using wifi_prov_scheme_softap*/ +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE + .scheme_event_handler = WIFI_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BTDM +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ + .scheme_event_handler = WIFI_PROV_EVENT_HANDLER_NONE, +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + }; + + /* Initialize provisioning manager with the + * configuration parameters set above */ + ESP_ERROR_CHECK(wifi_prov_mgr_init(config)); +#endif // RMAKER_USING_NETWORK_PROV + /* Let's find out if the device is provisioned */ +#if RMAKER_USING_NETWORK_PROV + network_prov_mgr_is_wifi_provisioned(provisioned); +#else + wifi_prov_mgr_is_provisioned(provisioned); +#endif + /* If device is not yet provisioned start provisioning service */ + if (!(*provisioned)) { + ESP_LOGI(TAG, "Starting provisioning"); +#ifdef ESP_NETIF_SUPPORTED +#if CONFIG_ESP_WIFI_SOFTAP_SUPPORT + esp_netif_create_default_wifi_ap(); +#endif +#endif + /* What is the security level that we want (0 or 1): + * - NETWORK_PROV_SECURITY_0/WIFI_PROV_SECURITY_0 is simply plain text communication. + * - NETWORK_PROV_SECURITY_1/WIFI_PROV_SECURITY_1 is secure communication which consists of secure handshake + * using X25519 key exchange and proof of possession (pop) and AES-CTR + * for encryption/decryption of messages. + */ +#if RMAKER_USING_NETWORK_PROV + network_prov_security_t security = NETWORK_PROV_SECURITY_1; +#else + wifi_prov_security_t security = WIFI_PROV_SECURITY_1; +#endif + +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE + /* This step is only useful when scheme is wifi_prov_scheme_ble. This will + * set a custom 128 bit UUID which will be included in the BLE advertisement + * and will correspond to the primary GATT service that provides provisioning + * endpoints as GATT characteristics. Each GATT characteristic will be + * formed using the primary service UUID as base, with different auto assigned + * 12th and 13th bytes (assume counting starts from 0th byte). The client side + * applications must identify the endpoints by reading the User Characteristic + * Description descriptor (0x2901) for each characteristic, which contains the + * endpoint name of the characteristic */ + uint8_t custom_service_uuid[] = { + /* This is a random uuid. This can be modified if you want to change the BLE uuid. */ + /* 12th and 13th bit will be replaced by internal bits. */ + 0xb4, 0xdf, 0x5a, 0x1c, 0x3f, 0x6b, 0xf4, 0xbf, + 0xea, 0x4a, 0x82, 0x03, 0x04, 0x90, 0x1a, 0x02, + }; +#if RMAKER_USING_NETWORK_PROV + esp_err_t err = network_prov_scheme_ble_set_service_uuid(custom_service_uuid); +#else + esp_err_t err = wifi_prov_scheme_ble_set_service_uuid(custom_service_uuid); +#endif + if (err != ESP_OK) { + ESP_LOGE(TAG, "wifi_prov_scheme_ble_set_service_uuid failed %d", err); + return err; + } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + if (mfg_data) { +#if RMAKER_USING_NETWORK_PROV + err = network_prov_scheme_ble_set_mfg_data(mfg_data, mfg_data_len); +#else + err = wifi_prov_scheme_ble_set_mfg_data(mfg_data, mfg_data_len); +#endif + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to set mfg data, err=0x%x", err); + return err; + } + } +#endif +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + + /* Start provisioning service */ +#if RMAKER_USING_NETWORK_PROV + ESP_ERROR_CHECK(network_prov_mgr_start_provisioning(security, pop, service_name, service_key)); +#else + ESP_ERROR_CHECK(wifi_prov_mgr_start_provisioning(security, pop, service_name, service_key)); +#endif + } else { + ESP_LOGI(TAG, "Already provisioned, starting Wi-Fi STA"); + /* We don't need the manager as device is already provisioned, + * so let's release it's resources */ +#if RMAKER_USING_NETWORK_PROV + network_prov_mgr_deinit(); +#else + wifi_prov_mgr_deinit(); +#endif + + /* Start Wi-Fi station */ + wifi_init_sta(); + } + return ESP_OK; +#else /* CONFIG_ESP_RMAKER_NETWORK_OVER_THREAD */ + return ESP_ERR_NOT_SUPPORTED; +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ +} diff --git a/examples/common/app_network/component.mk b/examples/common/app_network/component.mk new file mode 100644 index 0000000..1cee370 --- /dev/null +++ b/examples/common/app_network/component.mk @@ -0,0 +1,5 @@ +COMPONENT_ADD_INCLUDEDIRS := . +COMPONENT_SRCDIRS := . +ifdef CONFIG_APP_WIFI_SHOW_DEMO_INTRO_TEXT +CPPFLAGS += -D RMAKER_DEMO_PROJECT_NAME=\"$(PROJECT_NAME)\" +endif diff --git a/examples/common/app_network/idf_component.yml b/examples/common/app_network/idf_component.yml new file mode 100644 index 0000000..712d54f --- /dev/null +++ b/examples/common/app_network/idf_component.yml @@ -0,0 +1,4 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/qrcode: + version: "*" diff --git a/examples/common/app_network/private_include/app_thread_internal.h b/examples/common/app_network/private_include/app_thread_internal.h new file mode 100644 index 0000000..88d0b87 --- /dev/null +++ b/examples/common/app_network/private_include/app_thread_internal.h @@ -0,0 +1,75 @@ +/* + * This example code is in the Public Domain (or CC0 licensed, at your option.) + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + * */ +#pragma once +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) && defined(CONFIG_ESP_RMAKER_USING_NETWORK_PROV) +#define RMAKER_USING_NETWORK_PROV 1 +#else +#define RMAKER_USING_NETWORK_PROV 0 +#endif + +#if !RMAKER_USING_NETWORK_PROV +#error "Please use IDF v5.1+ and enable ESP_RMAKER_USING_NETWORK_PROV for Thread devices" +#endif + +#if SOC_IEEE802154_SUPPORTED +#define ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG() \ + { \ + .radio_mode = RADIO_MODE_NATIVE, \ + } + +#else +#define ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG() \ + { \ + .radio_mode = RADIO_MODE_UART_RCP, \ + .radio_uart_config = { \ + .port = 1, \ + .uart_config = { \ + .baud_rate = 460800, \ + .data_bits = UART_DATA_8_BITS, \ + .parity = UART_PARITY_DISABLE, \ + .stop_bits = UART_STOP_BITS_1, \ + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, \ + .rx_flow_ctrl_thresh = 0, \ + .source_clk = UART_SCLK_DEFAULT, \ + }, \ + .rx_pin = 4, \ + .tx_pin = 5, \ + }, \ + } +#endif + +#define ESP_OPENTHREAD_DEFAULT_HOST_CONFIG() \ + { \ + .host_connection_mode = HOST_CONNECTION_MODE_NONE, \ + } + +#define ESP_OPENTHREAD_DEFAULT_PORT_CONFIG() \ + { \ + .storage_partition_name = "nvs", \ + .netif_queue_size = 10, \ + .task_queue_size = 10, \ + } + +esp_err_t thread_init(); + +esp_err_t thread_start(const char *pop, const char *service_name, const char *service_key, uint8_t *mfg_data, + size_t mfg_data_len, bool *provisioned); + + +#ifdef __cplusplus +} +#endif diff --git a/examples/common/app_network/private_include/app_wifi_internal.h b/examples/common/app_network/private_include/app_wifi_internal.h new file mode 100644 index 0000000..2f59e90 --- /dev/null +++ b/examples/common/app_network/private_include/app_wifi_internal.h @@ -0,0 +1,56 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#pragma once +#include +#include +#include +#include "app_network.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) && defined(CONFIG_ESP_RMAKER_USING_NETWORK_PROV) +#define RMAKER_USING_NETWORK_PROV 1 +#else +#define RMAKER_USING_NETWORK_PROV 0 +#endif + +/** Initialize Wi-Fi + * + * This initializes Wi-Fi and the network/wifi provisioning manager + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t wifi_init(); + +/** Start Wi-Fi + * + * This will start provisioning if the node is not provisioned and will connect to Wi-Fi + * if node is provisioned. Function will return successfully only after Wi-Fi is connect + * + * @param[in] pop The Proof of Possession (PoP) pin + * @param[in] service_name The service name of network/wifi provisioning. This translates to + * - Wi-Fi SSID when scheme is network_prov_scheme_softap/wifi_prov_scheme_softap + * - device name when scheme is network_prov_scheme_ble/wifi_prov_scheme_ble + * @param[in] service_key The service key of network/wifi provisioning. This translates to + * - Wi-Fi password when scheme is network_prov_scheme_softap/wifi_prov_scheme_softap (NULL = Open network) + * @param[in] mfg_data The manufactuer specific data of network/wifi provisioning. + * @param[in] mfg_data The manufactuer specific data length of network/wifi provisioning. + * @param[out] provisioned Whether the device is provisioned. + * + * @return ESP_OK on success (Wi-Fi connected). + * @return error in case of failure. + */ +esp_err_t wifi_start(const char *pop, const char *service_name, const char *service_key, uint8_t *mfg_data, + size_t mfg_data_len, bool *provisioned); + +#ifdef __cplusplus +} +#endif diff --git a/examples/common/app_network/sdkconfig.rename b/examples/common/app_network/sdkconfig.rename new file mode 100644 index 0000000..02b70d9 --- /dev/null +++ b/examples/common/app_network/sdkconfig.rename @@ -0,0 +1,13 @@ +# sdkconfig replacement configurations for deprecated options formatted as +# CONFIG_DEPRECATED_OPTION CONFIG_NEW_OPTION + + +CONFIG_APP_WIFI_PROV_SHOW_QR CONFIG_APP_NETWORK_PROV_SHOW_QR +CONFIG_APP_WIFI_PROV_MAX_POP_MISMATCH CONFIG_APP_NETWORK_PROV_MAX_POP_MISMATCH +CONFIG_APP_WIFI_PROV_TRANSPORT_SOFTAP CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP +CONFIG_APP_WIFI_PROV_TRANSPORT_BLE CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE +CONFIG_APP_WIFI_PROV_TRANSPORT CONFIG_APP_NETWORK_PROV_TRANSPORT +CONFIG_APP_WIFI_RESET_PROV_ON_FAILURE CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE +CONFIG_APP_WIFI_SHOW_DEMO_INTRO_TEXT CONFIG_APP_NETWORK_SHOW_DEMO_INTRO_TEXT +CONFIG_APP_WIFI_PROV_TIMEOUT_PERIOD CONFIG_APP_NETWORK_PROV_TIMEOUT_PERIOD +CONFIG_APP_WIFI_PROV_NAME_PREFIX CONFIG_APP_NETWORK_PROV_NAME_PREFIX diff --git a/examples/common/app_reset/CMakeLists.txt b/examples/common/app_reset/CMakeLists.txt new file mode 100644 index 0000000..9829d29 --- /dev/null +++ b/examples/common/app_reset/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "app_reset.c" + INCLUDE_DIRS "." + REQUIRES gpio_button rmaker_common) diff --git a/examples/common/app_reset/app_reset.c b/examples/common/app_reset/app_reset.c new file mode 100644 index 0000000..d268ff3 --- /dev/null +++ b/examples/common/app_reset/app_reset.c @@ -0,0 +1,66 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +/* It is recommended to copy this code in your example so that you can modify as + * per your application's needs, especially for the indicator calbacks, + * wifi_reset_indicate() and factory_reset_indicate(). + */ +#include +#include +#include +#include + +static const char *TAG = "app_reset"; + +#define REBOOT_DELAY 2 +#define RESET_DELAY 2 + +static void wifi_reset_trigger(void *arg) +{ + esp_rmaker_wifi_reset(RESET_DELAY, REBOOT_DELAY); +} + +static void wifi_reset_indicate(void *arg) +{ + ESP_LOGI(TAG, "Release button now for Wi-Fi reset. Keep pressed for factory reset."); +} + +static void factory_reset_trigger(void *arg) +{ + esp_rmaker_factory_reset(RESET_DELAY, REBOOT_DELAY); +} + +static void factory_reset_indicate(void *arg) +{ + ESP_LOGI(TAG, "Release button to trigger factory reset."); +} + +esp_err_t app_reset_button_register(button_handle_t btn_handle, uint8_t wifi_reset_timeout, + uint8_t factory_reset_timeout) +{ + if (!btn_handle) { + return ESP_ERR_INVALID_ARG; + } + if (wifi_reset_timeout) { + iot_button_add_on_release_cb(btn_handle, wifi_reset_timeout, wifi_reset_trigger, NULL); + iot_button_add_on_press_cb(btn_handle, wifi_reset_timeout, wifi_reset_indicate, NULL); + } + if (factory_reset_timeout) { + if (factory_reset_timeout <= wifi_reset_timeout) { + ESP_LOGW(TAG, "It is recommended to have factory_reset_timeout > wifi_reset_timeout"); + } + iot_button_add_on_release_cb(btn_handle, factory_reset_timeout, factory_reset_trigger, NULL); + iot_button_add_on_press_cb(btn_handle, factory_reset_timeout, factory_reset_indicate, NULL); + } + return ESP_OK; +} + +button_handle_t app_reset_button_create(gpio_num_t gpio_num, button_active_t active_level) +{ + return iot_button_create(gpio_num, active_level); +} diff --git a/examples/common/app_reset/app_reset.h b/examples/common/app_reset/app_reset.h new file mode 100644 index 0000000..94bb8b1 --- /dev/null +++ b/examples/common/app_reset/app_reset.h @@ -0,0 +1,50 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#pragma once +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Create a button handle + * + * This is just a wrapper over iot_button_create(). This can be used to register + * Wi-Fi/Factory reset functionality for a button. + * + * @param[in] gpio_num GPIO index of the pin that the button uses. + * @param[in] active_level button hardware active level. + * "BUTTON_ACTIVE_LOW" means that when the button is pressed, the GPIO will read low level. + * For "BUTTON_ACTIVE_HIGH", it will be reverse. + * + * @return A button_handle_t handle to the created button object, or NULL in case of error. + */ +button_handle_t app_reset_button_create(gpio_num_t gpio_num, button_active_t active_level); + +/** Register callbacks for Wi-Fi/Factory reset + * + * Register Wi-Fi reset or factory reset functionality on a button. + * If you want to use different buttons for these two, call this API twice, with appropriate + * button handles. + * + * @param[in] btn_handle Button handle returned by iot_button_create() or app_button_create() + * @param[in] wifi_reset_timeout Timeout after which the Wi-Fi reset should be triggered. Set to 0, + * if you do not want Wi-Fi reset. + * @param[in] factory_reset_timeout Timeout after which the factory reset should be triggered. Set to 0, + * if you do not want factory reset. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t app_reset_button_register(button_handle_t btn_handle, uint8_t wifi_reset_timeout, uint8_t factory_reset_timeout); + +#ifdef __cplusplus +} +#endif diff --git a/examples/common/app_reset/component.mk b/examples/common/app_reset/component.mk new file mode 100644 index 0000000..2ec0e78 --- /dev/null +++ b/examples/common/app_reset/component.mk @@ -0,0 +1,2 @@ +COMPONENT_ADD_INCLUDEDIRS := . +COMPONENT_SRCDIRS := . diff --git a/examples/common/gpio_button/CMakeLists.txt b/examples/common/gpio_button/CMakeLists.txt new file mode 100644 index 0000000..fe53b13 --- /dev/null +++ b/examples/common/gpio_button/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "button/button.c" "button/button_obj.cpp" + INCLUDE_DIRS "button/include" + REQUIRES "driver") diff --git a/examples/common/gpio_button/Kconfig b/examples/common/gpio_button/Kconfig new file mode 100644 index 0000000..30b9f9b --- /dev/null +++ b/examples/common/gpio_button/Kconfig @@ -0,0 +1,6 @@ +menu "GPIO Button" + config IO_GLITCH_FILTER_TIME_MS + int "IO glitch filter timer ms (10~100)" + range 10 100 + default 50 +endmenu \ No newline at end of file diff --git a/examples/common/gpio_button/button/button.c b/examples/common/gpio_button/button/button.c new file mode 100644 index 0000000..5764577 --- /dev/null +++ b/examples/common/gpio_button/button/button.c @@ -0,0 +1,353 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include + +#define IOT_CHECK(tag, a, ret) if(!(a)) { \ + ESP_LOGE(tag,"%s:%d (%s)", __FILE__, __LINE__, __FUNCTION__); \ + return (ret); \ + } +#define ERR_ASSERT(tag, param) IOT_CHECK(tag, (param) == ESP_OK, ESP_FAIL) +#define POINT_ASSERT(tag, param, ret) IOT_CHECK(tag, (param) != NULL, (ret)) + +typedef enum { + BUTTON_STATE_IDLE = 0, + BUTTON_STATE_PUSH, + BUTTON_STATE_PRESSED, +} button_status_t; + +typedef struct button_dev button_dev_t; +typedef struct btn_cb button_cb_t; + +struct btn_cb{ + TickType_t interval; + button_cb cb; + void* arg; + uint8_t on_press; + TimerHandle_t tmr; + button_dev_t *pbtn; + button_cb_t *next_cb; +}; + +struct button_dev{ + uint8_t io_num; + uint8_t active_level; + uint32_t serial_thres_sec; + uint8_t taskq_on; + QueueHandle_t taskq; + QueueHandle_t argq; + button_status_t state; + button_cb_t tap_short_cb; + button_cb_t tap_psh_cb; + button_cb_t tap_rls_cb; + button_cb_t press_serial_cb; + button_cb_t* cb_head; +}; + +#define BUTTON_GLITCH_FILTER_TIME_MS CONFIG_IO_GLITCH_FILTER_TIME_MS +static const char* TAG = "button"; + +static void button_press_cb(TimerHandle_t tmr) +{ + button_cb_t* btn_cb = (button_cb_t*) pvTimerGetTimerID(tmr); + button_dev_t* btn = btn_cb->pbtn; + // low, then restart + if (btn->active_level == gpio_get_level(btn->io_num)) { + btn->state = BUTTON_STATE_PRESSED; + if (btn->taskq != NULL && btn->argq != NULL && btn->taskq_on && !btn_cb->on_press) { + void *tmp = btn_cb->cb; + xQueueOverwrite(btn->taskq, &tmp); + xQueueOverwrite(btn->argq, &btn_cb->arg); + } else if (btn_cb->cb) { + btn_cb->cb(btn_cb->arg); + } + } +} + +static void button_tap_psh_cb(TimerHandle_t tmr) +{ + button_cb_t* btn_cb = (button_cb_t*) pvTimerGetTimerID(tmr); + button_dev_t* btn = btn_cb->pbtn; + xTimerStop(btn->tap_rls_cb.tmr, portMAX_DELAY); + int lv = gpio_get_level(btn->io_num); + + if (btn->active_level == lv) { + // True implies key is pressed + btn->state = BUTTON_STATE_PUSH; + if (btn->press_serial_cb.tmr) { + xTimerChangePeriod(btn->press_serial_cb.tmr, btn->serial_thres_sec*1000 / portTICK_PERIOD_MS, portMAX_DELAY); + xTimerReset(btn->press_serial_cb.tmr, portMAX_DELAY); + } + if (btn->tap_psh_cb.cb) { + btn->tap_psh_cb.cb(btn->tap_psh_cb.arg); + } + } else { + // 50ms, check if this is a real key up + if (btn->tap_rls_cb.tmr) { + xTimerStop(btn->tap_rls_cb.tmr, portMAX_DELAY); + xTimerReset(btn->tap_rls_cb.tmr, portMAX_DELAY); + } + } +} + +static void button_tap_rls_cb(TimerHandle_t tmr) +{ + button_cb_t* btn_cb = (button_cb_t*) pvTimerGetTimerID(tmr); + button_dev_t* btn = btn_cb->pbtn; + xTimerStop(btn->tap_rls_cb.tmr, portMAX_DELAY); + if (btn->active_level == gpio_get_level(btn->io_num)) { + + } else { + // high, then key is up + button_cb_t *pcb = btn->cb_head; + while (pcb != NULL) { + if (pcb->tmr != NULL) { + xTimerStop(pcb->tmr, portMAX_DELAY); + } + pcb = pcb->next_cb; + } + if (btn->taskq != NULL && btn->argq != NULL && btn->taskq_on && uxQueueMessagesWaiting(btn->taskq) != 0 && btn->state != BUTTON_STATE_IDLE) { + void (*task)(void*); + void *arg; + xQueueReceive(btn->taskq, &task, 0); + xQueueReceive(btn->argq, &arg, 0); + task(arg); + } + if (btn->press_serial_cb.tmr && btn->press_serial_cb.tmr != NULL) { + xTimerStop(btn->press_serial_cb.tmr, portMAX_DELAY); + } + if (btn->tap_short_cb.cb && btn->state == BUTTON_STATE_PUSH) { + btn->tap_short_cb.cb(btn->tap_short_cb.arg); + } + if(btn->tap_rls_cb.cb && btn->state != BUTTON_STATE_IDLE) { + btn->tap_rls_cb.cb(btn->tap_rls_cb.arg); + } + btn->state = BUTTON_STATE_IDLE; + } +} + +static void button_press_serial_cb(TimerHandle_t tmr) +{ + button_dev_t* btn = (button_dev_t*) pvTimerGetTimerID(tmr); + if (btn->press_serial_cb.cb) { + btn->press_serial_cb.cb(btn->press_serial_cb.arg); + } + xTimerChangePeriod(btn->press_serial_cb.tmr, btn->press_serial_cb.interval, portMAX_DELAY); + xTimerReset(btn->press_serial_cb.tmr, portMAX_DELAY); +} + +static void button_gpio_isr_handler(void* arg) +{ + button_dev_t* btn = (button_dev_t*) arg; + portBASE_TYPE HPTaskAwoken = pdFALSE; + int level = gpio_get_level(btn->io_num); + if (level == btn->active_level) { + if (btn->tap_psh_cb.tmr) { + xTimerStopFromISR(btn->tap_psh_cb.tmr, &HPTaskAwoken); + xTimerResetFromISR(btn->tap_psh_cb.tmr, &HPTaskAwoken); + } + + button_cb_t *pcb = btn->cb_head; + while (pcb != NULL) { + if (pcb->tmr != NULL) { + xTimerStopFromISR(pcb->tmr, &HPTaskAwoken); + xTimerResetFromISR(pcb->tmr, &HPTaskAwoken); + } + pcb = pcb->next_cb; + } + } else { + // 50ms, check if this is a real key up + if (btn->tap_rls_cb.tmr) { + xTimerStopFromISR(btn->tap_rls_cb.tmr, &HPTaskAwoken); + xTimerResetFromISR(btn->tap_rls_cb.tmr, &HPTaskAwoken); + } + } + if(HPTaskAwoken == pdTRUE) { + portYIELD_FROM_ISR(); + } +} + +static void button_free_tmr(TimerHandle_t* tmr) +{ + if (tmr && *tmr) { + xTimerStop(*tmr, portMAX_DELAY); + xTimerDelete(*tmr, portMAX_DELAY); + *tmr = NULL; + } +} + +esp_err_t iot_button_delete(button_handle_t btn_handle) +{ + POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG); + button_dev_t* btn = (button_dev_t*) btn_handle; + gpio_set_intr_type(btn->io_num, GPIO_INTR_DISABLE); + gpio_isr_handler_remove(btn->io_num); + + button_free_tmr(&btn->tap_rls_cb.tmr); + button_free_tmr(&btn->tap_psh_cb.tmr); + button_free_tmr(&btn->tap_short_cb.tmr); + button_free_tmr(&btn->press_serial_cb.tmr); + + button_cb_t *pcb = btn->cb_head; + while (pcb != NULL) { + button_cb_t *cb_next = pcb->next_cb; + button_free_tmr(&pcb->tmr); + free(pcb); + pcb = cb_next; + } + free(btn); + return ESP_OK; +} + +button_handle_t iot_button_create(gpio_num_t gpio_num, button_active_t active_level) +{ + IOT_CHECK(TAG, gpio_num < GPIO_NUM_MAX, NULL); + button_dev_t* btn = (button_dev_t*) calloc(1, sizeof(button_dev_t)); + POINT_ASSERT(TAG, btn, NULL); + btn->active_level = active_level; + btn->io_num = gpio_num; + btn->state = BUTTON_STATE_IDLE; + btn->taskq_on = 0; + btn->taskq = xQueueCreate(1, sizeof(void*)); + btn->argq = xQueueCreate(1, sizeof(void *)); + btn->tap_rls_cb.arg = NULL; + btn->tap_rls_cb.cb = NULL; + btn->tap_rls_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS; + btn->tap_rls_cb.pbtn = btn; + btn->tap_rls_cb.tmr = xTimerCreate("btn_rls_tmr", btn->tap_rls_cb.interval, pdFALSE, + &btn->tap_rls_cb, button_tap_rls_cb); + btn->tap_psh_cb.arg = NULL; + btn->tap_psh_cb.cb = NULL; + btn->tap_psh_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS; + btn->tap_psh_cb.pbtn = btn; + btn->tap_psh_cb.tmr = xTimerCreate("btn_psh_tmr", btn->tap_psh_cb.interval, pdFALSE, + &btn->tap_psh_cb, button_tap_psh_cb); + gpio_install_isr_service(0); + gpio_config_t gpio_conf; + gpio_conf.intr_type = GPIO_INTR_ANYEDGE; + gpio_conf.mode = GPIO_MODE_INPUT; + gpio_conf.pin_bit_mask = (uint64_t)1 << gpio_num; + gpio_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + gpio_conf.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&gpio_conf); + gpio_isr_handler_add(gpio_num, button_gpio_isr_handler, btn); + return (button_handle_t) btn; +} + +esp_err_t iot_button_rm_cb(button_handle_t btn_handle, button_cb_type_t type) +{ + button_dev_t* btn = (button_dev_t*) btn_handle; + button_cb_t* btn_cb = NULL; + if (type == BUTTON_CB_PUSH) { + btn_cb = &btn->tap_psh_cb; + } else if (type == BUTTON_CB_RELEASE) { + btn_cb = &btn->tap_rls_cb; + } else if (type == BUTTON_CB_TAP) { + btn_cb = &btn->tap_short_cb; + } else if (type == BUTTON_CB_SERIAL) { + btn_cb = &btn->press_serial_cb; + } + btn_cb->cb = NULL; + btn_cb->arg = NULL; + btn_cb->pbtn = btn; + button_free_tmr(&btn_cb->tmr); + return ESP_OK; +} + +esp_err_t iot_button_set_serial_cb(button_handle_t btn_handle, uint32_t start_after_sec, TickType_t interval_tick, button_cb cb, void* arg) +{ + button_dev_t* btn = (button_dev_t*) btn_handle; + btn->serial_thres_sec = start_after_sec; + if (btn->press_serial_cb.tmr == NULL) { + btn->press_serial_cb.tmr = xTimerCreate("btn_serial_tmr", btn->serial_thres_sec*1000 / portTICK_PERIOD_MS, + pdFALSE, btn, button_press_serial_cb); + } + btn->press_serial_cb.arg = arg; + btn->press_serial_cb.cb = cb; + btn->press_serial_cb.interval = interval_tick; + btn->press_serial_cb.pbtn = btn; + xTimerChangePeriod(btn->press_serial_cb.tmr, btn->serial_thres_sec*1000 / portTICK_PERIOD_MS, portMAX_DELAY); + return ESP_OK; +} + +esp_err_t iot_button_set_evt_cb(button_handle_t btn_handle, button_cb_type_t type, button_cb cb, void* arg) +{ + POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG); + button_dev_t* btn = (button_dev_t*) btn_handle; + if (type == BUTTON_CB_PUSH) { + btn->tap_psh_cb.arg = arg; + btn->tap_psh_cb.cb = cb; + btn->tap_psh_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS; + btn->tap_psh_cb.pbtn = btn; + xTimerChangePeriod(btn->tap_psh_cb.tmr, btn->tap_psh_cb.interval, portMAX_DELAY); + } else if (type == BUTTON_CB_RELEASE) { + btn->tap_rls_cb.arg = arg; + btn->tap_rls_cb.cb = cb; + btn->tap_rls_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS; + btn->tap_rls_cb.pbtn = btn; + xTimerChangePeriod(btn->tap_rls_cb.tmr, btn->tap_psh_cb.interval, portMAX_DELAY); + } else if (type == BUTTON_CB_TAP) { + btn->tap_short_cb.arg = arg; + btn->tap_short_cb.cb = cb; + btn->tap_short_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS; + btn->tap_short_cb.pbtn = btn; + } else if (type == BUTTON_CB_SERIAL) { + iot_button_set_serial_cb(btn_handle, 1, 1000 / portTICK_PERIOD_MS, cb, arg); + } + return ESP_OK; +} + +esp_err_t iot_button_add_on_press_cb(button_handle_t btn_handle, uint32_t press_sec, button_cb cb, void* arg) +{ + POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG); + IOT_CHECK(TAG, press_sec != 0, ESP_ERR_INVALID_ARG); + button_dev_t* btn = (button_dev_t*) btn_handle; + button_cb_t* cb_new = (button_cb_t*) calloc(1, sizeof(button_cb_t)); + POINT_ASSERT(TAG, cb_new, ESP_FAIL); + cb_new->on_press = 1; + cb_new->arg = arg; + cb_new->cb = cb; + cb_new->interval = press_sec * 1000 / portTICK_PERIOD_MS; + cb_new->pbtn = btn; + cb_new->tmr = xTimerCreate("btn_press_tmr", cb_new->interval, pdFALSE, cb_new, button_press_cb); + cb_new->next_cb = btn->cb_head; + btn->cb_head = cb_new; + return ESP_OK; +} + +esp_err_t iot_button_add_on_release_cb(button_handle_t btn_handle, uint32_t press_sec, button_cb cb, void* arg) +{ + POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG); + IOT_CHECK(TAG, press_sec != 0, ESP_ERR_INVALID_ARG); + button_dev_t* btn = (button_dev_t*) btn_handle; + button_cb_t* cb_new = (button_cb_t*) calloc(1, sizeof(button_cb_t)); + POINT_ASSERT(TAG, cb_new, ESP_FAIL); + btn->taskq_on = 1; + cb_new->arg = arg; + cb_new->cb = cb; + cb_new->interval = press_sec * 1000 / portTICK_PERIOD_MS; + cb_new->pbtn = btn; + cb_new->tmr = xTimerCreate("btn_press_tmr", cb_new->interval, pdFALSE, cb_new, button_press_cb); + cb_new->next_cb = btn->cb_head; + btn->cb_head = cb_new; + return ESP_OK; +} + diff --git a/examples/common/gpio_button/button/button_obj.cpp b/examples/common/gpio_button/button/button_obj.cpp new file mode 100644 index 0000000..efdcff8 --- /dev/null +++ b/examples/common/gpio_button/button/button_obj.cpp @@ -0,0 +1,54 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +CButton::CButton(gpio_num_t gpio_num, button_active_t active_level) +{ + m_btn_handle = iot_button_create(gpio_num, active_level); +} + +CButton::~CButton() +{ + iot_button_delete(m_btn_handle); + m_btn_handle = NULL; +} + +esp_err_t CButton::set_evt_cb(button_cb_type_t type, button_cb cb, void* arg) +{ + return iot_button_set_evt_cb(m_btn_handle, type, cb, arg); +} + +esp_err_t CButton::set_serial_cb(button_cb cb, void* arg, TickType_t interval_tick, uint32_t start_after_sec) +{ + return iot_button_set_serial_cb(m_btn_handle, start_after_sec, interval_tick, cb, arg); +} + +esp_err_t CButton::add_on_press_cb(uint32_t press_sec, button_cb cb, void* arg) +{ + return iot_button_add_on_press_cb(m_btn_handle, press_sec, cb, arg); +} + +esp_err_t CButton::add_on_release_cb(uint32_t press_sec, button_cb cb, void* arg) +{ + return iot_button_add_on_release_cb(m_btn_handle, press_sec, cb, arg); +} + +esp_err_t CButton::rm_cb(button_cb_type_t type) +{ + return iot_button_rm_cb(m_btn_handle, type); +} diff --git a/examples/common/gpio_button/button/include/iot_button.h b/examples/common/gpio_button/button/include/iot_button.h new file mode 100644 index 0000000..3e73513 --- /dev/null +++ b/examples/common/gpio_button/button/include/iot_button.h @@ -0,0 +1,272 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#ifndef _IOT_BUTTON_H_ +#define _IOT_BUTTON_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +typedef void (* button_cb)(void*); +typedef void* button_handle_t; + +typedef enum { + BUTTON_ACTIVE_HIGH = 1, /*!