diff --git a/components/peripheral/include/maix_light.hpp b/components/peripheral/include/maix_light.hpp new file mode 100644 index 00000000..6de95223 --- /dev/null +++ b/components/peripheral/include/maix_light.hpp @@ -0,0 +1,102 @@ +/** + * @author Sipeed + * @copyright Sipeed Ltd 2026- + * @license Apache 2.0 + * @update 2026.11.5: create this file. + */ + +#pragma once + +#include +#include "maix_basic.hpp" + +namespace maix::peripheral::light +{ + /** + * @brief Fill light(补光灯) control class. + * + * The light pin is read from the board config file(/boot/board or /boot/boards/board.) + * item "cam_light_io", default is "B25" on MaixCAM2 and "B3" on MaixCAM. + * + * On MaixCAM2 the light is driven by PWM so its brightness can be adjusted, + * the PWM frequency and duty are read from the system config file(/boot/configs): + * maix_light_freq: PWM frequency, unit Hz, default 10000 + * maix_light_duty: PWM duty, unit %, range [0, 100], default 50 + * On MaixCAM the light is driven by GPIO, only on/off is supported. + * + * @maixpy maix.peripheral.light.Light + */ + class Light + { + public: + /** + * @brief Fill light constructor, will open the light hardware and turn it off by default. + * @throw err::Exception if open the light hardware failed. + * @maixpy maix.peripheral.light.Light.__init__ + */ + Light(); + ~Light(); + + /** + * @brief Turn on the fill light. + * On MaixCAM2 the PWM duty will be set to the configured duty(maix_light_duty), + * on MaixCAM the GPIO will be set to high level. + * @return error code, err::ERR_NONE means success. + * @maixpy maix.peripheral.light.Light.on + */ + err::Err on(); + + /** + * @brief Turn off the fill light. + * @return error code, err::ERR_NONE means success. + * @maixpy maix.peripheral.light.Light.off + */ + err::Err off(); + + /** + * @brief Get the fill light state. + * @return true if the light is on, else false. + * @maixpy maix.peripheral.light.Light.is_on + */ + bool is_on(); + + /** + * @brief Set the fill light brightness(PWM duty), only take effect at runtime, + * will not modify the config file. + * + * On MaixCAM only 0(off) and non-zero(on) are supported. + * + * @note If duty > 70, a warning will be printed, because long time high power + * running may cause the LED to age quickly. + * + * @param[in] duty PWM duty, unit %, range [0, 100]. 0 means turn off the light. + * @return error code, err::ERR_NONE means success. + * @maixpy maix.peripheral.light.Light.set_duty + */ + err::Err set_duty(double duty); + + /** + * @brief Get the current fill light brightness(PWM duty). + * @return PWM duty, unit %, range [0, 100]. + * @maixpy maix.peripheral.light.Light.duty + */ + double duty(); + + /** + * @brief Get the fill light pin name. + * @return pin name, e.g. "B25". + * @maixpy maix.peripheral.light.Light.pin + */ + std::string pin(); + + /** + * @brief Get the fill light PWM frequency. + * @return PWM frequency, unit Hz. Return 0 if the light is driven by GPIO. + * @maixpy maix.peripheral.light.Light.freq + */ + int freq(); + + private: + void *_priv; + }; +} // namespace maix::peripheral::light diff --git a/components/peripheral/include/maix_peripheral.hpp b/components/peripheral/include/maix_peripheral.hpp index 88d75213..b58e8125 100755 --- a/components/peripheral/include/maix_peripheral.hpp +++ b/components/peripheral/include/maix_peripheral.hpp @@ -1,5 +1,6 @@ #include "maix_adc.hpp" #include "maix_gpio.hpp" +#include "maix_light.hpp" #include "maix_i2c.hpp" #include "maix_pwm.hpp" #include "maix_spi.hpp" diff --git a/components/peripheral/port/linux_common/maix_light.cpp b/components/peripheral/port/linux_common/maix_light.cpp new file mode 100644 index 00000000..986b4afe --- /dev/null +++ b/components/peripheral/port/linux_common/maix_light.cpp @@ -0,0 +1,317 @@ +/** + * @author Sipeed + * @copyright Sipeed Ltd 2026- + * @license Apache 2.0 + * @update 2026.11.5: create this file. + */ + +#include "maix_light.hpp" +#include "maix_log.hpp" +#include "maix_sys.hpp" +#include "maix_app.hpp" +#include "maix_pinmap.hpp" +#include "maix_gpio.hpp" +#include "maix_pwm.hpp" +#include +#include +#include + +#if defined(PLATFORM_MAIXCAM2) +#define LIGHT_DEFAULT_PIN "B25" +#else +#define LIGHT_DEFAULT_PIN "B3" +#endif + +namespace maix::peripheral::light +{ + static std::string _str_trim(const std::string &s) + { + size_t start = s.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) + return ""; + size_t end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); + } + + static std::string _get_light_pin() + { + std::string pin = LIGHT_DEFAULT_PIN; + try + { + auto configs = sys::device_configs(); + auto it = configs.find("cam_light_io"); + if (it != configs.end()) + { + std::string value = _str_trim(it->second); + if (!value.empty()) + pin = value; + } + } + catch (const std::exception &e) + { + log::warn("read device configs failed: %s", e.what()); + } + return pin; + } + +#if defined(PLATFORM_MAIXCAM2) +#define LIGHT_DUTY_WARN_THRESHOLD 70.0 + + static void _warn_high_duty(double duty) + { + log::warn("light duty %.0f%% is greater than %.0f%%, long time high power running may cause the LED to age quickly", + duty, LIGHT_DUTY_WARN_THRESHOLD); + } +#endif + + struct LightPriv + { + bool is_on; + std::string pin; + int freq; // PWM frequency, 0 if driven by GPIO + double duty; // configured duty from /boot/configs, used when on() called + double cur_duty; +#if defined(PLATFORM_MAIXCAM2) + pwm::PWM *pwm; +#endif + gpio::GPIO *gpio; + + LightPriv() + : is_on(false) + , freq(0) + , duty(0) + , cur_duty(0) +#if defined(PLATFORM_MAIXCAM2) + , pwm(nullptr) +#endif + , gpio(nullptr) + { + } + }; + +#if defined(PLATFORM_MAIXCAM2) + static int _get_pin_pwm_id(const std::string &pin, int &pwm_id) + { + try + { + std::vector funcs = pinmap::get_pin_functions(pin); + for (auto &func : funcs) + { + if (func.rfind("PWM", 0) == 0) + { + pwm_id = atoi(func.c_str() + 3); + return 0; + } + } + } + catch (const std::exception &e) + { + log::warn("get pin %s functions failed: %s", pin.c_str(), e.what()); + } + return -1; + } +#endif + + Light::Light() + { + LightPriv *priv = new LightPriv(); + _priv = priv; + + priv->pin = _get_light_pin(); + bool use_pwm = false; + +#if defined(PLATFORM_MAIXCAM2) + { + std::string freq_str = app::get_sys_config_kv("light", "freq", "10000"); + std::string duty_str = app::get_sys_config_kv("light", "duty", "50"); + priv->freq = atoi(freq_str.c_str()); + if (priv->freq <= 0) + priv->freq = 10000; + priv->duty = atof(duty_str.c_str()); + if (priv->duty < 0) + priv->duty = 0; + if (priv->duty > 100) + priv->duty = 100; + if (priv->duty > LIGHT_DUTY_WARN_THRESHOLD) + _warn_high_duty(priv->duty); + + int pwm_id = -1; + if (_get_pin_pwm_id(priv->pin, pwm_id) == 0) + { + err::check_raise(pinmap::set_pin_function(priv->pin, "PWM" + std::to_string(pwm_id)), "set light pin function failed"); + try + { + priv->pwm = new pwm::PWM(pwm_id, priv->freq, 0); + use_pwm = true; + } + catch (const std::exception &e) + { + log::warn("open light pwm failed: %s, fallback to gpio", e.what()); + priv->pwm = nullptr; + } + } + else + { + log::warn("pin %s has no PWM function, fallback to gpio", priv->pin.c_str()); + } + } +#endif + + if (!use_pwm) + { + priv->freq = 0; + err::check_raise(pinmap::set_pin_function(priv->pin, "GPIO" + priv->pin), "set light pin function failed"); + priv->gpio = new gpio::GPIO(priv->pin, gpio::Mode::OUT); + err::check_null_raise(priv->gpio, "open light gpio failed"); + priv->gpio->low(); + } + priv->is_on = false; + priv->cur_duty = 0; + log::info("light pin: %s, freq: %d, duty: %.1f", priv->pin.c_str(), priv->freq, priv->duty); + } + + Light::~Light() + { + LightPriv *priv = (LightPriv *)_priv; + if (!priv) + return; +#if defined(PLATFORM_MAIXCAM2) + if (priv->pwm) + { + priv->pwm->duty(0); + delete priv->pwm; + priv->pwm = nullptr; + } +#endif + if (priv->gpio) + { + priv->gpio->low(); + delete priv->gpio; + priv->gpio = nullptr; + } + delete priv; + _priv = nullptr; + } + + err::Err Light::on() + { + LightPriv *priv = (LightPriv *)_priv; + if (!priv) + return err::ERR_RUNTIME; +#if defined(PLATFORM_MAIXCAM2) + if (priv->pwm) + { + if (priv->pwm->duty(priv->duty) < 0) + return err::ERR_IO; + priv->cur_duty = priv->duty; + priv->is_on = true; + return err::ERR_NONE; + } +#endif + if (priv->gpio) + { + priv->gpio->high(); + priv->cur_duty = 100; + priv->is_on = true; + return err::ERR_NONE; + } + return err::ERR_RUNTIME; + } + + err::Err Light::off() + { + LightPriv *priv = (LightPriv *)_priv; + if (!priv) + return err::ERR_RUNTIME; +#if defined(PLATFORM_MAIXCAM2) + if (priv->pwm) + { + if (priv->pwm->duty(0) < 0) + return err::ERR_IO; + priv->cur_duty = 0; + priv->is_on = false; + return err::ERR_NONE; + } +#endif + if (priv->gpio) + { + priv->gpio->low(); + priv->cur_duty = 0; + priv->is_on = false; + return err::ERR_NONE; + } + return err::ERR_RUNTIME; + } + + bool Light::is_on() + { + LightPriv *priv = (LightPriv *)_priv; + if (!priv) + return false; + return priv->is_on; + } + + err::Err Light::set_duty(double duty) + { + LightPriv *priv = (LightPriv *)_priv; + if (!priv) + return err::ERR_RUNTIME; + if (duty < 0) + duty = 0; + if (duty > 100) + duty = 100; +#if defined(PLATFORM_MAIXCAM2) + if (priv->pwm) + { + if (duty > LIGHT_DUTY_WARN_THRESHOLD && priv->cur_duty <= LIGHT_DUTY_WARN_THRESHOLD) + _warn_high_duty(duty); + if (priv->pwm->duty(duty) < 0) + return err::ERR_IO; + priv->cur_duty = duty; + priv->is_on = duty > 0; + return err::ERR_NONE; + } +#endif + if (priv->gpio) + { + if (duty > 0) + { + priv->gpio->high(); + priv->cur_duty = 100; + priv->is_on = true; + } + else + { + priv->gpio->low(); + priv->cur_duty = 0; + priv->is_on = false; + } + return err::ERR_NONE; + } + return err::ERR_RUNTIME; + } + + double Light::duty() + { + LightPriv *priv = (LightPriv *)_priv; + if (!priv) + return 0; + return priv->cur_duty; + } + + std::string Light::pin() + { + LightPriv *priv = (LightPriv *)_priv; + if (!priv) + return ""; + return priv->pin; + } + + int Light::freq() + { + LightPriv *priv = (LightPriv *)_priv; + if (!priv) + return 0; + return priv->freq; + } +} // namespace maix::peripheral::light diff --git a/projects/app_camera/README.md b/projects/app_camera/README.md index 50379d27..833025e6 100644 --- a/projects/app_camera/README.md +++ b/projects/app_camera/README.md @@ -46,14 +46,14 @@ 2. 码率修改:仅在非录制状态下可修改码率,录制过程中修改码率会提示「视频忙」且不生效; 3. 延时摄影:开启延时摄影后,音频会自动关闭,关闭延时摄影后需手动重新开启音频; 4. RAW格式:开启RAW格式后,照片保存体积会增大,且需要专用软件解析; -5. 补光灯:补光灯的硬件引脚默认配置为GPIOB3,可通过设备配置文件修改引脚映射; +5. 补光灯:补光灯由 `maix::peripheral::light::Light` 控制,引脚可通过设备配置文件中的「cam_light_io」修改(MaixCam 默认 B3,MaixCam2 默认 B25);MaixCam2 使用 PWM 调光,频率和占空比由 `/boot/configs` 中的 `maix_light_freq`(默认 10000)和 `maix_light_duty`(默认 50)决定,MaixCam 使用 GPIO 仅支持开关; 6. 存储路径:照片/视频默认按日期分目录存储,需确保存储设备有足够空间,否则会保存失败; 7. 音视频同步:录制视频时,若出现音频卡顿,可检查音频采样率配置(默认48000Hz)。 ## 5. 更多介绍 ### 5.1 硬件适配 - 支持MaixCam、MaixCam2两款硬件平台,MaixCam2支持更高码率(最高100Mbps)和AI-ISP功能; -- 补光灯引脚可通过设备配置文件中的「cam_light_io」参数自定义,默认B3引脚; +- 补光灯由 `maix::peripheral::light::Light` 控制,引脚可通过设备配置文件中的「cam_light_io」参数自定义(MaixCam 默认 B3,MaixCam2 默认 B25);MaixCam2 通过 PWM 调光,频率/占空比由 `/boot/configs` 的 `maix_light_freq`/`maix_light_duty` 配置,MaixCam 通过 GPIO 控制; - 摄像头传感器最大分辨率决定可选分辨率档位,适配前需确认硬件传感器规格。 ### 5.2 存储说明 diff --git a/projects/app_camera/README_EN.md b/projects/app_camera/README_EN.md index bf98ef85..4b54fe49 100644 --- a/projects/app_camera/README_EN.md +++ b/projects/app_camera/README_EN.md @@ -52,7 +52,7 @@ This application is a camera control program developed based on Maix series hard 2. **Bitrate Modification:** Bitrate can only be modified when not recording. Attempting to change it during recording will show a "Video Busy" warning and will not take effect. 3. **Time-lapse Photography:** Audio is automatically disabled in time-lapse mode. You need to manually re-enable it if you switch back to normal recording. 4. **RAW Format:** Enabling RAW format increases file size significantly, and dedicated software is required for viewing/processing. -5. **Fill Light:** The default hardware pin for the light is GPIOB3. This can be modified via the device configuration file (`cam_light_io`). +5. **Fill Light:** The fill light is controlled by `maix::peripheral::light::Light`. The pin can be changed via `cam_light_io` in the device config file (default: B3 on MaixCam, B25 on MaixCam2). On MaixCam2 the light is driven by PWM, the frequency and duty are read from `/boot/configs` (`maix_light_freq`, default 10000; `maix_light_duty`, default 50). On MaixCam it is driven by GPIO and only on/off is supported. 6. **Storage Path:** Files are stored in date-based directories by default. Ensure sufficient storage space is available to prevent save failures. 7. **AV Sync:** If audio stutters during recording, check the audio sample rate configuration (default is 48000Hz). @@ -61,7 +61,7 @@ This application is a camera control program developed based on Maix series hard ### 5.1 Hardware Compatibility * Supports **MaixCam** and **MaixCam2** platforms. * **MaixCam2** supports higher bitrates (up to 100Mbps) and features AI-ISP. -* The fill light pin is configurable via the `cam_light_io` parameter in the device config file (default: B3). +* The fill light is controlled by `maix::peripheral::light::Light`. Its pin is configurable via `cam_light_io` (default: B3 on MaixCam, B25 on MaixCam2). On MaixCam2 it is driven by PWM with `maix_light_freq`/`maix_light_duty` from `/boot/configs`; on MaixCam it is driven by GPIO. * The available resolution options are determined by the camera sensor's maximum resolution. ### 5.2 Storage Details diff --git a/projects/app_camera/main/app/app.cpp b/projects/app_camera/main/app/app.cpp index 417ad455..4a109615 100644 --- a/projects/app_camera/main/app/app.cpp +++ b/projects/app_camera/main/app/app.cpp @@ -6,8 +6,7 @@ #include "maix_util.hpp" #include "maix_image.hpp" #include "maix_app.hpp" -#include "maix_pinmap.hpp" -#include "maix_gpio.hpp" +#include "maix_light.hpp" #include "maix_fs.hpp" #include "maix_vision.hpp" #include "maix_ffmpeg.hpp" @@ -67,7 +66,7 @@ static struct { display::Display *other_disp; touchscreen::TouchScreen *touchscreen; video::Encoder *encoder; - gpio::GPIO *light; + maix::peripheral::light::Light *light; ffmpeg::FFmpegPacker *ffmpeg_packer; audio::Recorder *audio_recorder; Region *region; @@ -347,22 +346,11 @@ int app_base_init(void) priv.touchscreen = new touchscreen::TouchScreen(); err::check_bool_raise(priv.touchscreen->is_opened(), "touchscreen open failed"); - // init light - auto device_configs = sys::device_configs(); - auto light_io = std::string("B3"); // default use GPIOB3 - auto it = device_configs.find("cam_light_io"); - if (it != device_configs.end()) { - auto new_io = it->second; - trim(new_io); - if (new_io.size() > 0) { - light_io = new_io; - } - } - log::info("light_io: %s", light_io.c_str()); - pinmap::set_pin_function(light_io, "GPIO" + light_io); - priv.light = new gpio::GPIO(light_io, gpio::Mode::OUT); - err::check_null_raise(priv.light, "light gpio open failed"); - priv.light->low(); + // init fill light + // MaixCAM2: PWM dimming, read frequency and duty from /boot/configs(maix_light_freq, maix_light_duty) + // MaixCAM : GPIO on/off + priv.light = new light::Light(); + priv.light->off(); // init audio priv.audio_recorder = new audio::Recorder(); @@ -410,7 +398,7 @@ int app_base_deinit(void) } if (priv.light) { - priv.light->low(); + priv.light->off(); delete priv.light; priv.light = NULL; } @@ -884,12 +872,12 @@ static int app_config_param(void) if (ui_get_light_btn_touched()) { log::info("light on"); if (priv.light) { - priv.light->high(); + priv.light->on(); } } else { log::info("light off"); if (priv.light) { - priv.light->low(); + priv.light->off(); } } }