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/TactilityFreeRtos/include"
|
||||||
"Libraries/lvgl/include"
|
"Libraries/lvgl/include"
|
||||||
"Modules/lvgl-module/include"
|
"Modules/lvgl-module/include"
|
||||||
"Drivers/bm8563-module/include"
|
# DRIVER_INCLUDE_DIRS_PLACEHOLDER
|
||||||
"Drivers/bmi270-module/include"
|
|
||||||
"Drivers/mpu6886-module/include"
|
|
||||||
"Drivers/pi4ioe5v6408-module/include"
|
|
||||||
"Drivers/qmi8658-module/include"
|
|
||||||
"Drivers/rx8130ce-module/include"
|
|
||||||
REQUIRES esp_timer
|
REQUIRES esp_timer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,7 @@ macro(tactility_project project_name)
|
|||||||
|
|
||||||
set(COMPONENTS
|
set(COMPONENTS
|
||||||
TactilityFreeRtos
|
TactilityFreeRtos
|
||||||
bm8563-module
|
# DRIVER_COMPONENTS_PLACEHOLDER
|
||||||
bmi270-module
|
|
||||||
mpu6886-module
|
|
||||||
pi4ioe5v6408-module
|
|
||||||
qmi8658-module
|
|
||||||
rx8130ce-module
|
|
||||||
)
|
)
|
||||||
|
|
||||||
endmacro()
|
endmacro()
|
||||||
|
|||||||
+59
-10
@@ -88,6 +88,17 @@ def write_module_cmakelists(path, content):
|
|||||||
with open(path, 'w') as f:
|
with open(path, 'w') as f:
|
||||||
f.write(content)
|
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):
|
def add_driver(target_path, driver_name):
|
||||||
mappings = get_driver_mappings(driver_name)
|
mappings = get_driver_mappings(driver_name)
|
||||||
map_copy(mappings, target_path)
|
map_copy(mappings, target_path)
|
||||||
@@ -100,6 +111,44 @@ def add_module(target_path, module_name):
|
|||||||
cmakelists_content = create_module_cmakelists(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)
|
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():
|
def main():
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: release-sdk.py [target_path]")
|
print("Usage: release-sdk.py [target_path]")
|
||||||
@@ -136,9 +185,6 @@ def main():
|
|||||||
# elf_loader
|
# elf_loader
|
||||||
{'src': 'Libraries/elf_loader/elf_loader.cmake', 'dst': 'Libraries/elf_loader/'},
|
{'src': 'Libraries/elf_loader/elf_loader.cmake', 'dst': 'Libraries/elf_loader/'},
|
||||||
{'src': 'Libraries/elf_loader/license.txt', '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)
|
map_copy(mappings, target_path)
|
||||||
@@ -147,13 +193,16 @@ def main():
|
|||||||
add_module(target_path, "lvgl-module")
|
add_module(target_path, "lvgl-module")
|
||||||
add_module(target_path, "crypt-module")
|
add_module(target_path, "crypt-module")
|
||||||
|
|
||||||
# Drivers
|
# Drivers - only ones actually built for this target (chip-restricted drivers like
|
||||||
add_driver(target_path, "bm8563-module")
|
# sc2356-module won't have a .a outside ESP32-P4)
|
||||||
add_driver(target_path, "bmi270-module")
|
available_drivers = [d for d in discover_all_drivers() if driver_is_available(d)]
|
||||||
add_driver(target_path, "mpu6886-module")
|
for driver_name in available_drivers:
|
||||||
add_driver(target_path, "pi4ioe5v6408-module")
|
add_driver(target_path, driver_name)
|
||||||
add_driver(target_path, "qmi8658-module")
|
|
||||||
add_driver(target_path, "rx8130ce-module")
|
# 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
|
# Output ESP-IDF SDK version to file
|
||||||
esp_idf_version = os.environ.get("ESP_IDF_VERSION", "")
|
esp_idf_version = os.environ.get("ESP_IDF_VERSION", "")
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ dependencies:
|
|||||||
- Drivers/pi4ioe5v6408-module
|
- Drivers/pi4ioe5v6408-module
|
||||||
- Drivers/qmi8658-module
|
- Drivers/qmi8658-module
|
||||||
- Drivers/rx8130ce-module
|
- Drivers/rx8130ce-module
|
||||||
|
- Drivers/sc2356-module
|
||||||
dts: generic,esp32p4.dts
|
dts: generic,esp32p4.dts
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
|||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS ${SOURCE_FILES}
|
SRCS ${SOURCE_FILES}
|
||||||
INCLUDE_DIRS "Source"
|
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 <Tactility/hal/Configuration.h>
|
||||||
|
|
||||||
|
#include <esp_clock_output.h>
|
||||||
|
|
||||||
using namespace tt::hal;
|
using namespace tt::hal;
|
||||||
|
|
||||||
static constexpr auto* TAG = "Tab5";
|
static constexpr auto* TAG = "Tab5";
|
||||||
@@ -249,6 +251,29 @@ static error_t initMicrophone(::Device* i2c_controller) {
|
|||||||
return error;
|
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() {
|
static bool initBoot() {
|
||||||
auto* i2c0 = device_find_by_name("i2c0");
|
auto* i2c0 = device_find_by_name("i2c0");
|
||||||
check(i2c0, "i2c0 not found");
|
check(i2c0, "i2c0 not found");
|
||||||
@@ -258,6 +283,7 @@ static bool initBoot() {
|
|||||||
auto* io_expander1 = device_find_by_name("io_expander1");
|
auto* io_expander1 = device_find_by_name("io_expander1");
|
||||||
check(io_expander1, "io_expander1 not found");
|
check(io_expander1, "io_expander1 not found");
|
||||||
|
|
||||||
|
initCameraOsc();
|
||||||
initExpander0(io_expander0);
|
initExpander0(io_expander0);
|
||||||
initExpander1(io_expander1);
|
initExpander1(io_expander1);
|
||||||
|
|
||||||
|
|||||||
@@ -45,3 +45,7 @@ sdkconfig.CONFIG_CACHE_L2_CACHE_256KB=y
|
|||||||
sdkconfig.CONFIG_LVGL_PORT_ENABLE_PPA=y
|
sdkconfig.CONFIG_LVGL_PORT_ENABLE_PPA=y
|
||||||
sdkconfig.CONFIG_LV_DRAW_BUF_ALIGN=64
|
sdkconfig.CONFIG_LV_DRAW_BUF_ALIGN=64
|
||||||
sdkconfig.CONFIG_LV_DEF_REFR_PERIOD=15
|
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/bmi270-module
|
||||||
- Drivers/ina226-module
|
- Drivers/ina226-module
|
||||||
- Drivers/rx8130ce-module
|
- Drivers/rx8130ce-module
|
||||||
|
- Drivers/sc2356-module
|
||||||
dts: m5stack,tab5.dts
|
dts: m5stack,tab5.dts
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <bindings/ina226.h>
|
#include <bindings/ina226.h>
|
||||||
#include <bindings/pi4ioe5v6408.h>
|
#include <bindings/pi4ioe5v6408.h>
|
||||||
#include <bindings/rx8130ce.h>
|
#include <bindings/rx8130ce.h>
|
||||||
|
#include <bindings/sc2356.h>
|
||||||
|
|
||||||
/ {
|
/ {
|
||||||
compatible = "root";
|
compatible = "root";
|
||||||
@@ -60,6 +61,11 @@
|
|||||||
reg = <0x41>;
|
reg = <0x41>;
|
||||||
shunt-milliohms = <5>;
|
shunt-milliohms = <5>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sc2356 {
|
||||||
|
compatible = "smartsens,sc2356";
|
||||||
|
reg = <0x36>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
port_a: grove0 {
|
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 (Linux/macOS, no ESP-IDF needed)
|
||||||
|
|
||||||
|
Simulator is not supported on Windows
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cmake -B buildsim -G Ninja
|
cmake -B buildsim -G Ninja
|
||||||
ninja -C buildsim # build firmware + tests
|
ninja -C buildsim # build firmware + tests
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <tactility/drivers/rtc.h>
|
||||||
#include <tactility/error.h>
|
#include <tactility/error.h>
|
||||||
|
|
||||||
struct Device;
|
struct Device;
|
||||||
@@ -15,30 +16,21 @@ struct Bm8563Config {
|
|||||||
uint8_t address;
|
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.
|
* Read the current date and time from the RTC.
|
||||||
* @param[in] device bm8563 device
|
* @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
|
* @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.
|
* Write the date and time to the RTC.
|
||||||
* @param[in] device bm8563 device
|
* @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
|
* @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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ static error_t stop(Device* device) {
|
|||||||
|
|
||||||
extern "C" {
|
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* i2c_controller = device_get_parent(device);
|
||||||
auto address = GET_CONFIG(device)->address;
|
auto address = GET_CONFIG(device)->address;
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ error_t bm8563_get_datetime(Device* device, Bm8563DateTime* dt) {
|
|||||||
return ERROR_NONE;
|
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 ||
|
if (dt->year < 2000 || dt->year > 2199 ||
|
||||||
dt->month < 1 || dt->month > 12 ||
|
dt->month < 1 || dt->month > 12 ||
|
||||||
dt->day < 1 || dt->day > 31 ||
|
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);
|
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 = {
|
Driver bm8563_driver = {
|
||||||
.name = "bm8563",
|
.name = "bm8563",
|
||||||
.compatible = (const char*[]) { "belling,bm8563", nullptr },
|
.compatible = (const char*[]) { "belling,bm8563", nullptr },
|
||||||
.start_device = start,
|
.start_device = start,
|
||||||
.stop_device = stop,
|
.stop_device = stop,
|
||||||
.api = nullptr,
|
.api = &bm8563_rtc_api,
|
||||||
.device_type = nullptr,
|
.device_type = &RTC_TYPE,
|
||||||
.owner = &bm8563_module,
|
.owner = &bm8563_module,
|
||||||
.internal = nullptr
|
.internal = nullptr
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,13 +21,11 @@ static error_t stop() {
|
|||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern const ModuleSymbol bm8563_module_symbols[];
|
|
||||||
|
|
||||||
Module bm8563_module = {
|
Module bm8563_module = {
|
||||||
.name = "bm8563",
|
.name = "bm8563",
|
||||||
.start = start,
|
.start = start,
|
||||||
.stop = stop,
|
.stop = stop,
|
||||||
.symbols = bm8563_module_symbols,
|
.symbols = nullptr,
|
||||||
.internal = 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
|
#pragma once
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <tactility/drivers/rtc.h>
|
||||||
#include <tactility/error.h>
|
#include <tactility/error.h>
|
||||||
|
|
||||||
struct Device;
|
struct Device;
|
||||||
@@ -15,30 +16,21 @@ struct Rx8130ceConfig {
|
|||||||
uint8_t address;
|
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.
|
* Read the current date and time from the RTC.
|
||||||
* @param[in] device rx8130ce device
|
* @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)
|
* @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.
|
* Write the date and time to the RTC.
|
||||||
* @param[in] device rx8130ce device
|
* @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
|
* @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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,11 @@ static error_t stop() {
|
|||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern const ModuleSymbol rx8130ce_module_symbols[];
|
|
||||||
|
|
||||||
Module rx8130ce_module = {
|
Module rx8130ce_module = {
|
||||||
.name = "rx8130ce",
|
.name = "rx8130ce",
|
||||||
.start = start,
|
.start = start,
|
||||||
.stop = stop,
|
.stop = stop,
|
||||||
.symbols = rx8130ce_module_symbols,
|
.symbols = nullptr,
|
||||||
.internal = nullptr
|
.internal = nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ static error_t stop(Device* device) {
|
|||||||
|
|
||||||
extern "C" {
|
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* i2c_controller = device_get_parent(device);
|
||||||
auto address = GET_CONFIG(device)->address;
|
auto address = GET_CONFIG(device)->address;
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ error_t rx8130ce_get_datetime(Device* device, Rx8130ceDateTime* dt) {
|
|||||||
return ERROR_NONE;
|
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 ||
|
if (dt->month < 1 || dt->month > 12 ||
|
||||||
dt->day < 1 || dt->day > 31 ||
|
dt->day < 1 || dt->day > 31 ||
|
||||||
dt->hour > 23 || dt->minute > 59 || dt->second > 59) {
|
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;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RtcApi rx8130ce_rtc_api = {
|
||||||
|
.get_time = rx8130ce_get_datetime,
|
||||||
|
.set_time = rx8130ce_set_datetime
|
||||||
|
};
|
||||||
|
|
||||||
Driver rx8130ce_driver = {
|
Driver rx8130ce_driver = {
|
||||||
.name = "rx8130ce",
|
.name = "rx8130ce",
|
||||||
.compatible = (const char*[]) { "epson,rx8130ce", nullptr },
|
.compatible = (const char*[]) { "epson,rx8130ce", nullptr },
|
||||||
.start_device = start,
|
.start_device = start,
|
||||||
.stop_device = stop,
|
.stop_device = stop,
|
||||||
.api = nullptr,
|
.api = &rx8130ce_rtc_api,
|
||||||
.device_type = nullptr,
|
.device_type = &RTC_TYPE,
|
||||||
.owner = &rx8130ce_module,
|
.owner = &rx8130ce_module,
|
||||||
.internal = nullptr
|
.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:
|
dependencies:
|
||||||
|
espressif/esp_video:
|
||||||
|
version: "2.2.0"
|
||||||
|
rules:
|
||||||
|
- if: "target in [esp32s3, esp32c6, esp32p4]"
|
||||||
espressif/esp_hosted:
|
espressif/esp_hosted:
|
||||||
version: "*"
|
version: "*"
|
||||||
rules:
|
rules:
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(lv_color_darken),
|
DEFINE_MODULE_SYMBOL(lv_color_darken),
|
||||||
DEFINE_MODULE_SYMBOL(lv_color_hsv_to_rgb),
|
DEFINE_MODULE_SYMBOL(lv_color_hsv_to_rgb),
|
||||||
DEFINE_MODULE_SYMBOL(lv_color_to_32),
|
DEFINE_MODULE_SYMBOL(lv_color_to_32),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_color_mix),
|
||||||
DEFINE_MODULE_SYMBOL(lv_obj_center),
|
DEFINE_MODULE_SYMBOL(lv_obj_center),
|
||||||
DEFINE_MODULE_SYMBOL(lv_obj_clean),
|
DEFINE_MODULE_SYMBOL(lv_obj_clean),
|
||||||
DEFINE_MODULE_SYMBOL(lv_obj_create),
|
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_rotation),
|
||||||
DEFINE_MODULE_SYMBOL(lv_display_set_offset),
|
DEFINE_MODULE_SYMBOL(lv_display_set_offset),
|
||||||
DEFINE_MODULE_SYMBOL(lv_display_trigger_activity),
|
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
|
// lv_pct
|
||||||
DEFINE_MODULE_SYMBOL(lv_pct),
|
DEFINE_MODULE_SYMBOL(lv_pct),
|
||||||
DEFINE_MODULE_SYMBOL(lv_pct_to_px),
|
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/Service.h>
|
||||||
#include <Tactility/service/loader/Loader.h>
|
#include <Tactility/service/loader/Loader.h>
|
||||||
|
|
||||||
|
#include <Tactility/Semaphore.h>
|
||||||
|
|
||||||
#include <tactility/concurrent/dispatcher.h>
|
#include <tactility/concurrent/dispatcher.h>
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -30,6 +32,16 @@ class GuiService final : public Service {
|
|||||||
RecursiveMutex mutex;
|
RecursiveMutex mutex;
|
||||||
PubSub<loader::LoaderService::Event>::SubscriptionHandle loader_pubsub_subscription = nullptr;
|
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
|
// Layers and Canvas
|
||||||
lv_obj_t* appRootWidget = nullptr;
|
lv_obj_t* appRootWidget = nullptr;
|
||||||
lv_obj_t* statusbarWidget = nullptr;
|
lv_obj_t* statusbarWidget = nullptr;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <tactility/concurrent/thread.h>
|
#include <tactility/concurrent/thread.h>
|
||||||
#include <tactility/crypt_module.h>
|
#include <tactility/crypt_module.h>
|
||||||
#include <tactility/drivers/grove.h>
|
#include <tactility/drivers/grove.h>
|
||||||
|
#include <tactility/drivers/rtc.h>
|
||||||
#include <tactility/drivers/uart_controller.h>
|
#include <tactility/drivers/uart_controller.h>
|
||||||
#include <tactility/filesystem/file_system.h>
|
#include <tactility/filesystem/file_system.h>
|
||||||
#include <tactility/hal_device_module.h>
|
#include <tactility/hal_device_module.h>
|
||||||
@@ -85,6 +86,7 @@ namespace service {
|
|||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
namespace displayidle { extern const ServiceManifest manifest; }
|
namespace displayidle { extern const ServiceManifest manifest; }
|
||||||
namespace keyboardidle { extern const ServiceManifest manifest; }
|
namespace keyboardidle { extern const ServiceManifest manifest; }
|
||||||
|
namespace rtctime { extern const ServiceManifest manifest; }
|
||||||
#endif
|
#endif
|
||||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||||
namespace screenshot { extern const ServiceManifest manifest; }
|
namespace screenshot { extern const ServiceManifest manifest; }
|
||||||
@@ -280,6 +282,9 @@ static void registerAndStartSecondaryServices() {
|
|||||||
addService(service::statusbar::manifest);
|
addService(service::statusbar::manifest);
|
||||||
addService(service::memorychecker::manifest);
|
addService(service::memorychecker::manifest);
|
||||||
#if defined(ESP_PLATFORM)
|
#if defined(ESP_PLATFORM)
|
||||||
|
if (device_exists_of_type(&RTC_TYPE)) {
|
||||||
|
addService(service::rtctime::manifest);
|
||||||
|
}
|
||||||
addService(service::displayidle::manifest);
|
addService(service::displayidle::manifest);
|
||||||
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
||||||
addService(service::keyboardidle::manifest);
|
addService(service::keyboardidle::manifest);
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ namespace tt::app {
|
|||||||
|
|
||||||
std::string AppPaths::getUserDataPath() const {
|
std::string AppPaths::getUserDataPath() const {
|
||||||
if (manifest.appLocation.isInternal()) {
|
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 {
|
} 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());
|
auto app_instance = std::static_pointer_cast<app::AppInstance>(app::getCurrentAppContext());
|
||||||
item = new GuiDispatchItem{this, GuiDispatchType::Show, app_instance};
|
item = new GuiDispatchItem{this, GuiDispatchType::Show, app_instance};
|
||||||
} else if (event == LoaderService::Event::ApplicationHiding) {
|
} 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};
|
item = new GuiDispatchItem{this, GuiDispatchType::Hide, nullptr};
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@@ -71,6 +79,19 @@ void GuiService::onLoaderEvent(LoaderService::Event event) {
|
|||||||
if (dispatcher_dispatch(dispatcher, item, onGuiDispatch) != ERROR_NONE) {
|
if (dispatcher_dispatch(dispatcher, item, onGuiDispatch) != ERROR_NONE) {
|
||||||
LOG_E(TAG, "Failed to dispatch gui event");
|
LOG_E(TAG, "Failed to dispatch gui event");
|
||||||
delete item;
|
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() {
|
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();
|
auto lock = mutex.asScopedLock();
|
||||||
lock.lock();
|
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);
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#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;
|
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"
|
} // 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_serial.h>
|
||||||
#include <tactility/drivers/bluetooth_midi.h>
|
#include <tactility/drivers/bluetooth_midi.h>
|
||||||
#include <tactility/drivers/bluetooth_hid_device.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_hid.h>
|
||||||
#include <tactility/drivers/usb_host_midi.h>
|
#include <tactility/drivers/usb_host_midi.h>
|
||||||
#include <tactility/drivers/usb_host_msc.h>
|
#include <tactility/drivers/usb_host_msc.h>
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
#include <tactility/drivers/i2c_controller.h>
|
#include <tactility/drivers/i2c_controller.h>
|
||||||
#include <tactility/drivers/i2s_controller.h>
|
#include <tactility/drivers/i2s_controller.h>
|
||||||
#include <tactility/drivers/root.h>
|
#include <tactility/drivers/root.h>
|
||||||
|
#include <tactility/drivers/rtc.h>
|
||||||
#include <tactility/drivers/spi_controller.h>
|
#include <tactility/drivers/spi_controller.h>
|
||||||
#include <tactility/drivers/uart_controller.h>
|
#include <tactility/drivers/uart_controller.h>
|
||||||
#include <tactility/drivers/wifi.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_by_name),
|
||||||
DEFINE_MODULE_SYMBOL(device_find_first_active_by_type),
|
DEFINE_MODULE_SYMBOL(device_find_first_active_by_type),
|
||||||
DEFINE_MODULE_SYMBOL(device_find_first_by_type),
|
DEFINE_MODULE_SYMBOL(device_find_first_by_type),
|
||||||
|
DEFINE_MODULE_SYMBOL(device_find_first_by_compatible),
|
||||||
// driver
|
// driver
|
||||||
DEFINE_MODULE_SYMBOL(driver_construct),
|
DEFINE_MODULE_SYMBOL(driver_construct),
|
||||||
DEFINE_MODULE_SYMBOL(driver_destruct),
|
DEFINE_MODULE_SYMBOL(driver_destruct),
|
||||||
@@ -118,6 +121,10 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(I2S_CONTROLLER_TYPE),
|
DEFINE_MODULE_SYMBOL(I2S_CONTROLLER_TYPE),
|
||||||
// drivers/root
|
// drivers/root
|
||||||
DEFINE_MODULE_SYMBOL(root_is_model),
|
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
|
// drivers/spi_controller
|
||||||
DEFINE_MODULE_SYMBOL(spi_controller_lock),
|
DEFINE_MODULE_SYMBOL(spi_controller_lock),
|
||||||
DEFINE_MODULE_SYMBOL(spi_controller_try_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_send_gamepad),
|
||||||
DEFINE_MODULE_SYMBOL(bluetooth_hid_device_is_connected),
|
DEFINE_MODULE_SYMBOL(bluetooth_hid_device_is_connected),
|
||||||
DEFINE_MODULE_SYMBOL(BLUETOOTH_HID_DEVICE_TYPE),
|
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
|
// drivers/wifi
|
||||||
DEFINE_MODULE_SYMBOL(wifi_find_first_registered_device),
|
DEFINE_MODULE_SYMBOL(wifi_find_first_registered_device),
|
||||||
DEFINE_MODULE_SYMBOL(wifi_get_radio_state),
|
DEFINE_MODULE_SYMBOL(wifi_get_radio_state),
|
||||||
|
|||||||
Reference in New Issue
Block a user