mirror of
https://github.com/MrUnknownDE/OpenIris-ESPIDF.git
synced 2026-04-09 09:53:51 +02:00
332 lines
12 KiB
Python
332 lines
12 KiB
Python
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"
|
|
|
|
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]
|
|
|
|
|
|
def get_boards_root() -> str:
|
|
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.
|
|
|
|
Naming strategy:
|
|
- Relative path from boards/ to file with path separators replaced by '_'.
|
|
- If the last two path segments are identical (e.g. project_babble/project_babble) collapse to a single segment.
|
|
- For facefocusvr eye boards we keep eye_L / eye_R suffix to distinguish configs even though WHO_AM_I is same.
|
|
"""
|
|
boards_dir = get_boards_root()
|
|
mapping: Dict[str, str] = {}
|
|
if not os.path.isdir(boards_dir):
|
|
return mapping
|
|
for root, _dirs, files in os.walk(boards_dir):
|
|
for f in files:
|
|
if f == SDKCONFIG_DEFAULTS_FILENAME:
|
|
continue
|
|
rel_path = os.path.relpath(os.path.join(root, f), boards_dir)
|
|
parts = rel_path.split(os.sep)
|
|
if len(parts) >= 2 and parts[-1] == parts[-2]: # collapse duplicate tail
|
|
parts = parts[:-1]
|
|
board_key = "_".join(parts)
|
|
mapping[board_key] = os.path.join(root, f)
|
|
return mapping
|
|
|
|
|
|
BOARD_CONFIGS = enumerate_board_configs()
|
|
|
|
|
|
def build_arg_parser() -> argparse.ArgumentParser:
|
|
p = argparse.ArgumentParser()
|
|
p.add_argument(
|
|
"-b",
|
|
"--board",
|
|
help="Board name (run with --list to see options). Flexible: accepts path-like or partial if unique.",
|
|
)
|
|
p.add_argument(
|
|
"--list", help="List discovered boards and exit", action="store_true"
|
|
)
|
|
p.add_argument("--dry-run", help="Dry run, won't modify files", action="store_true")
|
|
p.add_argument(
|
|
"--diff",
|
|
help="Show the difference between base config and selected board",
|
|
action="store_true",
|
|
)
|
|
p.add_argument("--ssid", help="Set the WiFi SSID", type=str, default="")
|
|
p.add_argument("--password", help="Set the WiFi password", type=str, default="")
|
|
p.add_argument("--clear-wifi", help="Clear WiFi credentials", action="store_true")
|
|
return p
|
|
|
|
|
|
def list_boards():
|
|
print("Discovered boards:")
|
|
width = max((len(k) for k in BOARD_CONFIGS), default=0)
|
|
for name, path in sorted(BOARD_CONFIGS.items()):
|
|
print(f" {name.ljust(width)} -> {os.path.relpath(path, get_root_path())}")
|
|
|
|
|
|
def _suggest_boards(partial: str) -> List[str]:
|
|
if not partial:
|
|
return []
|
|
partial_low = partial.lower()
|
|
contains = [b for b in BOARD_CONFIGS if partial_low in b.lower()]
|
|
if contains:
|
|
return contains[:10]
|
|
# fallback to fuzzy matching using difflib
|
|
choices = list(BOARD_CONFIGS.keys())
|
|
return difflib.get_close_matches(partial, choices, n=5, cutoff=0.4)
|
|
|
|
|
|
def normalize_board_name(raw: Optional[str]) -> Optional[str]:
|
|
if raw is None:
|
|
return None
|
|
candidate = raw.strip()
|
|
if not candidate:
|
|
return None
|
|
candidate = candidate.replace("\\", "/").rstrip("/")
|
|
# strip leading folders like tools/, boards/
|
|
parts = [
|
|
p
|
|
for p in candidate.split("/")
|
|
if p not in (".", "") and p not in ("tools", "boards")
|
|
]
|
|
if parts:
|
|
candidate = parts[-1] if len(parts) == 1 else "_".join(parts)
|
|
candidate = candidate.replace("-", "_")
|
|
# exact match
|
|
if candidate in BOARD_CONFIGS:
|
|
return candidate
|
|
# try ending match
|
|
endings = [b for b in BOARD_CONFIGS if b.endswith(candidate)]
|
|
if len(endings) == 1:
|
|
return endings[0]
|
|
if len(endings) > 1:
|
|
print(f"Ambiguous board '{raw}'. Could be: {', '.join(endings)}")
|
|
return None
|
|
# attempt case-insensitive
|
|
lower_map = {b.lower(): b for b in BOARD_CONFIGS}
|
|
if candidate.lower() in lower_map:
|
|
return lower_map[candidate.lower()]
|
|
return None
|
|
|
|
|
|
def get_main_config_path() -> str:
|
|
return os.path.join(get_root_path(), "sdkconfig")
|
|
|
|
|
|
def get_board_config_path(board_key: str) -> str:
|
|
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)
|
|
|
|
|
|
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
|
|
|
|
|
|
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.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
|
|
|
|
|
|
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()
|
|
|
|
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)
|
|
|
|
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)}"
|
|
)
|
|
|
|
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)
|
|
|
|
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"{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()
|