mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-13 15:11:20 +10:00
592 lines
30 KiB
Python
592 lines
30 KiB
Python
#!/usr/bin/env python3
|
||
|
||
"""Generate a markdown inventory of board-support metadata and common target-definition macros."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import re
|
||
from collections import Counter, defaultdict
|
||
from pathlib import Path
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
VARIANTS_DIR = ROOT / "variants"
|
||
OUTPUT_PATH = ROOT / "docs" / "hardware-support-context.md"
|
||
|
||
PLATFORMIO_METADATA_KEYS = [
|
||
"custom_meshtastic_hw_model",
|
||
"custom_meshtastic_hw_model_slug",
|
||
"custom_meshtastic_architecture",
|
||
"custom_meshtastic_actively_supported",
|
||
"custom_meshtastic_support_level",
|
||
"custom_meshtastic_display_name",
|
||
"custom_meshtastic_images",
|
||
"custom_meshtastic_tags",
|
||
"custom_meshtastic_requires_dfu",
|
||
"custom_meshtastic_partition_scheme",
|
||
]
|
||
|
||
MACRO_PATTERNS = {
|
||
"Input": [
|
||
"BUTTON_PIN",
|
||
"BUTTON_PIN_TOUCH",
|
||
"ALT_BUTTON_PIN",
|
||
"CANCEL_BUTTON_PIN",
|
||
"ROTARY_",
|
||
"KB_",
|
||
"INPUTDRIVER_",
|
||
],
|
||
"Radio": [
|
||
"USE_RF95",
|
||
"USE_SX126",
|
||
"USE_SX128",
|
||
"USE_LLCC68",
|
||
"USE_LR11",
|
||
"LORA_",
|
||
"SX126X_",
|
||
"SX128X_",
|
||
"LR1121_",
|
||
"RF95_",
|
||
],
|
||
"GPS": [
|
||
"HAS_GPS",
|
||
"GPS_",
|
||
"PIN_GPS_",
|
||
"GPS_DEFAULT_NOT_PRESENT",
|
||
],
|
||
"Display": [
|
||
"HAS_SCREEN",
|
||
"USE_TFTDISPLAY",
|
||
"USE_TFT",
|
||
"USE_SSD",
|
||
"USE_SH",
|
||
"USE_ST",
|
||
"TFT_",
|
||
"OLED_",
|
||
"SCREEN_",
|
||
"PIN_EINK_",
|
||
"EINK_",
|
||
"DISPLAY_",
|
||
"LGFX_",
|
||
"HAS_TFT",
|
||
],
|
||
"I2C/SPI": [
|
||
"I2C_",
|
||
"SPI_",
|
||
"PIN_SPI",
|
||
"WIRE_",
|
||
],
|
||
"Power": [
|
||
"BATTERY_",
|
||
"ADC_",
|
||
"PIN_POWER",
|
||
"USE_POWERSAVE",
|
||
"SLEEP_TIME",
|
||
"XPOWERS_",
|
||
"HAS_AXP",
|
||
"HAS_PPM",
|
||
"HAS_BQ",
|
||
"EXT_NOTIFY_OUT",
|
||
"FAN_CTRL_PIN",
|
||
"RF95_FAN_EN",
|
||
],
|
||
"Connectivity/Other": [
|
||
"HAS_WIFI",
|
||
"HAS_BLUETOOTH",
|
||
"HAS_ETHERNET",
|
||
"HAS_NEOPIXEL",
|
||
"HAS_I2S",
|
||
"HAS_TOUCH",
|
||
"HAS_TOUCHSCREEN",
|
||
"HAS_NFC",
|
||
"NFC_",
|
||
"USE_XL9555",
|
||
"EXPANDS_",
|
||
"PCF",
|
||
"RTC",
|
||
],
|
||
}
|
||
|
||
|
||
def classify_macro(name: str) -> str:
|
||
for category, prefixes in MACRO_PATTERNS.items():
|
||
for prefix in prefixes:
|
||
if name.startswith(prefix):
|
||
return category
|
||
return "Other"
|
||
|
||
|
||
def parse_platformio_envs(path: Path) -> list[dict[str, object]]:
|
||
envs: list[dict[str, object]] = []
|
||
current: dict[str, object] | None = None
|
||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||
line = raw_line.strip()
|
||
if not line or line.startswith(";"):
|
||
continue
|
||
if line.startswith("[") and line.endswith("]"):
|
||
if current:
|
||
envs.append(current)
|
||
current = {"section": line[1:-1], "metadata": {}}
|
||
continue
|
||
if current is None or "=" not in line:
|
||
continue
|
||
key, value = [part.strip() for part in line.split("=", 1)]
|
||
if key in PLATFORMIO_METADATA_KEYS:
|
||
current["metadata"][key] = value
|
||
if current:
|
||
envs.append(current)
|
||
return envs
|
||
|
||
|
||
def parse_variant_macros(path: Path) -> tuple[dict[str, str], Counter[str]]:
|
||
defines: dict[str, str] = {}
|
||
category_counts: Counter[str] = Counter()
|
||
pattern = re.compile(r"^#define\s+([A-Za-z0-9_]+)(?:\s+(.*?))?\s*(?://.*)?$")
|
||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||
match = pattern.match(raw_line.strip())
|
||
if not match:
|
||
continue
|
||
name = match.group(1)
|
||
value = (match.group(2) or "1").strip()
|
||
defines[name] = value
|
||
category_counts[classify_macro(name)] += 1
|
||
return defines, category_counts
|
||
|
||
|
||
MACRO_DESCRIPTIONS: dict[str, str] = {
|
||
# ---------- Input ----------
|
||
"BUTTON_PIN": "Primary user button GPIO pin number.",
|
||
"ALT_BUTTON_PIN": "Secondary / alternate button pin, used on boards with two buttons.",
|
||
"CANCEL_BUTTON_PIN": "Button wired to cancel/back actions (e.g., T-Deck cancel key).",
|
||
"BUTTON_NEED_PULLUP": "Set to 1 when the button GPIO requires the internal pull-up to be enabled.",
|
||
"BUTTON_PIN_ALT": "Alternative spelling used by some older board families for a second button.",
|
||
"KB_BL_PIN": "Keyboard backlight control pin (e.g., T-LoRa Pager keyboard).",
|
||
"KB_INT": "Keyboard interrupt input pin; signals a keypress to the MCU.",
|
||
"KB_POWERON": "Pin used to power-on or enable the keyboard peripheral.",
|
||
"KB_SLAVE_ADDRESS": "I2C address of the keyboard controller IC.",
|
||
"INPUTDRIVER_ENCODER_TYPE": "Selects the rotary encoder driver variant (0 = none, 1+ = specific type).",
|
||
"INPUTDRIVER_TWO_WAY_ROCKER": "Enables the two-way rocker input driver.",
|
||
"INPUTDRIVER_TWO_WAY_ROCKER_RIGHT": "GPIO pin for the rightward rocker direction.",
|
||
"INPUTDRIVER_TWO_WAY_ROCKER_LEFT": "GPIO pin for the leftward rocker direction.",
|
||
"INPUTDRIVER_TWO_WAY_ROCKER_BTN": "GPIO pin for the rocker click/press direction.",
|
||
"ROTARY_A": "First encoder channel pin (clock / A signal).",
|
||
"ROTARY_B": "Second encoder channel pin (data / B signal).",
|
||
# ---------- Radio ----------
|
||
"USE_SX1262": "Select the SX1262 sub-GHz LoRa chip driver.",
|
||
"USE_SX1268": "Select the SX1268 sub-GHz LoRa chip driver (higher power variant of SX1262).",
|
||
"USE_SX1280": "Select the SX1280 2.4 GHz LoRa chip driver.",
|
||
"USE_RF95": "Select the RFM95/SX1276 legacy LoRa chip driver.",
|
||
"USE_LLCC68": "Select the LLCC68 low-cost LoRa chip driver.",
|
||
"USE_LR1110": "Select the LR1110 wideband radio driver.",
|
||
"USE_LR1120": "Select the LR1120 wideband radio driver.",
|
||
"USE_LR1121": "Select the LR1121 wideband radio driver.",
|
||
"LORA_CS": "LoRa radio SPI chip-select GPIO pin.",
|
||
"LORA_RESET": "LoRa radio hardware-reset GPIO pin (active low).",
|
||
"LORA_DIO0": "LoRa radio DIO0 interrupt pin (RF95/SX1276 done/RxDone).",
|
||
"LORA_DIO1": "LoRa radio DIO1 interrupt pin; on SX126x this is the primary IRQ line.",
|
||
"LORA_DIO2": "LoRa radio DIO2 pin; on RF95 used for FSK interrupt; on SX126x tx/rx control.",
|
||
"LORA_SCK": "LoRa radio SPI clock pin.",
|
||
"LORA_MISO": "LoRa radio SPI MISO pin.",
|
||
"LORA_MOSI": "LoRa radio SPI MOSI pin.",
|
||
"SX126X_CS": "SX126x chip-select pin (often same as LORA_CS).",
|
||
"SX126X_RESET": "SX126x reset pin (often same as LORA_RESET).",
|
||
"SX126X_BUSY": "SX126x BUSY pin; must be polled low before issuing SPI commands.",
|
||
"SX126X_DIO1": "SX126x DIO1 interrupt output used for all IRQ events.",
|
||
"SX126X_DIO3_TCXO_VOLTAGE": "Drives the TCXO regulator via DIO3; set to the supply voltage (e.g., 1.8).",
|
||
"SX126X_DIO2_AS_RF_SWITCH": "Set to 1 so the driver controls the TX/RX RF switch via DIO2.",
|
||
"LR1121_IRQ_PIN": "LR1121 interrupt request pin.",
|
||
"RF95_FAN_EN": "Enables a cooling fan for high-power RF95 installations.",
|
||
# ---------- GPS ----------
|
||
"HAS_GPS": "Set to 1 if the board has an on-board GPS receiver; 0 to disable GPS entirely.",
|
||
"GPS_RX_PIN": "UART RX pin connected to the GPS module TX output.",
|
||
"GPS_TX_PIN": "UART TX pin connected to the GPS module RX input.",
|
||
"PIN_GPS_EN": "GPIO to power-enable or power-gate the GPS module.",
|
||
"PIN_GPS_PPS": "GPS pulse-per-second input pin for timing synchronisation.",
|
||
"PIN_GPS_STANDBY": "Places the GPS into standby/low-power mode when driven.",
|
||
"PIN_GPS_RESET": "Hardware-reset line to the GPS module.",
|
||
"PIN_GPS_REINIT": "Pin used to trigger GPS re-initialisation sequences.",
|
||
"GPS_THREAD_INTERVAL": "Millisecond poll interval for the GPS background thread.",
|
||
"GPS_BAUDRATE": "UART baud rate for GPS serial communication.",
|
||
"GPS_L76K": "Selects the Quectel L76K GPS driver and protocol.",
|
||
"GPS_UBLOX": "Selects the u-blox GPS driver and UBX protocol.",
|
||
"GPS_EN_ACTIVE": "Logic level (HIGH or LOW) that enables the GPS power pin.",
|
||
"GPS_RESET_MODE": "Defines the reset signal polarity or protocol for the GPS chip.",
|
||
"GPS_DEFAULT_NOT_PRESENT": "Compiled-in default assuming no GPS; overridden at runtime if detected.",
|
||
# ---------- Display ----------
|
||
"HAS_SCREEN": "Set to 1 if the board has any display hardware.",
|
||
"USE_TFTDISPLAY": "Selects the TFT LCD driver path.",
|
||
"USE_SSD1306": "Selects the SSD1306 128×64 OLED driver over I2C.",
|
||
"USE_SH1106": "Selects the SH1106 128×64 OLED driver.",
|
||
"USE_ST7735": "Selects the ST7735 TFT SPI driver.",
|
||
"USE_ST7789": "Selects the ST7789 TFT SPI driver.",
|
||
"TFT_WIDTH": "Horizontal pixel resolution of the TFT display.",
|
||
"TFT_HEIGHT": "Vertical pixel resolution of the TFT display.",
|
||
"TFT_OFFSET_X": "Horizontal pixel offset for display alignment correction.",
|
||
"TFT_OFFSET_Y": "Vertical pixel offset for display alignment correction.",
|
||
"TFT_BL": "Backlight control PWM or GPIO pin for the TFT panel.",
|
||
"SCREEN_ROTATE": "Non-zero value rotates the display 180° for mounted-upside-down screens.",
|
||
"SCREEN_TRANSITION_FRAMERATE": "Target framerate for UI animations and transitions.",
|
||
"PIN_EINK_CS": "E-ink display SPI chip-select pin.",
|
||
"PIN_EINK_BUSY": "E-ink display BUSY output; high when a page update is in progress.",
|
||
"PIN_EINK_DC": "E-ink display data/command select pin.",
|
||
"PIN_EINK_RES": "E-ink display hardware-reset pin.",
|
||
"PIN_EINK_SCLK": "E-ink display SPI clock pin.",
|
||
"PIN_EINK_MOSI": "E-ink display SPI MOSI pin.",
|
||
# ---------- I2C/SPI ----------
|
||
"I2C_SDA": "Primary I2C data line GPIO pin.",
|
||
"I2C_SCL": "Primary I2C clock line GPIO pin.",
|
||
"PIN_SPI_MISO": "Primary SPI MISO (data from peripheral) GPIO pin.",
|
||
"PIN_SPI_MOSI": "Primary SPI MOSI (data to peripheral) GPIO pin.",
|
||
"PIN_SPI_SCK": "Primary SPI clock GPIO pin.",
|
||
"SPI_INTERFACES_COUNT": "Number of hardware SPI buses available on this board.",
|
||
"WIRE_INTERFACES_COUNT": "Number of hardware I2C buses available on this board.",
|
||
"PIN_SPI1_MISO": "Secondary SPI bus MISO pin.",
|
||
"PIN_SPI1_MOSI": "Secondary SPI bus MOSI pin.",
|
||
"PIN_SPI1_SCK": "Secondary SPI bus clock pin.",
|
||
"SPI_FREQUENCY": "Default SPI clock frequency in Hz for this board.",
|
||
"SPI_READ_FREQUENCY": "Reduced SPI clock rate used for read transactions.",
|
||
"SPI_SCK": "SPI clock pin alias used in older board files.",
|
||
"SPI_MOSI": "SPI MOSI pin alias used in older board files.",
|
||
"SPI_MISO": "SPI MISO pin alias used in older board files.",
|
||
# ---------- Power ----------
|
||
"BATTERY_PIN": "ADC input GPIO connected to the battery voltage divider.",
|
||
"ADC_MULTIPLIER": "Floating-point scale factor to convert raw ADC reading to battery voltage.",
|
||
"ADC_CHANNEL": "ADC channel enum or number for the battery sense input.",
|
||
"BATTERY_SENSE_RESOLUTION_BITS": "ADC resolution in bits used for battery voltage sampling.",
|
||
"ADC_RESOLUTION": "Board-level ADC resolution definition, referenced by other power macros.",
|
||
"BATTERY_SENSE_RESOLUTION": "Alias for the effective ADC resolution for battery sense.",
|
||
"ADC_CTRL": "GPIO that enables or gates the ADC voltage-divider circuit.",
|
||
"ADC_CTRL_ENABLED": "Logic level (HIGH or LOW) that turns on the ADC control switch.",
|
||
"ADC_ATTENUATION": "ESP32 ADC input attenuation setting; controls measurable voltage range.",
|
||
"BATTERY_SENSE_SAMPLES": "Number of ADC samples to average for a stable battery reading.",
|
||
"EXT_NOTIFY_OUT": "GPIO output used to signal an external LED or buzzer for notifications.",
|
||
"USE_POWERSAVE": "Enables aggressive power-save mode (deep sleep, reduced poll intervals).",
|
||
"SLEEP_TIME": "Default light-sleep duration in milliseconds between wakeups.",
|
||
"PIN_POWER_EN": "GPIO to assert to enable a board power rail or load switch.",
|
||
"HAS_PPM": "Set to 1 if the board has an IP5306 or similar PPM power path IC.",
|
||
# ---------- Connectivity/Other ----------
|
||
"HAS_TOUCHSCREEN": "Set to 1 for boards with a capacitive or resistive touch panel.",
|
||
"HAS_NEOPIXEL": "Set to 1 if the board has addressable RGB LEDs (WS2812 / NeoPixel).",
|
||
"PCF8563_RTC": "I2C address of the PCF8563 real-time clock IC.",
|
||
"PCF85063_RTC": "I2C address of the PCF85063 real-time clock IC.",
|
||
"HAS_ETHERNET": "Set to 1 for boards with a wired Ethernet interface.",
|
||
"HAS_I2S": "Set to 1 if I2S audio output is present.",
|
||
"NFC_INT": "Interrupt pin from the NFC controller IC.",
|
||
"NFC_CS": "SPI chip-select for the NFC controller.",
|
||
"USE_XL9555": "Enables the XL9555 16-bit I2C GPIO expander driver.",
|
||
"EXPANDS_DRV_EN": "GPIO expander pin used to enable the haptic driver.",
|
||
"EXPANDS_AMP_EN": "GPIO expander pin used to power-on the audio amplifier.",
|
||
"EXPANDS_KB_RST": "GPIO expander pin used to reset the keyboard controller.",
|
||
"EXPANDS_LORA_EN": "GPIO expander pin used to power-gate the LoRa radio.",
|
||
"EXPANDS_GPS_EN": "GPIO expander pin used to power-gate the GPS module.",
|
||
"EXPANDS_NFC_EN": "GPIO expander pin used to power-gate the NFC controller.",
|
||
# ---------- Other ----------
|
||
"LED_POWER": "GPIO for the status LED, defines the LED pin number.",
|
||
"LED_STATE_ON": "Logic level (HIGH or LOW) that turns the status LED on.",
|
||
"PIN_SERIAL1_RX": "Secondary UART RX pin (used for accessories, GPS on some boards).",
|
||
"PIN_SERIAL1_TX": "Secondary UART TX pin.",
|
||
"PIN_WIRE_SDA": "Arduino-framework I2C SDA pin alias (nRF52 / RP2040 style).",
|
||
"PIN_WIRE_SCL": "Arduino-framework I2C SCL pin alias.",
|
||
"PIN_LED1": "First LED GPIO pin in the nRF52 Arduino BSP pin table.",
|
||
"VARIANT_MCK": "Crystal oscillator frequency in Hz for nRF52 variant clock configuration.",
|
||
"PINS_COUNT": "Total number of GPIO pins defined in the Arduino BSP variant table.",
|
||
"NUM_DIGITAL_PINS": "Count of digital-capable pins in the BSP variant table.",
|
||
"NUM_ANALOG_INPUTS": "Count of analog-input pins in the BSP variant table.",
|
||
"NUM_ANALOG_OUTPUTS": "Count of analog-output (DAC) pins in the BSP variant table.",
|
||
"LED_BLUE": "GPIO number of the blue status LED (typical on nRF52 and RP2040 boards).",
|
||
"USE_LFXO": "Instructs the nRF52 BSP to use the low-frequency crystal oscillator.",
|
||
"BUTTON_NEED_PULLUP": "Enables internal pull-up on the button GPIO (duplicate entry for clarity).",
|
||
}
|
||
|
||
|
||
def shorten(value: str, limit: int = 60) -> str:
|
||
compact = " ".join(value.split())
|
||
return compact if len(compact) <= limit else compact[: limit - 3] + "..."
|
||
|
||
|
||
# Architecture families known to rely heavily on BSP or base-environment defaults rather
|
||
# than declaring every field locally. Used in the Inherited Defaults Note section.
|
||
_BSP_DEFAULT_FAMILIES: dict[str, list[str]] = {
|
||
"nrf52840": [
|
||
"VARIANT_MCK — nRF52 BSP clock constant (e.g., 64000000ul); always inherited from BSP unless overridden.",
|
||
"USE_LFXO — low-frequency crystal oscillator selection; declared locally only when the board uses LFXO rather than the RC oscillator.",
|
||
"PIN_SPI_* / PIN_SPI1_* — SPI bus pin numbers come from the BSP variant table; boards override only when the LoRa radio or display uses non-default SPI routing.",
|
||
"WIRE_INTERFACES_COUNT / SPI_INTERFACES_COUNT — bus count comes from BSP; explicitly set only when the board deviates.",
|
||
"LED_BLUE / PIN_LED1 / PINS_COUNT / NUM_DIGITAL_PINS — standard BSP pin-table entries inherited from the nRF52 Arduino core.",
|
||
],
|
||
"rp2040": [
|
||
"PIN_SPI_* — primary SPI pins come from the RP2040 Arduino BSP; most boards declare them explicitly, but the defaults align with the Pico pin assignments.",
|
||
"NUM_DIGITAL_PINS / NUM_ANALOG_INPUTS — Arduino BSP counts; rarely overridden locally.",
|
||
],
|
||
"stm32": [
|
||
"Radio and pin assignments for STM32WL targets are largely internal to the WL SoC and declared via STM32 HAL/BSP headers; variant.h files are minimal.",
|
||
"USE_STM32WLx is typically the only explicit define; all other radio config comes from the BSP.",
|
||
],
|
||
"native": [
|
||
"The native/Portduino target uses runtime configuration rather than compile-time pin defines; variant.h only sets display and GPS stubs.",
|
||
],
|
||
}
|
||
|
||
|
||
def collect_inventory() -> dict[str, object]:
|
||
architectures: defaultdict[str, list[dict[str, object]]] = defaultdict(list)
|
||
category_frequency: defaultdict[str, Counter[str]] = defaultdict(Counter)
|
||
total_variant_dirs = 0
|
||
total_envs = 0
|
||
no_variant_h: defaultdict[str, int] = defaultdict(int) # arch -> count of dirs with no variant.h
|
||
has_metadata: int = 0 # env count that has at least one custom_meshtastic_* key
|
||
|
||
for platformio_path in sorted(VARIANTS_DIR.glob("**/platformio.ini")):
|
||
variant_dir = platformio_path.parent
|
||
total_variant_dirs += 1
|
||
variant_path = variant_dir / "variant.h"
|
||
board_level_arch_raw = variant_dir.parts[-2] if len(variant_dir.parts) >= 2 else "unknown"
|
||
if not variant_path.exists():
|
||
no_variant_h[board_level_arch_raw] += 1
|
||
continue
|
||
|
||
envs = parse_platformio_envs(platformio_path)
|
||
defines, category_counts = parse_variant_macros(variant_path)
|
||
total_envs += len(envs)
|
||
|
||
board_level_arch = board_level_arch_raw
|
||
for env in envs:
|
||
section = str(env["section"])
|
||
if not section.startswith("env:"):
|
||
continue
|
||
metadata = dict(env["metadata"])
|
||
if metadata:
|
||
has_metadata += 1
|
||
architecture = str(metadata.get("custom_meshtastic_architecture") or board_level_arch)
|
||
if architecture == "esp32s3":
|
||
architecture = "esp32-s3"
|
||
if architecture == "esp32c3":
|
||
architecture = "esp32-c3"
|
||
if architecture == "esp32c6":
|
||
architecture = "esp32-c6"
|
||
|
||
entry = {
|
||
"environment": section.split(":", 1)[1],
|
||
"variant_dir": str(variant_dir.relative_to(ROOT)),
|
||
"metadata": metadata,
|
||
"defines": defines,
|
||
"category_counts": category_counts,
|
||
}
|
||
architectures[architecture].append(entry)
|
||
|
||
for name in defines:
|
||
category = classify_macro(name)
|
||
category_frequency[category][name] += 1
|
||
|
||
return {
|
||
"architectures": architectures,
|
||
"category_frequency": category_frequency,
|
||
"total_variant_dirs": total_variant_dirs,
|
||
"total_envs": total_envs,
|
||
"no_variant_h": dict(no_variant_h),
|
||
"has_metadata": has_metadata,
|
||
}
|
||
|
||
|
||
def render_markdown(inventory: dict[str, object]) -> str:
|
||
architectures = inventory["architectures"]
|
||
category_frequency = inventory["category_frequency"]
|
||
lines: list[str] = []
|
||
|
||
lines.append("# Hardware Support Context")
|
||
lines.append("")
|
||
lines.append("This document inventories the board-support inputs and common target-definition fields")
|
||
lines.append("currently used in the Meshtastic firmware repository. It is intended as reusable context")
|
||
lines.append("for adding new hardware support without re-discovering naming patterns, metadata keys, and")
|
||
lines.append("frequently used pin or capability macros from scratch.")
|
||
lines.append("")
|
||
lines.append("## Scope")
|
||
lines.append("")
|
||
lines.append(f"- Variant directories scanned: {inventory['total_variant_dirs']}")
|
||
lines.append(f"- PlatformIO environments summarized: {inventory['total_envs']}")
|
||
lines.append("- Sources: `variants/**/platformio.ini` and `variants/**/variant.h`")
|
||
lines.append("- Notes: This inventory reflects explicit per-variant declarations. Some boards also inherit")
|
||
lines.append(" defaults from architecture headers or shared base environments, which must still be checked")
|
||
lines.append(" before creating new hardware support.")
|
||
lines.append("")
|
||
lines.append("## Repository Metadata Inputs")
|
||
lines.append("")
|
||
lines.append("The following `custom_meshtastic_*` metadata keys are already used across board environments:")
|
||
lines.append("")
|
||
for key in PLATFORMIO_METADATA_KEYS:
|
||
lines.append(f"- `{key}`")
|
||
lines.append("")
|
||
lines.append("## Common Target-Definition Categories")
|
||
lines.append("")
|
||
for category in ["Input", "Radio", "GPS", "Display", "I2C/SPI", "Power", "Connectivity/Other", "Other"]:
|
||
counter = category_frequency.get(category, Counter())
|
||
if not counter:
|
||
continue
|
||
lines.append(f"### {category}")
|
||
lines.append("")
|
||
lines.append("| Macro | Used in | Description |")
|
||
lines.append("| --- | --- | --- |")
|
||
for name, count in counter.most_common(15):
|
||
description = MACRO_DESCRIPTIONS.get(name, "")
|
||
lines.append(f"| `{name}` | {count} variants | {description} |")
|
||
lines.append("")
|
||
|
||
lines.append("## Architecture and Environment Inventory")
|
||
lines.append("")
|
||
for architecture in sorted(architectures):
|
||
entries = sorted(architectures[architecture], key=lambda item: item["environment"])
|
||
lines.append(f"### {architecture}")
|
||
lines.append("")
|
||
lines.append("| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |")
|
||
lines.append("| --- | --- | --- | --- | --- | --- |")
|
||
for entry in entries:
|
||
metadata = entry["metadata"]
|
||
category_counts = entry["category_counts"]
|
||
categories = []
|
||
for category in ["Display", "Radio", "Input", "GPS", "Power", "Connectivity/Other"]:
|
||
count = category_counts.get(category, 0)
|
||
if count:
|
||
categories.append(f"{category}:{count}")
|
||
category_summary = ", ".join(categories) if categories else "none"
|
||
lines.append(
|
||
"| {environment} | {display_name} | {hw_model} | {hw_slug} | {variant_dir} | {categories} |".format(
|
||
environment=entry["environment"],
|
||
display_name=metadata.get("custom_meshtastic_display_name", ""),
|
||
hw_model=metadata.get("custom_meshtastic_hw_model", ""),
|
||
hw_slug=metadata.get("custom_meshtastic_hw_model_slug", ""),
|
||
variant_dir=entry["variant_dir"],
|
||
categories=category_summary,
|
||
)
|
||
)
|
||
lines.append("")
|
||
|
||
lines.append("## Representative Board Examples")
|
||
lines.append("")
|
||
for architecture in sorted(architectures):
|
||
entries = sorted(architectures[architecture], key=lambda item: item["environment"])
|
||
if not entries:
|
||
continue
|
||
sample = entries[0]
|
||
lines.append(f"### {architecture}: `{sample['environment']}`")
|
||
lines.append("")
|
||
lines.append(f"- Variant directory: `{sample['variant_dir']}`")
|
||
metadata = sample["metadata"]
|
||
for key in PLATFORMIO_METADATA_KEYS:
|
||
value = metadata.get(key)
|
||
if value:
|
||
lines.append(f"- `{key}`: `{value}`")
|
||
defines = sample["defines"]
|
||
for category in ["Input", "Radio", "GPS", "Display", "I2C/SPI", "Power", "Connectivity/Other"]:
|
||
selected = [name for name in defines if classify_macro(name) == category][:8]
|
||
if not selected:
|
||
continue
|
||
lines.append(f"- {category} examples:")
|
||
for name in selected:
|
||
lines.append(f" - `{name}` = `{shorten(defines[name])}`")
|
||
lines.append("")
|
||
|
||
lines.append("## Intake Guidance For New Hardware")
|
||
lines.append("")
|
||
lines.append("When using this context to add a new board, collect these inputs before generating files:")
|
||
lines.append("")
|
||
lines.append("- PlatformIO environment name")
|
||
lines.append("- `custom_meshtastic_hw_model` and `custom_meshtastic_hw_model_slug`")
|
||
lines.append("- Display name and architecture")
|
||
lines.append("- Whether the board is actively supported and its support level")
|
||
lines.append("- Partition scheme, DFU requirement, and image/tag metadata if applicable")
|
||
lines.append("- Radio chip family and complete radio pin group")
|
||
lines.append("- Input/button/rotary/keyboard pins")
|
||
lines.append("- Display interface pins and driver-related macros")
|
||
lines.append("- GPS, power-management, I2C, SPI, storage, and auxiliary peripheral definitions")
|
||
lines.append("- Any board-specific initialization that requires `variant.cpp` or extra variant hooks")
|
||
lines.append("")
|
||
lines.append("## Inherited Defaults Note")
|
||
lines.append("")
|
||
lines.append(
|
||
"Some architecture families rely on BSP (Board Support Package) or base-environment defaults "
|
||
"rather than declaring every pin or capability macro explicitly in `variant.h`. "
|
||
"When adding a new board for one of these families, check the relevant BSP headers before "
|
||
"assuming a missing define means a feature is absent."
|
||
)
|
||
lines.append("")
|
||
no_variant_h = inventory.get("no_variant_h", {})
|
||
if no_variant_h:
|
||
lines.append(
|
||
f"Directories scanned that had no `variant.h` "
|
||
f"(relying entirely on BSP/base-environment): "
|
||
+ ", ".join(f"{arch}: {count}" for arch, count in sorted(no_variant_h.items()))
|
||
)
|
||
lines.append("")
|
||
for family, notes in _BSP_DEFAULT_FAMILIES.items():
|
||
lines.append(f"### {family}")
|
||
lines.append("")
|
||
for note in notes:
|
||
lines.append(f"- {note}")
|
||
lines.append("")
|
||
lines.append("## Cautions")
|
||
lines.append("")
|
||
lines.append("- Some boards rely on architecture defaults rather than declaring every field locally.")
|
||
lines.append("- Some board families expose multiple environments or display variants that share one hardware model.")
|
||
lines.append("- Source materials such as schematics still need human verification before new pin mappings are trusted.")
|
||
lines.append("- This document is a starting context artifact, not proof that a new board definition is safe to merge.")
|
||
lines.append("")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def print_validate_summary(inventory: dict[str, object]) -> None:
|
||
"""Print a validation summary to stdout without writing the output file."""
|
||
total_dirs = inventory["total_variant_dirs"]
|
||
total_envs = inventory["total_envs"]
|
||
has_metadata = inventory["has_metadata"]
|
||
no_variant_h = inventory.get("no_variant_h", {})
|
||
no_variant_h_total = sum(no_variant_h.values())
|
||
architectures = inventory["architectures"]
|
||
arch_count = len(architectures)
|
||
|
||
print("Hardware Support Context — Validation Summary")
|
||
print("=" * 48)
|
||
print(f"Variant directories scanned : {total_dirs}")
|
||
print(f"Directories with no variant.h (BSP-only) : {no_variant_h_total}")
|
||
if no_variant_h:
|
||
for arch, count in sorted(no_variant_h.items()):
|
||
print(f" {arch}: {count}")
|
||
print(f"PlatformIO environments found : {total_envs}")
|
||
print(f"Environments with custom_meshtastic_* metadata : {has_metadata}")
|
||
print(f"Architecture families represented : {arch_count}")
|
||
print("")
|
||
print("Architecture families:")
|
||
for arch in sorted(architectures):
|
||
entries = architectures[arch]
|
||
print(f" {arch}: {len(entries)} environment(s)")
|
||
print("")
|
||
print("BSP-default families with special notes:")
|
||
for family in _BSP_DEFAULT_FAMILIES:
|
||
marker = "YES" if family in {a.split("-")[0] for a in architectures} else "not in scan"
|
||
print(f" {family}: {marker}")
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(description="Generate hardware support context markdown")
|
||
parser.add_argument("--output", default=str(OUTPUT_PATH), help="Output markdown path")
|
||
parser.add_argument(
|
||
"--validate",
|
||
action="store_true",
|
||
help="Print a validation summary (variant counts, BSP-only dirs, metadata coverage) without writing output",
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
inventory = collect_inventory()
|
||
|
||
if args.validate:
|
||
print_validate_summary(inventory)
|
||
return
|
||
|
||
markdown = render_markdown(inventory)
|
||
output_path = Path(args.output)
|
||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||
output_path.write_text(markdown + "\n", encoding="utf-8")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |