diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 42b33ea..191303c 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -22,20 +22,30 @@ jobs: compression-level: 6 custom-hostname: picompose + build-lva: + uses: ./.github/workflows/build-image-template.yml + with: + image-name: PiCompose_Linux-Voice-Assistant + stage-list: stage0 stage1 stage2 ./01-stage-picompose ./02-stage-audiodriver-other ./03-stage-linux-voice-assistant ./04-stage-finish + compression: zip + compression-level: 6 + custom-hostname: picompose # 2MICHAT-v1 build-2michat: + if: false # disabled uses: ./.github/workflows/build-image-template.yml with: - image-name: PiCompose-2MicHat + image-name: PiCompose-2MicHat-v1 stage-list: stage0 stage1 stage2 ./01-stage-picompose ./02-stage-audiodriver-2michat-v1 ./04-stage-finish compression: xz compression-level: 6 custom-hostname: picompose build-2michat-lva: + if: false # disabled uses: ./.github/workflows/build-image-template.yml with: - image-name: PiCompose_2MicHat_Linux-Voice-Assistant + image-name: PiCompose_2MicHat-v1_Linux-Voice-Assistant stage-list: stage0 stage1 stage2 ./01-stage-picompose ./02-stage-audiodriver-2michat-v1 ./03-stage-linux-voice-assistant ./04-stage-finish compression: xz compression-level: 6 @@ -54,7 +64,7 @@ jobs: build-respeaker_lite-lva: uses: ./.github/workflows/build-image-template.yml with: - image-name: PiCompose_Respeaker-lite_Linux-Voice-Assistant + image-name: PiCompose_Respeaker_lite_Linux-Voice-Assistant stage-list: stage0 stage1 stage2 ./01-stage-picompose ./02-stage-audiodriver-respeaker_lite ./03-stage-linux-voice-assistant ./04-stage-finish compression: xz compression-level: 6 @@ -63,4 +73,5 @@ jobs: # RPI IMAGER JSON generate-rpi-imager-json: needs: [build, build-2michat, build-respeaker_lite, build-2michat-lva, build-respeaker_lite-lva] + if: always() uses: ./.github/workflows/create-rpi-image-json.yml diff --git a/.github/workflows/build-image-template.yml b/.github/workflows/build-image-template.yml index cf69015..4d37ea6 100644 --- a/.github/workflows/build-image-template.yml +++ b/.github/workflows/build-image-template.yml @@ -31,6 +31,11 @@ on: type: boolean default: false description: "Enable rpi-imager.json snippet generation" + releaseversion: + required: false + type: string + default: "trixie" + description: "OS Release" jobs: build_images: @@ -84,7 +89,7 @@ jobs: stage-list: ${{ inputs.stage-list }} # Build configuration - release: trixie + release: ${{ inputs.releaseversion }} compression: ${{ inputs.compression }} compression-level: ${{ inputs.compression-level }} diff --git a/README.md b/README.md index 887f296..2865b40 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,12 @@ There is a seperated page for the supported hardware. You can find the link to i Here is a Overview for the specific images of each hardware: - -| Name | Hardware | What's in the Image? | -| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Base Image** | Your own hardware... | • Docker & Docker Compose (piCompose)
• Automatic Docker Compose deployment
• Pipewire Audio Server
• SSH enabled (pi User) | -| **[Sattelite1](docs/hardware_sattelite1.md)** | ReSpeaker Lite Board | • Base Image
• Satellite1 Hat Driver

Image is currently work in progress! | -| **[Sattelite1](docs/hardware_sattelite1.md)**
**+Linux-Voice-Assistant**
**+Snapcast** | ReSpeaker Lite Board | • Satellite1 Hat Image
• Linux-Voice-Assistant (OpenHomeFoundation)
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant

Image is currently work in progress! | -| **[ReSpeaker 2-Mic HAT v1](docs/hardware_2mic_v1.md)** | ReSpeaker 2-Mics Pi HAT | • Base Image
• Seeed Voicecard Driver | -| **[ReSpeaker 2-Mic HAT v1](docs/hardware_2mic_v1.md)**
**+Linux-Voice-Assistant**
**+Snapcast** | ReSpeaker 2-Mics Pi HAT | • 2-Mic HAT Image
• Linux-Voice-Assistant (OpenHomeFoundation)
• 2-Mic HAT GPIO LED Control
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant | -| **[ReSpeaker Lite](docs/hardware_respeaker_lite.md)** | ReSpeaker Lite Board | • Base Image
• Audio keep-alive service
• Workaround for connectivity issues in combination with the Pi Zero 2W.

There is a USB connectivity issue with the Pi Zero 2W. Use this board with at least Raspberry PI 3. | -| **[ReSpeaker Lite](docs/hardware_respeaker_lite.md)**
**+Linux-Voice-Assistant**
**+Snapcast** | ReSpeaker Lite Board | • ReSpeaker Lite Image
• Linux-Voice-Assistant (OpenHomeFoundation)
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant
• Workaround for connectivity issues in combination with the Pi Zero 2W.

There is a USB connectivity issue with the Pi Zero 2W. Use this board with at least Raspberry PI 3. | - - +| Name | Hardware | What's in the Image? | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Base Image** | If you use other hardware... | • Docker & Docker Compose (piCompose)
• Automatic Docker Compose deployment
• Pipewire Audio Server
• SSH enabled (pi User) | +| **[Satellite1](docs/hardware_sattelite1.md)** | ReSpeaker Lite Board | • * Everything from Base Image
• Satellite1 Hat Driver
• Linux-Voice-Assistant (OpenHomeFoundation)
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant

Image is currently work in progress! | +| **[ReSpeaker 2-Mic HAT v1](docs/hardware_2mic_v1.md)** | ReSpeaker 2-Mics Pi HAT | • * Everything from Base Image
• Seeed Voicecard Driver
• Linux-Voice-Assistant (OpenHomeFoundation)
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant

