118 lines
4.6 KiB
Python
118 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate src/helpers/ota/OtaTargets.h - the target_id -> env-name table for OTA-capable builds.
|
|
|
|
A target_id is sha2-256:4(pio_env_name); it travels in beacons / the .mota manifest as 4 bytes. This
|
|
table lets a node (and motatool) print the human-readable env name for a target seen over the air, WITHOUT
|
|
ever transmitting the string. The set is the union of PlatformIO environments whose resolved build flags
|
|
enable LoRa OTA and qualified release-only aliases composed by build.sh. Keeping those aliases in the
|
|
table prevents a valid release target from being displayed as unknown merely because it is not a literal
|
|
``[env:...]`` section.
|
|
|
|
Run: ./meshcore/bin/python tools/mota/gen_targets.py # resolves via `pio project config`
|
|
./meshcore/bin/python tools/mota/gen_targets.py cfg.json # or reuse a cached config dump (faster)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import motalib as ml
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
OUT = ROOT / "src" / "helpers" / "ota" / "OtaTargets.h"
|
|
INTERNAL_BOOTLOADER_TARGETS = ROOT / "tools" / "mota" / "nrf52_internal_bootloader_targets.txt"
|
|
|
|
|
|
def resolved_config():
|
|
if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]):
|
|
return json.load(open(sys.argv[1]))
|
|
pio = os.environ.get("PIO", "pio")
|
|
out = subprocess.run([pio, "project", "config", "--json-output"],
|
|
cwd=ROOT, capture_output=True, text=True, check=True).stdout
|
|
return json.loads(out)
|
|
|
|
|
|
def ota_envs(cfg):
|
|
envs = []
|
|
for section, opts in cfg:
|
|
if not section.startswith("env:"):
|
|
continue
|
|
for k, v in opts:
|
|
if k == "build_flags":
|
|
flags = " ".join(v) if isinstance(v, list) else str(v)
|
|
if "ENABLE_OTA" in flags and "DISABLE_LORA_OTA" not in flags:
|
|
envs.append(section[4:])
|
|
break
|
|
return sorted(set(envs))
|
|
|
|
|
|
def release_aliases():
|
|
"""Return qualified build.sh aliases that carry their own OTA target ID."""
|
|
return sorted({
|
|
line.strip()
|
|
for line in INTERNAL_BOOTLOADER_TARGETS.read_text(encoding="utf-8").splitlines()
|
|
if line.strip() and not line.lstrip().startswith("#")
|
|
})
|
|
|
|
|
|
def main():
|
|
envs = sorted(set(ota_envs(resolved_config())) | set(release_aliases()))
|
|
rows = [(ml.target_id_for_env(e), e) for e in envs]
|
|
|
|
seen = {}
|
|
for tid, e in rows: # sha2-256:4 over all names - verify no collisions
|
|
if tid in seen and seen[tid] != e:
|
|
raise RuntimeError(
|
|
f"target_id collision 0x{tid:08x}: {seen[tid]!r} vs {e!r}"
|
|
)
|
|
seen[tid] = e
|
|
rows.sort(key=lambda r: r[1].lower()) # by env name (readable, deterministic)
|
|
|
|
out = [
|
|
"#pragma once",
|
|
"#include <stdint.h>",
|
|
"",
|
|
"// AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand.",
|
|
f"// {len(rows)} OTA-capable build targets. Maps target_id (= sha2-256:4 of the target name, LE uint32)",
|
|
"// to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT",
|
|
"// transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes.",
|
|
"// Size-constrained receivers can set OTA_TARGET_NAME_TABLE=0. They still match targets by ID;",
|
|
"// OTA_LOCAL_TARGET_ID + OTA_LOCAL_TARGET_NAME optionally retain the local human-readable name.",
|
|
"",
|
|
"namespace mesh { namespace ota {",
|
|
"",
|
|
"#ifndef OTA_TARGET_NAME_TABLE",
|
|
" #define OTA_TARGET_NAME_TABLE 1",
|
|
"#endif",
|
|
"",
|
|
"// target_id -> env name, or nullptr if unknown. Linear scan; lookups are rare.",
|
|
"inline const char* ota_target_env_name(uint32_t target_id) {",
|
|
"#if OTA_TARGET_NAME_TABLE",
|
|
" static const struct { uint32_t id; const char* env; } T[] = {",
|
|
]
|
|
out += [f' {{ 0x{tid:08x}, "{e}" }},' for tid, e in rows]
|
|
out += [
|
|
" };",
|
|
" for (unsigned i = 0; i < sizeof(T) / sizeof(T[0]); i++)",
|
|
" if (T[i].id == target_id) return T[i].env;",
|
|
"#elif defined(OTA_LOCAL_TARGET_ID) && defined(OTA_LOCAL_TARGET_NAME)",
|
|
" if (target_id == (uint32_t)(OTA_LOCAL_TARGET_ID)) return OTA_LOCAL_TARGET_NAME;",
|
|
"#else",
|
|
" (void)target_id;",
|
|
"#endif",
|
|
" return nullptr;",
|
|
"}",
|
|
"",
|
|
"} } // namespace mesh::ota",
|
|
]
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
OUT.write_text("\n".join(out) + "\n")
|
|
print(f"wrote {OUT} ({len(rows)} OTA targets)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|