Organizes 11 projects for Cerbo GX/Venus OS into a single repository: - axiom-nmea: Raymarine LightHouse protocol decoder - dbus-generator-ramp: Generator current ramp controller - dbus-lightning: Blitzortung lightning monitor - dbus-meteoblue-forecast: Meteoblue weather forecast - dbus-no-foreign-land: noforeignland.com tracking - dbus-tides: Tide prediction from depth + harmonics - dbus-vrm-history: VRM cloud history proxy - dbus-windy-station: Windy.com weather upload - mfd-custom-app: MFD app deployment package - venus-html5-app: Custom Victron HTML5 app fork - watermaker: Watermaker PLC control UI Adds root README, .gitignore, project template, and per-project .gitignore files. Sensitive config files excluded via .gitignore with .example templates provided. Made-with: Cursor
181 lines
5.1 KiB
Python
181 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Standalone GUI v2 plugin compiler for Venus OS.
|
|
|
|
Generates the plugin JSON file without requiring Qt SDK tools
|
|
(lupdate, lrelease, rcc). Builds a Qt binary resource (.rcc)
|
|
natively in Python and embeds it in the JSON descriptor.
|
|
|
|
Compatible with the gui-v2 plugin loader on Venus OS v3.70+.
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import io
|
|
import json
|
|
import os
|
|
import struct
|
|
import sys
|
|
|
|
|
|
def qt_hash(name):
|
|
"""Compute Qt resource name hash (matches qt_hash in QResourceRoot)."""
|
|
h = 0
|
|
for ch in name:
|
|
h = ((h << 4) + ord(ch)) & 0xFFFFFFFF
|
|
h ^= (h & 0xF0000000) >> 23
|
|
h &= 0x0FFFFFFF
|
|
return h
|
|
|
|
|
|
def encode_name(name):
|
|
"""Encode a name entry for the RCC names section."""
|
|
buf = io.BytesIO()
|
|
buf.write(struct.pack('>H', len(name)))
|
|
buf.write(struct.pack('>I', qt_hash(name)))
|
|
buf.write(name.encode('utf-16-be'))
|
|
return buf.getvalue()
|
|
|
|
|
|
def build_rcc(prefix, files):
|
|
"""
|
|
Build a Qt binary resource (.rcc) containing the given files
|
|
under the specified prefix.
|
|
|
|
Tree nodes are sorted by name hash for binary search compatibility
|
|
with QResourceRoot::findNode().
|
|
"""
|
|
sorted_files = sorted(files.keys(), key=lambda n: qt_hash(n))
|
|
|
|
names_buf = io.BytesIO()
|
|
name_offsets = {}
|
|
|
|
name_offsets[""] = names_buf.tell()
|
|
names_buf.write(encode_name(""))
|
|
|
|
name_offsets[prefix] = names_buf.tell()
|
|
names_buf.write(encode_name(prefix))
|
|
|
|
for fname in sorted_files:
|
|
name_offsets[fname] = names_buf.tell()
|
|
names_buf.write(encode_name(fname))
|
|
|
|
names_bytes = names_buf.getvalue()
|
|
|
|
data_buf = io.BytesIO()
|
|
data_offsets = {}
|
|
|
|
for fname in sorted_files:
|
|
data_offsets[fname] = data_buf.tell()
|
|
content = files[fname]
|
|
data_buf.write(struct.pack('>I', len(content)))
|
|
data_buf.write(content)
|
|
|
|
data_bytes = data_buf.getvalue()
|
|
|
|
tree_buf = io.BytesIO()
|
|
|
|
tree_buf.write(struct.pack('>I', name_offsets[""]))
|
|
tree_buf.write(struct.pack('>H', 0x02))
|
|
tree_buf.write(struct.pack('>I', 1))
|
|
tree_buf.write(struct.pack('>I', 1))
|
|
|
|
tree_buf.write(struct.pack('>I', name_offsets[prefix]))
|
|
tree_buf.write(struct.pack('>H', 0x02))
|
|
tree_buf.write(struct.pack('>I', len(sorted_files)))
|
|
tree_buf.write(struct.pack('>I', 2))
|
|
|
|
for fname in sorted_files:
|
|
tree_buf.write(struct.pack('>I', name_offsets[fname]))
|
|
tree_buf.write(struct.pack('>H', 0x00))
|
|
tree_buf.write(struct.pack('>H', 0))
|
|
tree_buf.write(struct.pack('>H', 0))
|
|
tree_buf.write(struct.pack('>I', data_offsets[fname]))
|
|
|
|
tree_bytes = tree_buf.getvalue()
|
|
|
|
HEADER_SIZE = 20
|
|
tree_offset = HEADER_SIZE
|
|
data_offset = tree_offset + len(tree_bytes)
|
|
names_offset = data_offset + len(data_bytes)
|
|
|
|
out = io.BytesIO()
|
|
out.write(b'qres')
|
|
out.write(struct.pack('>I', 1))
|
|
out.write(struct.pack('>I', tree_offset))
|
|
out.write(struct.pack('>I', data_offset))
|
|
out.write(struct.pack('>I', names_offset))
|
|
out.write(tree_bytes)
|
|
out.write(data_bytes)
|
|
out.write(names_bytes)
|
|
|
|
return out.getvalue()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
prog='compile-gui-v2-plugin',
|
|
description='Compile a GUI v2 plugin JSON without Qt SDK tools')
|
|
parser.add_argument('-n', '--name', required=True)
|
|
parser.add_argument('-v', '--version', default='1.0')
|
|
parser.add_argument('-z', '--min-required-version', default='')
|
|
parser.add_argument('-x', '--max-required-version', default='')
|
|
parser.add_argument('-s', '--settings', default='')
|
|
|
|
args = parser.parse_args()
|
|
|
|
files = {}
|
|
for fname in os.listdir('.'):
|
|
if fname.endswith(('.qml', '.svg', '.png')):
|
|
with open(fname, 'rb') as f:
|
|
files[fname] = f.read()
|
|
|
|
if not files:
|
|
print("ERROR: No .qml/.svg/.png files found in current directory",
|
|
file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print("--- files to bundle:")
|
|
for fname in sorted(files.keys()):
|
|
print(" %s (%d bytes)" % (fname, len(files[fname])))
|
|
|
|
if args.settings and args.settings not in files:
|
|
print("ERROR: Settings page '%s' not found" % args.settings,
|
|
file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print("--- building resource binary")
|
|
rcc_data = build_rcc(args.name, files)
|
|
|
|
print("--- base64 encoding resource (%d bytes)" % len(rcc_data))
|
|
resource = base64.b64encode(rcc_data).decode('utf-8')
|
|
|
|
integrations = []
|
|
if args.settings:
|
|
integrations.append({
|
|
"type": 1,
|
|
"url": "qrc:/%s/%s" % (args.name, args.settings)
|
|
})
|
|
|
|
output = {
|
|
"name": args.name,
|
|
"version": args.version,
|
|
"minRequiredVersion": args.min_required_version,
|
|
"maxRequiredVersion": args.max_required_version,
|
|
"translations": [],
|
|
"integrations": integrations,
|
|
"resource": resource
|
|
}
|
|
|
|
output_file = "%s.json" % args.name
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
json.dump(output, f, indent=4)
|
|
f.write('\n')
|
|
|
|
print("--- wrote %s" % output_file)
|
|
print("--- done!")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|