Merge pull request #1 from EyeTrackVR/feature/add-support-for-other-boards

Feature/add support for other boards
This commit is contained in:
Lorow
2025-12-14 21:11:26 +01:00
committed by GitHub
57 changed files with 4756 additions and 873 deletions
+119
View File
@@ -0,0 +1,119 @@
name: Build nad Release the OpenIris bin files
on:
workflow_dispatch:
push:
tags:
- "*.*.*"
branches:
- "main"
- "beta"
pull_request:
types:
- opened
- reopened
- synchronize
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
deployments: write
jobs:
setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: echo "matrix={\"firmware_config\":[{\"board_name\":\"esp32AIThinker\", \"target\":\"esp32\"}, {\"board_name\":\"esp32M5Stack\", \"target\":\"esp32\"}, {\"board_name\":\"esp32cam\", \"target\":\"esp32\"}, {\"board_name\":\"esp_eye\", \"target\":\"esp32s3\"}, {\"board_name\":\"facefocusvr_eye_L\", \"target\":\"esp32s3\"}, {\"board_name\":\"facefocusvr_eye_R\", \"target\":\"esp32s3\"}, {\"board_name\":\"facefocusvr_face\", \"target\":\"esp32s3\"}, {\"board_name\":\"project_babble\", \"target\":\"esp32s3\"}, {\"board_name\":\"seed_studio_xiao_esp32s3\", \"target\":\"esp32s3\"}, {\"board_name\":\"wrooms3\", \"target\":\"esp32s3\"}, {\"board_name\":\"wrooms3QIO\", \"target\":\"esp32s3\"}, {\"board_name\":\"wrover\", \"target\":\"esp32s3\"}]}" >> $GITHUB_OUTPUT
build:
needs: setup
permissions:
contents: write
strategy:
fail-fast: false
matrix: ${{fromJson(needs.setup.outputs.matrix)}}
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5
with:
submodules: "recursive"
- name: Setup UV
uses: astral-sh/setup-uv@v6
- name: Set up Python
run: uv python install
- name: Setup SDKConfig
run: uv run ./tools/switchBoardType.py --board ${{ matrix.firmware_config.board_name }} --diff
- name: Show SDKConfig
run: cat ./sdkconfig
- name: Build
uses: espressif/esp-idf-ci-action@v1
with:
esp_idf_version: v5.4.2
target: ${{ matrix.firmware_config.target }}
path: ./
- name: Merge bins
uses: espressif/esp-idf-ci-action@v1
with:
esp_idf_version: v5.4.2
target: ${{ matrix.firmware_config.target }}
path: ./
command: idf.py merge-bin -f raw
- name: Zip the resulting bin
run: zip -r ${{matrix.firmware_config.board_name}}.zip build/merged-binary.bin
- name: Archive Firmware binaries
uses: actions/upload-artifact@v4
with:
name: ${{matrix.firmware_config.board_name}}-firmware
path: ./${{matrix.firmware_config.board_name}}.zip
retention-days: 5
if-no-files-found: error
release:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout repo
uses: actions/checkout@v5
- name: Prepare directory
run: mkdir -p build
- name: Download firmware builds
uses: actions/download-artifact@v4
with:
path: build/
- name: Make Release
uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag'
with:
files: build/*.zip
prerelease: ${{contains(github.ref_type, 'rc')}}
cleanup:
needs: [setup, release]
strategy:
fail-fast: false
matrix: ${{fromJson(needs.setup.outputs.matrix)}}
name: Cleanup actions
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: "♻️ remove build artifacts"
uses: geekyeggo/delete-artifact@v5
with:
name: ${{matrix.firmware_config.board_name}}-firmware
+3 -2
View File
@@ -90,11 +90,12 @@ sdkconfig
# Local History for Visual Studio Code
.history/
tests/.env
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
.ionide
*\__pycache__
+1
View File
@@ -0,0 +1 @@
3.13
-2
View File
@@ -1,5 +1,3 @@
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
set(EXTRA_COMPONENT_DIRS ${CMAKE_CURRENT_LIST_DIR}/components)
+101 -39
View File
@@ -13,12 +13,12 @@ Firmware and tools for OpenIris — WiFi, 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/setup_openiris.py` — interactive CLI for WiFi, 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`)
- Autodiscovered perboard configuration overlays under `boards/`
- Command framework (JSON over serial / CDC / REST) for mode switching, WiFi 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)
- 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`)
- Autodiscovered perboard configuration overlays under `boards/`
- Command framework (JSON over serial / CDC / REST) for mode switching, WiFi 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)
---
@@ -63,23 +63,32 @@ After this, youre ready for the Quick start below.
## Quick start
### 1) Grab UV.
We're using UV to manage our tools, grab and install it from [here](https://docs.astral.sh/uv/getting-started/installation/).
Once installed, you'll be able to just run the commands below and UV will take care of setting up everything.
### 1) Pick your board (loads the default configuration)
Boards are autodiscovered from the `boards/` directory. First list them, then pick one:
Windows (cmd):
```cmd
python .\tools\switchBoardType.py --list
python .\tools\switchBoardType.py --board seed_studio_xiao_esp32s3 --diff
uv run .\tools\switchBoardType.py --list
uv run .\tools\switchBoardType.py --board seed_studio_xiao_esp32s3 --diff
```
macOS/Linux (bash):
```bash
python3 ./tools/switchBoardType.py --list
python3 ./tools/switchBoardType.py --board seed_studio_xiao_esp32s3 --diff
uv run ./tools/switchBoardType.py --list
uv run ./tools/switchBoardType.py --board seed_studio_xiao_esp32s3 --diff
```
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`.
@@ -89,8 +98,8 @@ Notes:
- Set the target (e.g., ESP32S3).
- 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 WiFi services for pure wired builds: `GENERAL_ENABLE_WIRELESS=n`.
- (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 WiFi services for pure wired builds: `GENERAL_ENABLE_WIRELESS=n`.
### 3) Use the Python setup CLI (recommended)
@@ -99,19 +108,18 @@ Configure the device over USB serial.
Before you run it:
- If you still have the serial monitor open, close it (the port must be free).
- In VS Code, open the sidebar “ESPIDF: Explorer” and click “Open ESPIDF Terminal”. Well run the CLI there so Python and packages are in the right environment.
Then run:
```cmd
python .\tools\setup_openiris.py --port COMxx
uv run .\tools\setup_openiris.py --port COMxx
```
Examples:
- Windows: `python .\tools\setup_openiris.py --port COM69`, …
- macOS: idk
- Linux: idk
- Windows: `uv run .\tools\setup_openiris.py --port COM69`, …
- macOS: `uv run .\tools\setup_openiris.py --port \dev\tty<port>`
- Linux: `uv run .\tools\setup_openiris.py --port \dev\tty<port>`
What the CLI can do:
@@ -131,7 +139,9 @@ What the CLI can do:
- 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
@@ -144,10 +154,10 @@ Runtime override: If the setup CLI (or a JSON command) provides a new device nam
- Fast WiFi setup: in the CLI, go to “WiFi settings” → “Automatic setup”, then check “status”.
- Change name/MDNS: set the device name in the CLI, then replug USB — UVC will show the new name.
- Adjust brightness/LED: set LED PWM in the CLI.
- 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"}]}`
- 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"}]}`
---
@@ -156,19 +166,62 @@ Runtime override: If the setup CLI (or a JSON command) provides a new device nam
- `main/` — entry point
- `components/` — modules (Camera, WiFi, UVC, CommandManager, …)
- `tools/` — Python helper tools (board switch, setup CLI, scanner)
If you want to dig deeper: commands are mapped via the `CommandManager` under `components/CommandManager/...`.
- `tests/` - Hardware in the loop tests, with support for different boards and automatic skips if a board can't perform a given test
If you want to dig deeper: commands are mapped via the `CommandManager` under `components/CommandManager/...`.
---
## Running Hardware In The Loop Tests
In order to run the tests, you'll need to setup a couple of things:
- copy the `.env.example` file in `/tests/` and rename it to `.env`. Then, open it and fill out the network details - SSID and Password.
- plug in your board to your pc and wait for it to boot.
- open the terminal (`ctrl/cmd + j` in VSC) and head over to `/tests/` directory.
Once there, you can invoke the tests with `UV` like so:
```cmd
uv run pytest --board=<your board name> --connection=COM<the number your board connected to>
```
This will auto select every test we have, connect to your board automatically and have pytest skip tests that don't fit your board.
For example, tests involving switching modes to UVC and testing commands over there are disabled for esp32 based boards as only esp32s3 can do it. Same goes for WiFi for FaceFocus Boards.
If you see any fails, you can try rerunning them one by one with:
```cmd
uv run pytest --board=<your board name> --connection=COM<the number your board connected to> -k name_of_the_test
```
Or rerun every single failed test like so:
```cmd
uv run pytest --board=<your board name> --connection=COM<the number your board connected to> --lf
```
Sometimes tests will fail due to timeouts, this is normal.
You should see the tests starting to go off, with any luck - all of them passing, your board should also start reacting to the changes - reboots, blinking lights etc are **expected** as we're performing hardware in the loop tests.
### Warning:
After the testing session ends **WE WILL RESET THE BOARD**, any changes you've made yourself to it will be lost. This is done to ensure that the test we perform are unaffected by any changes done by said tests.
If we skipped that, tests involving adding fake networks would break some that rely on the board connecting to real ones in a timely manner, for example.
There is currently no way to skip that behavior.
## 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 newlineterminated JSON objects
Example newlineterminated JSON commands over CDC (one per line):
```
{"commands":[{"command":"ping"}]}
{"commands":[{"command":"get_who_am_i"}]}
@@ -176,59 +229,68 @@ Example newlineterminated JSON commands over CDC (one per line):
```
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 (0100) |
| LED_EXTERNAL_AS_DEBUG | Mirrors only error patterns onto external LED (0%/50%) when debug LED absent or also for redundancy |
| 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 (0100) |
| 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 WiFi overrides via `switchBoardType.py` flags. Duplicate trailing segment directories collapse to unique keys.
- UVC doesnt 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 its detected, then switch using `-b my_family_my_variant`.
4. Run `uv run tools/switchBoardType.py --list` to verify its 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 sanitycheck only the intended keys change.
- For WiFi 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). Nonerror patterns are not mirrored.
| State | Visual | Category | Timing Pattern (ms) | Meaning |
|-------|--------|----------|---------------------|---------|
| LedStateNone | ![idle](docs/led_patterns/idle.svg) | idle | (off) | No activity / heartbeat window waiting |
| LedStateStreaming | ![stream](docs/led_patterns/streaming.svg) | active | steady on | Streaming running (UVC or WiFi) |
| LedStateStoppedStreaming | ![stopped](docs/led_patterns/stopped.svg) | inactive | steady off | Streaming intentionally stopped |
| CameraError | ![camera error](docs/led_patterns/camera_error.svg) | error | 300/300 300/700 (loop) | Camera init/runtime failure (check sensor, ribbon, power) |
| WiFiStateConnecting | ![wifi connecting](docs/led_patterns/wifi_connecting.svg) | transitional | 400/400 (loop) | WiFi associating / DHCP pending |
| WiFiStateConnected | ![wifi connected](docs/led_patterns/wifi_connected.svg) | notification | 150/150×3 then 600 off | WiFi connected successfully |
| WiFiStateError | ![wifi error](docs/led_patterns/wifi_error.svg) | error | 200/100 500/300 (loop) | WiFi failed (auth timeout or no AP) |
| State | Visual | Category | Timing Pattern (ms) | Meaning |
| ------------------------ | --------------------------------------------------------- | ------------ | ---------------------- | --------------------------------------------------------- |
| LedStateNone | ![idle](docs/led_patterns/idle.svg) | idle | (off) | No activity / heartbeat window waiting |
| LedStateStreaming | ![stream](docs/led_patterns/streaming.svg) | active | steady on | Streaming running (UVC or WiFi) |
| LedStateStoppedStreaming | ![stopped](docs/led_patterns/stopped.svg) | inactive | steady off | Streaming intentionally stopped |
| CameraError | ![camera error](docs/led_patterns/camera_error.svg) | error | 300/300 300/700 (loop) | Camera init/runtime failure (check sensor, ribbon, power) |
| WiFiStateConnecting | ![wifi connecting](docs/led_patterns/wifi_connecting.svg) | transitional | 400/400 (loop) | WiFi associating / DHCP pending |
| WiFiStateConnected | ![wifi connected](docs/led_patterns/wifi_connected.svg) | notification | 150/150×3 then 600 off | WiFi connected successfully |
| WiFiStateError | ![wifi error](docs/led_patterns/wifi_error.svg) | error | 200/100 500/300 (loop) | WiFi failed (auth timeout or no AP) |
---
+63
View File
@@ -0,0 +1,63 @@
CONFIG_IDF_TARGET="esp32"
# CONFIG_IDF_TARGET_ESP32S3 is not set
# CONFIG_WIRED_MODE is not set
CONFIG_LED_DEBUG_GPIO=33
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240 is not set
# CONFIG_ESP32S3_SPIRAM_SUPPORT is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set
# 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="4MB"
# Camera sensor pinout configuration
CONFIG_CAMERA_MODULE_NAME="ESP32AITHINKER"
CONFIG_PWDN_GPIO_NUM=32
CONFIG_RESET_GPIO_NUM=-1
CONFIG_XCLK_GPIO_NUM=0
CONFIG_SIOD_GPIO_NUM=26
CONFIG_SIOC_GPIO_NUM=27
CONFIG_Y9_GPIO_NUM=35
CONFIG_Y8_GPIO_NUM=34
CONFIG_Y7_GPIO_NUM=39
CONFIG_Y6_GPIO_NUM=36
CONFIG_Y5_GPIO_NUM=21
CONFIG_Y4_GPIO_NUM=19
CONFIG_Y3_GPIO_NUM=18
CONFIG_Y2_GPIO_NUM=5
CONFIG_VSYNC_GPIO_NUM=25
CONFIG_HREF_GPIO_NUM=23
CONFIG_PCLK_GPIO_NUM=22
# end of Camera sensor pinout configuration
# CONFIG_FLASHMODE_QIO is not set
# CONFIG_FLASHMODE_QOUT is not set
# CONFIG_FLASHMODE_DIO is not set
CONFIG_SPIRAM_MODE_QUAD=y
# CONFIG_SPIRAM_MODE_OCT is not set
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_120M is not set
CONFIG_SPIRAM_SPEED_80M=y
# CONFIG_SUPPORTS_EXTERNAL_LED_CONTROL is not set
#
# OpenIris: Serial Communication Settings
#
CONFIG_UART_PORT_NUMBER=0
CONFIG_UART_RX_PIN=3
CONFIG_UART_TX_PIN=1
# end of OpenIris: Serial Communication Settings
# CONFIG_MONITORING_LED_CURRENT is not set
+62
View File
@@ -0,0 +1,62 @@
CONFIG_IDF_TARGET="esp32"
# CONFIG_IDF_TARGET_ESP32S3 is not set
# CONFIG_WIRED_MODE is not set
CONFIG_LED_DEBUG_GPIO=33
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240 is not set
# CONFIG_ESP32S3_SPIRAM_SUPPORT is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set
# 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="4MB"
# Camera sensor pinout configuration
CONFIG_CAMERA_MODULE_NAME="ESP32CAM"
CONFIG_PWDN_GPIO_NUM=32
CONFIG_RESET_GPIO_NUM=33
CONFIG_XCLK_GPIO_NUM=4
CONFIG_SIOD_GPIO_NUM=18
CONFIG_SIOC_GPIO_NUM=23
CONFIG_Y9_GPIO_NUM=36
CONFIG_Y8_GPIO_NUM=19
CONFIG_Y7_GPIO_NUM=21
CONFIG_Y6_GPIO_NUM=39
CONFIG_Y5_GPIO_NUM=35
CONFIG_Y4_GPIO_NUM=14
CONFIG_Y3_GPIO_NUM=13
CONFIG_Y2_GPIO_NUM=34
CONFIG_VSYNC_GPIO_NUM=5
CONFIG_HREF_GPIO_NUM=27
CONFIG_PCLK_GPIO_NUM=25
# end of Camera sensor pinout configuration
# CONFIG_FLASHMODE_QIO is not set
# CONFIG_FLASHMODE_QOUT is not set
# CONFIG_FLASHMODE_DIO is not set
CONFIG_SPIRAM_MODE_QUAD=y
# CONFIG_SPIRAM_MODE_OCT is not set
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_120M is not set
CONFIG_SPIRAM_SPEED_80M=y
# CONFIG_SUPPORTS_EXTERNAL_LED_CONTROL is not set
#
# OpenIris: Serial Communication Settings
#
CONFIG_UART_PORT_NUMBER=0
CONFIG_UART_RX_PIN=3
CONFIG_UART_TX_PIN=1
# end of OpenIris: Serial Communication Settings
# CONFIG_MONITORING_LED_CURRENT is not set
+64
View File
@@ -0,0 +1,64 @@
# TODO test out on real hardware
CONFIG_IDF_TARGET="esp32"
# CONFIG_IDF_TARGET_ESP32S3 is not set
# CONFIG_WIRED_MODE is not set
CONFIG_LED_DEBUG_GPIO=33
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240 is not set
# CONFIG_ESP32S3_SPIRAM_SUPPORT is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set
# 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="4MB"
# Camera sensor pinout configuration
CONFIG_CAMERA_MODULE_NAME="ESP32M5STACK"
CONFIG_PWDN_GPIO_NUM=-1
CONFIG_RESET_GPIO_NUM=15
CONFIG_XCLK_GPIO_NUM=27
CONFIG_SIOD_GPIO_NUM=25
CONFIG_SIOC_GPIO_NUM=23
CONFIG_Y9_GPIO_NUM=19
CONFIG_Y8_GPIO_NUM=36
CONFIG_Y7_GPIO_NUM=18
CONFIG_Y6_GPIO_NUM=39
CONFIG_Y5_GPIO_NUM=5
CONFIG_Y4_GPIO_NUM=34
CONFIG_Y3_GPIO_NUM=35
CONFIG_Y2_GPIO_NUM=17
CONFIG_VSYNC_GPIO_NUM=22
CONFIG_HREF_GPIO_NUM=26
CONFIG_PCLK_GPIO_NUM=21
# end of Camera sensor pinout configuration
# CONFIG_FLASHMODE_QIO is not set
# CONFIG_FLASHMODE_QOUT is not set
# CONFIG_FLASHMODE_DIO is not set
CONFIG_SPIRAM_MODE_QUAD=y
# CONFIG_SPIRAM_MODE_OCT is not set
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_120M is not set
CONFIG_SPIRAM_SPEED_80M=y
# CONFIG_SUPPORTS_EXTERNAL_LED_CONTROL is not set
#
# OpenIris: Serial Communication Settings
#
CONFIG_UART_PORT_NUMBER=0
CONFIG_UART_RX_PIN=3
CONFIG_UART_TX_PIN=1
# end of OpenIris: Serial Communication Settings
# CONFIG_MONITORING_LED_CURRENT is not set
+52
View File
@@ -0,0 +1,52 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_IDF_TARGET_ESP32S3=y
CONFIG_WIRED_MODE=y
CONFIG_BLINK_GPIO=38 # todo check this
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set
# 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="4MB"
# Camera sensor pinout configuration
CONFIG_CAMERA_MODULE_NAME="ESP322ESP_EYE"
CONFIG_PWDN_GPIO_NUM=-1
CONFIG_RESET_GPIO_NUM=-1
CONFIG_XCLK_GPIO_NUM=4
CONFIG_SIOD_GPIO_NUM=18
CONFIG_SIOC_GPIO_NUM=23
CONFIG_Y9_GPIO_NUM=36
CONFIG_Y8_GPIO_NUM=37
CONFIG_Y7_GPIO_NUM=38
CONFIG_Y6_GPIO_NUM=39
CONFIG_Y5_GPIO_NUM=14
CONFIG_Y4_GPIO_NUM=19
CONFIG_Y3_GPIO_NUM=13
CONFIG_Y2_GPIO_NUM=34
CONFIG_VSYNC_GPIO_NUM=5
CONFIG_HREF_GPIO_NUM=27
CONFIG_PCLK_GPIO_NUM=25
# end of Camera sensor pinout configuration
# CONFIG_FLASHMODE_QIO is not set
# CONFIG_FLASHMODE_QOUT is not set
# CONFIG_FLASHMODE_DIO is not set
CONFIG_SPIRAM_MODE_QUAD=y
# CONFIG_SPIRAM_MODE_OCT is not set
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_120M is not set
CONFIG_SPIRAM_SPEED_80M=y
# CONFIG_SUPPORTS_EXTERNAL_LED_CONTROL is not set
+1 -1
View File
@@ -68,4 +68,4 @@ 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"
CONFIG_GENERAL_ADVERTISED_NAME="FFVR Face"
+2
View File
@@ -1,3 +1,5 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_IDF_TARGET_ESP32S3=y
CONFIG_LED_DEBUG_GPIO=38
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
-1
View File
@@ -2228,7 +2228,6 @@ CONFIG_TUSB_PID=0x8000
CONFIG_TUSB_MANUFACTURER="ETVR"
CONFIG_TUSB_PRODUCT="OpenIris Camera"
CONFIG_TUSB_SERIAL_NUM="12345678"
# CONFIG_UVC_SUPPORT_TWO_CAM is not set
#
# USB Cam1 Config
+2
View File
@@ -1,3 +1,5 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_IDF_TARGET_ESP32S3=y
CONFIG_LED_DEBUG_GPIO=21
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
+51
View File
@@ -0,0 +1,51 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_IDF_TARGET_ESP32S3=y
CONFIG_WIRED_MODE=y
CONFIG_BLINK_GPIO=38
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
CONFIG_ESP32S3_SPIRAM_SUPPORT=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="WROOMS3"
CONFIG_PWDN_GPIO_NUM=-1
CONFIG_RESET_GPIO_NUM=-1
CONFIG_XCLK_GPIO_NUM=15
CONFIG_SIOD_GPIO_NUM=4
CONFIG_SIOC_GPIO_NUM=5
CONFIG_Y9_GPIO_NUM=16
CONFIG_Y8_GPIO_NUM=17
CONFIG_Y7_GPIO_NUM=18
CONFIG_Y6_GPIO_NUM=12
CONFIG_Y5_GPIO_NUM=10
CONFIG_Y4_GPIO_NUM=8
CONFIG_Y3_GPIO_NUM=9
CONFIG_Y2_GPIO_NUM=11
CONFIG_VSYNC_GPIO_NUM=6
CONFIG_HREF_GPIO_NUM=7
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 is not set
CONFIG_SPIRAM_MODE_QUAD=y
# CONFIG_SPIRAM_MODE_OCT is not set
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_120M is not set
CONFIG_SPIRAM_SPEED_80M=y
+52
View File
@@ -0,0 +1,52 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_IDF_TARGET_ESP32S3=y
CONFIG_WIRED_MODE=y
CONFIG_BLINK_GPIO=38
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
CONFIG_ESP32S3_SPIRAM_SUPPORT=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="WROOMS3"
CONFIG_PWDN_GPIO_NUM=-1
CONFIG_RESET_GPIO_NUM=-1
CONFIG_XCLK_GPIO_NUM=15
CONFIG_SIOD_GPIO_NUM=4
CONFIG_SIOC_GPIO_NUM=5
CONFIG_Y9_GPIO_NUM=16
CONFIG_Y8_GPIO_NUM=17
CONFIG_Y7_GPIO_NUM=18
CONFIG_Y6_GPIO_NUM=12
CONFIG_Y5_GPIO_NUM=10
CONFIG_Y4_GPIO_NUM=8
CONFIG_Y3_GPIO_NUM=9
CONFIG_Y2_GPIO_NUM=11
CONFIG_VSYNC_GPIO_NUM=6
CONFIG_HREF_GPIO_NUM=7
CONFIG_PCLK_GPIO_NUM=13
# end of Camera sensor pinout configuration
CONFIG_FLASHMODE_QIO = y
# CONFIG_FLASHMODE_QOUT is not set
# CONFIG_FLASHMODE_DIO is not set
CONFIG_SPIRAM_MODE_QUAD=y
# CONFIG_SPIRAM_MODE_OCT is not set
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_120M is not set
CONFIG_SPIRAM_SPEED_80M=y
# CONFIG_SUPPORTS_EXTERNAL_LED_CONTROL is not set
+52
View File
@@ -0,0 +1,52 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_IDF_TARGET_ESP32S3=y
CONFIG_WIRED_MODE=y
CONFIG_BLINK_GPIO=38
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set
# 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="4MB"
# Camera sensor pinout configuration
CONFIG_CAMERA_MODULE_NAME="WROVER"
CONFIG_PWDN_GPIO_NUM=-1
CONFIG_RESET_GPIO_NUM=-1
CONFIG_XCLK_GPIO_NUM=21
CONFIG_SIOD_GPIO_NUM=26
CONFIG_SIOC_GPIO_NUM=27
CONFIG_Y9_GPIO_NUM=35
CONFIG_Y8_GPIO_NUM=34
CONFIG_Y7_GPIO_NUM=39
CONFIG_Y6_GPIO_NUM=36
CONFIG_Y5_GPIO_NUM=19
CONFIG_Y4_GPIO_NUM=18
CONFIG_Y3_GPIO_NUM=5
CONFIG_Y2_GPIO_NUM=4
CONFIG_VSYNC_GPIO_NUM=25
CONFIG_HREF_GPIO_NUM=23
CONFIG_PCLK_GPIO_NUM=22
# end of Camera sensor pinout configuration
CONFIG_FLASHMODE_QIO=y
# CONFIG_FLASHMODE_QOUT is not set
# CONFIG_FLASHMODE_DIO is not set
CONFIG_SPIRAM_MODE_QUAD=y
# CONFIG_SPIRAM_MODE_OCT is not set
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_120M is not set
CONFIG_SPIRAM_SPEED_80M=y
# CONFIG_SUPPORTS_EXTERNAL_LED_CONTROL is not set
@@ -75,33 +75,16 @@ void CameraManager::setupCameraPinout()
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
// this causes problems
.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 = 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
.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
};
}
void CameraManager::setupBasicResolution()
{
if (!esp_psram_is_initialized())
{
ESP_LOGE(CAMERA_MANAGER_TAG, "PSRAM not initialized!");
ESP_LOGD(CAMERA_MANAGER_TAG, "Setting fb_location to CAMERA_FB_IN_DRAM with lower picture quality");
config.fb_location = CAMERA_FB_IN_DRAM;
config.jpeg_quality = 7;
config.fb_count = 2;
return;
}
ESP_LOGI(CAMERA_MANAGER_TAG, "PSRAM size: %u", esp_psram_get_size());
}
void CameraManager::setupCameraSensor()
{
ESP_LOGI(CAMERA_MANAGER_TAG, "Setting up camera sensor");
@@ -137,8 +120,8 @@ void CameraManager::setupCameraSensor()
// automatic gain control gain, controls by how much the resulting image
// should be amplified
camera_sensor->set_agc_gain(camera_sensor, 2); // 0 to 30
camera_sensor->set_gainceiling(camera_sensor, (gainceiling_t)6); // 0 to 6
camera_sensor->set_agc_gain(camera_sensor, 2); // 0 to 30
camera_sensor->set_gainceiling(camera_sensor, static_cast<gainceiling_t>(6)); // 0 to 6
// black and white pixel correction, averages the white and black spots
camera_sensor->set_bpc(camera_sensor, 1); // 0 = disable , 1 = enable
@@ -170,9 +153,6 @@ bool CameraManager::setupCamera()
{
ESP_LOGI(CAMERA_MANAGER_TAG, "Setting up camera pinout");
this->setupCameraPinout();
ESP_LOGI(CAMERA_MANAGER_TAG, "Setting up camera with resolution");
// this->setupBasicResolution();
ESP_LOGI(CAMERA_MANAGER_TAG, "Initializing camera...");
if (auto const hasCameraBeenInitialized = esp_camera_init(&config); hasCameraBeenInitialized == ESP_OK)
@@ -212,7 +192,6 @@ bool CameraManager::setupCamera()
#endif
this->setupCameraSensor();
// this->loadConfigData(); // move this to update method once implemented
return true;
}
@@ -36,7 +36,6 @@ private:
void loadConfigData();
void setupCameraPinout();
void setupCameraSensor();
void setupBasicResolution();
};
#endif // CAMERAMANAGER_HPP
@@ -26,7 +26,7 @@ std::unordered_map<std::string, CommandType> commandTypeMap = {
{"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},
{"get_who_am_i", CommandType::GET_WHO_AM_I},
};
std::function<CommandResult()> CommandManager::createCommand(const CommandType type, const nlohmann::json &json) const
@@ -161,5 +161,5 @@ CommandManagerResponse CommandManager::executeFromType(const CommandType type, c
return CommandManagerResponse({{"command", type}, {"error", "Unknown command"}});
}
return CommandManagerResponse({"result", command()});
return CommandManagerResponse(nlohmann::json{{"result", command()}});
}
@@ -5,7 +5,7 @@
CommandResult setWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
#if !CONFIG_GENERAL_ENABLE_WIRELESS
return CommandResult::getErrorResult("Not supported by current firmware");
return CommandResult::getErrorResult("Not supported by current firmware");
#endif
auto payload = json.get<WifiPayload>();
@@ -33,7 +33,7 @@ CommandResult setWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const
CommandResult deleteWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
#if !CONFIG_GENERAL_ENABLE_WIRELESS
return CommandResult::getErrorResult("Not supported by current firmware");
return CommandResult::getErrorResult("Not supported by current firmware");
#endif
const auto payload = json.get<deleteNetworkPayload>();
@@ -49,7 +49,7 @@ CommandResult deleteWiFiCommand(std::shared_ptr<DependencyRegistry> registry, co
CommandResult updateWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
#if !CONFIG_GENERAL_ENABLE_WIRELESS
return CommandResult::getErrorResult("Not supported by current firmware");
return CommandResult::getErrorResult("Not supported by current firmware");
#endif
auto payload = json.get<UpdateWifiPayload>();
@@ -82,7 +82,7 @@ CommandResult updateWiFiCommand(std::shared_ptr<DependencyRegistry> registry, co
CommandResult updateAPWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
#if !CONFIG_GENERAL_ENABLE_WIRELESS
return CommandResult::getErrorResult("Not supported by current firmware");
return CommandResult::getErrorResult("Not supported by current firmware");
#endif
const auto payload = json.get<UpdateAPWiFiPayload>();
+33 -5
View File
@@ -1,6 +1,34 @@
idf_component_register(SRCS
"Monitoring/CurrentMonitor.cpp"
"Monitoring/MonitoringManager.cpp"
INCLUDE_DIRS "Monitoring"
REQUIRES driver esp_adc Helpers
set(
requires
Helpers
)
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3")
list(APPEND requires
driver
esp_adc
)
endif()
set(
source_files
""
)
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3")
list(APPEND source_files
"Monitoring/CurrentMonitor_esp32s3.cpp"
"Monitoring/MonitoringManager_esp32s3.cpp"
)
else()
list(APPEND source_files
"Monitoring/CurrentMonitor_esp32.cpp"
"Monitoring/MonitoringManager_esp32.cpp"
)
endif()
idf_component_register(SRCS ${source_files}
INCLUDE_DIRS "Monitoring"
REQUIRES ${requires}
)
@@ -6,7 +6,8 @@
#include <vector>
#include "sdkconfig.h"
class CurrentMonitor {
class CurrentMonitor
{
public:
CurrentMonitor();
~CurrentMonitor() = default;
@@ -25,15 +26,15 @@ public:
// Whether monitoring is enabled by Kconfig
static constexpr bool isEnabled()
{
#ifdef CONFIG_MONITORING_LED_CURRENT
#ifdef CONFIG_MONITORING_LED_CURRENT
return true;
#else
#else
return false;
#endif
#endif
}
private:
#if CONFIG_MONITORING_LED_CURRENT
#ifdef CONFIG_MONITORING_LED_CURRENT
void init_adc();
int read_mv_once();
int gpio_to_adc_channel(int gpio);
@@ -0,0 +1,42 @@
#include "CurrentMonitor.hpp"
#include <esp_log.h>
static const char *TAG_CM = "[CurrentMonitor]";
CurrentMonitor::CurrentMonitor()
{
// empty as esp32 doesn't support this
// but without a separate implementation, the linker will complain :c
}
void CurrentMonitor::setup()
{
ESP_LOGI(TAG_CM, "LED current monitoring disabled");
}
float CurrentMonitor::getCurrentMilliAmps() const
{
return 0.0f;
}
float CurrentMonitor::pollAndGetMilliAmps()
{
sampleOnce();
return getCurrentMilliAmps();
}
void CurrentMonitor::sampleOnce()
{
(void)0;
}
#ifdef CONFIG_MONITORING_LED_CURRENT
void CurrentMonitor::init_adc()
{
}
int CurrentMonitor::read_mv_once()
{
return 0;
}
#endif
@@ -2,7 +2,7 @@
#include <esp_log.h>
#include <cmath>
#if CONFIG_MONITORING_LED_CURRENT
#ifdef CONFIG_MONITORING_LED_CURRENT
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
@@ -12,14 +12,14 @@ static const char *TAG_CM = "[CurrentMonitor]";
CurrentMonitor::CurrentMonitor()
{
#if CONFIG_MONITORING_LED_CURRENT
#ifdef CONFIG_MONITORING_LED_CURRENT
samples_.assign(CONFIG_MONITORING_LED_SAMPLES, 0);
#endif
}
void CurrentMonitor::setup()
{
#if CONFIG_MONITORING_LED_CURRENT
#ifdef CONFIG_MONITORING_LED_CURRENT
init_adc();
#else
ESP_LOGI(TAG_CM, "LED current monitoring disabled");
@@ -28,7 +28,7 @@ void CurrentMonitor::setup()
float CurrentMonitor::getCurrentMilliAmps() const
{
#if CONFIG_MONITORING_LED_CURRENT
#ifdef CONFIG_MONITORING_LED_CURRENT
const int shunt_milliohm = CONFIG_MONITORING_LED_SHUNT_MILLIOHM; // mΩ
if (shunt_milliohm <= 0)
return 0.0f;
@@ -48,7 +48,7 @@ float CurrentMonitor::pollAndGetMilliAmps()
void CurrentMonitor::sampleOnce()
{
#if CONFIG_MONITORING_LED_CURRENT
#ifdef 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)
@@ -76,7 +76,7 @@ void CurrentMonitor::sampleOnce()
#endif
}
#if CONFIG_MONITORING_LED_CURRENT
#ifdef CONFIG_MONITORING_LED_CURRENT
static adc_oneshot_unit_handle_t s_adc_handle = nullptr;
static adc_cali_handle_t s_cali_handle = nullptr;
@@ -4,9 +4,9 @@
#include <atomic>
#include "CurrentMonitor.hpp"
class MonitoringManager {
class MonitoringManager
{
public:
void setup();
void start();
void stop();
@@ -15,7 +15,7 @@ public:
float getCurrentMilliAmps() const { return last_current_ma_.load(); }
private:
static void taskEntry(void* arg);
static void taskEntry(void *arg);
void run();
TaskHandle_t task_{nullptr};
@@ -0,0 +1,25 @@
#include "MonitoringManager.hpp"
#include <esp_log.h>
static const char *TAG_MM = "[MonitoringManager]";
void MonitoringManager::setup()
{
ESP_LOGI(TAG_MM, "Monitoring disabled by Kconfig");
}
void MonitoringManager::start()
{
}
void MonitoringManager::stop()
{
}
void MonitoringManager::taskEntry(void *arg)
{
}
void MonitoringManager::run()
{
}
@@ -2,11 +2,11 @@
#include <esp_log.h>
#include "sdkconfig.h"
static const char* TAG_MM = "[MonitoringManager]";
static const char *TAG_MM = "[MonitoringManager]";
void MonitoringManager::setup()
{
#if CONFIG_MONITORING_LED_CURRENT
#ifdef 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,
@@ -20,7 +20,7 @@ void MonitoringManager::setup()
void MonitoringManager::start()
{
#if CONFIG_MONITORING_LED_CURRENT
#ifdef CONFIG_MONITORING_LED_CURRENT
if (task_ == nullptr)
{
xTaskCreate(&MonitoringManager::taskEntry, "MonitoringTask", 2048, this, 1, &task_);
@@ -38,14 +38,14 @@ void MonitoringManager::stop()
}
}
void MonitoringManager::taskEntry(void* arg)
void MonitoringManager::taskEntry(void *arg)
{
static_cast<MonitoringManager*>(arg)->run();
static_cast<MonitoringManager *>(arg)->run();
}
void MonitoringManager::run()
{
#if CONFIG_MONITORING_LED_CURRENT
#ifdef CONFIG_MONITORING_LED_CURRENT
while (true)
{
float ma = cm_.pollAndGetMilliAmps();
@@ -35,14 +35,14 @@ struct DeviceMode_t : BaseConfigModel
void load()
{
// Default mode can be controlled via sdkconfig:
// - If CONFIG_START_IN_UVC_MODE is enabled, default to UVC
// - Otherwise default to SETUP
// 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::SETUP);
static_cast<int>(StreamingMode::SETUP);
#endif
int stored_mode = this->pref->getInt("mode", default_mode);
@@ -103,14 +103,8 @@ struct MDNSConfig_t : BaseConfigModel
void load()
{
// 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
// Default hostname comes from GENERAL_ADVERTISED_NAME (unified advertised name)
std::string default_hostname = CONFIG_GENERAL_ADVERTISED_NAME;
if (default_hostname.empty())
{
default_hostname = "openiristracker";
@@ -146,7 +140,7 @@ struct CameraConfig_t : BaseConfigModel
{
this->vflip = this->pref->getInt("vflip", 0);
this->href = this->pref->getInt("href", 0);
this->framesize = this->pref->getInt("framesize", 4);
this->framesize = this->pref->getInt("framesize", 5);
this->quality = this->pref->getInt("quality", 7);
this->brightness = this->pref->getInt("brightness", 2);
};
@@ -331,14 +325,11 @@ public:
{
for (auto i = 0; i < this->networks.size() - 1; i++)
{
printf("we're at %d while networks size is %d ", i, this->networks.size() - 2);
WifiConfigRepresentation += Helpers::format_string("%s, ", this->networks[i].toRepresentation().c_str());
}
}
WifiConfigRepresentation += Helpers::format_string("%s", this->networks[networks.size() - 1].toRepresentation().c_str());
printf(WifiConfigRepresentation.c_str());
printf("\n");
}
return Helpers::format_string(
+68 -113
View File
@@ -2,7 +2,10 @@
#include <utility>
#define PATCH_METHOD "PATCH"
#define POST_METHOD "POST"
#define GET_METHOD "GET"
#define DELETE_METHOD "DELETE"
bool getIsSuccess(const nlohmann::json &response)
{
@@ -19,27 +22,55 @@ bool getIsSuccess(const nlohmann::json &response)
RestAPI::RestAPI(std::string url, std::shared_ptr<CommandManager> commandManager) : command_manager(commandManager)
{
// until we stumble on a simpler way to handle the commands over the rest api
// the formula will be like this:
// each command gets its own endpoint
// each endpoint must include the action it performs in its path
// for example
// /get/ for getters
// /set/ for posts
// /delete/ for deletes
// /update/ for updates
// additional actions on the resource should be appended after the resource name
// like for example /api/set/config/save/
//
// one endpoint must not contain more than one action
this->url = std::move(url);
// updates
routes.emplace("/api/update/wifi/", &RestAPI::handle_update_wifi);
routes.emplace("/api/update/device/", &RestAPI::handle_update_device);
routes.emplace("/api/update/camera/", &RestAPI::handle_update_camera);
// updates via PATCH
routes.emplace("/api/update/wifi/", RequestBaseData(PATCH_METHOD, CommandType::UPDATE_WIFI, 200, 400));
routes.emplace("/api/update/device/mode/", RequestBaseData(PATCH_METHOD, CommandType::SWITCH_MODE, 200, 400));
routes.emplace("/api/update/camera/", RequestBaseData(PATCH_METHOD, CommandType::UPDATE_CAMERA, 200, 400));
routes.emplace("/api/update/ota/credentials", RequestBaseData(PATCH_METHOD, CommandType::UPDATE_OTA_CREDENTIALS, 200, 400));
routes.emplace("/api/update/ap/", RequestBaseData(PATCH_METHOD, CommandType::UPDATE_AP_WIFI, 200, 400));
routes.emplace("/api/update/led_duty_cycle/", RequestBaseData(PATCH_METHOD, CommandType::SET_LED_DUTY_CYCLE, 200, 400));
// post will reset it
// resets
routes.emplace("/api/reset/config/", &RestAPI::handle_reset_config);
// gets
routes.emplace("/api/get/config/", &RestAPI::handle_get_config);
// POST will set the data
routes.emplace("/api/set/pause/", RequestBaseData(POST_METHOD, CommandType::PAUSE, 200, 400));
routes.emplace("/api/set/wifi/", RequestBaseData(POST_METHOD, CommandType::SET_WIFI, 200, 400));
routes.emplace("/api/set/mdns/", RequestBaseData(POST_METHOD, CommandType::SET_MDNS, 200, 400));
routes.emplace("/api/set/config/save/", RequestBaseData(POST_METHOD, CommandType::SAVE_CONFIG, 200, 400));
routes.emplace("/api/set/wifi/connect/", RequestBaseData(POST_METHOD, CommandType::CONNECT_WIFI, 200, 400));
// reboots
routes.emplace("/api/reboot/device/", &RestAPI::handle_reboot);
routes.emplace("/api/reboot/camera/", &RestAPI::handle_camera_reboot);
// resets via POST as well
routes.emplace("/api/reset/config/", RequestBaseData(POST_METHOD, CommandType::RESET_CONFIG, 200, 400));
// heartbeat
routes.emplace("/api/ping/", &RestAPI::pong);
// gets via GET
routes.emplace("/api/get/config/", RequestBaseData(GET_METHOD, CommandType::GET_CONFIG, 200, 400));
routes.emplace("/api/get/mdns/", RequestBaseData(GET_METHOD, CommandType::GET_MDNS_NAME, 200, 400));
routes.emplace("/api/get/led_duty_cycle/", RequestBaseData(GET_METHOD, CommandType::GET_LED_DUTY_CYCLE, 200, 400));
routes.emplace("/api/get/serial_number/", RequestBaseData(GET_METHOD, CommandType::GET_SERIAL, 200, 400));
routes.emplace("/api/get/led_current/", RequestBaseData(GET_METHOD, CommandType::GET_LED_CURRENT, 200, 400));
routes.emplace("/api/get/who_am_i/", RequestBaseData(GET_METHOD, CommandType::GET_WHO_AM_I, 200, 400));
// special
routes.emplace("/api/save/", &RestAPI::handle_save);
// deletes via DELETE
routes.emplace("/api/delete/wifi", RequestBaseData(DELETE_METHOD, CommandType::DELETE_NETWORK, 200, 400));
// reboots via POST
routes.emplace("/api/reboot/device/", RequestBaseData(GET_METHOD, CommandType::RESTART_DEVICE, 200, 500));
// heartbeat via GET
routes.emplace("/api/ping/", RequestBaseData(GET_METHOD, CommandType::PING, 200, 400));
}
void RestAPI::begin()
@@ -58,19 +89,24 @@ void RestAPI::handle_request(struct mg_connection *connection, int event, void *
auto const *message = static_cast<struct mg_http_message *>(event_data);
auto const uri = std::string(message->uri.buf, message->uri.len);
if (auto const handler = this->routes[uri])
{
auto *context = new RequestContext{
.connection = connection,
.method = std::string(message->method.buf, message->method.len),
.body = std::string(message->body.buf, message->body.len),
};
(*this.*handler)(context);
}
else
if (this->routes.find(uri) == this->routes.end())
{
mg_http_reply(connection, 404, "", "Wrong URL");
return;
}
auto const base_request_params = this->routes.at(uri);
auto *context = new RequestContext{
.connection = connection,
.method = std::string(message->method.buf, message->method.len),
.body = std::string(message->body.buf, message->body.len),
};
this->handle_endpoint_command(context,
base_request_params.allowed_method,
base_request_params.command_type,
base_request_params.success_code,
base_request_params.error_code);
}
}
@@ -95,97 +131,16 @@ void HandleRestAPIPollTask(void *pvParameter)
}
}
// COMMANDS
// updates
void RestAPI::handle_update_wifi(RequestContext *context)
void RestAPI::handle_endpoint_command(RequestContext *context, std::string allowed_method, CommandType command_type, int success_code, int error_code)
{
if (context->method != POST_METHOD)
if (context->method != allowed_method)
{
mg_http_reply(context->connection, 401, JSON_RESPONSE, "{%m:%m}", MG_ESC("error"), "Method not allowed");
return;
}
const nlohmann::json result = command_manager->executeFromType(CommandType::UPDATE_WIFI, context->body);
const auto code = getIsSuccess(result) ? 200 : 400;
const nlohmann::json result = command_manager->executeFromType(command_type, context->body);
const auto code = getIsSuccess(result) ? success_code : error_code;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.dump().c_str());
}
void RestAPI::handle_update_device(RequestContext *context)
{
if (context->method != POST_METHOD)
{
mg_http_reply(context->connection, 401, JSON_RESPONSE, "{%m:%m}", MG_ESC("error"), "Method not allowed");
return;
}
const nlohmann::json result = command_manager->executeFromType(CommandType::UPDATE_OTA_CREDENTIALS, context->body);
const auto code = getIsSuccess(result) ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.dump().c_str());
}
void RestAPI::handle_update_camera(RequestContext *context)
{
if (context->method != POST_METHOD)
{
mg_http_reply(context->connection, 401, JSON_RESPONSE, "{%m:%m}", MG_ESC("error"), "Method not allowed");
return;
}
const nlohmann::json result = command_manager->executeFromType(CommandType::UPDATE_CAMERA, context->body);
const auto code = getIsSuccess(result) ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.dump().c_str());
}
// gets
void RestAPI::handle_get_config(RequestContext *context)
{
auto const result = this->command_manager->executeFromType(CommandType::GET_CONFIG, "");
const nlohmann::json jsonResult = result;
mg_http_reply(context->connection, 200, JSON_RESPONSE, "{%m:%m}", MG_ESC("result"), jsonResult.dump().c_str());
}
// resets
void RestAPI::handle_reset_config(RequestContext *context)
{
if (context->method != POST_METHOD)
{
mg_http_reply(context->connection, 401, JSON_RESPONSE, "{%m:%m}", MG_ESC("error"), "Method not allowed");
return;
}
const nlohmann::json result = this->command_manager->executeFromType(CommandType::RESET_CONFIG, "{\"section\": \"all\"}");
const auto code = getIsSuccess(result) ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, "{%m:%m}", MG_ESC("result"), result.dump().c_str());
}
// reboots
void RestAPI::handle_reboot(RequestContext *context)
{
const auto result = this->command_manager->executeFromType(CommandType::RESTART_DEVICE, "");
mg_http_reply(context->connection, 200, JSON_RESPONSE, "{%m:%m}", MG_ESC("result"), "Ok");
}
void RestAPI::handle_camera_reboot(RequestContext *context)
{
mg_http_reply(context->connection, 200, JSON_RESPONSE, "{%m:%m}", MG_ESC("result"), "Ok");
}
// heartbeat
void RestAPI::pong(RequestContext *context)
{
const nlohmann::json result = this->command_manager->executeFromType(CommandType::PING, "");
const auto code = getIsSuccess(result) ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.dump().c_str());
}
// special
void RestAPI::handle_save(RequestContext *context)
{
const nlohmann::json result = this->command_manager->executeFromType(CommandType::SAVE_CONFIG, "");
const auto code = getIsSuccess(result) ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.dump().c_str());
}
}
+11 -22
View File
@@ -18,10 +18,18 @@ struct RequestContext
std::string body;
};
struct RequestBaseData
{
std::string allowed_method;
CommandType command_type;
int success_code;
int error_code;
RequestBaseData(std::string allowed_method, CommandType command_type, int success_code, int error_code) : allowed_method(allowed_method), command_type(command_type), success_code(success_code), error_code(error_code) {};
};
class RestAPI
{
using route_handler = void (RestAPI::*)(RequestContext *);
typedef std::unordered_map<std::string, route_handler> route_map;
typedef std::unordered_map<std::string, RequestBaseData> route_map;
std::string url;
route_map routes;
@@ -29,26 +37,7 @@ class RestAPI
std::shared_ptr<CommandManager> command_manager;
private:
// updates
void handle_update_wifi(RequestContext *context);
void handle_update_device(RequestContext *context);
void handle_update_camera(RequestContext *context);
// gets
void handle_get_config(RequestContext *context);
// resets
void handle_reset_config(RequestContext *context);
// reboots
void handle_reboot(RequestContext *context);
void handle_camera_reboot(RequestContext *context);
// heartbeat
void pong(RequestContext *context);
// special
void handle_save(RequestContext *context);
void handle_endpoint_command(RequestContext *context, std::string allowed_method, CommandType command_type, int success_code, int error_code);
public:
// this will also need command manager
+31 -3
View File
@@ -1,4 +1,32 @@
idf_component_register(SRCS "SerialManager/SerialManager.cpp"
INCLUDE_DIRS "SerialManager"
REQUIRES esp_driver_uart CommandManager ProjectConfig tinyusb
set (
requires
esp_driver_uart
CommandManager
ProjectConfig
)
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3")
list(APPEND requires
tinyusb
)
endif()
set (
source_files
"SerialManager/SerialManager.cpp"
)
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3" )
list(APPEND source_files
"SerialManager/SerialManager_esp32s3.cpp"
)
else()
list(APPEND source_files
"SerialManager/SerialManager_esp32.cpp"
)
endif()
idf_component_register(SRCS ${source_files}
INCLUDE_DIRS "SerialManager"
REQUIRES ${requires}
)
@@ -1,9 +1,6 @@
#include "SerialManager.hpp"
#include "esp_log.h"
#include "main_globals.hpp"
#include "tusb.h"
#define BUF_SIZE (1024)
SerialManager::SerialManager(std::shared_ptr<CommandManager> commandManager, esp_timer_handle_t *timerHandle)
: commandManager(commandManager), timerHandle(timerHandle)
@@ -12,62 +9,6 @@ SerialManager::SerialManager(std::shared_ptr<CommandManager> commandManager, esp
this->temp_data = static_cast<uint8_t *>(malloc(256));
}
void SerialManager::setup()
{
usb_serial_jtag_driver_config_t usb_serial_jtag_config;
usb_serial_jtag_config.rx_buffer_size = BUF_SIZE;
usb_serial_jtag_config.tx_buffer_size = BUF_SIZE;
usb_serial_jtag_driver_install(&usb_serial_jtag_config);
}
void SerialManager::try_receive()
{
static auto current_position = 0;
int len = usb_serial_jtag_read_bytes(this->temp_data, 256, 1000 / 20);
// If driver is uninstalled or an error occurs, abort read gracefully
if (len < 0)
{
return;
}
if (len > 0)
{
// Notify main that a command was received during startup
notify_startup_command_received();
}
// since we've got something on the serial port
// we gotta keep reading until we've got the whole message
// we will submit the command once we get a newline, a return or the buffer is full
for (auto i = 0; i < len; i++)
{
this->data[current_position++] = this->temp_data[i];
// if we're at the end of the buffer, try to process the command anyway
// if we've got a new line, we've finished sending the commands, process them
if (current_position >= BUF_SIZE || this->data[current_position - 1] == '\n' || this->data[current_position - 1] == '\r')
{
data[current_position] = '\0';
current_position = 0;
const nlohmann::json result = this->commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(this->data)));
const auto resultMessage = result.dump();
usb_serial_jtag_write_bytes_chunked(resultMessage.c_str(), resultMessage.length(), 1000 / 20);
}
}
}
void SerialManager::usb_serial_jtag_write_bytes_chunked(const char *data, size_t len, size_t timeout)
{
while (len > 0)
{
auto to_write = len > BUF_SIZE ? BUF_SIZE : len;
auto written = usb_serial_jtag_write_bytes(data, to_write, timeout);
data += written;
len -= written;
}
}
// Function to notify that a command was received during startup
void SerialManager::notify_startup_command_received()
{
@@ -83,21 +24,6 @@ void SerialManager::notify_startup_command_received()
}
}
void SerialManager::shutdown()
{
// Stop heartbeats; timer will be deleted by main if needed.
// Uninstall the USB Serial JTAG driver to free the internal USB for TinyUSB.
esp_err_t err = usb_serial_jtag_driver_uninstall();
if (err == ESP_OK)
{
ESP_LOGI("[SERIAL]", "usb_serial_jtag driver uninstalled");
}
else if (err != ESP_ERR_INVALID_STATE)
{
ESP_LOGW("[SERIAL]", "usb_serial_jtag_driver_uninstall returned %s", esp_err_to_name(err));
}
}
// we can cancel this task once we're in cdc
void HandleSerialManagerTask(void *pvParameters)
{
@@ -107,72 +33,3 @@ void HandleSerialManagerTask(void *pvParameters)
serialManager->try_receive();
}
}
void HandleCDCSerialManagerTask(void *pvParameters)
{
auto const commandManager = static_cast<CommandManager *>(pvParameters);
static char buffer[BUF_SIZE];
auto idx = 0;
cdc_command_packet_t packet;
while (true)
{
if (xQueueReceive(cdcMessageQueue, &packet, portMAX_DELAY) == pdTRUE)
{
for (auto i = 0; i < packet.len; i++)
{
buffer[idx++] = packet.data[i];
// if we're at the end of the buffer, try to process the command anyway
// if we've got a new line, we've finished sending the commands, process them
if (idx >= BUF_SIZE || buffer[idx - 1] == '\n' || buffer[idx - 1] == '\r')
{
buffer[idx - 1] = '\0';
const nlohmann::json result = commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(buffer)));
const auto resultMessage = result.dump();
tud_cdc_write(resultMessage.c_str(), resultMessage.length());
tud_cdc_write_flush();
idx = 0;
}
}
}
}
}
// tud_cdc_rx_cb is defined as TU_ATTR_WEAK so we can override it, we will be called back if we get some data
// but we don't want to do any processing here since we don't want to risk blocking
// grab the data and send it to a queue, a special task will process it and handle with the command manager
extern "C" void tud_cdc_rx_cb(uint8_t itf)
{
// we can void the interface number
(void)itf;
cdc_command_packet_t packet;
auto len = tud_cdc_available();
if (len > 0)
{
auto read = tud_cdc_read(packet.data, sizeof(packet.data));
if (read > 0)
{
// we should be safe here, given that the max buffer size is 64
packet.len = static_cast<uint8_t>(read);
xQueueSend(cdcMessageQueue, &packet, 1);
}
}
}
extern "C" void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts)
{
(void)itf;
(void)dtr;
(void)rts;
ESP_LOGI("[SERIAL]", "CDC line state changed: DTR=%d, RTS=%d", dtr, rts);
}
void tud_cdc_line_coding_cb(uint8_t itf, cdc_line_coding_t const *p_line_coding)
{
(void)itf;
ESP_LOGI("[SERIAL]", "CDC line coding: %" PRIu32 " bps, %d stop bits, %d parity, %d data bits",
p_line_coding->bit_rate, p_line_coding->stop_bits,
p_line_coding->parity, p_line_coding->data_bits);
}
@@ -10,14 +10,16 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/uart.h"
#include "sdkconfig.h"
#include "esp_log.h"
#include "driver/gpio.h"
#include "driver/usb_serial_jtag.h"
#include "esp_vfs_usb_serial_jtag.h"
#include "esp_vfs_dev.h"
#include "esp_mac.h"
#ifndef BUF_SIZE
#define BUF_SIZE (1024)
#endif
extern "C" void tud_cdc_rx_cb(uint8_t itf);
extern "C" void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts);
@@ -39,8 +41,6 @@ public:
void shutdown();
private:
void usb_serial_jtag_write_bytes_chunked(const char *data, size_t len, size_t timeout);
std::shared_ptr<CommandManager> commandManager;
esp_timer_handle_t *timerHandle;
uint8_t *data;
@@ -0,0 +1,102 @@
#include "SerialManager.hpp"
#include "esp_log.h"
#include "main_globals.hpp"
#include "driver/uart.h"
void SerialManager::setup()
{
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
const auto uart_num = static_cast<uart_port_t>(CONFIG_UART_PORT_NUMBER);
uart_driver_install(uart_num, BUF_SIZE, BUF_SIZE, 0, NULL, 0);
uart_param_config(uart_num, &uart_config);
uart_set_pin(uart_num,
CONFIG_UART_TX_PIN,
CONFIG_UART_RX_PIN,
UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE);
gpio_set_pull_mode(static_cast<gpio_num_t>(CONFIG_UART_RX_PIN), GPIO_PULLDOWN_ONLY);
// ----- Startup Flush -----
uart_flush(uart_num);
uint8_t dump_buf[256];
// clean up initial onslaught of logs
while (uart_read_bytes(uart_num, dump_buf, sizeof(dump_buf), 10 / portTICK_PERIOD_MS) > 0)
{
}
}
void uart_write_bytes_chunked(uart_port_t uart_num, const void *src, size_t size)
{
while (size > 0)
{
auto to_write = size > BUF_SIZE ? BUF_SIZE : size;
auto written = uart_write_bytes(uart_num, src, to_write);
src += written;
size -= written;
}
}
void SerialManager::try_receive()
{
static auto current_position = 0;
const auto uart_num = static_cast<uart_port_t>(CONFIG_UART_PORT_NUMBER);
int len = uart_read_bytes(uart_num, this->temp_data, BUF_SIZE, 1000 / 20);
// If driver is uninstalled or an error occurs, abort read gracefully
if (len <= 0)
{
return;
}
if (len > 0)
{
notify_startup_command_received();
}
// since we've got something on the serial port
// we gotta keep reading until we've got the whole message
// we will submit the command once we get a newline, a return or the buffer is full
for (auto i = 0; i < len; i++)
{
this->data[current_position++] = this->temp_data[i];
// if we're at the end of the buffer, try to process the command anyway
// if we've got a new line, we've finished sending the commands, process them
if (current_position >= BUF_SIZE || this->data[current_position - 1] == '\n' || this->data[current_position - 1] == '\r')
{
data[current_position] = '\0';
current_position = 0;
const nlohmann::json result = this->commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(this->data)));
const auto resultMessage = result.dump();
// todo check if this works
// uart_write_bytes_chunked(uart_num, resultMessage.c_str(), resultMessage.length())s
uart_write_bytes(uart_num, resultMessage.c_str(), resultMessage.length());
}
}
}
void SerialManager::shutdown()
{
// Uninstall the UART driver to free the internal to keep compatibility with JTAG implementation.
const auto uart_num = static_cast<uart_port_t>(CONFIG_UART_PORT_NUMBER);
esp_err_t err = uart_driver_delete(uart_num);
if (err == ESP_OK)
{
ESP_LOGI("[SERIAL]", "usb_serial_jtag driver uninstalled");
}
else if (err != ESP_ERR_INVALID_STATE)
{
ESP_LOGW("[SERIAL]", "usb_serial_jtag_driver_uninstall returned %s", esp_err_to_name(err));
}
}
@@ -0,0 +1,152 @@
#include "SerialManager.hpp"
#include "esp_log.h"
#include "main_globals.hpp"
#include "driver/usb_serial_jtag.h"
#include "esp_vfs_usb_serial_jtag.h"
#include "tusb.h"
void SerialManager::setup()
{
#ifndef CONFIG_USE_UART_FOR_COMMUNICATION
usb_serial_jtag_driver_config_t usb_serial_jtag_config;
usb_serial_jtag_config.rx_buffer_size = BUF_SIZE;
usb_serial_jtag_config.tx_buffer_size = BUF_SIZE;
usb_serial_jtag_driver_install(&usb_serial_jtag_config);
#endif
}
void usb_serial_jtag_write_bytes_chunked(const char *data, size_t len, size_t timeout)
{
#ifndef CONFIG_USE_UART_FOR_COMMUNICATION
while (len > 0)
{
auto to_write = len > BUF_SIZE ? BUF_SIZE : len;
auto written = usb_serial_jtag_write_bytes(data, to_write, timeout);
data += written;
len -= written;
}
#endif
}
void SerialManager::try_receive()
{
static auto current_position = 0;
int len = usb_serial_jtag_read_bytes(this->temp_data, 256, 1000 / 20);
// If driver is uninstalled or an error occurs, abort read gracefully
if (len < 0)
{
return;
}
if (len > 0)
{
// Notify main that a command was received during startup
notify_startup_command_received();
}
// since we've got something on the serial port
// we gotta keep reading until we've got the whole message
// we will submit the command once we get a newline, a return or the buffer is full
for (auto i = 0; i < len; i++)
{
this->data[current_position++] = this->temp_data[i];
// if we're at the end of the buffer, try to process the command anyway
// if we've got a new line, we've finished sending the commands, process them
if (current_position >= BUF_SIZE || this->data[current_position - 1] == '\n' || this->data[current_position - 1] == '\r')
{
data[current_position] = '\0';
current_position = 0;
const nlohmann::json result = this->commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(this->data)));
const auto resultMessage = result.dump();
usb_serial_jtag_write_bytes_chunked(resultMessage.c_str(), resultMessage.length(), 1000 / 20);
}
}
}
void SerialManager::shutdown()
{
// Uninstall the USB Serial JTAG driver to free the internal USB for TinyUSB.
esp_err_t err = usb_serial_jtag_driver_uninstall();
if (err == ESP_OK)
{
ESP_LOGI("[SERIAL]", "usb_serial_jtag driver uninstalled");
}
else if (err != ESP_ERR_INVALID_STATE)
{
ESP_LOGW("[SERIAL]", "usb_serial_jtag_driver_uninstall returned %s", esp_err_to_name(err));
}
}
void HandleCDCSerialManagerTask(void *pvParameters)
{
#ifndef CONFIG_USE_UART_FOR_COMMUNICATION
auto const commandManager = static_cast<CommandManager *>(pvParameters);
static char buffer[BUF_SIZE];
auto idx = 0;
cdc_command_packet_t packet;
while (true)
{
if (xQueueReceive(cdcMessageQueue, &packet, portMAX_DELAY) == pdTRUE)
{
for (auto i = 0; i < packet.len; i++)
{
buffer[idx++] = packet.data[i];
// if we're at the end of the buffer, try to process the command anyway
// if we've got a new line, we've finished sending the commands, process them
if (idx >= BUF_SIZE || buffer[idx - 1] == '\n' || buffer[idx - 1] == '\r')
{
buffer[idx - 1] = '\0';
const nlohmann::json result = commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(buffer)));
const auto resultMessage = result.dump();
tud_cdc_write(resultMessage.c_str(), resultMessage.length());
tud_cdc_write_flush();
idx = 0;
}
}
}
}
#endif
}
// tud_cdc_rx_cb is defined as TU_ATTR_WEAK so we can override it, we will be called back if we get some data
// but we don't want to do any processing here since we don't want to risk blocking
// grab the data and send it to a queue, a special task will process it and handle with the command manager
extern "C" void tud_cdc_rx_cb(uint8_t itf)
{
// we can void the interface number
(void)itf;
cdc_command_packet_t packet;
auto len = tud_cdc_available();
if (len > 0)
{
auto read = tud_cdc_read(packet.data, sizeof(packet.data));
if (read > 0)
{
// we should be safe here, given that the max buffer size is 64
packet.len = static_cast<uint8_t>(read);
xQueueSend(cdcMessageQueue, &packet, 1);
}
}
}
extern "C" void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts)
{
(void)itf;
(void)dtr;
(void)rts;
ESP_LOGI("[SERIAL]", "CDC line state changed: DTR=%d, RTS=%d", dtr, rts);
}
void tud_cdc_line_coding_cb(uint8_t itf, cdc_line_coding_t const *p_line_coding)
{
(void)itf;
ESP_LOGI("[SERIAL]", "CDC line coding: %" PRIu32 " bps, %d stop bits, %d parity, %d data bits",
p_line_coding->bit_rate, p_line_coding->stop_bits,
p_line_coding->parity, p_line_coding->data_bits);
}
@@ -74,10 +74,11 @@ esp_err_t StreamHelpers::stream(httpd_req_t *req)
}
if (response != ESP_OK)
break;
// Only log every 100 frames to reduce overhead
static int frame_count = 0;
if (++frame_count % 100 == 0) {
if (++frame_count % 100 == 0)
{
long request_end = Helpers::getTimeInMillis();
long latency = (request_end - last_request_time);
last_request_time = request_end;
@@ -98,13 +99,12 @@ esp_err_t StreamServer::startStreamServer()
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.stack_size = 20480;
// todo bring this back to 1 once we're done with logs over websockets
config.max_uri_handlers = 10;
config.server_port = STREAM_SERVER_PORT;
config.ctrl_port = STREAM_SERVER_PORT;
config.recv_wait_timeout = 5; // 5 seconds for receiving
config.send_wait_timeout = 5; // 5 seconds for sending
config.lru_purge_enable = true; // Enable LRU purge for better connection handling
config.recv_wait_timeout = 5; // 5 seconds for receiving
config.send_wait_timeout = 5; // 5 seconds for sending
config.lru_purge_enable = true; // Enable LRU purge for better connection handling
httpd_uri_t stream_page = {
.uri = "/",
@@ -139,7 +139,6 @@ esp_err_t StreamServer::startStreamServer()
httpd_register_uri_handler(camera_stream, &stream_page);
ESP_LOGI(STREAM_SERVER_TAG, "Stream server started on port %d", STREAM_SERVER_PORT);
// todo add printing IP addr here
return ESP_OK;
}
+16 -1
View File
@@ -1,4 +1,19 @@
set (
requires
esp_timer
esp32-camera
StateManager
CameraManager
Helpers
)
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3")
list(APPEND requires
usb_device_uvc
)
endif()
idf_component_register(SRCS "UVCStream/UVCStream.cpp"
INCLUDE_DIRS "UVCStream"
REQUIRES esp_timer esp32-camera StateManager usb_device_uvc CameraManager Helpers
REQUIRES ${requires}
)
+9 -12
View File
@@ -1,8 +1,9 @@
#include "UVCStream.hpp"
#ifdef CONFIG_GENERAL_INCLUDE_UVC_MODE
#include <cstdio> // for snprintf
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// no deps on main globals here; handover is performed in main before calling setup when needed
static const char *UVC_STREAM_TAG = "[UVC DEVICE]";
@@ -94,10 +95,10 @@ static uvc_fb_t *UVCStreamHelpers::camera_fb_get_cb(void *cb_ctx)
// to the underlying camera buffer was overwritten before TinyUSB returned it.
// --- 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 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)
@@ -167,12 +168,6 @@ static void UVCStreamHelpers::camera_fb_return_cb(uvc_fb_t *fb, void *cb_ctx)
esp_err_t UVCStreamManager::setup()
{
#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
ESP_LOGI(UVC_STREAM_TAG, "Setting up UVC Stream");
// Allocate a fixed-size transfer buffer (compile-time constant)
uvc_buffer_size = UVCStreamManager::UVC_MAX_FRAMESIZE_SIZE;
@@ -218,4 +213,6 @@ esp_err_t UVCStreamManager::start()
ESP_LOGI(UVC_STREAM_TAG, "Starting UVC streaming");
// UVC device is already initialized in setup(), just log that we're starting
return ESP_OK;
}
}
#endif
+5 -2
View File
@@ -1,6 +1,10 @@
#pragma once
#ifndef UVCSTREAM_HPP
#define UVCSTREAM_HPP
#include "sdkconfig.h"
#ifdef CONFIG_GENERAL_INCLUDE_UVC_MODE
#include "esp_timer.h"
#include "esp_mac.h"
#include "esp_camera.h"
@@ -33,8 +37,6 @@ extern QueueHandle_t eventQueue;
namespace UVCStreamHelpers
{
// TODO move the camera handling code to the camera manager and have the uvc manager initialize it in wired mode
typedef struct
{
camera_fb_t *cam_fb_p;
@@ -64,3 +66,4 @@ public:
};
#endif // UVCSTREAM_HPP
#endif
@@ -8,69 +8,51 @@
#include "sdkconfig.h"
#ifdef CONFIG_FORMAT_MJPEG_CAM1
#define FORMAT_MJPEG_CAM1 1
#define FORMAT_MJPEG_CAM1 1
#endif
#ifdef CONFIG_UVC_CAM1_MULTI_FRAMESIZE
//If enable, add VGA and HVGA to list
#define UVC_CAM1_FRAME_MULTI 1
// If enable, add VGA and HVGA to list
#define UVC_CAM1_FRAME_MULTI 1
#endif
#define UVC_CAM1_FRAME_WIDTH CONFIG_UVC_CAM1_FRAMESIZE_WIDTH
#define UVC_CAM1_FRAME_HEIGHT CONFIG_UVC_CAM1_FRAMESIZE_HEIGT
#define UVC_CAM1_FRAME_RATE CONFIG_UVC_CAM1_FRAMERATE
#define UVC_CAM1_FRAME_WIDTH CONFIG_UVC_CAM1_FRAMESIZE_WIDTH
#define UVC_CAM1_FRAME_HEIGHT CONFIG_UVC_CAM1_FRAMESIZE_HEIGT
#define UVC_CAM1_FRAME_RATE CONFIG_UVC_CAM1_FRAMERATE
#ifdef CONFIG_UVC_MODE_BULK_CAM1
#define UVC_CAM1_BULK_MODE
#endif
#if CONFIG_UVC_SUPPORT_TWO_CAM
#ifdef CONFIG_FORMAT_MJPEG_CAM2
#define FORMAT_MJPEG_CAM2 1
#endif
#ifdef CONFIG_UVC_CAM2_MULTI_FRAMESIZE
//If enable, add VGA and HVGA to list
#define UVC_CAM2_FRAME_MULTI 1
#endif
#define UVC_CAM2_FRAME_WIDTH CONFIG_UVC_CAM2_FRAMESIZE_WIDTH
#define UVC_CAM2_FRAME_HEIGHT CONFIG_UVC_CAM2_FRAMESIZE_HEIGT
#define UVC_CAM2_FRAME_RATE CONFIG_UVC_CAM2_FRAMERATE
#ifdef CONFIG_UVC_MODE_BULK_CAM2
#define UVC_CAM2_BULK_MODE
#endif
#endif
#ifndef UVC_CAM2_FRAME_WIDTH
#define UVC_CAM2_FRAME_WIDTH UVC_CAM1_FRAME_WIDTH
#define UVC_CAM2_FRAME_WIDTH UVC_CAM1_FRAME_WIDTH
#endif
#ifndef UVC_CAM2_FRAME_HEIGHT
#define UVC_CAM2_FRAME_HEIGHT UVC_CAM1_FRAME_HEIGHT
#define UVC_CAM2_FRAME_HEIGHT UVC_CAM1_FRAME_HEIGHT
#endif
#ifndef UVC_CAM2_FRAME_RATE
#define UVC_CAM2_FRAME_RATE UVC_CAM1_FRAME_RATE
#define UVC_CAM2_FRAME_RATE UVC_CAM1_FRAME_RATE
#endif
static const struct {
static const struct
{
int width;
int height;
int rate;
} UVC_FRAMES_INFO[][4] = {{
{UVC_CAM1_FRAME_WIDTH, UVC_CAM1_FRAME_HEIGHT, UVC_CAM1_FRAME_RATE},
{CONFIG_UVC_MULTI_FRAME_WIDTH_1, CONFIG_UVC_MULTI_FRAME_HEIGHT_1, CONFIG_UVC_MULTI_FRAME_FPS_1},
{CONFIG_UVC_MULTI_FRAME_WIDTH_2, CONFIG_UVC_MULTI_FRAME_HEIGHT_2, CONFIG_UVC_MULTI_FRAME_FPS_2},
{CONFIG_UVC_MULTI_FRAME_WIDTH_3, CONFIG_UVC_MULTI_FRAME_HEIGHT_3, CONFIG_UVC_MULTI_FRAME_FPS_3},
}, {
{UVC_CAM2_FRAME_WIDTH, UVC_CAM2_FRAME_HEIGHT, UVC_CAM2_FRAME_RATE},
{CONFIG_UVC_MULTI_FRAME_WIDTH_1, CONFIG_UVC_MULTI_FRAME_HEIGHT_1, CONFIG_UVC_MULTI_FRAME_FPS_1},
{CONFIG_UVC_MULTI_FRAME_WIDTH_2, CONFIG_UVC_MULTI_FRAME_HEIGHT_2, CONFIG_UVC_MULTI_FRAME_FPS_2},
{CONFIG_UVC_MULTI_FRAME_WIDTH_3, CONFIG_UVC_MULTI_FRAME_HEIGHT_3, CONFIG_UVC_MULTI_FRAME_FPS_3},
}
};
{UVC_CAM1_FRAME_WIDTH, UVC_CAM1_FRAME_HEIGHT, UVC_CAM1_FRAME_RATE},
{CONFIG_UVC_MULTI_FRAME_WIDTH_1, CONFIG_UVC_MULTI_FRAME_HEIGHT_1, CONFIG_UVC_MULTI_FRAME_FPS_1},
{CONFIG_UVC_MULTI_FRAME_WIDTH_2, CONFIG_UVC_MULTI_FRAME_HEIGHT_2, CONFIG_UVC_MULTI_FRAME_FPS_2},
{CONFIG_UVC_MULTI_FRAME_WIDTH_3, CONFIG_UVC_MULTI_FRAME_HEIGHT_3, CONFIG_UVC_MULTI_FRAME_FPS_3},
},
{
{UVC_CAM2_FRAME_WIDTH, UVC_CAM2_FRAME_HEIGHT, UVC_CAM2_FRAME_RATE},
{CONFIG_UVC_MULTI_FRAME_WIDTH_1, CONFIG_UVC_MULTI_FRAME_HEIGHT_1, CONFIG_UVC_MULTI_FRAME_FPS_1},
{CONFIG_UVC_MULTI_FRAME_WIDTH_2, CONFIG_UVC_MULTI_FRAME_HEIGHT_2, CONFIG_UVC_MULTI_FRAME_FPS_2},
{CONFIG_UVC_MULTI_FRAME_WIDTH_3, CONFIG_UVC_MULTI_FRAME_HEIGHT_3, CONFIG_UVC_MULTI_FRAME_FPS_3},
}};
#define UVC_FRAME_NUM (sizeof(UVC_FRAMES_INFO[0]) / sizeof(UVC_FRAMES_INFO[0][0]))
_Static_assert(UVC_FRAME_NUM == 4, "UVC_FRAME_NUM must be 4");
@@ -20,11 +20,7 @@
static const char *TAG = "usbd_uvc";
#if CONFIG_UVC_SUPPORT_TWO_CAM
#define UVC_CAM_NUM 2
#else
#define UVC_CAM_NUM 1
#endif
typedef struct
{
@@ -85,12 +81,6 @@ void tud_suspend_cb(bool remote_wakeup_en)
{
s_uvc_device.user_config[0].stop_cb(s_uvc_device.user_config[0].cb_ctx);
}
#if CONFIG_UVC_SUPPORT_TWO_CAM
if (s_uvc_device.user_config[1].stop_cb)
{
s_uvc_device.user_config[1].stop_cb(s_uvc_device.user_config[1].cb_ctx);
}
#endif
ESP_LOGI(TAG, "Suspend");
}
@@ -178,82 +168,6 @@ static void video_task(void *arg)
}
}
#if CONFIG_UVC_SUPPORT_TWO_CAM
static void video_task2(void *arg)
{
uint32_t start_ms = 0;
uint32_t frame_num = 0;
uint32_t frame_len = 0;
uint32_t already_start = 0;
uint32_t tx_busy = 0;
uint8_t *uvc_buffer = s_uvc_device.user_config[1].uvc_buffer;
uint32_t uvc_buffer_size = s_uvc_device.user_config[1].uvc_buffer_size;
uvc_fb_t *pic = NULL;
while (1)
{
if (!tud_video_n_streaming(1, 0))
{
already_start = 0;
frame_num = 0;
tx_busy = 0;
vTaskDelay(1);
continue;
}
if (!already_start)
{
already_start = 1;
start_ms = get_time_millis();
}
uint32_t cur = get_time_millis();
if (cur - start_ms < s_uvc_device.interval_ms[1])
{
vTaskDelay(1);
continue;
}
if (tx_busy)
{
uint32_t xfer_done = ulTaskNotifyTake(pdTRUE, 1);
if (xfer_done == 0)
{
continue;
}
++frame_num;
tx_busy = 0;
}
start_ms += s_uvc_device.interval_ms[1];
ESP_LOGD(TAG, "frame %" PRIu32 " taking picture...", frame_num);
pic = s_uvc_device.user_config[1].fb_get_cb(s_uvc_device.user_config[1].cb_ctx);
if (pic)
{
ESP_LOGD(TAG, "Picture taken! Its size was: %zu bytes", pic->len);
}
else
{
ESP_LOGE(TAG, "Failed to capture picture");
continue;
}
if (pic->len > uvc_buffer_size)
{
ESP_LOGW(TAG, "frame size is too big, dropping frame");
s_uvc_device.user_config[1].fb_return_cb(pic, s_uvc_device.user_config[1].cb_ctx);
continue;
}
frame_len = pic->len;
memcpy(uvc_buffer, pic->buf, frame_len);
s_uvc_device.user_config[1].fb_return_cb(pic, s_uvc_device.user_config[1].cb_ctx);
tx_busy = 1;
tud_video_n_frame_xfer(1, 0, (void *)uvc_buffer, frame_len);
ESP_LOGD(TAG, "frame %" PRIu32 " transfer start, size %" PRIu32, frame_num, frame_len);
}
}
#endif
void tud_video_frame_xfer_complete_cb(uint_fast8_t ctl_idx, uint_fast8_t stm_idx)
{
(void)ctl_idx;
@@ -307,20 +221,11 @@ esp_err_t uvc_device_config(int index, uvc_device_config_t *config)
esp_err_t uvc_device_init(void)
{
ESP_RETURN_ON_FALSE(s_uvc_device.uvc_init[0], ESP_ERR_INVALID_STATE, TAG, "uvc device 0 not init");
#if CONFIG_UVC_SUPPORT_TWO_CAM
ESP_RETURN_ON_FALSE(s_uvc_device.uvc_init[1], ESP_ERR_INVALID_STATE, TAG, "uvc device 1 not init, if not use, please disable CONFIG_UVC_SUPPORT_TWO_CAM");
#endif
#ifdef CONFIG_FORMAT_MJPEG_CAM1
s_uvc_device.format[0] = UVC_FORMAT_JPEG;
#endif
#if CONFIG_UVC_SUPPORT_TWO_CAM
#ifdef CONFIG_FORMAT_MJPEG_CAM2
s_uvc_device.format[1] = UVC_FORMAT_JPEG;
#endif
#endif
// init device stack on configured roothub port
usb_phy_init();
bool usb_init = tusb_init();
@@ -335,10 +240,6 @@ esp_err_t uvc_device_init(void)
#if (CFG_TUD_VIDEO)
core_id = (CONFIG_UVC_CAM1_TASK_CORE < 0) ? tskNO_AFFINITY : CONFIG_UVC_CAM1_TASK_CORE;
xTaskCreatePinnedToCore(video_task, "UVC", 4096, NULL, CONFIG_UVC_CAM1_TASK_PRIORITY, &s_uvc_device.uvc_task_hdl[0], core_id);
#if CONFIG_UVC_SUPPORT_TWO_CAM
core_id = (CONFIG_UVC_CAM2_TASK_CORE < 0) ? tskNO_AFFINITY : CONFIG_UVC_CAM2_TASK_CORE;
xTaskCreatePinnedToCore(video_task2, "UVC2", 4096, NULL, CONFIG_UVC_CAM2_TASK_PRIORITY, &s_uvc_device.uvc_task_hdl[1], core_id);
#endif
#endif
ESP_LOGI(TAG, "UVC Device Start, Version: %d.%d.%d", USB_DEVICE_UVC_VER_MAJOR, USB_DEVICE_UVC_VER_MINOR, USB_DEVICE_UVC_VER_PATCH);
return ESP_OK;
+22
View File
@@ -107,6 +107,28 @@ menu "OpenIris: WiFi Configuration"
endmenu
menu "OpenIris: Serial Communication Settings"
config UART_PORT_NUMBER
int "UART Port number"
default 0
range 0 4
help
The UART port to use for communication
config UART_RX_PIN
int "UART RX PIN number"
default -1
help
The UART RX pin to use for communication. If set to -1 we won't override it. It will be treated as NO_CHANGE
config UART_TX_PIN
int "UART TX PIN number"
default -1
help
The UART TX pin to use for communication. If set to -1 we won't override it. It will be treated as NO_CHANGE
endmenu
menu "OpenIris: LED Configuration"
config LED_DEBUG_ENABLE
+18 -6
View File
@@ -22,7 +22,10 @@
#include <SerialManager.hpp>
#include <RestAPI.hpp>
#include <main_globals.hpp>
#ifdef CONFIG_MONITORING_LED_CURRENT
#include <MonitoringManager.hpp>
#endif
#ifdef CONFIG_GENERAL_INCLUDE_UVC_MODE
#include <UVCStream.hpp>
@@ -36,7 +39,7 @@
#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
#define BLINK_GPIO (gpio_num_t)(-1)
#endif
#define CONFIG_LED_C_PIN_GPIO (gpio_num_t) CONFIG_LED_EXTERNAL_GPIO
@@ -61,14 +64,18 @@ MDNSManager mdnsManager(deviceConfig, eventQueue);
std::shared_ptr<CameraManager> cameraHandler = std::make_shared<CameraManager>(deviceConfig, eventQueue);
StreamServer streamServer(80, stateManager);
auto *restAPI = new RestAPI("http://0.0.0.0:81", commandManager);
std::shared_ptr<RestAPI> restAPI = std::make_shared<RestAPI>("http://0.0.0.0:81", commandManager);
#ifdef CONFIG_GENERAL_INCLUDE_UVC_MODE
UVCStreamManager uvcStream;
#endif
auto ledManager = std::make_shared<LEDManager>(BLINK_GPIO, CONFIG_LED_C_PIN_GPIO, ledStateQueue, deviceConfig);
#ifdef CONFIG_MONITORING_LED_CURRENT
std::shared_ptr<MonitoringManager> monitoringManager = std::make_shared<MonitoringManager>();
#endif
auto *serialManager = new SerialManager(commandManager, &timerHandle);
void startWiFiMode();
@@ -165,7 +172,6 @@ void startWiredMode(bool shouldCloseSerialManager)
#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;
startWiFiMode();
#else
ESP_LOGI("[MAIN]", "Starting UVC streaming mode.");
@@ -216,6 +222,7 @@ void startWiFiMode()
mdnsManager.start();
restAPI->begin();
StreamingMode mode = deviceConfig->getDeviceMode();
// don't enable in SETUP mode
if (mode == StreamingMode::WIFI)
{
streamServer.startStreamServer();
@@ -223,8 +230,8 @@ void startWiFiMode()
xTaskCreate(
HandleRestAPIPollTask,
"HandleRestAPIPollTask",
1024 * 2,
restAPI,
2024 * 2,
restAPI.get(),
1, // it's the rest API, we only serve commands over it so we don't really need a higher priority
nullptr);
#else
@@ -265,18 +272,23 @@ extern "C" void app_main(void)
dependencyRegistry->registerService<WiFiManager>(DependencyType::wifi_manager, wifiManager);
#endif
dependencyRegistry->registerService<LEDManager>(DependencyType::led_manager, ledManager);
#ifdef CONFIG_MONITORING_LED_CURRENT
dependencyRegistry->registerService<MonitoringManager>(DependencyType::monitoring_manager, monitoringManager);
#endif
// add endpoint to check firmware version
// setup CI and building for other boards
// esp_log_set_vprintf(&websocket_logger);
Logo::printASCII();
initNVSStorage();
deviceConfig->load();
ledManager->setup();
#ifdef CONFIG_MONITORING_LED_CURRENT
monitoringManager->setup();
monitoringManager->start();
#endif
xTaskCreate(
HandleStateManagerTask,
+36
View File
@@ -0,0 +1,36 @@
[project]
name = "blink"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"pyserial>=3.5",
"pytest>=9.0.1",
"python-dotenv>=1.2.1",
]
[dependency-groups]
dev = [
"bumpver>=2025.1131",
]
[bumpver]
current_version = "0.1.0"
version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]"
commit = true
tag = true
push = false
[bumpver.file_patterns]
"pyproject.toml" = [
'version = "{version}"',
]
"sdkconfig" = [
'CONFIG_GENERAL_VERSION="{version}"',
]
[tool.pytest]
testpaths = [
"tests"
]
-16
View File
@@ -1,16 +0,0 @@
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import logging
import os
import pytest
from pytest_embedded_idf.dut import IdfDut
@pytest.mark.supported_targets
@pytest.mark.generic
def test_blink(dut: IdfDut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'blink.bin')
bin_size = os.path.getsize(binary_file)
logging.info('blink_bin_size : {}KB'.format(bin_size // 1024))
+8
View File
@@ -595,6 +595,14 @@ CONFIG_WIFI_AP_SSID="EyeTrackVR"
CONFIG_WIFI_AP_PASSWORD="12345678"
# end of OpenIris: WiFi Configuration
#
# OpenIris: Serial Communication Settings
#
CONFIG_UART_PORT_NUMBER=0
CONFIG_UART_RX_PIN=-1
CONFIG_UART_TX_PIN=-1
# end of OpenIris: Serial Communication Settings
#
# OpenIris: LED Configuration
#
+2057
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
WIFI_SSID=
WIFI_PASS=
SWITCH_MODE_REBOOT_TIME=5
WIFI_CONNECTION_TIMEOUT=5
INVALID_WIFI_CONNECTION_TIMEOUT=30
View File
+217
View File
@@ -0,0 +1,217 @@
from dataclasses import dataclass
import dotenv
import pytest
import time
from tests.utils import (
OpenIrisDeviceManager,
has_command_failed,
get_current_ports,
get_new_port,
)
board_capabilities = {
"esp_eye": ["wired", "wireless"],
"esp32AIThinker": ["wireless"],
"esp32Cam": ["wireless"],
"esp32M5Stack": ["wireless"],
"facefocusvr_eye_L": ["wired", "measure_current"],
"facefocusvr_eye_R": ["wired", "measure_current"],
"facefocusvr_face": ["wired", "measure_current"],
"project_babble": ["wireless", "wired"],
"seed_studio": ["wireless", "wired"],
"wrooms3": ["wireless", "wired"],
"wrooms3QIO": ["wireless", "wired"],
"wrover": ["wireless", "wired"],
}
def pytest_addoption(parser):
parser.addoption("--board", action="store")
parser.addoption(
"--connection",
action="store",
help="Defines how to connect to the given board, wireless by ip/mdns or wired by com/cdc",
)
def pytest_configure(config):
config.addinivalue_line(
"markers", "has_capability(caps): skip if the board does not have the capability"
)
config.addinivalue_line(
"markers", "lacks_capability(caps): skip if the board DOES have the capability"
)
@pytest.fixture(autouse=True)
def check_capability_marker(request, board_lacks_capability):
"""
Autorun on each test, checks if the board we started with, has the required capability
This lets us skip tests that are impossible to run on some boards - like for example:
It's impossible to run wired tests on a wireless board
It's impossible to run tests for measuring current on boards without this feature
"""
if marker := request.node.get_closest_marker("has_capability"):
if not len(marker.args):
raise ValueError(
"has_capability marker must be provided with a capability to check"
)
for capability in marker.args:
if board_lacks_capability(capability):
pytest.skip(f"Board does not have capability {capability}")
@pytest.fixture(autouse=True)
def check_lacks_capability_marker(request, board_lacks_capability):
if lacks_capability_marker := request.node.get_closest_marker("lacks_capability"):
if not len(lacks_capability_marker.args):
raise ValueError(
"lacks_capability marker must be provided with a capability to check"
)
for capability in lacks_capability_marker.args:
if not board_lacks_capability(capability):
pytest.skip(
"The board supports given capability: {required_capability}, skipping"
)
@pytest.fixture(scope="session", autouse=True)
def board_name(request):
board_name = request.config.getoption("--board")
if not board_name:
raise ValueError("No board defined")
yield board_name
@pytest.fixture()
def board_lacks_capability(board_name):
def func(capability: str):
if board_name:
if board_name not in board_capabilities:
raise ValueError(f"Unknown board {board_name}")
return capability not in board_capabilities[board_name]
return True
return func
@pytest.fixture(scope="session", autouse=True)
def board_connection(request):
"""
Grabs the specified connection connection method, to be used ONLY for the initial connection. Everything after it HAS to be handled via Device Manager.
Ports WILL change throughout the tests, device manager can keep track of that and reconnect the board as needed.
"""
board_connection = request.config.getoption("--connection")
if not board_connection:
raise ValueError("No connection method defined")
yield board_connection
@dataclass
class TestConfig:
WIFI_SSID: str
WIFI_PASS: str
SWITCH_MODE_REBOOT_TIME: int
WIFI_CONNECTION_TIMEOUT: int
INVALID_WIFI_CONNECTION_TIMEOUT: int
def __init__(
self,
WIFI_SSID: str,
WIFI_PASS: str,
SWITCH_MODE_REBOOT_TIME: int,
WIFI_CONNECTION_TIMEOUT: int,
INVALID_WIFI_CONNECTION_TIMEOUT: int,
):
self.WIFI_SSID = WIFI_SSID
self.WIFI_PASS = WIFI_PASS
self.SWITCH_MODE_REBOOT_TIME = int(SWITCH_MODE_REBOOT_TIME)
self.WIFI_CONNECTION_TIMEOUT = int(WIFI_CONNECTION_TIMEOUT)
self.INVALID_WIFI_CONNECTION_TIMEOUT = int(INVALID_WIFI_CONNECTION_TIMEOUT)
@pytest.fixture(scope="session")
def config():
config = TestConfig(**dotenv.dotenv_values())
yield config
@pytest.fixture(scope="session")
def openiris_device_manager(board_connection, config):
manager = OpenIrisDeviceManager()
manager.get_device(board_connection, config)
yield manager
if manager._device:
manager._device.disconnect()
@pytest.fixture()
def get_openiris_device(openiris_device_manager, config):
def func(port: str | None = None, _config: dict | None = None):
return openiris_device_manager.get_device(port, config or _config)
return func
@pytest.fixture()
def ensure_board_in_mode(openiris_device_manager, config):
"""
Given the OpenIrisDevice manager, grabs the current device and ensures it's in the required mode
if not, sends the command to switch and attempts reconnection if necessary, returning the device back
"""
supported_modes = ["wifi", "uvc"]
def func(mode, device):
if mode not in supported_modes:
raise ValueError(f"{mode} is not a supported mode")
command_result = device.send_command("get_device_mode")
if has_command_failed(command_result):
raise ValueError(f"Failed to get device mode, error: {command_result}")
current_mode = command_result["results"][0]["result"]["data"]["mode"].lower()
if mode == current_mode:
return device
old_ports = get_current_ports()
command_result = device.send_command("switch_mode", {"mode": mode})
if has_command_failed(command_result):
raise ValueError("Failed to switch mode, rerun the tests")
print("Rebooting the board after changing mode")
device.send_command("restart_device")
sleep_timeout = int(config.SWITCH_MODE_REBOOT_TIME)
print(
f"Sleeping for {sleep_timeout} seconds to allow the device to switch modes and boot up"
)
time.sleep(sleep_timeout)
new_ports = get_current_ports()
new_device = openiris_device_manager.get_device(
get_new_port(old_ports, new_ports), config
)
return new_device
return func
@pytest.fixture(scope="session", autouse=True)
def after_session_cleanup(openiris_device_manager, config):
yield
print("Cleanup: Resetting the config and restarting device")
device = openiris_device_manager.get_device(config=config)
device.send_command("reset_config", {"section": "all"})
device.send_command("restart_device")
+543
View File
@@ -0,0 +1,543 @@
import time
from tests.utils import has_command_failed, DetectPortChange
import pytest
def test_sending_invalid_command(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("some_invalid_command")
assert has_command_failed(command_result)
def test_sending_invalid_command_with_payload(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("some_invalid_command", {"param": "invalid"})
assert has_command_failed(command_result)
def test_ping_wired(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("ping")
assert not has_command_failed(command_result)
@pytest.mark.has_capability("wired", "wireless")
def test_changing_mode_to_wired(get_openiris_device, ensure_board_in_mode, config):
device = get_openiris_device()
# let's make sure we're in the wireless mode first, if we're going to try changing it
device = ensure_board_in_mode("wifi", device)
with DetectPortChange() as port_selector:
command_result = device.send_command("switch_mode", {"mode": "uvc"})
assert not has_command_failed(command_result)
device.send_command("restart_device")
time.sleep(config.SWITCH_MODE_REBOOT_TIME)
# and since we've changed the ports
device = get_openiris_device(port_selector.get_new_port())
# initial read to flush the logs first
device.send_command("get_device_mode")
result = device.send_command("get_device_mode")
assert not has_command_failed(result)
def test_changing_mode_same_mode(get_openiris_device):
device = get_openiris_device()
result = device.send_command("get_device_mode")
current_mode = result["results"][0]["result"]["data"]["mode"].lower()
command_result = device.send_command("switch_mode", {"mode": current_mode})
assert not has_command_failed(command_result)
result = device.send_command("get_device_mode")
assert not has_command_failed(result)
assert result["results"][0]["result"]["data"]["mode"].lower() == current_mode
def test_changing_mode_invalid_mode(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("switch_mode", {"mode": "NOT SUPPORTED"})
assert has_command_failed(command_result)
def test_setting_mdns_name(get_openiris_device, ensure_board_in_mode, config):
def check_mdns_name(name: str):
command_result = device.send_command("get_mdns_name")
assert not has_command_failed(command_result)
assert command_result["results"][0]["result"]["data"]["hostname"] == name
device = get_openiris_device()
device = ensure_board_in_mode("wifi", device)
first_name = "testname1"
second_name = "testname2"
# try setting the test mdns name first, just so we know the commands pass
command_result = device.send_command("set_mdns", {"hostname": first_name})
assert not has_command_failed(command_result)
check_mdns_name(first_name)
command_result = device.send_command("set_mdns", {"hostname": second_name})
assert not has_command_failed(command_result)
device.send_command("restart_device")
# let the board boot, wait till it connects
time.sleep(config.SWITCH_MODE_REBOOT_TIME)
check_mdns_name(second_name)
@pytest.mark.parametrize("payload", [{"name": "awd"}, {}])
def test_setting_mdns_name_invalid_payload(get_openiris_device, payload):
device = get_openiris_device()
command_result = device.send_command("set_mdns", payload)
assert has_command_failed(command_result)
@pytest.mark.has_capability("wired", "wireless")
# make this to be has_capabilities instead
def test_reboot_command(get_openiris_device, ensure_board_in_mode, config):
device = ensure_board_in_mode("wifi", get_openiris_device())
command_result = device.send_command("switch_mode", {"mode": "uvc"})
assert not has_command_failed(command_result)
# we're testing if rebooting actually triggers reboot
# so to be 100% sure of that, we're changing the mode to UVC and looking for new port
# which might be a little overkill kill and won't work on boards not supporting both modes
with DetectPortChange() as port_selector:
device.send_command("restart_device")
time.sleep(config.SWITCH_MODE_REBOOT_TIME)
assert port_selector.get_new_port()
def test_get_serial(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("get_serial")
assert not has_command_failed(command_result)
# test for response integrity as well to uphold the contract
assert "mac" in command_result["results"][0]["result"]["data"]
assert "serial" in command_result["results"][0]["result"]["data"]
def test_get_who_am_i(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("get_who_am_i")
assert not has_command_failed(command_result)
# test for response integrity as well to uphold the contract
assert "version" in command_result["results"][0]["result"]["data"]
assert "who_am_i" in command_result["results"][0]["result"]["data"]
@pytest.mark.has_capability("measure_current")
def test_get_led_current_supported(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("get_led_current")
assert not has_command_failed(command_result)
assert "led_current_ma" in command_result["results"][0]["result"]["data"]
@pytest.mark.lacks_capability("measure_current")
def test_get_led_current_unsupported(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("get_led_current")
assert has_command_failed(command_result)
def test_get_led_duty_cycle(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("get_led_duty_cycle")
assert not has_command_failed(command_result)
assert (
"led_external_pwm_duty_cycle" in command_result["results"][0]["result"]["data"]
)
def test_set_led_duty_cycle(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("set_led_duty_cycle", {"dutyCycle": 0})
assert not has_command_failed(command_result)
command_result = device.send_command("get_led_duty_cycle")
assert not has_command_failed(command_result)
assert (
command_result["results"][0]["result"]["data"]["led_external_pwm_duty_cycle"]
== 0
)
command_result = device.send_command("set_led_duty_cycle", {"dutyCycle": 100})
assert not has_command_failed(command_result)
command_result = device.send_command("get_led_duty_cycle")
assert not has_command_failed(command_result)
assert (
command_result["results"][0]["result"]["data"]["led_external_pwm_duty_cycle"]
== 100
)
@pytest.mark.parametrize(
"payload",
[
{},
{"dutyCycle": -1},
{"dutyCycle": 1.5},
{"dutyCycle": 150},
{"awd": 21},
{"dutyCycle": "21"},
],
)
def test_set_led_duty_cycle_invalid_payload(get_openiris_device, payload):
device = get_openiris_device()
command_result = device.send_command("set_led_duty_cycle", payload)
assert has_command_failed(command_result)
@pytest.mark.has_capability("wireless")
def test_check_wifi_status(get_openiris_device, ensure_board_in_mode):
device = ensure_board_in_mode("wifi", get_openiris_device())
command_result = device.send_command("get_wifi_status")
assert not has_command_failed(command_result)
@pytest.mark.has_capability("wireless")
def test_scan_networks(get_openiris_device, ensure_board_in_mode, config):
# this test might run after some tests that affect the network on the device
# which might prevent us from scanning and thus make the test fail, so we reset the config
device = get_openiris_device()
reset_command = device.send_command("reset_config", {"section": "all"})
assert not has_command_failed(reset_command)
with DetectPortChange() as port_selector:
device.send_command("restart_device")
time.sleep(config.SWITCH_MODE_REBOOT_TIME)
device = ensure_board_in_mode(
"wifi", get_openiris_device(port_selector.get_new_port())
)
command_result = device.send_command("scan_networks")
assert not has_command_failed(command_result)
assert len(command_result["results"][0]["result"]["data"]["networks"]) != 0
def test_get_config(get_openiris_device):
device = get_openiris_device()
command_result = device.send_command("get_config")
assert not has_command_failed(command_result)
def test_reset_config_invalid_payload(get_openiris_device):
# to test the config, we can do two things. Set the mdns, get the config, reset it, get it again and compare
device = get_openiris_device()
reset_command = device.send_command("reset_config")
assert has_command_failed(reset_command)
def test_reset_config(get_openiris_device, config):
device = get_openiris_device()
command_result = device.send_command("set_mdns", {"hostname": "somedifferentname"})
assert not has_command_failed(command_result)
current_config = device.send_command("get_config")
assert not has_command_failed(current_config)
reset_command = device.send_command("reset_config", {"section": "all"})
assert not has_command_failed(reset_command)
# since the config was reset, but the data will still be held in memory, we need to reboot the device
with DetectPortChange():
device.send_command("restart_device")
time.sleep(config.SWITCH_MODE_REBOOT_TIME)
new_config = device.send_command("get_config")
assert not has_command_failed(new_config)
assert not new_config == current_config
@pytest.mark.has_capability("wireless")
def test_set_wifi(get_openiris_device, ensure_board_in_mode, config):
# since we want to test actual connection to the network, let's reset the device and reboot it
device = get_openiris_device()
reset_command = device.send_command("reset_config", {"section": "all"})
assert not has_command_failed(reset_command)
with DetectPortChange():
device.send_command("restart_device")
time.sleep(config.SWITCH_MODE_REBOOT_TIME)
# now that the config is clear, let's try setting the wifi
device = ensure_board_in_mode("wifi", device)
params = {
"name": "main",
"ssid": config.WIFI_SSID,
"password": config.WIFI_PASS,
"channel": 0,
"power": 0,
}
set_wifi_result = device.send_command("set_wifi", params)
assert not has_command_failed(set_wifi_result)
# now, let's force connection and check if it worked
connect_wifi_result = device.send_command("connect_wifi")
assert not -has_command_failed(connect_wifi_result)
time.sleep(config.WIFI_CONNECTION_TIMEOUT) # and let it try to for some time
wifi_status_command = device.send_command("get_wifi_status")
assert not has_command_failed(wifi_status_command)
assert wifi_status_command["results"][0]["result"]["data"]["status"] == "connected"
@pytest.mark.has_capability("wireless")
def test_set_wifi_invalid_network(get_openiris_device, ensure_board_in_mode, config):
device = ensure_board_in_mode("wifi", get_openiris_device())
params = {
"name": "main",
"ssid": "PleaseDontBeARealNetwork",
"password": "AndThePasswordIsFake",
"channel": 0,
"power": 0,
}
set_wifi_result = device.send_command("set_wifi", params)
# even if the network is fake, we should not fail to set it
assert not has_command_failed(set_wifi_result)
device.send_command("connect_wifi")
time.sleep(
config.INVALID_WIFI_CONNECTION_TIMEOUT
) # and let it try to for some time
wifi_status_command = device.send_command("get_wifi_status")
# the command should not fail as well, but we should get an error result
assert not has_command_failed(wifi_status_command)
assert wifi_status_command["results"][0]["result"]["data"]["status"] == "error"
# and not to break other tests, clean up
device.send_command("reset_config", {"section": "all"})
device.send_command("restart_device")
@pytest.mark.has_capability("wireless")
@pytest.mark.parametrize(
"payload",
(
{},
{
"ssid": "PleaseDontBeARealNetwork",
"password": "AndThePasswordIsFake",
"channel": 0,
"power": 0,
},
{
"name": "IaintGotNoNameAndIMustConnect",
"password": "AndThePasswordIsFake",
"channel": 0,
"power": 0,
},
),
)
def test_set_wifi_invalid_payload(ensure_board_in_mode, get_openiris_device, payload):
device = ensure_board_in_mode("wifi", get_openiris_device())
set_wifi_result = device.send_command("set_wifi", payload)
# even if the network is fake, we should not fail to set it
assert has_command_failed(set_wifi_result)
# and not to break other tests, clean up
device.send_command("reset_config", {"section": "all"})
device.send_command("restart_device")
def test_update_main_wifi_network(ensure_board_in_mode, get_openiris_device, config):
# now that the config is clear, let's try setting the wifi
device = ensure_board_in_mode("wifi", get_openiris_device())
params1 = {
"name": "main",
"ssid": "Nada",
"password": "Nuuh",
"channel": 0,
"power": 0,
}
params2 = {
**params1,
"ssid": config.WIFI_SSID,
"password": config.WIFI_PASS,
}
set_wifi_result = device.send_command("set_wifi", params1)
assert not has_command_failed(set_wifi_result)
set_wifi_result = device.send_command("set_wifi", params2)
assert not has_command_failed(set_wifi_result)
# and not to break other tests, clean up
device.send_command("reset_config", {"section": "all"})
device.send_command("restart_device")
def test_set_wifi_add_another_network(ensure_board_in_mode, get_openiris_device):
device = ensure_board_in_mode("wifi", get_openiris_device())
params = {
"name": "anotherNetwork",
"ssid": "PleaseDontBeARealNetwork",
"password": "AndThePassowrdIsFake",
"channel": 0,
"power": 0,
}
set_wifi_result = device.send_command("set_wifi", params)
assert not has_command_failed(set_wifi_result)
# and not to break other tests, clean up
device.send_command("reset_config", {"section": "all"})
device.send_command("restart_device")
@pytest.mark.parametrize(
"payload",
(
{
"ssid": "testAP",
"password": "12345678",
"channel": 0,
},
{
"ssid": "testAP",
"channel": 0,
},
{
"ssid": "testAP",
"password": "12345678",
},
{
"ssid": "testAP",
},
{
"password": "12345678",
},
),
)
def test_update_ap_wifi(ensure_board_in_mode, get_openiris_device, payload):
device = ensure_board_in_mode("wifi", get_openiris_device())
result = device.send_command("update_ap_wifi", payload)
assert not has_command_failed(result)
# and not to break other tests, clean up
device.send_command("reset_config", {"section": "all"})
device.send_command("restart_device")
@pytest.mark.parametrize(
"payload",
(
{}, # completely empty payload
{
"channel": 2
}, # technically valid payload, but we're missing the network name,
{
"name": "IAMNOTTHERE",
"channel": 2,
}, # None-existent network
),
)
@pytest.mark.has_capability("wireless")
def test_update_wifi_command_fail(ensure_board_in_mode, get_openiris_device, payload):
device = ensure_board_in_mode("wifi", get_openiris_device())
result = device.send_command("update_wifi", payload)
assert has_command_failed(result)
@pytest.mark.parametrize(
"payload",
(
{
"name": "anotherNetwork",
"ssid": "WEUPDATEDTHESSID",
"password": "ACOMPLETELYDIFFERENTPASS",
"channel": 1,
"power": 2,
},
{
"name": "anotherNetwork",
"password": "ACOMPLETELYDIFFERENTPASS",
},
{
"name": "anotherNetwork",
"ssid": "WEUPDATEDTHESSID",
},
{
"name": "anotherNetwork",
"channel": 1,
},
{
"name": "anotherNetwork",
"power": 2,
},
),
)
@pytest.mark.has_capability("wireless")
def test_update_wifi_command(ensure_board_in_mode, get_openiris_device, payload):
device = ensure_board_in_mode("wifi", get_openiris_device())
params = {
"name": "anotherNetwork",
"ssid": "PleaseDontBeARealNetwork",
"password": "AndThePasswordIsFake",
"channel": 0,
"power": 0,
}
set_wifi_result = device.send_command("set_wifi", params)
assert not has_command_failed(set_wifi_result)
device = ensure_board_in_mode("wifi", get_openiris_device())
result = device.send_command("update_wifi", payload)
assert not has_command_failed(result)
@pytest.mark.parametrize(
"payload",
(
{},
{"name": ""},
),
)
@pytest.mark.has_capability("wireless")
def test_delete_network_fail(ensure_board_in_mode, get_openiris_device, payload):
device = ensure_board_in_mode("wifi", get_openiris_device())
result = device.send_command("delete_network", payload)
assert has_command_failed(result)
@pytest.mark.parametrize("payload", ({"name": "main"}, {"name": "NOTANETWORK"}))
@pytest.mark.has_capability("wireless")
def test_delete_network(ensure_board_in_mode, get_openiris_device, payload):
device = ensure_board_in_mode("wifi", get_openiris_device())
result = device.send_command("delete_network", payload)
assert not has_command_failed(result)
@pytest.mark.parametrize(
"payload",
(
{},
{
"vlip": 0,
"hflip": 0,
"framesize": 5,
"quality": 7,
"brightness": 2,
},
{
"vlip": 0,
},
{
"hflip": 0,
},
{
"framesize": 5,
},
{
"quality": 7,
},
{
"brightness": 2,
},
),
)
def test_update_camera(get_openiris_device, payload):
device = get_openiris_device()
result = device.send_command("update_camera", payload)
assert not has_command_failed(result)
+78
View File
@@ -0,0 +1,78 @@
import time
import serial.tools.list_ports
from tools.openiris_device import OpenIrisDevice
OPENIRIS_DEVICE = None
class OpenIrisDeviceManager:
def __init__(self):
self._device: OpenIrisDevice | None = None
self.stored_ports = []
self._current_port: str | None = None
def get_device(self, port: str | None = None, config=None) -> OpenIrisDevice:
"""
Returns the current OpenIrisDevice connection helper
if the port changed from the one given previously, it will attempt to reconnect
if no device exists, we will create one and try to connect
This helper is designed to be used within a session long fixture
"""
if not port and not self._device:
raise ValueError("No device connected yet, provide a port first")
# I'm not sure if I like this approach
# maybe I need to rethink this fixture
current_ports = get_current_ports()
new_port = get_new_port(self.stored_ports, current_ports)
if new_port is not None:
self.stored_ports = current_ports
if not port:
port = new_port
if port and port != self._current_port:
print(f"Port changed from {self._current_port} to {port}, reconnecting...")
self._current_port = port
if self._device:
self._device.disconnect()
self._device = None
self._device = OpenIrisDevice(port, False, False)
self._device.connect()
time.sleep(config.SWITCH_MODE_REBOOT_TIME)
return self._device
def has_command_failed(result) -> bool:
return "error" in result or result["results"][0]["result"]["status"] != "success"
def get_current_ports() -> list[str]:
return [port.name for port in serial.tools.list_ports.comports()]
def get_new_port(old_ports, new_ports) -> str:
if ports_diff := list(set(new_ports) - set(old_ports)):
return ports_diff[0]
return None
class DetectPortChange:
def __init__(self):
self.old_ports = []
self.new_ports = []
def __enter__(self, *args, **kwargs):
self.old_ports = get_current_ports()
return self
def __exit__(self, *args, **kwargs):
self.new_ports = get_current_ports()
def get_new_port(self):
return get_new_port(self.old_ports, self.new_ports)
View File
+143
View File
@@ -0,0 +1,143 @@
import time
import json
import serial
class OpenIrisDevice:
def __init__(self, port: str, debug: bool, debug_commands: bool):
self.port = port
self.debug = debug
self.debug_commands = debug_commands
self.connection: serial.Serial | None = None
self.connected = False
def __enter__(self):
self.connected = self.__connect()
return self
def __exit__(self, type, value, traceback):
self.__disconnect()
self.connected = False
def connect(self):
self.connected = self.__connect()
def disconnect(self):
self.__disconnect()
self.connected = False
def __connect(self) -> bool:
print(f"📡 Connecting directly to {self.port}...")
try:
self.connection = serial.Serial(
port=self.port, baudrate=115200, timeout=1, write_timeout=1
)
self.connection.dtr = False
self.connection.rts = False
print(f"✅ Connected to the device on {self.port}")
return True
except Exception as e:
print(f"❌ Failed to connect to {self.port}: {e}")
return False
def __disconnect(self):
if self.connection and self.connection.is_open:
self.connection.close()
print(f"🔌 Disconnected from {self.port}")
def __check_if_response_is_complete(self, response) -> dict | None:
try:
if self.debug:
print(f"\nCHECKING: {response} \n")
return json.loads(response)
except ValueError:
if self.debug:
print("\nCHECK FAILED\n")
return None
def __read_response(self, timeout: int | None = None) -> dict | None:
# we can try and retrieve the response now.
# it should be more or less immediate, but some commands may take longer
# so we gotta timeout
timeout = timeout if timeout is not None else 15
start_time = time.time()
response_buffer = ""
while time.time() - start_time < timeout:
if self.connection.in_waiting:
packet = self.connection.read_all().decode("utf-8", errors="ignore")
if self.debug and packet.strip():
print(f"Received: {packet}")
print("-" * 10)
print(f"Current buffer: {response_buffer}")
print("-" * 10)
# we can't rely on new lines to detect if we're done
# nor can we assume that we're always gonna get valid json response
# but we can assume that if we're to get a valid response, it's gonna be json
# so we can start actually building the buffer only when
# some part of the packet starts with "{", and start building from there
# we can assume that no further data will be returned, so we can validate
# right after receiving the last packet
if (not response_buffer and "{" in packet) or response_buffer:
# assume we just started building the buffer and we've received the first packet
# alternative approach in case this doesn't work - we're always sending a valid json
# so we can start building the buffer from the first packet and keep trying to find the
# starting and ending brackets, extract that part and validate, if the message is complete, return
if not response_buffer:
starting_idx = packet.find("{")
response_buffer = packet[starting_idx:]
else:
response_buffer += packet
# and once we get something, we can validate if it's a valid json
if parsed_response := self.__check_if_response_is_complete(
response_buffer
):
return parsed_response
else:
# if it's not a valid response just yet,
# we might've stumbled a case where we got a valid response
# but to the end of it, we've got some leftovers attached
# so, we can try and find the ending
ending_idx = response_buffer[::-1].find("}")
if reparsed_response := self.__check_if_response_is_complete(
response_buffer[: len(response_buffer) - ending_idx]
):
return reparsed_response
else:
time.sleep(0.1)
return None
def is_connected(self) -> bool:
return self.connected
def send_command(
self, command: str, params: dict | None = None, timeout: int | None = None
) -> dict:
if not self.connection or not self.connection.is_open:
return {"error": "Device Not Connected"}
cmd_obj = {"commands": [{"command": command}]}
if params:
cmd_obj["commands"][0]["data"] = params
# we're expecting the json string to end with a new line
# to signify we've finished sending the command
cmd_str = json.dumps(cmd_obj) + "\n"
try:
# clean it out first, just to be sure we're starting fresh
self.connection.reset_input_buffer()
if self.debug or self.debug_commands:
print(f"Sending command: {cmd_str}")
self.connection.write(cmd_str.encode())
response = self.__read_response(timeout)
if self.debug:
print(f"Received response: {response}")
return response or {"error": "Command timeout"}
except Exception as e:
return {"error": f"Communication error: {e}"}
+17 -126
View File
@@ -5,14 +5,14 @@
# ///
import json
import time
import argparse
import sys
import serial
import string
from dataclasses import dataclass
from openiris_device import OpenIrisDevice
def is_back(choice: str):
return choice.lower() in ["back", "b", "exit"]
@@ -79,122 +79,6 @@ class Menu(SubMenu):
super().__init__(title, context, parent_menu)
class OpenIrisDevice:
def __init__(self, port: str, debug: bool, debug_commands: bool):
self.port = port
self.debug = debug
self.debug_commands = debug_commands
self.connection: serial.Serial | None = None
self.connected = False
def __enter__(self):
self.connected = self.__connect()
return self
def __exit__(self, type, value, traceback):
self.__disconnect()
def __connect(self) -> bool:
print(f"📡 Connecting directly to {self.port}...")
try:
self.connection = serial.Serial(
port=self.port, baudrate=115200, timeout=1, write_timeout=1
)
print(f"✅ Connected to the device on {self.port}")
return True
except Exception as e:
print(f"❌ Failed to connect to {self.port}: {e}")
return False
def __disconnect(self):
if self.connection and self.connection.is_open:
self.connection.close()
print(f"🔌 Disconnected from {self.port}")
def __check_if_response_is_complete(self, response) -> dict | None:
try:
return json.loads(response)
except ValueError:
return None
def __read_response(self, timeout: int | None = None) -> dict | None:
# we can try and retrieve the response now.
# it should be more or less immediate, but some commands may take longer
# so we gotta timeout
timeout = timeout if timeout is not None else 15
start_time = time.time()
response_buffer = ""
while time.time() - start_time < timeout:
if self.connection.in_waiting:
packet = self.connection.read_all().decode("utf-8", errors="ignore")
if self.debug and packet.strip():
print(f"Received: {packet}")
print("-" * 10)
print(f"Current buffer: {response_buffer}")
print("-" * 10)
# we can't rely on new lines to detect if we're done
# nor can we assume that we're always gonna get valid json response
# but we can assume that if we're to get a valid response, it's gonna be json
# so we can start actually building the buffer only when
# some part of the packet starts with "{", and start building from there
# we can assume that no further data will be returned, so we can validate
# right after receiving the last packet
if (not response_buffer and "{" in packet) or response_buffer:
# assume we just started building the buffer and we've received the first packet
# alternative approach in case this doesn't work - we're always sending a valid json
# so we can start building the buffer from the first packet and keep trying to find the
# starting and ending brackets, extract that part and validate, if the message is complete, return
if not response_buffer:
starting_idx = packet.find("{")
response_buffer = packet[starting_idx:]
else:
response_buffer += packet
# and once we get something, we can validate if it's a valid json
if parsed_response := self.__check_if_response_is_complete(
response_buffer
):
return parsed_response
else:
time.sleep(0.1)
return None
def is_connected(self) -> bool:
return self.connected
def send_command(
self, command: str, params: dict | None = None, timeout: int | None = None
) -> dict:
if not self.connection or not self.connection.is_open:
return {"error": "Device Not Connected"}
cmd_obj = {"commands": [{"command": command}]}
if params:
cmd_obj["commands"][0]["data"] = params
# we're expecting the json string to end with a new line
# to signify we've finished sending the command
cmd_str = json.dumps(cmd_obj) + "\n"
try:
# clean it out first, just to be sure we're starting fresh
self.connection.reset_input_buffer()
if self.debug or self.debug_commands:
print(f"Sending command: {cmd_str}")
self.connection.write(cmd_str.encode())
response = self.__read_response(timeout)
if self.debug:
print(f"Received response: {response}")
return response or {"error": "Command timeout"}
except Exception as e:
return {"error": f"Communication error: {e}"}
@dataclass
class WiFiNetwork:
ssid: str
@@ -314,14 +198,18 @@ def get_serial_info(device: OpenIrisDevice) -> dict:
"mac": response["results"][0]["result"]["data"]["mac"],
}
def get_device_info(device: OpenIrisDevice) -> dict:
response = device.send_command("get_who_am_i")
if has_command_failed(response):
print(f"❌ Failed to get device info: {response['error']}")
return {"who_am_i": None, "version": None}
return {"who_am_i": response["results"][0]["result"]["data"]["who_am_i"], "version": response["results"][0]["result"]["data"]["version"]}
return {
"who_am_i": response["results"][0]["result"]["data"]["who_am_i"],
"version": response["results"][0]["result"]["data"]["version"],
}
def get_wifi_status(device: OpenIrisDevice) -> dict:
@@ -339,7 +227,9 @@ def get_led_current(device: OpenIrisDevice) -> dict:
print(f"❌ Failed to get LED current: {response}")
return {"led_current_ma": "unknown"}
return {"led_current_ma": response["results"][0]["result"]["data"]["led_current_ma"]}
return {
"led_current_ma": response["results"][0]["result"]["data"]["led_current_ma"]
}
def configure_device_name(device: OpenIrisDevice, *args, **kwargs):
@@ -462,11 +352,11 @@ def get_settings_summary(device: OpenIrisDevice, *args, **kwargs):
print(f"🔑 Serial: {summary['Identity']}")
print(f"💡 LED PWM Duty: {summary['LED']['duty_cycle']}%")
print(f"🎚️ Mode: {summary['Mode']['mode']}")
current_section = summary.get("Current", {})
led_current_ma = current_section.get("led_current_ma")
print(f"🔌 LED Current: {led_current_ma} mA")
advertised_name_data = summary.get("AdvertisedName", {})
advertised_name = advertised_name_data.get("name")
print(f"📛 Name: {advertised_name}")
@@ -479,7 +369,6 @@ def get_settings_summary(device: OpenIrisDevice, *args, **kwargs):
if ver:
print(f"🧭 Version: {ver}")
wifi = summary.get("WiFi", {}).get("wifi_status", {})
if wifi:
status = wifi.get("status", "unknown")
@@ -487,6 +376,7 @@ def get_settings_summary(device: OpenIrisDevice, *args, **kwargs):
configured = wifi.get("networks_configured", 0)
print(f"📶 WiFi: {status} | IP: {ip} | Networks configured: {configured}")
def restart_device_command(device: OpenIrisDevice, *args, **kwargs):
print("🔄 Restarting device...")
response = device.send_command("restart_device")
@@ -497,6 +387,7 @@ def restart_device_command(device: OpenIrisDevice, *args, **kwargs):
print("✅ Device restart command sent successfully")
print("💡 Please wait a few seconds for the device to reboot")
def scan_networks(wifi_scanner: WiFiScanner, *args, **kwargs):
use_custom_timeout = (
input("Should we use a custom scan timeout? (y/n)\n>> ").strip().lower() == "y"
@@ -626,7 +517,7 @@ def automatic_wifi_configuration(
ip = None
last_status = None
while time.time() - start < timeout_s:
status = get_wifi_status(device).get("wifi_status", {})
status = get_wifi_status(device).get("wifi_status") or {}
last_status = status
ip = status.get("ip_address")
if ip and ip not in ("0.0.0.0", "", None):
@@ -654,7 +545,7 @@ def attempt_wifi_connection(device: OpenIrisDevice, *args, **kwargs):
def check_wifi_status(device: OpenIrisDevice, *args, **kwargs):
status = get_wifi_status(device).get("wifi_status")
status = get_wifi_status(device).get("wifi_status") or {}
print(f"📶 WiFi Status: {status.get('status', 'Unknown')}")
if ip_address := status.get("ip_address"):
print(f"🌐 IP Address: {ip_address}")
+268 -158
View File
@@ -1,221 +1,331 @@
import os
import difflib
import shutil
import argparse
from typing import Dict, Optional, List
HEADER_COLOR = "\033[95m"
OKGREEN = '\033[92m'
WARNING = '\033[93m'
OKBLUE = '\033[94m'
ENDC = '\033[0m'
OKGREEN = "\033[92m"
WARNING = "\033[93m"
OKBLUE = "\033[94m"
ENDC = "\033[0m"
BOARDS_DIR_NAME = "boards"
SDKCONFIG_DEFAULTS_FILENAME = "sdkconfig.base_defaults"
# some components are super platform specific.
# to a point where building them with esp32 will fail every single time due to depndencies not supporting it
# with our own components, we can ship some shims to keep things clean
# but with those, unless we roll our own somehow, we're out of luck.
# So, to make things simpler, when selecting for which board to build, we're gonna reconfigure the components
# on the fly.
PLATFORM_SPECIFIC_COMPONENTS = {"esp32s3": ["usb_device_uvc"]}
PLATFORM_SPECIFIC_COMPONENTS_DIRS = {"esp32s3": "esp32s3"}
def get_root_path() -> str:
return os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]
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)
return os.path.join(get_root_path(), BOARDS_DIR_NAME)
def get_config_platform(_parsed_config: dict) -> str:
# 1:-1 to strip quotes
return _parsed_config["CONFIG_IDF_TARGET"][1:-1]
def enumerate_board_configs() -> Dict[str, str]:
"""Walk the boards directory and build a mapping of board names to absolute file paths.
"""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):
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
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
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())}")
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)
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:
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
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")
return os.path.join(get_root_path(), "sdkconfig")
def get_board_config_path(board_key: str) -> str:
return BOARD_CONFIGS[board_key]
return BOARD_CONFIGS[board_key]
def get_base_config_path() -> str:
# base defaults moved under boards directory
return os.path.join(get_boards_root(), SDKCONFIG_DEFAULTS_FILENAME)
# base defaults moved under boards directory
return os.path.join(get_boards_root(), SDKCONFIG_DEFAULTS_FILENAME)
def parse_config(config_file) -> dict:
config = {}
for line in config_file:
line = line.strip().split("=")
if len(line) == 2:
config[line[0]] = line[1]
else:
# lines without value are usually comments, we're safe to store empty string there
config[line[0]] = ""
return config
config = {}
for line in config_file:
line = line.strip().split("=")
if len(line) == 2:
config[line[0]] = line[1]
else:
# lines without value are usually comments, we're safe to store empty string there
config[line[0]] = ""
return config
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:
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.ssid:
_new_config["CONFIG_WIFI_SSID"] = f'"{_args.ssid}"'
_new_config["CONFIG_WIFI_PASSWORD"] = f'"{_args.password}"'
else:
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:
_new_config["CONFIG_WIFI_SSID"] = "\"\""
_new_config["CONFIG_WIFI_PASSWORD"] = "\"\""
return _new_config
if _args.clear_wifi:
_new_config["CONFIG_WIFI_SSID"] = '""'
_new_config["CONFIG_WIFI_PASSWORD"] = '""'
return _new_config
def compute_diff(_parsed_base_config: dict, _parsed_board_config: dict) -> dict:
_diff = {}
for _key in _parsed_board_config:
if _key not in _parsed_base_config:
if _parsed_board_config[_key] != "":
_diff[_key] = f"{OKGREEN}+{ENDC} {_parsed_board_config[_key]}"
else:
if _parsed_board_config[_key] != _parsed_base_config[_key]:
_diff[_key] = f"{OKGREEN}{_parsed_base_config[_key]}{ENDC} -> {OKBLUE}{_parsed_board_config[_key]}{ENDC}"
return _diff
_diff = {}
for _key in _parsed_board_config:
if _key not in _parsed_base_config:
if _parsed_board_config[_key] != "":
_diff[_key] = f"{OKGREEN}+{ENDC} {_parsed_board_config[_key]}"
else:
if _parsed_board_config[_key] != _parsed_base_config[_key]:
_diff[_key] = (
f"{OKGREEN}{_parsed_base_config[_key]}{ENDC} -> {OKBLUE}{_parsed_board_config[_key]}{ENDC}"
)
return _diff
def _move_directories(component: str, destination_path: str):
if os.path.exists(component):
shutil.move(component, destination_path)
def handle_extra_components(old_platform: str, new_platform: str, dry_run: bool):
print(
f"{OKGREEN}Switching components configuration from platform:{ENDC} {OKBLUE}{old_platform}{ENDC} {OKGREEN}to platform:{ENDC} {OKBLUE}{new_platform}{ENDC}"
)
if old_platform == new_platform:
print(f"{OKGREEN}The platform is the same. Nothing to do here.{ENDC}")
return
old_platform_components = PLATFORM_SPECIFIC_COMPONENTS.get(old_platform, [])
new_platform_components = PLATFORM_SPECIFIC_COMPONENTS.get(new_platform, [])
if dry_run:
print(f"{OKGREEN}Would remove: {ENDC}")
for component in old_platform_components:
print(f"{OKBLUE}- {component} {ENDC}")
print(f"{OKGREEN}Would add: {ENDC}")
for component in new_platform_components:
print(f"{OKBLUE}- {component} {ENDC}")
return
components_path = os.path.join(get_root_path(), "components")
if old_base_dir := PLATFORM_SPECIFIC_COMPONENTS_DIRS.get(old_platform):
old_extra_components_path = os.path.join(
os.path.join(get_root_path(), "extra_components"), old_base_dir
)
for component in old_platform_components:
component_path = os.path.join(components_path, component)
print(
f"{OKGREEN}Moving:{ENDC}{OKBLUE} {component}{ENDC} to {OKBLUE}{old_extra_components_path}{ENDC}"
)
_move_directories(component_path, old_extra_components_path)
if new_base_dir := PLATFORM_SPECIFIC_COMPONENTS_DIRS.get(new_platform):
new_extra_components_path = os.path.join(
os.path.join(get_root_path(), "extra_components"), new_base_dir
)
for component in new_platform_components:
component_path = os.path.join(new_extra_components_path, component)
print(
f"{OKGREEN}Moving:{ENDC}{OKBLUE} {component}{ENDC} to {OKBLUE}{components_path}{ENDC}"
)
_move_directories(component_path, components_path)
def main():
parser = build_arg_parser()
args = parser.parse_args()
parser = build_arg_parser()
args = parser.parse_args()
if args.list:
list_boards()
return
if args.list:
list_boards()
return
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)
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)
if not os.path.isfile(get_base_config_path()):
raise SystemExit(f"Base defaults file not found: {get_base_config_path()}")
if not os.path.isfile(get_base_config_path()):
raise SystemExit(f"Base defaults file not found: {get_base_config_path()}")
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)}")
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)}"
)
with open(get_main_config_path(), "r+") as main_config:
parsed_main_config = parse_config(main_config)
with open(get_main_config_path(), "r+") as main_config:
parsed_main_config = parse_config(main_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)
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)
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")
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:
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}{normalized}{ENDC}")
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 __name__ == "__main__": # pragma: no cover
main()
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"{WARNING}[DRY-RUN]{ENDC} Skipping writing to files")
handle_extra_components(
get_config_platform(parsed_main_config),
get_config_platform(new_board_config),
args.dry_run,
)
print(
f"{OKGREEN}Done. ESP-IDF is setup to build for:{ENDC} {OKBLUE}{normalized}{ENDC}"
)
if __name__ == "__main__":
main()
Generated
+152
View File
@@ -0,0 +1,152 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "blink"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "pyserial" },
{ name = "pytest" },
{ name = "python-dotenv" },
]
[package.dev-dependencies]
dev = [
{ name = "bumpver" },
]
[package.metadata]
requires-dist = [
{ name = "pyserial", specifier = ">=3.5" },
{ name = "pytest", specifier = ">=9.0.1" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
]
[package.metadata.requires-dev]
dev = [{ name = "bumpver", specifier = ">=2025.1131" }]
[[package]]
name = "bumpver"
version = "2025.1131"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "colorama" },
{ name = "lexid" },
{ name = "toml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8f/8a/cc13e816e9f0849dce423b904b06fd91b5444cba6df3200d512a702f2e95/bumpver-2025.1131.tar.gz", hash = "sha256:a35fd2d43a5f65f014035c094866bd3bd6c739606f29fd41246d6ec6e839d3f9", size = 115372, upload-time = "2025-07-02T20:36:11.982Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/5b/2d5ea6802495ee4506721977be522804314aa66ad629d9356e3c7e5af4a6/bumpver-2025.1131-py2.py3-none-any.whl", hash = "sha256:c02527f6ed7887afbc06c07630047b24a9f9d02d544a65639e99bf8b92aaa674", size = 65361, upload-time = "2025-07-02T20:36:10.103Z" },
]
[[package]]
name = "click"
version = "8.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "lexid"
version = "2021.1006"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/60/0b/28a3f9abc75abbf1fa996eb2dd77e1e33a5d1aac62566e3f60a8ec8b8a22/lexid-2021.1006.tar.gz", hash = "sha256:509a3a4cc926d3dbf22b203b18a4c66c25e6473fb7c0e0d30374533ac28bafe5", size = 11525, upload-time = "2021-04-02T20:18:34.668Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/e3/35764404a4b7e2021be1f88f42264c2e92e0c4720273559a62461ce64a47/lexid-2021.1006-py2.py3-none-any.whl", hash = "sha256:5526bb5606fd74c7add23320da5f02805bddd7c77916f2dc1943e6bada8605ed", size = 7587, upload-time = "2021-04-02T20:18:33.129Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pyserial"
version = "3.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" },
]
[[package]]
name = "pytest"
version = "9.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
[[package]]
name = "toml"
version = "0.10.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
]