"""Consolidation evidence for new applications; historical OTA names stay exact.""" import json import re import sys from optional_display_profiles import resolved_defines, spec_for_defines SPECS = ( dict(key="combinedCompanion", capability="companion.picow.combined", marker="meshcore.capability.companion.picow.combined.v1", platform="RP2040_PLATFORM", board="rpipicow", target=r"PicoW_companion_radio", defines={"PICOW_COMBINED_COMPANION": "1", "ENABLE_USB_INTERFACE": "1", "ENABLE_WIFI_INTERFACE": "1", "PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH": "1"}), dict(key="oledExpansionCompatible", capability="hardware.heltec_v4.oled_kit", marker="meshcore.capability.hardware.heltec_v4.oled_kit.v1", platform="ESP32_PLATFORM", board="heltec_v4", target=r"heltec_v4_(?:repeater|room_server)(?:_observer_mqtt)?", defines={"HELTEC_V4_OLED_KIT_COMPAT": "1", "HELTEC_LORA_V4_OLED": "1", "DISPLAY_CLASS": "SSD1306Display", "ENV_PIN_SDA": "4", "ENV_PIN_SCL": "3"}), dict(key="displayRotation", capability="display.rotation.runtime", marker="meshcore.capability.display.rotation.heltec_r8.v1", platform="ESP32_PLATFORM", board="heltec_v4_r8", target=r"heltec_v4_r8_tft_(?:(?:repeater|room_server)(?:_observer_mqtt)?|sensor|companion_radio_full)", defines={"HELTEC_TFT_RUNTIME_ROTATION": "1", "HELTEC_V4_R8_TFT": "1", "DISPLAY_CLASS": "ST7789LCDDisplay"}), dict(key="tftExpansionCompatible", capability="hardware.heltec_v4.tft_kit", marker="meshcore.capability.hardware.heltec_v4.tft_kit.v1", platform="ESP32_PLATFORM", board="heltec_v4", target=r"heltec_v4_tft_companion_radio_full_femon", defines={"HELTEC_V4_TFT_KIT_COMPAT": "1", "HELTEC_LORA_V4_TFT": "1", "DISPLAY_CLASS": "ST7789LCDDisplay"}), ) def specs_for_config(target, env): defines = resolved_defines(env.get("build_flags", [])) return [spec for spec in SPECS if env.get("board") == spec["board"] and re.fullmatch(spec["target"], target, re.I) and all(defines.get(key) == value for key, value in spec["defines"].items())] def proven(manifest, spec): return (manifest.get("verified") is True and manifest.get("platform") == spec["platform"] and re.fullmatch(spec["target"], manifest.get("target", ""), re.I) and spec["capability"] in manifest.get("capabilities", []) and any(row.get("capability") == spec["capability"] and row.get("present") is True and row.get("source") == "packaged application" and row.get("evidence") == spec["marker"] for row in manifest.get("verification", []))) def runtime_metadata(manifest, env, release_source): result = {} for spec in SPECS: if spec["capability"] not in manifest.get("capabilities", []): continue if spec not in specs_for_config(manifest["target"], env) or not proven(manifest, spec): raise ValueError(manifest["target"] + ": consolidation requires exact configuration and packaged application proof") source = manifest.get("source_commit", release_source) if not isinstance(source, str) or not re.fullmatch(r"[0-9a-f]{40}", source) or source != release_source: raise ValueError("Consolidation metadata requires the exact release source") result[spec["key"]] = True result[spec["key"] + "Source"] = source return result def replacements(target): """Known functionality successors, never aliases for deployed update IDs.""" if re.fullmatch(r"PicoW_companion_radio_(?:usb|ble|wifi)", target, re.I): return [("PicoW_companion_radio", "combinedCompanion")] if re.fullmatch(r"heltec_v4(?:_3)?_expansionkit_tft_companion_radio_(?:usb|ble|wifi|full)(?:_ps)?(?:_fem(?:on|off))?", target, re.I): return [("heltec_v4_tft_companion_radio_full_femon", "tftExpansionCompatible")] match = re.fullmatch(r"muzi_base_(uno|duo)_superio_companion_radio_(?:usb|ble|full)", target, re.I) if match: return [("muzi_base_" + match[1].lower() + "_companion_radio_full", "muziAccessories")] if re.fullmatch(r"LilyGo_T-Echo-Lite_non_shell_companion_radio_(?:usb|ble|full)", target, re.I): return [("LilyGo_T-Echo-Lite_companion_radio_full", "techoLiteDisplay")] match = re.fullmatch(r"heltec_v4_expansionkit_(repeater|room_server)(_observer_mqtt)?", target, re.I) if match: base = "heltec_v4_" + match[1].lower() return [(base + (match[2] or ""), "oledExpansionCompatible"), (base, "oledExpansionCompatible")] match = re.fullmatch(r"heltec_v4_r8_tft_portrait_(repeater|room_server)_observer_mqtt", target, re.I) if match: base = "heltec_v4_r8_tft_" + match[1].lower() return [(base + "_observer_mqtt", "displayRotation"), (base, "displayRotation")] match = re.fullmatch(r"heltec_rc32_without_display_(repeater|room_server|sensor|companion_radio_(?:usb|ble|wifi|full))(?:_observer_mqtt)?", target, re.I) if match: return [(target.lower().replace("_without_display_", "_", 1), "displayAuto")] return [] def configuration_replacement(target, envs): def environment(name): # Full nRF52 targets can be generated by build.sh from their exact # USB or BLE base. Never substitute another physical radio variant. for choice in (name, name.replace("_companion_radio_full", "_companion_radio_usb"), name.replace("_companion_radio_full", "_companion_radio_ble")): if "env:" + choice in envs: return envs["env:" + choice] return None for candidate, key in replacements(target): env = environment(candidate) source_env = environment(target) if not env or not source_env or env.get("board") != source_env.get("board"): continue if key in ("displayAuto", "muziAccessories", "techoLiteDisplay"): spec = spec_for_defines(resolved_defines(env.get("build_flags", [])), env.get("board")) expected = {"displayAuto": "rc32", "muziAccessories": "muzi_superio", "techoLiteDisplay": "techo_lite"}[key] qualified = spec and spec["marker"] == expected and re.fullmatch(spec["targets"], candidate.lower()) else: qualified = any(spec["key"] == key for spec in specs_for_config(candidate, env)) if qualified: return candidate return None def omit_redundant_records(records): """Remove resumed alternatives only with an actual qualified successor.""" def same_policy(left, right): return (left.get("build_profile") == right.get("build_profile") and sorted(x for x in left.get("capabilities", []) if x.startswith("sensor.profile.")) == sorted(x for x in right.get("capabilities", []) if x.startswith("sensor.profile."))) def qualified(manifest, key): if key in ("displayAuto", "muziAccessories", "techoLiteDisplay"): from optional_display_profiles import marker, SPECS as displays expected = {"displayAuto": "rc32", "muziAccessories": "muzi_superio", "techoLiteDisplay": "techo_lite"}[key] spec = next((s for s in displays if s["marker"] == expected and re.fullmatch(s["targets"], manifest.get("target", "").lower())), None) if spec is None: return False transport = key == "displayAuto" or ( {"companion.usb", "companion.bluetooth"} <= set(manifest.get("capabilities", [])) and {"companion.usb", "companion.bluetooth"} <= { row.get("capability") for row in manifest.get("verification", []) if row.get("present") is True and row.get("source") == "linked image" and row.get("evidence")}) return (transport and manifest.get("verified") is True and manifest.get("platform") == spec.get("platform", "ESP32_PLATFORM") and "display.autodetect" in manifest.get("capabilities", []) and any(row.get("capability") == "display.autodetect" and row.get("present") is True and row.get("source") == "packaged application" and row.get("evidence") == marker(spec) for row in manifest.get("verification", []))) return any(s["key"] == key and proven(manifest, s) for s in SPECS) result = [] for record in records: manifest = record["manifest"] alternatives = replacements(manifest["target"]) if not any(candidate["manifest"]["target"].lower() == target.lower() and (same_policy(manifest, candidate["manifest"]) or key in ("muziAccessories", "techoLiteDisplay") and candidate["manifest"].get("build_profile") == "full" and manifest["target"].lower().endswith(("_usb", "_ble"))) and qualified(candidate["manifest"], key) for target, key in alternatives for candidate in records): result.append(record) return result if __name__ == "__main__": envs = {name: dict(options) for name, options in json.load(sys.stdin)} mode, target = sys.argv[1:] if mode == "replacement": result = configuration_replacement(target, envs) if result is None: sys.exit(1) print(result) elif mode == "expectations": for spec in specs_for_config(target, envs.get("env:" + target, {})): print(spec["capability"] + "=" + spec["marker"]) else: raise SystemExit("Unknown consolidation operation")