diff --git a/Devices/lilygo-tembed-cc1101-plus/lilygo,tembed-cc1101-plus.dts b/Devices/lilygo-tembed-cc1101-plus/lilygo,tembed-cc1101-plus.dts index 30cc27b51..dd4baf272 100644 --- a/Devices/lilygo-tembed-cc1101-plus/lilygo,tembed-cc1101-plus.dts +++ b/Devices/lilygo-tembed-cc1101-plus/lilygo,tembed-cc1101-plus.dts @@ -1,8 +1,6 @@ /dts-v1/; -#include #include -#include #include #include #include @@ -31,20 +29,6 @@ compatible = "root"; model = "T-Embed CC1101 Plus"; - adc0 { - compatible = "espressif,esp32-adc-oneshot"; - unit-id = ; - clk-src = ; - channels = ; - }; - - battery-sense { - compatible = "battery-sense"; - io-channel = <&adc0 0>; - reference-voltage-mv = <3300>; - multiplier = <2110>; - }; - wifi0 { compatible = "espressif,esp32-wifi-pinned"; }; @@ -153,7 +137,6 @@ invert-color; pixel-clock-hz = <80000000>; pin-dc = <&gpio0 16 GPIO_FLAG_NONE>; - pin-reset = <&gpio0 40 GPIO_FLAG_NONE>; backlight = <&display_backlight>; }; diff --git a/Devices/lilygo-tembed-cc1101/CMakeLists.txt b/Devices/lilygo-tembed-cc1101/CMakeLists.txt new file mode 100644 index 000000000..b2d9c4492 --- /dev/null +++ b/Devices/lilygo-tembed-cc1101/CMakeLists.txt @@ -0,0 +1,6 @@ +file(GLOB_RECURSE SOURCE_FILES source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + REQUIRES TactilityKernel Tactility +) diff --git a/Devices/lilygo-tembed-cc1101/LICENSE-Apache-2.0.md b/Devices/lilygo-tembed-cc1101/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Devices/lilygo-tembed-cc1101/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### 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/Devices/lilygo-tembed-cc1101/device.properties b/Devices/lilygo-tembed-cc1101/device.properties new file mode 100644 index 000000000..3f92c2703 --- /dev/null +++ b/Devices/lilygo-tembed-cc1101/device.properties @@ -0,0 +1,25 @@ +general.vendor=LilyGO +general.name=T-Embed CC1101 + +hardware.target=ESP32S3 +hardware.flashSize=16MB +hardware.spiRam=true +hardware.spiRamMode=OCT +hardware.spiRamSpeed=80M +hardware.spiRamXipDisabled=true +hardware.tinyUsbMsc=true +hardware.esptoolFlashFreq=80M +hardware.bluetooth=true + +storage.userDataLocation=SD + +display.size=1.9" +display.dpi=191 + +lvgl.colorDepth=16 +lvgl.fontSize=16 +lvgl.uiDensity=compact + +sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y + + diff --git a/Devices/lilygo-tembed-cc1101/lilygo,tembed-cc1101.dts b/Devices/lilygo-tembed-cc1101/lilygo,tembed-cc1101.dts new file mode 100644 index 000000000..090243f3a --- /dev/null +++ b/Devices/lilygo-tembed-cc1101/lilygo,tembed-cc1101.dts @@ -0,0 +1,175 @@ +/dts-v1/; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define WAKEUP_BUTTON_FLAGS (GPIO_FLAG_ACTIVE_LOW | GPIO_FLAG_PULL_UP) + +// Reference: +// https://wiki.lilygo.cc/products/t-embed-series/t-embed-cc1101/ +// https://github.com/Xinyuan-LilyGO/T-Embed-CC1101 +// https://lilygo.cc/products/t-embed-cc1101 +/ { + compatible = "root"; + model = "T-Embed CC1101"; + + wifi0 { + compatible = "espressif,esp32-wifi-pinned"; + }; + + ble0 { + compatible = "espressif,esp32-ble"; + }; + + gpio0 { + compatible = "espressif,esp32-gpio"; + gpio-count = <49>; + }; + + // Required to enable peripherals like BQ25896 + peripheral-power-hog { + compatible = "gpio-hog"; + pin = <&gpio0 15 GPIO_FLAG_NONE>; + mode = ; + }; + + // Speaker I2S (MAX98357A amplifier) + i2s0 { + compatible = "espressif,esp32-i2s"; + port = ; + pin-bclk = <&gpio0 46 GPIO_FLAG_NONE>; + pin-ws = <&gpio0 40 GPIO_FLAG_NONE>; + pin-data-out = <&gpio0 7 GPIO_FLAG_NONE>; + pin-data-in = <&gpio0 42 GPIO_FLAG_NONE>; + pin-mclk = <&gpio0 39 GPIO_FLAG_NONE>; + }; + + // MAX98357A class-D speaker amplifier. No I2C. + speaker0 { + compatible = "maxim,max98357a"; + i2s = <&i2s0>; + }; + + mic0 { + compatible = "generic,spm1423"; + i2s = <&i2s0>; + channels = <1>; + }; + + // WS2812 RGB LED Ring + // TODO - Sync on/off with DisplayIdle? + ledstrip0 { + compatible = "espressif,esp32-led-strip"; + pin-data = <&gpio0 14 GPIO_FLAG_NONE>; + led-count = <8>; + }; + + i2c_internal: i2c0 { + compatible = "espressif,esp32-i2c-master"; + port = ; + clock-frequency = <100000>; + pin-sda = <&gpio0 8 GPIO_FLAG_NONE>; + pin-scl = <&gpio0 18 GPIO_FLAG_NONE>; + + bq27220 { + compatible = "ti,bq27220"; + reg = <0x55>; + }; + + bq25896 { + compatible = "ti,bq25896"; + reg = <0x6B>; + }; + + // TODO - Add PN532 NFC - IRQ GPIO 17, RES GPIO 45, address 0x24 - driver needed + }; + + display_backlight_pwm { + compatible = "espressif,esp32-pwm-ledc"; + pin = <&gpio0 21 GPIO_FLAG_NONE>; + period-ns = <33333>; + ledc-timer = <0>; + ledc-channel = <0>; + }; + + display_backlight { + compatible = "pwm-backlight"; + // Off by default so dispaly power-on won't show the screen from before the last power loss. + // The display backlight is turned on during the boot process. + status = "disabled"; + pwm = <&display_backlight_pwm>; + }; + + spi0 { + compatible = "espressif,esp32-spi"; + host = ; + cs-gpios = <&gpio0 41 GPIO_FLAG_NONE>, // Display + <&gpio0 13 GPIO_FLAG_NONE>, // SD card + <&gpio0 12 GPIO_FLAG_NONE>; // Radio - CC1101 + pin-mosi = <&gpio0 9 GPIO_FLAG_NONE>; + pin-miso = <&gpio0 10 GPIO_FLAG_NONE>; + pin-sclk = <&gpio0 11 GPIO_FLAG_NONE>; + + display@0 { + compatible = "sitronix,st7789"; + horizontal-resolution = <320>; + vertical-resolution = <170>; + gap-x = <35>; + swap-xy; + mirror-y; + invert-color; + pixel-clock-hz = <80000000>; + pin-dc = <&gpio0 16 GPIO_FLAG_NONE>; + backlight = <&display_backlight>; + }; + + sdcard@1 { + compatible = "espressif,esp32-sdspi"; + status = "disabled"; + frequency-khz = <20000>; + }; + + // TODO - Add CC1101 Radio - CS GPIO 12, SCK GPIO 11, MOSI GPIO 9, MISO GPIO 10, IO2 GPIO 38, IO0 GPIO 3, SW1 GPIO 47, SW0 GPIO 48 - driver needed + }; + + encoder { + compatible = "tactility,gpio-encoder"; + pin-a = <&gpio0 5 GPIO_FLAG_NONE>; + pin-b = <&gpio0 4 GPIO_FLAG_NONE>; + pin-enter = <&gpio0 0 GPIO_FLAG_NONE>; + pulses-per-detent = <2>; + pending-capacity = <8>; + }; + + sleep { + compatible = "espressif,esp32-sleep"; + wakeup-gpios = <&gpio0 6 WAKEUP_BUTTON_FLAGS>; + }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; +}; diff --git a/Devices/lilygo-tembed-cc1101/module.yaml b/Devices/lilygo-tembed-cc1101/module.yaml new file mode 100644 index 000000000..0266a9e4a --- /dev/null +++ b/Devices/lilygo-tembed-cc1101/module.yaml @@ -0,0 +1,11 @@ +dependencies: + - Platforms/platform-esp32 + - Drivers/bq27220-module + - Drivers/bq25896-module + - Drivers/gpio-encoder-module + - Drivers/st7789-module + - Drivers/dummy-i2s-amp-module + - Drivers/pdm-mic-module + - Drivers/audio-stream-module + - Drivers/esp32-led-strip-module +dts: lilygo,tembed-cc1101.dts diff --git a/Devices/lilygo-tembed-cc1101/source/module.cpp b/Devices/lilygo-tembed-cc1101/source/module.cpp new file mode 100644 index 000000000..359657581 --- /dev/null +++ b/Devices/lilygo-tembed-cc1101/source/module.cpp @@ -0,0 +1,49 @@ +#include +#include +#include +#include +#include +#include +#include + +constexpr auto* TAG = "tembed-cc1101-plus"; + +extern "C" { + +static void on_boot_completed(SystemEvent* event, void* context) { + (void)event; + (void)context; + Device* device = nullptr; + if (device_get_first_by_type(&LED_STRIP_TYPE, &device) != ERROR_NONE) { + LOG_W(TAG, "No LED strip found during boot restore"); + return; + } + + const auto settings = tt::settings::ledstrip::loadOrGetDefault(device->name); + const error_t result = tt::settings::ledstrip::apply(device, settings); + if (result != ERROR_NONE) { + LOG_E(TAG, "Failed to restore LED strip %s: %d", device->name, result); + } else { + LOG_I(TAG, "Restored LED strip %s", device->name); + } + device_put(device); +} + +static error_t start() { + return system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr); +} + +static error_t stop() { + return system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed); +} + +Module lilygo_tembed_cc1101_module = { + .name = "lilygo-tembed-cc1101", + .start = start, + .stop = stop, + .drivers = nullptr, + .symbols = nullptr, + .internal = nullptr +}; + +} diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-sleep.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-sleep.yaml new file mode 100644 index 000000000..da702e3de --- /dev/null +++ b/Platforms/platform-esp32/bindings/espressif,esp32-sleep.yaml @@ -0,0 +1,16 @@ +description: > + ESP32 deep sleep controller. Wakes the system from deep sleep when any of the wakeup pins + becomes active. All pins must be RTC/LP capable and share the same active level. On the + ESP32 only a single active-low pin is supported. The pins are not claimed, so they can + be owned by another driver (e.g. a button) while the system is awake. + +include: ["sleep-controller.yaml"] + +compatible: "espressif,esp32-sleep" + +properties: + wakeup-gpios: + type: phandle-array + element-type: "struct GpioPinSpec" + required: true + description: GPIO pins that wake the system. Use GPIO_FLAG_ACTIVE_LOW for buttons that pull to ground. diff --git a/Platforms/platform-esp32/include/tactility/bindings/esp32_sleep.h b/Platforms/platform-esp32/include/tactility/bindings/esp32_sleep.h new file mode 100644 index 000000000..bfa76c160 --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/bindings/esp32_sleep.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DEFINE_DEVICETREE(esp32_sleep, struct Esp32SleepConfig) + +#ifdef __cplusplus +} +#endif diff --git a/Platforms/platform-esp32/include/tactility/drivers/esp32_sleep.h b/Platforms/platform-esp32/include/tactility/drivers/esp32_sleep.h new file mode 100644 index 000000000..161d1cd69 --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/drivers/esp32_sleep.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Esp32SleepConfig { + /** Pins that wake the system from deep sleep when they become active */ + struct GpioPinSpec* wakeup_gpios; + /** The item count of wakeup_gpios */ + uint8_t wakeup_gpios_count; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Platforms/platform-esp32/source/drivers/esp32_sleep.cpp b/Platforms/platform-esp32/source/drivers/esp32_sleep.cpp new file mode 100644 index 000000000..c29c3ecd6 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/esp32_sleep.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define TAG "esp32_sleep" +#define GET_CONFIG(device) (static_cast((device)->config)) + +static constexpr TickType_t RELEASE_TIMEOUT = pdMS_TO_TICKS(2000); +static constexpr TickType_t RELEASE_POLL_INTERVAL = pdMS_TO_TICKS(10); + +extern "C" { + +static bool is_active_low(const GpioPinSpec& spec) { + return (spec.flags & GPIO_FLAG_ACTIVE_LOW) != 0; +} + +static bool any_pin_active(const Esp32SleepConfig* config) { + for (uint8_t i = 0; i < config->wakeup_gpios_count; i++) { + const auto& spec = config->wakeup_gpios[i]; + bool physical_high = gpio_get_level(static_cast(spec.pin)) != 0; + if (physical_high != is_active_low(spec)) { + return true; + } + } + return false; +} + +static error_t validate_config(const Esp32SleepConfig* config) { + if (config->wakeup_gpios_count == 0) { + return ERROR_INVALID_ARGUMENT; + } + bool active_low = is_active_low(config->wakeup_gpios[0]); + for (uint8_t i = 0; i < config->wakeup_gpios_count; i++) { + const auto& spec = config->wakeup_gpios[i]; + if (!esp_sleep_is_valid_wakeup_gpio(static_cast(spec.pin))) { + LOG_E(TAG, "GPIO %d can't wake from deep sleep", (int)spec.pin); + return ERROR_NOT_SUPPORTED; + } + if (is_active_low(spec) != active_low) { + LOG_E(TAG, "Wakeup pins must share the same active level"); + return ERROR_NOT_SUPPORTED; + } + } +#if CONFIG_IDF_TARGET_ESP32 + // ESP32 EXT1 can only wake on all pins low, so active-low wakeup uses EXT0 with a single pin + if (active_low && config->wakeup_gpios_count > 1) { + LOG_E(TAG, "ESP32 supports only a single active-low wakeup pin"); + return ERROR_NOT_SUPPORTED; + } +#endif + return ERROR_NONE; +} + +static error_t start_device(Device* device) { + return validate_config(GET_CONFIG(device)); +} + +static error_t deep_sleep(Device* device) { + const auto* config = GET_CONFIG(device); + + uint64_t mask = 0; + for (uint8_t i = 0; i < config->wakeup_gpios_count; i++) { + const auto& spec = config->wakeup_gpios[i]; + auto pin = static_cast(spec.pin); + gpio_input_enable(pin); + if (spec.flags & GPIO_FLAG_PULL_UP) { + gpio_pullup_en(pin); + } + if (spec.flags & GPIO_FLAG_PULL_DOWN) { + gpio_pulldown_en(pin); + } + mask |= 1ULL << spec.pin; + } + + // Sleeping while a wakeup pin is still active wakes the system right away + TickType_t waited = 0; + while (any_pin_active(config) && waited < RELEASE_TIMEOUT) { + vTaskDelay(RELEASE_POLL_INTERVAL); + waited += RELEASE_POLL_INTERVAL; + } + + // Digital pull resistors are powered down during deep sleep, so the RTC pulls hold the pin state + bool needs_rtc_pulls = false; + for (uint8_t i = 0; i < config->wakeup_gpios_count; i++) { + const auto& spec = config->wakeup_gpios[i]; + auto pin = static_cast(spec.pin); + if (spec.flags & GPIO_FLAG_PULL_UP) { + rtc_gpio_pullup_en(pin); + needs_rtc_pulls = true; + } + if (spec.flags & GPIO_FLAG_PULL_DOWN) { + rtc_gpio_pulldown_en(pin); + needs_rtc_pulls = true; + } + } + if (needs_rtc_pulls) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + } + + bool active_low = is_active_low(config->wakeup_gpios[0]); + esp_err_t esp_error; +#if CONFIG_IDF_TARGET_ESP32 + if (active_low) { + esp_error = esp_sleep_enable_ext0_wakeup(static_cast(config->wakeup_gpios[0].pin), 0); + } else { + esp_error = esp_sleep_enable_ext1_wakeup_io(mask, ESP_EXT1_WAKEUP_ANY_HIGH); + } +#else + esp_error = esp_sleep_enable_ext1_wakeup_io(mask, active_low ? ESP_EXT1_WAKEUP_ANY_LOW : ESP_EXT1_WAKEUP_ANY_HIGH); +#endif + if (esp_error != ESP_OK) { + LOG_E(TAG, "Failed to configure wakeup: %s", esp_err_to_name(esp_error)); + return esp_err_to_error(esp_error); + } + + LOG_I(TAG, "Entering deep sleep"); + esp_deep_sleep_start(); + return ERROR_NONE; +} + +static const SleepControllerApi esp32_sleep_api = { + .deep_sleep = deep_sleep +}; + +Driver esp32_sleep_driver = { + .name = "esp32_sleep", + .compatible = (const char*[]) { "espressif,esp32-sleep", nullptr }, + .start_device = start_device, + .stop_device = nullptr, + .api = &esp32_sleep_api, + .device_type = &SLEEP_CONTROLLER_TYPE, + .internal = nullptr +}; + +} // extern "C" diff --git a/Platforms/platform-esp32/source/module.cpp b/Platforms/platform-esp32/source/module.cpp index f63e7dd47..6d5d1b579 100644 --- a/Platforms/platform-esp32/source/module.cpp +++ b/Platforms/platform-esp32/source/module.cpp @@ -339,6 +339,7 @@ extern Driver esp32_pwm_ledc_driver; extern Driver esp32_sdmmc_driver; #endif extern Driver esp32_sdspi_driver; +extern Driver esp32_sleep_driver; extern Driver esp32_spi_driver; extern Driver esp32_uart_driver; extern Driver esp32_grove_driver; @@ -391,6 +392,7 @@ static Driver* const platform_esp32_drivers[] = { &esp32_sdmmc_driver, #endif &esp32_sdspi_driver, + &esp32_sleep_driver, &esp32_spi_driver, &esp32_uart_driver, &esp32_grove_driver, diff --git a/Tactility/Source/app/power/Power.cpp b/Tactility/Source/app/power/Power.cpp index b58baed18..d0fadaa00 100644 --- a/Tactility/Source/app/power/Power.cpp +++ b/Tactility/Source/app/power/Power.cpp @@ -78,10 +78,6 @@ void setPropertyLabelText(lv_obj_t* label, PowerSupplyProperty property, const P } void updateUi(Context* ctx) { - if (ctx->entries.empty()) { - return; - } - lvgl_lock(); for (auto& entry : ctx->entries) { @@ -268,6 +264,11 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } +void destroyWidgets(void* userData) { + auto* ctx = static_cast(userData); + ctx->entries.clear(); +} + int32_t appMain(int argc, char* argv[]) { uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; @@ -286,7 +287,7 @@ int32_t appMain(int argc, char* argv[]) { AppEventSubscription sub {}; check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); - WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); + WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); ctx.timer->start(); bool shouldClose = false; diff --git a/Tactility/Source/app/poweroff/PowerOff.cpp b/Tactility/Source/app/poweroff/PowerOff.cpp index b97703bbe..3043f31b2 100644 --- a/Tactility/Source/app/poweroff/PowerOff.cpp +++ b/Tactility/Source/app/poweroff/PowerOff.cpp @@ -11,12 +11,17 @@ #include #include #include +#include #include #include #include +#include +#include namespace tt::app::poweroff { +constexpr auto* TAG = "PowerOff"; + extern const ::AppManifest manifest; namespace { @@ -26,7 +31,7 @@ struct Context { }; -void showPoweredOffScreen() { +lv_obj_t* showPoweredOffScreen(const char* subtitleText) { auto* screen = lv_obj_create(nullptr); lv_obj_set_style_bg_color(screen, lv_color_white(), 0); lv_obj_set_flex_flow(screen, LV_FLEX_FLOW_COLUMN); @@ -38,10 +43,11 @@ void showPoweredOffScreen() { lv_obj_set_style_text_color(title, lv_color_black(), 0); auto* subtitle = lv_label_create(screen); - lv_label_set_text(subtitle, "Powered off"); + lv_label_set_text(subtitle, subtitleText); lv_obj_set_style_text_color(subtitle, lv_color_black(), 0); lv_screen_load(screen); + return screen; } bool anyDeviceSupportsPowerOff() { @@ -56,48 +62,97 @@ bool anyDeviceSupportsPowerOff() { return any_supported; } -void onYesPressed(lv_event_t* /*event*/) { - if (!anyDeviceSupportsPowerOff()) { - return; - } +Device* findSleepController() { + Device* found = nullptr; + device_for_each_of_type(&SLEEP_CONTROLLER_TYPE, &found, [](Device* device, void* context) { + if (device_is_ready(device)) { + *static_cast(context) = device; + return false; + } + return true; + }); + return found; +} +bool powerOffAll() { + bool any_succeeded = false; + device_for_each_of_type(&POWER_SUPPLY_TYPE, &any_succeeded, [](Device* device, void* context) { + if (device_is_ready(device) && power_supply_supports_power_off(device)) { + error_t error = power_supply_power_off(device); + if (error == ERROR_NONE) { + *static_cast(context) = true; + } else { + LOG_E(TAG, "Power off failed for %s: %s", device->name, error_to_string(error)); + } + } + return true; + }); + return any_succeeded; +} + +bool deepSleep() { + auto* sleep_controller = findSleepController(); + if (sleep_controller == nullptr) { + LOG_E(TAG, "Sleep controller not found"); + return false; + } + error_t error = sleep_controller_deep_sleep(sleep_controller); + if (error != ERROR_NONE) { + LOG_E(TAG, "Deep sleep failed: %s", error_to_string(error)); + return false; + } + return true; +} + +void runWithShutdownScreen(const char* subtitle, bool (*action)()) { Device* display; error_t error = device_get_first_by_type(&DISPLAY_TYPE, &display); // TODO: remove this logic path when all displays have been migrated to kernel display drivers if (error != ERROR_NONE) { - // No display, power off now - device_for_each_of_type(&POWER_SUPPLY_TYPE, nullptr, [](Device* device, void* /*context*/) { - if (device_is_ready(device) && power_supply_supports_power_off(device)) { - power_supply_power_off(device); - } - return true; - }); + action(); return; } bool is_slow_refresh = display_has_capability(display, DISPLAY_CAPABILITY_SLOW_REFRESH); + lv_obj_t* previous_screen = nullptr; + lv_obj_t* shutdown_screen = nullptr; if (is_slow_refresh) { auto* lvgl_display = lv_display_get_default(); - showPoweredOffScreen(); + previous_screen = lv_screen_active(); + shutdown_screen = showPoweredOffScreen(subtitle); if (lvgl_display != nullptr) { lv_refr_now(lvgl_display); } } - getMainDispatcher().dispatch([is_slow_refresh] { + getMainDispatcher().dispatch([is_slow_refresh, action, previous_screen, shutdown_screen] { // Not necessary for LilyGO Paper S3, but other drivers with async rendering might need us to wait a bit. if (is_slow_refresh) { vTaskDelay(pdMS_TO_TICKS(2000)); } - device_for_each_of_type(&POWER_SUPPLY_TYPE, nullptr, [](Device* device, void* /*context*/) { - if (device_is_ready(device) && power_supply_supports_power_off(device)) { - power_supply_power_off(device); - } - return true; - }); + if (!action() && shutdown_screen != nullptr) { + lvgl_lock(); + lv_screen_load(previous_screen); + lv_obj_delete(shutdown_screen); + lvgl_unlock(); + } }); } +void onYesPressed(lv_event_t* /*event*/) { + if (!anyDeviceSupportsPowerOff()) { + return; + } + runWithShutdownScreen("Powered off", powerOffAll); +} + +void onSleepPressed(lv_event_t* /*event*/) { + if (findSleepController() == nullptr) { + return; + } + runWithShutdownScreen("Sleeping", deepSleep); +} + void onNoPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); app_event_emit_close(ctx->appInstanceId); @@ -109,8 +164,11 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(parent, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + bool supports_power_off = anyDeviceSupportsPowerOff(); + bool supports_sleep = findSleepController() != nullptr; + auto* label = lv_label_create(parent); - lv_label_set_text(label, "Power off?"); + lv_label_set_text(label, supports_power_off ? "Power off?" : "Sleep?"); lv_obj_set_style_text_font(label, lvgl_get_text_font(FONT_SIZE_LARGE), 0); auto* button_wrapper = lv_obj_create(parent); @@ -125,19 +183,33 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_pad_ver(button_wrapper, 6, LV_STATE_DEFAULT); lv_obj_set_style_pad_hor(button_wrapper, 6, LV_STATE_DEFAULT); - auto* yes_button = lv_button_create(button_wrapper); - auto* yes_label = lv_label_create(yes_button); - lv_label_set_text(yes_label, "Yes"); - lv_obj_add_event_cb(yes_button, onYesPressed, LV_EVENT_SHORT_CLICKED, nullptr); + lv_obj_t* first_button = nullptr; + if (supports_power_off) { + auto* yes_button = lv_button_create(button_wrapper); + auto* yes_label = lv_label_create(yes_button); + lv_label_set_text(yes_label, "Yes"); + lv_obj_add_event_cb(yes_button, onYesPressed, LV_EVENT_SHORT_CLICKED, nullptr); + first_button = yes_button; + } + + if (supports_sleep) { + auto* sleep_button = lv_button_create(button_wrapper); + auto* sleep_label = lv_label_create(sleep_button); + lv_label_set_text(sleep_label, "Sleep"); + lv_obj_add_event_cb(sleep_button, onSleepPressed, LV_EVENT_SHORT_CLICKED, nullptr); + if (first_button == nullptr) { + first_button = sleep_button; + } + } auto* no_button = lv_button_create(button_wrapper); auto* no_label = lv_label_create(no_button); lv_label_set_text(no_label, "No"); lv_obj_add_event_cb(no_button, onNoPressed, LV_EVENT_SHORT_CLICKED, ctx); - if (!lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) { - lv_group_focus_obj(yes_button); - lv_obj_add_state(yes_button, LV_STATE_FOCUS_KEY); + if (first_button != nullptr && !lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) { + lv_group_focus_obj(first_button); + lv_obj_add_state(first_button, LV_STATE_FOCUS_KEY); } } diff --git a/Tactility/Source/lvgl/SystemMenu.cpp b/Tactility/Source/lvgl/SystemMenu.cpp index 3ee1e3095..ec3f5c4a4 100644 --- a/Tactility/Source/lvgl/SystemMenu.cpp +++ b/Tactility/Source/lvgl/SystemMenu.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -184,6 +185,13 @@ static bool supportsPowerOff() { } return true; }); + device_for_each_of_type(&SLEEP_CONTROLLER_TYPE, &supported, [](Device* device, void* context) { + if (device_is_ready(device)) { + *static_cast(context) = true; + return false; + } + return true; + }); return supported; } diff --git a/TactilityKernel/bindings/sleep-controller.yaml b/TactilityKernel/bindings/sleep-controller.yaml new file mode 100644 index 000000000..ee38b4185 --- /dev/null +++ b/TactilityKernel/bindings/sleep-controller.yaml @@ -0,0 +1 @@ +description: Sleep controller diff --git a/TactilityKernel/include/tactility/drivers/sleep_controller.h b/TactilityKernel/include/tactility/drivers/sleep_controller.h new file mode 100644 index 000000000..2dc483602 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/sleep_controller.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * @brief API for sleep controller drivers. + */ +struct SleepControllerApi { + /** + * @brief Puts the system into deep sleep, configured with the driver's wakeup sources. + * @param[in] device the sleep controller device + * @retval ERROR_NOT_SUPPORTED when the configured wakeup sources are not supported + * @retval ERROR_NONE when the operation was successful (this call typically does not return) + */ + error_t (*deep_sleep)(struct Device* device); +}; + +/** + * @brief Puts the system into deep sleep using the specified sleep controller device. + * Emits KERNEL_EVENT_DEEP_SLEEP_PREPARE synchronously before entering deep sleep. + */ +error_t sleep_controller_deep_sleep(struct Device* device); + +extern const struct DeviceType SLEEP_CONTROLLER_TYPE; + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/system_event.h b/TactilityKernel/include/tactility/system_event.h index 7ebd6a176..1d6c032a9 100644 --- a/TactilityKernel/include/tactility/system_event.h +++ b/TactilityKernel/include/tactility/system_event.h @@ -25,6 +25,7 @@ enum SystemEventType { KERNEL_EVENT_SERVICE_STARTED, // ServiceStartedEvent KERNEL_EVENT_SERVICE_STOPPED, // ServiceStoppedEvent KERNEL_EVENT_TIME_CHANGED, // No data - fired whenever system time is set (NTP sync, RTC restore, manual change) + KERNEL_EVENT_DEEP_SLEEP_PREPARE, // No data - fired synchronously right before the system enters deep sleep }; /** Data for KERNEL_EVENT_NETWORK_CONNECTED. */ diff --git a/TactilityKernel/source/drivers/sleep_controller.cpp b/TactilityKernel/source/drivers/sleep_controller.cpp new file mode 100644 index 000000000..1231f0f08 --- /dev/null +++ b/TactilityKernel/source/drivers/sleep_controller.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#define SLEEP_CONTROLLER_DRIVER_API(driver) ((SleepControllerApi*)driver->api) + +extern "C" { + +error_t sleep_controller_deep_sleep(Device* device) { + system_event_emit(KERNEL_EVENT_DEEP_SLEEP_PREPARE, nullptr, 0); + const auto* driver = device_get_driver(device); + return SLEEP_CONTROLLER_DRIVER_API(driver)->deep_sleep(device); +} + +const DeviceType SLEEP_CONTROLLER_TYPE { + .name = "sleep_controller" +}; + +} diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 9317bcc48..261f62691 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -326,6 +327,9 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(RTC_TYPE), // drivers/sdcard DEFINE_MODULE_SYMBOL(SDCARD_TYPE), + // drivers/sleep_controller + DEFINE_MODULE_SYMBOL(sleep_controller_deep_sleep), + DEFINE_MODULE_SYMBOL(SLEEP_CONTROLLER_TYPE), // drivers/spi_controller DEFINE_MODULE_SYMBOL(spi_controller_lock), DEFINE_MODULE_SYMBOL(spi_controller_try_lock), diff --git a/TactilityKernel/tests/source/sleep_controller_test.cpp b/TactilityKernel/tests/source/sleep_controller_test.cpp new file mode 100644 index 000000000..c1f8edd78 --- /dev/null +++ b/TactilityKernel/tests/source/sleep_controller_test.cpp @@ -0,0 +1,60 @@ +#include "doctest.h" + +#include +#include +#include +#include + +static int call_order = 0; +static int prepare_event_order = 0; +static int deep_sleep_order = 0; + +static void on_deep_sleep_prepare(SystemEvent* /*event*/, void* /*context*/) { + prepare_event_order = ++call_order; +} + +static error_t fake_deep_sleep(Device* /*device*/) { + deep_sleep_order = ++call_order; + return ERROR_NOT_SUPPORTED; +} + +static const SleepControllerApi fake_sleep_controller_api = { + .deep_sleep = fake_deep_sleep +}; + +static Driver fake_sleep_controller_driver = { + .name = "fake_sleep_controller_driver", + .compatible = (const char*[]) { "sleep_controller_test,fake", nullptr }, + .start_device = nullptr, + .stop_device = nullptr, + .api = &fake_sleep_controller_api, + .device_type = &SLEEP_CONTROLLER_TYPE, + .internal = nullptr, +}; + +TEST_CASE("sleep_controller_deep_sleep emits the prepare event before calling the driver") { + call_order = 0; + prepare_event_order = 0; + deep_sleep_order = 0; + + static Device fake_device { + .name = "fake_sleep_controller_device", + .config = nullptr, + .parent = nullptr, + }; + + CHECK_EQ(driver_construct_add(&fake_sleep_controller_driver), ERROR_NONE); + CHECK_EQ(device_construct_add(&fake_device, "sleep_controller_test,fake"), ERROR_NONE); + CHECK_EQ(device_start(&fake_device), ERROR_NONE); + CHECK_EQ(system_event_callback_add(KERNEL_EVENT_DEEP_SLEEP_PREPARE, on_deep_sleep_prepare, nullptr), ERROR_NONE); + + CHECK_EQ(sleep_controller_deep_sleep(&fake_device), ERROR_NOT_SUPPORTED); + CHECK_EQ(prepare_event_order, 1); + CHECK_EQ(deep_sleep_order, 2); + + CHECK_EQ(system_event_callback_remove(KERNEL_EVENT_DEEP_SLEEP_PREPARE, on_deep_sleep_prepare), ERROR_NONE); + CHECK_EQ(device_stop(&fake_device), ERROR_NONE); + CHECK_EQ(device_remove(&fake_device), ERROR_NONE); + CHECK_EQ(device_destruct(&fake_device), ERROR_NONE); + CHECK_EQ(driver_remove_destruct(&fake_sleep_controller_driver), ERROR_NONE); +}