"""Role-specific feature-preserving flash trims for nRF52 firmware. Decide from the effective source filter as well as the environment/OTA alias and flags: build.sh can synthesize profiles over another PlatformIO recipe. Unknown roles fail closed. No shared framework or downloaded library is edited. CDC, USB host, sensor/GPS support, BLE P-256, and stored Ed25519 keys remain. Infrastructure uses RAM-built compact Ed25519 and display-font trims. Companion uses a 960-byte Ed25519 flash table to keep signing stack bounded, and retains all UI fonts and the Reader. The nRF52 base keeps an 8 KiB loop stack for crypto and the existing OTA path. """ import fnmatch import hashlib import importlib.util import re import shlex from pathlib import Path ROLE_SOURCES = ( "../examples/simple_repeater/main.cpp", "../examples/simple_room_server/main.cpp", "../examples/simple_sensor/main.cpp", "../examples/kiss_modem/main.cpp", ) COMPANION_SOURCES = ( "../examples/companion_radio/MyMesh.cpp", "../examples/simple_secure_chat/main.cpp", ) TRIM_DEFINES = { "MESH_NRF52_FLASH_TRIM": 1, "ED25519_COMPACT_BASE": 1, "ED25519_COMPACT_SHA512": 1, # No infrastructure role instantiates these USB *device* classes. Do not # touch CDC (including dual CDC logging), DFU, or the MAX3421 USB host. "CFG_TUD_MSC": 0, "CFG_TUD_HID": 0, "CFG_TUD_MIDI": 0, "CFG_TUD_VENDOR": 0, "CFG_TUD_VIDEO": 0, "CFG_TUD_VIDEO_STREAMING": 0, # MeshCore uses UBX navigation, not SparkFun's automatic NMEA cache. This # does not disable navigation, the UBX parser, or MeshCore NMEA logging. "SFE_UBLOX_DISABLE_AUTO_NMEA": 1, "SFE_UBLOX_REDUCED_PROG_MEM": 1, # SSD1306Display clears the vendor splash before its first panel transfer. # Keep MeshCore's own startup logo; only omit the invisible library bitmap. "SSD1306_NO_SPLASH": 1, } CC310_ARCHIVE_SHA256 = "97dc648d44520e47252f62c402a71f976e36d7b3aef99235bbdc4de79a928577" CC310_ARCHIVE = "src/cortex-m4/fpv4-sp-d16-hard/libnrf_cc310_0.9.13-no-interrupts.a" # Companion retains all UI fonts and uses a small Ed25519 flash table instead # of the infrastructure implementation's extra 1.8 KiB of signing stack. # These options remove library code/data that its application never uses; # CDC, DFU, BLE pairing, USB host and UBX navigation remain available. COMPANION_TRIM_DEFINES = { name: value for name, value in TRIM_DEFINES.items() if name not in ("MESH_NRF52_FLASH_TRIM", "ED25519_COMPACT_BASE", "ED25519_COMPACT_SHA512") } COMPANION_TRIM_DEFINES["MESH_NRF52_COMPANION_TRIM"] = 1 COMPANION_TRIM_DEFINES["ED25519_SMALL_BASE"] = 1 COMPANION_TRIM_DEFINES["ED25519_COMPACT_SHA512"] = 1 COMPANION_TRIM_DEFINES["ED25519_COMPACT_SCALAR"] = 1 def flag_defines(flags=(), cppdefines=()): """Read effective defines without importing PlatformIO/SCons for tests.""" values = {} for define in cppdefines: if isinstance(define, (tuple, list)): values[str(define[0])] = str(define[1]) if len(define) > 1 else "1" else: values[str(define)] = "1" text = flags if isinstance(flags, str) else " ".join(map(str, flags)) words = shlex.split(text) index = 0 while index < len(words): word = words[index] if word in ("-D", "-U"): index += 1 word += words[index] if index < len(words) else "" if word.startswith("-D"): name, _, value = word[2:].partition("=") values[name] = value if "=" in word else "1" elif word.startswith("-U"): values[word[2:]] = None index += 1 return values def _enabled(value): return value is not None and str(value).strip("\"'() ") not in ("", "0") def source_selected(src_filter, source): """Resolve ordered includes/excludes for the explicit outside-src roles.""" text = src_filter if isinstance(src_filter, str) else " ".join(map(str, src_filter)) selected = False for sign, pattern in re.findall(r"([+-])<([^>]+)>", text.replace("\\", "/")): # +<*.cpp> and +<*> scan PROJECT_SRC_DIR, not ../examples. They do # not imply that any example role is part of the image. if source.startswith("../examples/") and "examples/" not in pattern: continue pattern = pattern.rstrip("/") if fnmatch.fnmatchcase(source, pattern) or source.startswith(pattern + "/"): selected = sign == "+" return selected def trim_policy(platform, mcu, env_name, src_filter, flags=(), cppdefines=(), ota_name_mode="front-coded"): """Return defaults for a qualified non-Companion role, otherwise nothing. The name policy is explicit so the profile can use a different verified representation later without changing role selection or hardware trims. """ if platform != "nordicnrf52" or not str(mcu).lower().startswith("nrf52"): return {} values = flag_defines(flags, cppdefines) names = (str(env_name), str(values.get("OTA_VARIANT", ""))) if any(any(tag in name.lower() for tag in ( "companion", "comp_radio", "terminal_chat", "term_chat", "terminal-chat", "secure_chat")) for name in names): return {} if any(_enabled(value) for name, value in values.items() if name.startswith("COMPANION_")): return {} if any(source_selected(src_filter, source) for source in COMPANION_SOURCES): return {} if not any(source_selected(src_filter, source) for source in ROLE_SOURCES): return {} if "MESH_NRF52_FLASH_TRIM" in values and not _enabled(values["MESH_NRF52_FLASH_TRIM"]): return {} if ota_name_mode != "front-coded": raise ValueError("unsupported nRF52 OTA name trim policy: " + ota_name_mode) definitions = dict(TRIM_DEFINES, OTA_TARGET_NAME_FRONT_CODED=1) # Explicit board/user flags retain their value. In particular never undo # an opt-out or a board-specific USB configuration by re-defining it. return {name: value for name, value in definitions.items() if name not in values} def supported_cc310(framework_dir, mcu, defines): """Only wrap the exact inspected archive; future SDKs retain their getter.""" if str(mcu).lower() != "nrf52840" or not _enabled(defines.get("USE_CC310_HW_CRYPTO")): return False archive = Path(framework_dir) / "libraries/Adafruit_nRFCrypto" / CC310_ARCHIVE return archive.is_file() and hashlib.sha256(archive.read_bytes()).hexdigest() == CC310_ARCHIVE_SHA256 def companion_trim_policy(platform, mcu, src_filter, flags=(), cppdefines=()): """Qualify the actual Companion sources, independent of board/alias name.""" if platform != "nordicnrf52" or not str(mcu).lower().startswith("nrf52"): return {} if not source_selected(src_filter, COMPANION_SOURCES[0]): return {} if any(source_selected(src_filter, source) for source in ROLE_SOURCES): return {} values = flag_defines(flags, cppdefines) for name in ("MESH_NRF52_FLASH_TRIM", "MESH_NRF52_COMPANION_TRIM"): if name in values and not _enabled(values[name]): return {} definitions = {name: value for name, value in COMPANION_TRIM_DEFINES.items() if name not in values} # The older compact option uses #ifdef, so even an explicit =0 selects # that path. Preserve any caller choice instead of enabling both modes. if values.get("ED25519_COMPACT_BASE") is not None: definitions.pop("ED25519_SMALL_BASE", None) return definitions def trim_tft_library(build_env, node): # PlatformIO invokes this on TFT_eSPI's library builder, which is already # a private clone. Do not add a global forced include to the project: its # User_Setup defines are not appropriate for unrelated sources. The # matching ST7735Display translation unit includes this header directly. if not build_env.get("MESH_NRF52_TFT_FONT_TRIM_APPLIED", False): build_env["MESH_NRF52_TFT_FONT_TRIM_APPLIED"] = True build_env.Append(CCFLAGS=[ "-include", build_env.subst("$PROJECT_DIR/src/helpers/ui/Nrf52TftFontTrim.h"), ]) return node def install(build_env): platform = build_env.PioPlatform() mcu = build_env.BoardConfig().get("build.mcu", "") flags = build_env.get("BUILD_FLAGS", ()) cppdefines = build_env.get("CPPDEFINES", ()) definitions = trim_policy(platform.name, mcu, build_env["PIOENV"], build_env.get("SRC_FILTER", ()), flags, cppdefines) infrastructure = bool(definitions) if not infrastructure: definitions = companion_trim_policy(platform.name, mcu, build_env.get("SRC_FILTER", ()), flags, cppdefines) if not definitions: return build_env["MESH_NRF52_FLASH_TRIM_ACTIVE"] = True build_env.AppendUnique(CPPDEFINES=list(definitions.items())) if infrastructure: print("nRF52 infrastructure flash trim: compact crypto, unused USB device classes/GPS cache/vendor splash, compact OTA target names") else: print("nRF52 Companion flash trim: small crypto table, unused USB device classes/GPS cache/vendor splash; fonts and Reader retained") framework = platform.get_package_dir("framework-arduinoadafruitnrf52") values = flag_defines(flags, cppdefines) if infrastructure and values.get("ST7789") is not None: # Keep the font license with this qualified display build, including # cached DFU packages. The hook independently checks the effective # font override and excludes every Companion transport/profile. hook_path = build_env.subst("$PROJECT_DIR/scripts/package_nrf52_font_license.py") hook_spec = importlib.util.spec_from_file_location("mesh_nrf52_font_license", hook_path) font_license = importlib.util.module_from_spec(hook_spec) hook_spec.loader.exec_module(font_license) font_license.install(build_env) if (infrastructure and values.get("DISPLAY_CLASS") == "ST7735Display" and source_selected(build_env.get("SRC_FILTER", ()), "helpers/ui/ST7735Display.cpp")): build_env.AddBuildMiddleware(trim_tft_library, "*TFT_eSPI*/*.cpp") if framework and supported_cc310(framework, mcu, values): build_env.AppendUnique(LINKFLAGS=["-Wl,--wrap=CRYS_ECPKI_GetEcDomain"]) build_env.AppendUnique(CPPPATH=[str(Path(framework) / "libraries/Adafruit_nRFCrypto/src/nrf_cc310/include")]) # BuildSources is called on this selected nRF52 environment only. The # wrapper is not in the common source filter and cannot leak to other # platforms or an unqualified role. build_env.BuildSources("$BUILD_DIR/nrf52-flash-trim", "$PROJECT_DIR/src/helpers/nrf52", "+") else: print("nRF52 CC310 trim: unsupported/unused SDK; ordinary domain lookup retained") if "Import" in globals(): Import("env") install(env)