Tab5 camera + other stuff (#558)
* Tab5 camera + other stuff Tab5 camera driver - SC2356 Custom SliderBox widget - slider with plus and minus buttons + value label and snapping Rtc Time service + rtc api Sdk release - only include drivers built for that specific target, eg: sc2356 driver is mipi / p4 only. No more hardcoded manual sdk cmakelists New function to find device by compatible string match. no more static cast bool wildness when trying to match a single device (like M5Stack PaperS3 for example) * feedback + fixes Fixed external app user data path. fix(gui): block app teardown until onHide() completes, preventing ELF unload racing a still-running app added camera device type and api * drain the snake sssem --------- Co-authored-by: Ken Van Hoeylandt <git@kenvanhoeylandt.net>
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
+59
-10
@@ -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", "")
|
||||
|
||||
@@ -7,4 +7,5 @@ dependencies:
|
||||
- Drivers/pi4ioe5v6408-module
|
||||
- Drivers/qmi8658-module
|
||||
- Drivers/rx8130ce-module
|
||||
- Drivers/sc2356-module
|
||||
dts: generic,esp32p4.dts
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <Tactility/hal/Configuration.h>
|
||||
|
||||
#include <esp_clock_output.h>
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,4 +4,5 @@ dependencies:
|
||||
- Drivers/bmi270-module
|
||||
- Drivers/ina226-module
|
||||
- Drivers/rx8130ce-module
|
||||
- Drivers/sc2356-module
|
||||
dts: m5stack,tab5.dts
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <bindings/ina226.h>
|
||||
#include <bindings/pi4ioe5v6408.h>
|
||||
#include <bindings/rx8130ce.h>
|
||||
#include <bindings/sc2356.h>
|
||||
|
||||
/ {
|
||||
compatible = "root";
|
||||
@@ -60,6 +61,11 @@
|
||||
reg = <0x41>;
|
||||
shunt-milliohms = <5>;
|
||||
};
|
||||
|
||||
sc2356 {
|
||||
compatible = "smartsens,sc2356";
|
||||
reg = <0x36>;
|
||||
};
|
||||
};
|
||||
|
||||
port_a: grove0 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <tactility/drivers/rtc.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/bm8563.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol bm8563_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(bm8563_get_datetime),
|
||||
DEFINE_MODULE_SYMBOL(bm8563_set_datetime),
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <tactility/drivers/rtc.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/rx8130ce.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol rx8130ce_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(rx8130ce_get_datetime),
|
||||
DEFINE_MODULE_SYMBOL(rx8130ce_set_datetime),
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,5 @@
|
||||
description: Smartsens SC2356 2MP MIPI CSI camera sensor
|
||||
|
||||
include: [ "i2c-device.yaml" ]
|
||||
|
||||
compatible: "smartsens,sc2356"
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/sc2356.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(sc2356, struct Sc2356Config)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <tactility/drivers/camera.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module sc2356_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
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"
|
||||
@@ -0,0 +1,562 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/sc2356.h>
|
||||
#include <sc2356_module.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/esp32_i2c_master.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_video_device.h>
|
||||
#include <esp_video_init.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/videodev2.h>
|
||||
#include <sys/errno.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/poll.h>
|
||||
|
||||
#include <driver/jpeg_encode.h>
|
||||
#include <driver/ppa.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#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<const Sc2356Config*>((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<Sc2356State*>(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<gpio_num_t>(-1);
|
||||
csi_config.pwdn_pin = static_cast<gpio_num_t>(-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<uint8_t*>(
|
||||
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<uint8_t*>(
|
||||
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<Sc2356State*>(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<Sc2356State*>(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<Sc2356State*>(handle);
|
||||
|
||||
// Use poll to enforce timeout
|
||||
struct pollfd pfd = {};
|
||||
pfd.fd = state->fd;
|
||||
pfd.events = POLLIN;
|
||||
int ret = poll(&pfd, 1, static_cast<int>(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<int>(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<Sc2356State*>(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<uint32_t>(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<Sc2356State*>(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<Sc2356State*>(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<Sc2356State*>(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<uint8_t*>(
|
||||
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<uint32_t>(frame_len),
|
||||
jpeg_buf, static_cast<uint32_t>(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<size_t>(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"
|
||||
@@ -1,4 +1,8 @@
|
||||
dependencies:
|
||||
espressif/esp_video:
|
||||
version: "2.2.0"
|
||||
rules:
|
||||
- if: "target in [esp32s3, esp32c6, esp32p4]"
|
||||
espressif/esp_hosted:
|
||||
version: "*"
|
||||
rules:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/kernel/SystemEvents.h>
|
||||
#include <Tactility/service/Service.h>
|
||||
#include <Tactility/service/ServiceContext.h>
|
||||
#include <Tactility/service/rtctime/RtcTime.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
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<RtcTimeService> findRtcTimeService();
|
||||
|
||||
} // namespace tt::service::rtctime
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <Tactility/service/Service.h>
|
||||
#include <Tactility/service/loader/Loader.h>
|
||||
|
||||
#include <Tactility/Semaphore.h>
|
||||
|
||||
#include <tactility/concurrent/dispatcher.h>
|
||||
|
||||
#include <cstdio>
|
||||
@@ -30,6 +32,16 @@ class GuiService final : public Service {
|
||||
RecursiveMutex mutex;
|
||||
PubSub<loader::LoaderService::Event>::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;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/crypt_module.h>
|
||||
#include <tactility/drivers/grove.h>
|
||||
#include <tactility/drivers/rtc.h>
|
||||
#include <tactility/drivers/uart_controller.h>
|
||||
#include <tactility/filesystem/file_system.h>
|
||||
#include <tactility/hal_device_module.h>
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
#define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration
|
||||
|
||||
#include <Tactility/lvgl/SliderBox.h>
|
||||
|
||||
#include <tactility/lvgl_fonts.h>
|
||||
|
||||
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_obj_t*>(lv_event_get_user_data(event));
|
||||
auto* sliderBox = reinterpret_cast<SliderBox*>(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_obj_t*>(lv_event_get_user_data(event));
|
||||
auto* sliderBox = reinterpret_cast<SliderBox*>(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<SliderBox*>(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<SliderBox*>(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<SliderBox*>(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
|
||||
@@ -63,6 +63,14 @@ void GuiService::onLoaderEvent(LoaderService::Event event) {
|
||||
auto app_instance = std::static_pointer_cast<app::AppInstance>(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::AppInstance> 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();
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#include <Tactility/service/rtctime/RtcTime.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <Tactility/service/rtctime/RtcTimeService.h>
|
||||
#include <Tactility/service/ServiceManifest.h>
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
#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<RtcTimeService>(manifest.id);
|
||||
return service != nullptr && service->isAvailable();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace tt::service::rtctime
|
||||
@@ -0,0 +1,152 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/service/rtctime/RtcTimeService.h>
|
||||
|
||||
#include <Tactility/service/ServiceContext.h>
|
||||
#include <Tactility/service/ServiceManifest.h>
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/rtc.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <cassert>
|
||||
#include <ctime>
|
||||
#include <sys/time.h>
|
||||
|
||||
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<uint16_t>(tm_struct.tm_year + 1900),
|
||||
.month = static_cast<uint8_t>(tm_struct.tm_mon + 1),
|
||||
.day = static_cast<uint8_t>(tm_struct.tm_mday),
|
||||
.hour = static_cast<uint8_t>(tm_struct.tm_hour),
|
||||
.minute = static_cast<uint8_t>(tm_struct.tm_min),
|
||||
.second = static_cast<uint8_t>(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<RtcTimeService>
|
||||
};
|
||||
|
||||
// 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<RtcTimeService> findRtcTimeService() {
|
||||
auto service = findServiceById(manifest.id);
|
||||
assert(service != nullptr);
|
||||
return std::static_pointer_cast<RtcTimeService>(service);
|
||||
}
|
||||
|
||||
} // namespace tt::service::rtctime
|
||||
|
||||
#endif
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
#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
|
||||
@@ -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<Ctx*>(raw_ctx);
|
||||
if (device_is_compatible(dev, c->compatible)) {
|
||||
c->found = dev;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return ctx.found;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/drivers/camera.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/device.h>
|
||||
|
||||
#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"
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/drivers/rtc.h>
|
||||
#include <tactility/device.h>
|
||||
|
||||
#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"
|
||||
};
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <tactility/drivers/bluetooth_serial.h>
|
||||
#include <tactility/drivers/bluetooth_midi.h>
|
||||
#include <tactility/drivers/bluetooth_hid_device.h>
|
||||
#include <tactility/drivers/camera.h>
|
||||
#include <tactility/drivers/usb_host_hid.h>
|
||||
#include <tactility/drivers/usb_host_midi.h>
|
||||
#include <tactility/drivers/usb_host_msc.h>
|
||||
@@ -16,6 +17,7 @@
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <tactility/drivers/root.h>
|
||||
#include <tactility/drivers/rtc.h>
|
||||
#include <tactility/drivers/spi_controller.h>
|
||||
#include <tactility/drivers/uart_controller.h>
|
||||
#include <tactility/drivers/wifi.h>
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user