diff --git a/Buildscripts/TactilitySDK/CMakeLists.txt b/Buildscripts/TactilitySDK/CMakeLists.txt index 49d6f1025..5ee600eb2 100644 --- a/Buildscripts/TactilitySDK/CMakeLists.txt +++ b/Buildscripts/TactilitySDK/CMakeLists.txt @@ -5,12 +5,7 @@ idf_component_register( "Libraries/TactilityFreeRtos/include" "Libraries/lvgl/include" "Modules/lvgl-module/include" - "Drivers/bm8563-module/include" - "Drivers/bmi270-module/include" - "Drivers/mpu6886-module/include" - "Drivers/pi4ioe5v6408-module/include" - "Drivers/qmi8658-module/include" - "Drivers/rx8130ce-module/include" + # DRIVER_INCLUDE_DIRS_PLACEHOLDER REQUIRES esp_timer ) diff --git a/Buildscripts/TactilitySDK/TactilitySDK.cmake b/Buildscripts/TactilitySDK/TactilitySDK.cmake index 78eb67c97..71af31dd7 100644 --- a/Buildscripts/TactilitySDK/TactilitySDK.cmake +++ b/Buildscripts/TactilitySDK/TactilitySDK.cmake @@ -25,12 +25,7 @@ macro(tactility_project project_name) set(COMPONENTS TactilityFreeRtos - bm8563-module - bmi270-module - mpu6886-module - pi4ioe5v6408-module - qmi8658-module - rx8130ce-module + # DRIVER_COMPONENTS_PLACEHOLDER ) endmacro() diff --git a/Buildscripts/release-sdk.py b/Buildscripts/release-sdk.py index 72fafc88c..54b16b58d 100644 --- a/Buildscripts/release-sdk.py +++ b/Buildscripts/release-sdk.py @@ -88,6 +88,17 @@ def write_module_cmakelists(path, content): with open(path, 'w') as f: f.write(content) +def driver_is_available(driver_name): + """ + Some drivers only build for certain chip targets (e.g. sc2356-module is ESP32-P4 only, + since it depends on esp_video/esp_cam_sensor/PPA which are themselves chip-restricted). + Build output presence is the single source of truth for "does this driver support the + current target" - no separate manifest to keep in sync with the real CMakeLists.txt + REQUIRES/Kconfig guards. + """ + binary_pattern = f'build/esp-idf/{driver_name}/lib{driver_name}.a' + return bool(glob.glob(binary_pattern)) + def add_driver(target_path, driver_name): mappings = get_driver_mappings(driver_name) map_copy(mappings, target_path) @@ -100,6 +111,44 @@ def add_module(target_path, module_name): cmakelists_content = create_module_cmakelists(module_name) write_module_cmakelists(os.path.join(target_path, f"Modules/{module_name}/CMakeLists.txt"), cmakelists_content) +def discover_all_drivers(): + """ + Discover all *-module directories under Drivers/ (not Modules/ - those are handled + separately via add_module). Sorted for deterministic output across OS/filesystem order. + """ + pattern = os.path.join('Drivers', '*-module') + return sorted( + os.path.basename(p) for p in glob.glob(pattern) if os.path.isdir(p) + ) + +def generate_tactility_sdk_cmake(target_path, available_drivers): + src = os.path.join('Buildscripts', 'TactilitySDK', 'TactilitySDK.cmake') + with open(src) as f: + content = f.read() + placeholder = " # DRIVER_COMPONENTS_PLACEHOLDER" + assert placeholder in content, \ + f"Placeholder '{placeholder.strip()}' not found in {src} - template drifted, generator needs updating" + components = "\n".join(f" {d}" for d in available_drivers) + new_content = content.replace(placeholder, components) + assert placeholder not in new_content, \ + f"Placeholder '{placeholder.strip()}' still present after replacement in {src}" + with open(os.path.join(target_path, 'TactilitySDK.cmake'), 'w') as f: + f.write(new_content) + +def generate_tactility_sdk_top_cmakelists(target_path, available_drivers): + src = os.path.join('Buildscripts', 'TactilitySDK', 'CMakeLists.txt') + with open(src) as f: + content = f.read() + placeholder = " # DRIVER_INCLUDE_DIRS_PLACEHOLDER" + assert placeholder in content, \ + f"Placeholder '{placeholder.strip()}' not found in {src} - template drifted, generator needs updating" + include_dirs = "\n".join(f' "Drivers/{d}/include"' for d in available_drivers) + new_content = content.replace(placeholder, include_dirs) + assert placeholder not in new_content, \ + f"Placeholder '{placeholder.strip()}' still present after replacement in {src}" + with open(os.path.join(target_path, 'CMakeLists.txt'), 'w') as f: + f.write(new_content) + def main(): if len(sys.argv) < 2: print("Usage: release-sdk.py [target_path]") @@ -136,9 +185,6 @@ def main(): # elf_loader {'src': 'Libraries/elf_loader/elf_loader.cmake', 'dst': 'Libraries/elf_loader/'}, {'src': 'Libraries/elf_loader/license.txt', 'dst': 'Libraries/elf_loader/'}, - # Final scripts - {'src': 'Buildscripts/TactilitySDK/TactilitySDK.cmake', 'dst': ''}, - {'src': 'Buildscripts/TactilitySDK/CMakeLists.txt', 'dst': ''}, ] map_copy(mappings, target_path) @@ -147,13 +193,16 @@ def main(): add_module(target_path, "lvgl-module") add_module(target_path, "crypt-module") - # Drivers - add_driver(target_path, "bm8563-module") - add_driver(target_path, "bmi270-module") - add_driver(target_path, "mpu6886-module") - add_driver(target_path, "pi4ioe5v6408-module") - add_driver(target_path, "qmi8658-module") - add_driver(target_path, "rx8130ce-module") + # Drivers - only ones actually built for this target (chip-restricted drivers like + # sc2356-module won't have a .a outside ESP32-P4) + available_drivers = [d for d in discover_all_drivers() if driver_is_available(d)] + for driver_name in available_drivers: + add_driver(target_path, driver_name) + + # Final scripts - generated (not copied verbatim) so COMPONENTS/INCLUDE_DIRS only list + # drivers actually available for this target + generate_tactility_sdk_cmake(target_path, available_drivers) + generate_tactility_sdk_top_cmakelists(target_path, available_drivers) # Output ESP-IDF SDK version to file esp_idf_version = os.environ.get("ESP_IDF_VERSION", "") diff --git a/Devices/generic-esp32p4/devicetree.yaml b/Devices/generic-esp32p4/devicetree.yaml index 9f5e26586..c700c1ced 100644 --- a/Devices/generic-esp32p4/devicetree.yaml +++ b/Devices/generic-esp32p4/devicetree.yaml @@ -7,4 +7,5 @@ dependencies: - Drivers/pi4ioe5v6408-module - Drivers/qmi8658-module - Drivers/rx8130ce-module + - Drivers/sc2356-module dts: generic,esp32p4.dts diff --git a/Devices/m5stack-tab5/CMakeLists.txt b/Devices/m5stack-tab5/CMakeLists.txt index ed36355cf..25bc75361 100644 --- a/Devices/m5stack-tab5/CMakeLists.txt +++ b/Devices/m5stack-tab5/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c esp_lcd_st7123 esp_lcd_touch_st7123 GT911 PwmBacklight driver esp_driver_i2c vfs fatfs ina226-module + REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c esp_lcd_st7123 esp_lcd_touch_st7123 GT911 PwmBacklight driver esp_driver_i2c vfs fatfs ina226-module sc2356-module ) diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp index 9927116e1..4f6c3eabb 100644 --- a/Devices/m5stack-tab5/Source/Configuration.cpp +++ b/Devices/m5stack-tab5/Source/Configuration.cpp @@ -7,6 +7,8 @@ #include +#include + using namespace tt::hal; static constexpr auto* TAG = "Tab5"; @@ -249,6 +251,29 @@ static error_t initMicrophone(::Device* i2c_controller) { return error; } +static esp_clock_output_mapping_handle_t camera_osc_handle = nullptr; + +static void initCameraOsc() { + if (camera_osc_handle != nullptr) { + return; + } + + // 24 MHz clock on GPIO 36 for the SC2356 MIPI CSI sensor, required before esp_video_init. + // Uses SPLL (480 MHz) via esp_clock_output with divider 20 = 24 MHz exactly. + // This avoids any LEDC clock source conflict with the display backlight. + if (esp_clock_output_start(CLKOUT_SIG_SPLL, GPIO_NUM_36, &camera_osc_handle) != ESP_OK) { + LOG_E(TAG, "Camera OSC clock output start failed"); + return; + } + if (esp_clock_output_set_divider(camera_osc_handle, 20) != ESP_OK) { + LOG_E(TAG, "Camera OSC clock divider set failed"); + esp_clock_output_stop(camera_osc_handle); + camera_osc_handle = nullptr; + return; + } + LOG_I(TAG, "Camera OSC 24MHz started on GPIO 36 (SPLL/20)"); +} + static bool initBoot() { auto* i2c0 = device_find_by_name("i2c0"); check(i2c0, "i2c0 not found"); @@ -258,6 +283,7 @@ static bool initBoot() { auto* io_expander1 = device_find_by_name("io_expander1"); check(io_expander1, "io_expander1 not found"); + initCameraOsc(); initExpander0(io_expander0); initExpander1(io_expander1); diff --git a/Devices/m5stack-tab5/device.properties b/Devices/m5stack-tab5/device.properties index 8c11c4b69..31683c46f 100644 --- a/Devices/m5stack-tab5/device.properties +++ b/Devices/m5stack-tab5/device.properties @@ -45,3 +45,7 @@ sdkconfig.CONFIG_CACHE_L2_CACHE_256KB=y sdkconfig.CONFIG_LVGL_PORT_ENABLE_PPA=y sdkconfig.CONFIG_LV_DRAW_BUF_ALIGN=64 sdkconfig.CONFIG_LV_DEF_REFR_PERIOD=15 +# SC202CS (SC2356) MIPI CSI camera sensor + ISP pipeline (AE/AWB/demosaicing) +sdkconfig.CONFIG_CAMERA_SC202CS=y +sdkconfig.CONFIG_CAMERA_SC202CS_AUTO_DETECT_MIPI_INTERFACE_SENSOR=y +sdkconfig.CONFIG_ESP_VIDEO_ENABLE_ISP_PIPELINE_CONTROLLER=y diff --git a/Devices/m5stack-tab5/devicetree.yaml b/Devices/m5stack-tab5/devicetree.yaml index e3f5b1abb..1135e2a39 100644 --- a/Devices/m5stack-tab5/devicetree.yaml +++ b/Devices/m5stack-tab5/devicetree.yaml @@ -4,4 +4,5 @@ dependencies: - Drivers/bmi270-module - Drivers/ina226-module - Drivers/rx8130ce-module +- Drivers/sc2356-module dts: m5stack,tab5.dts diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index dbbf11a94..7db61c6e8 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -13,6 +13,7 @@ #include #include #include +#include / { compatible = "root"; @@ -60,6 +61,11 @@ reg = <0x41>; shunt-milliohms = <5>; }; + + sc2356 { + compatible = "smartsens,sc2356"; + reg = <0x36>; + }; }; port_a: grove0 { diff --git a/Documentation/README.md b/Documentation/README.md index af6aa2fbd..b92e05374 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -8,6 +8,8 @@ Tactility is an operating system for the ESP32 microcontroller family. It runs o ### Simulator (Linux/macOS, no ESP-IDF needed) +Simulator is not supported on Windows + ```bash cmake -B buildsim -G Ninja ninja -C buildsim # build firmware + tests diff --git a/Drivers/bm8563-module/include/drivers/bm8563.h b/Drivers/bm8563-module/include/drivers/bm8563.h index dc5b57748..b7038d729 100644 --- a/Drivers/bm8563-module/include/drivers/bm8563.h +++ b/Drivers/bm8563-module/include/drivers/bm8563.h @@ -2,6 +2,7 @@ #pragma once #include +#include #include struct Device; @@ -15,30 +16,21 @@ struct Bm8563Config { uint8_t address; }; -struct Bm8563DateTime { - uint16_t year; // 2000–2199 - uint8_t month; // 1–12 - uint8_t day; // 1–31 - uint8_t hour; // 0–23 - uint8_t minute; // 0–59 - uint8_t second; // 0–59 -}; - /** * Read the current date and time from the RTC. * @param[in] device bm8563 device - * @param[out] dt Pointer to Bm8563DateTime to populate + * @param[out] dt Pointer to RtcDateTime to populate * @return ERROR_NONE on success */ -error_t bm8563_get_datetime(struct Device* device, struct Bm8563DateTime* dt); +error_t bm8563_get_datetime(struct Device* device, struct RtcDateTime* dt); /** * Write the date and time to the RTC. * @param[in] device bm8563 device - * @param[in] dt Pointer to Bm8563DateTime to write (year must be 2000–2199) + * @param[in] dt Pointer to RtcDateTime to write (year must be 2000–2199) * @return ERROR_NONE on success, ERROR_INVALID_ARGUMENT if any field is out of range */ -error_t bm8563_set_datetime(struct Device* device, const struct Bm8563DateTime* dt); +error_t bm8563_set_datetime(struct Device* device, const struct RtcDateTime* dt); #ifdef __cplusplus } diff --git a/Drivers/bm8563-module/source/bm8563.cpp b/Drivers/bm8563-module/source/bm8563.cpp index 4d3f7804e..a86bf725b 100644 --- a/Drivers/bm8563-module/source/bm8563.cpp +++ b/Drivers/bm8563-module/source/bm8563.cpp @@ -52,7 +52,7 @@ static error_t stop(Device* device) { extern "C" { -error_t bm8563_get_datetime(Device* device, Bm8563DateTime* dt) { +error_t bm8563_get_datetime(Device* device, RtcDateTime* dt) { auto* i2c_controller = device_get_parent(device); auto address = GET_CONFIG(device)->address; @@ -78,7 +78,7 @@ error_t bm8563_get_datetime(Device* device, Bm8563DateTime* dt) { return ERROR_NONE; } -error_t bm8563_set_datetime(Device* device, const Bm8563DateTime* dt) { +error_t bm8563_set_datetime(Device* device, const RtcDateTime* dt) { if (dt->year < 2000 || dt->year > 2199 || dt->month < 1 || dt->month > 12 || dt->day < 1 || dt->day > 31 || @@ -104,13 +104,18 @@ error_t bm8563_set_datetime(Device* device, const Bm8563DateTime* dt) { return i2c_controller_write_register(i2c_controller, address, REG_SECONDS, buf, sizeof(buf), I2C_TIMEOUT_TICKS); } +RtcApi bm8563_rtc_api = { + .get_time = bm8563_get_datetime, + .set_time = bm8563_set_datetime +}; + Driver bm8563_driver = { .name = "bm8563", .compatible = (const char*[]) { "belling,bm8563", nullptr }, .start_device = start, .stop_device = stop, - .api = nullptr, - .device_type = nullptr, + .api = &bm8563_rtc_api, + .device_type = &RTC_TYPE, .owner = &bm8563_module, .internal = nullptr }; diff --git a/Drivers/bm8563-module/source/module.cpp b/Drivers/bm8563-module/source/module.cpp index 259e029da..5d376792f 100644 --- a/Drivers/bm8563-module/source/module.cpp +++ b/Drivers/bm8563-module/source/module.cpp @@ -21,13 +21,11 @@ static error_t stop() { return ERROR_NONE; } -extern const ModuleSymbol bm8563_module_symbols[]; - Module bm8563_module = { .name = "bm8563", .start = start, .stop = stop, - .symbols = bm8563_module_symbols, + .symbols = nullptr, .internal = nullptr }; diff --git a/Drivers/bm8563-module/source/symbols.c b/Drivers/bm8563-module/source/symbols.c deleted file mode 100644 index 43312b41b..000000000 --- a/Drivers/bm8563-module/source/symbols.c +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include - -const struct ModuleSymbol bm8563_module_symbols[] = { - DEFINE_MODULE_SYMBOL(bm8563_get_datetime), - DEFINE_MODULE_SYMBOL(bm8563_set_datetime), - MODULE_SYMBOL_TERMINATOR -}; diff --git a/Drivers/rx8130ce-module/include/drivers/rx8130ce.h b/Drivers/rx8130ce-module/include/drivers/rx8130ce.h index e348b556d..68216740b 100644 --- a/Drivers/rx8130ce-module/include/drivers/rx8130ce.h +++ b/Drivers/rx8130ce-module/include/drivers/rx8130ce.h @@ -2,6 +2,7 @@ #pragma once #include +#include #include struct Device; @@ -15,30 +16,21 @@ struct Rx8130ceConfig { uint8_t address; }; -struct Rx8130ceDateTime { - uint16_t year; // 2000–2099 - uint8_t month; // 1–12 - uint8_t day; // 1–31 - uint8_t hour; // 0–23 - uint8_t minute; // 0–59 - uint8_t second; // 0–59 -}; - /** * Read the current date and time from the RTC. * @param[in] device rx8130ce device - * @param[out] dt Pointer to Rx8130ceDateTime to populate + * @param[out] dt Pointer to RtcDateTime to populate * @return ERROR_NONE on success, ERROR_INVALID_STATE if VLF is set (clock data unreliable) */ -error_t rx8130ce_get_datetime(struct Device* device, struct Rx8130ceDateTime* dt); +error_t rx8130ce_get_datetime(struct Device* device, struct RtcDateTime* dt); /** * Write the date and time to the RTC. * @param[in] device rx8130ce device - * @param[in] dt Pointer to Rx8130ceDateTime to write (year must be 2000–2099) + * @param[in] dt Pointer to RtcDateTime to write (year must be 2000–2099) * @return ERROR_NONE on success, ERROR_INVALID_ARGUMENT if any field is out of range */ -error_t rx8130ce_set_datetime(struct Device* device, const struct Rx8130ceDateTime* dt); +error_t rx8130ce_set_datetime(struct Device* device, const struct RtcDateTime* dt); #ifdef __cplusplus } diff --git a/Drivers/rx8130ce-module/source/module.cpp b/Drivers/rx8130ce-module/source/module.cpp index dcce39ce4..e639b829b 100644 --- a/Drivers/rx8130ce-module/source/module.cpp +++ b/Drivers/rx8130ce-module/source/module.cpp @@ -21,13 +21,11 @@ static error_t stop() { return ERROR_NONE; } -extern const ModuleSymbol rx8130ce_module_symbols[]; - Module rx8130ce_module = { .name = "rx8130ce", .start = start, .stop = stop, - .symbols = rx8130ce_module_symbols, + .symbols = nullptr, .internal = nullptr }; diff --git a/Drivers/rx8130ce-module/source/rx8130ce.cpp b/Drivers/rx8130ce-module/source/rx8130ce.cpp index 33359b89b..13df61574 100644 --- a/Drivers/rx8130ce-module/source/rx8130ce.cpp +++ b/Drivers/rx8130ce-module/source/rx8130ce.cpp @@ -78,7 +78,7 @@ static error_t stop(Device* device) { extern "C" { -error_t rx8130ce_get_datetime(Device* device, Rx8130ceDateTime* dt) { +error_t rx8130ce_get_datetime(Device* device, RtcDateTime* dt) { auto* i2c_controller = device_get_parent(device); auto address = GET_CONFIG(device)->address; @@ -107,7 +107,7 @@ error_t rx8130ce_get_datetime(Device* device, Rx8130ceDateTime* dt) { return ERROR_NONE; } -error_t rx8130ce_set_datetime(Device* device, const Rx8130ceDateTime* dt) { +error_t rx8130ce_set_datetime(Device* device, const RtcDateTime* dt) { if (dt->month < 1 || dt->month > 12 || dt->day < 1 || dt->day > 31 || dt->hour > 23 || dt->minute > 59 || dt->second > 59) { @@ -151,13 +151,18 @@ error_t rx8130ce_set_datetime(Device* device, const Rx8130ceDateTime* dt) { return error; } +RtcApi rx8130ce_rtc_api = { + .get_time = rx8130ce_get_datetime, + .set_time = rx8130ce_set_datetime +}; + Driver rx8130ce_driver = { .name = "rx8130ce", .compatible = (const char*[]) { "epson,rx8130ce", nullptr }, .start_device = start, .stop_device = stop, - .api = nullptr, - .device_type = nullptr, + .api = &rx8130ce_rtc_api, + .device_type = &RTC_TYPE, .owner = &rx8130ce_module, .internal = nullptr }; diff --git a/Drivers/rx8130ce-module/source/symbols.c b/Drivers/rx8130ce-module/source/symbols.c deleted file mode 100644 index 21183d30d..000000000 --- a/Drivers/rx8130ce-module/source/symbols.c +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include - -const struct ModuleSymbol rx8130ce_module_symbols[] = { - DEFINE_MODULE_SYMBOL(rx8130ce_get_datetime), - DEFINE_MODULE_SYMBOL(rx8130ce_set_datetime), - MODULE_SYMBOL_TERMINATOR -}; diff --git a/Drivers/sc2356-module/CMakeLists.txt b/Drivers/sc2356-module/CMakeLists.txt new file mode 100644 index 000000000..009a1192f --- /dev/null +++ b/Drivers/sc2356-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(sc2356-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_video esp_cam_sensor driver esp_driver_jpeg esp_driver_ppa +) diff --git a/Drivers/sc2356-module/LICENSE-Apache-2.0.md b/Drivers/sc2356-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/sc2356-module/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/Drivers/sc2356-module/README.md b/Drivers/sc2356-module/README.md new file mode 100644 index 000000000..8d4b40c41 --- /dev/null +++ b/Drivers/sc2356-module/README.md @@ -0,0 +1,9 @@ +# SC2356 MIPI CSI Camera Driver + +A driver for the `SC2356` (also known as SC202CS) 2MP MIPI CSI camera sensor by Smartsens. + +- Datasheet: https://www.smartsens.com/en/product/SC2356/ +- M5Stack Tab5 hardware: https://docs.m5stack.com/en/core/Tab5 +- ESP Video Components: https://github.com/espressif/esp-video-components + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/sc2356-module/bindings/smartsens,sc2356.yaml b/Drivers/sc2356-module/bindings/smartsens,sc2356.yaml new file mode 100644 index 000000000..958942584 --- /dev/null +++ b/Drivers/sc2356-module/bindings/smartsens,sc2356.yaml @@ -0,0 +1,5 @@ +description: Smartsens SC2356 2MP MIPI CSI camera sensor + +include: [ "i2c-device.yaml" ] + +compatible: "smartsens,sc2356" diff --git a/Drivers/sc2356-module/devicetree.yaml b/Drivers/sc2356-module/devicetree.yaml new file mode 100644 index 000000000..9424b9a3a --- /dev/null +++ b/Drivers/sc2356-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: +- TactilityKernel +bindings: bindings diff --git a/Drivers/sc2356-module/include/bindings/sc2356.h b/Drivers/sc2356-module/include/bindings/sc2356.h new file mode 100644 index 000000000..dd20ba8b7 --- /dev/null +++ b/Drivers/sc2356-module/include/bindings/sc2356.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DEFINE_DEVICETREE(sc2356, struct Sc2356Config) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/sc2356-module/include/drivers/sc2356.h b/Drivers/sc2356-module/include/drivers/sc2356.h new file mode 100644 index 000000000..e6d22d1db --- /dev/null +++ b/Drivers/sc2356-module/include/drivers/sc2356.h @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include +#include + +struct Device; + +#ifdef __cplusplus +extern "C" { +#endif + +struct Sc2356Config { + /** SCCB I2C address (0x36) */ + uint8_t address; +}; + +/** + * Opaque camera handle returned by sc2356_open(). + * Not thread-safe: the capture flow (sc2356_get_frame/sc2356_capture_jpeg + sc2356_release_frame) + * must be driven from a single consumer at a time. state->last_dqbuf_index and the shared PPA + * output buffer are not guarded by a lock, so overlapping calls from multiple tasks on the same + * handle will race. + */ +typedef CameraHandle Sc2356Handle; + +/** + * Initialize the esp_video subsystem and open the MIPI CSI video device. + * Reuses the parent I2C controller's bus handle for SCCB communication. + * Must be called before any other sc2356_* functions. + * @param device the sc2356 device from the device tree + * @param handle out: opaque handle to pass to subsequent calls + * @return ERROR_NONE on success + */ +error_t sc2356_open(struct Device* device, Sc2356Handle* handle); + +/** + * Stop streaming, unmap frame buffers, and release all resources. + * @param handle handle returned by sc2356_open() + * @return ERROR_NONE on success + */ +error_t sc2356_close(Sc2356Handle handle); + +/** + * Dequeue one RGB565 frame. Blocks until a frame is available or the timeout expires. + * The caller must call sc2356_release_frame() to return the buffer to the camera queue. + * @param handle handle returned by sc2356_open() + * @param buf out: pointer to the DMA-mapped RGB565 frame buffer + * @param len out: byte length of buf (width * height * 2) + * @param timeout_ms maximum time to wait in milliseconds + * @param out_width optional out: width of buf at the moment it was produced, atomic with the frame data (may be NULL) + * @param out_height optional out: height of buf at the moment it was produced, atomic with the frame data (may be NULL) + * @return ERROR_NONE on success, ERROR_TIMEOUT if no frame arrived in time + */ +error_t sc2356_get_frame(Sc2356Handle handle, uint8_t** buf, size_t* len, uint32_t timeout_ms, uint32_t* out_width, uint32_t* out_height); + +/** + * Return the last dequeued frame buffer to the V4L2 capture queue. + * Must be called after each successful sc2356_get_frame(). + * @param handle handle returned by sc2356_open() + * @return ERROR_NONE on success + */ +error_t sc2356_release_frame(Sc2356Handle handle); + +/** + * Return the frame width in pixels (1280 for the default 720p config). + * @param handle handle returned by sc2356_open() + */ +uint32_t sc2356_get_width(Sc2356Handle handle); + +/** + * Return the frame height in pixels (720 for the default 720p config). + * @param handle handle returned by sc2356_open() + */ +uint32_t sc2356_get_height(Sc2356Handle handle); + +/** + * Change the clockwise rotation applied to subsequent frames. + * Can be called at any time while the handle is open, including during streaming. + * @param handle handle returned by sc2356_open() + * @param rotation new rotation + * @return ERROR_NONE on success + */ +error_t sc2356_set_rotation(Sc2356Handle handle, CameraRotation rotation); + +/** + * Capture one frame and JPEG-encode it using the hardware JPEG encoder. + * Allocates output buffer in SPIRAM; caller must free it with heap_caps_free(). + * @param handle handle returned by sc2356_open() + * @param out_buf out: pointer to JPEG-encoded data (caller must free) + * @param out_len out: byte length of JPEG data + * @param quality JPEG quality 1-100 + * @return ERROR_NONE on success + */ +error_t sc2356_capture_jpeg(Sc2356Handle handle, uint8_t** out_buf, size_t* out_len, uint8_t quality); + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/sc2356-module/include/sc2356_module.h b/Drivers/sc2356-module/include/sc2356_module.h new file mode 100644 index 000000000..b0203bf98 --- /dev/null +++ b/Drivers/sc2356-module/include/sc2356_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module sc2356_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/sc2356-module/source/module.cpp b/Drivers/sc2356-module/source/module.cpp new file mode 100644 index 000000000..693b34ea0 --- /dev/null +++ b/Drivers/sc2356-module/source/module.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver sc2356_driver; + +static error_t start() { + check(driver_construct_add(&sc2356_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + check(driver_remove_destruct(&sc2356_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module sc2356_module = { + .name = "sc2356", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} // extern "C" diff --git a/Drivers/sc2356-module/source/sc2356.cpp b/Drivers/sc2356-module/source/sc2356.cpp new file mode 100644 index 000000000..a3f533bd8 --- /dev/null +++ b/Drivers/sc2356-module/source/sc2356.cpp @@ -0,0 +1,562 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#define TAG "SC2356" + +// SC2356 (SC202CS) chip ID registers (16-bit address, 8-bit value) +// PID = 0xEB52: high byte 0xEB at register 0x3107, low byte 0x52 at 0x3108 +static constexpr uint8_t SC2356_REG_CHIP_ID_H[2] = { 0x31, 0x07 }; +static constexpr uint8_t SC2356_CHIP_ID_H = 0xEB; +static constexpr uint8_t SC2356_REG_CHIP_ID_L[2] = { 0x31, 0x08 }; +static constexpr uint8_t SC2356_CHIP_ID_L = 0x52; + +static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(20); +static constexpr int VIDEO_BUFFER_COUNT = 2; + +static bool s_video_initialized = false; +static SemaphoreHandle_t s_video_init_mutex = nullptr; + +struct Sc2356State : CameraHandleData { + int fd; + // Native sensor output dimensions (always 1280×720 from the sensor) + uint32_t native_width; + uint32_t native_height; + // Post-rotation dimensions (swapped for 90/270) + uint32_t width; + uint32_t height; + uint8_t* buffers[VIDEO_BUFFER_COUNT]; + size_t buf_lengths[VIDEO_BUFFER_COUNT]; + int last_dqbuf_index; + jpeg_encoder_handle_t enc; + i2c_master_bus_handle_t sccb_bus; + CameraRotation rotation; + ppa_client_handle_t ppa; + // PPA output buffer — fixed size, large enough for any rotation (native_width * native_height * 2) + uint8_t* ppa_out_buf; + size_t ppa_out_buf_size; + SemaphoreHandle_t rotation_mutex; // protects rotation, width, height +}; + +#define GET_CONFIG(device) (static_cast((device)->config)) + +static error_t start(Device* device) { + auto* i2c = device_get_parent(device); + if (device_get_type(i2c) != &I2C_CONTROLLER_TYPE) { + LOG_E(TAG, "Parent is not an I2C controller"); + return ERROR_RESOURCE; + } + + auto address = GET_CONFIG(device)->address; + + uint8_t chip_id_h = 0, chip_id_l = 0; + error_t err = i2c_controller_write_read(i2c, address, SC2356_REG_CHIP_ID_H, sizeof(SC2356_REG_CHIP_ID_H), &chip_id_h, 1, I2C_TIMEOUT); + if (err != ERROR_NONE) { + LOG_W(TAG, "WHO_AM_I read failed: %s — sensor may still be starting up", error_to_string(err)); + return ERROR_NONE; + } + + err = i2c_controller_write_read(i2c, address, SC2356_REG_CHIP_ID_L, sizeof(SC2356_REG_CHIP_ID_L), &chip_id_l, 1, I2C_TIMEOUT); + if (err != ERROR_NONE) { + LOG_W(TAG, "WHO_AM_I read (low) failed: %s", error_to_string(err)); + return ERROR_NONE; + } + + if (chip_id_h != SC2356_CHIP_ID_H || chip_id_l != SC2356_CHIP_ID_L) { + LOG_E(TAG, "WHO_AM_I mismatch: got 0x%02X%02X, expected 0x%02X%02X", chip_id_h, chip_id_l, SC2356_CHIP_ID_H, SC2356_CHIP_ID_L); + return ERROR_RESOURCE; + } + LOG_I(TAG, "WHO_AM_I OK (0x%02X%02X)", chip_id_h, chip_id_l); + return ERROR_NONE; +} + +static error_t stop([[maybe_unused]] Device* device) { + return ERROR_NONE; +} + +static SemaphoreHandle_t get_video_init_mutex() { + static portMUX_TYPE creation_lock = portMUX_INITIALIZER_UNLOCKED; + portENTER_CRITICAL(&creation_lock); + if (!s_video_init_mutex) { + s_video_init_mutex = xSemaphoreCreateMutex(); + if (!s_video_init_mutex) { + LOG_E(TAG, "Failed to create video init mutex"); + } + } + portEXIT_CRITICAL(&creation_lock); + return s_video_init_mutex; +} + +static ppa_srm_rotation_angle_t to_ppa_rotation(CameraRotation rotation) { + switch (rotation) { + case CAMERA_ROTATION_90: return PPA_SRM_ROTATION_ANGLE_90; + case CAMERA_ROTATION_180: return PPA_SRM_ROTATION_ANGLE_180; + case CAMERA_ROTATION_270: return PPA_SRM_ROTATION_ANGLE_270; + default: return PPA_SRM_ROTATION_ANGLE_0; + } +} + +extern "C" { + +error_t sc2356_open(Device* device, Sc2356Handle* out_handle) { + if (!device || !out_handle) return ERROR_INVALID_ARGUMENT; + + auto* state = static_cast(heap_caps_calloc(1, sizeof(Sc2356State), MALLOC_CAP_DEFAULT)); + if (!state) return ERROR_OUT_OF_MEMORY; + state->device = device; + state->fd = -1; + state->last_dqbuf_index = -1; + state->rotation_mutex = xSemaphoreCreateMutex(); + if (!state->rotation_mutex) { + heap_caps_free(state); + return ERROR_OUT_OF_MEMORY; + } + + state->rotation = CAMERA_ROTATION_0; + + // Retrieve bus handle from the parent I2C controller + auto* i2c = device_get_parent(device); + state->sccb_bus = esp32_i2c_master_get_bus_handle(i2c); + if (!state->sccb_bus) { + LOG_E(TAG, "Failed to get i2c_master bus handle from parent device"); + vSemaphoreDelete(state->rotation_mutex); + heap_caps_free(state); + return ERROR_RESOURCE; + } + + // esp_video_init only needs to run once across all sc2356_open calls + { + SemaphoreHandle_t init_mutex = get_video_init_mutex(); + if (!init_mutex) { + vSemaphoreDelete(state->rotation_mutex); + heap_caps_free(state); + return ERROR_OUT_OF_MEMORY; + } + xSemaphoreTake(init_mutex, portMAX_DELAY); + if (!s_video_initialized) { + esp_video_init_csi_config_t csi_config = {}; + csi_config.sccb_config.init_sccb = false; + csi_config.sccb_config.i2c_handle = state->sccb_bus; + csi_config.sccb_config.freq = 400000; + csi_config.reset_pin = static_cast(-1); + csi_config.pwdn_pin = static_cast(-1); + + esp_video_init_config_t cam_config = {}; + cam_config.csi = &csi_config; + + esp_err_t esp_err = esp_video_init(&cam_config); + if (esp_err != ESP_OK) { + LOG_E(TAG, "esp_video_init failed: %s", esp_err_to_name(esp_err)); + xSemaphoreGive(init_mutex); + vSemaphoreDelete(state->rotation_mutex); + heap_caps_free(state); + return ERROR_RESOURCE; + } + s_video_initialized = true; + } + xSemaphoreGive(init_mutex); + } + + // Open the CSI video device. The ISP pipeline controller (running in the background + // after esp_video_init) applies AE/AWB/demosaicing automatically; pixel output + // is still on /dev/video0, which delivers processed RGB565 frames. + state->fd = open(ESP_VIDEO_MIPI_CSI_DEVICE_NAME, O_RDONLY); + if (state->fd < 0) { + LOG_E(TAG, "Failed to open %s", ESP_VIDEO_MIPI_CSI_DEVICE_NAME); + vSemaphoreDelete(state->rotation_mutex); + heap_caps_free(state); + return ERROR_RESOURCE; + } + + // Query current format to get sensor dimensions + struct v4l2_format fmt = {}; + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (ioctl(state->fd, VIDIOC_G_FMT, &fmt) != 0) { + LOG_E(TAG, "VIDIOC_G_FMT failed"); + goto err_close; + } + + // Set RGB565 output format + if (fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_RGB565) { + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565; + if (ioctl(state->fd, VIDIOC_S_FMT, &fmt) != 0) { + LOG_E(TAG, "VIDIOC_S_FMT RGB565 failed"); + goto err_close; + } + // Re-read to get the actual negotiated dimensions + memset(&fmt, 0, sizeof(fmt)); + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (ioctl(state->fd, VIDIOC_G_FMT, &fmt) != 0) { + LOG_E(TAG, "VIDIOC_G_FMT after set failed"); + goto err_close; + } + } + + state->native_width = fmt.fmt.pix.width; + state->native_height = fmt.fmt.pix.height; + + // Post-rotation dimensions (swapped for 90/270) + if (state->rotation == CAMERA_ROTATION_90 || state->rotation == CAMERA_ROTATION_270) { + state->width = state->native_height; + state->height = state->native_width; + } else { + state->width = state->native_width; + state->height = state->native_height; + } + LOG_I(TAG, "Video format: %lux%lu native", + (unsigned long)state->native_width, (unsigned long)state->native_height); + + // PPA output buffer — fixed size (max of any rotation), cache-line aligned per PPA requirements + state->ppa_out_buf_size = (size_t)state->native_width * state->native_height * 2; + state->ppa_out_buf = static_cast( + heap_caps_aligned_alloc(64, state->ppa_out_buf_size, MALLOC_CAP_SPIRAM)); + if (!state->ppa_out_buf) { + LOG_E(TAG, "Failed to alloc PPA output buffer (%zu bytes)", state->ppa_out_buf_size); + goto err_close; + } + + // PPA client for hardware-accelerated rotation + { + ppa_client_config_t ppa_cfg = {}; + ppa_cfg.oper_type = PPA_OPERATION_SRM; + ppa_cfg.max_pending_trans_num = 1; + if (ppa_register_client(&ppa_cfg, &state->ppa) != ESP_OK) { + LOG_E(TAG, "ppa_register_client failed"); + goto err_close; + } + } + + // Request mmap buffers + { + struct v4l2_requestbuffers req = {}; + req.count = VIDEO_BUFFER_COUNT; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_MMAP; + if (ioctl(state->fd, VIDIOC_REQBUFS, &req) != 0) { + LOG_E(TAG, "VIDIOC_REQBUFS failed"); + goto err_close; + } + } + + // Query, mmap, and queue each buffer + for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) { + struct v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + if (ioctl(state->fd, VIDIOC_QUERYBUF, &buf) != 0) { + LOG_E(TAG, "VIDIOC_QUERYBUF[%d] failed", i); + goto err_unmap; + } + + state->buf_lengths[i] = buf.length; + state->buffers[i] = static_cast( + mmap(nullptr, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, state->fd, buf.m.offset) + ); + if (state->buffers[i] == MAP_FAILED) { + state->buffers[i] = nullptr; + LOG_E(TAG, "mmap[%d] failed", i); + goto err_unmap; + } + + if (ioctl(state->fd, VIDIOC_QBUF, &buf) != 0) { + LOG_E(TAG, "VIDIOC_QBUF[%d] failed", i); + goto err_unmap; + } + } + + // Start streaming + { + int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (ioctl(state->fd, VIDIOC_STREAMON, &type) != 0) { + LOG_E(TAG, "VIDIOC_STREAMON failed"); + goto err_unmap; + } + } + + // Create hardware JPEG encoder once; reused across all sc2356_capture_jpeg() calls + { + jpeg_encode_engine_cfg_t eng_cfg = { .intr_priority = 0, .timeout_ms = 1000 }; + esp_err_t je = jpeg_new_encoder_engine(&eng_cfg, &state->enc); + if (je != ESP_OK) { + LOG_E(TAG, "jpeg_new_encoder_engine failed: %s", esp_err_to_name(je)); + goto err_unmap; + } + } + + *out_handle = state; + return ERROR_NONE; + +err_unmap: + { + // Stop streaming before unmapping - correct V4L2 cleanup order, avoids DMA-into- + // unmapped-buffer races. Harmless no-op if STREAMON never succeeded (e.g. it's the + // failure that brought us here). + int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + ioctl(state->fd, VIDIOC_STREAMOFF, &type); + } + for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) { + if (state->buffers[i]) munmap(state->buffers[i], state->buf_lengths[i]); + } +err_close: + if (state->ppa) ppa_unregister_client(state->ppa); + if (state->ppa_out_buf) heap_caps_free(state->ppa_out_buf); + if (state->rotation_mutex) vSemaphoreDelete(state->rotation_mutex); + close(state->fd); + heap_caps_free(state); + return ERROR_RESOURCE; +} + +error_t sc2356_close(Sc2356Handle handle) { + if (!handle) return ERROR_INVALID_ARGUMENT; + auto* state = static_cast(handle); + + if (state->enc) jpeg_del_encoder_engine(state->enc); + if (state->ppa) ppa_unregister_client(state->ppa); + + int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + ioctl(state->fd, VIDIOC_STREAMOFF, &type); + + for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) { + if (state->buffers[i]) munmap(state->buffers[i], state->buf_lengths[i]); + } + + if (state->ppa_out_buf) heap_caps_free(state->ppa_out_buf); + if (state->rotation_mutex) vSemaphoreDelete(state->rotation_mutex); + close(state->fd); + // Bus handle is owned by esp32_i2c_master driver - do not delete it + heap_caps_free(state); + return ERROR_NONE; +} + +error_t sc2356_set_rotation(Sc2356Handle handle, CameraRotation rotation) { + if (!handle) return ERROR_INVALID_ARGUMENT; + auto* state = static_cast(handle); + + xSemaphoreTake(state->rotation_mutex, portMAX_DELAY); + + if (state->rotation == rotation) { + xSemaphoreGive(state->rotation_mutex); + return ERROR_NONE; + } + + bool needs_swap = (rotation == CAMERA_ROTATION_90 || rotation == CAMERA_ROTATION_270); + + state->rotation = rotation; + if (needs_swap) { + state->width = state->native_height; + state->height = state->native_width; + } else { + state->width = state->native_width; + state->height = state->native_height; + } + + xSemaphoreGive(state->rotation_mutex); + return ERROR_NONE; +} + +error_t sc2356_get_frame(Sc2356Handle handle, uint8_t** buf, size_t* len, uint32_t timeout_ms, uint32_t* out_width, uint32_t* out_height) { + if (!handle || !buf || !len) return ERROR_INVALID_ARGUMENT; + auto* state = static_cast(handle); + + // Use poll to enforce timeout + struct pollfd pfd = {}; + pfd.fd = state->fd; + pfd.events = POLLIN; + int ret = poll(&pfd, 1, static_cast(timeout_ms)); + if (ret == 0) return ERROR_TIMEOUT; + if (ret < 0) { + LOG_E(TAG, "poll failed: %d", errno); + return ERROR_RESOURCE; + } + + struct v4l2_buffer vbuf = {}; + vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + vbuf.memory = V4L2_MEMORY_MMAP; + if (ioctl(state->fd, VIDIOC_DQBUF, &vbuf) != 0) { + LOG_E(TAG, "VIDIOC_DQBUF failed: %d", errno); + return ERROR_RESOURCE; + } + + state->last_dqbuf_index = static_cast(vbuf.index); + uint8_t* raw = state->buffers[vbuf.index]; + + xSemaphoreTake(state->rotation_mutex, portMAX_DELAY); + + uint32_t frame_width = state->width; + uint32_t frame_height = state->height; + + ppa_srm_oper_config_t srm_cfg = {}; + srm_cfg.in.buffer = raw; + srm_cfg.in.pic_w = srm_cfg.in.block_w = state->native_width; + srm_cfg.in.pic_h = srm_cfg.in.block_h = state->native_height; + srm_cfg.in.srm_cm = PPA_SRM_COLOR_MODE_RGB565; + srm_cfg.out.buffer = state->ppa_out_buf; + srm_cfg.out.buffer_size = state->ppa_out_buf_size; + srm_cfg.out.pic_w = frame_width; + srm_cfg.out.pic_h = frame_height; + srm_cfg.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565; + srm_cfg.rotation_angle = to_ppa_rotation(state->rotation); + srm_cfg.scale_x = srm_cfg.scale_y = 1.0f; + srm_cfg.mode = PPA_TRANS_MODE_BLOCKING; + + esp_err_t ppa_err = ppa_do_scale_rotate_mirror(state->ppa, &srm_cfg); + xSemaphoreGive(state->rotation_mutex); + + if (ppa_err != ESP_OK) { + LOG_E(TAG, "ppa_do_scale_rotate_mirror failed: %s", esp_err_to_name(ppa_err)); + // Buffer was already dequeued (VIDIOC_DQBUF above) - the caller receives an error + // and may never call sc2356_release_frame(), so return it to the queue here or + // repeated PPA failures exhaust VIDEO_BUFFER_COUNT and poll() blocks forever. + sc2356_release_frame(handle); + return ERROR_RESOURCE; + } + + *buf = state->ppa_out_buf; + *len = (size_t)frame_width * frame_height * 2; + if (out_width != nullptr) { + *out_width = frame_width; + } + if (out_height != nullptr) { + *out_height = frame_height; + } + return ERROR_NONE; +} + +error_t sc2356_release_frame(Sc2356Handle handle) { + if (!handle) return ERROR_INVALID_ARGUMENT; + auto* state = static_cast(handle); + + if (state->last_dqbuf_index < 0) return ERROR_INVALID_STATE; + + struct v4l2_buffer vbuf = {}; + vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + vbuf.memory = V4L2_MEMORY_MMAP; + vbuf.index = static_cast(state->last_dqbuf_index); + state->last_dqbuf_index = -1; + + if (ioctl(state->fd, VIDIOC_QBUF, &vbuf) != 0) { + LOG_E(TAG, "VIDIOC_QBUF failed: %d", errno); + return ERROR_RESOURCE; + } + return ERROR_NONE; +} + +uint32_t sc2356_get_width(Sc2356Handle handle) { + if (!handle) return 0; + auto* state = static_cast(handle); + xSemaphoreTake(state->rotation_mutex, portMAX_DELAY); + uint32_t width = state->width; + xSemaphoreGive(state->rotation_mutex); + return width; +} + +uint32_t sc2356_get_height(Sc2356Handle handle) { + if (!handle) return 0; + auto* state = static_cast(handle); + xSemaphoreTake(state->rotation_mutex, portMAX_DELAY); + uint32_t height = state->height; + xSemaphoreGive(state->rotation_mutex); + return height; +} + +error_t sc2356_capture_jpeg(Sc2356Handle handle, uint8_t** out_buf, size_t* out_len, uint8_t quality) { + if (!handle || !out_buf || !out_len) return ERROR_INVALID_ARGUMENT; + auto* state = static_cast(handle); + + // Dequeue one frame. Dimensions come back atomically with the frame data itself, + // so they can't drift if a concurrent sc2356_set_rotation() changes state->width/height. + uint8_t* frame = nullptr; + size_t frame_len = 0; + uint32_t capture_width = 0; + uint32_t capture_height = 0; + error_t err = sc2356_get_frame(handle, &frame, &frame_len, 500, &capture_width, &capture_height); + if (err != ERROR_NONE) return err; + + // Output buffer must be allocated with jpeg_alloc_encoder_mem for DMA alignment. + // jpeg_alloc_encoder_mem() allocates exactly the requested size with no headroom, so the + // hint must cover the worst case, not the typical case. At high quality settings (low + // compression) on detailed/noisy images, a width*height (1 byte/pixel) hint can be smaller + // than the actual encoded output, causing the DMA write to overrun the buffer - this was + // observed as JPEG corruption confined to the tail of the image. width*height*2 matches + // the uncompressed RGB565 source size, which JPEG output can never exceed. + size_t out_sz = 0; + jpeg_encode_memory_alloc_cfg_t mem_cfg = { .buffer_direction = JPEG_ENC_ALLOC_OUTPUT_BUFFER }; + uint8_t* jpeg_buf = static_cast( + jpeg_alloc_encoder_mem(capture_width * capture_height * 2, &mem_cfg, &out_sz)); + if (!jpeg_buf) { + sc2356_release_frame(handle); + return ERROR_OUT_OF_MEMORY; + } + + jpeg_encode_cfg_t enc_cfg = { + .height = capture_height, + .width = capture_width, + .src_type = JPEG_ENCODE_IN_FORMAT_RGB565, + .sub_sample = JPEG_DOWN_SAMPLING_YUV420, + .image_quality = quality, + }; + uint32_t encoded_size = 0; + esp_err_t esp_err = jpeg_encoder_process(state->enc, &enc_cfg, frame, + static_cast(frame_len), + jpeg_buf, static_cast(out_sz), + &encoded_size); + sc2356_release_frame(handle); + + if (esp_err != ESP_OK || encoded_size == 0) { + LOG_E(TAG, "jpeg_encoder_process failed: %s", esp_err_to_name(esp_err)); + heap_caps_free(jpeg_buf); + return ERROR_RESOURCE; + } + + *out_buf = jpeg_buf; + *out_len = static_cast(encoded_size); + return ERROR_NONE; +} + +CameraApi sc2356_camera_api = { + .open = sc2356_open, + .close = sc2356_close, + .get_frame = sc2356_get_frame, + .release_frame = sc2356_release_frame, + .get_width = sc2356_get_width, + .get_height = sc2356_get_height, + .set_rotation = sc2356_set_rotation, + .capture_jpeg = sc2356_capture_jpeg +}; + +Driver sc2356_driver = { + .name = "sc2356", + .compatible = (const char*[]) { "smartsens,sc2356", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &sc2356_camera_api, + .device_type = &CAMERA_TYPE, + .owner = &sc2356_module, + .internal = nullptr +}; + +} // extern "C" diff --git a/Firmware/idf_component.yml b/Firmware/idf_component.yml index 79818203d..3b5892f0b 100644 --- a/Firmware/idf_component.yml +++ b/Firmware/idf_component.yml @@ -1,4 +1,8 @@ dependencies: + espressif/esp_video: + version: "2.2.0" + rules: + - if: "target in [esp32s3, esp32c6, esp32p4]" espressif/esp_hosted: version: "*" rules: diff --git a/Modules/lvgl-module/source/symbols.c b/Modules/lvgl-module/source/symbols.c index f3ad3b072..005e68183 100644 --- a/Modules/lvgl-module/source/symbols.c +++ b/Modules/lvgl-module/source/symbols.c @@ -43,6 +43,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = { DEFINE_MODULE_SYMBOL(lv_color_darken), DEFINE_MODULE_SYMBOL(lv_color_hsv_to_rgb), DEFINE_MODULE_SYMBOL(lv_color_to_32), + DEFINE_MODULE_SYMBOL(lv_color_mix), DEFINE_MODULE_SYMBOL(lv_obj_center), DEFINE_MODULE_SYMBOL(lv_obj_clean), DEFINE_MODULE_SYMBOL(lv_obj_create), @@ -334,6 +335,8 @@ const struct ModuleSymbol lvgl_module_symbols[] = { DEFINE_MODULE_SYMBOL(lv_display_set_rotation), DEFINE_MODULE_SYMBOL(lv_display_set_offset), DEFINE_MODULE_SYMBOL(lv_display_trigger_activity), + DEFINE_MODULE_SYMBOL(lv_display_add_event_cb), + DEFINE_MODULE_SYMBOL(lv_display_remove_event_cb_with_user_data), // lv_pct DEFINE_MODULE_SYMBOL(lv_pct), DEFINE_MODULE_SYMBOL(lv_pct_to_px), diff --git a/Tactility/Include/Tactility/lvgl/SliderBox.h b/Tactility/Include/Tactility/lvgl/SliderBox.h new file mode 100644 index 000000000..263aeb831 --- /dev/null +++ b/Tactility/Include/Tactility/lvgl/SliderBox.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +namespace tt::lvgl { + +/** + * Create a SliderBox: a slider flanked by "-" and "+" buttons with a value label. + * @param parent the parent object + * @param min minimum value (inclusive) + * @param max maximum value (inclusive) + * @param step the amount each +/- button press changes the value by + * @param value the initial value + * @return the created SliderBox instance + */ +lv_obj_t* sliderbox_create(lv_obj_t* parent, int32_t min, int32_t max, int32_t step, int32_t value); + +/** @return the current value of the SliderBox */ +int32_t sliderbox_get_value(lv_obj_t* obj); + +/** + * Set the current value of the SliderBox (clamped to its [min, max] range). + * Updates the slider position and the value label. + */ +void sliderbox_set_value(lv_obj_t* obj, int32_t value, lv_anim_enable_t anim); + +/** + * Register a callback that is fired with LV_EVENT_VALUE_CHANGED whenever the value + * changes, whether the change came from the slider or from the +/- buttons. + * @param obj the SliderBox instance + * @param callback the callback to invoke + * @param userData user data that is attached to the event + */ +void sliderbox_add_value_changed_cb(lv_obj_t* obj, lv_event_cb_t callback, void* userData); + +} // namespace tt::lvgl diff --git a/Tactility/Include/Tactility/service/rtctime/RtcTime.h b/Tactility/Include/Tactility/service/rtctime/RtcTime.h new file mode 100644 index 000000000..49e2aac7c --- /dev/null +++ b/Tactility/Include/Tactility/service/rtctime/RtcTime.h @@ -0,0 +1,8 @@ +#pragma once + +namespace tt::service::rtctime { + +/** @return true when an RTC_TYPE device is bound and ready */ +bool isAvailable(); + +} // namespace tt::service::rtctime diff --git a/Tactility/Include/Tactility/service/rtctime/RtcTimeService.h b/Tactility/Include/Tactility/service/rtctime/RtcTimeService.h new file mode 100644 index 000000000..0eca7f52d --- /dev/null +++ b/Tactility/Include/Tactility/service/rtctime/RtcTimeService.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#include + +struct Device; + +namespace tt::service::rtctime { + +class RtcTimeService final : public Service { + + kernel::SystemEventSubscription timeEventSubscription = 0; + Device* rtcDevice = nullptr; + + Device* findRtcDevice(); + void onTimeChanged(kernel::SystemEvent event); + +public: + + bool onStart(ServiceContext& serviceContext) override; + void onStop(ServiceContext& serviceContext) override; + + bool isAvailable() const; +}; + +std::shared_ptr findRtcTimeService(); + +} // namespace tt::service::rtctime diff --git a/Tactility/Private/Tactility/service/gui/GuiService.h b/Tactility/Private/Tactility/service/gui/GuiService.h index a1116e93f..25f878995 100644 --- a/Tactility/Private/Tactility/service/gui/GuiService.h +++ b/Tactility/Private/Tactility/service/gui/GuiService.h @@ -7,6 +7,8 @@ #include #include +#include + #include #include @@ -30,6 +32,16 @@ class GuiService final : public Service { RecursiveMutex mutex; PubSub::SubscriptionHandle loader_pubsub_subscription = nullptr; + // Signaled by hideApp() once App::onHide() has actually finished running on the GUI + // task. onLoaderEvent() blocks on this (still on the Loader thread, inside the + // synchronous pubsub publish() call) before returning from the ApplicationHiding + // branch, so LoaderService::transitionAppToState(Hiding) can't return - and therefore + // the immediately-following Destroyed transition (which unloads an ELF app's code via + // esp_elf_deinit) can't run - until onHide() has fully completed. Without this, the + // ELF's code/data can be unmapped while onHide() (and anything it spawned, like a + // camera capture task) is still executing it. + Semaphore hideDoneSem { 1, 0 }; + // Layers and Canvas lv_obj_t* appRootWidget = nullptr; lv_obj_t* statusbarWidget = nullptr; diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 0b588aaa9..cd368ce94 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -85,6 +86,7 @@ namespace service { #ifdef ESP_PLATFORM namespace displayidle { extern const ServiceManifest manifest; } namespace keyboardidle { extern const ServiceManifest manifest; } + namespace rtctime { extern const ServiceManifest manifest; } #endif #if TT_FEATURE_SCREENSHOT_ENABLED namespace screenshot { extern const ServiceManifest manifest; } @@ -280,6 +282,9 @@ static void registerAndStartSecondaryServices() { addService(service::statusbar::manifest); addService(service::memorychecker::manifest); #if defined(ESP_PLATFORM) + if (device_exists_of_type(&RTC_TYPE)) { + addService(service::rtctime::manifest); + } addService(service::displayidle::manifest); #if defined(CONFIG_TT_TDECK_WORKAROUND) addService(service::keyboardidle::manifest); diff --git a/Tactility/Source/app/AppPaths.cpp b/Tactility/Source/app/AppPaths.cpp index 13ff32a2f..3f41edf62 100644 --- a/Tactility/Source/app/AppPaths.cpp +++ b/Tactility/Source/app/AppPaths.cpp @@ -16,9 +16,9 @@ namespace tt::app { std::string AppPaths::getUserDataPath() const { if (manifest.appLocation.isInternal()) { - return std::format("{}{}/user/app/{}", PARTITION_PREFIX, file::DATA_PARTITION_NAME, manifest.appId); + return std::format("{}{}/tactility/user/app/{}", PARTITION_PREFIX, file::DATA_PARTITION_NAME, manifest.appId); } else { - return std::format("{}/user/app/{}", file::getFirstPathSegment(manifest.appLocation.getPath()), manifest.appId); + return std::format("{}/tactility/user/app/{}", file::getFirstPathSegment(manifest.appLocation.getPath()), manifest.appId); } } diff --git a/Tactility/Source/lvgl/SliderBox.cpp b/Tactility/Source/lvgl/SliderBox.cpp new file mode 100644 index 000000000..eab358159 --- /dev/null +++ b/Tactility/Source/lvgl/SliderBox.cpp @@ -0,0 +1,207 @@ +#define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration + +#include + +#include + +namespace tt::lvgl { + +typedef struct { + lv_obj_t obj; + lv_obj_t* minusButton; + lv_obj_t* slider; + lv_obj_t* valueLabel; + lv_obj_t* plusButton; + int32_t min; + int32_t max; + int32_t step; +} SliderBox; + +static void sliderbox_constructor(const lv_obj_class_t* classPointer, lv_obj_t* obj); + +static lv_obj_class_t sliderbox_class = { + .base_class = &lv_obj_class, + .constructor_cb = &sliderbox_constructor, + .destructor_cb = nullptr, + .event_cb = nullptr, + .user_data = nullptr, + .name = "tt_sliderbox", + .width_def = LV_PCT(100), + .height_def = LV_SIZE_CONTENT, + .editable = false, + .group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE, + .instance_size = sizeof(SliderBox), + .theme_inheritable = false +}; + +static void sliderbox_constructor(const lv_obj_class_t* classPointer, lv_obj_t* obj) { + LV_UNUSED(classPointer); + lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(obj, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(obj, 4, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(obj, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(obj, 0, LV_STATE_DEFAULT); +} + +static void update_value_label(SliderBox* sliderBox) { + auto value = lv_slider_get_value(sliderBox->slider); + lv_label_set_text_fmt(sliderBox->valueLabel, "%" LV_PRId32, value); +} + +static void update_button_states(SliderBox* sliderBox) { + auto value = lv_slider_get_value(sliderBox->slider); + + if (value <= sliderBox->min) { + lv_obj_add_state(sliderBox->minusButton, LV_STATE_DISABLED); + } else { + lv_obj_remove_state(sliderBox->minusButton, LV_STATE_DISABLED); + } + + if (value >= sliderBox->max) { + lv_obj_add_state(sliderBox->plusButton, LV_STATE_DISABLED); + } else { + lv_obj_remove_state(sliderBox->plusButton, LV_STATE_DISABLED); + } +} + +static void notify_value_changed(lv_obj_t* obj) { + lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, nullptr); +} + +static void on_slider_value_changed(lv_event_t* event) { + auto* obj = static_cast(lv_event_get_user_data(event)); + auto* sliderBox = reinterpret_cast(obj); + + auto raw_value = lv_slider_get_value(sliderBox->slider); + auto steps_from_min = (raw_value - sliderBox->min + sliderBox->step / 2) / sliderBox->step; + auto snapped_value = sliderBox->min + steps_from_min * sliderBox->step; + if (snapped_value > sliderBox->max) { + snapped_value = sliderBox->max; + } + + if (snapped_value != raw_value) { + lv_slider_set_value(sliderBox->slider, snapped_value, LV_ANIM_OFF); + } + + update_value_label(sliderBox); + update_button_states(sliderBox); + notify_value_changed(obj); +} + +static void on_step_button_clicked(lv_event_t* event, int32_t direction) { + auto* obj = static_cast(lv_event_get_user_data(event)); + auto* sliderBox = reinterpret_cast(obj); + + auto value = lv_slider_get_value(sliderBox->slider); + auto new_value = value + direction * sliderBox->step; + if (new_value < sliderBox->min) { + new_value = sliderBox->min; + } else if (new_value > sliderBox->max) { + new_value = sliderBox->max; + } + + if (new_value != value) { + lv_slider_set_value(sliderBox->slider, new_value, LV_ANIM_ON); + update_value_label(sliderBox); + notify_value_changed(obj); + } + + update_button_states(sliderBox); +} + +static void on_minus_button_clicked(lv_event_t* event) { + on_step_button_clicked(event, -1); +} + +static void on_plus_button_clicked(lv_event_t* event) { + on_step_button_clicked(event, 1); +} + +static lv_obj_t* create_step_button(lv_obj_t* parent, const char* symbol, lv_event_cb_t callback, void* userData, uint32_t size) { + auto* button = lv_button_create(parent); + lv_obj_remove_flag(button, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(button, size, size); + lv_obj_set_style_pad_all(button, 0, LV_STATE_DEFAULT); + lv_obj_add_event_cb(button, callback, LV_EVENT_SHORT_CLICKED, userData); + + auto* label = lv_label_create(button); + lv_label_set_text(label, symbol); + lv_obj_center(label); + + return button; +} + +lv_obj_t* sliderbox_create(lv_obj_t* parent, int32_t min, int32_t max, int32_t step, int32_t value) { + if (max <= min) { + max = min + 1; + } + if (step <= 0) { + step = 1; + } + + lv_obj_t* obj = lv_obj_class_create_obj(&sliderbox_class, parent); + lv_obj_class_init_obj(obj); + + auto* sliderBox = reinterpret_cast(obj); + sliderBox->min = min; + sliderBox->max = max; + sliderBox->step = step; + + auto button_size = lvgl_get_text_font_height(FONT_SIZE_DEFAULT) + 8; + + sliderBox->minusButton = create_step_button(obj, LV_SYMBOL_MINUS, &on_minus_button_clicked, obj, button_size); + + sliderBox->slider = lv_slider_create(obj); + lv_obj_remove_flag(sliderBox->slider, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_grow(sliderBox->slider, 1); + lv_slider_set_range(sliderBox->slider, min, max); + lv_obj_add_event_cb(sliderBox->slider, &on_slider_value_changed, LV_EVENT_VALUE_CHANGED, obj); + + sliderBox->valueLabel = lv_label_create(obj); + lv_obj_set_style_text_align(sliderBox->valueLabel, LV_TEXT_ALIGN_RIGHT, LV_STATE_DEFAULT); + lv_label_set_long_mode(sliderBox->valueLabel, LV_LABEL_LONG_MODE_CLIP); + + // Reserve enough width for the largest value so the layout doesn't jump around. + // +4 padding wasn't enough margin for 3-digit values to never wrap; widened to + // a flat per-character estimate instead of trusting raw glyph width too tightly. + char buffer[16]; + lv_snprintf(buffer, sizeof(buffer), "%" LV_PRId32, min); + auto width_min = lv_text_get_width(buffer, lv_strlen(buffer), lv_obj_get_style_text_font(sliderBox->valueLabel, LV_PART_MAIN), 0); + lv_snprintf(buffer, sizeof(buffer), "%" LV_PRId32, max); + auto width_max = lv_text_get_width(buffer, lv_strlen(buffer), lv_obj_get_style_text_font(sliderBox->valueLabel, LV_PART_MAIN), 0); + auto max_width = (width_min > width_max) ? width_min : width_max; + lv_obj_set_width(sliderBox->valueLabel, max_width + 8); + + sliderBox->plusButton = create_step_button(obj, LV_SYMBOL_PLUS, &on_plus_button_clicked, obj, button_size); + + sliderbox_set_value(obj, value, LV_ANIM_OFF); + + return obj; +} + +int32_t sliderbox_get_value(lv_obj_t* obj) { + auto* sliderBox = reinterpret_cast(obj); + return lv_slider_get_value(sliderBox->slider); +} + +void sliderbox_set_value(lv_obj_t* obj, int32_t value, lv_anim_enable_t anim) { + auto* sliderBox = reinterpret_cast(obj); + + if (value < sliderBox->min) { + value = sliderBox->min; + } else if (value > sliderBox->max) { + value = sliderBox->max; + } + + lv_slider_set_value(sliderBox->slider, value, anim); + update_value_label(sliderBox); + update_button_states(sliderBox); +} + +void sliderbox_add_value_changed_cb(lv_obj_t* obj, lv_event_cb_t callback, void* userData) { + lv_obj_add_event_cb(obj, callback, LV_EVENT_VALUE_CHANGED, userData); +} + +} // namespace tt::lvgl diff --git a/Tactility/Source/service/gui/GuiService.cpp b/Tactility/Source/service/gui/GuiService.cpp index d5b244d1b..2f625f604 100644 --- a/Tactility/Source/service/gui/GuiService.cpp +++ b/Tactility/Source/service/gui/GuiService.cpp @@ -63,6 +63,14 @@ void GuiService::onLoaderEvent(LoaderService::Event event) { auto app_instance = std::static_pointer_cast(app::getCurrentAppContext()); item = new GuiDispatchItem{this, GuiDispatchType::Show, app_instance}; } else if (event == LoaderService::Event::ApplicationHiding) { + // hideDoneSem is a binary semaphore signaled by every hideApp() completion, + // including the one showApp() triggers internally (GuiDispatchType::Show, when an + // app is already being shown) - that release has no waiter and leaves a stale + // permit sitting available. Drain it before dispatching, or the acquire() below + // could consume that leftover permit instead of the one this specific Hide + // dispatch is about to produce, letting Destroyed run before the real onHide() + // for this app has finished. + hideDoneSem.acquire(0); item = new GuiDispatchItem{this, GuiDispatchType::Hide, nullptr}; } else { return; @@ -71,6 +79,19 @@ void GuiService::onLoaderEvent(LoaderService::Event event) { if (dispatcher_dispatch(dispatcher, item, onGuiDispatch) != ERROR_NONE) { LOG_E(TAG, "Failed to dispatch gui event"); delete item; + return; + } + + if (event == LoaderService::Event::ApplicationHiding) { + // Block here (still on the Loader thread, inside publish()'s synchronous + // subscriber call) until hideApp() has actually run to completion on the GUI + // task. LoaderService::transitionAppToState(Hiding) must not return - and + // therefore the Destroyed transition right after it, which unloads an ELF app's + // code, must not run - until App::onHide() has fully finished. Bounded so a stuck + // GUI task can't wedge app shutdown forever. + if (!hideDoneSem.acquire(pdMS_TO_TICKS(5000))) { + LOG_E(TAG, "Timed out waiting for hideApp() to complete"); + } } } @@ -282,6 +303,14 @@ void GuiService::showApp(std::shared_ptr app) { } void GuiService::hideApp() { + // Signals hideDoneSem on every return path (including the early-return guards below) - + // onLoaderEvent() blocks on this to know App::onHide() has actually finished before + // Loader proceeds to destroy the app (see hideDoneSem's declaration for why). + struct SignalOnExit { + Semaphore& sem; + ~SignalOnExit() { sem.release(); } + } signal_on_exit { hideDoneSem }; + auto lock = mutex.asScopedLock(); lock.lock(); diff --git a/Tactility/Source/service/rtctime/RtcTime.cpp b/Tactility/Source/service/rtctime/RtcTime.cpp new file mode 100644 index 000000000..b251eb1cd --- /dev/null +++ b/Tactility/Source/service/rtctime/RtcTime.cpp @@ -0,0 +1,26 @@ +#include + +#ifdef ESP_PLATFORM +#include +#include +#include +#endif + +namespace tt::service::rtctime { + +#ifdef ESP_PLATFORM +extern const ServiceManifest manifest; +#endif + +bool isAvailable() { +#ifdef ESP_PLATFORM + // The service is only registered when an RTC device is present (see Tactility.cpp); + // treat "not registered" the same as "no device bound" rather than asserting. + auto service = findServiceById(manifest.id); + return service != nullptr && service->isAvailable(); +#else + return false; +#endif +} + +} // namespace tt::service::rtctime diff --git a/Tactility/Source/service/rtctime/RtcTimeService.cpp b/Tactility/Source/service/rtctime/RtcTimeService.cpp new file mode 100644 index 000000000..04a34f598 --- /dev/null +++ b/Tactility/Source/service/rtctime/RtcTimeService.cpp @@ -0,0 +1,152 @@ +#ifdef ESP_PLATFORM + +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace tt::service::rtctime { + +constexpr auto* TAG = "RtcTime"; + +Device* RtcTimeService::findRtcDevice() { + if (!rtcDevice) { + rtcDevice = device_find_first_active_by_type(&RTC_TYPE); + } + return rtcDevice; +} + +bool RtcTimeService::isAvailable() const { + return rtcDevice != nullptr; +} + +static bool setSystemTimeFromRtc(Device* rtc) { + RtcDateTime dt = {}; + + error_t err = rtc_get_time(rtc, &dt); + if (err != ERROR_NONE) { + LOG_E(TAG, "Failed to read RTC datetime"); + return false; + } + + if (dt.year < 2024 || dt.year > 2199 || + dt.month < 1 || dt.month > 12 || + dt.day < 1 || dt.day > 31 || + dt.hour > 23 || dt.minute > 59 || dt.second > 59) { + LOG_W(TAG, "Ignoring invalid RTC datetime: %02d-%02d-%02d %02d:%02d:%02d", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second); + return false; + } + + struct tm tm_struct = {}; + tm_struct.tm_year = dt.year - 1900; + tm_struct.tm_mon = dt.month - 1; + tm_struct.tm_mday = dt.day; + tm_struct.tm_hour = dt.hour; + tm_struct.tm_min = dt.minute; + tm_struct.tm_sec = dt.second; + tm_struct.tm_isdst = -1; + + time_t t = mktime(&tm_struct); + if (t == -1) { + LOG_E(TAG, "Failed to convert RTC datetime to time_t"); + return false; + } + + timeval tv = {.tv_sec = t, .tv_usec = 0}; + int result = settimeofday(&tv, nullptr); + if (result != 0) { + LOG_E(TAG, "settimeofday failed"); + return false; + } + + LOG_I(TAG, "System time set from RTC: %02d-%02d-%02d %02d:%02d:%02d", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second); + return true; +} + +static void writeRtcFromSystemTime(Device* rtc) { + time_t now = time(nullptr); + tm tm_struct = {}; + if (!localtime_r(&now, &tm_struct)) { + LOG_E(TAG, "localtime_r failed"); + return; + } + + RtcDateTime dt = { + .year = static_cast(tm_struct.tm_year + 1900), + .month = static_cast(tm_struct.tm_mon + 1), + .day = static_cast(tm_struct.tm_mday), + .hour = static_cast(tm_struct.tm_hour), + .minute = static_cast(tm_struct.tm_min), + .second = static_cast(tm_struct.tm_sec) + }; + + error_t err = rtc_set_time(rtc, &dt); + if (err != ERROR_NONE) { + LOG_E(TAG, "Failed to write system time to RTC"); + } else { + LOG_I(TAG, "RTC updated from system time: %02d-%02d-%02d %02d:%02d:%02d", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second); + } +} + +void RtcTimeService::onTimeChanged(kernel::SystemEvent event) { + if (event == kernel::SystemEvent::Time) { + Device* rtc = findRtcDevice(); + if (rtc) { + writeRtcFromSystemTime(rtc); + } + } +} + +bool RtcTimeService::onStart(ServiceContext& serviceContext) { + Device* rtc = findRtcDevice(); + if (!rtc) { + LOG_W(TAG, "No RTC device found"); + return true; // Continue without RTC + } + + if (setSystemTimeFromRtc(rtc)) { + // Publish time event so other components know time is now valid + kernel::publishSystemEvent(kernel::SystemEvent::Time); + } + + timeEventSubscription = kernel::subscribeSystemEvent( + kernel::SystemEvent::Time, + [this](kernel::SystemEvent event) { onTimeChanged(event); } + ); + + return true; +} + +void RtcTimeService::onStop(ServiceContext& serviceContext) { + if (timeEventSubscription != 0) { + kernel::unsubscribeSystemEvent(timeEventSubscription); + timeEventSubscription = 0; + } +} + +extern const ServiceManifest manifest = { + .id = "RtcTime", + .createService = create +}; + +// Precondition: RtcTimeService must already be registered and started. RtcTime.cpp's +// isAvailable() does NOT use this (it must tolerate the service never having been +// registered on RTC-less devices) - this is for callers that require the service to exist. +std::shared_ptr findRtcTimeService() { + auto service = findServiceById(manifest.id); + assert(service != nullptr); + return std::static_pointer_cast(service); +} + +} // namespace tt::service::rtctime + +#endif diff --git a/TactilityKernel/include/tactility/device.h b/TactilityKernel/include/tactility/device.h index 59f823e29..5943c3187 100644 --- a/TactilityKernel/include/tactility/device.h +++ b/TactilityKernel/include/tactility/device.h @@ -299,6 +299,14 @@ struct Device* device_find_first_active_by_type(const struct DeviceType* type); */ struct Device* device_find_first_by_type(const struct DeviceType* type); +/** + * Find the first device whose driver matches the given compatible string. + * + * @param[in] compatible non-null compatible string to match + * @return the first matching device, or NULL if none found + */ +struct Device* device_find_first_by_compatible(const char* compatible); + #ifdef __cplusplus } #endif diff --git a/TactilityKernel/include/tactility/drivers/camera.h b/TactilityKernel/include/tactility/drivers/camera.h new file mode 100644 index 000000000..be59e4d21 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/camera.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Handle to an open camera stream (see camera_open). + * + * The `device` field identifies the owning camera device so that camera_get_frame/ + * camera_close/etc. can dispatch to the correct driver; implementations embed this as + * the first member (C) or base class (C++) of a larger private struct and may store + * additional state after it. + */ +struct CameraHandleData { + struct Device* device; +}; + +typedef struct CameraHandleData* CameraHandle; + +/** Clockwise rotation applied to frames */ +typedef enum { + CAMERA_ROTATION_0 = 0, + CAMERA_ROTATION_90 = 90, + CAMERA_ROTATION_180 = 180, + CAMERA_ROTATION_270 = 270, +} CameraRotation; + +/** + * @brief API for camera drivers. + */ +struct CameraApi { + /** + * @brief Opens the camera and starts streaming. + * @param[in] device the camera device + * @param[out] out_handle receives the opened camera handle + * @return ERROR_NONE on success + */ + error_t (*open)(struct Device* device, CameraHandle* out_handle); + + /** + * @brief Stops streaming and releases all resources associated with the handle. + * @param[in] handle handle returned by open + * @return ERROR_NONE on success + */ + error_t (*close)(CameraHandle handle); + + /** + * @brief Dequeues one RGB565 frame. Blocks until a frame is available or the timeout expires. + * The caller must call release_frame to return the buffer to the camera queue. + * @param[in] handle handle returned by open + * @param[out] buf pointer to the frame buffer + * @param[out] len byte length of buf + * @param[in] timeout_ms maximum time to wait in milliseconds + * @param[out] out_width optional: width of buf at the moment it was produced (may be NULL) + * @param[out] out_height optional: height of buf at the moment it was produced (may be NULL) + * @retval ERROR_NONE on success + * @retval ERROR_TIMEOUT if no frame arrived in time + */ + error_t (*get_frame)(CameraHandle handle, uint8_t** buf, size_t* len, uint32_t timeout_ms, uint32_t* out_width, uint32_t* out_height); + + /** + * @brief Returns the last dequeued frame buffer to the capture queue. + * Must be called after each successful get_frame. + * @param[in] handle handle returned by open + * @return ERROR_NONE on success + */ + error_t (*release_frame)(CameraHandle handle); + + /** @brief Returns the current frame width in pixels. */ + uint32_t (*get_width)(CameraHandle handle); + + /** @brief Returns the current frame height in pixels. */ + uint32_t (*get_height)(CameraHandle handle); + + /** + * @brief Changes the clockwise rotation applied to subsequent frames. + * Can be called at any time while the handle is open, including during streaming. + * @param[in] handle handle returned by open + * @param[in] rotation new rotation + * @return ERROR_NONE on success + */ + error_t (*set_rotation)(CameraHandle handle, CameraRotation rotation); + + /** + * @brief Captures one frame and JPEG-encodes it. + * @param[in] handle handle returned by open + * @param[out] out_buf pointer to JPEG-encoded data (caller must free with heap_caps_free) + * @param[out] out_len byte length of JPEG data + * @param[in] quality JPEG quality 1-100 + * @return ERROR_NONE on success + */ + error_t (*capture_jpeg)(CameraHandle handle, uint8_t** out_buf, size_t* out_len, uint8_t quality); +}; + +/** @brief See CameraApi::open */ +error_t camera_open(struct Device* device, CameraHandle* out_handle); + +/** @brief See CameraApi::close */ +error_t camera_close(CameraHandle handle); + +/** @brief See CameraApi::get_frame */ +error_t camera_get_frame(CameraHandle handle, uint8_t** buf, size_t* len, uint32_t timeout_ms, uint32_t* out_width, uint32_t* out_height); + +/** @brief See CameraApi::release_frame */ +error_t camera_release_frame(CameraHandle handle); + +/** @brief See CameraApi::get_width */ +uint32_t camera_get_width(CameraHandle handle); + +/** @brief See CameraApi::get_height */ +uint32_t camera_get_height(CameraHandle handle); + +/** @brief See CameraApi::set_rotation */ +error_t camera_set_rotation(CameraHandle handle, CameraRotation rotation); + +/** @brief See CameraApi::capture_jpeg */ +error_t camera_capture_jpeg(CameraHandle handle, uint8_t** out_buf, size_t* out_len, uint8_t quality); + +extern const struct DeviceType CAMERA_TYPE; + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/rtc.h b/TactilityKernel/include/tactility/drivers/rtc.h new file mode 100644 index 000000000..c87ba473a --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/rtc.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct RtcDateTime { + uint16_t year; // 2000–2199 + uint8_t month; // 1–12 + uint8_t day; // 1–31 + uint8_t hour; // 0–23 + uint8_t minute; // 0–59 + uint8_t second; // 0–59 +}; + +/** + * @brief API for RTC (real-time clock) drivers. + */ +struct RtcApi { + /** + * @brief Reads the current date and time from the RTC. + * @param[in] device the RTC device + * @param[out] dt pointer to store the current date and time + * @return ERROR_NONE on success + */ + error_t (*get_time)(struct Device* device, struct RtcDateTime* dt); + + /** + * @brief Writes the date and time to the RTC. + * @param[in] device the RTC device + * @param[in] dt the date and time to write + * @return ERROR_NONE on success + */ + error_t (*set_time)(struct Device* device, const struct RtcDateTime* dt); +}; + +/** + * @brief Reads the current date and time using the specified RTC device. + */ +error_t rtc_get_time(struct Device* device, struct RtcDateTime* dt); + +/** + * @brief Writes the date and time using the specified RTC device. + */ +error_t rtc_set_time(struct Device* device, const struct RtcDateTime* dt); + +extern const struct DeviceType RTC_TYPE; + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/source/device.cpp b/TactilityKernel/source/device.cpp index e04eb6622..8ecd4e951 100644 --- a/TactilityKernel/source/device.cpp +++ b/TactilityKernel/source/device.cpp @@ -395,4 +395,18 @@ Device* device_find_first_by_type(const DeviceType* type) { return found; } +Device* device_find_first_by_compatible(const char* compatible) { + struct Ctx { Device* found; const char* compatible; }; + Ctx ctx = { nullptr, compatible }; + device_for_each(&ctx, [](Device* dev, void* raw_ctx) -> bool { + auto* c = static_cast(raw_ctx); + if (device_is_compatible(dev, c->compatible)) { + c->found = dev; + return false; + } + return true; + }); + return ctx.found; +} + } // extern "C" diff --git a/TactilityKernel/source/drivers/camera.cpp b/TactilityKernel/source/drivers/camera.cpp new file mode 100644 index 000000000..28b4fea79 --- /dev/null +++ b/TactilityKernel/source/drivers/camera.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#define CAMERA_DRIVER_API(driver) ((struct CameraApi*)driver->api) + +extern "C" { + +error_t camera_open(Device* device, CameraHandle* out_handle) { + const auto* driver = device_get_driver(device); + return CAMERA_DRIVER_API(driver)->open(device, out_handle); +} + +error_t camera_close(CameraHandle handle) { + const auto* driver = device_get_driver(handle->device); + return CAMERA_DRIVER_API(driver)->close(handle); +} + +error_t camera_get_frame(CameraHandle handle, uint8_t** buf, size_t* len, uint32_t timeout_ms, uint32_t* out_width, uint32_t* out_height) { + const auto* driver = device_get_driver(handle->device); + return CAMERA_DRIVER_API(driver)->get_frame(handle, buf, len, timeout_ms, out_width, out_height); +} + +error_t camera_release_frame(CameraHandle handle) { + const auto* driver = device_get_driver(handle->device); + return CAMERA_DRIVER_API(driver)->release_frame(handle); +} + +uint32_t camera_get_width(CameraHandle handle) { + const auto* driver = device_get_driver(handle->device); + return CAMERA_DRIVER_API(driver)->get_width(handle); +} + +uint32_t camera_get_height(CameraHandle handle) { + const auto* driver = device_get_driver(handle->device); + return CAMERA_DRIVER_API(driver)->get_height(handle); +} + +error_t camera_set_rotation(CameraHandle handle, CameraRotation rotation) { + const auto* driver = device_get_driver(handle->device); + return CAMERA_DRIVER_API(driver)->set_rotation(handle, rotation); +} + +error_t camera_capture_jpeg(CameraHandle handle, uint8_t** out_buf, size_t* out_len, uint8_t quality) { + const auto* driver = device_get_driver(handle->device); + return CAMERA_DRIVER_API(driver)->capture_jpeg(handle, out_buf, out_len, quality); +} + +const struct DeviceType CAMERA_TYPE { + .name = "camera" +}; + +} diff --git a/TactilityKernel/source/drivers/rtc.cpp b/TactilityKernel/source/drivers/rtc.cpp new file mode 100644 index 000000000..f600e1d2f --- /dev/null +++ b/TactilityKernel/source/drivers/rtc.cpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#define RTC_DRIVER_API(driver) ((struct RtcApi*)driver->api) + +extern "C" { + +error_t rtc_get_time(struct Device* device, struct RtcDateTime* dt) { + const auto* driver = device_get_driver(device); + return RTC_DRIVER_API(driver)->get_time(device, dt); +} + +error_t rtc_set_time(struct Device* device, const struct RtcDateTime* dt) { + const auto* driver = device_get_driver(device); + return RTC_DRIVER_API(driver)->set_time(device, dt); +} + +const struct DeviceType RTC_TYPE { + .name = "rtc" +}; + +} diff --git a/TactilityKernel/source/kernel_symbols.c b/TactilityKernel/source/kernel_symbols.c index 70c79d7c2..a84d24b26 100644 --- a/TactilityKernel/source/kernel_symbols.c +++ b/TactilityKernel/source/kernel_symbols.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +67,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(device_find_by_name), DEFINE_MODULE_SYMBOL(device_find_first_active_by_type), DEFINE_MODULE_SYMBOL(device_find_first_by_type), + DEFINE_MODULE_SYMBOL(device_find_first_by_compatible), // driver DEFINE_MODULE_SYMBOL(driver_construct), DEFINE_MODULE_SYMBOL(driver_destruct), @@ -118,6 +121,10 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(I2S_CONTROLLER_TYPE), // drivers/root DEFINE_MODULE_SYMBOL(root_is_model), + // drivers/rtc + DEFINE_MODULE_SYMBOL(rtc_get_time), + DEFINE_MODULE_SYMBOL(rtc_set_time), + DEFINE_MODULE_SYMBOL(RTC_TYPE), // drivers/spi_controller DEFINE_MODULE_SYMBOL(spi_controller_lock), DEFINE_MODULE_SYMBOL(spi_controller_try_lock), @@ -182,6 +189,16 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(bluetooth_hid_device_send_gamepad), DEFINE_MODULE_SYMBOL(bluetooth_hid_device_is_connected), DEFINE_MODULE_SYMBOL(BLUETOOTH_HID_DEVICE_TYPE), + // drivers/camera + DEFINE_MODULE_SYMBOL(camera_open), + DEFINE_MODULE_SYMBOL(camera_close), + DEFINE_MODULE_SYMBOL(camera_get_frame), + DEFINE_MODULE_SYMBOL(camera_release_frame), + DEFINE_MODULE_SYMBOL(camera_get_width), + DEFINE_MODULE_SYMBOL(camera_get_height), + DEFINE_MODULE_SYMBOL(camera_set_rotation), + DEFINE_MODULE_SYMBOL(camera_capture_jpeg), + DEFINE_MODULE_SYMBOL(CAMERA_TYPE), // drivers/wifi DEFINE_MODULE_SYMBOL(wifi_find_first_registered_device), DEFINE_MODULE_SYMBOL(wifi_get_radio_state),