Files
venus/dbus-windy-station/compile_gui_v2_plugin.py
dev 9756538f16 Initial commit: Venus OS boat addons monorepo
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
2026-03-16 17:04:16 +00:00

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()