#!/usr/bin/env python3 # Author: cirakg & claude ai # Summary: Check each manifest product: a watch with API 5.0+, 512 KB or more, and the watchdog the job runner assumes. """Check the manifest's product list against the downloaded device files. The target rule (README section 14, decision D4): a product belongs in manifest.xml when it is in Garmin's "Watches/Wearables" group, every firmware variant supports Connect IQ API 5.0.0 or newer, and watch apps get at least 512 KB of memory. The data comes from each device's compiler.json, which `make sdk` downloads for exactly the products in the manifest. The app's job runner (source/util/Jobs.mc) also assumes a watchdog limit from the memory size: watches with more than 600,000 bytes for watch apps run two job steps per event, which needs 240,000 bytecodes per event (simulator.json's watchdogCount); the others run one step and need at least 120,000. Usage: gen_products.py --check [--manifest manifest.xml] [--devices DIR] Exit: 0 every product qualifies, 1 some don't, 2 device files missing. Finding devices Garmin releases later needs the device catalogue (a login); re-run `make sdk` after adding a new product to the manifest. """ from __future__ import annotations import argparse import json import sys import xml.etree.ElementTree as ET from dataclasses import dataclass from pathlib import Path EXIT_OK, EXIT_UNQUALIFIED, EXIT_MISSING = 0, 1, 2 IQ_NS = "{http://www.garmin.com/xml/connectiq}" MIN_API = (5, 0, 0) MIN_WATCH_APP_MEMORY = 512 * 1024 # Jobs.LARGE_WATCH_MEMORY and the watchdog each memory class needs. LARGE_WATCH_MEMORY = 600_000 WATCHDOG_SMALL, WATCHDOG_LARGE = 120_000, 240_000 WATCH_GROUP = "Watches/Wearables" DEFAULT_DEVICES = Path.home() / ".Garmin" / "ConnectIQ" / "Devices" @dataclass(frozen=True) class Device: """What the target rule needs from one compiler.json.""" product: str name: str group: str min_api: tuple[int, ...] watch_app_memory: int watchdog: int def problems(self) -> list[str]: reasons = [] if self.group != WATCH_GROUP: reasons.append(f"group is '{self.group}', not '{WATCH_GROUP}'") if self.min_api < MIN_API: reasons.append(f"API {'.'.join(map(str, self.min_api))} is below 5.0.0") if self.watch_app_memory < MIN_WATCH_APP_MEMORY: reasons.append(f"watch apps get {self.watch_app_memory // 1024} KB, below 512 KB") needed = WATCHDOG_LARGE if self.watch_app_memory > LARGE_WATCH_MEMORY else WATCHDOG_SMALL if self.watchdog < needed: reasons.append( f"watchdog allows {self.watchdog} bytecodes per event; the job runner needs {needed} at" f" {self.watch_app_memory // 1024} KB (change Jobs.stepsPerTick)" ) return reasons def parse_version(text: str) -> tuple[int, ...]: return tuple(int(part) for part in text.split(".")) def load_device(devices: Path, product: str) -> Device: compiler = json.loads((devices / product / "compiler.json").read_text(encoding="utf-8")) memory = {app["type"]: int(app["memoryLimit"]) for app in compiler["appTypes"]} simulator = devices / product / "simulator.json" watchdog = 0 if simulator.exists(): watchdog = int(json.loads(simulator.read_text(encoding="utf-8")).get("watchdogCount", 0)) return Device( product=product, name=str(compiler.get("displayName", product)), group=str(compiler.get("webDocDeviceGroup", "")), min_api=min(parse_version(p["connectIQVersion"]) for p in compiler["partNumbers"]), watch_app_memory=memory.get("watchApp", 0), watchdog=watchdog, ) def manifest_products(manifest: Path) -> list[str]: root = ET.parse(manifest).getroot() return [p.attrib["id"] for p in root.iter(f"{IQ_NS}product")] def check(manifest: Path, devices: Path) -> int: products = manifest_products(manifest) worst = EXIT_OK for product in products: if not (devices / product / "compiler.json").exists(): print(f"{product}: no device files (run 'make sdk')") worst = max(worst, EXIT_MISSING) continue device = load_device(devices, product) for reason in device.problems(): print(f"{product} ({device.name}): {reason}") worst = max(worst, EXIT_UNQUALIFIED) print(f"gen_products: {len(products)} products checked") return worst def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Check the manifest's products against the target rule.") parser.add_argument("--check", action="store_true", required=True) parser.add_argument("--manifest", type=Path, default=Path("manifest.xml")) parser.add_argument("--devices", type=Path, default=DEFAULT_DEVICES) args = parser.parse_args(argv) return check(args.manifest, args.devices) if __name__ == "__main__": sys.exit(main())