mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-16 23:24:53 +00:00
130 lines
7.4 KiB
Python
130 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate release-bound picker controls from qualified manifests and resolved PIO config.
|
|
|
|
Resolve PlatformIO configuration separately, with no other PIO process running:
|
|
pio project config --json-output > /tmp/pio-config.json
|
|
"""
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def capacity_note(reductions, defines):
|
|
notes = []
|
|
for reduction in reductions:
|
|
contacts = re.fullmatch(r'companion.capacity limited to (\d+) contacts for runtime RAM; (\d+) queued frames and all Full transports retained', reduction)
|
|
compact = re.fullmatch(r'companion.capacity limited to (\d+) contacts, (\d+) channels, and (\d+) queued frames by measured internal DRAM', reduction)
|
|
queue = re.fullmatch(r'nRF52 Full: (\d+) offline frames normally; (\d+) while mOTA borrows queue storage', reduction)
|
|
paper = re.fullmatch(r'Wireless Paper Full: (\d+) contacts; (\d+) offline frames normally, (\d+) while mOTA borrows queue storage', reduction)
|
|
neighbors = re.match(r'mesh.neighbors limited to (\d+)\b', reduction)
|
|
rules = re.match(r'mesh.flood_rules limited to (\d+)\b', reduction)
|
|
if contacts:
|
|
count, frames = contacts.groups()
|
|
notes.append(f'Full Companion capacity: {count} contacts and {frames} queued messages; all Full transports are retained. '
|
|
f'Export contacts before updating if you have more than {count}; entries beyond this limit may be unavailable or omitted by a later save.')
|
|
elif compact:
|
|
count, channels, frames = compact.groups()
|
|
notes.append(f'Full Companion capacity: {count} contacts, {channels} channels and {frames} queued messages. '
|
|
'Export contacts and channels before updating if they exceed these limits; extra entries may be unavailable or omitted by a later save.')
|
|
elif queue or paper:
|
|
if paper:
|
|
count, normal, borrowed = paper.groups()
|
|
channels = defines.get('MAX_GROUP_CHANNELS', '')
|
|
capacity = f'{count} contacts'
|
|
if re.fullmatch(r'\d+', channels):
|
|
capacity += f' and {channels} channels'
|
|
notes.append(f'Wireless Paper Full capacity: {capacity}.')
|
|
else:
|
|
normal, borrowed = queue.groups()
|
|
notes.append(f'Full Companion queue: {normal} offline messages normally; {borrowed} while mOTA borrows queue storage. '
|
|
f'Sync unread messages with a Companion app before starting mOTA if more than {borrowed} are pending. '
|
|
f'Stopping or disconnecting the source restores all {normal} slots.')
|
|
elif neighbors:
|
|
notes.append(f'Neighbor table: {neighbors[1]} entries.')
|
|
elif rules:
|
|
notes.append(f'Flood rules: {rules[1]} entries; the complete rule engine is retained.')
|
|
elif reduction.startswith(('companion.capacity', 'nRF52 Full:', 'Wireless Paper Full:', 'mesh.neighbors', 'mesh.flood_rules')):
|
|
raise ValueError('Unrecognized capacity reduction: ' + reduction)
|
|
return ' '.join(notes)
|
|
|
|
|
|
def generate(stage, config):
|
|
plan = json.loads((stage / 'release-plan.json').read_text())
|
|
envs = {name: dict(options) for name, options in config}
|
|
profiles = {}
|
|
for group in plan['groups']:
|
|
for manifest in json.loads((stage / group['key'] / 'TARGET-MANIFEST.json').read_text()):
|
|
if not manifest.get('verified'):
|
|
raise ValueError('Unqualified target: ' + manifest['target'])
|
|
env = envs['env:' + manifest['platformio_env']]
|
|
flags = env.get('build_flags', [])
|
|
defines = {}
|
|
for flag in flags:
|
|
undefine = re.match(r'-U\s*(\w+)$', flag.strip())
|
|
if undefine:
|
|
defines.pop(undefine[1], None)
|
|
match = re.match(r'-D\s*([\w]+)(?:=(.*))?$', flag.strip())
|
|
if match:
|
|
defines[match[1]] = match[2] or '1'
|
|
def enabled(name):
|
|
return name in defines and defines[name] != '0'
|
|
board_source = ''
|
|
for flag in flags:
|
|
match = re.fullmatch(r'-I\s+(variants/[\w.-]+)', flag.strip())
|
|
if match:
|
|
for file in sorted((ROOT / match[1]).glob('*')):
|
|
if file.suffix in {'.h', '.cpp'}:
|
|
board_source += file.read_text(errors='replace')
|
|
caps = set(manifest['capabilities'])
|
|
controls = {
|
|
'platform': manifest['platform'],
|
|
'gps': enabled('ENV_INCLUDE_GPS'),
|
|
'display': defines.get('DISPLAY_CLASS', 'NullDisplayDriver') != 'NullDisplayDriver',
|
|
'rxgain': enabled('SX126X_RX_BOOSTED_GAIN') or enabled('LR1110_RX_BOOSTED_GAIN'),
|
|
'rxps': 'SX126' in defines.get('RADIO_CLASS', ''),
|
|
'femRx': bool(re.search(r'bool\s+\w*(?:::)?setLoRaFemLnaEnabled\s*\(', board_source)),
|
|
'femTx': bool(re.search(r'bool\s+\w*(?:::)?setLoRaFemPaGainEnabled\s*\(', board_source)),
|
|
'webconfig': 'web.webconfig' in caps,
|
|
'mqtt': enabled('WITH_MQTT_BRIDGE'),
|
|
'rs232': enabled('WITH_RS232_BRIDGE'),
|
|
'espnowBridge': enabled('WITH_ESPNOW_BRIDGE'),
|
|
'primaryEspnow': enabled('MESH_PRIMARY_ESPNOW'),
|
|
'snmp': enabled('WITH_SNMP'),
|
|
'updateMethods': manifest.get('ota_update_methods', []),
|
|
}
|
|
if manifest.get('ota_update_requirements'):
|
|
controls['updateRequirements'] = manifest['ota_update_requirements']
|
|
note = capacity_note(manifest.get('reductions', []), defines)
|
|
if note:
|
|
source = manifest.get('source_commit', plan['source'])
|
|
if not isinstance(source, str) or not re.fullmatch(r'[0-9a-f]{40}', source):
|
|
raise ValueError('Capacity notes require an exact source commit: ' + manifest['target'])
|
|
controls['memoryNote'] = note
|
|
controls['memorySource'] = source
|
|
for name in manifest['files']:
|
|
if not name.endswith(('.bin', '.uf2', '.zip', '.hex')):
|
|
continue
|
|
# Match parseFirmwareAsset's target extraction, including the
|
|
# separate -ota- marker and firmware's unchanged source tag.
|
|
target = re.split(r'-(?:ota-)?v\d+\.', name, maxsplit=1)[0]
|
|
if target in profiles and profiles[target] != controls:
|
|
raise ValueError('Conflicting target metadata: ' + target)
|
|
profiles[target] = controls
|
|
family = next(g['tag'] for g in plan['groups'] if g['key'] == 'companion')
|
|
return {'familyTag': family, 'source': plan['source'], 'profiles': profiles}
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--stage', type=Path, required=True)
|
|
parser.add_argument('--pio-config', type=Path, required=True)
|
|
parser.add_argument('--output', type=Path, default=ROOT / 'docs/_data/firmware_controls.json')
|
|
args = parser.parse_args()
|
|
result = generate(args.stage, json.loads(args.pio_config.read_text()))
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + '\n')
|
|
print(f"Wrote controls for {len(result['profiles'])} qualified profiles")
|