mirror of
https://github.com/apache/nuttx-apps.git
synced 2026-08-01 20:29:00 +00:00
tools: Export package metadata and RGB565 icons.
Keep the existing package specification syntax backward compatible, including source paths containing colons. Add separate name-keyed options for optional descriptions, categories, and icon source images. Convert icons to a fixed 48 by 48 little-endian RGB565 payload with an LVGL-compatible image header, and place them at package-level repository paths for nxstore caching. Assisted-by: OpenAI Codex:gpt-5.6-sol Signed-off-by: aviralgarg05 <gargaviral99@gmail.com>
This commit is contained in:
parent
5e09f0b0de
commit
de1a59a972
1 changed files with 122 additions and 0 deletions
|
|
@ -32,6 +32,7 @@ import argparse
|
|||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
|
@ -41,6 +42,21 @@ PACKAGE_SPEC_HELP = (
|
|||
"package must look like " "<name>:<version>:<elf|shared-lib>:<source>"
|
||||
)
|
||||
|
||||
# Fixed square icon size nxstore renders app-list icons at (see
|
||||
# nxstore_load_icon() in system/nxstore/nxstore_main.c) - small enough to
|
||||
# stay a quick download over the board's WiFi, large enough to read as
|
||||
# actual artwork rather than a favicon.
|
||||
ICON_SIZE = 48
|
||||
|
||||
# Matches lv_image_header_t's own bit layout (LVGL v9, see
|
||||
# graphics/lvgl/lvgl/src/draw/lv_image_dsc.h): magic/cf/flags packed into
|
||||
# the first 4 bytes, then w/h, then stride/reserved - all little-endian,
|
||||
# 12 bytes total, immediately followed by raw pixel data. This is
|
||||
# deliberately the simplest format LVGL can render directly with no
|
||||
# decoder, since the board has no PNG/JPEG decode capability wired up.
|
||||
LV_IMAGE_HEADER_MAGIC = 0x19
|
||||
LV_COLOR_FORMAT_RGB565 = 0x12
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageSpec:
|
||||
|
|
@ -88,6 +104,24 @@ def parse_package_spec(value: str) -> PackageSpec:
|
|||
)
|
||||
|
||||
|
||||
def parse_name_value(value: str) -> tuple:
|
||||
parts = value.split("=", 1)
|
||||
if len(parts) != 2 or not parts[0] or not parts[1]:
|
||||
raise argparse.ArgumentTypeError("value must look like <name>=<text>")
|
||||
|
||||
return parts[0], parts[1]
|
||||
|
||||
|
||||
def parse_icon_spec(value: str) -> tuple:
|
||||
name, source = parse_name_value(value)
|
||||
source_path = Path(source).expanduser().resolve()
|
||||
if not source_path.is_file():
|
||||
msg = f"icon source does not exist: {source_path}"
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
|
||||
return name, source_path
|
||||
|
||||
|
||||
def artifact_relpath(arch: str, chip: str, compat: str,
|
||||
spec: PackageSpec) -> Path: # fmt: skip
|
||||
filename = spec.source.name
|
||||
|
|
@ -96,6 +130,47 @@ def artifact_relpath(arch: str, chip: str, compat: str,
|
|||
return path
|
||||
|
||||
|
||||
def icon_relpath(arch: str, chip: str, compat: str, name: str) -> Path:
|
||||
# Keep one repository object per package. nxstore keys its local cache
|
||||
# by package name and version, so a new catalog version fetches this
|
||||
# object again without requiring versioned repository duplication.
|
||||
return Path("icons") / arch / chip / compat / f"{name}.bin"
|
||||
|
||||
|
||||
def encode_icon_rgb565(source: Path, size: int = ICON_SIZE) -> bytes:
|
||||
"""Convert an arbitrary source image into nxstore's raw icon format:
|
||||
a 12-byte lv_image_header_t-compatible header (see the module
|
||||
docstring comment above) followed by size*size RGB565 pixel data.
|
||||
"""
|
||||
|
||||
from PIL import Image
|
||||
|
||||
with Image.open(source) as img:
|
||||
img = img.convert("RGB").resize((size, size), Image.Resampling.LANCZOS)
|
||||
pixels = list(img.getdata())
|
||||
|
||||
stride = size * 2
|
||||
header = struct.pack(
|
||||
"<BBHHHHH",
|
||||
LV_IMAGE_HEADER_MAGIC,
|
||||
LV_COLOR_FORMAT_RGB565,
|
||||
0, # flags
|
||||
size, # w
|
||||
size, # h
|
||||
stride,
|
||||
0, # reserved_2
|
||||
)
|
||||
|
||||
body = bytearray(stride * size)
|
||||
offset = 0
|
||||
for r, g, b in pixels:
|
||||
rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
|
||||
struct.pack_into("<H", body, offset, rgb565)
|
||||
offset += 2
|
||||
|
||||
return header + bytes(body)
|
||||
|
||||
|
||||
def package_identity(package: Dict[str, str]) -> tuple:
|
||||
return (
|
||||
package["name"],
|
||||
|
|
@ -171,6 +246,30 @@ def main() -> int:
|
|||
type=parse_package_spec,
|
||||
help=PACKAGE_SPEC_HELP,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--package-description",
|
||||
action="append",
|
||||
default=[],
|
||||
type=parse_name_value,
|
||||
metavar="NAME=TEXT",
|
||||
help="Optional package description, keyed by package name",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--package-category",
|
||||
action="append",
|
||||
default=[],
|
||||
type=parse_name_value,
|
||||
metavar="NAME=TEXT",
|
||||
help="Optional package category, keyed by package name",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--package-icon",
|
||||
action="append",
|
||||
default=[],
|
||||
type=parse_icon_spec,
|
||||
metavar="NAME=PATH",
|
||||
help="Optional source image for a package icon",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_dir = args.repo_dir.expanduser().resolve()
|
||||
|
|
@ -181,6 +280,9 @@ def main() -> int:
|
|||
for package in packages:
|
||||
packages_by_id[package_identity(package)] = package
|
||||
prefix = args.artifact_prefix.rstrip("/")
|
||||
descriptions = dict(args.package_description)
|
||||
categories = dict(args.package_category)
|
||||
icons = dict(args.package_icon)
|
||||
|
||||
for spec in args.package:
|
||||
relpath = artifact_relpath(args.arch, args.chip, args.compat, spec)
|
||||
|
|
@ -201,6 +303,26 @@ def main() -> int:
|
|||
"sha256": sha256_file(destination),
|
||||
"type": spec.payload_type,
|
||||
}
|
||||
|
||||
if spec.name in descriptions:
|
||||
package["description"] = descriptions[spec.name]
|
||||
|
||||
if spec.name in categories:
|
||||
package["category"] = categories[spec.name]
|
||||
|
||||
if spec.name in icons:
|
||||
icon_bytes = encode_icon_rgb565(icons[spec.name])
|
||||
icon_dest = repo_dir / icon_relpath(
|
||||
args.arch, args.chip, args.compat, spec.name
|
||||
)
|
||||
icon_dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
icon_dest.write_bytes(icon_bytes)
|
||||
|
||||
icon_rel = icon_relpath(
|
||||
args.arch, args.chip, args.compat, spec.name
|
||||
).as_posix()
|
||||
package["icon"] = f"{prefix}/{icon_rel}" if prefix else icon_rel
|
||||
|
||||
packages_by_id[package_identity(package)] = package
|
||||
|
||||
packages = sorted(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue