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:
@@ -2,6 +2,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <tactility/drivers/rtc.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
struct Device;
|
||||
@@ -15,30 +16,21 @@ struct Bm8563Config {
|
||||
uint8_t address;
|
||||
};
|
||||
|
||||
struct Bm8563DateTime {
|
||||
uint16_t year; // 2000–2199
|
||||
uint8_t month; // 1–12
|
||||
uint8_t day; // 1–31
|
||||
uint8_t hour; // 0–23
|
||||
uint8_t minute; // 0–59
|
||||
uint8_t second; // 0–59
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the current date and time from the RTC.
|
||||
* @param[in] device bm8563 device
|
||||
* @param[out] dt Pointer to Bm8563DateTime to populate
|
||||
* @param[out] dt Pointer to RtcDateTime to populate
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t bm8563_get_datetime(struct Device* device, struct Bm8563DateTime* dt);
|
||||
error_t bm8563_get_datetime(struct Device* device, struct RtcDateTime* dt);
|
||||
|
||||
/**
|
||||
* Write the date and time to the RTC.
|
||||
* @param[in] device bm8563 device
|
||||
* @param[in] dt Pointer to Bm8563DateTime to write (year must be 2000–2199)
|
||||
* @param[in] dt Pointer to RtcDateTime to write (year must be 2000–2199)
|
||||
* @return ERROR_NONE on success, ERROR_INVALID_ARGUMENT if any field is out of range
|
||||
*/
|
||||
error_t bm8563_set_datetime(struct Device* device, const struct Bm8563DateTime* dt);
|
||||
error_t bm8563_set_datetime(struct Device* device, const struct RtcDateTime* dt);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ static error_t stop(Device* device) {
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t bm8563_get_datetime(Device* device, Bm8563DateTime* dt) {
|
||||
error_t bm8563_get_datetime(Device* device, RtcDateTime* dt) {
|
||||
auto* i2c_controller = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
|
||||
@@ -78,7 +78,7 @@ error_t bm8563_get_datetime(Device* device, Bm8563DateTime* dt) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t bm8563_set_datetime(Device* device, const Bm8563DateTime* dt) {
|
||||
error_t bm8563_set_datetime(Device* device, const RtcDateTime* dt) {
|
||||
if (dt->year < 2000 || dt->year > 2199 ||
|
||||
dt->month < 1 || dt->month > 12 ||
|
||||
dt->day < 1 || dt->day > 31 ||
|
||||
@@ -104,13 +104,18 @@ error_t bm8563_set_datetime(Device* device, const Bm8563DateTime* dt) {
|
||||
return i2c_controller_write_register(i2c_controller, address, REG_SECONDS, buf, sizeof(buf), I2C_TIMEOUT_TICKS);
|
||||
}
|
||||
|
||||
RtcApi bm8563_rtc_api = {
|
||||
.get_time = bm8563_get_datetime,
|
||||
.set_time = bm8563_set_datetime
|
||||
};
|
||||
|
||||
Driver bm8563_driver = {
|
||||
.name = "bm8563",
|
||||
.compatible = (const char*[]) { "belling,bm8563", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = nullptr,
|
||||
.device_type = nullptr,
|
||||
.api = &bm8563_rtc_api,
|
||||
.device_type = &RTC_TYPE,
|
||||
.owner = &bm8563_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
@@ -21,13 +21,11 @@ static error_t stop() {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
extern const ModuleSymbol bm8563_module_symbols[];
|
||||
|
||||
Module bm8563_module = {
|
||||
.name = "bm8563",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = bm8563_module_symbols,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/bm8563.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol bm8563_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(bm8563_get_datetime),
|
||||
DEFINE_MODULE_SYMBOL(bm8563_set_datetime),
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <tactility/drivers/rtc.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
struct Device;
|
||||
@@ -15,30 +16,21 @@ struct Rx8130ceConfig {
|
||||
uint8_t address;
|
||||
};
|
||||
|
||||
struct Rx8130ceDateTime {
|
||||
uint16_t year; // 2000–2099
|
||||
uint8_t month; // 1–12
|
||||
uint8_t day; // 1–31
|
||||
uint8_t hour; // 0–23
|
||||
uint8_t minute; // 0–59
|
||||
uint8_t second; // 0–59
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the current date and time from the RTC.
|
||||
* @param[in] device rx8130ce device
|
||||
* @param[out] dt Pointer to Rx8130ceDateTime to populate
|
||||
* @param[out] dt Pointer to RtcDateTime to populate
|
||||
* @return ERROR_NONE on success, ERROR_INVALID_STATE if VLF is set (clock data unreliable)
|
||||
*/
|
||||
error_t rx8130ce_get_datetime(struct Device* device, struct Rx8130ceDateTime* dt);
|
||||
error_t rx8130ce_get_datetime(struct Device* device, struct RtcDateTime* dt);
|
||||
|
||||
/**
|
||||
* Write the date and time to the RTC.
|
||||
* @param[in] device rx8130ce device
|
||||
* @param[in] dt Pointer to Rx8130ceDateTime to write (year must be 2000–2099)
|
||||
* @param[in] dt Pointer to RtcDateTime to write (year must be 2000–2099)
|
||||
* @return ERROR_NONE on success, ERROR_INVALID_ARGUMENT if any field is out of range
|
||||
*/
|
||||
error_t rx8130ce_set_datetime(struct Device* device, const struct Rx8130ceDateTime* dt);
|
||||
error_t rx8130ce_set_datetime(struct Device* device, const struct RtcDateTime* dt);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -21,13 +21,11 @@ static error_t stop() {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
extern const ModuleSymbol rx8130ce_module_symbols[];
|
||||
|
||||
Module rx8130ce_module = {
|
||||
.name = "rx8130ce",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = rx8130ce_module_symbols,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ static error_t stop(Device* device) {
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t rx8130ce_get_datetime(Device* device, Rx8130ceDateTime* dt) {
|
||||
error_t rx8130ce_get_datetime(Device* device, RtcDateTime* dt) {
|
||||
auto* i2c_controller = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
|
||||
@@ -107,7 +107,7 @@ error_t rx8130ce_get_datetime(Device* device, Rx8130ceDateTime* dt) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t rx8130ce_set_datetime(Device* device, const Rx8130ceDateTime* dt) {
|
||||
error_t rx8130ce_set_datetime(Device* device, const RtcDateTime* dt) {
|
||||
if (dt->month < 1 || dt->month > 12 ||
|
||||
dt->day < 1 || dt->day > 31 ||
|
||||
dt->hour > 23 || dt->minute > 59 || dt->second > 59) {
|
||||
@@ -151,13 +151,18 @@ error_t rx8130ce_set_datetime(Device* device, const Rx8130ceDateTime* dt) {
|
||||
return error;
|
||||
}
|
||||
|
||||
RtcApi rx8130ce_rtc_api = {
|
||||
.get_time = rx8130ce_get_datetime,
|
||||
.set_time = rx8130ce_set_datetime
|
||||
};
|
||||
|
||||
Driver rx8130ce_driver = {
|
||||
.name = "rx8130ce",
|
||||
.compatible = (const char*[]) { "epson,rx8130ce", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = nullptr,
|
||||
.device_type = nullptr,
|
||||
.api = &rx8130ce_rtc_api,
|
||||
.device_type = &RTC_TYPE,
|
||||
.owner = &rx8130ce_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/rx8130ce.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol rx8130ce_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(rx8130ce_get_datetime),
|
||||
DEFINE_MODULE_SYMBOL(rx8130ce_set_datetime),
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(sc2356-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel platform-esp32 esp_video esp_cam_sensor driver esp_driver_jpeg esp_driver_ppa
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# SC2356 MIPI CSI Camera Driver
|
||||
|
||||
A driver for the `SC2356` (also known as SC202CS) 2MP MIPI CSI camera sensor by Smartsens.
|
||||
|
||||
- Datasheet: https://www.smartsens.com/en/product/SC2356/
|
||||
- M5Stack Tab5 hardware: https://docs.m5stack.com/en/core/Tab5
|
||||
- ESP Video Components: https://github.com/espressif/esp-video-components
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,5 @@
|
||||
description: Smartsens SC2356 2MP MIPI CSI camera sensor
|
||||
|
||||
include: [ "i2c-device.yaml" ]
|
||||
|
||||
compatible: "smartsens,sc2356"
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/sc2356.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(sc2356, struct Sc2356Config)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <tactility/drivers/camera.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
struct Device;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct Sc2356Config {
|
||||
/** SCCB I2C address (0x36) */
|
||||
uint8_t address;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opaque camera handle returned by sc2356_open().
|
||||
* Not thread-safe: the capture flow (sc2356_get_frame/sc2356_capture_jpeg + sc2356_release_frame)
|
||||
* must be driven from a single consumer at a time. state->last_dqbuf_index and the shared PPA
|
||||
* output buffer are not guarded by a lock, so overlapping calls from multiple tasks on the same
|
||||
* handle will race.
|
||||
*/
|
||||
typedef CameraHandle Sc2356Handle;
|
||||
|
||||
/**
|
||||
* Initialize the esp_video subsystem and open the MIPI CSI video device.
|
||||
* Reuses the parent I2C controller's bus handle for SCCB communication.
|
||||
* Must be called before any other sc2356_* functions.
|
||||
* @param device the sc2356 device from the device tree
|
||||
* @param handle out: opaque handle to pass to subsequent calls
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t sc2356_open(struct Device* device, Sc2356Handle* handle);
|
||||
|
||||
/**
|
||||
* Stop streaming, unmap frame buffers, and release all resources.
|
||||
* @param handle handle returned by sc2356_open()
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t sc2356_close(Sc2356Handle handle);
|
||||
|
||||
/**
|
||||
* Dequeue one RGB565 frame. Blocks until a frame is available or the timeout expires.
|
||||
* The caller must call sc2356_release_frame() to return the buffer to the camera queue.
|
||||
* @param handle handle returned by sc2356_open()
|
||||
* @param buf out: pointer to the DMA-mapped RGB565 frame buffer
|
||||
* @param len out: byte length of buf (width * height * 2)
|
||||
* @param timeout_ms maximum time to wait in milliseconds
|
||||
* @param out_width optional out: width of buf at the moment it was produced, atomic with the frame data (may be NULL)
|
||||
* @param out_height optional out: height of buf at the moment it was produced, atomic with the frame data (may be NULL)
|
||||
* @return ERROR_NONE on success, ERROR_TIMEOUT if no frame arrived in time
|
||||
*/
|
||||
error_t sc2356_get_frame(Sc2356Handle handle, uint8_t** buf, size_t* len, uint32_t timeout_ms, uint32_t* out_width, uint32_t* out_height);
|
||||
|
||||
/**
|
||||
* Return the last dequeued frame buffer to the V4L2 capture queue.
|
||||
* Must be called after each successful sc2356_get_frame().
|
||||
* @param handle handle returned by sc2356_open()
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t sc2356_release_frame(Sc2356Handle handle);
|
||||
|
||||
/**
|
||||
* Return the frame width in pixels (1280 for the default 720p config).
|
||||
* @param handle handle returned by sc2356_open()
|
||||
*/
|
||||
uint32_t sc2356_get_width(Sc2356Handle handle);
|
||||
|
||||
/**
|
||||
* Return the frame height in pixels (720 for the default 720p config).
|
||||
* @param handle handle returned by sc2356_open()
|
||||
*/
|
||||
uint32_t sc2356_get_height(Sc2356Handle handle);
|
||||
|
||||
/**
|
||||
* Change the clockwise rotation applied to subsequent frames.
|
||||
* Can be called at any time while the handle is open, including during streaming.
|
||||
* @param handle handle returned by sc2356_open()
|
||||
* @param rotation new rotation
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t sc2356_set_rotation(Sc2356Handle handle, CameraRotation rotation);
|
||||
|
||||
/**
|
||||
* Capture one frame and JPEG-encode it using the hardware JPEG encoder.
|
||||
* Allocates output buffer in SPIRAM; caller must free it with heap_caps_free().
|
||||
* @param handle handle returned by sc2356_open()
|
||||
* @param out_buf out: pointer to JPEG-encoded data (caller must free)
|
||||
* @param out_len out: byte length of JPEG data
|
||||
* @param quality JPEG quality 1-100
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t sc2356_capture_jpeg(Sc2356Handle handle, uint8_t** out_buf, size_t* out_len, uint8_t quality);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module sc2356_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver sc2356_driver;
|
||||
|
||||
static error_t start() {
|
||||
check(driver_construct_add(&sc2356_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
check(driver_remove_destruct(&sc2356_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module sc2356_module = {
|
||||
.name = "sc2356",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,562 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/sc2356.h>
|
||||
#include <sc2356_module.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/esp32_i2c_master.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_video_device.h>
|
||||
#include <esp_video_init.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/videodev2.h>
|
||||
#include <sys/errno.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/poll.h>
|
||||
|
||||
#include <driver/jpeg_encode.h>
|
||||
#include <driver/ppa.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#define TAG "SC2356"
|
||||
|
||||
// SC2356 (SC202CS) chip ID registers (16-bit address, 8-bit value)
|
||||
// PID = 0xEB52: high byte 0xEB at register 0x3107, low byte 0x52 at 0x3108
|
||||
static constexpr uint8_t SC2356_REG_CHIP_ID_H[2] = { 0x31, 0x07 };
|
||||
static constexpr uint8_t SC2356_CHIP_ID_H = 0xEB;
|
||||
static constexpr uint8_t SC2356_REG_CHIP_ID_L[2] = { 0x31, 0x08 };
|
||||
static constexpr uint8_t SC2356_CHIP_ID_L = 0x52;
|
||||
|
||||
static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(20);
|
||||
static constexpr int VIDEO_BUFFER_COUNT = 2;
|
||||
|
||||
static bool s_video_initialized = false;
|
||||
static SemaphoreHandle_t s_video_init_mutex = nullptr;
|
||||
|
||||
struct Sc2356State : CameraHandleData {
|
||||
int fd;
|
||||
// Native sensor output dimensions (always 1280×720 from the sensor)
|
||||
uint32_t native_width;
|
||||
uint32_t native_height;
|
||||
// Post-rotation dimensions (swapped for 90/270)
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint8_t* buffers[VIDEO_BUFFER_COUNT];
|
||||
size_t buf_lengths[VIDEO_BUFFER_COUNT];
|
||||
int last_dqbuf_index;
|
||||
jpeg_encoder_handle_t enc;
|
||||
i2c_master_bus_handle_t sccb_bus;
|
||||
CameraRotation rotation;
|
||||
ppa_client_handle_t ppa;
|
||||
// PPA output buffer — fixed size, large enough for any rotation (native_width * native_height * 2)
|
||||
uint8_t* ppa_out_buf;
|
||||
size_t ppa_out_buf_size;
|
||||
SemaphoreHandle_t rotation_mutex; // protects rotation, width, height
|
||||
};
|
||||
|
||||
#define GET_CONFIG(device) (static_cast<const Sc2356Config*>((device)->config))
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* i2c = device_get_parent(device);
|
||||
if (device_get_type(i2c) != &I2C_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "Parent is not an I2C controller");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
|
||||
uint8_t chip_id_h = 0, chip_id_l = 0;
|
||||
error_t err = i2c_controller_write_read(i2c, address, SC2356_REG_CHIP_ID_H, sizeof(SC2356_REG_CHIP_ID_H), &chip_id_h, 1, I2C_TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
LOG_W(TAG, "WHO_AM_I read failed: %s — sensor may still be starting up", error_to_string(err));
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
err = i2c_controller_write_read(i2c, address, SC2356_REG_CHIP_ID_L, sizeof(SC2356_REG_CHIP_ID_L), &chip_id_l, 1, I2C_TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
LOG_W(TAG, "WHO_AM_I read (low) failed: %s", error_to_string(err));
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (chip_id_h != SC2356_CHIP_ID_H || chip_id_l != SC2356_CHIP_ID_L) {
|
||||
LOG_E(TAG, "WHO_AM_I mismatch: got 0x%02X%02X, expected 0x%02X%02X", chip_id_h, chip_id_l, SC2356_CHIP_ID_H, SC2356_CHIP_ID_L);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
LOG_I(TAG, "WHO_AM_I OK (0x%02X%02X)", chip_id_h, chip_id_l);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop([[maybe_unused]] Device* device) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static SemaphoreHandle_t get_video_init_mutex() {
|
||||
static portMUX_TYPE creation_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
portENTER_CRITICAL(&creation_lock);
|
||||
if (!s_video_init_mutex) {
|
||||
s_video_init_mutex = xSemaphoreCreateMutex();
|
||||
if (!s_video_init_mutex) {
|
||||
LOG_E(TAG, "Failed to create video init mutex");
|
||||
}
|
||||
}
|
||||
portEXIT_CRITICAL(&creation_lock);
|
||||
return s_video_init_mutex;
|
||||
}
|
||||
|
||||
static ppa_srm_rotation_angle_t to_ppa_rotation(CameraRotation rotation) {
|
||||
switch (rotation) {
|
||||
case CAMERA_ROTATION_90: return PPA_SRM_ROTATION_ANGLE_90;
|
||||
case CAMERA_ROTATION_180: return PPA_SRM_ROTATION_ANGLE_180;
|
||||
case CAMERA_ROTATION_270: return PPA_SRM_ROTATION_ANGLE_270;
|
||||
default: return PPA_SRM_ROTATION_ANGLE_0;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t sc2356_open(Device* device, Sc2356Handle* out_handle) {
|
||||
if (!device || !out_handle) return ERROR_INVALID_ARGUMENT;
|
||||
|
||||
auto* state = static_cast<Sc2356State*>(heap_caps_calloc(1, sizeof(Sc2356State), MALLOC_CAP_DEFAULT));
|
||||
if (!state) return ERROR_OUT_OF_MEMORY;
|
||||
state->device = device;
|
||||
state->fd = -1;
|
||||
state->last_dqbuf_index = -1;
|
||||
state->rotation_mutex = xSemaphoreCreateMutex();
|
||||
if (!state->rotation_mutex) {
|
||||
heap_caps_free(state);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
state->rotation = CAMERA_ROTATION_0;
|
||||
|
||||
// Retrieve bus handle from the parent I2C controller
|
||||
auto* i2c = device_get_parent(device);
|
||||
state->sccb_bus = esp32_i2c_master_get_bus_handle(i2c);
|
||||
if (!state->sccb_bus) {
|
||||
LOG_E(TAG, "Failed to get i2c_master bus handle from parent device");
|
||||
vSemaphoreDelete(state->rotation_mutex);
|
||||
heap_caps_free(state);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// esp_video_init only needs to run once across all sc2356_open calls
|
||||
{
|
||||
SemaphoreHandle_t init_mutex = get_video_init_mutex();
|
||||
if (!init_mutex) {
|
||||
vSemaphoreDelete(state->rotation_mutex);
|
||||
heap_caps_free(state);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
xSemaphoreTake(init_mutex, portMAX_DELAY);
|
||||
if (!s_video_initialized) {
|
||||
esp_video_init_csi_config_t csi_config = {};
|
||||
csi_config.sccb_config.init_sccb = false;
|
||||
csi_config.sccb_config.i2c_handle = state->sccb_bus;
|
||||
csi_config.sccb_config.freq = 400000;
|
||||
csi_config.reset_pin = static_cast<gpio_num_t>(-1);
|
||||
csi_config.pwdn_pin = static_cast<gpio_num_t>(-1);
|
||||
|
||||
esp_video_init_config_t cam_config = {};
|
||||
cam_config.csi = &csi_config;
|
||||
|
||||
esp_err_t esp_err = esp_video_init(&cam_config);
|
||||
if (esp_err != ESP_OK) {
|
||||
LOG_E(TAG, "esp_video_init failed: %s", esp_err_to_name(esp_err));
|
||||
xSemaphoreGive(init_mutex);
|
||||
vSemaphoreDelete(state->rotation_mutex);
|
||||
heap_caps_free(state);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
s_video_initialized = true;
|
||||
}
|
||||
xSemaphoreGive(init_mutex);
|
||||
}
|
||||
|
||||
// Open the CSI video device. The ISP pipeline controller (running in the background
|
||||
// after esp_video_init) applies AE/AWB/demosaicing automatically; pixel output
|
||||
// is still on /dev/video0, which delivers processed RGB565 frames.
|
||||
state->fd = open(ESP_VIDEO_MIPI_CSI_DEVICE_NAME, O_RDONLY);
|
||||
if (state->fd < 0) {
|
||||
LOG_E(TAG, "Failed to open %s", ESP_VIDEO_MIPI_CSI_DEVICE_NAME);
|
||||
vSemaphoreDelete(state->rotation_mutex);
|
||||
heap_caps_free(state);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Query current format to get sensor dimensions
|
||||
struct v4l2_format fmt = {};
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (ioctl(state->fd, VIDIOC_G_FMT, &fmt) != 0) {
|
||||
LOG_E(TAG, "VIDIOC_G_FMT failed");
|
||||
goto err_close;
|
||||
}
|
||||
|
||||
// Set RGB565 output format
|
||||
if (fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_RGB565) {
|
||||
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
|
||||
if (ioctl(state->fd, VIDIOC_S_FMT, &fmt) != 0) {
|
||||
LOG_E(TAG, "VIDIOC_S_FMT RGB565 failed");
|
||||
goto err_close;
|
||||
}
|
||||
// Re-read to get the actual negotiated dimensions
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (ioctl(state->fd, VIDIOC_G_FMT, &fmt) != 0) {
|
||||
LOG_E(TAG, "VIDIOC_G_FMT after set failed");
|
||||
goto err_close;
|
||||
}
|
||||
}
|
||||
|
||||
state->native_width = fmt.fmt.pix.width;
|
||||
state->native_height = fmt.fmt.pix.height;
|
||||
|
||||
// Post-rotation dimensions (swapped for 90/270)
|
||||
if (state->rotation == CAMERA_ROTATION_90 || state->rotation == CAMERA_ROTATION_270) {
|
||||
state->width = state->native_height;
|
||||
state->height = state->native_width;
|
||||
} else {
|
||||
state->width = state->native_width;
|
||||
state->height = state->native_height;
|
||||
}
|
||||
LOG_I(TAG, "Video format: %lux%lu native",
|
||||
(unsigned long)state->native_width, (unsigned long)state->native_height);
|
||||
|
||||
// PPA output buffer — fixed size (max of any rotation), cache-line aligned per PPA requirements
|
||||
state->ppa_out_buf_size = (size_t)state->native_width * state->native_height * 2;
|
||||
state->ppa_out_buf = static_cast<uint8_t*>(
|
||||
heap_caps_aligned_alloc(64, state->ppa_out_buf_size, MALLOC_CAP_SPIRAM));
|
||||
if (!state->ppa_out_buf) {
|
||||
LOG_E(TAG, "Failed to alloc PPA output buffer (%zu bytes)", state->ppa_out_buf_size);
|
||||
goto err_close;
|
||||
}
|
||||
|
||||
// PPA client for hardware-accelerated rotation
|
||||
{
|
||||
ppa_client_config_t ppa_cfg = {};
|
||||
ppa_cfg.oper_type = PPA_OPERATION_SRM;
|
||||
ppa_cfg.max_pending_trans_num = 1;
|
||||
if (ppa_register_client(&ppa_cfg, &state->ppa) != ESP_OK) {
|
||||
LOG_E(TAG, "ppa_register_client failed");
|
||||
goto err_close;
|
||||
}
|
||||
}
|
||||
|
||||
// Request mmap buffers
|
||||
{
|
||||
struct v4l2_requestbuffers req = {};
|
||||
req.count = VIDEO_BUFFER_COUNT;
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
req.memory = V4L2_MEMORY_MMAP;
|
||||
if (ioctl(state->fd, VIDIOC_REQBUFS, &req) != 0) {
|
||||
LOG_E(TAG, "VIDIOC_REQBUFS failed");
|
||||
goto err_close;
|
||||
}
|
||||
}
|
||||
|
||||
// Query, mmap, and queue each buffer
|
||||
for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) {
|
||||
struct v4l2_buffer buf = {};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = i;
|
||||
if (ioctl(state->fd, VIDIOC_QUERYBUF, &buf) != 0) {
|
||||
LOG_E(TAG, "VIDIOC_QUERYBUF[%d] failed", i);
|
||||
goto err_unmap;
|
||||
}
|
||||
|
||||
state->buf_lengths[i] = buf.length;
|
||||
state->buffers[i] = static_cast<uint8_t*>(
|
||||
mmap(nullptr, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, state->fd, buf.m.offset)
|
||||
);
|
||||
if (state->buffers[i] == MAP_FAILED) {
|
||||
state->buffers[i] = nullptr;
|
||||
LOG_E(TAG, "mmap[%d] failed", i);
|
||||
goto err_unmap;
|
||||
}
|
||||
|
||||
if (ioctl(state->fd, VIDIOC_QBUF, &buf) != 0) {
|
||||
LOG_E(TAG, "VIDIOC_QBUF[%d] failed", i);
|
||||
goto err_unmap;
|
||||
}
|
||||
}
|
||||
|
||||
// Start streaming
|
||||
{
|
||||
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (ioctl(state->fd, VIDIOC_STREAMON, &type) != 0) {
|
||||
LOG_E(TAG, "VIDIOC_STREAMON failed");
|
||||
goto err_unmap;
|
||||
}
|
||||
}
|
||||
|
||||
// Create hardware JPEG encoder once; reused across all sc2356_capture_jpeg() calls
|
||||
{
|
||||
jpeg_encode_engine_cfg_t eng_cfg = { .intr_priority = 0, .timeout_ms = 1000 };
|
||||
esp_err_t je = jpeg_new_encoder_engine(&eng_cfg, &state->enc);
|
||||
if (je != ESP_OK) {
|
||||
LOG_E(TAG, "jpeg_new_encoder_engine failed: %s", esp_err_to_name(je));
|
||||
goto err_unmap;
|
||||
}
|
||||
}
|
||||
|
||||
*out_handle = state;
|
||||
return ERROR_NONE;
|
||||
|
||||
err_unmap:
|
||||
{
|
||||
// Stop streaming before unmapping - correct V4L2 cleanup order, avoids DMA-into-
|
||||
// unmapped-buffer races. Harmless no-op if STREAMON never succeeded (e.g. it's the
|
||||
// failure that brought us here).
|
||||
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
ioctl(state->fd, VIDIOC_STREAMOFF, &type);
|
||||
}
|
||||
for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) {
|
||||
if (state->buffers[i]) munmap(state->buffers[i], state->buf_lengths[i]);
|
||||
}
|
||||
err_close:
|
||||
if (state->ppa) ppa_unregister_client(state->ppa);
|
||||
if (state->ppa_out_buf) heap_caps_free(state->ppa_out_buf);
|
||||
if (state->rotation_mutex) vSemaphoreDelete(state->rotation_mutex);
|
||||
close(state->fd);
|
||||
heap_caps_free(state);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t sc2356_close(Sc2356Handle handle) {
|
||||
if (!handle) return ERROR_INVALID_ARGUMENT;
|
||||
auto* state = static_cast<Sc2356State*>(handle);
|
||||
|
||||
if (state->enc) jpeg_del_encoder_engine(state->enc);
|
||||
if (state->ppa) ppa_unregister_client(state->ppa);
|
||||
|
||||
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
ioctl(state->fd, VIDIOC_STREAMOFF, &type);
|
||||
|
||||
for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) {
|
||||
if (state->buffers[i]) munmap(state->buffers[i], state->buf_lengths[i]);
|
||||
}
|
||||
|
||||
if (state->ppa_out_buf) heap_caps_free(state->ppa_out_buf);
|
||||
if (state->rotation_mutex) vSemaphoreDelete(state->rotation_mutex);
|
||||
close(state->fd);
|
||||
// Bus handle is owned by esp32_i2c_master driver - do not delete it
|
||||
heap_caps_free(state);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t sc2356_set_rotation(Sc2356Handle handle, CameraRotation rotation) {
|
||||
if (!handle) return ERROR_INVALID_ARGUMENT;
|
||||
auto* state = static_cast<Sc2356State*>(handle);
|
||||
|
||||
xSemaphoreTake(state->rotation_mutex, portMAX_DELAY);
|
||||
|
||||
if (state->rotation == rotation) {
|
||||
xSemaphoreGive(state->rotation_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
bool needs_swap = (rotation == CAMERA_ROTATION_90 || rotation == CAMERA_ROTATION_270);
|
||||
|
||||
state->rotation = rotation;
|
||||
if (needs_swap) {
|
||||
state->width = state->native_height;
|
||||
state->height = state->native_width;
|
||||
} else {
|
||||
state->width = state->native_width;
|
||||
state->height = state->native_height;
|
||||
}
|
||||
|
||||
xSemaphoreGive(state->rotation_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t sc2356_get_frame(Sc2356Handle handle, uint8_t** buf, size_t* len, uint32_t timeout_ms, uint32_t* out_width, uint32_t* out_height) {
|
||||
if (!handle || !buf || !len) return ERROR_INVALID_ARGUMENT;
|
||||
auto* state = static_cast<Sc2356State*>(handle);
|
||||
|
||||
// Use poll to enforce timeout
|
||||
struct pollfd pfd = {};
|
||||
pfd.fd = state->fd;
|
||||
pfd.events = POLLIN;
|
||||
int ret = poll(&pfd, 1, static_cast<int>(timeout_ms));
|
||||
if (ret == 0) return ERROR_TIMEOUT;
|
||||
if (ret < 0) {
|
||||
LOG_E(TAG, "poll failed: %d", errno);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
struct v4l2_buffer vbuf = {};
|
||||
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
vbuf.memory = V4L2_MEMORY_MMAP;
|
||||
if (ioctl(state->fd, VIDIOC_DQBUF, &vbuf) != 0) {
|
||||
LOG_E(TAG, "VIDIOC_DQBUF failed: %d", errno);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
state->last_dqbuf_index = static_cast<int>(vbuf.index);
|
||||
uint8_t* raw = state->buffers[vbuf.index];
|
||||
|
||||
xSemaphoreTake(state->rotation_mutex, portMAX_DELAY);
|
||||
|
||||
uint32_t frame_width = state->width;
|
||||
uint32_t frame_height = state->height;
|
||||
|
||||
ppa_srm_oper_config_t srm_cfg = {};
|
||||
srm_cfg.in.buffer = raw;
|
||||
srm_cfg.in.pic_w = srm_cfg.in.block_w = state->native_width;
|
||||
srm_cfg.in.pic_h = srm_cfg.in.block_h = state->native_height;
|
||||
srm_cfg.in.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
|
||||
srm_cfg.out.buffer = state->ppa_out_buf;
|
||||
srm_cfg.out.buffer_size = state->ppa_out_buf_size;
|
||||
srm_cfg.out.pic_w = frame_width;
|
||||
srm_cfg.out.pic_h = frame_height;
|
||||
srm_cfg.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
|
||||
srm_cfg.rotation_angle = to_ppa_rotation(state->rotation);
|
||||
srm_cfg.scale_x = srm_cfg.scale_y = 1.0f;
|
||||
srm_cfg.mode = PPA_TRANS_MODE_BLOCKING;
|
||||
|
||||
esp_err_t ppa_err = ppa_do_scale_rotate_mirror(state->ppa, &srm_cfg);
|
||||
xSemaphoreGive(state->rotation_mutex);
|
||||
|
||||
if (ppa_err != ESP_OK) {
|
||||
LOG_E(TAG, "ppa_do_scale_rotate_mirror failed: %s", esp_err_to_name(ppa_err));
|
||||
// Buffer was already dequeued (VIDIOC_DQBUF above) - the caller receives an error
|
||||
// and may never call sc2356_release_frame(), so return it to the queue here or
|
||||
// repeated PPA failures exhaust VIDEO_BUFFER_COUNT and poll() blocks forever.
|
||||
sc2356_release_frame(handle);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*buf = state->ppa_out_buf;
|
||||
*len = (size_t)frame_width * frame_height * 2;
|
||||
if (out_width != nullptr) {
|
||||
*out_width = frame_width;
|
||||
}
|
||||
if (out_height != nullptr) {
|
||||
*out_height = frame_height;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t sc2356_release_frame(Sc2356Handle handle) {
|
||||
if (!handle) return ERROR_INVALID_ARGUMENT;
|
||||
auto* state = static_cast<Sc2356State*>(handle);
|
||||
|
||||
if (state->last_dqbuf_index < 0) return ERROR_INVALID_STATE;
|
||||
|
||||
struct v4l2_buffer vbuf = {};
|
||||
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
vbuf.memory = V4L2_MEMORY_MMAP;
|
||||
vbuf.index = static_cast<uint32_t>(state->last_dqbuf_index);
|
||||
state->last_dqbuf_index = -1;
|
||||
|
||||
if (ioctl(state->fd, VIDIOC_QBUF, &vbuf) != 0) {
|
||||
LOG_E(TAG, "VIDIOC_QBUF failed: %d", errno);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
uint32_t sc2356_get_width(Sc2356Handle handle) {
|
||||
if (!handle) return 0;
|
||||
auto* state = static_cast<Sc2356State*>(handle);
|
||||
xSemaphoreTake(state->rotation_mutex, portMAX_DELAY);
|
||||
uint32_t width = state->width;
|
||||
xSemaphoreGive(state->rotation_mutex);
|
||||
return width;
|
||||
}
|
||||
|
||||
uint32_t sc2356_get_height(Sc2356Handle handle) {
|
||||
if (!handle) return 0;
|
||||
auto* state = static_cast<Sc2356State*>(handle);
|
||||
xSemaphoreTake(state->rotation_mutex, portMAX_DELAY);
|
||||
uint32_t height = state->height;
|
||||
xSemaphoreGive(state->rotation_mutex);
|
||||
return height;
|
||||
}
|
||||
|
||||
error_t sc2356_capture_jpeg(Sc2356Handle handle, uint8_t** out_buf, size_t* out_len, uint8_t quality) {
|
||||
if (!handle || !out_buf || !out_len) return ERROR_INVALID_ARGUMENT;
|
||||
auto* state = static_cast<Sc2356State*>(handle);
|
||||
|
||||
// Dequeue one frame. Dimensions come back atomically with the frame data itself,
|
||||
// so they can't drift if a concurrent sc2356_set_rotation() changes state->width/height.
|
||||
uint8_t* frame = nullptr;
|
||||
size_t frame_len = 0;
|
||||
uint32_t capture_width = 0;
|
||||
uint32_t capture_height = 0;
|
||||
error_t err = sc2356_get_frame(handle, &frame, &frame_len, 500, &capture_width, &capture_height);
|
||||
if (err != ERROR_NONE) return err;
|
||||
|
||||
// Output buffer must be allocated with jpeg_alloc_encoder_mem for DMA alignment.
|
||||
// jpeg_alloc_encoder_mem() allocates exactly the requested size with no headroom, so the
|
||||
// hint must cover the worst case, not the typical case. At high quality settings (low
|
||||
// compression) on detailed/noisy images, a width*height (1 byte/pixel) hint can be smaller
|
||||
// than the actual encoded output, causing the DMA write to overrun the buffer - this was
|
||||
// observed as JPEG corruption confined to the tail of the image. width*height*2 matches
|
||||
// the uncompressed RGB565 source size, which JPEG output can never exceed.
|
||||
size_t out_sz = 0;
|
||||
jpeg_encode_memory_alloc_cfg_t mem_cfg = { .buffer_direction = JPEG_ENC_ALLOC_OUTPUT_BUFFER };
|
||||
uint8_t* jpeg_buf = static_cast<uint8_t*>(
|
||||
jpeg_alloc_encoder_mem(capture_width * capture_height * 2, &mem_cfg, &out_sz));
|
||||
if (!jpeg_buf) {
|
||||
sc2356_release_frame(handle);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
jpeg_encode_cfg_t enc_cfg = {
|
||||
.height = capture_height,
|
||||
.width = capture_width,
|
||||
.src_type = JPEG_ENCODE_IN_FORMAT_RGB565,
|
||||
.sub_sample = JPEG_DOWN_SAMPLING_YUV420,
|
||||
.image_quality = quality,
|
||||
};
|
||||
uint32_t encoded_size = 0;
|
||||
esp_err_t esp_err = jpeg_encoder_process(state->enc, &enc_cfg, frame,
|
||||
static_cast<uint32_t>(frame_len),
|
||||
jpeg_buf, static_cast<uint32_t>(out_sz),
|
||||
&encoded_size);
|
||||
sc2356_release_frame(handle);
|
||||
|
||||
if (esp_err != ESP_OK || encoded_size == 0) {
|
||||
LOG_E(TAG, "jpeg_encoder_process failed: %s", esp_err_to_name(esp_err));
|
||||
heap_caps_free(jpeg_buf);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*out_buf = jpeg_buf;
|
||||
*out_len = static_cast<size_t>(encoded_size);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
CameraApi sc2356_camera_api = {
|
||||
.open = sc2356_open,
|
||||
.close = sc2356_close,
|
||||
.get_frame = sc2356_get_frame,
|
||||
.release_frame = sc2356_release_frame,
|
||||
.get_width = sc2356_get_width,
|
||||
.get_height = sc2356_get_height,
|
||||
.set_rotation = sc2356_set_rotation,
|
||||
.capture_jpeg = sc2356_capture_jpeg
|
||||
};
|
||||
|
||||
Driver sc2356_driver = {
|
||||
.name = "sc2356",
|
||||
.compatible = (const char*[]) { "smartsens,sc2356", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &sc2356_camera_api,
|
||||
.device_type = &CAMERA_TYPE,
|
||||
.owner = &sc2356_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
Reference in New Issue
Block a user