Merge pull request #9 from PhosphorosVR/main

LED Control, USB Serial handover before starting UVC, FPS Limiting, default UVC mode for facefocus CLI Enhancements and bugfixes
This commit is contained in:
Lorow
2025-08-26 22:02:32 +02:00
committed by GitHub
33 changed files with 945 additions and 374 deletions

162
README.md
View File

@@ -1,69 +1,123 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
| Supported Targets | ESP32-S3 |
| ----------------- | -------- |
# Blink Example
## OpenIris-ESPIDF
(See the README.md file in the upper level 'examples' directory for more information about examples.)
Firmware and tools for OpenIris — WiFi, UVC streaming, and a Python setup CLI.
This example demonstrates how to blink a LED by using the GPIO driver or using the [led_strip](https://components.espressif.com/component/espressif/led_strip) library if the LED is addressable e.g. [WS2812](https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf). The `led_strip` library is installed via [component manager](main/idf_component.yml).
---
## How to Use Example
## Whats inside
- ESPIDF firmware (C/C++) with modules for Camera, WiFi, UVC, REST/Serial commands, and more
- Python tools for setup over USB serial:
- `tools/switchBoardType.py` — choose a board profile (builds the right sdkconfig)
- `tools/openiris_setup.py` — interactive CLI for WiFi, MDNS/Name, Mode, LED PWM, Logs, and a Settings Summary
Before project configuration and build, be sure to set the correct chip target using `idf.py set-target <chip_name>`.
---
### Hardware Required
## First-time setup on Windows (VS Code + ESPIDF extension)
If youre starting fresh on Windows, this workflow is smooth and reliable:
* A development board with normal LED or addressable LED on-board (e.g., ESP32-S3-DevKitC, ESP32-C6-DevKitC etc.)
* A USB cable for Power supply and programming
1) Install tooling
- Git: https://git-scm.com/downloads/win
- Visual Studio Code: https://code.visualstudio.com/
See [Development Boards](https://www.espressif.com/en/products/devkits) for more information about it.
### Configure the Project
Open the project configuration menu (`idf.py menuconfig`).
In the `Example Configuration` menu:
* Select the LED type in the `Blink LED type` option.
* Use `GPIO` for regular LED
* Use `LED strip` for addressable LED
* If the LED type is `LED strip`, select the backend peripheral
* `RMT` is only available for ESP targets with RMT peripheral supported
* `SPI` is available for all ESP targets
* Set the GPIO number used for the signal in the `Blink GPIO number` option.
* Set the blinking period in the `Blink period in ms` option.
### Build and Flash
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects.
## Example Output
As you run the example, you will see the LED blinking, according to the previously defined period. For the addressable LED, you can also change the LED color by setting the `led_strip_set_pixel(led_strip, 0, 16, 16, 16);` (LED Strip, Pixel Number, Red, Green, Blue) with values from 0 to 255 in the [source file](main/blink_example_main.c).
```text
I (315) example: Example configured to blink addressable LED!
I (325) example: Turning the LED OFF!
I (1325) example: Turning the LED ON!
I (2325) example: Turning the LED OFF!
I (3325) example: Turning the LED ON!
I (4325) example: Turning the LED OFF!
I (5325) example: Turning the LED ON!
I (6325) example: Turning the LED OFF!
I (7325) example: Turning the LED ON!
I (8325) example: Turning the LED OFF!
2) Get the source code
- Create a folder where you want the repo (e.g., `D:\OpenIris-ESPIDF\`). In File Explorer, rightclick the folder and choose “Open in Terminal”.
- Clone and open in VS Code:
```cmd
git clone https://github.com/lorow/OpenIris-ESPIDF.git
cd OpenIris-ESPIDF
code .
```
Note: The color order could be different according to the LED model.
3) Install the ESPIDF VS Code extension
- In VS Code, open the Extensions tab and install: https://marketplace.visualstudio.com/items?itemName=espressif.esp-idf-extension
The pixel number indicates the pixel position in the LED strip. For a single LED, use 0.
4) Set the default terminal profile to Command Prompt
- Press Ctrl+Shift+P → search “Terminal: Select Default Profile” → choose “Command Prompt”.
- Restart VS Code from its normal shortcut (not from Git Bash). This avoids running ESPIDF in the wrong shell.
5) Configure ESPIDF in the extension
- On first launch, the extension may prompt to install ESPIDF and tools — follow the steps. It can take a while.
- If you see the extensions home page instead, click “Configure extension”, pick “EXPRESS”, choose “GitHub” as the server and version “v5.4.2”.
- Then open the ESPIDF Explorer tab and click “Open ESPIDF Terminal”. Well use that for builds.
After this, youre ready for the Quick start below.
---
## Quick start
### 1) Pick your board (loads the default configuration)
Windows (cmd):
```cmd
python .\tools\switchBoardType.py --board xiao-esp32s3 --diff
```
macOS/Linux (bash):
```bash
python3 ./tools/switchBoardType.py --board xiao-esp32s3 --diff
```
- Set `--board` to your target board
- `--diff` shows what changed in the config
### 2) Build & flash
- Set the target (e.g., ESP32S3).
- Build, flash, and open the serial monitor.
### 3) Use the Python setup CLI (recommended)
Configure the device over USB serial.
Before you run it:
- If you still have the serial monitor open, close it (the port must be free).
- In VS Code, open the sidebar “ESPIDF: Explorer” and click “Open ESPIDF Terminal”. Well run the CLI there so Python and packages are in the right environment.
Then run:
```cmd
python .\tools\openiris_setup.py --port COMxx
```
Examples:
- Windows: `python .\tools\openiris_setup.py --port COM69`, …
- macOS: idk
- Linux: idk
What the CLI can do:
- WiFi menu: automatic (scan → pick → password → connect → wait for IP) or manual (scan, show, configure, connect, status)
- Set MDNS/Device name (also used for the UVC device name)
- Switch mode (WiFi / UVC / Auto)
- Adjust LED PWM
- Show a Settings Summary (MAC, WiFi status, mode, PWM, …)
- View logs
---
## Serial number & MAC
- Internally, the serial number is derived from the WiFi MAC address.
- The CLI displays the MAC by default (clearer); its the value used as the serial number.
- The UVC device name is based on the MDNS hostname.
---
## Common workflows
- Fast WiFi setup: in the CLI, go to “WiFi settings” → “Automatic setup”, then check “status”.
- Change name/MDNS: set the device name in the CLI, then replug USB — UVC will show the new name.
- Adjust brightness/LED: set LED PWM in the CLI.
---
## Project layout (short)
- `main/` — entry point
- `components/` — modules (Camera, WiFi, UVC, CommandManager, …)
- `tools/` — Python helper tools (board switch, setup CLI, scanner)
If you want to dig deeper: commands are mapped via the `CommandManager` under `components/CommandManager/...`.
---
## Troubleshooting
- UVC doesnt appear on the host?
- Switch mode to UVC via CLI tool, replug USB and wait 20s.
* If the LED isn't blinking, check the GPIO or the LED type selection in the `Example Configuration` menu.
---
For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.
Feedback, issues, and PRs are welcome.

View File

@@ -1,6 +1,6 @@
// source: https://github.com/espressif/esp-iot-solution/blob/4730d91db70df7e6e0a3191d725ab1c5f98ff9ce/examples/usb/device/usb_webcam/bootloader_components/boot_hooks/boot_hooks.c
#ifdef CONFIG_GENERAL_WIRED_MODE
#ifdef CONFIG_GENERAL_DEFAULT_WIRED_MODE
#include "esp_log.h"
#include "soc/rtc_cntl_struct.h"
#include "soc/usb_serial_jtag_reg.h"

View File

@@ -48,7 +48,7 @@ void CameraManager::setupCameraPinout()
ESP_LOGI(CAMERA_MANAGER_TAG, "CAM_BOARD");
#endif
#if CONFIG_GENERAL_WIRED_MODE
#if CONFIG_GENERAL_DEFAULT_WIRED_MODE
xclk_freq_hz = CONFIG_CAMERA_USB_XCLK_FREQ;
#endif
@@ -82,7 +82,7 @@ void CameraManager::setupCameraPinout()
.jpeg_quality = 7, // 0-63, for OV series camera sensors, lower number means higher quality // Below 6 stability problems
.fb_count = 2, // When jpeg mode is used, if fb_count more than one, the driver will work in continuous mode.
.fb_location = CAMERA_FB_IN_DRAM,
.grab_mode = CAMERA_GRAB_WHEN_EMPTY,
.grab_mode = CAMERA_GRAB_LATEST, //CAMERA_GRAB_WHEN_EMPTY
};
}
@@ -99,7 +99,7 @@ void CameraManager::setupBasicResolution()
return;
}
ESP_LOGE(CAMERA_MANAGER_TAG, "PSRAM size: %u", esp_psram_get_size());
ESP_LOGI(CAMERA_MANAGER_TAG, "PSRAM size: %u", esp_psram_get_size());
}
void CameraManager::setupCameraSensor()
@@ -196,7 +196,7 @@ bool CameraManager::setupCamera()
return false;
}
#if CONFIG_GENERAL_WIRED_MODE
#if CONFIG_GENERAL_DEFAULT_WIRED_MODE
const auto temp_sensor = esp_camera_sensor_get();
// Thanks to lick_it, we discovered that OV5640 likes to overheat when

View File

@@ -11,5 +11,5 @@ idf_component_register(
INCLUDE_DIRS
"CommandManager"
"CommandManager/commands"
REQUIRES ProjectConfig cJSON CameraManager OpenIrisTasks wifiManager Helpers
REQUIRES ProjectConfig cJSON CameraManager OpenIrisTasks wifiManager Helpers LEDManager
)

View File

@@ -23,53 +23,84 @@ std::unordered_map<std::string, CommandType> commandTypeMap = {
{"connect_wifi", CommandType::CONNECT_WIFI},
{"switch_mode", CommandType::SWITCH_MODE},
{"get_device_mode", CommandType::GET_DEVICE_MODE},
{"set_led_duty_cycle", CommandType::SET_LED_DUTY_CYCLE},
{"get_led_duty_cycle", CommandType::GET_LED_DUTY_CYCLE},
{"get_serial", CommandType::GET_SERIAL},
};
std::function<CommandResult()> CommandManager::createCommand(const CommandType type, std::string_view json) const {
std::function<CommandResult()> CommandManager::createCommand(const CommandType type, std::string_view json) const
{
switch (type)
{
case CommandType::PING:
return { PingCommand };
return {PingCommand};
case CommandType::PAUSE:
return [json] { return PauseCommand(json); };
return [json]
{ return PauseCommand(json); };
case CommandType::SET_STREAMING_MODE:
return [this, json] {return setDeviceModeCommand(this->registry, json); };
return [this, json]
{ return setDeviceModeCommand(this->registry, json); };
case CommandType::UPDATE_OTA_CREDENTIALS:
return [this, json] { return updateOTACredentialsCommand(this->registry, json); };
return [this, json]
{ return updateOTACredentialsCommand(this->registry, json); };
case CommandType::SET_WIFI:
return [this, json] { return setWiFiCommand(this->registry, json); };
return [this, json]
{ return setWiFiCommand(this->registry, json); };
case CommandType::UPDATE_WIFI:
return [this, json] { return updateWiFiCommand(this->registry, json); };
return [this, json]
{ return updateWiFiCommand(this->registry, json); };
case CommandType::UPDATE_AP_WIFI:
return [this, json] { return updateAPWiFiCommand(this->registry, json); };
return [this, json]
{ return updateAPWiFiCommand(this->registry, json); };
case CommandType::DELETE_NETWORK:
return [this, json] { return deleteWiFiCommand(this->registry, json); };
return [this, json]
{ return deleteWiFiCommand(this->registry, json); };
case CommandType::SET_MDNS:
return [this, json] { return setMDNSCommand(this->registry, json); };
return [this, json]
{ return setMDNSCommand(this->registry, json); };
case CommandType::UPDATE_CAMERA:
return [this, json] { return updateCameraCommand(this->registry, json); };
return [this, json]
{ return updateCameraCommand(this->registry, json); };
case CommandType::RESTART_CAMERA:
return [this, json] { return restartCameraCommand(this->registry, json); };
return [this, json]
{ return restartCameraCommand(this->registry, json); };
case CommandType::GET_CONFIG:
return [this] { return getConfigCommand(this->registry); };
return [this]
{ return getConfigCommand(this->registry); };
case CommandType::SAVE_CONFIG:
return [this] { return saveConfigCommand(this->registry); };
return [this]
{ return saveConfigCommand(this->registry); };
case CommandType::RESET_CONFIG:
return [this, json] { return resetConfigCommand(this->registry, json); };
return [this, json]
{ return resetConfigCommand(this->registry, json); };
case CommandType::RESTART_DEVICE:
return restartDeviceCommand;
case CommandType::SCAN_NETWORKS:
return [this] { return scanNetworksCommand(this->registry); };
return [this]
{ return scanNetworksCommand(this->registry); };
case CommandType::START_STREAMING:
return startStreamingCommand;
case CommandType::GET_WIFI_STATUS:
return [this] { return getWiFiStatusCommand(this->registry); };
return [this]
{ return getWiFiStatusCommand(this->registry); };
case CommandType::CONNECT_WIFI:
return [this] { return connectWiFiCommand(this->registry); };
return [this]
{ return connectWiFiCommand(this->registry); };
case CommandType::SWITCH_MODE:
return [this, json] { return switchModeCommand(this->registry, json); };
return [this, json]
{ return switchModeCommand(this->registry, json); };
case CommandType::GET_DEVICE_MODE:
return [this] { return getDeviceModeCommand(this->registry); };
return [this]
{ return getDeviceModeCommand(this->registry); };
case CommandType::SET_LED_DUTY_CYCLE:
return [this, json]
{ return updateLEDDutyCycleCommand(this->registry, json); };
case CommandType::GET_LED_DUTY_CYCLE:
return [this]
{ return getLEDDutyCycleCommand(this->registry); };
case CommandType::GET_SERIAL:
return [this]
{ return getSerialNumberCommand(this->registry); };
default:
return nullptr;
}

View File

@@ -44,6 +44,9 @@ enum class CommandType
CONNECT_WIFI,
SWITCH_MODE,
GET_DEVICE_MODE,
SET_LED_DUTY_CYCLE,
GET_LED_DUTY_CYCLE,
GET_SERIAL,
};
class CommandManager

View File

@@ -8,7 +8,8 @@ enum class DependencyType
{
project_config,
camera_manager,
wifi_manager
wifi_manager,
led_manager
};
class DependencyRegistry

View File

@@ -1,4 +1,7 @@
#include "device_commands.hpp"
#include "LEDManager.hpp"
#include "esp_mac.h"
#include <cstdio>
// Implementation inspired by SummerSigh work, initial PR opened in openiris repo, adapted to this rewrite
CommandResult setDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
@@ -25,6 +28,8 @@ CommandResult setDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry,
const auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
projectConfig->setDeviceMode(static_cast<StreamingMode>(mode));
cJSON_Delete(parsedJson);
return CommandResult::getSuccessResult("Device mode set");
}
@@ -64,16 +69,63 @@ CommandResult updateOTACredentialsCommand(std::shared_ptr<DependencyRegistry> re
}
}
projectConfig->setDeviceConfig(OTALogin, OTAPassword, OTAPort);
cJSON_Delete(parsedJson);
projectConfig->setOTAConfig(OTALogin, OTAPassword, OTAPort);
return CommandResult::getSuccessResult("OTA Config set");
}
CommandResult updateLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
{
const auto parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
{
return CommandResult::getErrorResult("Invalid payload");
}
const auto dutyCycleObject = cJSON_GetObjectItem(parsedJson, "dutyCycle");
if (dutyCycleObject == nullptr)
{
return CommandResult::getErrorResult("Invalid payload - missing dutyCycle");
}
const auto dutyCycle = dutyCycleObject->valueint;
if (dutyCycle < 0 || dutyCycle > 100)
{
return CommandResult::getErrorResult("Invalid payload - unsupported dutyCycle");
}
const auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
projectConfig->setLEDDUtyCycleConfig(dutyCycle);
// Try to apply the change live via LEDManager if available
auto ledMgr = registry->resolve<LEDManager>(DependencyType::led_manager);
if (ledMgr)
{
ledMgr->setExternalLEDDutyCycle(static_cast<uint8_t>(dutyCycle));
}
cJSON_Delete(parsedJson);
return CommandResult::getSuccessResult("LED duty cycle set");
}
CommandResult restartDeviceCommand()
{
OpenIrisTasks::ScheduleRestart(2000);
return CommandResult::getSuccessResult("Device restarted");
}
CommandResult getLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> registry)
{
const auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
const auto deviceCfg = projectConfig->getDeviceConfig();
int duty = deviceCfg.led_external_pwm_duty_cycle;
auto result = std::format("{{ \"led_external_pwm_duty_cycle\": {} }}", duty);
return CommandResult::getSuccessResult(result);
}
CommandResult startStreamingCommand()
{
activateStreaming(false); // Don't disable setup interfaces by default
@@ -147,3 +199,23 @@ CommandResult getDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry)
auto result = std::format("{{ \"mode\": \"{}\", \"value\": {} }}", modeStr, static_cast<int>(currentMode));
return CommandResult::getSuccessResult(result);
}
CommandResult getSerialNumberCommand(std::shared_ptr<DependencyRegistry> /*registry*/)
{
// Read MAC for STA interface
uint8_t mac[6] = {0};
esp_read_mac(mac, ESP_MAC_WIFI_STA);
char serial_no_sep[13];
// Serial without separators (12 hex chars)
std::snprintf(serial_no_sep, sizeof(serial_no_sep), "%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
char mac_colon[18];
// MAC with colons
std::snprintf(mac_colon, sizeof(mac_colon), "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
auto result = std::format("{{ \"serial\": \"{}\", \"mac\": \"{}\" }}", serial_no_sep, mac_colon);
return CommandResult::getSuccessResult(result);
}

View File

@@ -13,10 +13,15 @@ CommandResult setDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry,
CommandResult updateOTACredentialsCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult updateLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult getLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> registry);
CommandResult restartDeviceCommand();
CommandResult startStreamingCommand();
CommandResult switchModeCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult getDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry);
CommandResult getDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry);
CommandResult getSerialNumberCommand(std::shared_ptr<DependencyRegistry> registry);

View File

@@ -4,15 +4,15 @@
// Forward declarations
extern void start_video_streaming(void *arg);
bool startupCommandReceived = false;
static bool s_startupCommandReceived = false;
bool getStartupCommandReceived()
{
return startupCommandReceived;
return s_startupCommandReceived;
}
void setStartupCommandReceived(bool startupCommandReceived)
{
startupCommandReceived = startupCommandReceived;
s_startupCommandReceived = startupCommandReceived;
}
static TaskHandle_t *g_serial_manager_handle = nullptr;
@@ -27,15 +27,15 @@ void setSerialManagerHandle(TaskHandle_t *serialManagerHandle)
}
// Global pause state
bool startupPaused = false;
static bool s_startupPaused = false;
bool getStartupPaused()
{
return startupPaused;
return s_startupPaused;
}
void setStartupPaused(bool startupPaused)
{
startupPaused = startupPaused;
s_startupPaused = startupPaused;
}
// Function to manually activate streaming
@@ -47,4 +47,9 @@ void activateStreaming(bool disableSetup)
void *serialTaskHandle = (serialHandle && *serialHandle) ? *serialHandle : nullptr;
start_video_streaming(serialTaskHandle);
}
}
// USB handover state
static bool s_usbHandoverDone = false;
bool getUsbHandoverDone() { return s_usbHandoverDone; }
void setUsbHandoverDone(bool done) { s_usbHandoverDone = done; }

View File

@@ -21,4 +21,8 @@ void setStartupCommandReceived(bool startupCommandReceived);
bool getStartupPaused();
void setStartupPaused(bool startupPaused);
// Tracks whether USB handover from usb_serial_jtag to TinyUSB was performed
bool getUsbHandoverDone();
void setUsbHandoverDone(bool done);
#endif

View File

@@ -1,4 +1,4 @@
idf_component_register(SRCS "LEDManager/LEDManager.cpp"
INCLUDE_DIRS "LEDManager"
REQUIRES StateManager driver esp_driver_ledc Helpers
REQUIRES StateManager driver esp_driver_ledc Helpers ProjectConfig
)

View File

@@ -48,10 +48,7 @@ ledStateMap_t LEDManager::ledStateMap = {
{
false,
false,
{
{LED_ON, 200}, {LED_OFF, 200}, {LED_ON, 200}, {LED_OFF, 200}, {LED_ON, 200}, {LED_OFF, 200},
{LED_ON, 200}, {LED_OFF, 200}, {LED_ON, 200}, {LED_OFF, 200}
},
{{LED_ON, 200}, {LED_OFF, 200}, {LED_ON, 200}, {LED_OFF, 200}, {LED_ON, 200}, {LED_OFF, 200}, {LED_ON, 200}, {LED_OFF, 200}, {LED_ON, 200}, {LED_OFF, 200}},
},
},
{
@@ -65,32 +62,38 @@ ledStateMap_t LEDManager::ledStateMap = {
};
LEDManager::LEDManager(gpio_num_t pin, gpio_num_t illumninator_led_pin,
QueueHandle_t ledStateQueue) : blink_led_pin(pin),
illumninator_led_pin(illumninator_led_pin),
ledStateQueue(ledStateQueue),
currentState(LEDStates_e::LedStateNone) {
QueueHandle_t ledStateQueue, std::shared_ptr<ProjectConfig> deviceConfig) : blink_led_pin(pin),
illumninator_led_pin(illumninator_led_pin),
ledStateQueue(ledStateQueue),
currentState(LEDStates_e::LedStateNone),
deviceConfig(deviceConfig)
{
}
void LEDManager::setup() {
ESP_LOGD(LED_MANAGER_TAG, "Setting up status led.");
void LEDManager::setup()
{
ESP_LOGI(LED_MANAGER_TAG, "Setting up status led.");
gpio_reset_pin(blink_led_pin);
/* Set the GPIO as a push/pull output */
gpio_set_direction(blink_led_pin, GPIO_MODE_OUTPUT);
this->toggleLED(LED_OFF);
#ifdef CONFIG_LED_EXTERNAL_CONTROL
ESP_LOGD(LED_MANAGER_TAG, "Setting up illuminator led.");
ESP_LOGI(LED_MANAGER_TAG, "Setting up illuminator led.");
const int freq = CONFIG_LED_EXTERNAL_PWM_FREQ;
const auto resolution = LEDC_TIMER_8_BIT;
const int dutyCycle = (CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE * 255) / 100;
const auto deviceConfig = this->deviceConfig->getDeviceConfig();
const uint32_t dutyCycle = (deviceConfig.led_external_pwm_duty_cycle * 255) / 100;
ESP_LOGI(LED_MANAGER_TAG, "Setting dutyCycle to: %lu ", dutyCycle);
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = resolution,
.timer_num = LEDC_TIMER_0,
.freq_hz = freq,
.clk_cfg = LEDC_AUTO_CLK
};
.clk_cfg = LEDC_AUTO_CLK};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
@@ -101,8 +104,7 @@ void LEDManager::setup() {
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = LEDC_TIMER_0,
.duty = dutyCycle,
.hpoint = 0
};
.hpoint = 0};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
#endif
@@ -110,65 +112,98 @@ void LEDManager::setup() {
ESP_LOGD(LED_MANAGER_TAG, "Done.");
}
void LEDManager::handleLED() {
if (!this->finishedPattern) {
void LEDManager::handleLED()
{
if (!this->finishedPattern)
{
displayCurrentPattern();
return;
}
if (xQueueReceive(this->ledStateQueue, &buffer, 10)) {
if (xQueueReceive(this->ledStateQueue, &buffer, 10))
{
this->updateState(buffer);
} else {
}
else
{
// we've finished displaying the pattern, so let's check if it's repeatable and if so - reset it
if (ledStateMap[this->currentState].isRepeatable || ledStateMap[this->currentState].isError) {
if (ledStateMap[this->currentState].isRepeatable || ledStateMap[this->currentState].isError)
{
this->currentPatternIndex = 0;
this->finishedPattern = false;
}
}
}
void LEDManager::displayCurrentPattern() {
void LEDManager::displayCurrentPattern()
{
auto [state, delayTime] = ledStateMap[this->currentState].patterns[this->currentPatternIndex];
this->toggleLED(state);
this->timeToDelayFor = delayTime;
if (this->currentPatternIndex < ledStateMap[this->currentState].patterns.size() - 1)
this->currentPatternIndex++;
else {
else
{
this->finishedPattern = true;
this->toggleLED(LED_OFF);
}
}
void LEDManager::updateState(const LEDStates_e newState) {
// we should change the displayed state
// only if we finished displaying the current one - which is handled by the task
// if the new state is not the same as the current one
// and if can actually display the state
// if we've got an error state - that's it, we'll just keep repeating it indefinitely
void LEDManager::updateState(const LEDStates_e newState)
{
// If we've got an error state - that's it, keep repeating it indefinitely
if (ledStateMap[this->currentState].isError)
return;
// Alternative (recoverable error states):
// Allow recovery from error states by only blocking transitions when both, current and new states are error. Uncomment to enable recovery.
// if (ledStateMap[this->currentState].isError && ledStateMap[newState].isError)
// return;
// Only update when new state differs and is known.
if (!ledStateMap.contains(newState))
return;
if (newState == this->currentState)
return;
if (ledStateMap.contains(newState)) {
this->currentState = newState;
this->currentPatternIndex = 0;
this->finishedPattern = false;
}
this->currentState = newState;
this->currentPatternIndex = 0;
this->finishedPattern = false;
}
void LEDManager::toggleLED(const bool state) const {
void LEDManager::toggleLED(const bool state) const
{
gpio_set_level(blink_led_pin, state);
}
void HandleLEDDisplayTask(void *pvParameter) {
auto *ledManager = static_cast<LEDManager *>(pvParameter);
void LEDManager::setExternalLEDDutyCycle(uint8_t dutyPercent)
{
#ifdef CONFIG_LED_EXTERNAL_CONTROL
const uint32_t dutyCycle = (static_cast<uint32_t>(dutyPercent) * 255) / 100;
ESP_LOGI(LED_MANAGER_TAG, "Updating external LED duty to %u%% (raw %lu)", dutyPercent, dutyCycle);
while (true) {
// Apply to LEDC hardware live
// We configured channel 0 in setup with LEDC_LOW_SPEED_MODE
ESP_ERROR_CHECK_WITHOUT_ABORT(ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, dutyCycle));
ESP_ERROR_CHECK_WITHOUT_ABORT(ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0));
#else
(void)dutyPercent; // unused
ESP_LOGW(LED_MANAGER_TAG, "CONFIG_LED_EXTERNAL_CONTROL not enabled; ignoring duty update");
#endif
}
void HandleLEDDisplayTask(void *pvParameter)
{
auto *ledManager = static_cast<LEDManager *>(pvParameter);
TickType_t lastWakeTime = xTaskGetTickCount();
while (true)
{
ledManager->handleLED();
vTaskDelay(ledManager->getTimeToDelayFor());
const TickType_t delayTicks = pdMS_TO_TICKS(ledManager->getTimeToDelayFor());
// Ensure at least 1 tick delay to yield CPU
vTaskDelayUntil(&lastWakeTime, delayTicks > 0 ? delayTicks : 1);
}
}

View File

@@ -16,6 +16,7 @@
#include <unordered_map>
#include <vector>
#include <StateManager.hpp>
#include <ProjectConfig.hpp>
#include <helpers.hpp>
// it kinda looks like different boards have these states swapped
@@ -41,12 +42,16 @@ typedef std::unordered_map<LEDStates_e, LEDStage>
class LEDManager
{
public:
LEDManager(gpio_num_t blink_led_pin, gpio_num_t illumninator_led_pin, QueueHandle_t ledStateQueue);
LEDManager(gpio_num_t blink_led_pin, gpio_num_t illumninator_led_pin, QueueHandle_t ledStateQueue, std::shared_ptr<ProjectConfig> deviceConfig);
void setup();
void handleLED();
size_t getTimeToDelayFor() const { return timeToDelayFor; }
// Apply new external LED PWM duty cycle immediately (0-100)
void setExternalLEDDutyCycle(uint8_t dutyPercent);
uint8_t getExternalLEDDutyCycle() const { return deviceConfig ? deviceConfig->getDeviceConfig().led_external_pwm_duty_cycle : 0; }
private:
void toggleLED(bool state) const;
void displayCurrentPattern();
@@ -60,6 +65,7 @@ private:
LEDStates_e buffer;
LEDStates_e currentState;
std::shared_ptr<ProjectConfig> deviceConfig;
size_t currentPatternIndex = 0;
size_t timeToDelayFor = 100;

View File

@@ -21,35 +21,49 @@ struct BaseConfigModel
Preferences *pref;
};
enum class StreamingMode {
enum class StreamingMode
{
AUTO,
UVC,
WIFI,
};
struct DeviceMode_t : BaseConfigModel {
struct DeviceMode_t : BaseConfigModel
{
StreamingMode mode;
explicit DeviceMode_t( Preferences *pref) : BaseConfigModel(pref), mode(StreamingMode::AUTO){}
explicit DeviceMode_t(Preferences *pref) : BaseConfigModel(pref), mode(StreamingMode::AUTO) {}
void load() {
int stored_mode = this->pref->getInt("mode", 0);
void load()
{
// Default mode can be controlled via sdkconfig:
// - If CONFIG_GENERAL_DEFAULT_WIRED_MODE is enabled, default to UVC
// - Otherwise default to AUTO
int default_mode =
#if CONFIG_GENERAL_DEFAULT_WIRED_MODE
static_cast<int>(StreamingMode::UVC);
#else
static_cast<int>(StreamingMode::AUTO);
#endif
int stored_mode = this->pref->getInt("mode", default_mode);
this->mode = static_cast<StreamingMode>(stored_mode);
ESP_LOGI("DeviceMode", "Loaded device mode: %d", stored_mode);
}
void save() const {
void save() const
{
this->pref->putInt("mode", static_cast<int>(this->mode));
ESP_LOGI("DeviceMode", "Saved device mode: %d", static_cast<int>(this->mode));
}
};
struct DeviceConfig_t : BaseConfigModel
{
DeviceConfig_t(Preferences *pref) : BaseConfigModel(pref) {}
std::string OTALogin;
std::string OTAPassword;
int led_external_pwm_duty_cycle;
int OTAPort;
void load()
@@ -57,20 +71,23 @@ struct DeviceConfig_t : BaseConfigModel
this->OTALogin = this->pref->getString("OTALogin", "openiris");
this->OTAPassword = this->pref->getString("OTAPassword", "openiris");
this->OTAPort = this->pref->getInt("OTAPort", 3232);
this->led_external_pwm_duty_cycle = this->pref->getInt("led_ext_pwm", CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE);
};
void save() const {
void save() const
{
this->pref->putString("OTALogin", this->OTALogin.c_str());
this->pref->putString("OTAPassword", this->OTAPassword.c_str());
this->pref->putInt("OTAPort", this->OTAPort);
this->pref->putInt("led_ext_pwm", this->led_external_pwm_duty_cycle);
};
std::string toRepresentation() const
{
return Helpers::format_string(
"\"device_config\": {\"OTALogin\": \"%s\", \"OTAPassword\": \"%s\", "
"\"OTAPort\": %u}",
this->OTALogin.c_str(), this->OTAPassword.c_str(), this->OTAPort);
"\"OTAPort\": %u, \"led_external_pwm_duty_cycle\": %u}",
this->OTALogin.c_str(), this->OTAPassword.c_str(), this->OTAPort, this->led_external_pwm_duty_cycle);
};
};
@@ -94,7 +111,8 @@ struct MDNSConfig_t : BaseConfigModel
this->hostname = this->pref->getString("hostname", default_hostname);
};
void save() const {
void save() const
{
this->pref->putString("hostname", this->hostname.c_str());
};
@@ -125,7 +143,8 @@ struct CameraConfig_t : BaseConfigModel
this->brightness = this->pref->getInt("brightness", 2);
};
void save() const {
void save() const
{
this->pref->putInt("vflip", this->vflip);
this->pref->putInt("href", this->href);
this->pref->putInt("framesize", this->framesize);
@@ -186,12 +205,13 @@ struct WiFiConfig_t : BaseConfigModel
this->password = this->pref->getString(("password" + iter_str).c_str(), "");
this->channel = this->pref->getUInt(("channel" + iter_str).c_str());
this->power = this->pref->getUInt(("power" + iter_str).c_str());
ESP_LOGI("WiFiConfig", "Loaded network %d: name=%s, ssid=%s, channel=%d",
ESP_LOGI("WiFiConfig", "Loaded network %d: name=%s, ssid=%s, channel=%d",
index, this->name.c_str(), this->ssid.c_str(), this->channel);
};
void save() const {
void save() const
{
char buffer[2];
auto const iter_str = std::string(Helpers::itoa(this->index, buffer, 10));
@@ -200,8 +220,8 @@ struct WiFiConfig_t : BaseConfigModel
this->pref->putString(("password" + iter_str).c_str(), this->password.c_str());
this->pref->putUInt(("channel" + iter_str).c_str(), this->channel);
this->pref->putUInt(("power" + iter_str).c_str(), this->power);
ESP_LOGI("WiFiConfig", "Saved network %d: name=%s, ssid=%s, channel=%d",
ESP_LOGI("WiFiConfig", "Saved network %d: name=%s, ssid=%s, channel=%d",
this->index, this->name.c_str(), this->ssid.c_str(), this->channel);
};
@@ -228,7 +248,8 @@ struct AP_WiFiConfig_t : BaseConfigModel
this->password = this->pref->getString("apPassword", CONFIG_WIFI_AP_PASSWORD);
};
void save() const {
void save() const
{
this->pref->putString("apSSID", this->ssid.c_str());
this->pref->putString("apPass", this->password.c_str());
this->pref->putUInt("apChannel", this->channel);
@@ -254,7 +275,8 @@ struct WiFiTxPower_t : BaseConfigModel
this->power = this->pref->getUInt("txpower", 52);
};
void save() const {
void save() const
{
this->pref->putUInt("txpower", this->power);
};

View File

@@ -24,7 +24,8 @@ ProjectConfig::ProjectConfig(Preferences *pref) : pref(pref),
ProjectConfig::~ProjectConfig() = default;
void ProjectConfig::save() const {
void ProjectConfig::save() const
{
ESP_LOGD(CONFIGURATION_TAG, "Saving project config");
this->config.device.save();
this->config.device_mode.save();
@@ -92,14 +93,22 @@ bool ProjectConfig::reset()
//! DeviceConfig
//*
//**********************************************************************************************************************
void ProjectConfig::setDeviceConfig(const std::string &OTALogin,
const std::string &OTAPassword,
const int OTAPort)
void ProjectConfig::setOTAConfig(const std::string &OTALogin,
const std::string &OTAPassword,
const int OTAPort)
{
ESP_LOGD(CONFIGURATION_TAG, "Updating device config");
this->config.device.OTALogin.assign(OTALogin);
this->config.device.OTAPassword.assign(OTAPassword);
this->config.device.OTAPort = OTAPort;
this->config.device.save();
}
void ProjectConfig::setLEDDUtyCycleConfig(int led_external_pwm_duty_cycle)
{
this->config.device.led_external_pwm_duty_cycle = led_external_pwm_duty_cycle;
ESP_LOGI(CONFIGURATION_TAG, "Setting duty cycle to %d", led_external_pwm_duty_cycle);
this->config.device.save();
}
void ProjectConfig::setMDNSConfig(const std::string &hostname)
@@ -120,6 +129,7 @@ void ProjectConfig::setCameraConfig(const uint8_t vflip,
this->config.camera.framesize = framesize;
this->config.camera.quality = quality;
this->config.camera.brightness = brightness;
this->config.camera.save();
ESP_LOGD(CONFIGURATION_TAG, "Updating Camera config");
}
@@ -133,8 +143,8 @@ void ProjectConfig::setWifiConfig(const std::string &networkName,
const auto size = this->config.networks.size();
const auto it = std::ranges::find_if(this->config.networks,
[&](const WiFiConfig_t &network)
{ return network.name == networkName; });
[&](const WiFiConfig_t &network)
{ return network.name == networkName; });
if (it != this->config.networks.end())
{
@@ -191,8 +201,8 @@ void ProjectConfig::deleteWifiConfig(const std::string &networkName)
}
const auto it = std::ranges::find_if(this->config.networks,
[&](const WiFiConfig_t &network)
{ return network.name == networkName; });
[&](const WiFiConfig_t &network)
{ return network.name == networkName; });
if (it != this->config.networks.end())
{
@@ -205,6 +215,7 @@ void ProjectConfig::deleteWifiConfig(const std::string &networkName)
void ProjectConfig::setWiFiTxPower(uint8_t power)
{
this->config.txpower.power = power;
this->config.txpower.save();
ESP_LOGD(CONFIGURATION_TAG, "Updating wifi tx power");
}
@@ -215,12 +226,14 @@ void ProjectConfig::setAPWifiConfig(const std::string &ssid,
this->config.ap_network.ssid.assign(ssid);
this->config.ap_network.password.assign(password);
this->config.ap_network.channel = channel;
this->config.ap_network.save();
ESP_LOGD(CONFIGURATION_TAG, "Updating access point config");
}
void ProjectConfig::setDeviceMode(const StreamingMode deviceMode) {
void ProjectConfig::setDeviceMode(const StreamingMode deviceMode)
{
this->config.device_mode.mode = deviceMode;
this->config.device_mode.save(); // Save immediately
this->config.device_mode.save(); // Save immediately
}
//**********************************************************************************************************************
@@ -258,10 +271,12 @@ TrackerConfig_t &ProjectConfig::getTrackerConfig()
return this->config;
}
DeviceMode_t &ProjectConfig::getDeviceModeConfig() {
DeviceMode_t &ProjectConfig::getDeviceModeConfig()
{
return this->config.device_mode;
}
StreamingMode ProjectConfig::getDeviceMode() {
StreamingMode ProjectConfig::getDeviceMode()
{
return this->config.device_mode.mode;
}

View File

@@ -22,11 +22,6 @@ public:
void load();
void save() const;
void wifiConfigSave();
void cameraConfigSave();
void deviceConfigSave();
void mdnsConfigSave();
void wifiTxPowerConfigSave();
bool reset();
DeviceConfig_t &getDeviceConfig();
@@ -38,9 +33,10 @@ public:
WiFiTxPower_t &getWiFiTxPowerConfig();
TrackerConfig_t &getTrackerConfig();
void setDeviceConfig(const std::string &OTALogin,
const std::string &OTAPassword,
int OTAPort);
void setOTAConfig(const std::string &OTALogin,
const std::string &OTAPassword,
int OTAPort);
void setLEDDUtyCycleConfig(int led_external_pwm_duty_cycle);
void setMDNSConfig(const std::string &hostname);
void setCameraConfig(uint8_t vflip,
uint8_t framesize,

View File

@@ -24,13 +24,36 @@ void SerialManager::try_receive()
int current_position = 0;
int len = usb_serial_jtag_read_bytes(this->temp_data, 256, 1000 / 20);
// If driver is uninstalled or an error occurs, abort read gracefully
if (len < 0)
{
return;
}
// since we've got something on the serial port
// we gotta keep reading until we've got the whole message
while (len)
while (len > 0)
{
memcpy(this->data + current_position, this->temp_data, len);
// Prevent buffer overflow
if (current_position + len >= BUF_SIZE)
{
int copy_len = BUF_SIZE - 1 - current_position;
if (copy_len > 0)
{
memcpy(this->data + current_position, this->temp_data, copy_len);
current_position += copy_len;
}
// Drop the rest of the input to avoid overflow
break;
}
memcpy(this->data + current_position, this->temp_data, (size_t)len);
current_position += len;
len = usb_serial_jtag_read_bytes(this->temp_data, 256, 1000 / 20);
if (len < 0)
{
// Driver likely uninstalled during handover; stop processing this cycle
break;
}
}
if (current_position)
@@ -42,9 +65,10 @@ void SerialManager::try_receive()
// Notify main that a command was received during startup
notify_startup_command_received();
const auto result = this->commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(this->data)));
const auto resultMessage = result.getResult();
usb_serial_jtag_write_bytes(resultMessage.c_str(), resultMessage.length(), 1000 / 20);
const auto result = this->commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(this->data)));
const auto resultMessage = result.getResult();
int written = usb_serial_jtag_write_bytes(resultMessage.c_str(), resultMessage.length(), 1000 / 20);
(void)written; // ignore errors if driver already uninstalled
}
}
@@ -79,6 +103,7 @@ void SerialManager::send_heartbeat()
sprintf(heartbeat, "{\"heartbeat\":\"openiris_setup_mode\",\"serial\":\"%s\"}\n", serial_number);
usb_serial_jtag_write_bytes(heartbeat, strlen(heartbeat), 1000 / 20);
// Ignore return value; if the driver was uninstalled, this is a no-op
}
bool SerialManager::should_send_heartbeat()
@@ -123,4 +148,19 @@ void HandleSerialManagerTask(void *pvParameters)
lastHeartbeat = currentTime;
}
}
}
void SerialManager::shutdown()
{
// Stop heartbeats; timer will be deleted by main if needed.
// Uninstall the USB Serial JTAG driver to free the internal USB for TinyUSB.
esp_err_t err = usb_serial_jtag_driver_uninstall();
if (err == ESP_OK)
{
ESP_LOGI("[SERIAL]", "usb_serial_jtag driver uninstalled");
}
else if (err != ESP_ERR_INVALID_STATE)
{
ESP_LOGW("[SERIAL]", "usb_serial_jtag_driver_uninstall returned %s", esp_err_to_name(err));
}
}

View File

@@ -27,6 +27,7 @@ public:
void send_heartbeat();
bool should_send_heartbeat();
void notify_startup_command_received();
void shutdown();
private:
std::shared_ptr<CommandManager> commandManager;

View File

@@ -20,7 +20,8 @@ esp_err_t StreamHelpers::stream(httpd_req_t *req)
size_t _jpg_buf_len = 0;
uint8_t *_jpg_buf = nullptr;
char *part_buf[256];
// Buffer for multipart header; was mistakenly declared as array of pointers
char part_buf[256];
static int64_t last_frame = 0;
if (!last_frame)
last_frame = esp_timer_get_time();
@@ -55,7 +56,7 @@ esp_err_t StreamHelpers::stream(httpd_req_t *req)
response = httpd_resp_send_chunk(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY));
if (response == ESP_OK)
{
size_t hlen = snprintf((char *)part_buf, 128, STREAM_PART, _jpg_buf_len, _timestamp.tv_sec, _timestamp.tv_usec);
size_t hlen = snprintf((char *)part_buf, sizeof(part_buf), STREAM_PART, _jpg_buf_len, _timestamp.tv_sec, _timestamp.tv_usec);
response = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
}
if (response == ESP_OK)

View File

@@ -1,4 +1,4 @@
idf_component_register(SRCS "UVCStream/UVCStream.cpp"
INCLUDE_DIRS "UVCStream"
REQUIRES esp_timer esp32-camera StateManager usb_device_uvc CameraManager
REQUIRES esp_timer esp32-camera StateManager usb_device_uvc CameraManager Helpers
)

View File

@@ -1,10 +1,13 @@
#include "UVCStream.hpp"
constexpr int UVC_MAX_FRAMESIZE_SIZE(75 * 1024);
#include <cstdio> // for snprintf
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// no deps on main globals here; handover is performed in main before calling setup when needed
static const char *UVC_STREAM_TAG = "[UVC DEVICE]";
extern "C" {
static char serial_number_str[18];
static char serial_number_str[13];
const char *get_uvc_device_name()
{
@@ -23,17 +26,19 @@ extern "C" {
return CONFIG_TUSB_SERIAL_NUM;
}
sniprintf(serial_number_str, sizeof(serial_number_str), "%02X:%02X:%02X:%02X:%02X:%02X",
mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5]
);
// 12 hex chars without separators
snprintf(serial_number_str, sizeof(serial_number_str), "%02X%02X%02X%02X%02X%02X",
mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5]);
}
return serial_number_str;
}
}
// single definition of shared framebuffer storage
UVCStreamHelpers::fb_t UVCStreamHelpers::s_fb = {};
static esp_err_t UVCStreamHelpers::camera_start_cb(uvc_format_t format, int width, int height, int rate, void *cb_ctx)
{
(void)cb_ctx;
ESP_LOGI(UVC_STREAM_TAG, "Camera Start");
ESP_LOGI(UVC_STREAM_TAG, "Format: %d, width: %d, height: %d, rate: %d", format, width, height, rate);
framesize_t frame_size = FRAMESIZE_QVGA;
@@ -78,7 +83,7 @@ static void UVCStreamHelpers::camera_stop_cb(void *cb_ctx)
static uvc_fb_t *UVCStreamHelpers::camera_fb_get_cb(void *cb_ctx)
{
(void)cb_ctx;
auto *mgr = static_cast<UVCStreamManager *>(cb_ctx);
s_fb.cam_fb_p = esp_camera_fb_get();
if (!s_fb.cam_fb_p)
@@ -86,6 +91,31 @@ static uvc_fb_t *UVCStreamHelpers::camera_fb_get_cb(void *cb_ctx)
return nullptr;
}
//--------------------------------------------------------------------------------------------------------------
// Pace frames to exactly 60 fps (drop extras). Uses fixed-point accumulator
// to achieve an exact average of 60.000 fps without drifting.
static int64_t next_deadline_us = 0;
static int rem_acc = 0; // remainder accumulator for 1e6 % fps distribution
constexpr int target_fps = 60;
constexpr int64_t us_per_sec = 1000000LL;
constexpr int base_interval_us = us_per_sec / target_fps; // 16666
constexpr int rem_us = us_per_sec % target_fps; // 40
const int64_t now_us = esp_timer_get_time();
if (next_deadline_us == 0)
{
// First frame: allow immediately and schedule next slot from now
next_deadline_us = now_us;
}
if (now_us < next_deadline_us)
{
// Too early for next frame: drop this camera buffer
esp_camera_fb_return(s_fb.cam_fb_p);
s_fb.cam_fb_p = nullptr;
return nullptr;
}
//--------------------------------------------------------------------------------------------------------------
s_fb.uvc_fb.buf = s_fb.cam_fb_p->buf;
s_fb.uvc_fb.len = s_fb.cam_fb_p->len;
s_fb.uvc_fb.width = s_fb.cam_fb_p->width;
@@ -93,12 +123,27 @@ static uvc_fb_t *UVCStreamHelpers::camera_fb_get_cb(void *cb_ctx)
s_fb.uvc_fb.format = UVC_FORMAT_JPEG; // we gotta make sure we're ALWAYS using JPEG
s_fb.uvc_fb.timestamp = s_fb.cam_fb_p->timestamp;
if (s_fb.uvc_fb.len > UVC_MAX_FRAMESIZE_SIZE)
// Ensure frame fits into configured UVC transfer buffer
if (mgr && s_fb.uvc_fb.len > mgr->getUvcBufferSize())
{
ESP_LOGE(UVC_STREAM_TAG, "Frame size %d is larger than max frame size %d", s_fb.uvc_fb.len, UVC_MAX_FRAMESIZE_SIZE);
ESP_LOGE(UVC_STREAM_TAG, "Frame size %d exceeds UVC buffer size %u", (int)s_fb.uvc_fb.len, (unsigned)mgr->getUvcBufferSize());
esp_camera_fb_return(s_fb.cam_fb_p);
return nullptr;
}
//--------------------------------------------------------------------------------------------------------------
// Schedule the next allowed frame time: base interval plus distributed remainder
rem_acc += rem_us;
int extra_us = 0;
if (rem_acc >= target_fps)
{
rem_acc -= target_fps;
extra_us = 1;
}
// Accumulate from the previous deadline to avoid drift; if we are badly late, catch up from now
const int64_t base_next = next_deadline_us + base_interval_us + extra_us;
next_deadline_us = (base_next < now_us) ? now_us : base_next;
//--------------------------------------------------------------------------------------------------------------
return &s_fb.uvc_fb;
}
@@ -113,14 +158,15 @@ static void UVCStreamHelpers::camera_fb_return_cb(uvc_fb_t *fb, void *cb_ctx)
esp_err_t UVCStreamManager::setup()
{
#ifndef CONFIG_GENERAL_WIRED_MODE
#ifndef CONFIG_GENERAL_DEFAULT_WIRED_MODE
ESP_LOGE(UVC_STREAM_TAG, "The board does not support UVC, please, setup WiFi connection.");
return ESP_FAIL;
#endif
ESP_LOGI(UVC_STREAM_TAG, "Setting up UVC Stream");
uvc_buffer = static_cast<uint8_t *>(malloc(UVC_MAX_FRAMESIZE_SIZE));
// Allocate a fixed-size transfer buffer (compile-time constant)
uvc_buffer_size = UVCStreamManager::UVC_MAX_FRAMESIZE_SIZE;
uvc_buffer = static_cast<uint8_t *>(malloc(uvc_buffer_size));
if (uvc_buffer == nullptr)
{
ESP_LOGE(UVC_STREAM_TAG, "Allocating buffer for UVC Device failed");
@@ -129,11 +175,12 @@ esp_err_t UVCStreamManager::setup()
uvc_device_config_t config = {
.uvc_buffer = uvc_buffer,
.uvc_buffer_size = UVC_MAX_FRAMESIZE_SIZE,
.uvc_buffer_size = UVCStreamManager::UVC_MAX_FRAMESIZE_SIZE,
.start_cb = UVCStreamHelpers::camera_start_cb,
.fb_get_cb = UVCStreamHelpers::camera_fb_get_cb,
.fb_return_cb = UVCStreamHelpers::camera_fb_return_cb,
.stop_cb = UVCStreamHelpers::camera_stop_cb,
.cb_ctx = this,
};
esp_err_t ret = uvc_device_config(0, &config);

View File

@@ -40,7 +40,8 @@ namespace UVCStreamHelpers
uvc_fb_t uvc_fb;
} fb_t;
static fb_t s_fb;
// single storage is defined in UVCStream.cpp
extern fb_t s_fb;
static esp_err_t camera_start_cb(uvc_format_t format, int width, int height, int rate, void *cb_ctx);
static void camera_stop_cb(void *cb_ctx);
@@ -51,10 +52,14 @@ namespace UVCStreamHelpers
class UVCStreamManager
{
uint8_t *uvc_buffer = nullptr;
uint32_t uvc_buffer_size = 0;
public:
// Compile-time buffer size; keep conservative headroom for MJPEG QVGA
static constexpr uint32_t UVC_MAX_FRAMESIZE_SIZE = 75 * 1024;
esp_err_t setup();
esp_err_t start();
uint32_t getUvcBufferSize() const { return uvc_buffer_size; }
};
#endif // UVCSTREAM_HPP

View File

@@ -21,15 +21,16 @@
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#include "tusb.h"
#include "usb_descriptors.h"
#include <string.h> // memcpy, strlen
//--------------------------------------------------------------------+
// Device Descriptors
//--------------------------------------------------------------------+
// Device descriptor: identifies this as a composite device using IAD for UVC
tusb_desc_device_t const desc_device = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
@@ -63,6 +64,16 @@ uint8_t const *tud_descriptor_device_cb(void)
//--------------------------------------------------------------------+
// Configuration Descriptor
//--------------------------------------------------------------------+
// String descriptor indices used in interface descriptors
#define STRID_LANGID 0
#define STRID_MANUFACTURER 1
#define STRID_PRODUCT 2
#define STRID_SERIAL 3
#define STRID_UVC_CAM1 4
// Endpoint numbers for UVC video IN endpoints (device -> host)
#define EPNUM_CAM1_VIDEO_IN 0x81
#if CFG_TUD_CAM1_VIDEO_STREAMING_BULK
#if CONFIG_UVC_CAM1_MULTI_FRAMESIZE
@@ -101,76 +112,39 @@ uint8_t const *tud_descriptor_device_cb(void)
#endif // CFG_TUD_CAM1_VIDEO_STREAMING_BULK
#if CONFIG_UVC_SUPPORT_TWO_CAM
#if CFG_TUD_CAM2_VIDEO_STREAMING_BULK
// Total length of this configuration
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CAM1_VIDEO_CAPTURE_DESC_LEN)
#if CONFIG_UVC_CAM2_MULTI_FRAMESIZE
#if CONFIG_FORMAT_MJPEG_CAM2
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN (TUD_VIDEO_CAPTURE_DESC_MULTI_MJPEG_BULK_LEN(4))
#elif CONFIG_FORMAT_H264_CAM2
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN (TUD_VIDEO_CAPTURE_DESC_MULTI_FRAME_BASED_BULK_LEN(4))
#endif
#else
#if CONFIG_FORMAT_MJPEG_CAM2
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN (TUD_VIDEO_CAPTURE_DESC_MJPEG_BULK_LEN)
#elif CONFIG_FORMAT_H264_CAM2
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN (TUD_VIDEO_CAPTURE_DESC_FRAME_BASED_BULK_LEN)
#else
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN (TUD_VIDEO_CAPTURE_DESC_UNCOMPR_BULK_LEN)
#endif
#endif // CONFIG_UVC_CAM2_MULTI_FRAMESIZE
#else
#if CONFIG_UVC_CAM2_MULTI_FRAMESIZE
#if CONFIG_FORMAT_MJPEG_CAM2
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN (TUD_VIDEO_CAPTURE_DESC_MULTI_MJPEG_LEN(4))
#elif CONFIG_FORMAT_H264_CAM2
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN (TUD_VIDEO_CAPTURE_DESC_MULTI_FRAME_BASED_LEN(4))
#endif
#else
#if CONFIG_FORMAT_MJPEG_CAM2
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN (TUD_VIDEO_CAPTURE_DESC_MJPEG_LEN)
#elif CONFIG_FORMAT_H264_CAM2
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN (TUD_VIDEO_CAPTURE_DESC_FRAME_BASED_LEN)
#else
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN (TUD_VIDEO_CAPTURE_DESC_UNCOMPR_LEN)
#endif
#endif // CONFIG_UVC_CAM2_MULTI_FRAMESIZE
#endif // CFG_TUD_CAM2_VIDEO_STREAMING_BULK
#else
#define TUD_CAM2_VIDEO_CAPTURE_DESC_LEN 0
#endif
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CAM1_VIDEO_CAPTURE_DESC_LEN + TUD_CAM2_VIDEO_CAPTURE_DESC_LEN)
#define EPNUM_CAM1_VIDEO_IN 0x81
#if CONFIG_UVC_SUPPORT_TWO_CAM
#define EPNUM_CAM2_VIDEO_IN 0x82
#endif
uint8_t const desc_fs_configuration[] = {
// Config number, interface count, string index, total length, attribute, power in mA
// Full-speed configuration descriptor
static uint8_t const desc_fs_configuration[] = {
// TUD_CONFIG_DESCRIPTOR(config_number, interface_count, string_index,
// total_length, attributes, power_mA)
// attributes: 0 = bus-powered (default). Add TUSB_DESC_CONFIG_ATT_SELF_POWERED or _REMOTE_WAKEUP if needed.
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0, 500),
// IAD for Video Control
#if CFG_TUD_CAM1_VIDEO_STREAMING_BULK
#if CONFIG_UVC_CAM1_MULTI_FRAMESIZE
#if CONFIG_FORMAT_MJPEG_CAM1
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_MJPEG_BULK(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN, CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
// Camera 1, multi-size MJPEG over BULK
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_MJPEG_BULK(STRID_UVC_CAM1, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN, CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
#elif CONFIG_FORMAT_H264_CAM1
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_H264_BULK(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN, CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
// Camera 1, multi-size H.264 over BULK
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_H264_BULK(STRID_UVC_CAM1, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN, CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
#endif
#else
#if CONFIG_FORMAT_MJPEG_CAM1
TUD_VIDEO_CAPTURE_DESCRIPTOR_MJPEG_BULK(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
// Camera 1, single-size MJPEG over BULK
TUD_VIDEO_CAPTURE_DESCRIPTOR_MJPEG_BULK(STRID_UVC_CAM1, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
UVC_CAM1_FRAME_WIDTH, UVC_CAM1_FRAME_HEIGHT, UVC_CAM1_FRAME_RATE,
CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
#elif CONFIG_FORMAT_H264_CAM1
TUD_VIDEO_CAPTURE_DESCRIPTOR_H264_BULK(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
// Camera 1, single-size H.264 over BULK
TUD_VIDEO_CAPTURE_DESCRIPTOR_H264_BULK(STRID_UVC_CAM1, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
UVC_CAM1_FRAME_WIDTH, UVC_CAM1_FRAME_HEIGHT, UVC_CAM1_FRAME_RATE,
CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
#else
TUD_VIDEO_CAPTURE_DESCRIPTOR_UNCOMPR_BULK(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
// Camera 1, single-size Uncompressed (YUY2/etc) over BULK
TUD_VIDEO_CAPTURE_DESCRIPTOR_UNCOMPR_BULK(STRID_UVC_CAM1, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
UVC_CAM1_FRAME_WIDTH, UVC_CAM1_FRAME_HEIGHT, UVC_CAM1_FRAME_RATE,
CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
#endif
@@ -178,75 +152,32 @@ uint8_t const desc_fs_configuration[] = {
#else
#if CONFIG_UVC_CAM1_MULTI_FRAMESIZE
#if CONFIG_FORMAT_MJPEG_CAM1
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_MJPEG(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN, CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
// Camera 1, multi-size MJPEG over ISO
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_MJPEG(STRID_UVC_CAM1, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN, CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
#elif CONFIG_FORMAT_H264_CAM1
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_H264(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN, CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
// Camera 1, multi-size H.264 over ISO
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_H264(STRID_UVC_CAM1, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN, CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
#endif
#else
#if CONFIG_FORMAT_MJPEG_CAM1
TUD_VIDEO_CAPTURE_DESCRIPTOR_MJPEG(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
// Camera 1, single-size MJPEG over ISO
TUD_VIDEO_CAPTURE_DESCRIPTOR_MJPEG(STRID_UVC_CAM1, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
UVC_CAM1_FRAME_WIDTH, UVC_CAM1_FRAME_HEIGHT, UVC_CAM1_FRAME_RATE,
CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
#elif CONFIG_FORMAT_H264_CAM1
TUD_VIDEO_CAPTURE_DESCRIPTOR_H264(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
// Camera 1, single-size H.264 over ISO
TUD_VIDEO_CAPTURE_DESCRIPTOR_H264(STRID_UVC_CAM1, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
UVC_CAM1_FRAME_WIDTH, UVC_CAM1_FRAME_HEIGHT, UVC_CAM1_FRAME_RATE,
CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
#else
TUD_VIDEO_CAPTURE_DESCRIPTOR_UNCOMPR(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
// Camera 1, single-size Uncompressed over ISO
TUD_VIDEO_CAPTURE_DESCRIPTOR_UNCOMPR(STRID_UVC_CAM1, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM1_VIDEO_IN,
UVC_CAM1_FRAME_WIDTH, UVC_CAM1_FRAME_HEIGHT, UVC_CAM1_FRAME_RATE,
CFG_TUD_CAM1_VIDEO_STREAMING_EP_BUFSIZE),
#endif
#endif // CONFIG_UVC_CAM1_MULTI_FRAMESIZE
#endif // CFG_TUD_CAM1_VIDEO_STREAMING_BULK
#if CONFIG_UVC_SUPPORT_TWO_CAM
#if CFG_TUD_CAM2_VIDEO_STREAMING_BULK
#if CONFIG_UVC_CAM2_MULTI_FRAMESIZE
#if CONFIG_FORMAT_MJPEG_CAM2
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_MJPEG_BULK(4, ITF_NUM_VIDEO_CONTROL_2, EPNUM_CAM2_VIDEO_IN, CFG_TUD_CAM2_VIDEO_STREAMING_EP_BUFSIZE),
#elif CONFIG_FORMAT_H264_CAM2
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_H264_BULK(4, ITF_NUM_VIDEO_CONTROL_2, EPNUM_CAM2_VIDEO_IN, CFG_TUD_CAM2_VIDEO_STREAMING_EP_BUFSIZE),
#endif
#else
#if CONFIG_FORMAT_MJPEG_CAM2
TUD_VIDEO_CAPTURE_DESCRIPTOR_MJPEG_BULK(4, ITF_NUM_VIDEO_CONTROL_2, EPNUM_CAM2_VIDEO_IN,
UVC_CAM2_FRAME_WIDTH, UVC_CAM2_FRAME_HEIGHT, UVC_CAM2_FRAME_RATE,
CFG_TUD_CAM2_VIDEO_STREAMING_EP_BUFSIZE),
#elif CONFIG_FORMAT_H264_CAM2
TUD_VIDEO_CAPTURE_DESCRIPTOR_H264_BULK(4, ITF_NUM_VIDEO_CONTROL_2, EPNUM_CAM2_VIDEO_IN,
UVC_CAM2_FRAME_WIDTH, UVC_CAM2_FRAME_HEIGHT, UVC_CAM2_FRAME_RATE,
CFG_TUD_CAM2_VIDEO_STREAMING_EP_BUFSIZE),
#else
TUD_VIDEO_CAPTURE_DESCRIPTOR_UNCOMPR_BULK(4, ITF_NUM_VIDEO_CONTROL_2, EPNUM_CAM2_VIDEO_IN,
UVC_CAM2_FRAME_WIDTH, UVC_CAM2_FRAME_HEIGHT, UVC_CAM2_FRAME_RATE,
CFG_TUD_CAM2_VIDEO_STREAMING_EP_BUFSIZE),
#endif
#endif // CONFIG_UVC_CAM2_MULTI_FRAMESIZE
#else
#if CONFIG_UVC_CAM2_MULTI_FRAMESIZE
#if CONFIG_FORMAT_MJPEG_CAM2
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_MJPEG(4, ITF_NUM_VIDEO_CONTROL_2, EPNUM_CAM2_VIDEO_IN, CFG_TUD_CAM2_VIDEO_STREAMING_EP_BUFSIZE),
#elif CONFIG_FORMAT_H264_CAM2
TUD_VIDEO_CAPTURE_DESCRIPTOR_MULTI_H264(4, ITF_NUM_VIDEO_CONTROL_2, EPNUM_CAM2_VIDEO_IN, CFG_TUD_CAM2_VIDEO_STREAMING_EP_BUFSIZE),
#endif
#else
#if CONFIG_FORMAT_MJPEG_CAM2
TUD_VIDEO_CAPTURE_DESCRIPTOR_MJPEG(4, ITF_NUM_VIDEO_CONTROL_2, EPNUM_CAM2_VIDEO_IN,
UVC_CAM2_FRAME_WIDTH, UVC_CAM2_FRAME_HEIGHT, UVC_CAM2_FRAME_RATE,
CFG_TUD_CAM2_VIDEO_STREAMING_EP_BUFSIZE),
#elif CONFIG_FORMAT_H264_CAM2
TUD_VIDEO_CAPTURE_DESCRIPTOR_H264(4, ITF_NUM_VIDEO_CONTROL_2, EPNUM_CAM2_VIDEO_IN,
UVC_CAM2_FRAME_WIDTH, UVC_CAM2_FRAME_HEIGHT, UVC_CAM2_FRAME_RATE,
CFG_TUD_CAM2_VIDEO_STREAMING_EP_BUFSIZE),
#else
TUD_VIDEO_CAPTURE_DESCRIPTOR_UNCOMPR(4, ITF_NUM_VIDEO_CONTROL, EPNUM_CAM2_VIDEO_IN,
UVC_CAM2_FRAME_WIDTH, UVC_CAM2_FRAME_HEIGHT, UVC_CAM2_FRAME_RATE,
CFG_TUD_CAM2_VIDEO_STREAMING_EP_BUFSIZE),
#endif
#endif // CONFIG_UVC_CAM2_MULTI_FRAMESIZE
#endif // CFG_TUD_CAM2_VIDEO_STREAMING_BULK
#endif
};
// Invoked when received GET CONFIGURATION DESCRIPTOR
@@ -263,30 +194,26 @@ uint8_t const *tud_descriptor_configuration_cb(uint8_t index)
// String Descriptors
//--------------------------------------------------------------------+
// array of pointer to string descriptors
// Array of pointers to string literals. Indices must match STRID_* above.
char const *string_desc_arr[] = {
(const char[]){0x09, 0x04}, // 0: is supported language is English (0x0409)
(const char[]){0x09, 0x04}, // 0: Supported language: English (0x0409)
CONFIG_TUSB_MANUFACTURER, // 1: Manufacturer
CONFIG_TUSB_PRODUCT, // 2: Product
CONFIG_TUSB_SERIAL_NUM, // 3: Serials, should use chip ID, overridden with get_serial_number()
"UVC CAM1", // 4: UVC Interface, default, because we're overriding it get_uvc_device_name(), but we still have to keep the structure
#if CONFIG_UVC_SUPPORT_TWO_CAM
"UVC CAM2", // 5: UVC Interface
#endif
CONFIG_TUSB_SERIAL_NUM, // 3: Serial (overridden by get_serial_number())
"UVC CAM1", // 4: UVC Interface name for Cam1 (overridden by get_uvc_device_name())
};
static uint16_t _desc_str[32];
__attribute__((weak)) const char *get_uvc_device_name(void)
{
// ETVR Override, by default we're reporting ourselves as this, users can override it
// Default UVC device name, can be overridden by application
return "UVC OpenIris Camera";
}
__attribute__((weak)) const char *get_serial_number(void)
{
// ETVR Override, by default we're reporting ourselves as the predefined serial number
// this should get overwritten with a better implementation
// Default serial number, can be overridden by application (e.g., chip ID)
return CONFIG_TUSB_SERIAL_NUM;
}
@@ -294,7 +221,6 @@ __attribute__((weak)) const char *get_serial_number(void)
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid)
{
printf("am I being asked for this?");
(void)langid;
uint8_t chr_count;
@@ -309,15 +235,16 @@ uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid)
// Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors.
// https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors
if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0])))
if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0])))
{
return NULL;
}
const char *str = string_desc_arr[index];
if (index == 3)
// Allow dynamic overrides for specific indices
if (index == STRID_SERIAL)
str = get_serial_number();
if (index == 4)
if (index == STRID_UVC_CAM1)
str = get_uvc_device_name();
if (str == NULL)
str = string_desc_arr[index];
@@ -340,4 +267,4 @@ uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid)
_desc_str[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));
return _desc_str;
}
}

View File

@@ -6,9 +6,13 @@ endmenu
menu "OpenIris: General Configuration"
config GENERAL_WIRED_MODE
config GENERAL_DEFAULT_WIRED_MODE
bool "Wired mode"
default false
help
Enables UVC (wired) support in the firmware. When enabled, the
default device streaming mode will be UVC unless overridden by a
saved preference. When disabled, the default mode is AUTO.
config GENERAL_UVC_DELAY
int "UVC delay (s)"

View File

@@ -22,7 +22,7 @@
#include <RestAPI.hpp>
#include <main_globals.hpp>
#ifdef CONFIG_GENERAL_WIRED_MODE
#ifdef CONFIG_GENERAL_DEFAULT_WIRED_MODE
#include <UVCStream.hpp>
#endif
@@ -49,11 +49,11 @@ StreamServer streamServer(80, stateManager);
auto *restAPI = new RestAPI("http://0.0.0.0:81", commandManager);
#ifdef CONFIG_GENERAL_WIRED_MODE
#ifdef CONFIG_GENERAL_DEFAULT_WIRED_MODE
UVCStreamManager uvcStream;
#endif
auto *ledManager = new LEDManager(BLINK_GPIO, CONFIG_LED_C_PIN_GPIO, ledStateQueue);
auto ledManager = std::make_shared<LEDManager>(BLINK_GPIO, CONFIG_LED_C_PIN_GPIO, ledStateQueue, deviceConfig);
auto *serialManager = new SerialManager(commandManager, &timerHandle, deviceConfig);
static void initNVSStorage()
@@ -95,9 +95,21 @@ void start_video_streaming(void *arg)
if (deviceMode == StreamingMode::UVC)
{
#ifdef CONFIG_GENERAL_WIRED_MODE
#ifdef CONFIG_GENERAL_DEFAULT_WIRED_MODE
ESP_LOGI("[MAIN]", "Starting UVC streaming mode.");
ESP_LOGI("[MAIN]", "Initializing UVC hardware...");
// If we were given the Serial task handle, stop the task and uninstall the driver
if (arg != nullptr)
{
const auto serialTaskHandle = static_cast<TaskHandle_t>(arg);
vTaskDelete(serialTaskHandle);
ESP_LOGI("[MAIN]", "Serial task deleted before UVC init");
serialManager->shutdown();
ESP_LOGI("[MAIN]", "Serial driver uninstalled");
// Leave a small gap for the host to see COM disappear
vTaskDelay(pdMS_TO_TICKS(200));
setUsbHandoverDone(true);
}
esp_err_t ret = uvcStream.setup();
if (ret != ESP_OK)
{
@@ -105,8 +117,10 @@ void start_video_streaming(void *arg)
return;
}
uvcStream.start();
ESP_LOGI("[MAIN]", "UVC streaming started");
return; // UVC path complete, do not fall through to WiFi
#else
ESP_LOGE("[MAIN]", "UVC mode selected but the board likely does not support it.");
ESP_LOGE("[MAIN]", "UVC mode selected but the board likely does not support it.");
ESP_LOGI("[MAIN]", "Falling back to WiFi mode if credentials available");
deviceMode = StreamingMode::WIFI;
#endif
@@ -193,12 +207,12 @@ void startup_timer_callback(void *arg)
}
else if (deviceMode == StreamingMode::UVC)
{
#ifdef CONFIG_GENERAL_WIRED_MODE
#ifdef CONFIG_GENERAL_DEFAULT_WIRED_MODE
ESP_LOGI("[MAIN]", "Starting UVC streaming automatically");
activate_streaming(serialTaskHandle);
#else
ESP_LOGE("[MAIN]", "UVC mode selected but CONFIG_GENERAL_WIRED_MODE not enabled in build!");
ESP_LOGI("[MAIN]", "Device will stay in setup mode. Enable CONFIG_GENERAL_WIRED_MODE and rebuild.");
ESP_LOGE("[MAIN]", "UVC mode selected but CONFIG_GENERAL_DEFAULT_WIRED_MODE not enabled in build!");
ESP_LOGI("[MAIN]", "Device will stay in setup mode. Enable CONFIG_GENERAL_DEFAULT_WIRED_MODE and rebuild.");
#endif
}
else
@@ -228,6 +242,7 @@ extern "C" void app_main(void)
dependencyRegistry->registerService<ProjectConfig>(DependencyType::project_config, deviceConfig);
dependencyRegistry->registerService<CameraManager>(DependencyType::camera_manager, cameraHandler);
dependencyRegistry->registerService<WiFiManager>(DependencyType::wifi_manager, wifiManager);
dependencyRegistry->registerService<LEDManager>(DependencyType::led_manager, ledManager);
// uvc plan
// cleanup the logs - done
// prepare the camera to be initialized with UVC - done?
@@ -274,10 +289,10 @@ extern "C" void app_main(void)
// setup CI and building for other boards
// finish todos, overhaul stuff a bit
// esp_log_set_vprintf(&websocket_logger);
Logo::printASCII();
initNVSStorage();
// esp_log_set_vprintf(&websocket_logger);
deviceConfig->load();
ledManager->setup();
xTaskCreate(
@@ -293,11 +308,10 @@ extern "C" void app_main(void)
HandleLEDDisplayTask,
"HandleLEDDisplayTask",
1024 * 2,
ledManager,
ledManager.get(),
3,
nullptr);
deviceConfig->load();
serialManager->setup();
static TaskHandle_t serialManagerHandle = nullptr;
@@ -308,8 +322,7 @@ extern "C" void app_main(void)
1024 * 6,
serialManager,
1, // we only rely on the serial manager during provisioning, we can run it slower
&serialManagerHandle
);
&serialManagerHandle);
wifiManager->Begin();
mdnsManager.start();

View File

@@ -375,7 +375,7 @@ CONFIG_IDF_TOOLCHAIN_GCC=y
CONFIG_IDF_TARGET_ARCH_XTENSA=y
CONFIG_IDF_TARGET_ARCH="xtensa"
CONFIG_IDF_TARGET="esp32s3"
CONFIG_IDF_INIT_VERSION="$IDF_INIT_VERSION"
CONFIG_IDF_INIT_VERSION="5.4.2"
CONFIG_IDF_TARGET_ESP32S3=y
CONFIG_IDF_FIRMWARE_CHIP_ID=0x0009
@@ -570,7 +570,7 @@ CONFIG_ENV_GPIO_OUT_RANGE_MAX=48
#
# OpenIris: General Configuration
#
CONFIG_GENERAL_WIRED_MODE=y
# CONFIG_GENERAL_DEFAULT_WIRED_MODE is not set
CONFIG_GENERAL_UVC_DELAY=30
# end of OpenIris: General Configuration
@@ -2242,7 +2242,7 @@ CONFIG_FRAMESIZE_QVGA=y
# CONFIG_FRAMESIZE_SVGA is not set
# CONFIG_FRAMESIZE_HD is not set
# CONFIG_FRAMESIZE_FHD is not set
CONFIG_UVC_CAM1_FRAMERATE=90
CONFIG_UVC_CAM1_FRAMERATE=60
CONFIG_UVC_CAM1_FRAMESIZE_WIDTH=240
CONFIG_UVC_CAM1_FRAMESIZE_HEIGT=240
CONFIG_UVC_CAM1_MULTI_FRAMESIZE=y
@@ -2257,7 +2257,7 @@ CONFIG_UVC_CAM1_MULTI_FRAMESIZE=y
#
CONFIG_UVC_MULTI_FRAME_WIDTH_1=240
CONFIG_UVC_MULTI_FRAME_HEIGHT_1=240
CONFIG_UVC_MULTI_FRAME_FPS_1=90
CONFIG_UVC_MULTI_FRAME_FPS_1=60
# end of FRAME_SIZE_1
#
@@ -2265,7 +2265,7 @@ CONFIG_UVC_MULTI_FRAME_FPS_1=90
#
CONFIG_UVC_MULTI_FRAME_WIDTH_2=240
CONFIG_UVC_MULTI_FRAME_HEIGHT_2=240
CONFIG_UVC_MULTI_FRAME_FPS_2=90
CONFIG_UVC_MULTI_FRAME_FPS_2=60
# end of FRAME_SIZE_2
#
@@ -2273,7 +2273,7 @@ CONFIG_UVC_MULTI_FRAME_FPS_2=90
#
CONFIG_UVC_MULTI_FRAME_WIDTH_3=240
CONFIG_UVC_MULTI_FRAME_HEIGHT_3=240
CONFIG_UVC_MULTI_FRAME_FPS_3=90
CONFIG_UVC_MULTI_FRAME_FPS_3=60
# end of FRAME_SIZE_3
# end of UVC_MULTI_FRAME_CONFIG

View File

@@ -570,7 +570,7 @@ CONFIG_ENV_GPIO_OUT_RANGE_MAX=48
#
# OpenIris: General Configuration
#
# CONFIG_GENERAL_WIRED_MODE is not set
# CONFIG_GENERAL_DEFAULT_WIRED_MODE is not set
# CONFIG_GENERAL_UVC_DELAY is not set
# end of OpenIris: General Configuration
@@ -2242,7 +2242,7 @@ CONFIG_FRAMESIZE_QVGA=y
# CONFIG_FRAMESIZE_SVGA is not set
# CONFIG_FRAMESIZE_HD is not set
# CONFIG_FRAMESIZE_FHD is not set
CONFIG_UVC_CAM1_FRAMERATE=90
CONFIG_UVC_CAM1_FRAMERATE=60
CONFIG_UVC_CAM1_FRAMESIZE_WIDTH=240
CONFIG_UVC_CAM1_FRAMESIZE_HEIGT=240
CONFIG_UVC_CAM1_MULTI_FRAMESIZE=y
@@ -2257,7 +2257,7 @@ CONFIG_UVC_CAM1_MULTI_FRAMESIZE=y
#
CONFIG_UVC_MULTI_FRAME_WIDTH_1=240
CONFIG_UVC_MULTI_FRAME_HEIGHT_1=240
CONFIG_UVC_MULTI_FRAME_FPS_1=90
CONFIG_UVC_MULTI_FRAME_FPS_1=60
# end of FRAME_SIZE_1
#
@@ -2265,7 +2265,7 @@ CONFIG_UVC_MULTI_FRAME_FPS_1=90
#
CONFIG_UVC_MULTI_FRAME_WIDTH_2=240
CONFIG_UVC_MULTI_FRAME_HEIGHT_2=240
CONFIG_UVC_MULTI_FRAME_FPS_2=90
CONFIG_UVC_MULTI_FRAME_FPS_2=60
# end of FRAME_SIZE_2
#
@@ -2273,7 +2273,7 @@ CONFIG_UVC_MULTI_FRAME_FPS_2=90
#
CONFIG_UVC_MULTI_FRAME_WIDTH_3=240
CONFIG_UVC_MULTI_FRAME_HEIGHT_3=240
CONFIG_UVC_MULTI_FRAME_FPS_3=90
CONFIG_UVC_MULTI_FRAME_FPS_3=60
# end of FRAME_SIZE_3
# end of UVC_MULTI_FRAME_CONFIG

View File

@@ -59,4 +59,4 @@ CONFIG_LED_EXTERNAL_GPIO=9
CONFIG_LED_EXTERNAL_PWM_FREQ=20000
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=50
CONFIG_CAMERA_USB_XCLK_FREQ=23000000
CONFIG_GENERAL_WIRED_MODE=y
CONFIG_GENERAL_DEFAULT_WIRED_MODE=y

View File

@@ -59,4 +59,4 @@ CONFIG_LED_EXTERNAL_GPIO=9
CONFIG_LED_EXTERNAL_PWM_FREQ=20000
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=100
CONFIG_CAMERA_USB_XCLK_FREQ=23000000
CONFIG_GENERAL_WIRED_MODE=y
CONFIG_GENERAL_DEFAULT_WIRED_MODE=y

View File

@@ -50,5 +50,5 @@ CONFIG_LED_EXTERNAL_CONTROL=y
CONFIG_LED_EXTERNAL_PWM_FREQ=5000
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=100
CONFIG_LED_EXTERNAL_GPIO=1
CONFIG_CAMERA_USB_XCLK_FREQ=23000000 # NOT TESTED
CONFIG_GENERAL_WIRED_MODE=y
CONFIG_CAMERA_USB_XCLK_FREQ=23000000
# CONFIG_GENERAL_DEFAULT_WIRED_MODE is not set

View File

@@ -56,4 +56,4 @@ CONFIG_SPIRAM_SPEED=80
CONFIG_SPIRAM_SPEED_80M=y
# CONFIG_LED_EXTERNAL_CONTROL is not set
CONFIG_CAMERA_USB_XCLK_FREQ=23000000
CONFIG_GENERAL_WIRED_MODE=y
# CONFIG_GENERAL_DEFAULT_WIRED_MODE is not set

View File

@@ -410,6 +410,55 @@ class OpenIrisDevice:
print(f"❌ Failed to parse mode response: {e}")
return "unknown"
def set_led_duty_cycle(self, duty_cycle):
"""Sets the PWN duty cycle of the LED"""
print(f"🌟 Setting LED duty cycle to {duty_cycle}%...")
response = self.send_command("set_led_duty_cycle", {"dutyCycle": duty_cycle})
if "error" in response:
print(f"❌ Failed to set LED duty cycle: {response['error']}")
return False
print("✅ LED duty cycle set successfully")
return True
def get_led_duty_cycle(self) -> Optional[int]:
"""Get the current LED PWM duty cycle from the device"""
response = self.send_command("get_led_duty_cycle")
if "error" in response:
print(f"❌ Failed to get LED duty cycle: {response['error']}")
return None
try:
results = response.get("results", [])
if results:
result_data = json.loads(results[0])
payload = result_data["result"]
if isinstance(payload, str):
payload = json.loads(payload)
return int(payload.get("led_external_pwm_duty_cycle"))
except Exception as e:
print(f"❌ Failed to parse LED duty cycle: {e}")
return None
def get_serial_info(self) -> Optional[Tuple[str, str]]:
"""Get device serial number and MAC address"""
response = self.send_command("get_serial")
if "error" in response:
print(f"❌ Failed to get serial/MAC: {response['error']}")
return None
try:
results = response.get("results", [])
if results:
result_data = json.loads(results[0])
payload = result_data["result"]
if isinstance(payload, str):
payload = json.loads(payload)
serial = payload.get("serial")
mac = payload.get("mac")
return serial, mac
except Exception as e:
print(f"❌ Failed to parse serial/MAC: {e}")
return None
def monitor_logs(self):
"""Monitor device logs until interrupted"""
print("📋 Monitoring device logs (Press Ctrl+C to exit)...")
@@ -631,9 +680,9 @@ def configure_wifi(device: OpenIrisDevice, args = None):
if device.set_wifi(selected_network.ssid, password):
print("✅ WiFi configured successfully!")
print("💡 Next steps:")
print(" 4. Check WiFi status")
print(" 5. Connect to WiFi (if needed)")
print(" 6. Start streaming when connected")
print(" • Open WiFi menu to connect to WiFi (if needed)")
print(" • Open WiFi menu to check WiFi status")
print(" Start streaming from the main menu when connected")
break
else:
print("❌ Invalid network number")
@@ -708,12 +757,127 @@ def check_wifi_status(device: OpenIrisDevice, args = None):
def attempt_wifi_connection(device: OpenIrisDevice, args = None):
device.connect_wifi()
print("🕰️ Wait a few seconds then check status (option 4)")
print("🕰️ Wait a few seconds then check status in the WiFi menu")
def start_streaming(device: OpenIrisDevice, args = None):
device.start_streaming()
print("🚀 Streaming started! Use option 8 to monitor logs.")
print("🚀 Streaming started! Use 'Monitor logs' from the main menu.")
# ----- WiFi submenu -----
def wifi_auto_setup(device: OpenIrisDevice, args=None):
print("\n⚙️ Automatic WiFi setup starting...")
scan_timeout = getattr(args, "scan_timeout", 30) if args else 30
# 1) Scan
if not device.scan_networks(timeout=scan_timeout):
print("❌ Auto-setup aborted: no networks found or scan failed")
return
# 2) Show networks (sorted strongest-first already)
display_networks(device)
# 3) Select a network (default strongest)
choice = input("Select network number [default: 1] or 'back': ").strip()
if choice.lower() == "back":
return
try:
idx = int(choice) - 1 if choice else 0
except ValueError:
idx = 0
sorted_networks = sorted(device.networks, key=lambda x: x.rssi, reverse=True)
if not (0 <= idx < len(sorted_networks)):
print("⚠️ Invalid selection, using strongest network")
idx = 0
selected = sorted_networks[idx]
print(f"\n🔐 Selected: {selected.ssid if selected.ssid else '<hidden>'}")
if selected.auth_mode == 0:
password = ""
print("🔓 Open network - no password required")
else:
password = input("Enter WiFi password: ")
# 4) Configure WiFi
if not device.set_wifi(selected.ssid, password):
print("❌ Auto-setup aborted: failed to configure WiFi")
return
# 5) Connect
if not device.connect_wifi():
print("❌ Auto-setup aborted: failed to start WiFi connection")
return
# 6) Wait for IP / connected status
print("⏳ Connecting to WiFi, waiting for IP...")
start = time.time()
timeout_s = 30
ip = None
last_status = None
while time.time() - start < timeout_s:
status = device.get_wifi_status()
last_status = status
ip = (status or {}).get("ip_address")
if ip and ip not in ("0.0.0.0", "", None):
break
time.sleep(0.5)
if ip and ip not in ("0.0.0.0", "", None):
print(f"✅ Connected! IP Address: {ip}")
else:
print("⚠️ Connection not confirmed within timeout")
if last_status:
print(f" Status: {last_status.get('status', 'unknown')} | IP: {last_status.get('ip_address', '-')}")
def wifi_menu(device: OpenIrisDevice, args=None):
while True:
print("\n📶 WiFi Settings:")
print(f"{str(1):>2} ⚙️ Automatic WiFi setup")
print(f"{str(2):>2} 📁 Manual WiFi actions")
print("back Back")
choice = input("\nSelect option (1-2 or 'back'): ").strip()
if choice.lower() == "back":
break
if choice == "1":
wifi_auto_setup(device, args)
elif choice == "2":
wifi_manual_menu(device, args)
else:
print("❌ Invalid option")
def wifi_manual_menu(device: OpenIrisDevice, args=None):
while True:
print("\n📁 WiFi Manual Actions:")
print(f"{str(1):>2} 🔍 Scan for WiFi networks")
print(f"{str(2):>2} 📡 Show available networks")
print(f"{str(3):>2} 🔐 Configure WiFi")
print(f"{str(4):>2} 🔗 Connect to WiFi")
print(f"{str(5):>2} 🛰️ Check WiFi status")
print("back Back")
choice = input("\nSelect option (1-5 or 'back'): ").strip()
if choice.lower() == "back":
break
sub_map = {
"1": scan_networks,
"2": display_networks,
"3": configure_wifi,
"4": attempt_wifi_connection,
"5": check_wifi_status,
}
handler = sub_map.get(choice)
if not handler:
print("❌ Invalid option")
continue
handler(device, args)
def switch_device_mode(device: OpenIrisDevice, args = None):
@@ -736,21 +900,143 @@ def switch_device_mode(device: OpenIrisDevice, args = None):
print("❌ Invalid mode selection")
def set_led_duty_cycle(device: OpenIrisDevice, args=None):
# Show current duty cycle on entry
current = device.get_led_duty_cycle()
if current is not None:
print(f"💡 Current LED duty cycle: {current}%")
while True:
input_data = input("Enter LED external PWM duty cycle (0-100) or `back` to exit: \n")
if input_data.lower() == "back":
break
try:
duty_cycle = int(input_data)
except ValueError:
print("❌ Invalid input. Please enter a number between 0 and 100.")
if duty_cycle < 0 or duty_cycle > 100:
print("❌ Duty cycle must be between 0 and 100.")
else:
# Apply immediately; stay in loop for further tweaks
if device.set_led_duty_cycle(duty_cycle):
# Read back and display current value using existing getter
updated = device.get_led_duty_cycle()
if updated is not None:
print(f"💡 Current LED duty cycle: {updated}%")
else:
print(" Duty cycle updated, but current value could not be read back.")
def monitor_logs(device: OpenIrisDevice, args = None):
device.monitor_logs()
def get_led_duty_cycle(device: OpenIrisDevice, args=None):
duty = device.get_led_duty_cycle()
if duty is not None:
print(f"💡 Current LED duty cycle: {duty}%")
def get_serial(device: OpenIrisDevice, args=None):
info = device.get_serial_info()
if info is not None:
serial, mac = info
# print(f"🔑 Serial: {serial}")
print(f"🔗 MAC: {mac}")
# ----- Aggregated GET: settings summary -----
def _probe_serial(device: OpenIrisDevice) -> Dict:
info = device.get_serial_info()
if info is None:
return {"serial": None, "mac": None}
serial, mac = info
return {"serial": serial, "mac": mac}
def _probe_led_pwm(device: OpenIrisDevice) -> Dict:
duty = device.get_led_duty_cycle()
return {"led_external_pwm_duty_cycle": duty}
def _probe_mode(device: OpenIrisDevice) -> Dict:
mode = device.get_device_mode()
return {"mode": mode}
def _probe_wifi_status(device: OpenIrisDevice) -> Dict:
# Returns dict as provided by device; pass through
status = device.get_wifi_status() or {}
return {"wifi_status": status}
def get_settings(device: OpenIrisDevice, args=None):
print("\n🧩 Collecting device settings...\n")
probes = [
("Identity", _probe_serial),
("LED", _probe_led_pwm),
("Mode", _probe_mode),
("WiFi", _probe_wifi_status),
]
summary: Dict[str, Dict] = {}
for label, probe in probes:
try:
data = probe(device)
summary[label] = data
except Exception as e:
summary[label] = {"error": str(e)}
# Pretty print summary
# Identity
ident = summary.get("Identity", {})
serial = ident.get("serial")
mac = ident.get("mac")
if serial:
print(f"🔑 Serial: {serial}")
# if mac:
# print(f"🔗 MAC: {mac}")
if not serial and not mac:
print("🔑 Serial/MAC: unavailable")
# LED
led = summary.get("LED", {})
duty = led.get("led_external_pwm_duty_cycle")
if duty is not None:
print(f"💡 LED PWM Duty: {duty}%")
else:
print("💡 LED PWM Duty: unknown")
# Mode
mode = summary.get("Mode", {}).get("mode")
print(f"🎚️ Mode: {mode if mode else 'unknown'}")
# WiFi
wifi = summary.get("WiFi", {}).get("wifi_status", {})
if wifi:
status = wifi.get("status", "unknown")
ip = wifi.get("ip_address") or "-"
configured = wifi.get("networks_configured", 0)
print(f"📶 WiFi: {status} | IP: {ip} | Networks configured: {configured}")
else:
print("📶 WiFi: status unavailable")
print("")
COMMANDS_MAP = {
"1": scan_networks,
"2": display_networks,
"3": configure_wifi,
"4": configure_mdns,
"5": configure_mdns,
"6": check_wifi_status,
"7": attempt_wifi_connection,
"8": start_streaming,
"9": switch_device_mode,
"10": monitor_logs,
"1": wifi_menu,
"2": configure_mdns,
"3": configure_mdns,
"4": start_streaming,
"5": switch_device_mode,
"6": set_led_duty_cycle,
"7": monitor_logs,
"8": get_settings,
}
@@ -839,18 +1125,16 @@ def main():
# Main interaction loop
while True:
print("\n🔧 Setup Options:")
print("1. 🔍 Scan for WiFi networks")
print("2. 📡 Show available networks")
print("3. 🔐 Configure WiFi")
print("4. 🌐 Configure MDNS")
print("5. 💻 Configure UVC Name")
print("6. 📶 Check WiFi status")
print("7. 🔗 Connect to WiFi")
print("8. 🚀 Start streaming mode")
print("9. 🔄 Switch device mode (WiFi/UVC/Auto)")
print("10. 📋 Monitor logs")
print("exit. 🚪 Exit")
choice = input("\nSelect option (1-10): ").strip()
print(f"{str(1):>2} 📶 WiFi settings")
print(f"{str(2):>2} 🌐 Configure MDNS")
print(f"{str(3):>2} 💻 Configure UVC Name")
print(f"{str(4):>2} 🚀 Start streaming mode")
print(f"{str(5):>2} 🔄 Switch device mode (WiFi/UVC/Auto)")
print(f"{str(6):>2} 💡 Update PWM Duty Cycle")
print(f"{str(7):>2} 📖 Monitor logs")
print(f"{str(8):>2} 🧩 Get settings summary")
print("exit 🚪 Exit")
choice = input("\nSelect option (1-8): ").strip()
if choice == "exit":
break