Merge pull request #13 from PhosphorosVR/main
Current monitoring / External LED error mirroring / QoL improvements
105
README.md
@@ -1,5 +1,5 @@
|
||||
| Supported Targets | ESP32-S3 |
|
||||
| ----------------- | -------- |
|
||||
| Supported Targets | ESP32-S3 · Project Babble · FaceFocusVR |
|
||||
| ----------------- | --------------------------------------- |
|
||||
|
||||
## OpenIris-ESPIDF
|
||||
|
||||
@@ -12,6 +12,12 @@ Firmware and tools for OpenIris — Wi‑Fi, UVC streaming, and a Python setup C
|
||||
- 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 Wi‑Fi, MDNS/Name, Mode, LED PWM, Logs, and a Settings Summary
|
||||
- Composite USB (UVC + CDC) when UVC mode is enabled (`GENERAL_INCLUDE_UVC_MODE`) for simultaneous video streaming and command channel
|
||||
- LED current monitoring (if enabled via `MONITORING_LED_CURRENT`) with filtered mA readings
|
||||
- Configurable debug LED + external IR LED control with optional error mirroring (`LED_DEBUG_ENABLE`, `LED_EXTERNAL_AS_DEBUG`)
|
||||
- Auto‑discovered per‑board configuration overlays under `boards/`
|
||||
- Command framework (JSON over serial / CDC / REST) for mode switching, Wi‑Fi config, OTA credentials, LED brightness, info & monitoring
|
||||
- Single source advertised name (`CONFIG_GENERAL_ADVERTISED_NAME`) used for both UVC device name and mDNS hostname (unless overridden at runtime)
|
||||
|
||||
---
|
||||
|
||||
@@ -50,20 +56,29 @@ After this, you’re ready for the Quick start below.
|
||||
## Quick start
|
||||
|
||||
### 1) Pick your board (loads the default configuration)
|
||||
Boards are auto‑discovered from the `boards/` directory. First list them, then pick one:
|
||||
|
||||
Windows (cmd):
|
||||
```cmd
|
||||
python .\tools\switchBoardType.py --board xiao-esp32s3 --diff
|
||||
python .\tools\switchBoardType.py --list
|
||||
python .\tools\switchBoardType.py --board seed_studio_xiao_esp32s3 --diff
|
||||
```
|
||||
macOS/Linux (bash):
|
||||
```bash
|
||||
python3 ./tools/switchBoardType.py --board xiao-esp32s3 --diff
|
||||
python3 ./tools/switchBoardType.py --list
|
||||
python3 ./tools/switchBoardType.py --board seed_studio_xiao_esp32s3 --diff
|
||||
```
|
||||
- Set `--board` to your target board
|
||||
- `--diff` shows what changed in the config
|
||||
Notes:
|
||||
- Use `--list` to see all detected board keys.
|
||||
- Board key = relative path under `boards/` with `/` replaced by `_` (and duplicate tail segments collapsed, e.g. `project_babble/project_babble` -> `project_babble`).
|
||||
- `--diff` shows what will change vs the current `sdkconfig`.
|
||||
- You can also pass partial or path‑like inputs (e.g. `facefocusvr/eye_L`), the tool normalizes them.
|
||||
|
||||
### 2) Build & flash
|
||||
- Set the target (e.g., ESP32‑S3).
|
||||
- Build, flash, and open the serial monitor.
|
||||
- (Optional) For UVC mode ensure `GENERAL_INCLUDE_UVC_MODE=y`. If you want device to boot directly into UVC: also set `START_IN_UVC_MODE=y`.
|
||||
- Disable Wi‑Fi services for pure wired builds: `GENERAL_ENABLE_WIRELESS=n`.
|
||||
|
||||
### 3) Use the Python setup CLI (recommended)
|
||||
Configure the device over USB serial.
|
||||
@@ -84,7 +99,7 @@ Examples:
|
||||
What the CLI can do:
|
||||
- Wi‑Fi 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 (Wi‑Fi / UVC / Auto)
|
||||
- Switch mode (Wi‑Fi / UVC / Setup)
|
||||
- Adjust LED PWM
|
||||
- Show a Settings Summary (MAC, Wi‑Fi status, mode, PWM, …)
|
||||
- View logs
|
||||
@@ -96,12 +111,23 @@ What the CLI can do:
|
||||
- The CLI displays the MAC by default (clearer); it’s the value used as the serial number.
|
||||
- The UVC device name is based on the MDNS hostname.
|
||||
|
||||
## Advertised Name (UVC + mDNS)
|
||||
`CONFIG_GENERAL_ADVERTISED_NAME` (Kconfig) defines the base name announced over:
|
||||
- USB UVC descriptor (appears in OS camera list)
|
||||
- mDNS hostname / service name
|
||||
|
||||
Runtime override: If the setup CLI (or a JSON command) provides a new device name, that value supersedes the compile-time default until next flash/reset of settings.
|
||||
|
||||
---
|
||||
|
||||
## Common workflows
|
||||
- Fast Wi‑Fi setup: in the CLI, go to “Wi‑Fi 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.
|
||||
- Switch to UVC mode over commands (CDC/serial):
|
||||
`{"commands":[{"command":"switch_mode","data":{"mode":"uvc"}}]}` then reboot.
|
||||
- Read filtered LED current (if enabled):
|
||||
`{"commands":[{"command":"get_led_current"}]}`
|
||||
|
||||
---
|
||||
|
||||
@@ -114,10 +140,73 @@ If you want to dig deeper: commands are mapped via the `CommandManager` under `c
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
### USB Composite (UVC + CDC)
|
||||
When UVC support is compiled in the device enumerates as a composite USB device:
|
||||
- UVC interface: video streaming (JPEG frames)
|
||||
- CDC (virtual COM): command channel accepting newline‑terminated JSON objects
|
||||
|
||||
Example newline‑terminated JSON commands over CDC (one per line):
|
||||
```
|
||||
{"commands":[{"command":"ping"}]}
|
||||
{"commands":[{"command":"get_who_am_i"}]}
|
||||
{"commands":[{"command":"switch_mode","data":{"mode":"wifi"}}]}
|
||||
```
|
||||
|
||||
Chained commands in a single request (processed in order):
|
||||
```
|
||||
{"commands":[
|
||||
{"command":"set_mdns","data":{"hostname":"tracker"}},
|
||||
{"command":"set_wifi","data":{"name":"main","ssid":"your_network","password":"password","channel":0,"power":0}}
|
||||
]}
|
||||
```
|
||||
Responses are JSON blobs flushed immediately.
|
||||
|
||||
---
|
||||
|
||||
### Monitoring (LED Current)
|
||||
Enabled with `MONITORING_LED_CURRENT=y` plus shunt/gain settings. The task samples every `CONFIG_MONITORING_LED_INTERVAL_MS` ms and maintains a filtered moving average over `CONFIG_MONITORING_LED_SAMPLES` samples. Use `get_led_current` command to query.
|
||||
|
||||
### Debug & External LED Configuration
|
||||
| Kconfig | Effect |
|
||||
|---------|--------|
|
||||
| LED_DEBUG_ENABLE | Enables/disables discrete status LED GPIO init & drive |
|
||||
| LED_EXTERNAL_CONTROL | Enables PWM control for IR / external LED |
|
||||
| LED_EXTERNAL_PWM_DUTY_CYCLE | Default duty % applied at boot (0–100) |
|
||||
| LED_EXTERNAL_AS_DEBUG | Mirrors only error patterns onto external LED (0%/50%) when debug LED absent or also for redundancy |
|
||||
|
||||
### Board Profiles
|
||||
Each file under `boards/` overlays `sdkconfig.base_defaults`. The merge order: base → board file → (optional) dynamic Wi‑Fi overrides via `switchBoardType.py` flags. Duplicate trailing segment directories collapse to unique keys.
|
||||
|
||||
- UVC doesn’t appear on the host?
|
||||
- Switch mode to UVC via CLI tool, replug USB and wait 20s.
|
||||
|
||||
### Adding a new board configuration
|
||||
1. Create a new config file under `boards/` (you can nest folders): for example `boards/my_family/my_variant`.
|
||||
2. Populate it with only the `CONFIG_...` lines that differ from the shared defaults. Shared baseline lives in `boards/sdkconfig.base_defaults` and is always merged first.
|
||||
3. The board key the script accepts will be the relative path with `/` turned into `_` (example: `boards/my_family/my_variant` -> `my_family_my_variant`).
|
||||
4. Run `python tools/switchBoardType.py --list` to verify it’s detected, then switch using `-b my_family_my_variant`.
|
||||
5. If you accidentally create two files that collapse to the same key the last one found wins—rename to keep keys unique.
|
||||
|
||||
Tips:
|
||||
- Use `--diff` after adding a board to sanity‑check only the intended keys change.
|
||||
- For Wi‑Fi overrides on first flash: add none—pass `--ssid` / `--password` when switching if needed.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
### LED Status / Error Patterns
|
||||
The firmware uses a small set of LED patterns to indicate status and blocking errors. When `LED_DEBUG_ENABLE` is disabled and `LED_EXTERNAL_AS_DEBUG` is enabled the external IR LED mirrors ONLY error patterns (0%/50% duty). Non‑error patterns are not mirrored.
|
||||
|
||||
| State | Visual | Category | Timing Pattern (ms) | Meaning |
|
||||
|-------|--------|----------|---------------------|---------|
|
||||
| LedStateNone |  | idle | (off) | No activity / heartbeat window waiting |
|
||||
| LedStateStreaming |  | active | steady on | Streaming running (UVC or Wi‑Fi) |
|
||||
| LedStateStoppedStreaming |  | inactive | steady off | Streaming intentionally stopped |
|
||||
| CameraError |  | error | 300/300 300/700 (loop) | Camera init/runtime failure (check sensor, ribbon, power) |
|
||||
| WiFiStateConnecting |  | transitional | 400/400 (loop) | Wi‑Fi associating / DHCP pending |
|
||||
| WiFiStateConnected |  | notification | 150/150×3 then 600 off | Wi‑Fi connected successfully |
|
||||
| WiFiStateError |  | error | 200/100 500/300 (loop) | Wi‑Fi failed (auth timeout or no AP) |
|
||||
|
||||
---
|
||||
|
||||
Feedback, issues, and PRs are welcome.
|
||||
@@ -1,7 +1,4 @@
|
||||
CONFIG_BLINK_LED_GPIO=y
|
||||
CONFIG_BLINK_GPIO=21
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
|
||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
|
||||
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
|
||||
@@ -57,7 +54,18 @@ CONFIG_SPIRAM_SPEED_80M=y
|
||||
CONFIG_LED_EXTERNAL_CONTROL=y
|
||||
CONFIG_LED_EXTERNAL_GPIO=9
|
||||
CONFIG_LED_EXTERNAL_PWM_FREQ=20000
|
||||
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=100
|
||||
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=45
|
||||
CONFIG_CAMERA_USB_XCLK_FREQ=23000000
|
||||
CONFIG_GENERAL_DEFAULT_WIRED_MODE=y
|
||||
CONFIG_START_IN_UVC_MODE=y
|
||||
CONFIG_GENERAL_INCLUDE_UVC_MODE=y
|
||||
CONFIG_START_IN_UVC_MODE=y
|
||||
CONFIG_MONITORING_LED_CURRENT=y
|
||||
CONFIG_MONITORING_LED_ADC_GPIO=3
|
||||
CONFIG_MONITORING_LED_GAIN=11
|
||||
CONFIG_MONITORING_LED_SHUNT_MILLIOHM=22000
|
||||
CONFIG_MONITORING_LED_SAMPLES=10
|
||||
CONFIG_MONITORING_LED_INTERVAL_MS=500
|
||||
CONFIG_GENERAL_BOARD="facefocusvr_eye_l"
|
||||
# CONFIG_GENERAL_ENABLE_WIRELESS is not set
|
||||
# CONFIG_LED_DEBUG_ENABLE is not set
|
||||
CONFIG_LED_EXTERNAL_AS_DEBUG=y
|
||||
CONFIG_GENERAL_ADVERTISED_NAME="FFVR Eye L"
|
||||
71
boards/facefocusvr/eye_R
Normal file
@@ -0,0 +1,71 @@
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
|
||||
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
|
||||
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
|
||||
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
|
||||
# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set
|
||||
# CONFIG_ESPTOOLPY_FLASHSIZE_32MB is not set
|
||||
# CONFIG_ESPTOOLPY_FLASHSIZE_64MB is not set
|
||||
# CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
|
||||
# Camera sensor pinout configuration
|
||||
CONFIG_CAMERA_MODULE_NAME="FaceFocusVR_Face"
|
||||
CONFIG_PWDN_GPIO_NUM=-1
|
||||
CONFIG_RESET_GPIO_NUM=-1
|
||||
CONFIG_XCLK_GPIO_NUM=10
|
||||
CONFIG_SIOD_GPIO_NUM=40
|
||||
CONFIG_SIOC_GPIO_NUM=39
|
||||
CONFIG_Y9_GPIO_NUM=48
|
||||
CONFIG_Y8_GPIO_NUM=11
|
||||
CONFIG_Y7_GPIO_NUM=12
|
||||
CONFIG_Y6_GPIO_NUM=14
|
||||
CONFIG_Y5_GPIO_NUM=16
|
||||
CONFIG_Y4_GPIO_NUM=18
|
||||
CONFIG_Y3_GPIO_NUM=17
|
||||
CONFIG_Y2_GPIO_NUM=15
|
||||
CONFIG_VSYNC_GPIO_NUM=38
|
||||
CONFIG_HREF_GPIO_NUM=47
|
||||
CONFIG_PCLK_GPIO_NUM=13
|
||||
# end of Camera sensor pinout configuration
|
||||
# CONFIG_FLASHMODE_QIO is not set
|
||||
# CONFIG_FLASHMODE_QOUT is not set
|
||||
CONFIG_FLASHMODE_DIO=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
CONFIG_SPIRAM_TYPE_AUTO=y
|
||||
# CONFIG_SPIRAM_TYPE_ESPPSRAM16 is not set
|
||||
# CONFIG_SPIRAM_TYPE_ESPPSRAM32 is not set
|
||||
# CONFIG_SPIRAM_TYPE_ESPPSRAM64 is not set
|
||||
CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY=y
|
||||
CONFIG_SPIRAM_CLK_IO=30
|
||||
CONFIG_SPIRAM_CS_IO=26
|
||||
# CONFIG_SPIRAM_XIP_FROM_PSRAM is not set
|
||||
# CONFIG_SPIRAM_FETCH_INSTRUCTIONS is not set
|
||||
# CONFIG_SPIRAM_RODATA is not set
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
# CONFIG_SPIRAM_SPEED_40M is not set
|
||||
# CONFIG_SPIRAM_XIP_FROM_PSRAM is not set
|
||||
# CONFIG_SPIRAM_FETCH_INSTRUCTIONS is not set
|
||||
# CONFIG_SPIRAM_RODATA is not set
|
||||
# CONFIG_SPIRAM_SPEED_120M is not set
|
||||
CONFIG_SPIRAM_SPEED=80
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
CONFIG_LED_EXTERNAL_CONTROL=y
|
||||
CONFIG_LED_EXTERNAL_GPIO=9
|
||||
CONFIG_LED_EXTERNAL_PWM_FREQ=20000
|
||||
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=45
|
||||
CONFIG_CAMERA_USB_XCLK_FREQ=23000000
|
||||
CONFIG_GENERAL_INCLUDE_UVC_MODE=y
|
||||
CONFIG_START_IN_UVC_MODE=y
|
||||
CONFIG_MONITORING_LED_CURRENT=y
|
||||
CONFIG_MONITORING_LED_ADC_GPIO=3
|
||||
CONFIG_MONITORING_LED_GAIN=11
|
||||
CONFIG_MONITORING_LED_SHUNT_MILLIOHM=22000
|
||||
CONFIG_MONITORING_LED_SAMPLES=10
|
||||
CONFIG_MONITORING_LED_INTERVAL_MS=500
|
||||
CONFIG_GENERAL_BOARD="facefocusvr_eye_r"
|
||||
# CONFIG_GENERAL_ENABLE_WIRELESS is not set
|
||||
# CONFIG_LED_DEBUG_ENABLE is not set
|
||||
CONFIG_LED_EXTERNAL_AS_DEBUG=y
|
||||
CONFIG_GENERAL_ADVERTISED_NAME="FFVR Eye R"
|
||||
@@ -1,7 +1,4 @@
|
||||
CONFIG_BLINK_LED_GPIO=y
|
||||
CONFIG_BLINK_GPIO=21
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
|
||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
|
||||
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
|
||||
@@ -57,7 +54,18 @@ CONFIG_SPIRAM_SPEED_80M=y
|
||||
CONFIG_LED_EXTERNAL_CONTROL=y
|
||||
CONFIG_LED_EXTERNAL_GPIO=9
|
||||
CONFIG_LED_EXTERNAL_PWM_FREQ=20000
|
||||
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=50
|
||||
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=85
|
||||
CONFIG_CAMERA_USB_XCLK_FREQ=23000000
|
||||
CONFIG_GENERAL_DEFAULT_WIRED_MODE=y
|
||||
CONFIG_START_IN_UVC_MODE=y
|
||||
CONFIG_GENERAL_INCLUDE_UVC_MODE=y
|
||||
CONFIG_START_IN_UVC_MODE=y
|
||||
CONFIG_MONITORING_LED_CURRENT=y
|
||||
CONFIG_MONITORING_LED_ADC_GPIO=3
|
||||
CONFIG_MONITORING_LED_GAIN=11
|
||||
CONFIG_MONITORING_LED_SHUNT_MILLIOHM=22000
|
||||
CONFIG_MONITORING_LED_SAMPLES=10
|
||||
CONFIG_MONITORING_LED_INTERVAL_MS=500
|
||||
CONFIG_GENERAL_BOARD="facefocusvr_face"
|
||||
# CONFIG_GENERAL_ENABLE_WIRELESS is not set
|
||||
# CONFIG_LED_DEBUG_ENABLE is not set
|
||||
CONFIG_LED_EXTERNAL_AS_DEBUG=y
|
||||
CONFIG_GENERAL_ADVERTISED_NAME="FFVR Face"
|
||||
@@ -1,4 +1,4 @@
|
||||
CONFIG_BLINK_GPIO=38
|
||||
CONFIG_LED_DEBUG_GPIO=38
|
||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
|
||||
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
||||
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
|
||||
@@ -51,5 +51,8 @@ CONFIG_LED_EXTERNAL_PWM_FREQ=5000
|
||||
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=100
|
||||
CONFIG_LED_EXTERNAL_GPIO=1
|
||||
CONFIG_CAMERA_USB_XCLK_FREQ=23000000
|
||||
CONFIG_GENERAL_DEFAULT_WIRED_MODE=y
|
||||
# CONFIG_START_IN_UVC_MODE is not set
|
||||
CONFIG_GENERAL_INCLUDE_UVC_MODE=y
|
||||
# CONFIG_START_IN_UVC_MODE is not set
|
||||
# CONFIG_MONITORING_LED_CURRENT is not set
|
||||
CONFIG_GENERAL_BOARD="project_babble"
|
||||
CONFIG_GENERAL_ENABLE_WIRELESS=y
|
||||
@@ -570,9 +570,10 @@ CONFIG_ENV_GPIO_OUT_RANGE_MAX=48
|
||||
#
|
||||
# OpenIris: General Configuration
|
||||
#
|
||||
# CONFIG_GENERAL_DEFAULT_WIRED_MODE is not set
|
||||
# CONFIG_GENERAL_INCLUDE_UVC_MODE is not set
|
||||
# CONFIG_START_IN_UVC_MODE is not set
|
||||
# CONFIG_GENERAL_UVC_DELAY is not set
|
||||
# CONFIG_GENERAL_STARTUP_DELAY is not set
|
||||
CONFIG_GENERAL_VERSION="0.0.1"
|
||||
# end of OpenIris: General Configuration
|
||||
|
||||
#
|
||||
@@ -595,7 +596,7 @@ CONFIG_WIFI_AP_PASSWORD="12345678"
|
||||
#
|
||||
# OpenIris: LED Configuration
|
||||
#
|
||||
CONFIG_LED_BLINK_GPIO=8
|
||||
CONFIG_LED_DEBUG_GPIO=8
|
||||
CONFIG_LED_EXTERNAL_GPIO=1
|
||||
CONFIG_LED_EXTERNAL_CONTROL=y
|
||||
CONFIG_LED_EXTERNAL_PWM_FREQ=5000
|
||||
@@ -2566,4 +2567,4 @@ CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y
|
||||
CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y
|
||||
CONFIG_SUPPORT_TERMIOS=y
|
||||
CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1
|
||||
# End of deprecated options
|
||||
# End of deprecated options
|
||||
@@ -1,5 +1,4 @@
|
||||
CONFIG_BLINK_LED_GPIO=y
|
||||
CONFIG_BLINK_GPIO=21
|
||||
CONFIG_LED_DEBUG_GPIO=21
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
|
||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
|
||||
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
||||
@@ -56,5 +55,8 @@ CONFIG_SPIRAM_SPEED=80
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
# CONFIG_LED_EXTERNAL_CONTROL is not set
|
||||
CONFIG_CAMERA_USB_XCLK_FREQ=23000000
|
||||
CONFIG_GENERAL_DEFAULT_WIRED_MODE=y
|
||||
# CONFIG_START_IN_UVC_MODE is not set
|
||||
CONFIG_GENERAL_INCLUDE_UVC_MODE=y
|
||||
# CONFIG_START_IN_UVC_MODE is not set
|
||||
# CONFIG_MONITORING_LED_CURRENT is not set
|
||||
CONFIG_GENERAL_BOARD="xiao_esp32s3"
|
||||
CONFIG_GENERAL_ENABLE_WIRELESS=y
|
||||
@@ -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_DEFAULT_WIRED_MODE
|
||||
#ifdef CONFIG_GENERAL_INCLUDE_UVC_MODE
|
||||
#include "esp_log.h"
|
||||
#include "soc/rtc_cntl_struct.h"
|
||||
#include "soc/usb_serial_jtag_reg.h"
|
||||
|
||||
@@ -48,7 +48,7 @@ void CameraManager::setupCameraPinout()
|
||||
|
||||
ESP_LOGI(CAMERA_MANAGER_TAG, "CAM_BOARD");
|
||||
#endif
|
||||
#if CONFIG_GENERAL_DEFAULT_WIRED_MODE
|
||||
#if CONFIG_GENERAL_INCLUDE_UVC_MODE
|
||||
xclk_freq_hz = CONFIG_CAMERA_USB_XCLK_FREQ;
|
||||
#endif
|
||||
|
||||
@@ -79,10 +79,10 @@ void CameraManager::setupCameraPinout()
|
||||
.pixel_format = PIXFORMAT_JPEG, // YUV422,GRAYSCALE,RGB565,JPEG
|
||||
.frame_size = FRAMESIZE_240X240, // QQVGA-UXGA, For ESP32, do not use sizes above QVGA when not JPEG. The performance of the ESP32-S series has improved a lot, but JPEG mode always gives better frame rates.
|
||||
|
||||
.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_LATEST, // CAMERA_GRAB_WHEN_EMPTY
|
||||
.jpeg_quality = 8, // 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, // was CAMERA_GRAB_LATEST; new mode reduces frame skips at cost of minor latency
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ bool CameraManager::setupCamera()
|
||||
return false;
|
||||
}
|
||||
|
||||
#if CONFIG_GENERAL_DEFAULT_WIRED_MODE
|
||||
#if CONFIG_GENERAL_INCLUDE_UVC_MODE
|
||||
const auto temp_sensor = esp_camera_sensor_get();
|
||||
|
||||
// Thanks to lick_it, we discovered that OV5640 likes to overheat when
|
||||
|
||||
@@ -11,5 +11,5 @@ idf_component_register(
|
||||
INCLUDE_DIRS
|
||||
"CommandManager"
|
||||
"CommandManager/commands"
|
||||
REQUIRES ProjectConfig cJSON CameraManager OpenIrisTasks wifiManager Helpers LEDManager
|
||||
REQUIRES ProjectConfig cJSON CameraManager OpenIrisTasks wifiManager Helpers LEDManager Monitoring
|
||||
)
|
||||
@@ -26,6 +26,8 @@ std::unordered_map<std::string, CommandType> commandTypeMap = {
|
||||
{"set_led_duty_cycle", CommandType::SET_LED_DUTY_CYCLE},
|
||||
{"get_led_duty_cycle", CommandType::GET_LED_DUTY_CYCLE},
|
||||
{"get_serial", CommandType::GET_SERIAL},
|
||||
{"get_led_current", CommandType::GET_LED_CURRENT},
|
||||
{"get_who_am_i", CommandType::GET_WHO_AM_I},
|
||||
};
|
||||
|
||||
std::function<CommandResult()> CommandManager::createCommand(const CommandType type, std::string_view json) const
|
||||
@@ -103,6 +105,12 @@ std::function<CommandResult()> CommandManager::createCommand(const CommandType t
|
||||
case CommandType::GET_SERIAL:
|
||||
return [this]
|
||||
{ return getSerialNumberCommand(this->registry); };
|
||||
case CommandType::GET_LED_CURRENT:
|
||||
return [this]
|
||||
{ return getLEDCurrentCommand(this->registry); };
|
||||
case CommandType::GET_WHO_AM_I:
|
||||
return [this]
|
||||
{ return getInfoCommand(this->registry); };
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ enum class CommandType
|
||||
SET_LED_DUTY_CYCLE,
|
||||
GET_LED_DUTY_CYCLE,
|
||||
GET_SERIAL,
|
||||
GET_LED_CURRENT,
|
||||
GET_WHO_AM_I,
|
||||
};
|
||||
|
||||
class CommandManager
|
||||
|
||||
@@ -9,7 +9,8 @@ enum class DependencyType
|
||||
project_config,
|
||||
camera_manager,
|
||||
wifi_manager,
|
||||
led_manager
|
||||
led_manager,
|
||||
monitoring_manager
|
||||
};
|
||||
|
||||
class DependencyRegistry
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "device_commands.hpp"
|
||||
#include "LEDManager.hpp"
|
||||
#include "MonitoringManager.hpp"
|
||||
#include "esp_mac.h"
|
||||
#include <cstdio>
|
||||
|
||||
@@ -171,9 +172,9 @@ CommandResult switchModeCommand(std::shared_ptr<DependencyRegistry> registry, st
|
||||
{
|
||||
newMode = StreamingMode::WIFI;
|
||||
}
|
||||
else if (strcmp(modeStr, "auto") == 0)
|
||||
else if (strcmp(modeStr, "setup") == 0 || strcmp(modeStr, "auto") == 0)
|
||||
{
|
||||
newMode = StreamingMode::AUTO;
|
||||
newMode = StreamingMode::SETUP;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -203,8 +204,8 @@ CommandResult getDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry)
|
||||
case StreamingMode::WIFI:
|
||||
modeStr = "WiFi";
|
||||
break;
|
||||
case StreamingMode::AUTO:
|
||||
modeStr = "Auto";
|
||||
case StreamingMode::SETUP:
|
||||
modeStr = "Setup";
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -231,3 +232,30 @@ CommandResult getSerialNumberCommand(std::shared_ptr<DependencyRegistry> /*regis
|
||||
auto result = std::format("{{ \"serial\": \"{}\", \"mac\": \"{}\" }}", serial_no_sep, mac_colon);
|
||||
return CommandResult::getSuccessResult(result);
|
||||
}
|
||||
|
||||
CommandResult getLEDCurrentCommand(std::shared_ptr<DependencyRegistry> registry)
|
||||
{
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
auto mon = registry->resolve<MonitoringManager>(DependencyType::monitoring_manager);
|
||||
if (!mon)
|
||||
{
|
||||
return CommandResult::getErrorResult("MonitoringManager unavailable");
|
||||
}
|
||||
float ma = mon->getCurrentMilliAmps();
|
||||
auto result = std::format("{{ \"led_current_ma\": {:.3f} }}", static_cast<double>(ma));
|
||||
return CommandResult::getSuccessResult(result);
|
||||
#else
|
||||
return CommandResult::getErrorResult("Monitoring disabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
CommandResult getInfoCommand(std::shared_ptr<DependencyRegistry> /*registry*/)
|
||||
{
|
||||
const char* who = CONFIG_GENERAL_BOARD;
|
||||
const char* ver = CONFIG_GENERAL_VERSION;
|
||||
// Ensure non-null strings
|
||||
if (!who) who = "";
|
||||
if (!ver) ver = "";
|
||||
auto result = std::format("{{ \"who_am_i\": \"{}\", \"version\": \"{}\" }}", who, ver);
|
||||
return CommandResult::getSuccessResult(result);
|
||||
}
|
||||
|
||||
@@ -22,4 +22,10 @@ CommandResult switchModeCommand(std::shared_ptr<DependencyRegistry> registry, st
|
||||
|
||||
CommandResult getDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry);
|
||||
|
||||
CommandResult getSerialNumberCommand(std::shared_ptr<DependencyRegistry> registry);
|
||||
CommandResult getSerialNumberCommand(std::shared_ptr<DependencyRegistry> registry);
|
||||
|
||||
// Monitoring
|
||||
CommandResult getLEDCurrentCommand(std::shared_ptr<DependencyRegistry> registry);
|
||||
|
||||
// General info
|
||||
CommandResult getInfoCommand(std::shared_ptr<DependencyRegistry> registry);
|
||||
@@ -1,11 +1,15 @@
|
||||
#include "scan_commands.hpp"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
CommandResult scanNetworksCommand(std::shared_ptr<DependencyRegistry> registry)
|
||||
{
|
||||
#if !CONFIG_GENERAL_ENABLE_WIRELESS
|
||||
return CommandResult::getErrorResult("Not supported by current firmware");
|
||||
#endif
|
||||
auto wifiManager = registry->resolve<WiFiManager>(DependencyType::wifi_manager);
|
||||
if (!wifiManager)
|
||||
{
|
||||
return CommandResult::getErrorResult("WiFiManager not available");
|
||||
return CommandResult::getErrorResult("Not supported by current firmware");
|
||||
}
|
||||
|
||||
auto networks = wifiManager->ScanNetworks();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "wifi_commands.hpp"
|
||||
#include "esp_netif.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
std::optional<WifiPayload> parseSetWiFiCommandPayload(std::string_view jsonPayload)
|
||||
{
|
||||
@@ -143,6 +144,9 @@ std::optional<UpdateAPWiFiPayload> parseUpdateAPWiFiCommandPayload(const std::st
|
||||
|
||||
CommandResult setWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
|
||||
{
|
||||
#if !CONFIG_GENERAL_ENABLE_WIRELESS
|
||||
return CommandResult::getErrorResult("Not supported by current firmware");
|
||||
#endif
|
||||
const auto payload = parseSetWiFiCommandPayload(jsonPayload);
|
||||
|
||||
if (!payload.has_value())
|
||||
@@ -164,6 +168,9 @@ CommandResult setWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::
|
||||
|
||||
CommandResult deleteWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
|
||||
{
|
||||
#if !CONFIG_GENERAL_ENABLE_WIRELESS
|
||||
return CommandResult::getErrorResult("Not supported by current firmware");
|
||||
#endif
|
||||
const auto payload = parseDeleteWifiCommandPayload(jsonPayload);
|
||||
if (!payload.has_value())
|
||||
return CommandResult::getErrorResult("Invalid payload");
|
||||
@@ -176,6 +183,9 @@ CommandResult deleteWiFiCommand(std::shared_ptr<DependencyRegistry> registry, st
|
||||
|
||||
CommandResult updateWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
|
||||
{
|
||||
#if !CONFIG_GENERAL_ENABLE_WIRELESS
|
||||
return CommandResult::getErrorResult("Not supported by current firmware");
|
||||
#endif
|
||||
const auto payload = parseUpdateWifiCommandPayload(jsonPayload);
|
||||
if (!payload.has_value())
|
||||
{
|
||||
@@ -207,6 +217,9 @@ CommandResult updateWiFiCommand(std::shared_ptr<DependencyRegistry> registry, st
|
||||
|
||||
CommandResult updateAPWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
|
||||
{
|
||||
#if !CONFIG_GENERAL_ENABLE_WIRELESS
|
||||
return CommandResult::getErrorResult("Not supported by current firmware");
|
||||
#endif
|
||||
const auto payload = parseUpdateAPWiFiCommandPayload(jsonPayload);
|
||||
|
||||
if (!payload.has_value())
|
||||
@@ -226,7 +239,13 @@ CommandResult updateAPWiFiCommand(std::shared_ptr<DependencyRegistry> registry,
|
||||
}
|
||||
|
||||
CommandResult getWiFiStatusCommand(std::shared_ptr<DependencyRegistry> registry) {
|
||||
auto wifiManager = registry->resolve<WiFiManager>(DependencyType::wifi_manager);
|
||||
#if !CONFIG_GENERAL_ENABLE_WIRELESS
|
||||
return CommandResult::getErrorResult("Not supported by current firmware");
|
||||
#endif
|
||||
auto wifiManager = registry->resolve<WiFiManager>(DependencyType::wifi_manager);
|
||||
if (!wifiManager) {
|
||||
return CommandResult::getErrorResult("Not supported by current firmware");
|
||||
}
|
||||
auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
|
||||
|
||||
// Get current WiFi state
|
||||
@@ -287,7 +306,13 @@ CommandResult getWiFiStatusCommand(std::shared_ptr<DependencyRegistry> registry)
|
||||
}
|
||||
|
||||
CommandResult connectWiFiCommand(std::shared_ptr<DependencyRegistry> registry) {
|
||||
auto wifiManager = registry->resolve<WiFiManager>(DependencyType::wifi_manager);
|
||||
#if !CONFIG_GENERAL_ENABLE_WIRELESS
|
||||
return CommandResult::getErrorResult("Not supported by current firmware");
|
||||
#endif
|
||||
auto wifiManager = registry->resolve<WiFiManager>(DependencyType::wifi_manager);
|
||||
if (!wifiManager) {
|
||||
return CommandResult::getErrorResult("Not supported by current firmware");
|
||||
}
|
||||
auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
|
||||
|
||||
auto networks = projectConfig->getWifiConfigs();
|
||||
|
||||
@@ -2,63 +2,23 @@
|
||||
|
||||
const char *LED_MANAGER_TAG = "[LED_MANAGER]";
|
||||
|
||||
// Pattern design rules:
|
||||
// - Error states: isError=true, repeat indefinitely, easily distinguishable (avoid overlap).
|
||||
// - Non-error repeating: show continuous activity (e.g. streaming ON steady, connecting blink).
|
||||
// - Non-repeating notification (e.g. Connected) gives user a brief confirmation burst then turns off.
|
||||
// Durations in ms.
|
||||
ledStateMap_t LEDManager::ledStateMap = {
|
||||
{
|
||||
LEDStates_e::LedStateNone,
|
||||
{
|
||||
false,
|
||||
false,
|
||||
{{LED_OFF, 1000}},
|
||||
},
|
||||
},
|
||||
{
|
||||
LEDStates_e::LedStateStreaming,
|
||||
{
|
||||
false,
|
||||
true,
|
||||
{{LED_ON, 1000}},
|
||||
},
|
||||
},
|
||||
{
|
||||
LEDStates_e::LedStateStoppedStreaming,
|
||||
{
|
||||
false,
|
||||
true,
|
||||
{{LED_OFF, 1000}},
|
||||
},
|
||||
},
|
||||
{
|
||||
LEDStates_e::CameraError,
|
||||
{
|
||||
true,
|
||||
true,
|
||||
{{{LED_ON, 300}, {LED_OFF, 300}, {LED_ON, 300}, {LED_OFF, 300}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
LEDStates_e::WiFiStateConnecting,
|
||||
{
|
||||
false,
|
||||
true,
|
||||
{{LED_ON, 400}, {LED_OFF, 400}},
|
||||
},
|
||||
},
|
||||
{
|
||||
LEDStates_e::WiFiStateConnected,
|
||||
{
|
||||
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}},
|
||||
},
|
||||
},
|
||||
{
|
||||
LEDStates_e::WiFiStateError,
|
||||
{
|
||||
true,
|
||||
true,
|
||||
{{LED_ON, 200}, {LED_OFF, 100}, {LED_ON, 500}, {LED_OFF, 100}, {LED_ON, 200}},
|
||||
},
|
||||
},
|
||||
{ LEDStates_e::LedStateNone, { /*isError*/false, /*repeat*/false, {{LED_OFF, 1000}} } },
|
||||
{ LEDStates_e::LedStateStreaming, { false, /*repeat steady*/true, {{LED_ON, 1000}} } },
|
||||
{ LEDStates_e::LedStateStoppedStreaming, { false, true, {{LED_OFF, 1000}} } },
|
||||
// CameraError: double blink pattern repeating
|
||||
{ LEDStates_e::CameraError, { true, true, {{ {LED_ON,300}, {LED_OFF,300}, {LED_ON,300}, {LED_OFF,700} }} } },
|
||||
// WiFiStateConnecting: balanced slow blink 400/400
|
||||
{ LEDStates_e::WiFiStateConnecting, { false, true, {{ {LED_ON,400}, {LED_OFF,400} }} } },
|
||||
// WiFiStateConnected: short 3 quick flashes then done (was long noisy burst before)
|
||||
{ LEDStates_e::WiFiStateConnected, { false, false, {{ {LED_ON,150}, {LED_OFF,150}, {LED_ON,150}, {LED_OFF,150}, {LED_ON,150}, {LED_OFF,600} }} } },
|
||||
// WiFiStateError: asymmetric attention pattern (fast, pause, long, pause, fast)
|
||||
{ LEDStates_e::WiFiStateError, { true, true, {{ {LED_ON,200}, {LED_OFF,100}, {LED_ON,500}, {LED_OFF,300} }} } },
|
||||
};
|
||||
|
||||
LEDManager::LEDManager(gpio_num_t pin, gpio_num_t illumninator_led_pin,
|
||||
@@ -73,10 +33,13 @@ LEDManager::LEDManager(gpio_num_t pin, gpio_num_t illumninator_led_pin,
|
||||
void LEDManager::setup()
|
||||
{
|
||||
ESP_LOGI(LED_MANAGER_TAG, "Setting up status led.");
|
||||
#ifdef CONFIG_LED_DEBUG_ENABLE
|
||||
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);
|
||||
#else
|
||||
ESP_LOGI(LED_MANAGER_TAG, "Debug LED disabled via Kconfig (LED_DEBUG_ENABLE=n)");
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_LED_EXTERNAL_CONTROL
|
||||
ESP_LOGI(LED_MANAGER_TAG, "Setting up illuminator led.");
|
||||
@@ -168,6 +131,31 @@ void LEDManager::updateState(const LEDStates_e newState)
|
||||
if (newState == this->currentState)
|
||||
return;
|
||||
|
||||
// Handle external LED mirroring transitions (store/restore duty)
|
||||
#if defined(CONFIG_LED_EXTERNAL_CONTROL) && defined(CONFIG_LED_EXTERNAL_AS_DEBUG)
|
||||
bool wasError = ledStateMap[this->currentState].isError;
|
||||
bool willBeError = ledStateMap[newState].isError;
|
||||
if (!wasError && willBeError)
|
||||
{
|
||||
// store current duty once
|
||||
if (!hasStoredExternalDuty)
|
||||
{
|
||||
storedExternalDuty = ledc_get_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
|
||||
hasStoredExternalDuty = true;
|
||||
}
|
||||
}
|
||||
else if (wasError && !willBeError)
|
||||
{
|
||||
// restore duty
|
||||
if (hasStoredExternalDuty)
|
||||
{
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, storedExternalDuty));
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0));
|
||||
hasStoredExternalDuty = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
this->currentState = newState;
|
||||
this->currentPatternIndex = 0;
|
||||
this->finishedPattern = false;
|
||||
@@ -175,7 +163,20 @@ void LEDManager::updateState(const LEDStates_e newState)
|
||||
|
||||
void LEDManager::toggleLED(const bool state) const
|
||||
{
|
||||
#ifdef CONFIG_LED_DEBUG_ENABLE
|
||||
gpio_set_level(blink_led_pin, state);
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_LED_EXTERNAL_CONTROL) && defined(CONFIG_LED_EXTERNAL_AS_DEBUG)
|
||||
// Mirror only for error states
|
||||
if (ledStateMap.contains(this->currentState) && ledStateMap.at(this->currentState).isError)
|
||||
{
|
||||
// For pattern ON use 50%, OFF use 0%
|
||||
uint32_t duty = (state == LED_ON) ? ((50 * 255) / 100) : 0;
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty));
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void LEDManager::setExternalLEDDutyCycle(uint8_t dutyPercent)
|
||||
|
||||
@@ -70,6 +70,11 @@ private:
|
||||
size_t currentPatternIndex = 0;
|
||||
size_t timeToDelayFor = 100;
|
||||
bool finishedPattern = false;
|
||||
|
||||
#if defined(CONFIG_LED_EXTERNAL_CONTROL) && defined(CONFIG_LED_EXTERNAL_AS_DEBUG)
|
||||
bool hasStoredExternalDuty = false;
|
||||
uint32_t storedExternalDuty = 0; // raw 0-255
|
||||
#endif
|
||||
};
|
||||
|
||||
void HandleLEDDisplayTask(void *pvParameter);
|
||||
|
||||
6
components/Monitoring/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
idf_component_register(SRCS
|
||||
"Monitoring/CurrentMonitor.cpp"
|
||||
"Monitoring/MonitoringManager.cpp"
|
||||
INCLUDE_DIRS "Monitoring"
|
||||
REQUIRES driver esp_adc Helpers
|
||||
)
|
||||
179
components/Monitoring/Monitoring/CurrentMonitor.cpp
Normal file
@@ -0,0 +1,179 @@
|
||||
#include "CurrentMonitor.hpp"
|
||||
#include <esp_log.h>
|
||||
#include <cmath>
|
||||
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
#include "esp_adc/adc_oneshot.h"
|
||||
#include "esp_adc/adc_cali.h"
|
||||
#include "esp_adc/adc_cali_scheme.h"
|
||||
#endif
|
||||
|
||||
static const char *TAG_CM = "[CurrentMonitor]";
|
||||
|
||||
CurrentMonitor::CurrentMonitor()
|
||||
{
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
samples_.assign(CONFIG_MONITORING_LED_SAMPLES, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void CurrentMonitor::setup()
|
||||
{
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
init_adc();
|
||||
#else
|
||||
ESP_LOGI(TAG_CM, "LED current monitoring disabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
float CurrentMonitor::getCurrentMilliAmps() const
|
||||
{
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
const int shunt_milliohm = CONFIG_MONITORING_LED_SHUNT_MILLIOHM; // mΩ
|
||||
if (shunt_milliohm <= 0)
|
||||
return 0.0f;
|
||||
// Physically correct scaling:
|
||||
// I[mA] = 1000 * Vshunt[mV] / R[mΩ]
|
||||
return (1000.0f * static_cast<float>(filtered_mv_)) / static_cast<float>(shunt_milliohm);
|
||||
#else
|
||||
return 0.0f;
|
||||
#endif
|
||||
}
|
||||
|
||||
float CurrentMonitor::pollAndGetMilliAmps()
|
||||
{
|
||||
sampleOnce();
|
||||
return getCurrentMilliAmps();
|
||||
}
|
||||
|
||||
void CurrentMonitor::sampleOnce()
|
||||
{
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
int mv = read_mv_once();
|
||||
// Divide by analog gain/divider factor to get shunt voltage
|
||||
if (CONFIG_MONITORING_LED_GAIN > 0)
|
||||
mv = mv / CONFIG_MONITORING_LED_GAIN;
|
||||
|
||||
// Moving average over N samples
|
||||
if (samples_.empty())
|
||||
{
|
||||
samples_.assign(CONFIG_MONITORING_LED_SAMPLES, 0);
|
||||
sample_sum_ = 0;
|
||||
sample_idx_ = 0;
|
||||
sample_count_ = 0;
|
||||
}
|
||||
|
||||
sample_sum_ -= samples_[sample_idx_];
|
||||
samples_[sample_idx_] = mv;
|
||||
sample_sum_ += mv;
|
||||
sample_idx_ = (sample_idx_ + 1) % samples_.size();
|
||||
if (sample_count_ < samples_.size())
|
||||
sample_count_++;
|
||||
|
||||
filtered_mv_ = sample_sum_ / static_cast<int>(sample_count_ > 0 ? sample_count_ : 1);
|
||||
#else
|
||||
(void)0;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
|
||||
static adc_oneshot_unit_handle_t s_adc_handle = nullptr;
|
||||
static adc_cali_handle_t s_cali_handle = nullptr;
|
||||
static bool s_cali_inited = false;
|
||||
static adc_channel_t s_channel;
|
||||
static adc_unit_t s_unit;
|
||||
|
||||
void CurrentMonitor::init_adc()
|
||||
{
|
||||
// Derive ADC unit/channel from GPIO
|
||||
int gpio = CONFIG_MONITORING_LED_ADC_GPIO;
|
||||
|
||||
esp_err_t err;
|
||||
adc_oneshot_unit_init_cfg_t unit_cfg = {
|
||||
.unit_id = ADC_UNIT_1,
|
||||
};
|
||||
err = adc_oneshot_new_unit(&unit_cfg, &s_adc_handle);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG_CM, "adc_oneshot_new_unit failed: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to map GPIO to ADC channel automatically if helper exists; otherwise guess for ESP32-S3 ADC1
|
||||
#ifdef ADC1_GPIO1_CHANNEL
|
||||
(void)0; // placeholder for potential future macros
|
||||
#endif
|
||||
|
||||
// Use IO-to-channel helper where available
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||
// ESP32-S3: ADC1 channels on GPIO1..GPIO10 map to CH0..CH9
|
||||
if (gpio >= 1 && gpio <= 10)
|
||||
{
|
||||
s_unit = ADC_UNIT_1;
|
||||
s_channel = static_cast<adc_channel_t>(gpio - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW(TAG_CM, "Configured GPIO %d may not be ADC-capable on ESP32-S3", gpio);
|
||||
s_unit = ADC_UNIT_1;
|
||||
s_channel = ADC_CHANNEL_0;
|
||||
}
|
||||
#else
|
||||
// Fallback: assume ADC1 CH0
|
||||
s_unit = ADC_UNIT_1;
|
||||
s_channel = ADC_CHANNEL_0;
|
||||
#endif
|
||||
|
||||
adc_oneshot_chan_cfg_t chan_cfg = {
|
||||
.atten = ADC_ATTEN_DB_11,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
};
|
||||
err = adc_oneshot_config_channel(s_adc_handle, s_channel, &chan_cfg);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG_CM, "adc_oneshot_config_channel failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// Calibration using curve fitting if available
|
||||
adc_cali_curve_fitting_config_t cal_cfg = {
|
||||
.unit_id = s_unit,
|
||||
.atten = chan_cfg.atten,
|
||||
.bitwidth = chan_cfg.bitwidth,
|
||||
};
|
||||
if (adc_cali_create_scheme_curve_fitting(&cal_cfg, &s_cali_handle) == ESP_OK)
|
||||
{
|
||||
s_cali_inited = true;
|
||||
ESP_LOGI(TAG_CM, "ADC calibration initialized (curve fitting)");
|
||||
}
|
||||
else
|
||||
{
|
||||
s_cali_inited = false;
|
||||
ESP_LOGW(TAG_CM, "ADC calibration not available; using raw-to-mV approximation");
|
||||
}
|
||||
}
|
||||
|
||||
int CurrentMonitor::read_mv_once()
|
||||
{
|
||||
if (!s_adc_handle)
|
||||
return 0;
|
||||
int raw = 0;
|
||||
if (adc_oneshot_read(s_adc_handle, s_channel, &raw) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int mv = 0;
|
||||
if (s_cali_inited)
|
||||
{
|
||||
if (adc_cali_raw_to_voltage(s_cali_handle, raw, &mv) != ESP_OK)
|
||||
mv = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Very rough fallback for 11dB attenuation
|
||||
// Typical full-scale ~2450mV at raw max 4095 (12-bit). IDF defaults may vary.
|
||||
mv = (raw * 2450) / 4095;
|
||||
}
|
||||
return mv;
|
||||
}
|
||||
|
||||
#endif // CONFIG_MONITORING_LED_CURRENT
|
||||
49
components/Monitoring/Monitoring/CurrentMonitor.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#ifndef CURRENT_MONITOR_HPP
|
||||
#define CURRENT_MONITOR_HPP
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "sdkconfig.h"
|
||||
|
||||
class CurrentMonitor {
|
||||
public:
|
||||
CurrentMonitor();
|
||||
~CurrentMonitor() = default;
|
||||
|
||||
void setup();
|
||||
void sampleOnce();
|
||||
|
||||
// Returns filtered voltage in millivolts at shunt (after dividing by gain)
|
||||
int getFilteredMillivolts() const { return filtered_mv_; }
|
||||
// Returns current in milliamps computed as Vshunt[mV] / R[mΩ]
|
||||
float getCurrentMilliAmps() const;
|
||||
|
||||
// convenience: combined sampling and compute; returns mA
|
||||
float pollAndGetMilliAmps();
|
||||
|
||||
// Whether monitoring is enabled by Kconfig
|
||||
static constexpr bool isEnabled()
|
||||
{
|
||||
#ifdef CONFIG_MONITORING_LED_CURRENT
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
void init_adc();
|
||||
int read_mv_once();
|
||||
int gpio_to_adc_channel(int gpio);
|
||||
#endif
|
||||
|
||||
int filtered_mv_ = 0;
|
||||
int sample_sum_ = 0;
|
||||
std::vector<int> samples_;
|
||||
size_t sample_idx_ = 0;
|
||||
size_t sample_count_ = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
58
components/Monitoring/Monitoring/MonitoringManager.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#include "MonitoringManager.hpp"
|
||||
#include <esp_log.h>
|
||||
#include "sdkconfig.h"
|
||||
|
||||
static const char* TAG_MM = "[MonitoringManager]";
|
||||
|
||||
void MonitoringManager::setup()
|
||||
{
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
cm_.setup();
|
||||
ESP_LOGI(TAG_MM, "Monitoring enabled. Interval=%dms, Samples=%d, Gain=%d, R=%dmΩ",
|
||||
CONFIG_MONITORING_LED_INTERVAL_MS,
|
||||
CONFIG_MONITORING_LED_SAMPLES,
|
||||
CONFIG_MONITORING_LED_GAIN,
|
||||
CONFIG_MONITORING_LED_SHUNT_MILLIOHM);
|
||||
#else
|
||||
ESP_LOGI(TAG_MM, "Monitoring disabled by Kconfig");
|
||||
#endif
|
||||
}
|
||||
|
||||
void MonitoringManager::start()
|
||||
{
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
if (task_ == nullptr)
|
||||
{
|
||||
xTaskCreate(&MonitoringManager::taskEntry, "MonitoringTask", 2048, this, 1, &task_);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MonitoringManager::stop()
|
||||
{
|
||||
if (task_)
|
||||
{
|
||||
TaskHandle_t toDelete = task_;
|
||||
task_ = nullptr;
|
||||
vTaskDelete(toDelete);
|
||||
}
|
||||
}
|
||||
|
||||
void MonitoringManager::taskEntry(void* arg)
|
||||
{
|
||||
static_cast<MonitoringManager*>(arg)->run();
|
||||
}
|
||||
|
||||
void MonitoringManager::run()
|
||||
{
|
||||
#if CONFIG_MONITORING_LED_CURRENT
|
||||
while (true)
|
||||
{
|
||||
float ma = cm_.pollAndGetMilliAmps();
|
||||
last_current_ma_.store(ma);
|
||||
vTaskDelay(pdMS_TO_TICKS(CONFIG_MONITORING_LED_INTERVAL_MS));
|
||||
}
|
||||
#else
|
||||
vTaskDelete(nullptr);
|
||||
#endif
|
||||
}
|
||||
24
components/Monitoring/Monitoring/MonitoringManager.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <atomic>
|
||||
#include "CurrentMonitor.hpp"
|
||||
|
||||
class MonitoringManager {
|
||||
public:
|
||||
|
||||
void setup();
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
// Latest filtered current in mA
|
||||
float getCurrentMilliAmps() const { return last_current_ma_.load(); }
|
||||
|
||||
private:
|
||||
static void taskEntry(void* arg);
|
||||
void run();
|
||||
|
||||
TaskHandle_t task_{nullptr};
|
||||
std::atomic<float> last_current_ma_{0.0f};
|
||||
CurrentMonitor cm_;
|
||||
};
|
||||
@@ -23,7 +23,7 @@ struct BaseConfigModel
|
||||
|
||||
enum class StreamingMode
|
||||
{
|
||||
AUTO,
|
||||
SETUP,
|
||||
UVC,
|
||||
WIFI,
|
||||
};
|
||||
@@ -31,18 +31,18 @@ enum class StreamingMode
|
||||
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::SETUP) {}
|
||||
|
||||
void load()
|
||||
{
|
||||
// Default mode can be controlled via sdkconfig:
|
||||
// - If CONFIG_START_IN_UVC_MODE is enabled, default to UVC
|
||||
// - Otherwise default to AUTO
|
||||
// Default mode can be controlled via sdkconfig:
|
||||
// - If CONFIG_START_IN_UVC_MODE is enabled, default to UVC
|
||||
// - Otherwise default to SETUP
|
||||
int default_mode =
|
||||
#if CONFIG_START_IN_UVC_MODE
|
||||
static_cast<int>(StreamingMode::UVC);
|
||||
#else
|
||||
static_cast<int>(StreamingMode::AUTO);
|
||||
static_cast<int>(StreamingMode::SETUP);
|
||||
#endif
|
||||
|
||||
int stored_mode = this->pref->getInt("mode", default_mode);
|
||||
@@ -103,9 +103,13 @@ struct MDNSConfig_t : BaseConfigModel
|
||||
|
||||
void load()
|
||||
{
|
||||
// by default, this will be openiris
|
||||
// but we can override it at compile time
|
||||
std::string default_hostname = CONFIG_WIFI_MDNS_HOSTNAME;
|
||||
// Default hostname comes from GENERAL_ADVERTISED_NAME (unified advertised name)
|
||||
std::string default_hostname =
|
||||
#ifdef CONFIG_GENERAL_ADVERTISED_NAME
|
||||
CONFIG_GENERAL_ADVERTISED_NAME;
|
||||
#else
|
||||
"openiristracker";
|
||||
#endif
|
||||
|
||||
if (default_hostname.empty())
|
||||
{
|
||||
|
||||
@@ -115,7 +115,7 @@ void ProjectConfig::setMDNSConfig(const std::string &hostname)
|
||||
{
|
||||
ESP_LOGD(CONFIGURATION_TAG, "Updating MDNS config");
|
||||
this->config.mdns.hostname.assign(hostname);
|
||||
this->config.device.save();
|
||||
this->config.mdns.save();
|
||||
}
|
||||
|
||||
void ProjectConfig::setCameraConfig(const uint8_t vflip,
|
||||
|
||||
@@ -6,15 +6,17 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
|
||||
// LED status categories
|
||||
// Naming kept stable for existing queues; documented meanings added.
|
||||
enum class LEDStates_e
|
||||
{
|
||||
LedStateNone,
|
||||
LedStateStreaming,
|
||||
LedStateStoppedStreaming,
|
||||
CameraError,
|
||||
WiFiStateError,
|
||||
WiFiStateConnecting,
|
||||
WiFiStateConnected
|
||||
LedStateNone, // Idle / no indication (LED off)
|
||||
LedStateStreaming, // Active streaming (UVC or WiFi) – steady ON
|
||||
LedStateStoppedStreaming, // Streaming stopped intentionally – steady OFF (could differentiate later)
|
||||
CameraError, // Camera init / runtime failure – double blink pattern
|
||||
WiFiStateError, // WiFi connection error – distinctive blink sequence
|
||||
WiFiStateConnecting, // WiFi association / DHCP pending – slow blink
|
||||
WiFiStateConnected // WiFi connected (momentary confirmation burst)
|
||||
};
|
||||
|
||||
enum class WiFiState_e
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
static const char *UVC_STREAM_TAG = "[UVC DEVICE]";
|
||||
|
||||
// Tracks whether a frame has been handed to TinyUSB and not yet returned.
|
||||
// File scope so both get_cb and return_cb can access it safely.
|
||||
static bool s_frame_inflight = false;
|
||||
|
||||
extern "C"
|
||||
{
|
||||
static char serial_number_str[13];
|
||||
@@ -85,55 +89,57 @@ static void UVCStreamHelpers::camera_stop_cb(void *cb_ctx)
|
||||
static uvc_fb_t *UVCStreamHelpers::camera_fb_get_cb(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)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
// Guard against requesting a new frame while previous is still in flight.
|
||||
// This was causing intermittent corruption/glitches because the pointer
|
||||
// to the underlying camera buffer was overwritten before TinyUSB returned it.
|
||||
|
||||
//--------------------------------------------------------------------------------------------------------------
|
||||
// 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
|
||||
// --- Frame pacing BEFORE grabbing a new camera frame ---
|
||||
static int64_t next_deadline_us = 0; // next permitted capture time
|
||||
static int rem_acc = 0; // fractional remainder accumulator
|
||||
static const int target_fps = 60; // desired FPS
|
||||
static const int64_t us_per_sec = 1000000; // 1e6 microseconds
|
||||
static const int base_interval_us = us_per_sec / target_fps; // 16666
|
||||
static const int rem_us = us_per_sec % target_fps; // 40 (distributed)
|
||||
|
||||
const int64_t now_us = esp_timer_get_time();
|
||||
if (next_deadline_us == 0)
|
||||
{
|
||||
// First frame: allow immediately and schedule next slot from now
|
||||
// First allowed capture immediately
|
||||
next_deadline_us = now_us;
|
||||
}
|
||||
if (now_us < next_deadline_us)
|
||||
|
||||
// If a frame is still being transmitted or we are too early, just signal no frame
|
||||
if (s_frame_inflight || now_us < next_deadline_us)
|
||||
{
|
||||
return nullptr; // host will poll again
|
||||
}
|
||||
|
||||
// Acquire a fresh frame only when allowed and no frame in flight
|
||||
camera_fb_t *cam_fb = esp_camera_fb_get();
|
||||
if (!cam_fb)
|
||||
{
|
||||
// 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;
|
||||
s_fb.uvc_fb.height = s_fb.cam_fb_p->height;
|
||||
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;
|
||||
s_fb.cam_fb_p = cam_fb;
|
||||
s_fb.uvc_fb.buf = cam_fb->buf;
|
||||
s_fb.uvc_fb.len = cam_fb->len;
|
||||
s_fb.uvc_fb.width = cam_fb->width;
|
||||
s_fb.uvc_fb.height = cam_fb->height;
|
||||
s_fb.uvc_fb.format = UVC_FORMAT_JPEG;
|
||||
s_fb.uvc_fb.timestamp = cam_fb->timestamp;
|
||||
|
||||
// Ensure frame fits into configured UVC transfer buffer
|
||||
// Validate size fits into transfer buffer
|
||||
if (mgr && s_fb.uvc_fb.len > mgr->getUvcBufferSize())
|
||||
{
|
||||
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);
|
||||
esp_camera_fb_return(cam_fb);
|
||||
s_fb.cam_fb_p = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------------------
|
||||
// Schedule the next allowed frame time: base interval plus distributed remainder
|
||||
// Schedule next frame time (distribute remainder for exact long‑term 60.000 fps)
|
||||
rem_acc += rem_us;
|
||||
int extra_us = 0;
|
||||
if (rem_acc >= target_fps)
|
||||
@@ -141,11 +147,10 @@ static uvc_fb_t *UVCStreamHelpers::camera_fb_get_cb(void *cb_ctx)
|
||||
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;
|
||||
//--------------------------------------------------------------------------------------------------------------
|
||||
const int64_t candidate_next = next_deadline_us + base_interval_us + extra_us;
|
||||
next_deadline_us = (candidate_next < now_us) ? now_us : candidate_next;
|
||||
|
||||
s_frame_inflight = true;
|
||||
return &s_fb.uvc_fb;
|
||||
}
|
||||
|
||||
@@ -153,13 +158,18 @@ static void UVCStreamHelpers::camera_fb_return_cb(uvc_fb_t *fb, void *cb_ctx)
|
||||
{
|
||||
(void)cb_ctx;
|
||||
assert(fb == &s_fb.uvc_fb);
|
||||
esp_camera_fb_return(s_fb.cam_fb_p);
|
||||
if (s_fb.cam_fb_p)
|
||||
{
|
||||
esp_camera_fb_return(s_fb.cam_fb_p);
|
||||
s_fb.cam_fb_p = nullptr;
|
||||
}
|
||||
s_frame_inflight = false;
|
||||
}
|
||||
|
||||
esp_err_t UVCStreamManager::setup()
|
||||
{
|
||||
|
||||
#ifndef CONFIG_GENERAL_DEFAULT_WIRED_MODE
|
||||
#ifndef CONFIG_GENERAL_INCLUDE_UVC_MODE
|
||||
ESP_LOGE(UVC_STREAM_TAG, "The board does not support UVC, please, setup WiFi connection.");
|
||||
return ESP_FAIL;
|
||||
#endif
|
||||
|
||||
@@ -70,6 +70,8 @@ uint8_t const *tud_descriptor_device_cb(void)
|
||||
#define STRID_PRODUCT 2
|
||||
#define STRID_SERIAL 3
|
||||
#define STRID_UVC_CAM1 4
|
||||
// CDC interface string index used by TUD_CDC_DESCRIPTOR below
|
||||
#define STRID_CDC 6
|
||||
|
||||
// Endpoint numbers for CDC
|
||||
#define EPNUM_CDC_NOTIF 0x81
|
||||
@@ -124,7 +126,8 @@ 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),
|
||||
// Advertise max bus power consumption: 200 mA
|
||||
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0, 200),
|
||||
TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 6, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, EPNUM_CDC_IN, 64),
|
||||
// IAD for Video Control
|
||||
#if CFG_TUD_CAM1_VIDEO_STREAMING_BULK
|
||||
@@ -200,12 +203,15 @@ uint8_t const *tud_descriptor_configuration_cb(uint8_t index)
|
||||
//--------------------------------------------------------------------+
|
||||
|
||||
// Array of pointers to string literals. Indices must match STRID_* above.
|
||||
// NOTE: Indices must be contiguous up to the highest used index (STRID_CDC = 6)
|
||||
char const *string_desc_arr[] = {
|
||||
(const char[]){0x09, 0x04}, // 0: Supported language: English (0x0409)
|
||||
CONFIG_TUSB_MANUFACTURER, // 1: Manufacturer
|
||||
CONFIG_TUSB_PRODUCT, // 2: Product
|
||||
CONFIG_TUSB_PRODUCT, // 2: Product (overridden by advertised name)
|
||||
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())
|
||||
"UVC CAM1", // 4: UVC Interface name for Cam1 (overridden by get_uvc_device_name())
|
||||
"CDC", // 5: placeholder (unused)
|
||||
"CDC Interface", // 6: CDC Interface name (overridden to advertised name)
|
||||
};
|
||||
|
||||
static uint16_t _desc_str[32];
|
||||
@@ -249,7 +255,8 @@ uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid)
|
||||
// Allow dynamic overrides for specific indices
|
||||
if (index == STRID_SERIAL)
|
||||
str = get_serial_number();
|
||||
if (index == STRID_UVC_CAM1)
|
||||
// Unify all user-visible names (Product, UVC interface, CDC interface) to advertised name
|
||||
if (index == STRID_UVC_CAM1 || index == STRID_PRODUCT || index == STRID_CDC)
|
||||
str = get_uvc_device_name();
|
||||
if (str == NULL)
|
||||
str = string_desc_arr[index];
|
||||
|
||||
7
docs/led_patterns/camera_error.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="60" height="60" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="30" fill="#ff2d55">
|
||||
<!-- Pattern: ON300 OFF300 ON300 OFF700 (1600ms) -->
|
||||
<animate attributeName="fill-opacity" dur="1.6s" repeatCount="indefinite" calcMode="discrete"
|
||||
values="1;0;1;0;0" keyTimes="0;0.1875;0.375;0.5625;1" />
|
||||
</circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
3
docs/led_patterns/idle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="60" height="60" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="30" fill="#444"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 139 B |
3
docs/led_patterns/stopped.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="60" height="60" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="30" fill="#222"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 139 B |
3
docs/led_patterns/streaming.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="60" height="60" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="30" fill="#ffd60a"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 142 B |
7
docs/led_patterns/wifi_connected.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="60" height="60" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="30" fill="#34c759">
|
||||
<!-- Pattern: (ON150 OFF150)x3 then OFF600 (total 1350ms) -->
|
||||
<animate attributeName="fill-opacity" dur="1.35s" repeatCount="indefinite" calcMode="discrete"
|
||||
values="1;0;1;0;1;0" keyTimes="0;0.1111;0.2222;0.3333;0.4444;1" />
|
||||
</circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 391 B |
5
docs/led_patterns/wifi_connecting.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="60" height="60" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle id="dot" cx="50" cy="50" r="30" fill="#007aff">
|
||||
<animate attributeName="fill-opacity" values="1;0;1" dur="0.8s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 254 B |
7
docs/led_patterns/wifi_error.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="60" height="60" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="30" fill="#ff9500">
|
||||
<!-- Pattern: ON200 OFF100 ON500 OFF300 (1100ms) -->
|
||||
<animate attributeName="fill-opacity" dur="1.1s" repeatCount="indefinite" calcMode="discrete"
|
||||
values="1;0;1;0;0" keyTimes="0;0.1818;0.2727;0.7273;1" />
|
||||
</circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 372 B |
@@ -7,28 +7,64 @@ endmenu
|
||||
menu "OpenIris: General Configuration"
|
||||
|
||||
config START_IN_UVC_MODE
|
||||
bool "Start in UVC Mode"
|
||||
bool "Default initial streaming mode = UVC"
|
||||
default false
|
||||
help
|
||||
Enables UVC (wired) support in the firmware by default.
|
||||
To be used when a board is designed to be used primarily with wired headsets.
|
||||
When enabled, the default device streaming mode will be UVC unless overridden by a
|
||||
saved preference. When disabled, the default mode is AUTO.
|
||||
Sets the power‑on default streaming mode (before any user preference is stored).
|
||||
If enabled AND UVC support is compiled in (GENERAL_INCLUDE_UVC_MODE), the device
|
||||
will default to UVC mode on first boot. If disabled it defaults to SETUP mode,
|
||||
waiting for a user choice or commands. This option does NOT compile UVC support in;
|
||||
it only changes the initial preference used when no saved mode exists.
|
||||
|
||||
config GENERAL_DEFAULT_WIRED_MODE
|
||||
bool "Wired mode"
|
||||
config GENERAL_INCLUDE_UVC_MODE
|
||||
bool "Include UVC (USB Video Class) support"
|
||||
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.
|
||||
Compiles in UVC (USB Video Class) streaming support (camera + CDC bridge).
|
||||
Disable this on boards that are Wi‑Fi only or where USB bandwidth / memory
|
||||
should be conserved. If disabled any attempt to switch to UVC mode will log
|
||||
an error and fall back to Wi‑Fi (if wireless is enabled). Combine with
|
||||
START_IN_UVC_MODE only when the hardware supports UVC.
|
||||
|
||||
config GENERAL_UVC_DELAY
|
||||
int "UVC delay (s)"
|
||||
default 30
|
||||
config GENERAL_STARTUP_DELAY
|
||||
int "Setup grace period (s)"
|
||||
default 20
|
||||
range 10 10000
|
||||
help
|
||||
Delay in seconds before the ESP reports itself as a UVC device.
|
||||
Number of seconds the device remains in SETUP / heartbeat mode on boot (when the
|
||||
current streaming mode resolves to SETUP) before automatically launching the
|
||||
selected streaming backend (UVC or Wi‑Fi). During this window host commands can
|
||||
change mode or other settings. After the timer expires, streaming starts
|
||||
automatically unless a command was received or startup was paused.
|
||||
|
||||
config GENERAL_ENABLE_WIRELESS
|
||||
bool "Enable wireless (WiFi/Bluetooth)"
|
||||
default y
|
||||
help
|
||||
When disabled, the firmware will not start WiFi or related services (mDNS/REST),
|
||||
and any Bluetooth memory (if present on the SoC) should be left released. This can
|
||||
reduce power consumption when operating solely in UVC mode or without networking.
|
||||
|
||||
config GENERAL_BOARD
|
||||
string "Board / device identifier"
|
||||
default "OpenIris"
|
||||
help
|
||||
A human-readable board or device identifier exposed via the get_info command.
|
||||
|
||||
config GENERAL_VERSION
|
||||
string "Firmware version"
|
||||
default "0.0.1"
|
||||
help
|
||||
A firmware version string exposed via the get_info command.
|
||||
|
||||
config GENERAL_ADVERTISED_NAME
|
||||
string "Advertised device name (UVC + mDNS)"
|
||||
default "openiristracker"
|
||||
help
|
||||
Human-readable device name advertised uniformly across interfaces.
|
||||
Used as the default mDNS hostname and (indirectly) the UVC USB
|
||||
device name via get_uvc_device_name(). Users can still override
|
||||
the runtime hostname through preferences.
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -51,10 +87,7 @@ menu "OpenIris: Camera Configuration"
|
||||
endmenu
|
||||
|
||||
menu "OpenIris: WiFi Configuration"
|
||||
|
||||
config WIFI_MDNS_HOSTNAME
|
||||
string "mDNS hostname"
|
||||
default "openiristracker"
|
||||
# mDNS hostname now derives from GENERAL_ADVERTISED_NAME (no separate Kconfig)
|
||||
|
||||
config WIFI_SSID
|
||||
string "WiFi network name (SSID)"
|
||||
@@ -76,12 +109,20 @@ endmenu
|
||||
|
||||
menu "OpenIris: LED Configuration"
|
||||
|
||||
config LED_BLINK_GPIO
|
||||
int "Blink GPIO number"
|
||||
config LED_DEBUG_ENABLE
|
||||
bool "Enable debug/status LED"
|
||||
default y
|
||||
help
|
||||
When disabled the firmware will not drive the dedicated debug/status GPIO.
|
||||
Useful on boards without a discrete status LED. Error/state patterns can
|
||||
optionally be mirrored onto the external IR LED if LED_EXTERNAL_AS_DEBUG is set.
|
||||
|
||||
config LED_DEBUG_GPIO
|
||||
int "Debug LED GPIO number"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 8
|
||||
help
|
||||
GPIO number (IOxx) used to blink an onboard LED.
|
||||
GPIO number (IOxx) used to drive an onboard debug/status LED.
|
||||
Some GPIOs are reserved for other functions (e.g. flash) and cannot be used.
|
||||
|
||||
config LED_EXTERNAL_GPIO
|
||||
@@ -97,6 +138,17 @@ menu "OpenIris: LED Configuration"
|
||||
help
|
||||
Enable this if your board can control external IR LEDs.
|
||||
|
||||
config LED_EXTERNAL_AS_DEBUG
|
||||
bool "Mirror error pattern on external LED"
|
||||
depends on LED_EXTERNAL_CONTROL
|
||||
default n
|
||||
help
|
||||
When enabled and an error LED pattern is active, the external IR LED PWM output
|
||||
will blink (0% / 50% duty) to replicate the debug/status LED pattern. If
|
||||
LED_DEBUG_ENABLE is disabled this provides visual error feedback using only
|
||||
the external LED. Normal configured PWM brightness is restored when leaving
|
||||
the error pattern.
|
||||
|
||||
config LED_EXTERNAL_PWM_FREQ
|
||||
int "External LED PWM frequency (Hz)"
|
||||
default 5000
|
||||
@@ -114,4 +166,54 @@ menu "OpenIris: LED Configuration"
|
||||
Duty cycle of the PWM signal for external IR LEDs, in percent.
|
||||
0 means always off, 100 means always on.
|
||||
|
||||
endmenu
|
||||
|
||||
menu "OpenIris: Monitoring"
|
||||
|
||||
config MONITORING_LED_CURRENT
|
||||
bool "Enable LED current monitoring"
|
||||
default y
|
||||
help
|
||||
Enable sampling LED current via ADC and report it over commands.
|
||||
|
||||
config MONITORING_LED_ADC_GPIO
|
||||
int "ADC GPIO for LED current sense"
|
||||
depends on MONITORING_LED_CURRENT
|
||||
range 0 48
|
||||
default 3
|
||||
help
|
||||
GPIO connected to the current sense input (ADC1 on ESP32-S3: 1..10 supported).
|
||||
|
||||
config MONITORING_LED_GAIN
|
||||
int "Analog front-end gain/divider"
|
||||
depends on MONITORING_LED_CURRENT
|
||||
range 1 1024
|
||||
default 11
|
||||
help
|
||||
Divider or amplifier gain between shunt and ADC. The measured mV are divided by this value.
|
||||
|
||||
config MONITORING_LED_SHUNT_MILLIOHM
|
||||
int "Shunt resistance (milli-ohms)"
|
||||
depends on MONITORING_LED_CURRENT
|
||||
range 1 1000000
|
||||
default 22000
|
||||
help
|
||||
Shunt resistor value in milli-ohms. Current[mA] = 1000 * Vshunt[mV] / R[mΩ].
|
||||
|
||||
config MONITORING_LED_SAMPLES
|
||||
int "Filter window size (samples)"
|
||||
depends on MONITORING_LED_CURRENT
|
||||
range 1 200
|
||||
default 10
|
||||
help
|
||||
Moving-average window length for voltage filtering.
|
||||
|
||||
config MONITORING_LED_INTERVAL_MS
|
||||
int "Sampling interval (ms)"
|
||||
depends on MONITORING_LED_CURRENT
|
||||
range 10 60000
|
||||
default 500
|
||||
help
|
||||
Period between samples when background monitoring is active.
|
||||
|
||||
endmenu
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "freertos/queue.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
@@ -21,12 +22,18 @@
|
||||
#include <SerialManager.hpp>
|
||||
#include <RestAPI.hpp>
|
||||
#include <main_globals.hpp>
|
||||
#include <MonitoringManager.hpp>
|
||||
|
||||
#ifdef CONFIG_GENERAL_DEFAULT_WIRED_MODE
|
||||
#ifdef CONFIG_GENERAL_INCLUDE_UVC_MODE
|
||||
#include <UVCStream.hpp>
|
||||
#endif
|
||||
|
||||
#define BLINK_GPIO (gpio_num_t) CONFIG_LED_BLINK_GPIO
|
||||
#ifdef CONFIG_LED_DEBUG_ENABLE
|
||||
#define BLINK_GPIO (gpio_num_t) CONFIG_LED_DEBUG_GPIO
|
||||
#else
|
||||
// Use an invalid / unused GPIO when debug LED disabled to avoid accidental toggles
|
||||
#define BLINK_GPIO (gpio_num_t) -1
|
||||
#endif
|
||||
#define CONFIG_LED_C_PIN_GPIO (gpio_num_t) CONFIG_LED_EXTERNAL_GPIO
|
||||
|
||||
TaskHandle_t serialManagerHandle;
|
||||
@@ -52,12 +59,13 @@ StreamServer streamServer(80, stateManager);
|
||||
|
||||
auto *restAPI = new RestAPI("http://0.0.0.0:81", commandManager);
|
||||
|
||||
#ifdef CONFIG_GENERAL_DEFAULT_WIRED_MODE
|
||||
#ifdef CONFIG_GENERAL_INCLUDE_UVC_MODE
|
||||
UVCStreamManager uvcStream;
|
||||
#endif
|
||||
|
||||
auto ledManager = std::make_shared<LEDManager>(BLINK_GPIO, CONFIG_LED_C_PIN_GPIO, ledStateQueue, deviceConfig);
|
||||
auto *serialManager = new SerialManager(commandManager, &timerHandle, deviceConfig);
|
||||
std::shared_ptr<MonitoringManager> monitoringManager = std::make_shared<MonitoringManager>();
|
||||
|
||||
void startWiFiMode(bool shouldCloseSerialManager);
|
||||
void startWiredMode(bool shouldCloseSerialManager);
|
||||
@@ -81,14 +89,14 @@ int websocket_logger(const char *format, va_list args)
|
||||
|
||||
void launch_streaming()
|
||||
{
|
||||
// Note, when switching and later right away activating UVC mode when we were previously in WiFi or Auto mode, the WiFi
|
||||
// Note, when switching and later right away activating UVC mode when we were previously in WiFi or Setup mode, the WiFi
|
||||
// utilities will still be running since we've launched them with startAutoMode() -> startWiFiMode()
|
||||
// we could add detection of this case, but it's probably not worth it since the next start of the device literally won't launch them
|
||||
// and we're telling folks to just reboot the device anyway
|
||||
// same case goes for when switching from UVC to WiFi
|
||||
|
||||
StreamingMode deviceMode = deviceConfig->getDeviceMode();
|
||||
// if we've changed the mode from auto to something else, we can clean up serial manager
|
||||
// if we've changed the mode from setup to something else, we can clean up serial manager
|
||||
// either the API endpoints or CDC will take care of further configuration
|
||||
if (deviceMode == StreamingMode::WIFI)
|
||||
{
|
||||
@@ -98,10 +106,10 @@ void launch_streaming()
|
||||
{
|
||||
startWiredMode(true);
|
||||
}
|
||||
else if (deviceMode == StreamingMode::AUTO)
|
||||
else if (deviceMode == StreamingMode::SETUP)
|
||||
{
|
||||
// we're still in auto, the user didn't select anything yet, let's give a bit of time for them to make a choice
|
||||
ESP_LOGI("[MAIN]", "No mode was selected, staying in AUTO mode. WiFi streaming will be enabled still. \nPlease select another mode if you'd like.");
|
||||
// we're still in setup, the user didn't select anything yet, let's give a bit of time for them to make a choice
|
||||
ESP_LOGI("[MAIN]", "No mode was selected, staying in SETUP mode. WiFi streaming will be enabled still. \nPlease select another mode if you'd like.");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -150,7 +158,7 @@ void force_activate_streaming()
|
||||
|
||||
void startWiredMode(bool shouldCloseSerialManager)
|
||||
{
|
||||
#ifndef CONFIG_GENERAL_DEFAULT_WIRED_MODE
|
||||
#ifndef CONFIG_GENERAL_INCLUDE_UVC_MODE
|
||||
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;
|
||||
@@ -211,7 +219,7 @@ void startWiFiMode(bool shouldCloseSerialManager)
|
||||
ESP_LOGI("[MAIN]", "We're still connected to serial. Serial manager task will remain running.");
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CONFIG_GENERAL_ENABLE_WIRELESS
|
||||
wifiManager->Begin();
|
||||
mdnsManager.start();
|
||||
restAPI->begin();
|
||||
@@ -223,16 +231,20 @@ void startWiFiMode(bool shouldCloseSerialManager)
|
||||
restAPI,
|
||||
1, // it's the rest API, we only serve commands over it so we don't really need a higher priority
|
||||
nullptr);
|
||||
#else
|
||||
ESP_LOGW("[MAIN]", "Wireless is disabled by configuration; skipping WiFi/mDNS/REST startup.");
|
||||
#endif
|
||||
}
|
||||
|
||||
void startSetupMode()
|
||||
{
|
||||
// If we're in an auto mode - Device starts with a 20-second delay before deciding on what to do
|
||||
// If we're in SETUP mode - Device starts with a 20-second delay before deciding on what to do
|
||||
// during this time we await any commands
|
||||
const uint64_t startup_delay_s = CONFIG_GENERAL_STARTUP_DELAY;
|
||||
ESP_LOGI("[MAIN]", "=====================================");
|
||||
ESP_LOGI("[MAIN]", "STARTUP: 20-SECOND DELAY MODE ACTIVE");
|
||||
ESP_LOGI("[MAIN]", "STARTUP: %llu-SECOND DELAY MODE ACTIVE", (unsigned long long)startup_delay_s);
|
||||
ESP_LOGI("[MAIN]", "=====================================");
|
||||
ESP_LOGI("[MAIN]", "Device will wait 20 seconds for commands...");
|
||||
ESP_LOGI("[MAIN]", "Device will wait %llu seconds for commands...", (unsigned long long)startup_delay_s);
|
||||
|
||||
// Create a one-shot timer for 20 seconds
|
||||
const esp_timer_create_args_t startup_timer_args = {
|
||||
@@ -243,17 +255,21 @@ void startSetupMode()
|
||||
.skip_unhandled_events = false};
|
||||
|
||||
ESP_ERROR_CHECK(esp_timer_create(&startup_timer_args, &timerHandle));
|
||||
ESP_ERROR_CHECK(esp_timer_start_once(timerHandle, CONFIG_GENERAL_UVC_DELAY * 1000000));
|
||||
ESP_LOGI("[MAIN]", "Started 20-second startup timer");
|
||||
ESP_LOGI("[MAIN]", "Send any command within 20 seconds to enter heartbeat mode");
|
||||
ESP_ERROR_CHECK(esp_timer_start_once(timerHandle, startup_delay_s * 1000000));
|
||||
ESP_LOGI("[MAIN]", "Started %llu-second startup timer", (unsigned long long)startup_delay_s);
|
||||
ESP_LOGI("[MAIN]", "Send any command within %llu seconds to enter heartbeat mode", (unsigned long long)startup_delay_s);
|
||||
}
|
||||
|
||||
extern "C" void app_main(void)
|
||||
{
|
||||
dependencyRegistry->registerService<ProjectConfig>(DependencyType::project_config, deviceConfig);
|
||||
dependencyRegistry->registerService<CameraManager>(DependencyType::camera_manager, cameraHandler);
|
||||
// Register WiFiManager only when wireless is enabled to avoid exposing WiFi commands in no-wireless builds
|
||||
#ifdef CONFIG_GENERAL_ENABLE_WIRELESS
|
||||
dependencyRegistry->registerService<WiFiManager>(DependencyType::wifi_manager, wifiManager);
|
||||
#endif
|
||||
dependencyRegistry->registerService<LEDManager>(DependencyType::led_manager, ledManager);
|
||||
dependencyRegistry->registerService<MonitoringManager>(DependencyType::monitoring_manager, monitoringManager);
|
||||
|
||||
// add endpoint to check firmware version
|
||||
// add firmware version somewhere
|
||||
@@ -266,6 +282,8 @@ extern "C" void app_main(void)
|
||||
initNVSStorage();
|
||||
deviceConfig->load();
|
||||
ledManager->setup();
|
||||
monitoringManager->setup();
|
||||
monitoringManager->start();
|
||||
|
||||
xTaskCreate(
|
||||
HandleStateManagerTask,
|
||||
@@ -316,7 +334,8 @@ extern "C" void app_main(void)
|
||||
{
|
||||
// since we're in setup mode, we have to have wireless functionality on,
|
||||
// so we can do wifi scanning, test connection etc
|
||||
startWiFiMode(false);
|
||||
// if wireless is disabled by configuration, we will not start WiFi services here
|
||||
startWiFiMode(false);
|
||||
startSetupMode();
|
||||
}
|
||||
}
|
||||
19
sdkconfig
@@ -571,8 +571,12 @@ CONFIG_ENV_GPIO_OUT_RANGE_MAX=48
|
||||
# OpenIris: General Configuration
|
||||
#
|
||||
# CONFIG_START_IN_UVC_MODE is not set
|
||||
CONFIG_GENERAL_DEFAULT_WIRED_MODE=y
|
||||
CONFIG_GENERAL_UVC_DELAY=30
|
||||
CONFIG_GENERAL_INCLUDE_UVC_MODE=y
|
||||
CONFIG_GENERAL_STARTUP_DELAY=20
|
||||
CONFIG_GENERAL_ENABLE_WIRELESS=y
|
||||
CONFIG_GENERAL_BOARD="project_babble"
|
||||
CONFIG_GENERAL_VERSION="0.0.1"
|
||||
CONFIG_GENERAL_ADVERTISED_NAME="openiristracker"
|
||||
# end of OpenIris: General Configuration
|
||||
|
||||
#
|
||||
@@ -585,7 +589,6 @@ CONFIG_CAMERA_WIFI_XCLK_FREQ=16500000
|
||||
#
|
||||
# OpenIris: WiFi Configuration
|
||||
#
|
||||
CONFIG_WIFI_MDNS_HOSTNAME="openiristracker"
|
||||
CONFIG_WIFI_SSID=""
|
||||
CONFIG_WIFI_PASSWORD=""
|
||||
CONFIG_WIFI_AP_SSID="EyeTrackVR"
|
||||
@@ -595,13 +598,21 @@ CONFIG_WIFI_AP_PASSWORD="12345678"
|
||||
#
|
||||
# OpenIris: LED Configuration
|
||||
#
|
||||
CONFIG_LED_BLINK_GPIO=8
|
||||
CONFIG_LED_DEBUG_ENABLE=y
|
||||
CONFIG_LED_DEBUG_GPIO=38
|
||||
CONFIG_LED_EXTERNAL_GPIO=1
|
||||
CONFIG_LED_EXTERNAL_CONTROL=y
|
||||
# CONFIG_LED_EXTERNAL_AS_DEBUG is not set
|
||||
CONFIG_LED_EXTERNAL_PWM_FREQ=5000
|
||||
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=100
|
||||
# end of OpenIris: LED Configuration
|
||||
|
||||
#
|
||||
# OpenIris: Monitoring
|
||||
#
|
||||
# CONFIG_MONITORING_LED_CURRENT is not set
|
||||
# end of OpenIris: Monitoring
|
||||
|
||||
#
|
||||
# Camera sensor pinout configuration
|
||||
#
|
||||
|
||||
2532
sdkconfig.old
@@ -378,7 +378,7 @@ class OpenIrisDevice:
|
||||
return True
|
||||
|
||||
def switch_mode(self, mode: str) -> bool:
|
||||
"""Switch device mode between WiFi, UVC, and Auto"""
|
||||
"""Switch device mode between WiFi, UVC, and Setup"""
|
||||
print(f"🔄 Switching device mode to '{mode}'...")
|
||||
|
||||
params = {"mode": mode}
|
||||
@@ -707,11 +707,9 @@ def configure_wifi(device: OpenIrisDevice, args = None):
|
||||
|
||||
def configure_mdns(device: OpenIrisDevice, args = None):
|
||||
current_name = device.get_mdns_name()
|
||||
print(f"\n📍 Current device name: {current_name} \n")
|
||||
print("💡 Please enter your preferred device name, your board will be accessible under http://<name>.local/")
|
||||
print("💡 Please avoid spaces and special characters")
|
||||
print(" To back out, enter `back`")
|
||||
print("\n Note, this will also modify the name of the UVC device")
|
||||
print(f"\n📍 Current advertised name: {current_name} \n")
|
||||
print("💡 This single name is used for both: mDNS (http://<name>.local/) and USB UVC device descriptor.")
|
||||
print("💡 Avoid spaces / special chars. Enter 'back' to cancel.")
|
||||
|
||||
while True:
|
||||
name_choice = input("\nDevice name: ").strip()
|
||||
@@ -906,7 +904,7 @@ def switch_device_mode(device: OpenIrisDevice, args = None):
|
||||
print("\n🔄 Select new device mode:")
|
||||
print("1. WiFi - Stream over WiFi connection")
|
||||
print("2. UVC - Stream as USB webcam")
|
||||
print("3. Auto - Automatic mode selection")
|
||||
print("3. Setup - Configuration mode")
|
||||
|
||||
mode_choice = input("\nSelect mode (1-3): ").strip()
|
||||
|
||||
@@ -915,7 +913,7 @@ def switch_device_mode(device: OpenIrisDevice, args = None):
|
||||
elif mode_choice == "2":
|
||||
device.switch_mode("uvc")
|
||||
elif mode_choice == "3":
|
||||
device.switch_mode("auto")
|
||||
device.switch_mode("setup")
|
||||
else:
|
||||
print("❌ Invalid mode selection")
|
||||
|
||||
@@ -975,11 +973,49 @@ def _probe_serial(device: OpenIrisDevice) -> Dict:
|
||||
serial, mac = info
|
||||
return {"serial": serial, "mac": mac}
|
||||
|
||||
def _probe_info(device: OpenIrisDevice) -> Dict:
|
||||
resp = device.send_command("get_who_am_i")
|
||||
if "error" in resp:
|
||||
return {"who_am_i": None, "version": None, "error": resp["error"]}
|
||||
try:
|
||||
results = resp.get("results", [])
|
||||
if results:
|
||||
result_data = json.loads(results[0])
|
||||
payload = result_data["result"]
|
||||
if isinstance(payload, str):
|
||||
payload = json.loads(payload)
|
||||
return {"who_am_i": payload.get("who_am_i"), "version": payload.get("version")}
|
||||
except Exception as e:
|
||||
return {"who_am_i": None, "version": None, "error": str(e)}
|
||||
return {"who_am_i": None, "version": None}
|
||||
|
||||
def _probe_advertised_name(device: OpenIrisDevice) -> Dict:
|
||||
# Currently the advertised name == mdns hostname
|
||||
name = device.get_mdns_name()
|
||||
return {"advertised_name": name}
|
||||
|
||||
|
||||
def _probe_led_pwm(device: OpenIrisDevice) -> Dict:
|
||||
duty = device.get_led_duty_cycle()
|
||||
return {"led_external_pwm_duty_cycle": duty}
|
||||
|
||||
def _probe_led_current(device: OpenIrisDevice) -> Dict:
|
||||
# Query device for current in mA via new command
|
||||
resp = device.send_command("get_led_current")
|
||||
if "error" in resp:
|
||||
return {"led_current_ma": None, "error": resp["error"]}
|
||||
try:
|
||||
results = resp.get("results", [])
|
||||
if results:
|
||||
result_data = json.loads(results[0])
|
||||
payload = result_data["result"]
|
||||
if isinstance(payload, str):
|
||||
payload = json.loads(payload)
|
||||
return {"led_current_ma": float(payload.get("led_current_ma"))}
|
||||
except Exception as e:
|
||||
return {"led_current_ma": None, "error": str(e)}
|
||||
return {"led_current_ma": None}
|
||||
|
||||
|
||||
def _probe_mode(device: OpenIrisDevice) -> Dict:
|
||||
mode = device.get_device_mode()
|
||||
@@ -997,7 +1033,10 @@ def get_settings(device: OpenIrisDevice, args=None):
|
||||
|
||||
probes = [
|
||||
("Identity", _probe_serial),
|
||||
("AdvertisedName", _probe_advertised_name),
|
||||
("Info", _probe_info),
|
||||
("LED", _probe_led_pwm),
|
||||
("Current", _probe_led_current),
|
||||
("Mode", _probe_mode),
|
||||
("WiFi", _probe_wifi_status),
|
||||
]
|
||||
@@ -1023,6 +1062,20 @@ def get_settings(device: OpenIrisDevice, args=None):
|
||||
if not serial and not mac:
|
||||
print("🔑 Serial/MAC: unavailable")
|
||||
|
||||
# Advertised Name
|
||||
advertised_name_data = summary.get("AdvertisedName", {})
|
||||
if advertised_name := advertised_name_data.get("advertised_name"):
|
||||
print(f"📛 Name: {advertised_name}")
|
||||
|
||||
# Info
|
||||
info = summary.get("Info", {})
|
||||
who = info.get("who_am_i")
|
||||
ver = info.get("version")
|
||||
if who:
|
||||
print(f"🏷️ Device: {who}")
|
||||
if ver:
|
||||
print(f"🧭 Version: {ver}")
|
||||
|
||||
# LED
|
||||
led = summary.get("LED", {})
|
||||
duty = led.get("led_external_pwm_duty_cycle")
|
||||
@@ -1035,6 +1088,16 @@ def get_settings(device: OpenIrisDevice, args=None):
|
||||
mode = summary.get("Mode", {}).get("mode")
|
||||
print(f"🎚️ Mode: {mode if mode else 'unknown'}")
|
||||
|
||||
# Current
|
||||
current_section = summary.get("Current", {})
|
||||
if (led_current_ma := current_section.get("led_current_ma")) is not None:
|
||||
print(f"🔌 LED Current: {led_current_ma:.3f} mA")
|
||||
else:
|
||||
if (err := current_section.get("error")):
|
||||
print(f"🔌 LED Current: unavailable ({err})")
|
||||
else:
|
||||
print("🔌 LED Current: unavailable")
|
||||
|
||||
# WiFi
|
||||
wifi = summary.get("WiFi", {}).get("wifi_status", {})
|
||||
if wifi:
|
||||
@@ -1051,12 +1114,11 @@ def get_settings(device: OpenIrisDevice, args=None):
|
||||
COMMANDS_MAP = {
|
||||
"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,
|
||||
"3": start_streaming,
|
||||
"4": switch_device_mode,
|
||||
"5": set_led_duty_cycle,
|
||||
"6": monitor_logs,
|
||||
"7": get_settings,
|
||||
}
|
||||
|
||||
|
||||
@@ -1146,15 +1208,14 @@ def main():
|
||||
while True:
|
||||
print("\n🔧 Setup Options:")
|
||||
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(f"{str(2):>2} 📛 Configure advertised name (mDNS + UVC)")
|
||||
print(f"{str(3):>2} 🚀 Start streaming mode")
|
||||
print(f"{str(4):>2} 🔄 Switch device mode (WiFi/UVC/Setup)")
|
||||
print(f"{str(5):>2} 💡 Update PWM Duty Cycle")
|
||||
print(f"{str(6):>2} 📖 Monitor logs")
|
||||
print(f"{str(7):>2} 🧩 Get settings summary")
|
||||
print("exit 🚪 Exit")
|
||||
choice = input("\nSelect option (1-8): ").strip()
|
||||
choice = input("\nSelect option (1-7): ").strip()
|
||||
|
||||
if choice == "exit":
|
||||
break
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
import difflib
|
||||
import argparse
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
HEADER_COLOR = "\033[95m"
|
||||
OKGREEN = '\033[92m'
|
||||
@@ -7,38 +9,112 @@ WARNING = '\033[93m'
|
||||
OKBLUE = '\033[94m'
|
||||
ENDC = '\033[0m'
|
||||
|
||||
sdkconfig_defaults = "sdkconfig.base_defaults"
|
||||
supported_boards = [
|
||||
"xiao-esp32s3",
|
||||
"project_babble",
|
||||
"facefocusvr_face",
|
||||
"facefocusvr_eye"
|
||||
]
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-b", "--board", help="Board to switch to", choices=supported_boards)
|
||||
parser.add_argument("--dry-run", help="Dry run, won't modify files", action="store_true", required=False)
|
||||
parser.add_argument("--diff", help="Show the difference between base config and selected board", action="store_true", required=False)
|
||||
parser.add_argument("--ssid", help="Set the SSID for the selected board", required=False, type=str, default="")
|
||||
parser.add_argument("--password", help="Set the password For the provided network", required=False, type=str, default="")
|
||||
parser.add_argument("--clear-wifi", help="Should we clear the wifi details", action="store_true", required=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
BOARDS_DIR_NAME = "boards"
|
||||
SDKCONFIG_DEFAULTS_FILENAME = "sdkconfig.base_defaults"
|
||||
|
||||
def get_root_path() -> str:
|
||||
return os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]
|
||||
|
||||
|
||||
def get_boards_root() -> str:
|
||||
return os.path.join(get_root_path(), BOARDS_DIR_NAME)
|
||||
|
||||
|
||||
def enumerate_board_configs() -> Dict[str, str]:
|
||||
"""Walk the boards directory and build a mapping of board names to absolute file paths.
|
||||
|
||||
Naming strategy:
|
||||
- Relative path from boards/ to file with path separators replaced by '_'.
|
||||
- If the last two path segments are identical (e.g. project_babble/project_babble) collapse to a single segment.
|
||||
- For facefocusvr eye boards we keep eye_L / eye_R suffix to distinguish configs even though WHO_AM_I is same.
|
||||
"""
|
||||
boards_dir = get_boards_root()
|
||||
mapping: Dict[str, str] = {}
|
||||
if not os.path.isdir(boards_dir):
|
||||
return mapping
|
||||
for root, _dirs, files in os.walk(boards_dir):
|
||||
for f in files:
|
||||
if f == SDKCONFIG_DEFAULTS_FILENAME:
|
||||
continue
|
||||
rel_path = os.path.relpath(os.path.join(root, f), boards_dir)
|
||||
parts = rel_path.split(os.sep)
|
||||
if len(parts) >= 2 and parts[-1] == parts[-2]: # collapse duplicate tail
|
||||
parts = parts[:-1]
|
||||
board_key = "_".join(parts)
|
||||
mapping[board_key] = os.path.join(root, f)
|
||||
return mapping
|
||||
|
||||
|
||||
BOARD_CONFIGS = enumerate_board_configs()
|
||||
|
||||
def build_arg_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("-b", "--board", help="Board name (run with --list to see options). Flexible: accepts path-like or partial if unique.")
|
||||
p.add_argument("--list", help="List discovered boards and exit", action="store_true")
|
||||
p.add_argument("--dry-run", help="Dry run, won't modify files", action="store_true")
|
||||
p.add_argument("--diff", help="Show the difference between base config and selected board", action="store_true")
|
||||
p.add_argument("--ssid", help="Set the WiFi SSID", type=str, default="")
|
||||
p.add_argument("--password", help="Set the WiFi password", type=str, default="")
|
||||
p.add_argument("--clear-wifi", help="Clear WiFi credentials", action="store_true")
|
||||
return p
|
||||
|
||||
def list_boards():
|
||||
print("Discovered boards:")
|
||||
width = max((len(k) for k in BOARD_CONFIGS), default=0)
|
||||
for name, path in sorted(BOARD_CONFIGS.items()):
|
||||
print(f" {name.ljust(width)} -> {os.path.relpath(path, get_root_path())}")
|
||||
|
||||
def _suggest_boards(partial: str) -> List[str]:
|
||||
if not partial:
|
||||
return []
|
||||
partial_low = partial.lower()
|
||||
contains = [b for b in BOARD_CONFIGS if partial_low in b.lower()]
|
||||
if contains:
|
||||
return contains[:10]
|
||||
# fallback to fuzzy matching using difflib
|
||||
choices = list(BOARD_CONFIGS.keys())
|
||||
return difflib.get_close_matches(partial, choices, n=5, cutoff=0.4)
|
||||
|
||||
def normalize_board_name(raw: Optional[str]) -> Optional[str]:
|
||||
if raw is None:
|
||||
return None
|
||||
candidate = raw.strip()
|
||||
if not candidate:
|
||||
return None
|
||||
candidate = candidate.replace('\\', '/').rstrip('/')
|
||||
# strip leading folders like tools/, boards/
|
||||
parts = [p for p in candidate.split('/') if p not in ('.', '') and p not in ('tools', 'boards')]
|
||||
if parts:
|
||||
candidate = parts[-1] if len(parts) == 1 else "_".join(parts)
|
||||
candidate = candidate.replace('-', '_')
|
||||
# exact match
|
||||
if candidate in BOARD_CONFIGS:
|
||||
return candidate
|
||||
# try ending match
|
||||
endings = [b for b in BOARD_CONFIGS if b.endswith(candidate)]
|
||||
if len(endings) == 1:
|
||||
return endings[0]
|
||||
if len(endings) > 1:
|
||||
print(f"Ambiguous board '{raw}'. Could be: {', '.join(endings)}")
|
||||
return None
|
||||
# attempt case-insensitive
|
||||
lower_map = {b.lower(): b for b in BOARD_CONFIGS}
|
||||
if candidate.lower() in lower_map:
|
||||
return lower_map[candidate.lower()]
|
||||
return None
|
||||
|
||||
|
||||
def get_main_config_path() -> str:
|
||||
return os.path.join(get_root_path(), "sdkconfig")
|
||||
|
||||
|
||||
def get_board_config_path() -> str:
|
||||
return os.path.join(get_root_path(), f"sdkconfig.board.{args.board}")
|
||||
def get_board_config_path(board_key: str) -> str:
|
||||
return BOARD_CONFIGS[board_key]
|
||||
|
||||
|
||||
def get_base_config_path() -> str:
|
||||
return os.path.join(get_root_path(), sdkconfig_defaults)
|
||||
# base defaults moved under boards directory
|
||||
return os.path.join(get_boards_root(), SDKCONFIG_DEFAULTS_FILENAME)
|
||||
|
||||
|
||||
def parse_config(config_file) -> dict:
|
||||
@@ -53,15 +129,17 @@ def parse_config(config_file) -> dict:
|
||||
return config
|
||||
|
||||
|
||||
def handle_wifi_config(_new_config: dict, _main_config: dict) -> dict:
|
||||
if args.ssid:
|
||||
_new_config["CONFIG_WIFI_SSID"] = f"\"{args.ssid}\""
|
||||
_new_config["CONFIG_WIFI_PASSWORD"] = f"\"{args.password}\""
|
||||
def handle_wifi_config(_new_config: dict, _main_config: dict, _args) -> dict:
|
||||
if _args.ssid:
|
||||
_new_config["CONFIG_WIFI_SSID"] = f"\"{_args.ssid}\""
|
||||
_new_config["CONFIG_WIFI_PASSWORD"] = f"\"{_args.password}\""
|
||||
else:
|
||||
_new_config["CONFIG_WIFI_SSID"] = _main_config["CONFIG_WIFI_SSID"]
|
||||
_new_config["CONFIG_WIFI_PASSWORD"] = _main_config["CONFIG_WIFI_PASSWORD"]
|
||||
if "CONFIG_WIFI_SSID" in _main_config:
|
||||
_new_config["CONFIG_WIFI_SSID"] = _main_config["CONFIG_WIFI_SSID"]
|
||||
if "CONFIG_WIFI_PASSWORD" in _main_config:
|
||||
_new_config["CONFIG_WIFI_PASSWORD"] = _main_config["CONFIG_WIFI_PASSWORD"]
|
||||
|
||||
if args.clear_wifi:
|
||||
if _args.clear_wifi:
|
||||
_new_config["CONFIG_WIFI_SSID"] = "\"\""
|
||||
_new_config["CONFIG_WIFI_PASSWORD"] = "\"\""
|
||||
return _new_config
|
||||
@@ -79,51 +157,65 @@ def compute_diff(_parsed_base_config: dict, _parsed_board_config: dict) -> dict:
|
||||
return _diff
|
||||
|
||||
|
||||
print(f"{OKGREEN}Switching configuration to board:{ENDC} {OKBLUE}{args.board}{ENDC}")
|
||||
print(f"{OKGREEN}Using defaults from :{ENDC} {get_base_config_path()}", )
|
||||
print(f"{OKGREEN}Using board config from :{ENDC} {get_board_config_path()}")
|
||||
def main():
|
||||
parser = build_arg_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
main_config = open(get_main_config_path(), "r+")
|
||||
parsed_main_config = parse_config(main_config)
|
||||
main_config.close()
|
||||
if args.list:
|
||||
list_boards()
|
||||
return
|
||||
|
||||
base_config = open(get_base_config_path(), "r")
|
||||
board_config = open(get_board_config_path(), "r")
|
||||
board_input = args.board
|
||||
if not board_input:
|
||||
parser.error("--board is required (or use --list)")
|
||||
normalized = normalize_board_name(board_input)
|
||||
if not normalized:
|
||||
print(f"{WARNING}Unknown board '{board_input}'.")
|
||||
suggestions = _suggest_boards(board_input)
|
||||
if suggestions:
|
||||
print("Did you mean: " + ", ".join(suggestions))
|
||||
print("Use --list to see all boards.")
|
||||
raise SystemExit(2)
|
||||
|
||||
parsed_base_config = parse_config(base_config)
|
||||
parsed_board_config = parse_config(board_config)
|
||||
if not os.path.isfile(get_base_config_path()):
|
||||
raise SystemExit(f"Base defaults file not found: {get_base_config_path()}")
|
||||
|
||||
base_config.close()
|
||||
board_config.close()
|
||||
print(f"{OKGREEN}Switching configuration to board:{ENDC} {OKBLUE}{normalized}{ENDC}")
|
||||
print(f"{OKGREEN}Using defaults from :{ENDC} {get_base_config_path()}")
|
||||
print(f"{OKGREEN}Using board config from :{ENDC} {get_board_config_path(normalized)}")
|
||||
|
||||
new_board_config = {**parsed_base_config, **parsed_board_config}
|
||||
new_board_config = handle_wifi_config(new_board_config, parsed_main_config)
|
||||
with open(get_main_config_path(), "r+") as main_config:
|
||||
parsed_main_config = parse_config(main_config)
|
||||
|
||||
if args.diff:
|
||||
print("---"*5, f"{WARNING}DIFF{ENDC}", "---"*5)
|
||||
diff = compute_diff(parsed_main_config, new_board_config)
|
||||
if not diff:
|
||||
print(f"{HEADER_COLOR}[DIFF]{ENDC} Nothing has changed between the base config and {OKBLUE}{args.board}{ENDC} config")
|
||||
with open(get_base_config_path(), "r") as base_config, open(get_board_config_path(normalized), "r") as board_config:
|
||||
parsed_base_config = parse_config(base_config)
|
||||
parsed_board_config = parse_config(board_config)
|
||||
|
||||
new_board_config = {**parsed_base_config, **parsed_board_config}
|
||||
new_board_config = handle_wifi_config(new_board_config, parsed_main_config, args)
|
||||
|
||||
if args.diff:
|
||||
print("---"*5, f"{WARNING}DIFF{ENDC}", "---"*5)
|
||||
diff = compute_diff(parsed_main_config, new_board_config)
|
||||
if not diff:
|
||||
print(f"{HEADER_COLOR}[DIFF]{ENDC} No changes between existing main config and {OKBLUE}{normalized}{ENDC}")
|
||||
else:
|
||||
print(f"{HEADER_COLOR}[DIFF]{ENDC} Keys differing (main -> new {OKBLUE}{normalized}{ENDC}):")
|
||||
for key in sorted(diff):
|
||||
print(f"{HEADER_COLOR}[DIFF]{ENDC} {key} : {diff[key]}")
|
||||
print("---"*14)
|
||||
|
||||
if not args.dry_run:
|
||||
print(f"{WARNING}Writing changes to main config file{ENDC}")
|
||||
with open(get_main_config_path(), "w") as main_config:
|
||||
for key, value in new_board_config.items():
|
||||
if value:
|
||||
main_config.write(f"{key}={value}\n")
|
||||
else:
|
||||
main_config.write(f"{key}\n")
|
||||
else:
|
||||
print(f"{HEADER_COLOR}[DIFF]{ENDC} The following keys have changed between the base config and {OKBLUE}{args.board}{ENDC} config:")
|
||||
for key in diff:
|
||||
print(f"{HEADER_COLOR}[DIFF]{ENDC} {key} : {diff[key]}")
|
||||
print("---"*14)
|
||||
print(f"{WARNING}[DRY-RUN]{ENDC} Skipping writing to files")
|
||||
print(f"{OKGREEN}Done. ESP-IDF is setup to build for:{ENDC} {OKBLUE}{normalized}{ENDC}")
|
||||
|
||||
if not args.dry_run:
|
||||
# the main idea is to always replace the main config with the base config
|
||||
# and then add the board config on top of that, overriding where necessary.
|
||||
# This way we can have known working defaults safe from accidental modifications by espidf
|
||||
# with know working per-board config
|
||||
# and a still modifiable sdkconfig for espidf
|
||||
|
||||
print(f"{WARNING}Writing changes to main config file{ENDC}")
|
||||
with open(get_main_config_path(), "w") as main_config:
|
||||
for key, value in new_board_config.items():
|
||||
if value:
|
||||
main_config.write(f"{key}={value}\n")
|
||||
else:
|
||||
main_config.write(f"{key}\n")
|
||||
else:
|
||||
print(f"{WARNING}[DRY-RUN]{ENDC} Skipping writing to files")
|
||||
print(f"{OKGREEN}Done. ESP-IDF is setup to build for:{ENDC} {OKBLUE}{args.board}{ENDC}")
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||