from __future__ import annotations import json import os from pathlib import Path from typing import Any, Dict, List def _safe_int(v: Any, default: int) -> int: try: return int(v) except Exception: return int(default) def _safe_float(v: Any, default: float) -> float: try: return float(v) except Exception: return float(default) def _resolve_path(base_dir: Path, value: str | None) -> Path: raw = (value or "").strip() p = Path(raw).expanduser() if p.is_absolute(): return p return (base_dir / p).resolve() def run_startup_self_check(cfg: Dict[str, Any], base_dir: Path | None = None) -> Dict[str, Any]: """ Validate runtime-critical config and paths. Returns a report dict suitable for GUI popup / worker status. """ if base_dir is None: base_dir = Path(__file__).resolve().parent base = Path(base_dir).resolve() errors: List[str] = [] warnings: List[str] = [] info: List[str] = [] app_cfg = cfg.get("app", {}) or {} livox_cfg = cfg.get("livox", {}) or {} map_cfg = cfg.get("map", {}) or {} loc_cfg = cfg.get("localization", {}) or {} nav_cfg = cfg.get("navigation_export", {}) or {} nav_rt_cfg = cfg.get("navigation_runtime", {}) or {} loop_cfg = cfg.get("loop_closure", {}) or {} autosave_cfg = cfg.get("autosave", {}) or {} maps_dir = _resolve_path(base, str(app_cfg.get("maps_dir", "DataMap"))) livox_file = _resolve_path(base, str(livox_cfg.get("config_file", "mid360_config.json"))) try: maps_dir.mkdir(parents=True, exist_ok=True) except Exception as e: errors.append(f"maps_dir is not creatable: {maps_dir} ({e})") else: if not maps_dir.is_dir(): errors.append(f"maps_dir is not a directory: {maps_dir}") else: if not maps_dir.exists(): errors.append(f"maps_dir missing: {maps_dir}") if not os.access(maps_dir, os.W_OK): warnings.append(f"maps_dir is not writable: {maps_dir}") info.append(f"maps_dir: {maps_dir}") if not livox_file.exists(): errors.append(f"livox.config_file missing: {livox_file}") else: info.append(f"livox config: {livox_file.name}") try: livox_data = json.loads(livox_file.read_text()) lidar_cfgs = livox_data.get("lidar_configs", []) or [] ext = {} if lidar_cfgs and isinstance(lidar_cfgs[0], dict): ext = lidar_cfgs[0].get("extrinsic_parameter", {}) or {} ext_vals = [ _safe_float(ext.get("roll", 0.0), 0.0), _safe_float(ext.get("pitch", 0.0), 0.0), _safe_float(ext.get("yaw", 0.0), 0.0), _safe_float(ext.get("x", 0.0), 0.0), _safe_float(ext.get("y", 0.0), 0.0), _safe_float(ext.get("z", 0.0), 0.0), ] if all(abs(v) <= 1e-6 for v in ext_vals): warnings.append( "MID360 extrinsics are all zero in mid360_config.json; " "verify G1 upside-down mounting correction before trusting localization." ) else: info.append( "MID360 extrinsics: " f"rpy=({ext_vals[0]:.2f}, {ext_vals[1]:.2f}, {ext_vals[2]:.2f}) " f"xyz=({ext_vals[3]:.3f}, {ext_vals[4]:.3f}, {ext_vals[5]:.3f})" ) except Exception as e: warnings.append(f"Could not parse MID360 extrinsics from {livox_file.name}: {e}") min_map = _safe_int(map_cfg.get("min_points_to_save", 0), 0) min_loc = _safe_int(loc_cfg.get("min_points_for_localize", 0), 0) min_nav = _safe_int(nav_cfg.get("min_points", 0), 0) if min_map <= 0 or min_loc <= 0 or min_nav <= 0: errors.append("min points must be > 0 for map/localization/navigation.") if not (min_map == min_loc == min_nav): warnings.append( f"min points mismatch (map/localize/nav): {min_map}/{min_loc}/{min_nav}. " "This can confuse export/localization behavior." ) else: info.append(f"min points unified: {min_map}") z_min = _safe_float(nav_cfg.get("z_min_m", -0.4), -0.4) z_max = _safe_float(nav_cfg.get("z_max_m", 1.2), 1.2) if z_min >= z_max: errors.append(f"navigation_export z range invalid: z_min({z_min}) >= z_max({z_max})") rt_z_min = _safe_float(nav_rt_cfg.get("z_min_m", z_min), z_min) rt_z_max = _safe_float(nav_rt_cfg.get("z_max_m", z_max), z_max) if rt_z_min >= rt_z_max: errors.append(f"navigation_runtime z range invalid: z_min({rt_z_min}) >= z_max({rt_z_max})") loc_use_p2pl = bool(loc_cfg.get("use_point_to_plane", False)) loc_unsafe = bool(loc_cfg.get("allow_point_to_plane_unsafe", False)) if loc_use_p2pl and not loc_unsafe: warnings.append( "localization.use_point_to_plane=true but allow_point_to_plane_unsafe=false. " "Worker will use point-to-point for stability." ) if bool(loop_cfg.get("enabled", False)): max_corr_t = _safe_float(loop_cfg.get("max_correction_translation_m", 2.0), 2.0) max_corr_r = _safe_float(loop_cfg.get("max_correction_rotation_deg", 20.0), 20.0) if max_corr_t <= 0.0 or max_corr_r <= 0.0: warnings.append("loop_closure enabled but safe correction limits are non-positive.") else: info.append(f"loop safe limits: {max_corr_t:.2f}m / {max_corr_r:.1f}deg") if bool(autosave_cfg.get("enabled", False)): iv = _safe_float(autosave_cfg.get("interval_sec", 90.0), 90.0) if iv < 5.0: warnings.append(f"autosave interval is very low ({iv:.1f}s).") status = "ok" if errors: status = "error" elif warnings: status = "warn" return { "status": status, "errors": errors, "warnings": warnings, "info": info, }