mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 20:19:56 +00:00
Server roles (slice 7) - CommonCLI split into upstream's shape; region CLI moved into CommonCLI; ClientACL, RegionMap, TransportKeyStore and both server meshes in upstream shape over an Arduino compat shim (helpers/compat: Arduino.h, Stream/Print, Serial, SHA256, FILESYSTEM/File over Zephyr fs) - room server processAck port; `get acl` prints as upstream - room: default name Room-XXXX, display/input rotation, live radio UI refresh Event loops (slice 7b) - repeater and room server share server_main_common with a ServerRole descriptor; shared event bits in src/mesh_events.h - room server deadline-driven (idle wakes 2.2/s -> 0.08/s); companion loop() runs on housekeeping (closes the lazy contact-flush gap) - `get loop.wakes` in debug builds (CONFIG_ZEPHCORE_LOOP_WAKE_STATS) Companion transports and WiFi (slice 8) - upstream MultiSerialInterface: BLE, USB/UART and TCP are each a BaseSerialInterface, every connected transport gets every frame; CompanionSerial holds a lossless reply; shared frame_txq for BLE/TCP - SerialCompanionTransport and LinuxTCPTransport replaced by TcpCompanionTransport; ZephyrBLE split (gatt layout, DFU) - WiFi companion (upstream wifi.ssid/pwd/enabled CLI, TCP port 5000) on boards declaring `capabilities: wifi` in zephcore.yml: the PSRAM ESP32-S3 boards, plus Heltec V3 (140 contacts), Wireless Tracker (130) and both C6 boards (280); ESP32-S3 without PSRAM gets bounded WiFi buffers and a measured 72 KB heap; PSRAM boards' internal heap 72 KB - TCP queues: own depth (ZEPHCORE_TCP_QUEUE_SIZE=8), RX waits instead of dropping - RAM: companion ClientACL dropped; main/BT-TX/TCP stacks sized from measured peaks (-7 KB nRF companions, -9 KB ESP32 companions) - ESP32 WiFi stopped working under ~200 frames/s unicast. Fixed by: patches/zephyr/0017 never block the WiFi task in the RX callback; patches/zephyr/0018 lock shared_multi_heap (heap corruption with the WiFi heap in PSRAM); patches/modules/hal_espressif/0001 backport of hal_espressif af97fb02df (PSRAM realloc) Storage and prefs (slice 9) - prefs are now prefs.json through upstream's ConfigSerializer (ported verbatim), with upstream's key names for shared fields and ZephCore's own under "zc"; on first boot the binary new_prefs / repeater prefs is migrated and kept, so a downgrade still boots with the old settings - helpers/PrefsCodec: both legacy binary layouts (companion 272 B, server 311 B) as Zephyr-free codecs; sanitizeNodePrefs is the only validator - fix: server prefs could be replaced by a truncated file when a write failed; identity, prefs and channel writes go through one checked atomic replace (ZephyrFsUtil) - fix: a corrupt cad_auto byte turned adaptive CAD off instead of the default on - fix: a deliberate 300 s GPS duty on a repeater was rewritten to 48 h on every boot (that migration now runs on legacy files only) - one identity loader for all roles (IdentityFile); an unusable key file is always parked as .bad before regenerating - server store no longer stamps repeater defaults on room server and observer; companion first-boot volume policy moved into the store (adoptVolume) - ZephyrDataStore 1435 -> ~850 lines
236 lines
11 KiB
Python
236 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: MIT
|
|
"""ZephCore board manifests: the one list of boards and what we publish for them.
|
|
|
|
Every board directory boards/<platform>/<board>/ carries a zephcore.yml; each
|
|
native-Linux preset boards/linux_native/<preset>.conf carries a sibling
|
|
<preset>.zephcore.yml. Schema (all keys optional unless marked):
|
|
|
|
target: rak4631 # REQUIRED west -b string, with qualifiers
|
|
capabilities:
|
|
wifi: true # WiFi companion: RAM for WiFi + BLE together
|
|
light_sleep: true # DIO1 on an RTC-wake-capable GPIO AND
|
|
# validated on hardware (ESP32 repeaters)
|
|
release: # absent = not published (bring-up)
|
|
roles: [companion, repeater] # REQUIRED in release
|
|
variants: # extra published builds of this board
|
|
- id: noscreen # artifact infix
|
|
conf: no_display.conf # relative to the board dir
|
|
subtitle: No screen # catalog role-row subtitle
|
|
catalog: # absent = published but not in the catalog
|
|
device: RAK WisBlock / WisMesh (RAK 4631) # REQUIRED in catalog
|
|
maker: rak # REQUIRED in catalog, key of MAKERS
|
|
new: true # own tile instead of folding into MeshCore's
|
|
img: lora.svg | own_img: x.jpg
|
|
subtitle: 30 dBm
|
|
linux: # native-Linux presets only
|
|
host: arm | aarch64
|
|
cross_compile: /usr/bin/arm-linux-gnueabihf-
|
|
|
|
Readers:
|
|
- cmake/zephcore_board.cmake reads the capabilities with a line match (it
|
|
runs before Zephyr has set up Python), so `check` enforces the literal
|
|
two-space-indented ` <capability>: true|false` form.
|
|
- build.sh: board_manifest.py matrix <nrf|nrf54l|mg24|stm32wl|esp32|linux> [companions|repeaters]
|
|
- gen_provider_catalog.py imports load_boards().
|
|
- docs: board_manifest.py docs (rewrites the generated lists in docs/supported_boards.md)
|
|
|
|
`board_manifest.py check` validates every manifest; CI should run it.
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
import yaml
|
|
|
|
APP_DIR = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
|
BOARDS_DIR = os.path.join(APP_DIR, "boards")
|
|
|
|
PLATFORM_DIRS = {"nrf52840": "nrf52", "nrf54l": "nrf54l", "esp32": "esp32",
|
|
"mg24": "mg24", "stm32wl": "stm32wl"}
|
|
ROLES = ("companion", "repeater", "room_server", "observer")
|
|
KEYS = {"target", "capabilities", "release", "catalog", "linux"}
|
|
CAPS = {"light_sleep", "wifi"}
|
|
CATALOG_KEYS = {"device", "maker", "new", "img", "own_img", "subtitle"}
|
|
CAP_LINE = re.compile(r"^ (\w+): (true|false)\s*$", re.M)
|
|
|
|
|
|
class Board:
|
|
def __init__(self, name, platform, manifest_path, data, board_dir=None, conf=None):
|
|
self.name = name # board dir name or linux preset name
|
|
self.platform = platform # nrf52 | nrf54l | esp32 | mg24 | stm32wl | linux
|
|
self.manifest_path = manifest_path
|
|
self.board_dir = board_dir # None for linux presets
|
|
self.conf = conf # linux preset conf, relative to APP_DIR
|
|
self.target = data.get("target")
|
|
self.capabilities = data.get("capabilities") or {}
|
|
self.release = data.get("release")
|
|
self.catalog = data.get("catalog")
|
|
self.linux = data.get("linux") or {}
|
|
|
|
@property
|
|
def chip(self):
|
|
"""ESP32 chip name from the target (esp32, esp32s3, esp32c3, esp32c6)."""
|
|
m = re.search(r"(esp32[^/]*)", self.target or "")
|
|
return m.group(1) if m else None
|
|
|
|
@property
|
|
def stem(self):
|
|
"""Artifact filename stem, exactly as build.sh has always named files."""
|
|
if self.platform == "linux":
|
|
return f"zephcore_linux_{self.name}"
|
|
return self.target.replace("/", "-")
|
|
|
|
@property
|
|
def sysbuild(self):
|
|
"""--sysbuild / --no-sysbuild / None (west default) for release builds."""
|
|
if self.platform == "esp32":
|
|
return True if self.chip != "esp32" else None # classic ESP32: simple boot
|
|
if self.platform == "nrf54l":
|
|
return False
|
|
return None
|
|
|
|
|
|
def _load(path):
|
|
with open(path, encoding="utf-8") as f:
|
|
return yaml.safe_load(f) or {}
|
|
|
|
|
|
def load_boards():
|
|
boards = []
|
|
for plat_dir, platform in PLATFORM_DIRS.items():
|
|
root = os.path.join(BOARDS_DIR, plat_dir)
|
|
if not os.path.isdir(root):
|
|
continue
|
|
for name in sorted(os.listdir(root)):
|
|
bdir = os.path.join(root, name)
|
|
path = os.path.join(bdir, "zephcore.yml")
|
|
if os.path.isfile(path):
|
|
boards.append(Board(name, platform, path, _load(path), board_dir=bdir))
|
|
lroot = os.path.join(BOARDS_DIR, "linux_native")
|
|
if os.path.isdir(lroot):
|
|
for fn in sorted(os.listdir(lroot)):
|
|
if fn.endswith(".zephcore.yml"):
|
|
name = fn[: -len(".zephcore.yml")]
|
|
boards.append(Board(name, "linux", os.path.join(lroot, fn),
|
|
_load(os.path.join(lroot, fn)),
|
|
conf=f"boards/linux_native/{name}.conf"))
|
|
return boards
|
|
|
|
|
|
def check(boards, makers=None):
|
|
errors = []
|
|
for b in boards:
|
|
where = os.path.relpath(b.manifest_path, APP_DIR)
|
|
data = _load(b.manifest_path)
|
|
for k in set(data) - KEYS:
|
|
errors.append(f"{where}: unknown key '{k}'")
|
|
if not b.target:
|
|
errors.append(f"{where}: missing target")
|
|
elif b.platform != "linux" and not re.match(rf"^{re.escape(b.name)}(@|/|$)", b.target):
|
|
errors.append(f"{where}: target '{b.target}' does not name board '{b.name}'")
|
|
for k, v in b.capabilities.items():
|
|
if k not in CAPS:
|
|
errors.append(f"{where}: unknown capability '{k}'")
|
|
if not isinstance(v, bool):
|
|
errors.append(f"{where}: capability '{k}' must be true/false")
|
|
if b.capabilities:
|
|
text = open(b.manifest_path, encoding="utf-8").read()
|
|
block = re.search(r"^capabilities:\n((?: .*\n?)*)", text, re.M)
|
|
found = {m.group(1) for m in CAP_LINE.finditer(block.group(1))} if block else set()
|
|
if found != set(b.capabilities):
|
|
errors.append(f"{where}: write capabilities as two-space-indented "
|
|
f"' <name>: true|false' lines (CMake reads them by line)")
|
|
if b.release is not None:
|
|
roles = b.release.get("roles") or []
|
|
if not roles or any(r not in ROLES for r in roles):
|
|
errors.append(f"{where}: release.roles must be a non-empty subset of {ROLES}")
|
|
for v in b.release.get("variants") or []:
|
|
if not v.get("id") or not v.get("conf"):
|
|
errors.append(f"{where}: variant needs id and conf")
|
|
elif b.board_dir and not os.path.isfile(os.path.join(b.board_dir, v["conf"])):
|
|
errors.append(f"{where}: variant conf '{v['conf']}' not found")
|
|
if b.catalog is not None:
|
|
for k in set(b.catalog) - CATALOG_KEYS:
|
|
errors.append(f"{where}: unknown catalog key '{k}'")
|
|
if not b.catalog.get("device") or not b.catalog.get("maker"):
|
|
errors.append(f"{where}: catalog needs device and maker")
|
|
if makers is not None and b.catalog.get("maker") not in makers:
|
|
errors.append(f"{where}: catalog maker '{b.catalog.get('maker')}' not in MAKERS")
|
|
if b.release is None:
|
|
errors.append(f"{where}: catalog entry without a release")
|
|
if b.platform == "linux" and not {"host", "cross_compile"} <= set(b.linux):
|
|
errors.append(f"{where}: linux presets need linux.host and linux.cross_compile")
|
|
return errors
|
|
|
|
|
|
def matrix(boards, group, role_filter=None):
|
|
"""Release builds for one build.sh group: target|role|variant|confs|stem|sysbuild|host|cross."""
|
|
want = {"nrf": "nrf52"}.get(group, group)
|
|
if role_filter:
|
|
role_filter = {"companions": "companion", "repeaters": "repeater"}[role_filter]
|
|
rows = []
|
|
for b in boards:
|
|
if b.platform != want or b.release is None:
|
|
continue
|
|
sb = {True: "sysbuild", False: "no-sysbuild", None: ""}[b.sysbuild]
|
|
base_confs = [b.conf] if b.conf else []
|
|
builds = [("", base_confs)]
|
|
for v in b.release.get("variants") or []:
|
|
rel = os.path.relpath(os.path.join(b.board_dir, v["conf"]), APP_DIR).replace(os.sep, "/")
|
|
builds.append((v["id"], base_confs + [rel]))
|
|
for role in b.release["roles"]:
|
|
if role_filter and role != role_filter:
|
|
continue
|
|
for variant, confs in builds:
|
|
c = list(confs)
|
|
if role != "companion":
|
|
# Role conf before the variant conf, as build.sh always ordered it.
|
|
c = ([c[0]] if b.conf else []) + [f"boards/common/{role}.conf"] + c[1 if b.conf else 0:]
|
|
rows.append("|".join([b.target, role, variant, ";".join(c), b.stem, sb,
|
|
b.linux.get("host", ""), b.linux.get("cross_compile", "")]))
|
|
return rows
|
|
|
|
|
|
DOCS_GROUPS = [("nrf52", "nRF52840"), ("esp32", "ESP32"), ("stm32wl", "STM32WL"),
|
|
("mg24", "MG24"), ("nrf54l", "nRF54L")]
|
|
|
|
|
|
def docs(boards, path):
|
|
"""Rewrite each '<!-- boards:<platform> -->' ... '<!-- /boards -->' block."""
|
|
text = open(path, encoding="utf-8").read()
|
|
for platform, _ in DOCS_GROUPS:
|
|
group = [b for b in boards if b.platform == platform]
|
|
width = max((len(b.target) for b in group), default=0)
|
|
lines = [b.target if b.release is not None
|
|
else f"{b.target:<{width}} # source-only, no published firmware"
|
|
for b in group]
|
|
block = "<!-- boards:%s -->\n```\n%s\n```\n<!-- /boards -->" % (platform, "\n".join(lines))
|
|
text = re.sub(r"<!-- boards:%s -->.*?<!-- /boards -->" % platform, lambda _m: block,
|
|
text, flags=re.S)
|
|
open(path, "w", encoding="utf-8", newline="\n").write(text)
|
|
|
|
|
|
def main(argv):
|
|
boards = load_boards()
|
|
cmd = argv[1] if len(argv) > 1 else "check"
|
|
if cmd == "check":
|
|
errs = check(boards)
|
|
for e in errs:
|
|
print(e, file=sys.stderr)
|
|
print(f"{len(boards)} manifests, {len(errs)} errors")
|
|
return 1 if errs else 0
|
|
if cmd == "matrix":
|
|
for row in matrix(boards, argv[2], argv[3] if len(argv) > 3 else None):
|
|
print(row)
|
|
return 0
|
|
if cmd == "docs":
|
|
docs(boards, argv[2] if len(argv) > 2 else os.path.join(APP_DIR, "..", "docs", "supported_boards.md"))
|
|
return 0
|
|
print(__doc__)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|