The upstream River 3 schema spells field 355 `ups_alram`. Use `ups_alarm` in the decoder, the generated schema, and the field reference. The schema generator now applies a rename map, so a regenerated schema keeps the corrected name.
142 lines
5.8 KiB
Python
142 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate diagnostic descriptors from the Apache-2.0 ha-ef-ble pr705 schema.
|
|
|
|
Run manually from a checkout. No network access or generator dependency is needed
|
|
at runtime. Only message/enum declarations reachable from the two upload messages
|
|
are retained. The generated file contains data, not upstream decoder code.
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
from urllib.request import Request, urlopen
|
|
|
|
REPOSITORY = "rabits/ha-ef-ble"
|
|
SCALARS = {
|
|
"bool", "bytes", "string", "float", "double", "int32", "int64", "uint32",
|
|
"uint64", "sint32", "sint64", "fixed32", "fixed64", "sfixed32", "sfixed64",
|
|
}
|
|
# Corrected upstream field spellings. Field numbers, not names, identify wire data.
|
|
RENAMES = {"ups_alram": "ups_alarm"}
|
|
|
|
|
|
def api(path):
|
|
request = Request(f"https://api.github.com/repos/{REPOSITORY}/{path}",
|
|
headers={"User-Agent": "bmspy-schema-generator"})
|
|
with urlopen(request, timeout=30) as response:
|
|
return json.load(response)
|
|
|
|
|
|
def source_file(path, ref):
|
|
data = api(f"contents/{path}?ref={ref}")
|
|
if data["encoding"] != "base64":
|
|
raise ValueError("Expected a base64-encoded source file")
|
|
return base64.b64decode(data["content"]).decode("utf-8")
|
|
|
|
|
|
def declarations(text):
|
|
"""Yield top-level declarations without treating nested names as global."""
|
|
position = 0
|
|
while match := re.search(r"\b(message|enum)\s+(\w+)\s*\{", text[position:]):
|
|
start = position + match.end()
|
|
end, depth = start, 1
|
|
while depth and end < len(text):
|
|
depth += (text[end] == "{") - (text[end] == "}")
|
|
end += 1
|
|
if depth:
|
|
raise ValueError("Unterminated schema declaration")
|
|
yield *match.groups(), text[start:end - 1]
|
|
position = end
|
|
|
|
|
|
def parse_schema(text):
|
|
"""Parse reachable flat declarations. Reject unsupported reachable types."""
|
|
text = re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S)
|
|
messages, enums = {}, {}
|
|
for kind, name, body in declarations(text):
|
|
if "{" in body:
|
|
# EventPush has nested definitions but neither upload depends on it.
|
|
# If a reachable type needs nesting, resolution below must fail.
|
|
continue
|
|
fields = {}
|
|
for statement in body.split(";"):
|
|
statement = statement.strip()
|
|
if not statement:
|
|
continue
|
|
if kind == "message":
|
|
match = re.fullmatch(r"(optional|repeated|required)\s+(\w+)\s+(\w+)\s*=\s*(\d+)", statement)
|
|
if not match:
|
|
raise ValueError(f"Unsupported field declaration in {name}: {statement}")
|
|
cardinality, data_type, field_name, number = match.groups()
|
|
value = (RENAMES.get(field_name, field_name), data_type, cardinality == "repeated")
|
|
else:
|
|
match = re.fullmatch(r"(\w+)\s*=\s*(-?\d+)", statement)
|
|
if not match:
|
|
raise ValueError(f"Unsupported enum declaration in {name}: {statement}")
|
|
value, number = match.groups()
|
|
number = int(number)
|
|
if number in fields:
|
|
raise ValueError(f"Duplicate field/enum value in {name}: {number}")
|
|
fields[number] = value
|
|
target = messages if kind == "message" else enums
|
|
if name in target:
|
|
raise ValueError(f"Duplicate declaration: {name}")
|
|
target[name] = fields
|
|
wanted_messages, wanted_enums = {}, {}
|
|
pending = ["DisplayPropertyUpload", "RuntimePropertyUpload"]
|
|
while pending:
|
|
name = pending.pop()
|
|
if name in SCALARS or name in wanted_messages or name in wanted_enums:
|
|
continue
|
|
if name in enums:
|
|
wanted_enums[name] = enums[name]
|
|
elif name in messages:
|
|
wanted_messages[name] = messages[name]
|
|
pending.extend(data_type for _, data_type, _ in messages[name].values())
|
|
else:
|
|
raise ValueError(f"Unresolved schema type: {name}")
|
|
return wanted_messages, wanted_enums
|
|
|
|
|
|
def generate(messages, enums, ref):
|
|
lines = [
|
|
'# SPDX-License-Identifier: Apache-2.0',
|
|
f'# Source: https://github.com/{REPOSITORY}/blob/{ref}/proto/pr705.proto',
|
|
'# Generated by tools/generate_ecoflow_river3_schema.py. Do not edit by hand.',
|
|
'', f'SCHEMA_COMMIT = {ref!r}', '',
|
|
'# Field number: (name, protobuf type, repeated). Units are curated separately.',
|
|
'MESSAGES = {',
|
|
]
|
|
for name, fields in sorted(messages.items()):
|
|
lines.append(f' {name!r}: {{')
|
|
lines.extend(f' {number}: {field!r},' for number, field in sorted(fields.items()))
|
|
lines.append(' },')
|
|
lines.extend(['}', '', 'ENUMS = {'])
|
|
for name, values in sorted(enums.items()):
|
|
lines.append(f' {name!r}: {{')
|
|
lines.extend(f' {number}: {value!r},' for number, value in sorted(values.items()))
|
|
lines.append(' },')
|
|
lines.extend(['}', ''])
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--ref', help='Pinned upstream commit (defaults to current main)')
|
|
args = parser.parse_args()
|
|
ref = args.ref or api('commits/main')['sha']
|
|
if not re.fullmatch(r'[a-f0-9]{40}', ref):
|
|
parser.error('--ref must be a full commit SHA')
|
|
messages, enums = parse_schema(source_file('proto/pr705.proto', ref))
|
|
root = Path(__file__).resolve().parents[1]
|
|
(root / 'bmspy/ecoflow_river3_schema.py').write_text(generate(messages, enums, ref), encoding='utf-8')
|
|
print(f'Generated {len(messages)} messages and {len(enums)} enums from {ref}')
|
|
for name in ('DisplayPropertyUpload', 'RuntimePropertyUpload'):
|
|
print(f'{name}: {len(messages[name])} fields')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|