cleaning up board config and switch tool

This commit is contained in:
PhosphorosVR
2025-09-06 17:10:46 +02:00
parent ad7b9b8be9
commit 909a2779ac
12 changed files with 284 additions and 3176 deletions

View File

@@ -50,16 +50,23 @@ After this, youre ready for the Quick start below.
## Quick start
### 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 --board xiao-esp32s3 --diff
python .\tools\switchBoardType.py --list
python .\tools\switchBoardType.py --board seed_studio_xiao_esp32s3 --diff
```
macOS/Linux (bash):
```bash
python3 ./tools/switchBoardType.py --board xiao-esp32s3 --diff
python3 ./tools/switchBoardType.py --list
python3 ./tools/switchBoardType.py --board seed_studio_xiao_esp32s3 --diff
```
- Set `--board` to your target board
- `--diff` shows what changed in the config
Notes:
- Use `--list` to see all detected board keys.
- Board key = relative path under `boards/` with `/` replaced by `_` (and duplicate tail segments collapsed, e.g. `project_babble/project_babble` -> `project_babble`).
- `--diff` shows what will change vs the current `sdkconfig`.
- You can also pass partial or pathlike inputs (e.g. `facefocusvr/eye_L`), the tool normalizes them.
### 2) Build & flash
- Set the target (e.g., ESP32S3).
@@ -118,6 +125,17 @@ If you want to dig deeper: commands are mapped via the `CommandManager` under `c
- 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`.
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.
---
Feedback, issues, and PRs are welcome.

View File

@@ -67,7 +67,7 @@ CONFIG_MONITORING_LED_GAIN=11
CONFIG_MONITORING_LED_SHUNT_MILLIOHM=22000
CONFIG_MONITORING_LED_SAMPLES=10
CONFIG_MONITORING_LED_INTERVAL_MS=500
CONFIG_GENERAL_WHO_AM_I="facefocusvr_eye"
CONFIG_GENERAL_BOARD="facefocusvr_eye"
# CONFIG_GENERAL_ENABLE_WIRELESS is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80=y
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set

View File

@@ -14,7 +14,7 @@ CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
# Camera sensor pinout configuration
CONFIG_CAMERA_MODULE_NAME="ESP32S3_XIAO_SENSE"
CONFIG_CAMERA_MODULE_NAME="FaceFocusVR_Face"
CONFIG_PWDN_GPIO_NUM=-1
CONFIG_RESET_GPIO_NUM=-1
CONFIG_XCLK_GPIO_NUM=10
@@ -54,9 +54,26 @@ CONFIG_SPIRAM_SPEED_80M=y
# CONFIG_SPIRAM_SPEED_120M is not set
CONFIG_SPIRAM_SPEED=80
CONFIG_SPIRAM_SPEED_80M=y
# CONFIG_LED_EXTERNAL_CONTROL is not set
CONFIG_LED_EXTERNAL_CONTROL=y
CONFIG_LED_EXTERNAL_GPIO=9
CONFIG_LED_EXTERNAL_PWM_FREQ=20000
CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=50
CONFIG_CAMERA_USB_XCLK_FREQ=23000000
CONFIG_GENERAL_INCLUDE_UVC_MODE=y
# CONFIG_START_IN_UVC_MODE is not set
# CONFIG_MONITORING_LED_CURRENT is not set
CONFIG_GENERAL_WHO_AM_I="xiao_esp32s3"
CONFIG_START_IN_UVC_MODE=y
CONFIG_MONITORING_LED_CURRENT=y
CONFIG_MONITORING_LED_ADC_GPIO=3
CONFIG_MONITORING_LED_GAIN=11
CONFIG_MONITORING_LED_SHUNT_MILLIOHM=22000
CONFIG_MONITORING_LED_SAMPLES=10
CONFIG_MONITORING_LED_INTERVAL_MS=500
CONFIG_GENERAL_BOARD="facefocusvr_eye"
# CONFIG_GENERAL_ENABLE_WIRELESS is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80=y
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240 is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=80
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80=y
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160 is not set
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240 is not set
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=80

View File

@@ -67,7 +67,7 @@ CONFIG_MONITORING_LED_GAIN=11
CONFIG_MONITORING_LED_SHUNT_MILLIOHM=22000
CONFIG_MONITORING_LED_SAMPLES=10
CONFIG_MONITORING_LED_INTERVAL_MS=500
CONFIG_GENERAL_WHO_AM_I="facefocusvr_face"
CONFIG_GENERAL_BOARD="facefocusvr_face"
# CONFIG_GENERAL_ENABLE_WIRELESS is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80=y
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set

View File

@@ -54,5 +54,5 @@ CONFIG_CAMERA_USB_XCLK_FREQ=23000000
CONFIG_GENERAL_INCLUDE_UVC_MODE=y
# CONFIG_START_IN_UVC_MODE is not set
# CONFIG_MONITORING_LED_CURRENT is not set
CONFIG_GENERAL_WHO_AM_I="project_babble"
CONFIG_GENERAL_BOARD="project_babble"
CONFIG_GENERAL_ENABLE_WIRELESS=y

View File

@@ -573,7 +573,7 @@ CONFIG_ENV_GPIO_OUT_RANGE_MAX=48
# CONFIG_GENERAL_INCLUDE_UVC_MODE is not set
# CONFIG_START_IN_UVC_MODE is not set
# CONFIG_GENERAL_STARTUP_DELAY is not set
CONFIG_GENERAL_Version="0.0.1"
CONFIG_GENERAL_VERSION="0.0.1"
# end of OpenIris: General Configuration
#

View File

@@ -59,5 +59,5 @@ CONFIG_CAMERA_USB_XCLK_FREQ=23000000
CONFIG_GENERAL_INCLUDE_UVC_MODE=y
# CONFIG_START_IN_UVC_MODE is not set
# CONFIG_MONITORING_LED_CURRENT is not set
CONFIG_GENERAL_WHO_AM_I="xiao_esp32s3"
CONFIG_GENERAL_BOARD="xiao_esp32s3"
CONFIG_GENERAL_ENABLE_WIRELESS=y

View File

@@ -251,8 +251,8 @@ CommandResult getLEDCurrentCommand(std::shared_ptr<DependencyRegistry> registry)
CommandResult getInfoCommand(std::shared_ptr<DependencyRegistry> /*registry*/)
{
const char* who = CONFIG_GENERAL_WHO_AM_I;
const char* ver = CONFIG_GENERAL_Version;
const char* who = CONFIG_GENERAL_BOARD;
const char* ver = CONFIG_GENERAL_VERSION;
// Ensure non-null strings
if (!who) who = "";
if (!ver) ver = "";

View File

@@ -7,28 +7,35 @@ endmenu
menu "OpenIris: General Configuration"
config START_IN_UVC_MODE
bool "Start in UVC Mode"
bool "Default initial streaming mode = UVC"
default false
help
Enables UVC (wired) support in the firmware by default.
To be used when a board is designed to be used primarily with wired headsets.
When enabled, the default device streaming mode will be UVC unless overridden by a
saved preference. When disabled, the default mode is AUTO.
Sets the poweron default streaming mode (before any user preference is stored).
If enabled AND UVC support is compiled in (GENERAL_INCLUDE_UVC_MODE), the device
will default to UVC mode on first boot. If disabled it defaults to SETUP mode,
waiting for a user choice or commands. This option does NOT compile UVC support in;
it only changes the initial preference used when no saved mode exists.
config GENERAL_INCLUDE_UVC_MODE
bool "Wired mode"
bool "Include UVC (USB Video Class) support"
default false
help
Enables UVC (wired) support in the firmware. When enabled, the
default device streaming mode will be UVC unless overridden by a
saved preference. When disabled, the default mode is AUTO.
Compiles in UVC (USB Video Class) streaming support (camera + CDC bridge).
Disable this on boards that are WiFi only or where USB bandwidth / memory
should be conserved. If disabled any attempt to switch to UVC mode will log
an error and fall back to WiFi (if wireless is enabled). Combine with
START_IN_UVC_MODE only when the hardware supports UVC.
config GENERAL_STARTUP_DELAY
int "UVC delay (s)"
int "Setup grace period (s)"
default 30
range 10 10000
help
Delay in seconds before the ESP reports itself as a UVC device.
Number of seconds the device remains in SETUP / heartbeat mode on boot (when the
current streaming mode resolves to SETUP) before automatically launching the
selected streaming backend (UVC or WiFi). During this window host commands can
change mode or other settings. After the timer expires, streaming starts
automatically unless a command was received or startup was paused.
config GENERAL_ENABLE_WIRELESS
bool "Enable wireless (WiFi/Bluetooth)"
@@ -38,13 +45,13 @@ menu "OpenIris: General Configuration"
and any Bluetooth memory (if present on the SoC) should be left released. This can
reduce power consumption when operating solely in UVC mode or without networking.
config GENERAL_WHO_AM_I
string "Who am I (device identifier)"
config GENERAL_BOARD
string "Board / device identifier"
default "OpenIris"
help
A human-readable product or device identifier exposed via the get_info command.
A human-readable board or device identifier exposed via the get_info command.
config GENERAL_Version
config GENERAL_VERSION
string "Firmware version"
default "0.0.0"
help

586
sdkconfig

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import os
import argparse
from typing import Dict, Optional, List
HEADER_COLOR = "\033[95m"
OKGREEN = '\033[92m'
@@ -7,38 +8,125 @@ WARNING = '\033[93m'
OKBLUE = '\033[94m'
ENDC = '\033[0m'
sdkconfig_defaults = "sdkconfig.base_defaults"
supported_boards = [
"xiao-esp32s3",
"project_babble",
"facefocusvr_face",
"facefocusvr_eye"
]
parser = argparse.ArgumentParser()
parser.add_argument("-b", "--board", help="Board to switch to", choices=supported_boards)
parser.add_argument("--dry-run", help="Dry run, won't modify files", action="store_true", required=False)
parser.add_argument("--diff", help="Show the difference between base config and selected board", action="store_true", required=False)
parser.add_argument("--ssid", help="Set the SSID for the selected board", required=False, type=str, default="")
parser.add_argument("--password", help="Set the password For the provided network", required=False, type=str, default="")
parser.add_argument("--clear-wifi", help="Should we clear the wifi details", action="store_true", required=False)
args = parser.parse_args()
BOARDS_DIR_NAME = "boards"
SDKCONFIG_DEFAULTS_FILENAME = "sdkconfig.base_defaults"
def get_root_path() -> str:
return os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]
def get_boards_root() -> str:
return os.path.join(get_root_path(), BOARDS_DIR_NAME)
def enumerate_board_configs() -> Dict[str, str]:
"""Walk the boards directory and build a mapping of board names to absolute file paths.
Naming strategy:
- Relative path from boards/ to file with path separators replaced by '_'.
- If the last two path segments are identical (e.g. project_babble/project_babble) collapse to a single segment.
- For facefocusvr eye boards we keep eye_L / eye_R suffix to distinguish configs even though WHO_AM_I is same.
"""
boards_dir = get_boards_root()
mapping: Dict[str, str] = {}
if not os.path.isdir(boards_dir):
return mapping
for root, _dirs, files in os.walk(boards_dir):
for f in files:
if f == SDKCONFIG_DEFAULTS_FILENAME:
continue
rel_path = os.path.relpath(os.path.join(root, f), boards_dir)
parts = rel_path.split(os.sep)
if len(parts) >= 2 and parts[-1] == parts[-2]: # collapse duplicate tail
parts = parts[:-1]
board_key = "_".join(parts)
mapping[board_key] = os.path.join(root, f)
return mapping
BOARD_CONFIGS = enumerate_board_configs()
def build_arg_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser()
p.add_argument("-b", "--board", help="Board name (run with --list to see options). Flexible: accepts path-like or partial if unique.")
p.add_argument("--list", help="List discovered boards and exit", action="store_true")
p.add_argument("--dry-run", help="Dry run, won't modify files", action="store_true")
p.add_argument("--diff", help="Show the difference between base config and selected board", action="store_true")
p.add_argument("--ssid", help="Set the WiFi SSID", type=str, default="")
p.add_argument("--password", help="Set the WiFi password", type=str, default="")
p.add_argument("--clear-wifi", help="Clear WiFi credentials", action="store_true")
return p
def list_boards():
print("Discovered boards:")
width = max((len(k) for k in BOARD_CONFIGS), default=0)
for name, path in sorted(BOARD_CONFIGS.items()):
print(f" {name.ljust(width)} -> {os.path.relpath(path, get_root_path())}")
def _suggest_boards(partial: str) -> List[str]:
if not partial:
return []
partial_low = partial.lower()
contains = [b for b in BOARD_CONFIGS if partial_low in b.lower()]
if contains:
return contains[:10]
# simple levenshtein distance limited (manual lightweight)
def distance(a: str, b: str) -> int:
if len(a) < len(b):
a, b = b, a
prev = list(range(len(b)+1))
for i, ca in enumerate(a, 1):
cur = [i]
for j, cb in enumerate(b, 1):
ins = cur[j-1] + 1
dele = prev[j] + 1
sub = prev[j-1] + (ca != cb)
cur.append(min(ins, dele, sub))
prev = cur
return prev[-1]
ranked = sorted(BOARD_CONFIGS, key=lambda k: distance(partial_low, k.lower()))
return ranked[:5]
def normalize_board_name(raw: Optional[str]) -> Optional[str]:
if raw is None:
return None
candidate = raw.strip()
if not candidate:
return None
candidate = candidate.replace('\\', '/').rstrip('/')
# strip leading folders like tools/, boards/
parts = [p for p in candidate.split('/') if p not in ('.', '') and p not in ('tools', 'boards')]
if parts:
candidate = parts[-1] if len(parts) == 1 else "_".join(parts)
candidate = candidate.replace('-', '_')
# exact match
if candidate in BOARD_CONFIGS:
return candidate
# try ending match
endings = [b for b in BOARD_CONFIGS if b.endswith(candidate)]
if len(endings) == 1:
return endings[0]
if len(endings) > 1:
print(f"Ambiguous board '{raw}'. Could be: {', '.join(endings)}")
return None
# attempt case-insensitive
lower_map = {b.lower(): b for b in BOARD_CONFIGS}
if candidate.lower() in lower_map:
return lower_map[candidate.lower()]
return None
def get_main_config_path() -> str:
return os.path.join(get_root_path(), "sdkconfig")
def get_board_config_path() -> str:
return os.path.join(get_root_path(), f"sdkconfig.board.{args.board}")
def get_board_config_path(board_key: str) -> str:
return BOARD_CONFIGS[board_key]
def get_base_config_path() -> str:
return os.path.join(get_root_path(), sdkconfig_defaults)
# base defaults moved under boards directory
return os.path.join(get_boards_root(), SDKCONFIG_DEFAULTS_FILENAME)
def parse_config(config_file) -> dict:
@@ -53,15 +141,17 @@ def parse_config(config_file) -> dict:
return config
def handle_wifi_config(_new_config: dict, _main_config: dict) -> dict:
if args.ssid:
_new_config["CONFIG_WIFI_SSID"] = f"\"{args.ssid}\""
_new_config["CONFIG_WIFI_PASSWORD"] = f"\"{args.password}\""
def handle_wifi_config(_new_config: dict, _main_config: dict, _args) -> dict:
if _args.ssid:
_new_config["CONFIG_WIFI_SSID"] = f"\"{_args.ssid}\""
_new_config["CONFIG_WIFI_PASSWORD"] = f"\"{_args.password}\""
else:
_new_config["CONFIG_WIFI_SSID"] = _main_config["CONFIG_WIFI_SSID"]
_new_config["CONFIG_WIFI_PASSWORD"] = _main_config["CONFIG_WIFI_PASSWORD"]
if "CONFIG_WIFI_SSID" in _main_config:
_new_config["CONFIG_WIFI_SSID"] = _main_config["CONFIG_WIFI_SSID"]
if "CONFIG_WIFI_PASSWORD" in _main_config:
_new_config["CONFIG_WIFI_PASSWORD"] = _main_config["CONFIG_WIFI_PASSWORD"]
if args.clear_wifi:
if _args.clear_wifi:
_new_config["CONFIG_WIFI_SSID"] = "\"\""
_new_config["CONFIG_WIFI_PASSWORD"] = "\"\""
return _new_config
@@ -79,51 +169,65 @@ def compute_diff(_parsed_base_config: dict, _parsed_board_config: dict) -> dict:
return _diff
print(f"{OKGREEN}Switching configuration to board:{ENDC} {OKBLUE}{args.board}{ENDC}")
print(f"{OKGREEN}Using defaults from :{ENDC} {get_base_config_path()}", )
print(f"{OKGREEN}Using board config from :{ENDC} {get_board_config_path()}")
def main():
parser = build_arg_parser()
args = parser.parse_args()
main_config = open(get_main_config_path(), "r+")
parsed_main_config = parse_config(main_config)
main_config.close()
if args.list:
list_boards()
return
base_config = open(get_base_config_path(), "r")
board_config = open(get_board_config_path(), "r")
board_input = args.board
if not board_input:
parser.error("--board is required (or use --list)")
normalized = normalize_board_name(board_input)
if not normalized:
print(f"{WARNING}Unknown board '{board_input}'.")
suggestions = _suggest_boards(board_input)
if suggestions:
print("Did you mean: " + ", ".join(suggestions))
print("Use --list to see all boards.")
raise SystemExit(2)
parsed_base_config = parse_config(base_config)
parsed_board_config = parse_config(board_config)
if not os.path.isfile(get_base_config_path()):
raise SystemExit(f"Base defaults file not found: {get_base_config_path()}")
base_config.close()
board_config.close()
print(f"{OKGREEN}Switching configuration to board:{ENDC} {OKBLUE}{normalized}{ENDC}")
print(f"{OKGREEN}Using defaults from :{ENDC} {get_base_config_path()}")
print(f"{OKGREEN}Using board config from :{ENDC} {get_board_config_path(normalized)}")
new_board_config = {**parsed_base_config, **parsed_board_config}
new_board_config = handle_wifi_config(new_board_config, parsed_main_config)
with open(get_main_config_path(), "r+") as main_config:
parsed_main_config = parse_config(main_config)
if args.diff:
print("---"*5, f"{WARNING}DIFF{ENDC}", "---"*5)
diff = compute_diff(parsed_main_config, new_board_config)
if not diff:
print(f"{HEADER_COLOR}[DIFF]{ENDC} Nothing has changed between the base config and {OKBLUE}{args.board}{ENDC} config")
with open(get_base_config_path(), "r") as base_config, open(get_board_config_path(normalized), "r") as board_config:
parsed_base_config = parse_config(base_config)
parsed_board_config = parse_config(board_config)
new_board_config = {**parsed_base_config, **parsed_board_config}
new_board_config = handle_wifi_config(new_board_config, parsed_main_config, args)
if args.diff:
print("---"*5, f"{WARNING}DIFF{ENDC}", "---"*5)
diff = compute_diff(parsed_main_config, new_board_config)
if not diff:
print(f"{HEADER_COLOR}[DIFF]{ENDC} No changes between existing main config and {OKBLUE}{normalized}{ENDC}")
else:
print(f"{HEADER_COLOR}[DIFF]{ENDC} Keys differing (main -> new {OKBLUE}{normalized}{ENDC}):")
for key in sorted(diff):
print(f"{HEADER_COLOR}[DIFF]{ENDC} {key} : {diff[key]}")
print("---"*14)
if not args.dry_run:
print(f"{WARNING}Writing changes to main config file{ENDC}")
with open(get_main_config_path(), "w") as main_config:
for key, value in new_board_config.items():
if value:
main_config.write(f"{key}={value}\n")
else:
main_config.write(f"{key}\n")
else:
print(f"{HEADER_COLOR}[DIFF]{ENDC} The following keys have changed between the base config and {OKBLUE}{args.board}{ENDC} config:")
for key in diff:
print(f"{HEADER_COLOR}[DIFF]{ENDC} {key} : {diff[key]}")
print("---"*14)
print(f"{WARNING}[DRY-RUN]{ENDC} Skipping writing to files")
print(f"{OKGREEN}Done. ESP-IDF is setup to build for:{ENDC} {OKBLUE}{normalized}{ENDC}")
if not args.dry_run:
# the main idea is to always replace the main config with the base config
# and then add the board config on top of that, overriding where necessary.
# This way we can have known working defaults safe from accidental modifications by espidf
# with know working per-board config
# and a still modifiable sdkconfig for espidf
print(f"{WARNING}Writing changes to main config file{ENDC}")
with open(get_main_config_path(), "w") as main_config:
for key, value in new_board_config.items():
if value:
main_config.write(f"{key}={value}\n")
else:
main_config.write(f"{key}\n")
else:
print(f"{WARNING}[DRY-RUN]{ENDC} Skipping writing to files")
print(f"{OKGREEN}Done. ESP-IDF is setup to build for:{ENDC} {OKBLUE}{args.board}{ENDC}")
if __name__ == "__main__": # pragma: no cover
main()