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:
Shadowtrance
2026-07-10 16:45:03 +10:00
committed by GitHub
parent 16a61a087c
commit 7a7f09be35
47 changed files with 1894 additions and 83 deletions
+5 -13
View File
@@ -2,6 +2,7 @@
#pragma once
#include <stdint.h>
#include <tactility/drivers/rtc.h>
#include <tactility/error.h>
struct Device;
@@ -15,30 +16,21 @@ struct Bm8563Config {
uint8_t address;
};
struct Bm8563DateTime {
uint16_t year; // 20002199
uint8_t month; // 112
uint8_t day; // 131
uint8_t hour; // 023
uint8_t minute; // 059
uint8_t second; // 059
};
/**
* Read the current date and time from the RTC.
* @param[in] device bm8563 device
* @param[out] dt Pointer to Bm8563DateTime to populate
* @param[out] dt Pointer to RtcDateTime to populate
* @return ERROR_NONE on success
*/
error_t bm8563_get_datetime(struct Device* device, struct Bm8563DateTime* dt);
error_t bm8563_get_datetime(struct Device* device, struct RtcDateTime* dt);
/**
* Write the date and time to the RTC.
* @param[in] device bm8563 device
* @param[in] dt Pointer to Bm8563DateTime to write (year must be 20002199)
* @param[in] dt Pointer to RtcDateTime to write (year must be 20002199)
* @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
}
+9 -4
View File
@@ -52,7 +52,7 @@ static error_t stop(Device* device) {
extern "C" {
error_t bm8563_get_datetime(Device* device, Bm8563DateTime* dt) {
error_t bm8563_get_datetime(Device* device, RtcDateTime* dt) {
auto* i2c_controller = device_get_parent(device);
auto address = GET_CONFIG(device)->address;
@@ -78,7 +78,7 @@ error_t bm8563_get_datetime(Device* device, Bm8563DateTime* dt) {
return ERROR_NONE;
}
error_t bm8563_set_datetime(Device* device, const Bm8563DateTime* dt) {
error_t bm8563_set_datetime(Device* device, const RtcDateTime* dt) {
if (dt->year < 2000 || dt->year > 2199 ||
dt->month < 1 || dt->month > 12 ||
dt->day < 1 || dt->day > 31 ||
@@ -104,13 +104,18 @@ error_t bm8563_set_datetime(Device* device, const Bm8563DateTime* dt) {
return i2c_controller_write_register(i2c_controller, address, REG_SECONDS, buf, sizeof(buf), I2C_TIMEOUT_TICKS);
}
RtcApi bm8563_rtc_api = {
.get_time = bm8563_get_datetime,
.set_time = bm8563_set_datetime
};
Driver bm8563_driver = {
.name = "bm8563",
.compatible = (const char*[]) { "belling,bm8563", nullptr },
.start_device = start,
.stop_device = stop,
.api = nullptr,
.device_type = nullptr,
.api = &bm8563_rtc_api,
.device_type = &RTC_TYPE,
.owner = &bm8563_module,
.internal = nullptr
};
+1 -3
View File
@@ -21,13 +21,11 @@ static error_t stop() {
return ERROR_NONE;
}
extern const ModuleSymbol bm8563_module_symbols[];
Module bm8563_module = {
.name = "bm8563",
.start = start,
.stop = stop,
.symbols = bm8563_module_symbols,
.symbols = nullptr,
.internal = nullptr
};
-9
View File
@@ -1,9 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/bm8563.h>
#include <tactility/module.h>
const struct ModuleSymbol bm8563_module_symbols[] = {
DEFINE_MODULE_SYMBOL(bm8563_get_datetime),
DEFINE_MODULE_SYMBOL(bm8563_set_datetime),
MODULE_SYMBOL_TERMINATOR
};
@@ -2,6 +2,7 @@
#pragma once
#include <stdint.h>
#include <tactility/drivers/rtc.h>
#include <tactility/error.h>
struct Device;
@@ -15,30 +16,21 @@ struct Rx8130ceConfig {
uint8_t address;
};
struct Rx8130ceDateTime {
uint16_t year; // 20002099
uint8_t month; // 112
uint8_t day; // 131
uint8_t hour; // 023
uint8_t minute; // 059
uint8_t second; // 059
};
/**
* Read the current date and time from the RTC.
* @param[in] device rx8130ce device
* @param[out] dt Pointer to Rx8130ceDateTime to populate
* @param[out] dt Pointer to RtcDateTime to populate
* @return ERROR_NONE on success, ERROR_INVALID_STATE if VLF is set (clock data unreliable)
*/
error_t rx8130ce_get_datetime(struct Device* device, struct Rx8130ceDateTime* dt);
error_t rx8130ce_get_datetime(struct Device* device, struct RtcDateTime* dt);
/**
* Write the date and time to the RTC.
* @param[in] device rx8130ce device
* @param[in] dt Pointer to Rx8130ceDateTime to write (year must be 20002099)
* @param[in] dt Pointer to RtcDateTime to write (year must be 20002099)
* @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
}
+1 -3
View File
@@ -21,13 +21,11 @@ static error_t stop() {
return ERROR_NONE;
}
extern const ModuleSymbol rx8130ce_module_symbols[];
Module rx8130ce_module = {
.name = "rx8130ce",
.start = start,
.stop = stop,
.symbols = rx8130ce_module_symbols,
.symbols = nullptr,
.internal = nullptr
};
+9 -4
View File
@@ -78,7 +78,7 @@ static error_t stop(Device* device) {
extern "C" {
error_t rx8130ce_get_datetime(Device* device, Rx8130ceDateTime* dt) {
error_t rx8130ce_get_datetime(Device* device, RtcDateTime* dt) {
auto* i2c_controller = device_get_parent(device);
auto address = GET_CONFIG(device)->address;
@@ -107,7 +107,7 @@ error_t rx8130ce_get_datetime(Device* device, Rx8130ceDateTime* dt) {
return ERROR_NONE;
}
error_t rx8130ce_set_datetime(Device* device, const Rx8130ceDateTime* dt) {
error_t rx8130ce_set_datetime(Device* device, const RtcDateTime* dt) {
if (dt->month < 1 || dt->month > 12 ||
dt->day < 1 || dt->day > 31 ||
dt->hour > 23 || dt->minute > 59 || dt->second > 59) {
@@ -151,13 +151,18 @@ error_t rx8130ce_set_datetime(Device* device, const Rx8130ceDateTime* dt) {
return error;
}
RtcApi rx8130ce_rtc_api = {
.get_time = rx8130ce_get_datetime,
.set_time = rx8130ce_set_datetime
};
Driver rx8130ce_driver = {
.name = "rx8130ce",
.compatible = (const char*[]) { "epson,rx8130ce", nullptr },
.start_device = start,
.stop_device = stop,
.api = nullptr,
.device_type = nullptr,
.api = &rx8130ce_rtc_api,
.device_type = &RTC_TYPE,
.owner = &rx8130ce_module,
.internal = nullptr
};
-9
View File
@@ -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
};
+11
View File
@@ -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
)
+195
View File
@@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### 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.
+9
View File
@@ -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"
+3
View File
@@ -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
+28
View File
@@ -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"
+562
View File
@@ -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"