Current latest available build version is 1.1.1 See [#41](https://github.com/HinTak/seeed-voicecard/issues/41) | +| **[ReSpeaker Lite](docs/hardware_respeaker_lite.md)** | ReSpeaker Lite Board | • * Everything from Base Image
• Audio keep-alive service
• Linux-Voice-Assistant (OpenHomeFoundation)
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant
• Workaround for connectivity issues in combination with the Pi Zero 2W. ### Installation diff --git a/scripts/generate-imager-json.py b/scripts/generate-imager-json.py index 5590ff6..58fac9e 100755 --- a/scripts/generate-imager-json.py +++ b/scripts/generate-imager-json.py @@ -6,130 +6,229 @@ import sys import re import urllib.request +from collections import defaultdict OWNER = "florian-asche" REPO = "PiCompose" OUTPUT_FILE = "rpi-imager.json" API_URL = f"https://api.github.com/repos/{OWNER}/{REPO}/releases" -def main(): +HARDWARE_TYPES = ["Respeaker-lite", "2MicHat", "2MicHat-v1", "2MicHat-v2", "None"] + + +def fetch_releases(): + """Fetch releases from GitHub API.""" print("Fetching releases from GitHub...") - try: with urllib.request.urlopen(API_URL) as response: releases_data = json.loads(response.read().decode()) except Exception as e: print(f"Error: Failed to fetch releases - {e}") sys.exit(1) - print("Successfully fetched releases") + return releases_data + - # Get the latest version - LATEST_VERSION = releases_data[0]['tag_name'] - if LATEST_VERSION.startswith('v'): - LATEST_VERSION = LATEST_VERSION[1:] - print(f"Latest version: {LATEST_VERSION}") - - # Find the first release that has .xz or .zip files (this will be our "latest" with files) - latest_with_files_tag = None - for release in releases_data: - for asset in release['assets']: - if asset['name'].endswith('.xz') or asset['name'].endswith('.zip'): - latest_with_files_tag = release['tag_name'] - break - if latest_with_files_tag: - break - - # Collect all releases with .xz or .zip files +def get_latest_version(releases): + """Get the latest version tag from releases (only tags starting with 'v').""" + for release in releases: + tag = release['tag_name'] + if tag.startswith('v') and len(tag) > 1: + version = tag[1:] + return tag, version + return None, None + + +def find_release_by_tag(releases, tag): + """Find a specific release by tag name.""" + for release in releases: + if release['tag_name'] == tag: + return release + return None + + +def extract_image_files(release): + """Extract .xz and .zip image files from a release.""" + xz_files = [] + for asset in release['assets']: + if asset['name'].endswith('.xz') or asset['name'].endswith('.zip'): + xz_files.append({ + 'name': asset['name'], + 'url': asset['browser_download_url'] + }) + return xz_files + + +def get_releases_with_files(releases): + """Get all releases that have image files, excluding 'main' branch.""" releases_with_files = [] - for release in releases_data: + for release in releases: tag_name = release['tag_name'] - - xz_files = [] - for asset in release['assets']: - if asset['name'].endswith('.xz') or asset['name'].endswith('.zip'): - xz_files.append({ - 'name': asset['name'], - 'url': asset['browser_download_url'] - }) - - if xz_files and tag_name != "main": - # Format version name + files = extract_image_files(release) + if files and tag_name != "main": version = tag_name[1:] if tag_name.startswith('v') else tag_name - releases_with_files.append({ 'tag_name': tag_name, 'version': version, - 'files': xz_files + 'files': files }) + return releases_with_files - def format_image_name(filename): - """Extract clean image name from filename""" - # Remove .img.xz suffix - name = filename.replace('.img.xz', '') - # Remove date prefix (image_YYYY-MM-DD-) - name = re.sub(r'^image_\d{4}-\d{2}-\d{2}-?', '', name) - # Replace underscores and hyphens with spaces - name = name.replace('_', ' ').replace('-', ' ') - clean_name = name.strip() - # Add original filename in brackets for clarity - return f"{clean_name} ({filename})" - - def build_release_subitems(files): - """Build subitems list from files""" - # Umgekehrte Sortierung: neueste / letzte Datei zuerst anzeigen - reversed_files = reversed(files) - return [ - { - "name": format_image_name(f['name']), - "description": "PiCompose image", - "url": f['url'], - "init_format": "systemd", - "devices": ["pi5-64bit", "pi4-64bit", "pi3-64bit", "pi3-32bit"], - "capabilities": ["ssh", "wifi", "hostname", "locale"] - } - for f in reversed_files - ] - os_list = [] +def extract_metadata(filename): + """Extract date and hardware type from filename.""" + original = filename + name = filename.replace('.img.xz', '').replace('.zip', '') + + date_match = re.match(r'^image_(\d{4}-\d{2}-\d{2})', name) + date_prefix = date_match.group(1) if date_match else '' + name = re.sub(r'^image_\d{4}-\d{2}-\d{2}-?', '', name) + + hardware = "None" + + if 'Respeaker' in name and 'lite' in name.lower(): + hardware = "Respeaker-lite" + elif '2MicHat' in name: + if '2MicHat-v2' in name: + hardware = "2MicHat-v2" + else: + hardware = "2MicHat-v1" + + parts = name.split('_') + + return date_prefix, hardware, parts, original - # Find main branch release (nightly) - main_release = None - for release in releases_data: - if release['tag_name'] == "main": - main_xz_files = [] - for asset in release['assets']: - if asset['name'].endswith('.xz') or asset['name'].endswith('.zip'): - main_xz_files.append({ - 'name': asset['name'], - 'url': asset['browser_download_url'] - }) - if main_xz_files: - main_release = main_xz_files - break - - # PiCompose (Latest) - all files from the first stable release with .xz files - if releases_with_files: - latest = releases_with_files[0] - os_list.append({ - "name": "PiCompose (Latest)", - "description": "Latest stable PiCompose images", + +def format_image_name(parts, original): + """Format clean image name from parts.""" + clean_parts = [p.strip() for p in parts if p.strip()] + clean_name = ' - '.join(clean_parts) + return clean_name, original + + +def create_image_entry(filename, url): + """Create a single image entry for subitems.""" + date_prefix, hardware, parts, original = extract_metadata(filename) + clean_name, original = format_image_name(parts, original) + + return { + "name": clean_name, + "description": "PiCompose image", + "url": url, + "init_format": "systemd", + "devices": ["pi5-64bit", "pi4-64bit", "pi3-64bit", "pi3-32bit"], + "capabilities": ["ssh", "wifi", "hostname", "locale"] + } + + +def group_files_by_date_and_hardware(files): + """Group files by date and hardware type.""" + grouped = defaultdict(lambda: defaultdict(list)) + + for f in files: + date_prefix, hardware, parts, original = extract_metadata(f['name']) + + if date_prefix: + grouped[date_prefix][hardware].append(f) + + return grouped + + +def build_date_hardware_subitems(files): + """Build nested subitems: Date -> Hardware -> Images.""" + grouped = group_files_by_date_and_hardware(files) + + if not grouped: + return build_simple_subitems(files) + + date_items = [] + for date in sorted(grouped.keys(), reverse=True): + hardware_items = [] + + for hardware in grouped[date]: + image_entries = [ + create_image_entry(f['name'], f['url']) + for f in grouped[date][hardware] + ] + + hardware_items.append({ + "name": hardware, + "description": f"Hardware: {hardware}", + "icon": "icons/cat_raspberry_pi_os.png", + "subitems": image_entries + }) + + date_items.append({ + "name": date, + "description": f"Images from {date}", "icon": "icons/cat_raspberry_pi_os.png", - "subitems": build_release_subitems(latest['files']) + "subitems": hardware_items }) + + return date_items + + +def build_simple_subitems(files): + """Build simple subitems without date/hardware grouping.""" + return [ + create_image_entry(f['name'], f['url']) + for f in reversed(files) + ] + + +def get_main_release(releases): + """Get the main branch (nightly) release if available.""" + for release in releases: + if release['tag_name'] == "main": + files = extract_image_files(release) + if files: + return files + return None + + +def build_devices_list(): + """Build the devices list for rpi-imager.""" + return [ + {"name": "Raspberry Pi 5", "tags": ["pi5-64bit", "pi5-32bit"], "default": True, "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_5.png", "description": "Raspberry Pi 5, 500 / 500+, and Compute Module 5", "matching_type": "exclusive", "capabilities": []}, + {"name": "Raspberry Pi 4", "tags": ["pi4-64bit", "pi4-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_4.png", "description": "Raspberry Pi 4 Model B, 400, and Compute Module 4 / 4S", "matching_type": "inclusive", "capabilities": []}, + {"name": "Raspberry Pi 3", "tags": ["pi3-64bit", "pi3-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_3.png", "description": "Raspberry Pi 3 Model A+ / B / B+ and Compute Module 3 / 3+", "matching_type": "inclusive", "capabilities": []}, + {"name": "Raspberry Pi Zero 2 W", "tags": ["pi3-64bit", "pi3-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_Zero_2_W.png", "description": "Raspberry Pi Zero 2 W", "matching_type": "inclusive", "capabilities": []}, + {"name": "No filtering", "tags": [], "description": "Show every possible image", "matching_type": "inclusive", "capabilities": []} + ] + + +def main(): + releases = fetch_releases() + + latest_tag, latest_version = get_latest_version(releases) + print(f"Latest version: {latest_version}") + + releases_with_files = get_releases_with_files(releases) + main_release = get_main_release(releases) + + os_list = [] + + latest_release = find_release_by_tag(releases, latest_tag) + if latest_release: + latest_files = extract_image_files(latest_release) + if latest_files: + os_list.append({ + "name": "PiCompose - stable", + "description": "Latest stable tagged version images", + "icon": "icons/cat_raspberry_pi_os.png", + "subitems": build_date_hardware_subitems(latest_files) + }) - # PiCompose (Nightly) - files from main branch release if available if main_release: os_list.append({ - "name": "PiCompose (Nightly / Main)", - "description": "Latest development build from main branch", + "name": "PiCompose - development", + "description": "Latest development builds from main branch", "icon": "icons/cat_raspberry_pi_os.png", - "subitems": build_release_subitems(main_release) + "subitems": build_date_hardware_subitems(main_release) }) - # PiCompose (All Versions) - grouped by release os_list.append({ - "name": "PiCompose (All Versions)", + "name": "PiCompose - all versions / branches", "description": "All available PiCompose versions", "icon": "icons/cat_raspberry_pi_os.png", "subitems": [ @@ -137,31 +236,21 @@ def build_release_subitems(files): "name": r['version'], "description": f"PiCompose {r['tag_name']} release", "icon": "icons/cat_raspberry_pi_os.png", - "subitems": build_release_subitems(r['files']) + "subitems": build_date_hardware_subitems(r['files']) } for r in releases_with_files ] }) - # Build final JSON - devices = [ - {"name": "Raspberry Pi 5", "tags": ["pi5-64bit", "pi5-32bit"], "default": True, "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_5.png", "description": "Raspberry Pi 5, 500 / 500+, and Compute Module 5", "matching_type": "exclusive", "capabilities": []}, - {"name": "Raspberry Pi 4", "tags": ["pi4-64bit", "pi4-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_4.png", "description": "Raspberry Pi 4 Model B, 400, and Compute Module 4 / 4S", "matching_type": "inclusive", "capabilities": []}, - {"name": "Raspberry Pi 3", "tags": ["pi3-64bit", "pi3-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_3.png", "description": "Raspberry Pi 3 Model A+ / B / B+ and Compute Module 3 / 3+", "matching_type": "inclusive", "capabilities": []}, - {"name": "Raspberry Pi Zero 2 W", "tags": ["pi3-64bit", "pi3-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_Zero_2_W.png", "description": "Raspberry Pi Zero 2 W", "matching_type": "inclusive", "capabilities": []}, - {"name": "No filtering", "tags": [], "description": "Show every possible image", "matching_type": "inclusive", "capabilities": []} - ] - final_data = { "imager": { - "latest_version": LATEST_VERSION, + "latest_version": latest_version, "url": "https://www.raspberrypi.com/software/", - "devices": devices + "devices": build_devices_list() }, "os_list": os_list } - # Write output with open(OUTPUT_FILE, 'w') as f: json.dump(final_data, f, indent=2) @@ -169,11 +258,11 @@ def build_release_subitems(files): print("") print("Content preview:") print(json.dumps(final_data, indent=2)) - - # Count .xz files + total_xz = sum(len(r['files']) for r in releases_with_files) print(f"") print(f"Total .xz files: {total_xz}") + if __name__ == "__main__": - main() + main() \ No newline at end of file