From 891d7d16ce98a654ab90fac277ecbd7368eba203 Mon Sep 17 00:00:00 2001 From: Mike Rosseel Date: Mon, 25 May 2026 09:01:47 +0200 Subject: [PATCH 01/10] feat: NixOS migration support --- MIGRATION_BRANCH_STATE.md | 107 ++++ migration_gate.txt | 1 + python/PiFinder/solver_main.py | 1 + python/PiFinder/sys_utils.py | 86 +++- python/PiFinder/sys_utils_nixos.py | 591 +++++++++++++++++++++++ python/PiFinder/ui/preview.py | 1 + python/PiFinder/ui/software.py | 400 ++++++++++++++- python/PiFinder/utils.py | 2 +- python/noxfile.py | 7 +- python/pyproject.toml | 4 + python/scripts/migration_calc.py | 509 +++++++++++++++++++ python/scripts/migration_progress | Bin 0 -> 80440 bytes python/scripts/migration_progress.c | 522 ++++++++++++++++++++ python/scripts/nixos_migration.sh | 247 ++++++++++ python/scripts/nixos_migration_calc.py | 142 ++++++ python/scripts/nixos_migration_init.sh | 374 ++++++++++++++ python/scripts/test_migration_loopdev.sh | 498 +++++++++++++++++++ python/tests/test_software.py | 129 +++++ versions.json | 36 ++ 19 files changed, 3650 insertions(+), 7 deletions(-) create mode 100644 MIGRATION_BRANCH_STATE.md create mode 100644 migration_gate.txt create mode 100644 python/PiFinder/sys_utils_nixos.py create mode 100644 python/scripts/migration_calc.py create mode 100755 python/scripts/migration_progress create mode 100644 python/scripts/migration_progress.c create mode 100755 python/scripts/nixos_migration.sh create mode 100755 python/scripts/nixos_migration_calc.py create mode 100755 python/scripts/nixos_migration_init.sh create mode 100755 python/scripts/test_migration_loopdev.sh create mode 100644 python/tests/test_software.py create mode 100644 versions.json diff --git a/MIGRATION_BRANCH_STATE.md b/MIGRATION_BRANCH_STATE.md new file mode 100644 index 000000000..811339803 --- /dev/null +++ b/MIGRATION_BRANCH_STATE.md @@ -0,0 +1,107 @@ +# Migration Branch State + +Branch: `migration` + +## Overview + +This branch implements an in-place OS migration from Raspberry Pi OS to NixOS on PiFinder hardware (Pi 4, 2GB+ RAM, 16GB+ SD). The user triggers it from the OLED UI; the system downloads a NixOS bootstrap tarball, builds a custom initramfs, reboots into it, repartitions the SD card, and extracts NixOS — all without removing the SD card. + +The migration is gated behind a 7x square-button secret code on the SOFTWARE screen. The secret code directly triggers migration to v2.5.0 with hardcoded URL/SHA256. Regular users see the normal version.txt update flow (same as `main`). + +## Migration Flow + +``` +User presses 7x Square on SOFTWARE screen + │ + ▼ +UIMigrationConfirm (OLED: version, size, "IRREVERSIBLE" warning) + │ Confirm + ▼ +UIMigrationProgress (OLED: progress bar, status text) + │ Calls sys_utils.start_nixos_migration() + ▼ +nixos_migration.sh (Phase 1: RPi OS, runs as background bash) + ├─ Install deps (e2fsprogs, dosfstools, fdisk) + ├─ Pre-flight checks via nixos_migration_calc.py + │ (Pi4? RAM>=1800MB? SD>=16GB? WiFi=Client?) + ├─ Download tarball (349MB) with progress → JSON file + ├─ Verify SHA256 + ├─ Build initramfs: + │ busybox + e2fsck + resize2fs + mke2fs + mkfs.vfat + sfdisk + │ + migration_progress (OLED C binary) + init script + metadata + ├─ Stage initramfs to /boot + ├─ Set initramfs= in config.txt + └─ Reboot (5s countdown) + │ + ▼ +nixos_migration_init.sh (Phase 2: Initramfs, runs from RAM) + ├─ Save WiFi credentials to RAM (wpa_supplicant → iwd format) + ├─ e2fsck root + ├─ Shrink root FS + partition (resize2fs + sfdisk) + ├─ Copy tarball + PiFinder_data backup to freed staging area (raw dd) + ├─ === POINT OF NO RETURN === + ├─ Format boot (FAT32) + root (ext4) + ├─ Extract NixOS tarball to new root + ├─ Populate boot partition + ├─ Migrate WiFi to iwd format (early, before user data) + ├─ Write resume metadata to /var/lib/pifinder-migration/ + ├─ Restore PiFinder_data from staging + ├─ Expand partition to fill SD + └─ reboot -f → boots into bootstrap NixOS (Phase 3, not in this branch) +``` + +## Files + +### UI (python/PiFinder/ui/software.py) + +Simple `UISoftware` from `main` (version.txt checker, Update/Cancel toggle) plus: +- `_UNLOCK_SEQUENCE` / `_record_key()` / `key_square()` — 7x square triggers migration +- `UIMigrationConfirm` — warning screen with version info, size, irreversibility notice +- `UIMigrationProgress` — progress bar + scrollable status text, polls `sys_utils` +- `UIReleaseNotes` / `_strip_markdown()` — fetches and renders markdown release notes + +No manifest/channel infrastructure — that lives on the `nixos` branch. + +### Migration Scripts + +| File | Purpose | +|------|---------| +| `python/scripts/nixos_migration.sh` | Phase 1 (RPi OS): pre-flight, download, initramfs build, boot config, reboot | +| `python/scripts/nixos_migration_init.sh` | Phase 2 (initramfs): shrink/stage/format/extract/restore/expand | +| `python/scripts/nixos_migration_calc.py` | Pre-flight validator: Pi model, RAM, SD size, free space, WiFi mode | +| `python/scripts/migration_progress.c` | Standalone C OLED driver for initramfs (SSD1351 SPI, 5x7 font, progress bar) | +| `python/scripts/migration_progress` | Compiled aarch64 binary of above | + +### Other Modified Files + +| File | Change | +|------|--------| +| `python/PiFinder/sys_utils.py` | `start_nixos_migration()`, `get_migration_progress()` | +| `python/PiFinder/solver.py` | tetra3 path fix, `solution.pop()`, missing key guards | +| `python/PiFinder/utils.py` | `tetra3_dir` path correction | + +### Tests + +`python/tests/test_software.py` — `TestUpdateNeeded`, `TestUnlockSequence`, `TestStripMarkdown` + +## Key Constants + +| Constant | Value | +|----------|-------| +| Bootstrap tarball URL | `mrosseel/PiFinder` release `v2.5.0-bootstrap` | +| SHA256 | `d5e5dc7bfde57bb958d0dc55804af6fb14265f12d9e27a02da0385847f9ba742` | +| Tarball size | 349 MB | +| Staging area | 8 GB at end of SD card | +| Min RAM | 1800 MB (2GB Pi reports ~1849MB) | +| Min SD | 16 GB | +| Secret code | 7x square button | +| Progress file | `/tmp/nixos_migration_progress` (JSON: percent + status) | +| OLED binary | SSD1351 via SPI0.0, DC=GPIO24, RST=GPIO25, 128x128 BGR565 | + +## Architecture Notes + +- **Progress pipeline**: `nixos_migration.sh` writes JSON to progress file → `sys_utils.get_migration_progress()` reads it → `UIMigrationProgress.update()` polls it → renders on OLED +- **Initramfs OLED**: compiled C binary included in initramfs, called by init script at each stage +- **WiFi migration**: wpa_supplicant.conf parsed and converted to iwd format — done early in initramfs before user data restore so network recovery is possible if restore fails +- **Resume support**: metadata written to `/var/lib/pifinder-migration/` on new root so Phase 3 (bootstrap NixOS, not in this branch) can resume if interrupted +- **Data staging**: raw `dd` to write tarball + backup to freed space at end of SD (after shrinking root), then reads it back after formatting — avoids needing double the disk space diff --git a/migration_gate.txt b/migration_gate.txt new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/migration_gate.txt @@ -0,0 +1 @@ +0 diff --git a/python/PiFinder/solver_main.py b/python/PiFinder/solver_main.py index 5964e0e11..e74135a62 100644 --- a/python/PiFinder/solver_main.py +++ b/python/PiFinder/solver_main.py @@ -22,6 +22,7 @@ from PiFinder import utils sys.path.append(str(utils.tetra3_dir)) +sys.path.append(str(utils.tetra3_dir / "tetra3")) import tetra3 from tetra3 import cedar_detect_client diff --git a/python/PiFinder/sys_utils.py b/python/PiFinder/sys_utils.py index 6db15d5fa..e16aaafda 100644 --- a/python/PiFinder/sys_utils.py +++ b/python/PiFinder/sys_utils.py @@ -1,10 +1,12 @@ import glob +import json import re from typing import Dict, Any +import pam +import requests import sh from sh import wpa_cli, unzip, passwd -import pam import socket from PiFinder import utils @@ -408,3 +410,85 @@ def update_gpsd_config(baud_rate: int) -> None: except Exception as e: logger.error(f"SYS: Error updating GPSD config: {e}") raise + + +# --------------------------------------------------------------------------- +# NixOS migration +# --------------------------------------------------------------------------- + +MIGRATION_PROGRESS_FILE = "/tmp/nixos_migration_progress" +MIGRATION_SCRIPT = "/home/pifinder/PiFinder/python/scripts/nixos_migration.sh" + + +def _fetch_migration_sha256(version_info: dict) -> str: + """Fetch SHA256 from sidecar URL, falling back to hardcoded value.""" + sha256_url = version_info.get("migration_sha256_url", "") + if sha256_url: + try: + resp = requests.get(sha256_url, timeout=15) + if resp.status_code == 200: + sha256 = resp.text.strip().split()[0] + logger.info(f"SYS: Fetched migration SHA256: {sha256[:16]}...") + return sha256 + logger.warning(f"SYS: SHA256 fetch returned {resp.status_code}") + except requests.exceptions.RequestException as e: + logger.warning(f"SYS: Failed to fetch SHA256: {e}") + + sha256 = version_info.get("migration_sha256", "") + if sha256: + logger.info("SYS: Using hardcoded migration SHA256") + else: + logger.warning("SYS: No SHA256 available, checksum verification disabled") + return sha256 + + +def start_nixos_migration(version_info: dict) -> None: + """ + Start the NixOS migration process in the background. + """ + url = version_info.get("migration_url", "") + sha256 = _fetch_migration_sha256(version_info) + if not url: + raise ValueError("Missing migration_url") + + logger.info(f"SYS: Starting NixOS migration to {version_info.get('version', '?')}") + + with open(MIGRATION_PROGRESS_FILE, "w") as f: + json.dump({"percent": 0, "status": "Starting..."}, f) + + def _log_output(line): + logger.info(f"SYS: migration: {line.strip()}") + + def _log_error(line): + logger.error(f"SYS: migration: {line.strip()}") + + def _on_done(cmd, success, exit_code): + if not success: + logger.error(f"SYS: Migration script failed with exit code {exit_code}") + + try: + sh.bash( + MIGRATION_SCRIPT, + url, + sha256, + MIGRATION_PROGRESS_FILE, + _bg=True, + _bg_exc=False, + _out=_log_output, + _err=_log_error, + _done=_on_done, + ) + except Exception as e: + logger.error(f"SYS: Migration failed to start: {e}") + raise + + +def get_migration_progress() -> Dict[str, Any]: + """ + Read current migration progress from the progress file. + """ + try: + with open(MIGRATION_PROGRESS_FILE, "r") as f: + return json.load(f) + except (OSError, json.JSONDecodeError): + return {} diff --git a/python/PiFinder/sys_utils_nixos.py b/python/PiFinder/sys_utils_nixos.py new file mode 100644 index 000000000..5e9020393 --- /dev/null +++ b/python/PiFinder/sys_utils_nixos.py @@ -0,0 +1,591 @@ +""" +NixOS-native system utilities for PiFinder. + +Replaces sys_utils.py's wpa_supplicant/hostapd/file-editing approach with: +- NetworkManager GLib bindings (gi.repository.NM) for WiFi management +- python-pam for password verification +- D-Bus for hostname/reboot/shutdown +- stdlib zipfile for backup/restore +- nixos-rebuild for camera switching and software updates +""" +import glob +import os +import subprocess +import socket +import time +import zipfile +import logging +from pathlib import Path +from typing import Optional + +import requests + +import dbus +import pam +from PiFinder import utils + +import gi + +gi.require_version("NM", "1.0") +from gi.repository import GLib, NM # noqa: E402 + +BACKUP_PATH = str(utils.data_dir / "PiFinder_backup.zip") +AP_CONNECTION_NAME = "PiFinder-AP" + +logger = logging.getLogger("SysUtils.NixOS") + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + +def _run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess: + """Run a command, logging failures. Used only for nixos-rebuild and systemctl.""" + result = subprocess.run(cmd, capture_output=True, text=True, **kwargs) + if result.returncode != 0: + logger.error( + "Command %s failed (rc=%d): %s", + cmd, result.returncode, result.stderr.strip(), + ) + return result + + +def _nm_client() -> NM.Client: + """Create a NetworkManager client (synchronous).""" + return NM.Client.new(None) + + +def _nm_run_async(async_fn, *args): + """ + Run an async NM operation synchronously by spinning a local GLib MainLoop. + + Usage: + result = _nm_run_async(client.add_connection_async, profile, True, None) + """ + loop = GLib.MainLoop.new(None, False) + state = {"result": None, "error": None} + + def callback(source, async_result, _user_data): + try: + # The finish method name matches the async method name: + # add_connection_async -> add_connection_finish + # delete_async -> delete_finish + # activate_connection_async -> activate_connection_finish + # deactivate_connection_async -> deactivate_connection_finish + # commit_changes_async -> commit_changes_finish + method_name = async_fn.__name__.replace("_async", "_finish") + finish_fn = getattr(source, method_name) + state["result"] = finish_fn(async_result) + except Exception as e: + state["error"] = e + finally: + loop.quit() + + async_fn(*args, callback, None) + loop.run() + + if state["error"]: + raise state["error"] + return state["result"] + + +def _get_system_bus() -> dbus.SystemBus: + return dbus.SystemBus() + + +# --------------------------------------------------------------------------- +# Network class — WiFi management via NM GLib bindings +# --------------------------------------------------------------------------- + +class Network: + """ + Provides wifi network info via NetworkManager GLib bindings (libnm). + """ + + def __init__(self): + self._client = _nm_client() + self._wifi_networks: list[dict] = [] + self._wifi_mode = self._detect_wifi_mode() + self.populate_wifi_networks() + + def _detect_wifi_mode(self) -> str: + """Detect whether we're in AP or Client mode.""" + for ac in self._client.get_active_connections(): + if ac.get_id() == AP_CONNECTION_NAME: + return "AP" + return "Client" + + def populate_wifi_networks(self) -> None: + """Get saved WiFi connections from NetworkManager.""" + self._wifi_networks = [] + network_id = 0 + for conn in self._client.get_connections(): + s_wifi = conn.get_setting_wireless() + if s_wifi is None: + continue + if conn.get_id() == AP_CONNECTION_NAME: + continue + ssid_bytes = s_wifi.get_ssid() + ssid = ssid_bytes.get_data().decode("utf-8") if ssid_bytes else "" + self._wifi_networks.append({ + "id": network_id, + "ssid": ssid, + "psk": None, + "key_mgmt": "WPA-PSK", + }) + network_id += 1 + + def get_wifi_networks(self): + return self._wifi_networks + + def delete_wifi_network(self, network_id): + """Delete a saved WiFi connection.""" + if network_id < 0 or network_id >= len(self._wifi_networks): + logger.error("Invalid network_id: %d", network_id) + return + ssid = self._wifi_networks[network_id]["ssid"] + for conn in self._client.get_connections(): + if conn.get_id() == ssid: + try: + _nm_run_async(conn.delete_async, None) + except Exception as e: + logger.error("Failed to delete connection '%s': %s", ssid, e) + break + self.populate_wifi_networks() + + def add_wifi_network(self, ssid, key_mgmt, psk=None): + """Add and connect to a WiFi network.""" + profile = NM.SimpleConnection.new() + + s_con = NM.SettingConnection.new() + s_con.set_property(NM.SETTING_CONNECTION_ID, ssid) + s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless") + s_con.set_property(NM.SETTING_CONNECTION_AUTOCONNECT, True) + profile.add_setting(s_con) + + s_wifi = NM.SettingWireless.new() + s_wifi.set_property( + NM.SETTING_WIRELESS_SSID, + GLib.Bytes.new(ssid.encode("utf-8")), + ) + s_wifi.set_property(NM.SETTING_WIRELESS_MODE, "infrastructure") + profile.add_setting(s_wifi) + + if key_mgmt == "WPA-PSK" and psk: + s_wsec = NM.SettingWirelessSecurity.new() + s_wsec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk") + s_wsec.set_property(NM.SETTING_WIRELESS_SECURITY_PSK, psk) + profile.add_setting(s_wsec) + + s_ip4 = NM.SettingIP4Config.new() + s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto") + profile.add_setting(s_ip4) + + try: + _nm_run_async( + self._client.add_and_activate_connection_async, + profile, + self._client.get_device_by_iface("wlan0"), + None, + None, + ) + except Exception as e: + logger.error("Failed to add WiFi network '%s': %s", ssid, e) + + self.populate_wifi_networks() + + def get_ap_name(self) -> str: + """Get the current AP SSID from the PiFinder-AP profile.""" + for conn in self._client.get_connections(): + if conn.get_id() == AP_CONNECTION_NAME: + s_wifi = conn.get_setting_wireless() + if s_wifi: + ssid_bytes = s_wifi.get_ssid() + if ssid_bytes: + return ssid_bytes.get_data().decode("utf-8") + return "PiFinderAP" + + def set_ap_name(self, ap_name: str) -> None: + """Change the AP SSID.""" + if ap_name == self.get_ap_name(): + return + for conn in self._client.get_connections(): + if conn.get_id() == AP_CONNECTION_NAME: + s_wifi = conn.get_setting_wireless() + if s_wifi: + s_wifi.set_property( + NM.SETTING_WIRELESS_SSID, + GLib.Bytes.new(ap_name.encode("utf-8")), + ) + try: + _nm_run_async(conn.commit_changes_async, True, None) + except Exception as e: + logger.error("Failed to update AP SSID: %s", e) + return + + def get_host_name(self) -> str: + return socket.gethostname() + + def get_connected_ssid(self) -> str: + """Returns the SSID of the connected wifi network.""" + if self.wifi_mode() == "AP": + return "" + device = self._client.get_device_by_iface("wlan0") + if device is None: + return "" + ac = device.get_active_connection() + if ac is None: + return "" + conn = ac.get_connection() + if conn is None: + return "" + s_wifi = conn.get_setting_wireless() + if s_wifi is None: + return "" + ssid_bytes = s_wifi.get_ssid() + if ssid_bytes is None: + return "" + return ssid_bytes.get_data().decode("utf-8") + + def set_host_name(self, hostname: str) -> None: + """Set hostname via D-Bus to org.freedesktop.hostname1.""" + if hostname == self.get_host_name(): + return + try: + bus = _get_system_bus() + hostnamed = bus.get_object( + "org.freedesktop.hostname1", + "/org/freedesktop/hostname1", + ) + iface = dbus.Interface(hostnamed, "org.freedesktop.hostname1") + iface.SetStaticHostname(hostname, False) + except dbus.DBusException as e: + logger.error("Failed to set hostname via D-Bus: %s", e) + + def wifi_mode(self) -> str: + return self._wifi_mode + + def set_wifi_mode(self, mode: str) -> None: + if mode == self._wifi_mode: + return + if mode == "AP": + self._activate_connection(AP_CONNECTION_NAME) + elif mode == "Client": + self._deactivate_connection(AP_CONNECTION_NAME) + self._wifi_mode = mode + + def _activate_connection(self, name: str) -> None: + """Activate a saved connection by name.""" + conn = None + for c in self._client.get_connections(): + if c.get_id() == name: + conn = c + break + if conn is None: + logger.error("Connection '%s' not found", name) + return + device = self._client.get_device_by_iface("wlan0") + try: + _nm_run_async( + self._client.activate_connection_async, + conn, device, None, None, + ) + except Exception as e: + logger.error("Failed to activate '%s': %s", name, e) + + def _deactivate_connection(self, name: str) -> None: + """Deactivate an active connection by name.""" + for ac in self._client.get_active_connections(): + if ac.get_id() == name: + try: + _nm_run_async( + self._client.deactivate_connection_async, ac, None, + ) + except Exception as e: + logger.error("Failed to deactivate '%s': %s", name, e) + return + logger.warning("No active connection named '%s' to deactivate", name) + + def local_ip(self) -> str: + if self._wifi_mode == "AP": + return "10.10.10.1" + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("192.255.255.255", 1)) + ip = s.getsockname()[0] + except Exception: + ip = "NONE" + finally: + s.close() + return ip + + +# --------------------------------------------------------------------------- +# Backup / restore (stdlib zipfile) +# --------------------------------------------------------------------------- + +def remove_backup(): + """Removes backup file.""" + path = Path(BACKUP_PATH) + if path.exists(): + path.unlink() + + +def backup_userdata() -> str: + """ + Back up userdata to a single zip file. + + Backs up: + config.json + observations.db + obslists/* + """ + remove_backup() + + files = [ + utils.data_dir / "config.json", + utils.data_dir / "observations.db", + ] + for p in utils.data_dir.glob("obslists/*"): + files.append(p) + + with zipfile.ZipFile(BACKUP_PATH, "w", zipfile.ZIP_DEFLATED) as zf: + for filepath in files: + filepath = Path(filepath) + if filepath.exists(): + zf.write(filepath, filepath.relative_to("/")) + + return BACKUP_PATH + + +def restore_userdata(zip_path: str) -> None: + """ + Restore userdata from a zip backup. + OVERWRITES existing data! + """ + with zipfile.ZipFile(zip_path, "r") as zf: + zf.extractall("/") + + +# --------------------------------------------------------------------------- +# System control (systemctl subprocess + D-Bus for reboot/shutdown) +# --------------------------------------------------------------------------- + +def restart_pifinder() -> None: + """Restart the PiFinder service.""" + logger.info("SYS: Restarting PiFinder") + _run(["sudo", "systemctl", "restart", "pifinder"]) + + +def restart_system() -> None: + """Restart the system via D-Bus to login1.""" + logger.info("SYS: Initiating System Restart") + try: + bus = _get_system_bus() + login1 = bus.get_object( + "org.freedesktop.login1", + "/org/freedesktop/login1", + ) + manager = dbus.Interface(login1, "org.freedesktop.login1.Manager") + manager.Reboot(False) + except dbus.DBusException as e: + logger.error("D-Bus reboot failed, falling back to subprocess: %s", e) + _run(["sudo", "shutdown", "-r", "now"]) + + +def shutdown() -> None: + """Shut down the system via D-Bus to login1.""" + logger.info("SYS: Initiating Shutdown") + try: + bus = _get_system_bus() + login1 = bus.get_object( + "org.freedesktop.login1", + "/org/freedesktop/login1", + ) + manager = dbus.Interface(login1, "org.freedesktop.login1.Manager") + manager.PowerOff(False) + except dbus.DBusException as e: + logger.error("D-Bus shutdown failed, falling back to subprocess: %s", e) + _run(["sudo", "shutdown", "now"]) + + +# --------------------------------------------------------------------------- +# Software updates — async upgrade via systemd service +# --------------------------------------------------------------------------- + +UPGRADE_STATE_IDLE = "idle" +UPGRADE_STATE_RUNNING = "running" +UPGRADE_STATE_SUCCESS = "success" +UPGRADE_STATE_FAILED = "failed" + +VERSIONS_URL = ( + "https://raw.githubusercontent.com/mrosseel/PiFinder/release/versions.json" +) + +UPGRADE_REF_FILE = Path("/run/pifinder/upgrade-ref") + + +def fetch_version_manifest() -> Optional[dict]: + """Fetch the channel/version manifest from GitHub.""" + try: + resp = requests.get(VERSIONS_URL, timeout=10) + resp.raise_for_status() + return resp.json() + except Exception as e: + logger.error("Failed to fetch version manifest: %s", e) + return None + + +def get_versions_for_channel(channel: str) -> list[dict]: + """Get available versions for a channel. + + Returns list of {version, ref, date, notes}. + """ + manifest = fetch_version_manifest() + if manifest is None: + return [] + return manifest.get("channels", {}).get(channel, {}).get("versions", []) + + +def get_available_channels() -> list[str]: + """Get list of available channel names.""" + manifest = fetch_version_manifest() + if manifest is None: + return ["stable"] + return list(manifest.get("channels", {}).keys()) + + +def start_upgrade(ref: str = "release") -> bool: + """Start pifinder-upgrade.service with a specific git ref. + + Writes the ref to /run/pifinder/upgrade-ref for the service to read. + Returns True if the service was started successfully. + """ + try: + UPGRADE_REF_FILE.write_text(ref) + except OSError as e: + logger.error("Failed to write upgrade ref file: %s", e) + return False + + _run(["sudo", "systemctl", "reset-failed", "pifinder-upgrade.service"]) + result = _run([ + "sudo", "systemctl", "start", "--no-block", + "pifinder-upgrade.service", + ]) + return result.returncode == 0 + + +def get_upgrade_state() -> str: + """Poll upgrade service state.""" + result = _run(["systemctl", "is-active", "pifinder-upgrade.service"]) + status = result.stdout.strip() + if status == "activating": + return UPGRADE_STATE_RUNNING + elif status == "active": + return UPGRADE_STATE_SUCCESS + elif status == "failed": + return UPGRADE_STATE_FAILED + return UPGRADE_STATE_IDLE + + +def get_upgrade_log_tail(lines: int = 3) -> str: + """Last N lines from upgrade journal for UI display.""" + result = _run([ + "journalctl", "-u", "pifinder-upgrade.service", + "-n", str(lines), "--no-pager", "-o", "cat", + ]) + return result.stdout.strip() if result.returncode == 0 else "" + + +def update_software() -> bool: + """Blocking wrapper for backward compatibility (uses default ref).""" + if not start_upgrade(): + return False + while True: + time.sleep(10) + state = get_upgrade_state() + if state == UPGRADE_STATE_SUCCESS: + return True + elif state == UPGRADE_STATE_FAILED: + return False + + +# --------------------------------------------------------------------------- +# Password management (python-pam + chpasswd) +# --------------------------------------------------------------------------- + +def verify_password(username: str, password: str) -> bool: + """Verify a password against PAM.""" + p = pam.pam() + return p.authenticate(username, password, service="login") + + +def change_password(username: str, current_password: str, new_password: str) -> bool: + """Change the user password via chpasswd.""" + if not verify_password(username, current_password): + return False + result = subprocess.run( + ["sudo", "chpasswd"], + input=f"{username}:{new_password}\n", + capture_output=True, + text=True, + ) + return result.returncode == 0 + + +# --------------------------------------------------------------------------- +# Camera switching (nixos-rebuild + reboot) +# --------------------------------------------------------------------------- + +def switch_camera(cam_type: str) -> None: + """ + Switch camera by rebuilding NixOS with the appropriate camera type. + Requires reboot (dtoverlay change). + """ + logger.info("SYS: Switching camera to %s via nixos-rebuild", cam_type) + flake_path = str(utils.home_dir / "PiFinder") + result = _run([ + "sudo", "nixos-rebuild", "switch", + "--flake", f"{flake_path}#pifinder-{cam_type}", + ]) + if result.returncode == 0: + restart_system() + else: + logger.error("SYS: Camera switch rebuild failed: %s", result.stderr) + + +def switch_cam_imx477() -> None: + logger.info("SYS: Switching cam to imx477") + switch_camera("imx477") + + +def switch_cam_imx296() -> None: + logger.info("SYS: Switching cam to imx296") + switch_camera("imx296") + + +def switch_cam_imx462() -> None: + logger.info("SYS: Switching cam to imx462") + switch_camera("imx462") + + +# --------------------------------------------------------------------------- +# GPSD config (declarative on NixOS — no-ops) +# --------------------------------------------------------------------------- + +def check_and_sync_gpsd_config(baud_rate: int) -> bool: + """ + On NixOS, GPSD config is managed declaratively via services.nix. + This is a no-op. + """ + logger.info( + "SYS: GPSD baud rate %d — managed by NixOS configuration", baud_rate + ) + return False + + +def update_gpsd_config(baud_rate: int) -> None: + """On NixOS, GPSD configuration is declarative. This is a no-op.""" + logger.info( + "SYS: GPSD config is managed declaratively on NixOS (baud=%d)", baud_rate + ) diff --git a/python/PiFinder/ui/preview.py b/python/PiFinder/ui/preview.py index bac4ce300..d47689664 100644 --- a/python/PiFinder/ui/preview.py +++ b/python/PiFinder/ui/preview.py @@ -20,6 +20,7 @@ from PiFinder.ui.ui_utils import outline_text sys.path.append(str(utils.tetra3_dir)) +sys.path.append(str(utils.tetra3_dir / "tetra3")) class UIPreview(UIModule): diff --git a/python/PiFinder/ui/software.py b/python/PiFinder/ui/software.py index fe92660a3..8b76985a0 100644 --- a/python/PiFinder/ui/software.py +++ b/python/PiFinder/ui/software.py @@ -1,16 +1,44 @@ #!/usr/bin/python # -*- coding:utf-8 -*- """ -This module contains all the UI Module classes - +This module contains the UI Module classes for +software updates and NixOS migration. """ +import logging +import time + import requests from PiFinder import utils from PiFinder.ui.base import UIModule +from PiFinder.ui.ui_utils import TextLayouter sys_utils = utils.get_sys_utils() +logger = logging.getLogger("UISoftware") + +REQUEST_TIMEOUT = 10 +MIGRATION_GATE_URL = "https://raw.githubusercontent.com/brickbots/PiFinder/release/migration_gate.txt" + +# Secret unlock: 7x square button +_UNLOCK_SEQUENCE = ["square"] * 7 + +_MIGRATION_VERSION_INFO = { + "version": "2.5.0", + "type": "upgrade", + "migration_url": "https://github.com/mrosseel/PiFinder/releases/download/v2.5.0-migration/pifinder-nixos-v2.5.0.tar.zst", + "migration_size_mb": 292, + "migration_sha256_url": "https://github.com/mrosseel/PiFinder/releases/download/v2.5.0-migration/pifinder-nixos-v2.5.0.tar.zst.sha256", +} + + +def _fetch_migration_gate() -> bool: + """Check remote gate file. Returns True only if content is '1'.""" + try: + res = requests.get(MIGRATION_GATE_URL, timeout=REQUEST_TIMEOUT) + return res.status_code == 200 and res.text.strip() == "1" + except requests.exceptions.RequestException: + return False def update_needed(current_version: str, repo_version: str) -> bool: @@ -45,7 +73,8 @@ def update_needed(current_version: str, repo_version: str) -> bool: class UISoftware(UIModule): """ - UI for updating software versions + UI for updating software versions. + Includes secret 7x square unlock to trigger NixOS migration. """ __title__ = "SOFTWARE" @@ -64,11 +93,39 @@ def __init__(self, *args, **kwargs) -> None: self._go_for_update = False self._option_select = "Update" + # Unlock sequence tracking (7x square triggers migration) + self._key_buffer: list = [] + + def _record_key(self, key_name: str): + """Record a key press for unlock sequence detection.""" + self._key_buffer.append(key_name) + if len(self._key_buffer) > len(_UNLOCK_SEQUENCE): + self._key_buffer = self._key_buffer[-len(_UNLOCK_SEQUENCE) :] + if self._key_buffer == _UNLOCK_SEQUENCE: + self._key_buffer = [] + self._trigger_migration() + + def _trigger_migration(self): + """Push UIMigrationConfirm onto the UI stack.""" + self.message("System Upgrade", 1) + self.add_to_stack( + { + "class": UIMigrationConfirm, + "version_info": _MIGRATION_VERSION_INFO, + "current_version": self._software_version.strip(), + } + ) + def get_release_version(self): """ Fetches current release version from - github, sets class variable if found + github, sets class variable if found. + Also checks the remote migration gate. """ + if _fetch_migration_gate(): + self._trigger_migration() + return + try: res = requests.get( "https://raw.githubusercontent.com/brickbots/PiFinder/release/version.txt" @@ -224,6 +281,9 @@ def toggle_option(self): else: self._option_select = "Update" + def key_square(self): + self._record_key("square") + def key_up(self): self.toggle_option() @@ -235,3 +295,335 @@ def key_right(self): self.remove_from_stack() else: self.update_software() + + +class UIMigrationConfirm(UIModule): + """ + Warning screen before initiating NixOS migration. + Shows version info, warns about irreversibility, requires confirmation. + """ + + __title__ = "UPGRADE" + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._version_info = self.item_definition.get("version_info", {}) + self._current_version = self.item_definition.get("current_version", "?") + self._target_version = self._version_info.get("version", "?") + self._option_index = 0 + self._options = [_("Confirm"), _("Cancel")] + + def update(self, force=False): + time.sleep(1 / 30) + self.clear_screen() + y = self.display_class.titlebar_height + 2 + + self.draw.text( + (0, y), + _("Major Upgrade"), + font=self.fonts.bold.font, + fill=self.colors.get(255), + ) + y += 14 + + self.draw.text( + (5, y), + f"{self._current_version} -> {self._target_version}", + font=self.fonts.bold.font, + fill=self.colors.get(192), + ) + y += 16 + + # Separator + self.draw.line([(0, y), (127, y)], fill=self.colors.get(64)) + y += 4 + + self.draw.text( + (0, y), + _("IRREVERSIBLE"), + font=self.fonts.bold.font, + fill=self.colors.get(255), + ) + y += 12 + + size_mb = self._version_info.get("migration_size_mb", "?") + self.draw.text( + (0, y), + _("Download: {}MB").format(size_mb), + font=self.fonts.base.font, + fill=self.colors.get(128), + ) + y += 11 + + self.draw.text( + (0, y), + _("Power + WiFi req"), + font=self.fonts.base.font, + fill=self.colors.get(128), + ) + y += 11 + + if not self._version_info.get("migration_sha256_url") and not self._version_info.get("migration_sha256"): + self.draw.text( + (0, y), + _("No checksum avail."), + font=self.fonts.base.font, + fill=self.colors.get(128), + ) + y += 11 + + y += 5 + + # Options + for i, label in enumerate(self._options): + oy = y + i * 12 + self.draw.text( + (10, oy), + label, + font=self.fonts.bold.font, + fill=self.colors.get(255), + ) + if i == self._option_index: + self.draw.text( + (0, oy), + self._RIGHT_ARROW, + font=self.fonts.bold.font, + fill=self.colors.get(255), + ) + + return self.screen_update() + + def key_up(self): + self._option_index = (self._option_index - 1) % len(self._options) + + def key_down(self): + self._option_index = (self._option_index + 1) % len(self._options) + + def key_left(self): + return True + + def key_right(self): + if self._options[self._option_index] == _("Cancel"): + self.remove_from_stack() + elif self._options[self._option_index] == _("Confirm"): + self.add_to_stack( + { + "class": UIMigrationProgress, + "version_info": self._version_info, + } + ) + + +class UIMigrationProgress(UIModule): + """ + Migration download and preparation progress screen. + Triggers the actual migration via sys_utils. + """ + + __title__ = "UPGRADE" + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._version_info = self.item_definition.get("version_info", {}) + self._started = False + self._status = _("Starting...") + self._progress = 0 + self._status_layout = TextLayouter( + "", + draw=self.draw, + color=self.colors.get(255), + colors=self.colors, + font=self.fonts.base, + available_lines=4, + ) + + def active(self): + super().active() + if not self._started: + self._started = True + self._start_migration() + + def _start_migration(self): + """Kick off the migration process in the background.""" + self._status = _("Downloading...") + try: + sys_utils.start_nixos_migration(self._version_info) + except AttributeError: + logger.error("sys_utils.start_nixos_migration not available") + self._status = _("Not supported") + except Exception as e: + logger.error(f"Migration failed to start: {e}") + self._status = _("Failed") + + def update(self, force=False): + time.sleep(1 / 30) + self.clear_screen() + y = self.display_class.titlebar_height + 2 + + # Try to read progress from sys_utils + try: + progress = sys_utils.get_migration_progress() + if progress: + self._progress = progress.get("percent", self._progress) + new_status = progress.get("status", self._status) + if new_status != self._status: + self._status = new_status + self._status_layout.set_text(self._status) + except (AttributeError, Exception): + pass + + self.draw.text( + (0, y), + _("System Upgrade"), + font=self.fonts.bold.font, + fill=self.colors.get(255), + ) + y += 20 + + # Progress bar + bar_x, bar_w, bar_h = 4, 120, 12 + self.draw.rectangle( + [bar_x, y, bar_x + bar_w, y + bar_h], + outline=self.colors.get(64), + ) + fill_w = int(bar_w * self._progress / 100) + if fill_w > 0: + self.draw.rectangle( + [bar_x + 1, y + 1, bar_x + fill_w, y + bar_h - 1], + fill=self.colors.get(255), + ) + pct_text = f"{self._progress}%" + pct_bbox = self.fonts.base.font.getbbox(pct_text) + pct_w = pct_bbox[2] - pct_bbox[0] + pct_h = pct_bbox[3] - pct_bbox[1] + pct_x = bar_x + (bar_w - pct_w) // 2 + pct_y = y + (bar_h - pct_h) // 2 - pct_bbox[1] + self.draw.text( + (pct_x, pct_y), + pct_text, + font=self.fonts.base.font, + fill=self.colors.get(0) if self._progress > 45 else self.colors.get(192), + ) + y += bar_h + 4 + + # Use TextLayouter for scrollable status text + self._status_layout.draw((0, y)) + + return self.screen_update() + + def key_up(self): + self._status_layout.previous() + + def key_down(self): + self._status_layout.next() + + def key_left(self): + # No going back during migration + return False + + +class UIReleaseNotes(UIModule): + """ + Scrollable release notes viewer. + Fetches markdown from a URL and displays as plain text. + """ + + __title__ = "NOTES" + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._notes_url = self.item_definition.get("notes_url", "") + self._loaded = False + self._error = False + self._text_layout = TextLayouter( + "", + draw=self.draw, + color=self.colors.get(255), + colors=self.colors, + font=self.fonts.base, + available_lines=9, + ) + + def active(self): + super().active() + if not self._loaded: + self._fetch_notes() + + def _fetch_notes(self): + """Fetch release notes from the configured URL.""" + try: + res = requests.get(self._notes_url, timeout=REQUEST_TIMEOUT) + if res.status_code == 200: + text = _strip_markdown(res.text) + self._text_layout.set_text(text) + self._loaded = True + else: + self._error = True + logger.warning( + f"Failed to fetch release notes: HTTP {res.status_code}" + ) + except requests.exceptions.RequestException as e: + self._error = True + logger.warning(f"Failed to fetch release notes: {e}") + + def update(self, force=False): + time.sleep(1 / 30) + self.clear_screen() + draw_pos = self.display_class.titlebar_height + 2 + + if self._error: + self.draw.text( + (10, draw_pos + 20), + _("Could not load"), + font=self.fonts.large.font, + fill=self.colors.get(255), + ) + self.draw.text( + (10, draw_pos + 35), + _("release notes"), + font=self.fonts.large.font, + fill=self.colors.get(255), + ) + return self.screen_update() + + if not self._loaded: + self.draw.text( + (10, draw_pos + 20), + _("Loading..."), + font=self.fonts.large.font, + fill=self.colors.get(255), + ) + return self.screen_update() + + self._text_layout.draw((0, draw_pos)) + return self.screen_update() + + def key_down(self): + self._text_layout.next() + + def key_up(self): + self._text_layout.previous() + + def key_left(self): + return True + + +def _strip_markdown(text: str) -> str: + """ + Minimal markdown stripping for plain-text display on OLED. + Removes common markdown syntax while keeping readable text. + """ + lines = [] + for line in text.splitlines(): + stripped = line.lstrip("#").strip() + stripped = stripped.replace("**", "").replace("__", "") + stripped = stripped.replace("*", "").replace("_", "") + while "[" in stripped and "](" in stripped: + start = stripped.index("[") + mid = stripped.index("](", start) + end = stripped.index(")", mid) + link_text = stripped[start + 1 : mid] + stripped = stripped[:start] + link_text + stripped[end + 1 :] + stripped = stripped.replace("`", "") + lines.append(stripped) + return "\n".join(lines) diff --git a/python/PiFinder/utils.py b/python/PiFinder/utils.py index 523228537..faa5d5d49 100644 --- a/python/PiFinder/utils.py +++ b/python/PiFinder/utils.py @@ -10,7 +10,7 @@ cwd_dir = Path.cwd() pifinder_dir = Path("..") astro_data_dir = cwd_dir / pifinder_dir / "astro_data" -tetra3_dir = pifinder_dir / "python/PiFinder/tetra3/tetra3" +tetra3_dir = pifinder_dir / "python/PiFinder/tetra3" data_dir = Path(Path.home(), "PiFinder_data") pifinder_db = astro_data_dir / "pifinder_objects.db" observations_db = data_dir / "observations.db" diff --git a/python/noxfile.py b/python/noxfile.py index d5d17eed0..86eb7291d 100644 --- a/python/noxfile.py +++ b/python/noxfile.py @@ -46,7 +46,12 @@ def type_hints(session: nox.Session) -> None: """ session.install("-r", "requirements.txt") session.install("-r", "requirements_dev.txt") - session.run("mypy", "--install-types", "--non-interactive", ".") + # First run populates the cache so --install-types knows what stubs are needed. + # Targets PiFinder/ explicitly to avoid broken tetra3 symlink in the tree. + session.run("mypy", "PiFinder", success_codes=[0, 1]) + session.run( + "mypy", "--install-types", "--non-interactive", "PiFinder", success_codes=[0, 1] + ) @nox.session(reuse_venv=True, python="3.9") diff --git a/python/pyproject.toml b/python/pyproject.toml index 7430f3c0f..91c93f565 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -135,6 +135,10 @@ module = [ 'picamera2', 'bottle', 'libinput', + 'dbus', + 'gi', + 'gi.repository', + 'gi.repository.*', ] ignore_missing_imports = true ignore_errors = true diff --git a/python/scripts/migration_calc.py b/python/scripts/migration_calc.py new file mode 100644 index 000000000..b9404d5dd --- /dev/null +++ b/python/scripts/migration_calc.py @@ -0,0 +1,509 @@ +#!/usr/bin/env python3 +"""PiFinder A/B Migration - Pre-flight Validation and Configuration + +Validates the system for A/B partition migration and computes all +parameters needed by the initramfs migration script. Outputs a +shell-sourceable config file. + +Must be run as root on the target Raspberry Pi. + +Usage: + sudo python3 migration_calc.py --output /tmp/migration_config.sh + sudo python3 migration_calc.py --json +""" + +import argparse +import json +import os +import shutil +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import List, Tuple + + +# ── Partition Layout Constants ────────────────────────────────────── +# +# 4-partition MBR layout (all primary): +# p1: boot (FAT32, 256 MiB) shared boot with os_prefix A/B +# p2: root-a (ext4, 3584 MiB) active root +# p3: root-b (ext4, 3584 MiB) update root +# p4: data (ext4, remaining) user data (PiFinder_data) +# +# A/B switching uses os_prefix in autoboot.txt with tryboot. +# Boot partition contains a/ and b/ subdirectories, each with their +# own cmdline.txt pointing to p2 or p3 respectively. +# +# All sizes in MiB unless noted. Sector size is 512 bytes. +# 1 MiB = 2048 sectors. + +BOOT_MIB = 256 +ROOT_MIB = 3584 # 3.5 GiB +SECTORS_PER_MIB = 2048 + +# Partition table in sectors (512 bytes each) +# fmt: off +P1_START_S = 8192 # 4 MiB boot (RPi OS standard) +P1_SIZE_S = BOOT_MIB * SECTORS_PER_MIB +P2_START_S = P1_START_S + P1_SIZE_S # 260 MiB root-a +P2_SIZE_S = ROOT_MIB * SECTORS_PER_MIB +P3_START_S = P2_START_S + P2_SIZE_S # 3844 MiB root-b +P3_SIZE_S = ROOT_MIB * SECTORS_PER_MIB +P4_START_S = P3_START_S + P3_SIZE_S # 7428 MiB data +# fmt: on + +# The data partition (p4) starts here; everything before is fixed layout +FIXED_LAYOUT_END_MIB = P4_START_S // SECTORS_PER_MIB # 7428 MiB + +# Safety thresholds +MIN_SD_SIZE_GIB = 16 +MIN_FREE_SPACE_MIB = 1024 +MIN_BOOT_FREE_MIB = 10 +MIN_RAM_MIB = 500 +SHRINK_HEADROOM_MIB = 200 # Extra space kept when shrinking filesystem +BACKUP_END_BUFFER_MIB = 64 # Reserved at very end of SD card +USER_BACKUP_OVERHEAD = 1.05 # 5% overhead for tar headers (no compression) + +# sfdisk partition table template (sector values filled in) +SFDISK_LAYOUT = f"""\ +label: dos + +/dev/mmcblk0p1 : start={P1_START_S}, size={P1_SIZE_S}, type=c, bootable +/dev/mmcblk0p2 : start={P2_START_S}, size={P2_SIZE_S}, type=83 +/dev/mmcblk0p3 : start={P3_START_S}, size={P3_SIZE_S}, type=83 +/dev/mmcblk0p4 : start={P4_START_S}, type=83 +""" + + +@dataclass +class ValidationResult: + errors: List[str] = field(default_factory=list) + warnings: List[str] = field(default_factory=list) + checks: List[str] = field(default_factory=list) + config: dict = field(default_factory=dict) + + @property + def ok(self) -> bool: + return len(self.errors) == 0 + + +def _run(cmd: list) -> Tuple[str, int]: + """Run shell command, return (stdout, returncode).""" + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + return result.stdout.strip(), result.returncode + except (subprocess.TimeoutExpired, FileNotFoundError) as e: + return str(e), 1 + + +def _check_raspberry_pi(r: ValidationResult) -> bool: + """Check 1: Verify running on Raspberry Pi.""" + model_path = Path("/proc/device-tree/model") + if not model_path.exists(): + r.errors.append("Not running on Raspberry Pi") + return False + model = model_path.read_text().strip("\x00") + r.checks.append(f"Detected: {model}") + r.config["pi_model"] = model + return True + + +def _check_sd_card(r: ValidationResult, device: str) -> bool: + """Check 2: Verify SD card device exists.""" + if not os.path.exists(device): + r.errors.append(f"{device} not found") + return False + r.checks.append(f"SD card device: {device}") + return True + + +def _check_root_partition(r: ValidationResult, device: str) -> bool: + """Check 3: Verify root is on the expected partition.""" + root_src, _ = _run(["findmnt", "-n", "-o", "SOURCE", "/"]) + expected = f"{device}p2" + if root_src != expected: + r.errors.append( + f"Root not on {expected} (found: {root_src}). " + "Migration only works on standard SD card layout." + ) + return False + r.checks.append(f"Root filesystem: {root_src}") + return True + + +def _check_sd_size(r: ValidationResult, device: str) -> bool: + """Check 4: Verify SD card is large enough.""" + size_str, rc = _run(["blockdev", "--getsize64", device]) + if rc != 0: + r.errors.append("Cannot read SD card size") + return False + + sd_bytes = int(size_str) + sd_mib = sd_bytes // (1024 * 1024) + sd_gib = sd_bytes / (1024**3) + + if sd_gib < MIN_SD_SIZE_GIB: + r.errors.append( + f"SD card too small: {sd_gib:.1f} GiB (need {MIN_SD_SIZE_GIB} GiB+)" + ) + return False + + r.checks.append(f"SD card size: {sd_gib:.1f} GiB ({sd_mib} MiB)") + r.config["sd_size_bytes"] = sd_bytes + r.config["sd_size_mib"] = sd_mib + return True + + +def _check_free_space(r: ValidationResult) -> bool: + """Check 5: Verify enough free space on root. + + Excludes /home/pifinder/PiFinder_data from usage calculation since + it will be moved to the separate data partition during migration. + """ + st = os.statvfs("/") + free_mib = (st.f_bavail * st.f_frsize) // (1024 * 1024) + total_used_mib = ((st.f_blocks - st.f_bfree) * st.f_frsize) // (1024 * 1024) + + # Subtract PiFinder_data since it moves to data partition + pifinder_data = Path("/home/pifinder/PiFinder_data") + data_mib = 0 + if pifinder_data.exists(): + out, rc = _run(["du", "-sm", str(pifinder_data)]) + if rc == 0: + try: + data_mib = int(out.split()[0]) + except (ValueError, IndexError): + pass + + used_mib = total_used_mib - data_mib + + if free_mib < MIN_FREE_SPACE_MIB: + r.errors.append( + f"Insufficient free space: {free_mib} MiB (need {MIN_FREE_SPACE_MIB} MiB)" + ) + return False + + r.checks.append( + f"Root usage: {used_mib} MiB (excluding {data_mib} MiB PiFinder_data)" + ) + r.config["root_used_mib"] = used_mib + r.config["root_free_mib"] = free_mib + r.config["pifinder_data_mib"] = data_mib + return True + + +def _check_root_fits(r: ValidationResult) -> bool: + """Check 6: Verify current root usage fits in new partition.""" + used_mib = r.config.get("root_used_mib", 0) + max_allowed = ROOT_MIB - SHRINK_HEADROOM_MIB + + if used_mib > max_allowed: + r.errors.append( + f"Root uses {used_mib} MiB but new root partition is {ROOT_MIB} MiB " + f"(max usable: {max_allowed} MiB with {SHRINK_HEADROOM_MIB} MiB headroom)" + ) + return False + + r.checks.append(f"Root usage ({used_mib} MiB) fits in {ROOT_MIB} MiB partition") + return True + + +def _check_sd_health(r: ValidationResult, device: str) -> bool: + """Check 7: Verify SD card is not failing.""" + try: + with open("/proc/mounts") as f: + for line in f: + if f"{device}p2" in line and ",ro," in line: + r.errors.append("SD card is mounted read-only (possibly failing)") + return False + except OSError: + pass + + dmesg_out, _ = _run(["dmesg"]) + io_errors = [ + line.strip() + for line in dmesg_out.split("\n") + if "mmcblk0" in line.lower() + and ("error" in line.lower() or "fail" in line.lower()) + ] + if io_errors: + r.warnings.append("SD card showing I/O errors in dmesg:") + for line in io_errors[-3:]: + r.warnings.append(f" {line}") + else: + r.checks.append("SD card health: OK") + return True + + +def _check_required_tools(r: ValidationResult) -> bool: + """Check 8: Verify all required tools are installed.""" + required = [ + "sfdisk", + "dd", + "mkfs.ext4", + "e2fsck", + "resize2fs", + "dumpe2fs", + "cpio", + "gzip", + "zstd", + "blkid", + "md5sum", + "partprobe", + ] + missing = [t for t in required if not shutil.which(t)] + if missing: + r.errors.append(f"Missing required tools: {', '.join(missing)}") + return False + r.checks.append("Required tools: all present") + return True + + +def _check_boot_partition(r: ValidationResult) -> bool: + """Check 9: Verify boot partition is accessible with enough space.""" + # Detect boot mount point (Bullseye: /boot, Bookworm: /boot/firmware) + for boot_dir in ("/boot/firmware", "/boot"): + boot_src, rc = _run(["findmnt", "-n", "-o", "SOURCE", boot_dir]) + if rc == 0 and boot_src: + break + else: + r.errors.append("Boot partition is not mounted at /boot or /boot/firmware") + return False + + try: + st = os.statvfs(boot_dir) + free_mib = (st.f_bavail * st.f_frsize) // (1024 * 1024) + except OSError: + r.errors.append(f"Cannot stat {boot_dir}") + return False + + if free_mib < MIN_BOOT_FREE_MIB: + r.errors.append( + f"Insufficient space in {boot_dir}: {free_mib} MiB " + f"(need {MIN_BOOT_FREE_MIB} MiB)" + ) + return False + + r.checks.append(f"Boot partition: {boot_src} at {boot_dir} ({free_mib} MiB free)") + r.config["boot_dir"] = boot_dir + return True + + +def _check_system_state(r: ValidationResult) -> bool: + """Check 10: Verify system is not degraded.""" + state, _ = _run(["systemctl", "is-system-running"]) + if state in ("degraded", "maintenance"): + r.warnings.append( + f"System is in '{state}' state; some services may have failed" + ) + else: + r.checks.append(f"System state: {state}") + return True + + +def _compute_backup_params(r: ValidationResult, essential_only: bool = False) -> bool: + """Compute backup offsets and validate they fit on the SD card. + + Backup layout at END of SD card (working backwards from end): + [...partitions...] [root_backup] [user_data_tar] [64 MiB buffer] + + No boot backup needed — boot partition (p1) is unchanged by migration. + User data is moved (tar to raw SD, delete from root) so only one + copy exists at a time. + """ + sd_mib = r.config["sd_size_mib"] + used_mib = r.config["root_used_mib"] + + # The init script will use resize2fs -M to shrink to minimum, then + # read the actual size via dumpe2fs. For pre-flight validation, we + # estimate the shrunk size as used_space + headroom. + shrink_estimate_mib = used_mib + SHRINK_HEADROOM_MIB + r.config["shrink_estimate_mib"] = shrink_estimate_mib + + # Measure user data + pifinder_data = Path("/home/pifinder/PiFinder_data") + total_data_mib = 0 + + if pifinder_data.exists(): + if essential_only: + # Only measure essential files (databases, configs, obslists) + # Excludes: captures/, logs/, solver_debug_dumps/, screenshots/ + out, rc = _run( + [ + "du", + "-sm", + "--exclude=captures", + "--exclude=logs", + "--exclude=solver_debug_dumps", + "--exclude=screenshots", + "--exclude=images", + "--exclude=*.fits", + "--exclude=*.png", + "--exclude=*.jpg", + "--exclude=*.jpeg", + "--exclude=*.bmp", + str(pifinder_data), + ] + ) + else: + out, rc = _run(["du", "-sm", str(pifinder_data)]) + if rc == 0: + try: + total_data_mib = int(out.split()[0]) + except (ValueError, IndexError): + pass + + r.config["user_total_data_mib"] = total_data_mib + r.config["pifinder_data_mib"] = total_data_mib + r.config["essential_only"] = 1 if essential_only else 0 + + # User backup size (ALL user data with tar overhead). + user_backup_mib = int(total_data_mib * USER_BACKUP_OVERHEAD) + 10 + r.config["user_backup_size_mib"] = user_backup_mib + + # Backup layout at END of SD card (working backwards): + # [...partitions...] [root_backup] [user_data_tar] [buffer] + backup_end_mib = sd_mib - BACKUP_END_BUFFER_MIB + + # User data tar goes at the very end (before buffer) + user_backup_start = backup_end_mib - user_backup_mib + + # Root backup goes before user data tar (no boot backup needed) + root_backup_start = user_backup_start - shrink_estimate_mib + + r.config["root_backup_offset_mib"] = root_backup_start + + # Verify backup region doesn't overlap with new partition layout + if root_backup_start < FIXED_LAYOUT_END_MIB: + r.errors.append( + f"SD card too small for safe backup: " + f"leftmost backup at {root_backup_start} MiB but " + f"new partitions extend to {FIXED_LAYOUT_END_MIB} MiB" + ) + return False + + safety_gap = root_backup_start - FIXED_LAYOUT_END_MIB + r.checks.append( + f"Backup layout (end of SD): root@{root_backup_start}, " + f"user@{user_backup_start} MiB " + f"(safety gap: {safety_gap} MiB)" + ) + + if total_data_mib > 0: + r.checks.append( + f"User data: {total_data_mib} MiB (move to SD, backup: {user_backup_mib} MiB)" + ) + else: + r.checks.append("User data: none or empty") + + # Data partition size after migration + data_mib = sd_mib - FIXED_LAYOUT_END_MIB + r.config["data_partition_mib"] = data_mib + r.checks.append(f"Data partition after migration: ~{data_mib} MiB") + + return True + + +def validate( + device: str = "/dev/mmcblk0", essential_only: bool = False +) -> ValidationResult: + """Run all pre-flight checks and compute migration parameters.""" + r = ValidationResult() + + # Store layout constants in config for the init script + r.config["boot_mib"] = BOOT_MIB + r.config["root_mib"] = ROOT_MIB + r.config["fixed_layout_end_mib"] = FIXED_LAYOUT_END_MIB + r.config["backup_end_buffer_mib"] = BACKUP_END_BUFFER_MIB + r.config["device"] = device + + # Sequential checks (later checks depend on earlier ones) + checks = [ + lambda: _check_raspberry_pi(r), + lambda: _check_sd_card(r, device), + lambda: _check_root_partition(r, device), + lambda: _check_sd_size(r, device), + lambda: _check_free_space(r), + lambda: _check_root_fits(r), + lambda: _check_sd_health(r, device), + lambda: _check_required_tools(r), + lambda: _check_boot_partition(r), + lambda: _check_system_state(r), + lambda: _compute_backup_params(r, essential_only), + ] + + for check in checks: + if not check() and r.errors: + break # Stop on first error + + return r + + +def write_shell_config(config: dict, path: str) -> None: + """Write config as shell-sourceable file.""" + with open(path, "w") as f: + f.write("# PiFinder A/B Migration Configuration\n") + f.write("# Auto-generated by migration_calc.py — DO NOT EDIT\n\n") + for key in sorted(config): + val = config[key] + if isinstance(val, str): + f.write(f'{key.upper()}="{val}"\n') + else: + f.write(f"{key.upper()}={val}\n") + + # Also write the sfdisk layout + sfdisk_path = path.replace("_config.sh", "_sfdisk.txt") + with open(sfdisk_path, "w") as f: + f.write(SFDISK_LAYOUT) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="PiFinder A/B Migration — Pre-flight Validation" + ) + parser.add_argument("--device", default="/dev/mmcblk0", help="SD card device path") + parser.add_argument( + "--output", + default="/tmp/migration_config.sh", + help="Path for shell config output", + ) + parser.add_argument( + "--json", action="store_true", help="Print JSON to stdout instead of shell vars" + ) + parser.add_argument( + "--essential-only", + action="store_true", + help="Only back up essential user data (databases, configs, obslists)", + ) + args = parser.parse_args() + + result = validate(args.device, essential_only=args.essential_only) + + # Print results + for msg in result.checks: + print(f" OK {msg}") + for msg in result.warnings: + print(f" WARN {msg}") + for msg in result.errors: + print(f" FAIL {msg}", file=sys.stderr) + + if not result.ok: + n = len(result.errors) + print(f"\nValidation failed with {n} error(s).", file=sys.stderr) + sys.exit(1) + + print(f"\nAll {len(result.checks)} checks passed.") + + if args.json: + json.dump(result.config, sys.stdout, indent=2) + print() + else: + write_shell_config(result.config, args.output) + print(f"Config written to: {args.output}") + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/python/scripts/migration_progress b/python/scripts/migration_progress new file mode 100755 index 0000000000000000000000000000000000000000..92ba6dd8f97403fdbb2fb75d098fb0303c3e974c GIT binary patch literal 80440 zcmcG%3w%`7wLiYjIg@t?Ap{aaBxgc+7)6lsY{8sKf=ELVLTtVIyUk2OR1AV4(1M_u z5D;o?Wq=k8{+fWI%v4&dAm-LvCZK2=TP0N6+TP1d!b1(Myc9;k`F+1<=HP(z*MC3% z-^EVOIs38pT5GSp*4k^YbJ{R%=B=uti1kmEy~3x0M8hnK{1v${{>wIoOcI+I(ofRO zwsB@l>yOnVa(%EZQf^(v>=F4JkE%)GNd2U+!@RMs??i`7{6VDN>Nn@ul6ma+|Mg#b zv|%~=*>L}l#(Oi`&bnIEURtrwdr1?|r4=1J{aWRFSC4yX#jnl!vndift4U$DO*Re2 zFL{7$3TXWchiAy9JY9SDAN?+^*c<6TSMi?J6kncA>VBt4#W}zNN7g&zjgUQ%>{*d6 zn-p=EyG6!>h3oCOSI)(?Z&H=A)(ojjjhBjm$SFBGJ;Z)hD*E~3JegvI0$QeT6>#do zwL8w;g}z+|+DIVYb1quGU5zPkOYrm=cCN#FX~l0$zS?cl?~}V#?>O7ThtF4QUg!NwF4U`;&7q+Z zs_QG8J_SF>i-ouk@jlrI)#CmF@6(J>fAGWsm`n1-b6%6k_(c+U?FCmWTH$lkKu|aX;V&*zGQqb^l87uq-uN)_qp- zG@`r_WeF(bcqW}~DV|*z#~m;2`tS=AiA#NPoTpps?^85)`7ad6fK6_08!8p+UY2U0x;QO^7NM_x*{OCOKb*Kc!|llOU=)SXO_3Y%kH zxq-1*zp9VB+@9zz9~T{q&y7j$@-cWOKKW?F`Wx}gxsNRyfigk+AAla`BQiKC@i#en zk1OUFM}DN#{ZNyN9_U)E?=JGU7`PR?`*<8!qd`1hrXkr~z8SE-FS0wJ8k3zi8E?|m z*2m+=n8Z^B&gA_!@%x)V*9B7w|5Yq_RtB z|KH(o)=@3HutytINL&=?RTCehkLS85KGryH$WJ}Sc^Q3zT9%LHoC6&&A3*t^Q7(|f z6x{2mqfT&cx3#ZDdAF8Scoc1k1IKsc`d=9TRn(dC=S=bVppybon_%g84fepS*Hb-9 zQNKG%qeWi}=rijBDV`S8p8~w=AiG1jK4AwH%7%z1@8rr2xX+}0?1sLLL%n4u9jlEC zM=|c|!1PcLuGRkOArp^3O%F{~B{S9*Qa%;k=HXRvEfVSVi&O;B&;HKzkPCBl3U*P{ zy!TA|dsTbj(wTp;;jkmMvDtpnv9SZ~yV1A!&FP^kv#)x&Z>+8E4>-=^(#eJ*6OV+$ zn@#*)n;vqbk0J7c=m{R;NxnOaZIjQY7@|}$*-c(3fXn{mh206d zqHyRx90D)j!7sM|r4{X0*nir=aA;q?H@h3M(XbApK7Wva6mljH{D(R(qz-wufYbQK2WW; z3)kb%BqDDDdx&<>iJ{SB=ndP_1%;NSuk-7&UQ=5Sw>+Pu5 zpDH!WzlCy7v|QK4BQ~SmDHkR_Zj~jP&l4>FkJmM6U&?R-;Efb5B~H|o_*^CB1oW&m zHs`44wc*WJABDy6Z{`)qvPx0wjPz*_HHX6$c9b0ywO^4W)^=h(tTC*)<&`pPUukTl zJaw0dCkQ{&j2 zL<-M=kLcs;NlCi?Dadh8$_$ub=#!N)SZtCa%kt)mXk~deweESyVPDwfj}<91(B|Vl z{v0=KR9m8qO9v5)s|L-jlm02`YPN@K@7s|5rOrWf-`k&I>WF1FE#>?D-Cg3AIVZHf zg-WhuoQ#8=TKUM~qLoUW+t8ci#%ykRU5#u0b*6a27nQo7i!u45wXHd=mz28WTED^% zB_ri;c>VykGzB;$K)=@l#zDw;qN1f-uM9|OxK8T)s#N%|@66G|2@`^_Ljf(ZFd1zd z5~Z#YbifCwTb*q@WQSb_o&D^P@jQvIZMzh%$iuit-@uqylcj@c0^vC9_lX=ay?Xa0c+UP5f1OU z1M{(MRkp{OrZ(#~38|o64Ds}bZaWP7(eM*Fy$&*{*o=^C+1DfE6r5UABbaAMG~BJvBXK+OfB$hcYp~J6=2$xF%w(;4h*!+@DN) zc{)=XTh3w(@YWRknf1zu?Q*`UwEVa*%3TiOF!oKF$?gPnF-(mc@H z6EcWf(@Ybt3v(TK!U*Y?!h2X=5BwX_RwdlPIpHtVy_7flUYoUR@-C2TtG5kx|oqt1L z=vl0#Ot@{bzWN25r#s6Cox;3D@Hw!PKG0WXIvRhVJd?Knh|>06l(rvLUZ!nl3~j$3 zEScdZ1-4!DUV=V8i+win3Ic}x1;`%eKOeXeNAmcrjG5?Cb24nWx2MnU=TepAqV(By zu%{J4te>NI8|}fh;tiH49@93#9tRt2*H^23$I?HHM?cO_HEfv+^Nl@DR}0pfhUWn6 z?P1w6VWdVMQ}fYAg6b zYVgvu-%&pA1dZJzBs32+90hO5&mQos=LzxlXt-j$9`LRQyd(XD^B!d*Ixkn#?*jcM z?SNt7Vy%a(X|4ht;Sw9>JkEOw?YsM?d7)$8DF%*5v-*0E=cRiC3EHmizP4RQv*LCg z&x_wx0z7+wFJX3wTP%lgGFm;VNz*q&_!ZJYyPi%@T^{j2Ae{E6iYb=4=ppFRj} zLM(6Qrnk0GPmN!xZgo9ww383rpow~F7i5dNYo9u>7q+6}aPl9}w%AJ=B4fXPS;i>? zv176y*XQ}ckXgvc0?bV}<{0s}!iWwW==*UL{$ALm0P0%M=KNsDK5*r=JBqV~FLc~4e4WME%k z4;b7-+>!$MLwm#reAFo{+Z)lfKE@`>Ap^@FllEncCG;Ki<9?rXTfWAn6|aBA*2MCF zb957EA|9lhczp7Ccn|zq*xUiWAsD|JxNvOZ!LrR5--R+G!CgKUia4;=;HC@hIyfnaP*w$-GQYlSvO^uApZKU_1$Wwtyb`sj=(5XJeYz(n-g*_Vu1v z3mLm=y_yJy`EOHs~g-_a3bGp6GgqzjS%Mzm@d=cD;|seA^3T zPEo1U?Hnu>Lm?m4V{$yzce<9jMAC=u9NO%Zy!Ra0Mmu#?2lkJy8?nzh_3#D<{WftU z?1%j(_Bx=9BJ}J;SIihrF)q=c&<4Q|hd%+CoJt?!pUS2I+02c| z=Hu|IOA;fpc@nZ&g0bjB_CW6K91C%|q1cO*fR2tln*YnS4;uzb87#ldwF^uye z#^?{dduhdgc>6rxWX3tUf256~eF|5P>I`dYi1Cx2{@5EuX%CihpX8nj-vlu;#Jzrz z2Hf0T=QUel_&d@*7cd2}kwA*!odg@^5Jy)aLA*Nmokp66vJ%kzQ*S~$oFCQwFb-#Y zcn-|P+05azP)Hp-_Om~xg+BjFTIk}Dw9s_4Q*zs8HNgfbM`aPm_V<%b7r$e)7p+z5 z{An^?A0QPCs??Q@mx`~y;VBO!wB|gN``lvC+WycZ|FQM1uC*h|y6FKZ!=e*}M6M;c^i1aI20AaK=lx6S%a!pI3f1s>Sb z)#XQF6iZ)^-Jj<9TKcLM&>tGuSNe6A8SO_@wf6kp)|@=4KNCnsd;sT~Zp^bke&mDz za2b%=KV#4L(lhoY8+B$q&KUw`KdJR^cjp8xKlvB4uGlBIejpW`JIqz{mG=GM75)w7 zGuHQ{51oWxGJc4Obfx0g2fsA@5X-s3AF8>U#y0RN5TOwC8c`Px>wSaMiNlxBxb>E!;>?w5Y9 zi+Pjs5ocoIY=Mf#mr2WL0~d6RAc&Vuu%MWBm)da6e^GiJ$o+KV$_u1)F!k->$!CgZs# z-I6`wugT)^V_bi{Ovd^PwLfBPtitauO*-4ctzCieFV2Vz>J-W8ti)dDq)jS*qGfm1 zL$CRhSF*bDNX-H7Tr&_L$x|^_g_7XZgi#9jo zvp>#)&YT6EIV*Gy=Uv3>yf(Nn2>l|my8Nsuho^dUUrPng8t30-H=Z~!z=zJuBzAN9s0#JQ~TIR!&%L98jpAl6dZVP$pVR>y03uk1s7=tKDNZP<72NS8ynntla>k%#uF zalP|Ff7|}bSKwESL(I91cC4aIGBU>$;F6e=a>cpJk6Vc2bi`SYYYAnH z8yqQ+I_$~I55DtQ&VBZSIf-#H=Lgpx%t0J7%K%*ByAR_QWBvVAjq4=rZNRS^>-?#3 z@kRqQJk>Wh1$j}Kk|-(Azx{Y70o-a=0oO|4*w`QPI~4vyt5Ro6#C)PJbW$1evC6io z13T>(i#OU43$dpv%|(8_w@(IQ{_Bl?`S2~O;8(mAUYl_@#!*-1Z!Ny)+E~2Ojo4Xg zN^z>1f;dOn`@gj}3u5>@YfwN-E`E#+tVkK0!txa>^5*7&&PL!@6SE{0K_g5dY$kC9#v$5X$So2o;>IT#-aw}{KPDievvZh{2>_YS)BU{=F-6Y zHE2@1UGDm<3UhB9sC>mQGSTc~MEd{^a@;bnhHa;>0l3ZvrGF8}1MED}YTpDLv#tBf zG5yd9uP_E;`u!0*l$9@xX zvQL%XJsaFDH-*} zyXeXCV(?{!CMkDo(oXp>z5swh!x!aVW~$cF3A3 zW019L$r4~0!z z`TS%&183;UI_kfB4z}hTk=)u2o6KqKEd9F3bs0Yc%=7RQ?2=pNw8=OZd}I34gy&3x zZV0!Q_lGRPzAe5LR~O)&PnJ3E1Q~DXk7uDjhGTAq;#{PSYXSa&U_F3Oha^>+#XqGP z=X;%UvF))u>__CC#+J53*<+2P!&U~qpEq_m;B&0oFa}`}z8Clr{yoq;T;t@UwN9%^ zJJ#v`M1M~478zUx9mcr21$W!$(0kXwCmy&Qy!h;6M}W&g;Bx#X*+X8!zo^yU2v-mX z;zEDp@zGK-i#&{%I^+RW?1Zf+{10I7*^WH94)w&}?EiYCKg(YoCF6^tb-30VuZ1fP zn)CGnj)LOL2#044iNHA(?QjlPNI0}@AMFoU+yh^MZHSvS$Ie2jV0+@$3?HV+^v!N) zJ^|mX8*78~KL_6|*@UMQa&PQh`Nn1c$_4+5=X&(79PqE8i}%nTbFax~LsLz^0rAkU z_*(}2B?CSdeV*^YmkJ=>S*a!VKK`GeOLCych-Y@$5zu<_Rp@xgT$UD}Lc2)28-{J9 z?fne4u`N!<7eXI*gMRu2Jz7?w0zU%r?9eFKLN{#cF%7;K>@RhNwKrgW@n_-9tRwtb zdv3taDwv~g({IR%>@h$a`(y(q`%tcR=%BWA>WjGEPr=bE|(tp9P#=ddLgs|%W6 zrf-XWPk|@I!}-N%=Eh{1_ZLS`{44H{pl=9$U*E8%r4aqu_e6DJb36LxqHiAhx>o*q zs~vuAexlM`WmCa-#a9fzt5Lou!G8^6E-qAG-`t?Z^}_eB$diQLZMbqy5&>5M&jZyX zn-c+V3i=gEcJG^*8wGQahkLu;3VSHc^g)6$a>5GekOpmF?>fM!hJFfY2|SO>3W~t5 z&h(uCFOFfadO8%|d;&3d<8pngsZC&d*^|@#HAWM zGIf$@%`X8L`Y~0*rC%lN>8=#oo=hL~jp>*E2g74gH|NNOGU61E zGiC>T-7Q*tXFA$zz-5p*S3{f0&&mH_uHf@GlaKIcGT7IN{XxB&U1rW3Xsdeqj?cQW zE+TW6>pSpyc(-fid;d_5E?_M3nsidmk7I5h0*&-RIgTB3O1bBF$1&G-%&+5Oc%1QB zc>LA#Wc9xv{n@w6M>2(VlI8PriN=e7y~Pg-s-1OOWqE zS)K&G5@rB%Nm+}HUvS+@bgw|)J{KI*-H3(m8(_`-vU0(%+Jr_|TaZw^=d8Tpjdr+wb19?MPP^Y7qW zGqw~PSE`7|xnYxb=mggBr4@-d16OfAlg#`tUw}DSJTLB#^GoKy&rL#1EXB(+nNV#i za@E}BpV#;CPU*L%{2M?(VH~VT%sAMJ1G%0l*k^^2GX_UHQ_VFdRc=CF2y#cZVXZSi zLqj?2sR#Q8{@#Tr@%JeVj!f;jkH#b&N&-cO861#APQJVJsbM zG4M6eHCM*Z1#j-qWY4FtBOl?HiSZ$WnR8Qdh6ehB*vHGtm4_$X=X`~@5C`h29>gsi zSo`efx>LVg=SVds4?w*Me|7{u+YG>&XAflEg>x1s=1_^dfc+t^F=s6U;qP%Sp)>15 z>pzzBmHon&ocEsn$z4USNZq#d=)8eGB{jl3vFPKRf={F`z?dHCnM@YI8m0^9OM_<3p~qBGQf=$4v4QdN(5(a@%d;ay~qPvF!0U zzqaN~zt4FAF=g0uy)bztXoGE<0Gzv{b9WAK3Nfb+%pc`SjiLGEr{RhqXtsl9yI*g{ znP-;Op7&13DxWPpxX#8&W|;zBShTze-Lwk4SOs3RA}&kXHa(&ZUZp0d6v6-hA^4$y zAN?VNm;0cNa^sgUxG&&3CZ3edc#LB`b4*KS>EqG{!dED)g4`6_lsT10u`Z?{zeLjt zohu)P->-Bsk4Q(n4Ew!i<;|)|I(+)PT&bHuT)u(4rv*~y#vHcdeGAl8?9q@mw)5J5xF7Y3#`wQzgT_EGMHVRqAZAI!9LY&B)X0&t#`>$#DaZ zyuShu_@8zt4=h`c{Z8jHC(a&~o%UuOXOA4qK2a*}K(5mX#$p%~ytCkyqqtu>9{I?a z2j&ibG#KZmh+mvQjIfhB1vvp94Mt3NsL}ox%n7cckHCXw*eu?k>``!TEH9kUQhmj4 z#Po31jv!;^XQ-=*!^mICzYcl=>mis6ea1Ko^w(0zpL^_uiOeNw zRI@s}q06`*c@yWU#qbButJ2K;y9Us2V9paE*A2GFJ#+d@8-+0kL4JP$ni{eZk7OJ0 zow}$YUFz)bVvR-GOn#O;h}kY<69c#4}&#lFT37<*3ZtSLfcTNiImJRQP zFgB#58XM$d>0!rxu1PKh`AG05obc73PL$nS60;|)&MiF*I4_9N*HZ?WSq;C!LAk*g zWc7RAAWS^03q{T+V|S5S5%anMfLaXa+pJmkkn+oqP*4!t+?oyTE<=^v!PHfGk=!Y5dxrli2% zul?ipH93`9R&D2>0l#k~j_A*n@vIvKTic_>K}QXM&5Z74Am=ChtKQy;kagq?oQJG) z&7H`@9Q7~u9)ryHPJ~aCF>*qWmRi`VWY!iQ2v-o^XK%FT{2a7R!MhRyIi8Ixe0N`_ zc3~pzvSpKxBj=ko`6JYwgba|bb6Rp?Ht28dDC|WpiaD>xP-gLn`4A^4iy&0oeLZ3D zCqdS>Fh)$+!4<}fwD)gc1v_w`bCO&NkNXj1mvdRI$ev-CbIuvZv*4ctA5I)mJU3!J z_8d_rBcC|yvl0dG;JG(^2Kne|NWQ*5$HF-9^?Gl6n>1(DlBYq?eIB|Yc!RWKueeyp ze6`Ux#QH}6E}pY-eg6h!4*33!jFW@@N5CHi>l*I?Hn*x-y>DVYlGiwY@VK#mi|io> z_+I)nyqUQS+;4W9=THO8bEx&yLnx024uKnt_Ji1`9!WIngyTb(V;$3;Lk8SAFI+O| zZ;$QoM*s8ZZ_3}B-23{c_Tc>&_^HSN;(6Bq--~PmJ0`os6Q+WPT&J9`){b$#KLozy z<2lH|G5joEBBqhWHFQi%K^}EhAu1AMNB zjg0}#<~s@AA+~1f-8%5$B=|<%i`ZH_c~7}B_ve@c#>eTGJAfDEnleoq6!`MTDA#$a zw*>T1hEL`dBWEunzxSg(X|~$NjyVzSW5*D68BhwBotAAx1?z=VGpll!QXLgBrZa5#_ z+)yZ|gRq;EAxc1o}D`b=j7kSo}kPds-5Zmy#8Cr)6{TgbL~7$=KJx?_3HE1*j$}4 zz$1Jh{?O5<`Cje-Ez?_i*r&u!s!zB=2B_2l{du!KIxzC{CmJ%Zy!^YhU;w+4VYNCjOKQ6F<2-%R2x(aU$1DPrNX}ITrpe=57M)vmO3| zOO$r(3I@%dgvx2)bNPvrLWU4L_*F-oU?20m zycmyu-b2V=WBwt_RNzG(IhmJCILHem+=sr1^X5TsZOUl9D^&Y$UgA?EE6&(uU?K6@ zq_*A#dyicC?|3a<=}GB2@wCtmg7{U^Zn zKOuKcOBfQZ|*-e@f?TG9XHbmshOZF*9f(x8eYgA&L6ej;`L1}t5n%@1o!)Kza47> z{F>~(5izw)3Au9xbjM>K``XU`n+(R%-Mv}y9&J^TA1k3H$VKkn?C^3-=4m1ixfAgs z)FHp6wbo4d!N-Fz9-w`U5L&$o1m`Pi+5bIm_Kt2^mJn_F%P#0bBpuK z+b^x?kGyP~rxbE~baNzcrn|}JDQ3<^f*JclTnqW8)i&%oW&P^)Uxz#^yZCNR-b!Ux z-bxie4L=)xarnjW0^HG+;04YwJx8$z4^GFg3Hj>x;)*z4-h;3)2U0FRveMY~MT6`b z!TbW|YXGmF%~$5`x}fhat%&5)T#Jvf{3|u*F!FAO3jHGRy`V29=QFR*4L>ux^r89C zHP}0s7d;RE9d@S(=kzXI3s4qm?=Gj`WlvOiF66Nz{!PCQ{47%es|5S6h61VMexnt8 zK0UMXF2471HEw(-ATQ>9`~u7g-rZ`h6!r8@>^m#na{2@4uS!EsAHev-a}Dp~;~Kp2 zx#E3(T#=XkyKyc)9P_f)U3mdGg`RWgU9jNpmvq*(uk%;Fg1Yvd{=8RMr(&JVy68BJ zy2+2>9gUIqEu4h<$%*&fG|7kfnbn`;jVy3@AG`4pbKFW%PJaMAIDTS*;`QBl%Vj)B z$4yg;y<;(zyC(fY_nk8 z>PS1{3!CzzDJ#8f`)KaNX4`cIk9gVku^St_3&z>JKc6zi`)ux1@6Li-fK!C8q(@Ix zX4yC&poe>L&Vve{N12j1YdiR^fZvTV{9XlqqpZC#%I8&xdEj|+u_R7t4CE~%Uj;dE z`|#%$<^n8Em-wAzu+L^JbC6Zr$?RO}MKh`1aV+2-^ zIuiT39{6)dp{tHThhv`qa4kHn{4M$soKwOgAJ{hXp58vbYXNu>{t!3JcRBrvfQGYe z_|wbJ^1YoaX^horu`pxDz`ItP<7vAAgLq(`%=eQxpE}-o!5Qo3ymYx)(vg#aHkH>H zhkYy>t{n4(-+wri+>+PULac569a7|2w4^fDBa#1c$9eP*( z;LNw42YH;c;9GPn9_KB{O=24<3Dy5zyv{VhMt|$QXYlAN@6ez=gK>E4y+`Ik!q4~d z9p%YaFfu=4jK?=^q<4#SoF+~+OyJym`T_LK6lBi-gnN;S-PU2O0z@NT2;4cNaEyQhaZ zr;q*U`Nb*eZC$iOLy;ppRM9$BymPXe`47-pHpH5+M%pV89}%pZO2kgKVZ9+Hw43dF zu>Q}1UzLcPa2+3q402CaDao}RIKR)wm^;~D5_@Ohxl){`i`O@|Y=?e3j&{Ql1Mve+ z0eIViajOMqD$X3wFg%X{e%fmBZgXdQ4z4TqB1RTc(Hs>;S&PcmOvxeAa{5pph=Ymsw))4y>js+u@&e;Cc z9jqe`7M?6K(3fShUd20F9p?M3mY&H=RqJkp-N~n)ClvyJ@wh2Cg6b<~?F5j?18IbXc)L>ccdMcY|@WAz+iG3Fc#-wxOo z9*k*O`S1a}2gW#r@~W{xz3|}SV(drV&@DaTC(7Zo*Sbw#@=H5Fnc!I5(}ETs?S+|} zh%+q4&aNm&eCzZz#xnEjFFuI8u)cF6c>kyQoeI)Ie1gE|XqLmPqm3Onbs(NXe2RdN z4mlO0Uxr)m{i%$}8~ZeJm>~Bl4y?ntDYIW{lVAMQd>;&WQZHUD$FXy*AiuI3@!s#^ z46Pe_$_*V|2{?fS|5WbX&AE~c;N+jGActsEv|TsO()gYl_ClMx!3P6+K?2Gg1AT+& z=Y&5=-g9iy)NXzQ;7fZIivwh>+`==qoc)WVvZ3I;uxai)Ua>DzGKwUw){i}+ZH(%1 zU#wh5oY!2Gb#1Wyn2XGP-_YL0du(GVYmKNcLf%6o^u?-+$~xp}jZcoYNynA(7miJN zVy*}Ks_1Kcaee~piXnry!A2?R5naG`d0XU{$g8X{$436JHMR~qC|?#WvAV;u^`w>Y zDAJ|@e?F5xRlw5;{5pW2s+^igIH~0dA!wzh2gHMe2d-49byZpz9{oa6iI9n~A zad^H68f5J>uV8Phh|;WJPe~ivVeR_>6E+RDCi1>*5OTryP0!9P1rE~?H@FpXgDc-X zy_!BjyfUvr1y0E6LH-!|o`>;TlW}f}{7`4S5%R0j*#lf$uh zTl1x^Lw*I$j$pS7m34yUgY!itd!o8}nm6yFcvZv+9OOqL@xD^-s88&m1#?PTGAuo4 z$w4#bkMF1=W>KigD;X@0d|yNHeGM{M^+n~nRajHiU#RQ0Lmuf*9372ZO{@*(4&-3X zA03StA>LbEgZ)UJ6x4!m$iFB?>__Qs<9-PKeB(gI1q7~^ySy4OU4TdbyhJ@Uk-jx@ zO%dNY@DJBZtPEoRkJx3V?>yoa$ZaU3jbLo%TI+zhhOD#J0rPCgSIh6W^Bq&Xj|3SS zR|5S~eOa$S*F#TG#@UzWkR|Jp(+C?2+z;jRhXf2Ups*et`WF&zC62RbPbn3?OXmow^}&4Fb6&?pQOgiS4&* zB(eD!DR`_Ed#ONFf2iyWT=rvKA})!uPT&L_T+zK2;a-c@t9>NOq4CaO_#4R2$9*2o z@0{?*Nc#uiLq2FA?FwX^{ww>tVCRlPhtc-;Xo)^&n!D>2#Oear_x#_+C_+a%*K4iIJ%u&DC z2YFL#e*<1^hmLN%jSH{>1Vke`Wk!d!}R* z&&wHWcMckhIi8F)$~{;YeWCo7sdJ0(8*G;6-+1d`vn|@0ZP5;Am6M%l%d?pLl^Ju3 z7vx6Tmd-p}On7MXm44`>q$0M7et$+k3s2~S%RWggj^P8yX@?zw+?ntEUs|DhFEwF} zfp$@-dEEir$02d|Cm23A=;2)QmI2FN?J|4%0U* zhK->de@Pc{jiv1d%%dOc+44EhG8YN7(y!Tu_uX9sf!7d&@zq{GbWrs*;_R+fhWYIh zC&sKUWX_m$nz3uFFYbdQ-vPk;n)nXKFEJiSFQDfbQ*&xqwcG z-_+mt7VJR%*NxCi*yoCZeM@F(=na%v_pjlemIHnob7C) z?3!f(-4l!3)os~sF2-lPUBj48?EBb;a8%?573WI-eB?_96O^&^&jOh1#;9*trCpd% zjrPbRm`vZ0_G~-8?NN<9qj;PxA48u+lv{21T^d!8CkuPSb;tJ5(RFUfn-LCAR3t71 zF`7b-QwblFvcj?4uyH0mSMi)OaV;IW2K{&7eBbgT6M)NcJezW>*?ilfXRhUgLXInT zNJ0v6j`d@?uOhCrlW|{!CvF1`#Bm>Rbj0BJ$LL&e&5?fsA47*-{71M`CsL2wfS(KS zbg`v4FGhTX0C)%4t<|G8IZ9Ln~-Re7ua}B4GP*y2@dL@}TyLcxR zG4m09k>e76qN_nsd);=FzoXPWk9@Y(@~s!)OV!;tMk?a)jlvxwWygp4TUP0>w-igf z@0k7d7Vzx!F`Q?tF7Tr)4!MoC&i#O6m%R5KXZFB+C7N* z{m#>E$X_`+SoV}6e%*?=tRTMu`7FP*k85lxkZ*19XUaIiJA!zxe>ZJEVH_i#64%>- zyoE;CC&YdW3-V+);rfR};5sK@?}Hcy_kVUdwPD!BBp>&D=O70L^73kj$)0>k^R2oW zxkbp$a^8ybRlp4k=>K|z{!znl9vml!!l0!Qv~0U&%ia&QK8RnY`k046-i-ns32Cxt zmGt#h0SAHX2X02cC@=M_2&}h|%RoLp57_?#9}&Y%`x$u={G|0|(jPEsM?N#?GsM_X zaAD>Ku@(E&i;AxiXA|Ug$2okXE9QKJJYNjDiDPk21V=aK)4~xMHs*Z)?o_y9Kj=LY zox8>}tuI4gZ=g?Tf796D7$Cd#bpQJ@M&^))XCMo9-QQAL@LJ1iLF@r*uo%zqgLe$CMXXp!T+i__!r|E&L{(=;s(s+TF|Ez$eywrWcPFU?N|EtV*S1EH@3Xot)%sG z?ixjTpJT}&**y%fdcYTp4}QEeLb+c6ok6+ZgfYMmkK-foK?!tW?~L4qZ*6e*hQ~jH zI3ef54f#82&WWuTUxTY)PSo%>OAe1n-M{ezd{@miNiqgwjC%nm3|yU%2TRWUn8UY~ zzCY{1ngst^IDe~bZ*L4Iq1~CV$pgxC$MJAQQX1xsu#pR8!48_^D!t^_LBwS^7Imk6 zN@C&buNp4}Ap66ju?|1Jk;8oib<=$Kti^g{jZuca)yD^>-SUc}ht9FwuS5#T-JqxzeR4~t0o_EW|N;yL{szUhN{{X)2+HY2j$ zkh^ZKH^|>Oix2wn2;WCR4zb>lkCo~pzgK_Ix;F^jeg^p_-XQa(=yTlr(|pBoc-o}d zFLFHx5C=Myh?qZOFBW~JkWu$L(vR!ycOQi-R#RRfo2B||kj*$VuSkV#8jwx4>4Ut$ z;)^n8Rl)ui_(pZGhZP#W;|JSOwi@!>8i`#&W_gyO`o0aCtaV7*$n%2K-*4pm64g_Z ze8h1W_+Jy{J@$uL7Qam%_lf8<`r^L@ZGOlp&oPEUR@bC%*(=bYdM0C@aX!4`l4bG9 z58Vj)yt(!-+`D1zMRoJc9N>%fSPgt%hi=ZtJC)SK2kZwxubl4TS+l8|HOLv>rNH_o zkEzqRChWo1y_Mtp-(p9-s1Hz@S1$C10E`P0v;f@Ou>7P%A8gD{uaRU-JbqL z%bJ4MTf&O9w&Q$mz@S`|Wy>Bn*5w;m+X5RydB+-T>DKy!-fwMaEZDL)peR1~#qe&P z3H>(t&ItlvgKNLZ#3|0iNyRf|5I)6aemJ53O@2T(#PTEcU*iXKEY~3TF>ZAW^By@b zTx-?1kJTG~Jd4S8T~uxc?7t9Xs*emUIRF^DoMRS@w=iYlFy!&aa zi+sum)A9mw^?@guaEj0Oy>H%tKPkGjvasB{PG&K??AujWX>5o`VEz)mg4j^Ew#XlGE196TSwXye`HS6 z0h>Id?j{eG;@KaF%%Qa&^U(ia;ls^nPhWuRlXgBCFs-?K<6PLBOFzDC!Fg0*(>^>M z-c8=e&f)#&L%EOQ+|uz%ME0klyc4-uly&N4@`^lxk6jL##y;P@;cm2fBTjbPl=XXA zM_y8tq{x;qNEjdiNk9*a439!53 zCe3~+PI0_MeK-pJf6yB?Ws>+<@;H(BAYZ9fN$GW*j>yGY@L_da1iv*H1G%t!dJkev zgNDC@2I762NX7`*5U$DLfT098?={yr;u~v0U##w6d#+i^$tvVYtp!hq;<-ax|MC+k zR{EyiEk3{!B39lN5HSBK$`y0f0&3#WI%52dx#v?5o?T3E^8*au7thgk{ zu>6IClIVLBy6gz;{I84`)qcijj2W4@;w*>n{~g61J&ZG5=HrW$eui^L__uDHeJQnm z?Ae;|-T?NQzI_p2A&2?Me2xR(2$S~W2WW@>thjUk~(4fv|ZwfMqAfN@az z!A{0E?;9znpYMkp^iK|Kd{p_?i`9UeqNIG2-_vnkXKc6+Yp)mUXwxPhWDc-?IZh8sYva|`_zw=0;%}nMd#rK_$^%d%+|5u?_&Jtxrd+L z`owk5Y~Atj^IL!U@UvS#Zk*Np0_X@!-`+RSHg46Lmh~&2-TM27;ft0&v-S6leVcbZ z{M^>n?%B=1Rx+B4R9kQQBgTf`HT|&Vk(JxF?rJP;wym-?k3ruj+_yLHkiN~@BTsMj zJp97e$NOyC`ZYD9`SHf@Hve~nv7tdydk>&b-6~JZ&8ucMztxo5JmisQw;o^l!qyYZ zwr%YI9w*!-%}2C^-r?>?TU;xR4R&`z^9jJl|EI#w{x-1{dt+N~&j`H3Dca9^_zWB3 zOf)__VO!g>LsHE$e1G6)M^QHvZGyPALWXp-`Th7f-$C1>rrkLTAMT(eG_QGUn(vu$ zny=%n0~?PIob+-N_JGy4Qr|r6|4Pygj~#o62K0XbJ~iV?H)8KmAeyhVzwF_BGyWEb zGj!Uc+i_1hxLO9{PvML(X8-u8qWO-e$nLj>$nGEY^S^&^svLSJRr>A1w-29H;>#Yl zzteJ0!9n1c>Z{h@X_|;1dPP<{#!+mgte1_xXbQ|Ii#2K*} z#B$~Ix48Gmo{G<{*a!L{KhxkBT(9Jg-AA7QafyA58H~XDEok#PbjTXS4fY{!0AIag zbu?};TxOmbcA;!TF<@`QcZW*Sn_Bjpewo_)7WV5k*e4aFzP8bhecB}O2lC!U8jGb* z^RuuwPfNp#4UqE%IGdtu$I5jLeAr`%e|`(Rqkls^MgA5bZoLgY`*7?%_D_*RrQpvF z@MpWI*dxEzQiZ(*&Z2ztaK9hC;X1^6>g4yQI0x4O4(DbKezc{dqvcsB-yz0}Tz4lw ziQvO=Eu2Q--db*8^%TDk{pd6{d0 zcFDqtvS7{c^~!Z)HTWnRd=$>{>%g}P@^~xq8H-;NPvt4Bv0i+G<($l+U+;o%`|oIT z58C`5Z4RQ%5wsZQod;3IaXB8x z{%*95TnO)G?gnAEMz37Y4(Qs_)B_vYe*Q-zCi0zp%f`g|cYWaBokD#31K_2=CyA$z zgBW)=)~D^1v}2nO;ftKc-g5`qXy>N;jwp$}e#CmQ zFDds+TG{it#)cx;beut&aqm%vRQxDM_P8X;*EkCC3B+!l4yiZ-Kd%k`2xEbxu#ZQ+ z#G$aF^sYg?q^94Nm%+~sjLq!8K6%GT+1-Zq!<5A-x*Mg?X}RP$+wJK6DE z2|w@TXo1hDnlVwFZhIm^MWf8v8Gmu5v zhN0jg6=Xm6# zgBCy59Ao4oE++c|SaT`JeaI7>3qVibG(5_mf(U=)Yf=6nZcqN4L=1v4w8z0qE3Q2p zF(?=81o9Gn!(z?{oTL{tLGGLFh`Fpmyk$rHmY0b$)-mvv-ZsQUb@((-n{jQ2Pq`T9 ztAS5L9yDS-Qhv6<&fJGNgY0GmasKTWNlC@{#QW zi1kYe&b}~Sk&4*q$Os*8je|aeO(HF+cwbq?Tr$>m474;tM-cCB`~J)EYqv>@>O;&8 z-|SEjzw76#p?rMYx|g{6;X801pcl=#GJp?x?uYMV>nD5ksP4f0R@k9C7=PU{ZcPj4 z^1$iv_@k+jx#V8inoB3Xbs%_t8f$51YRAShMKT=lW$pt%P9pb%bju{EfPEcz57wa* zv6dkAcO%bfKG?zXYOL2@v{T^g1iyj!bu^A!4V(KNzz9I6KRR0WG=g8j5z#np(U-i^ zqh}d^BhHA^lIA$XX|*%0d+8HVmvQa*d9DQ6n2ES`bu>nq3S0dz$VX=|y56Y2+&MDd zfQ;lJe(r!zvJUuFjfIVb4B$PFm+{vo^rx)3Y`@u9gcy9=$t&lO@r#cDk8?oYa}J&cPYDbA zwq67N8F|S$v2+`+jJepSq!ix!s_~M*HjaQzAs7x0mu>gP0$AMqU|!P2TMWES%Zny9Q5`)2`FLlLp5W#iW5Y+M*$V_+c1m|yM2w+M;Xqp5Fiq%A6*k_=sN?@!l)&(otk-U(VLlRCboK$%>CeF9~Y z{~vJB>mJIl?XQ$yn=cb>saM_{B70)z&-87uj>n=t03I@41X+7OE;$mb54;`TvkPmT zF`#|0?ZknxQhcB7Fz0zK`q58Z1DzJaxNOHZ)8Xf^z5(?L=mmY3W9u%xqXl}!cYXQ~ zT51qmFT%djvJsPG;L-L9aQj~!#C_qJiZtho;I1V2HA{56vAkU!#ZRMx-D>(jD+ zuM6ef&^?4#4E;0o-2RP@s4k)&Is(|13{w6mhm5IlE%KRbdD`2NIN&hg_ZVVr3f>#G zV&GLM`v5*b<7W{)NBzdJEPcl@4gy|<0z)h4;j+S?YlSfZ>FWibuSQI?s-IzVQCpP(Fi_D~UYA#Ka+iCgws z_{~Qhd^1zG5l@!yi^kt6%R!t+Fa}RL`3=s;g5lkEz{frq_9V=>{)Ut&Z9l^|_bl3I z|8B&0UH%382U|}43Hvv$sbxn;>s|xn{VVc(2|Od+T)mMej-iWmTd*B<5b0(hA zg)grK)n`Y0j!pVo0LTCP@HqHZ=6O72yZbjh?@)X}(0?s^$amQ-{q5)ZEqG-51@L|7 z7q0tTcz4Giu9Wp>KHU8BJjmAx$nzS+-+Lh6RkEtZjy=S!;3?&rI9YN{+@}3UL_U@P zN6s(CH0OFb#&;hHn=u8pr~a+@7kR*QY5Jbj1@tQyVvIG2V-%^#Wq^zskUP>f3GD)m zC8w`{nd^k>CE^<({tJ32yAfL$!JTVx*h`T$SPGo-p?~-Pi83cx8;E59UK?~8b?~#P zzcJ(G)N6>%zNDip0ejJVu@|L|H+T)DR-Uc{An5vO-i--h}f zZ{qwHa{Dpl7O=~a|D3T7dcO&6-a?zzw)J~Suh8~kUl^;K*gk;qIW}b<{5N6z8vRV) zAMyBgXT!UBHe|D{f0=N2wl^R1pL7*0;?@Ydt1(^;>Zt#Y#;@PY@i-23A@^j2;e@Xu z$Y+biX1 z@@^gAvF=k`31=qork+m(y+N(tWj+6=X#ER-#dFx9pGM;R$I+g8oPF_~pa`$ zYj`%}u&~K_qTO#mmxrf2dK+Iciw7>P%|NUsh5MkPh~ZR; z>g$PHhcouLGCR)&ZH;#ogIB}_?*qcVD?Z}39sFS#-F?iqy4=kFow@Tr-C7_*oJcOmlfJX*$k`;X4 zN}h4wrNaMvEn!`G$rPEh8h+kt_`|eA0zL}-W9s@w_>$VI#!CkDg%y*GjX>MZ7 zP9iZo@?bU2^c3jnVW4H-B-mq|pKgCm`W?mD`fA7@V+)L15f*c^n#iNZwV*Su%!GXv zJO|{vAUgJH?D>cb+p%9Y@@P1=KrcF>r;D%$UkANKnWQ{0=aK6YiX`geM`X<6Bl;d@ zKjLkb;T!sg5r=%yDrem@m=pT{v{9DaTfBsgnt(jwzJb9-L+s#X8{YdKl77<={C_R? z^$(tEmWhIQ#M58MD3TFFI`(WS-?s7Ri*Lbw;D%ewdVc3Vw-jHXN||Cl`$eB1XiqqK zZ1;^@hUkDzT&!GJ=5X@)f%*3?b}gB|Xd#?P{+m8~#_g5g9$PtmT1Dl|8MCHU&bYm# zVy1vaVYB{@de^-*U)P7#>vxYDwe-Pxt|c}9u1|TeW^r}R!X>xpQ|gzXzH;Hx2QJ?) zS+vBp;PTT#*8?@TB#Y>})m`VmxwZ2j)EC#(*DSeFzkg}{5xxWE^1OKa+@7teo?jMCApI(8VMA<5I0xgL0ML4-Bq$LL?vC+Z`nEnBkK zRlQ{X!h7}l<@HNy9vES^WqXTy-t*t+c{Se~UH{;GT*llqMr6^0H4Ak-M{1+>_dYm( zQT2WEAGGRYo^Nw4p7(v%;u?Ma!ud<|dtCDu)Xd{xrMK&|Zm-a1-~OFx<@)Wn-fF>{ zHGkRdbMf%4tLA?$=T9FtZ}hNvX47Hwh7IG*>;*Moq5gf>{3SQtbdyNio;%EX6Yqsf z7hrE`{u?#s&Kt(usgJpH)R<*Aj9I3SSr#duF)K3kLRUo6W;te=74sL~V_pH${5R?b zaC^+1RyP?**w;rZV=nHWY>9cYZZ*TYi(CiCKF@yduek>HQ8E7>jIBrc7f=DQ|E{Wc zhp(!?`uY1v(Te@|MMLZp?i-@eD{)Q0f0+&Vub{A@Fk@){#CcT{X55iKTjDg-=H@$~ zOY(Ciu`stVF){HLMEPc8;&F>RrxPNYo@i5VnKNVBEmGWINU>|qj9cxFX=OKOXJ#~{ zkC--X+T3;cS$5(hNs5}7 zSy3^iA<<@ACZ!J>z^@cv$Y@9(SqgLwS!v6*EnCT2_MNm6)F&opW@fKsS>kPgqT7l} z4SkAX=qq!9U+xrRj4`EjBmkt&o;`a?>AZ@WrO?ut9^rI4CM04~3Ud?XF+64AFaNtq zrpf=3`sh=nY@7UA9Lh}PbD60)%>QQM>8tnzPPvXk0eEk@i(?UrYUYSVjlbP-u#mkDaK#?F9)SLIsAL= ze^=C-%)55G%cYr@?Hdy9qC8|C<`q{5NZnUbA@dqQ$U-_bgicK*Y6~ zv0y>Xy|D9D%a_#X^)+=%YZg}5Na>;nT=N%3noD_2{n7p8FSvbFDp4<+f#2mrwhldB|FIj?eAW^@x`ab<0SYaKw z%$vWsrh3Vu#mg_(Jh*sKbxnP}+{zX;%jVZFsh4lN7A&pNmn>SOFK{h}5eEJ`{3{H; z{6XRYcfeI$qc5nrXNhj&@V)t<;`XI@x<`MY<^jS78H<*}7t^bj)-RVC3zvWyOCJPI zQ3ph?cEMC%ZUvX*!9|N*i{~#`t}k8a`X1Z{ljs=>zvo&oADwDyYNcfHqWbz#a7-|q zzF_{sTJ(3#8?|U5I++wkd>bjXs5H3&$lzJYqJ;}VI(bH(F2NMQk*ir8!@4E&9{_rb zmj0jiu0A-ftGZvUKB7oM$iWm(F?0b>F(C*$alkF1?5;j6+p?`BN}?7g>(%aCY2(%I zW4XG$5m?oVOmF&olt@;laGQ|U;4Pa){qPC#~>J}I0!;U|y>7+Ae>KPm+({WqF z_IK_*XLsM#uD3JopYM63ci%bZ{_eTId+xdSy|?n_-Nx|(c5G@OjlEzZ=Z~fLrb-uu zRMv?^$F--ySPZ~947VQ@R4AL?jg3<#0me3ed`Y?EMFXix@0)a@Ggl^*wm&c~m=H4t~Nq1jo5JoGhb;nB);S%Qx8Ds;Z^nUGv0ig>2D|54&QYQkYNr|D`x9v`TZ#uiXH2vkea{5=xFDX;6GMz+5O}{YEqlI`J;b3xU zdBC~HNHtNDvdS!wS7l*OB&B=lq%ck5$gh6Gus=6m$m=v!UKJuqgjZ}I4RR)%9-~B6 zk@za5m>HO9dg_ixQZdYm`w*{~EafD2ZvpqJSSzBA6N7W;3i4*6Fo97nq+;qJJ?t#f zE5N3#vj3=glvpT_i_CAcROaV$^>kMgaX-_%;+2_*E+Bv$uI?p*-y%3r8SGHaakK?~RoP2-7dpmwZ`Juu_QqAtcf5J*;!OpH@zWIjU%a=;K zL#lTVeimS}j%;v{YzRN6AtN0fe*DDn^EPAyI0MUntZBY6G~?TT`O;a9=ZE+Je%9>e z_Igcwn=0Dd2>nU?1Yk3ZGjKA;I_KXJn(3Hr4;^efbZhUkt#h~Z9&S0({Ltk~=ihk! z+^esheMRHn0rM;PIf1$hrtVa8@YvA&j?m2Z*=;=s2M=uxw~_g!%bhCNz6Lg{8sN`l zu{cilJ;tBmHgYJ~9%R2(LluBs5_ZF6*Kh35G2JfdcR_y=dd(5{eM^OY3i=mo=>4dF z9QxDHQ~xWtn{5CHb&qggg3T4!bl|K)or_bx;IRY#`7ilHGf(+vzv%BhxZgkC_NsJG z^}_5rY!~+xi_|`FhmN()-x``}oxNq`Q1ii--nqtSn;w7d)!@0;L#7>=koI89q2_0s z<{HUdk97e1&yy3y;tAUC1v{r3j@>rDA~e%A`_3o97I_$K4;^~P!OqZJ`?DQAhua$c z4Q=N`8amisXgeR|BC69toXrCB#N)+c7YR7_;6{nl7oeZ5p}1`g>4z`Lfku(41U=%?@LjiT$*&>cm6W%|7<$c{eNc9sHr3*9u5ANaq2RsM6u;t0xWz|6-g-U2<)K{+zxXyz5CmHo^2XzdmXd#bu#V6&nu|A zda795Y1%8twyq2NxyHl3Fd)uLlGxP#*~-gcyXJE>?zq?C1=>r-hpPQsLv6zDH0+Lo zSLOSe6MUCZM4Em2%KP`+nFQh5ZfK-=N#w zVCSx59rNw|or|BEX`8+EVC$hy{&phnpUGZd~ZR%WTtnB!6K62uIYBu=B`QF!F?WL;l{4hq} z#Q8k?N4?O$_~*r9Z-pLHwv?}8&@VwhS)m`Pm~Z=`-}rU+`q(q$n{B|PNi#BZ*mtC%SJs$al}orEw$G#9BI>gG?_oWd6keL2)3BNP2Ja7e%|;s6{;h<~Rp{S_p2`?+jX}IL;i=Fy$X8X!Wv`>h zANniM2P*Vibkm`B()U8&`b~HJo{Dy3^wbIZwH5WL-V=~_L0)UTj&i+P`WK-mzX?nF zQ|b3b=&wRga}Rnw9$U5Fz1r^^&~HTh{tA6%{ndCOLHaXBuk*Iq18vsz?}7db^g5nG zhR@3SJE8AbD9u03_k?bLldk^+^e;m{Zt%5!KS{ei_k0RDXygd0~Pb( zBJ?YvuN-d~7piju`c~*iOiX2hlpsRS`sNz?W?E*O4>lcYjJBCnN@)yVcW>fs0}5;Z zg4a)uBU+IC1pPIX;kV|sm)1jB1vj_r_2MY(R{bUNuCden znDGm>4>FIHd(qRdpM?FD3i}N6LG50Jeh?p+TqHfu599aN_5rg@>3OuWL#a1m=O;Vv zUw;M82G$3NH{Pq8so38hVH|32C+u!~yI5R8nflpF>2h`bSOM@;D(1wU*TV0+JZdB0uWuMM$@_MiUa$0Tg-fW7$NR}mL)Lq7?9t@*wZ zkHj<3YX3k8@y=P5M3bt>VX$hY7 z?VM@pIh&;70F@q^X$iHR4Ydc)J_uRcS!|fj0*?G`LWW%X6T}VLk@g!uBmQQW`MUx3 z*DqpU0(02wymBvF(YOyoe-0lsS*+0C$9nOR=HwHw%UtGlk!*30e3t4%9@7%@i*npa z$OHd^`A*}q$FwVPNptNq^t+ad#f+)q{q`8}81NYI81NYI81NYI81NYI81NYI81NYI z81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI z81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI z81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI81NYI z81NWa#z5fKfR8FgHzab!|u5%abMHt|EQ*1BS z>ppMl3+_g?lX|Tdj>zq^e9yM@17CGi^%&X~s4A}|oMZd_e3pLKvd342Ejz@wWs3g~ zfuBHpYEw_oZ~ND7+Wx>t{Oj-TzLUN}zV6Y8FZQZ7evQBjR2kPs)rjw}3M|8IR81;S zRo<+ozf#S1PeaK$q_Zah%y&Wg!o3xwN_1OShniGBhbM5~6)D-69Sz+*(#~XeB4%z$L0@`wc?fY4NhUMcdznA5K_JEcQv;22)ZlJFoZ{c{DGKx{0 z2>&?S%lyi+{EQ`klI5x+fD*pN z;o;gB0@_Y~iF{$dTYeG-4HyBDUtoM=-;Q?N@&AIhFUt{T`k-(2Zcy!D$k>bA;B=@V zYaacbwlB+vdd zIbg{J|AdvteqIA#?Crew->`2`%k!bMJ7u-o$@A2fcd>lIvN!E6&u<@Mds*kDzrO`J z#ku4?TF+fQ|6O(P_y1aToLz_9@RbocGnhG4}2FuIP}eJdAw# zzESMuK0)d{f&%5k0~|kwFQtrqSuXY;uHldHd}38VO!fIL*wcI$x&CffXbgUndgdv& zz2F2^yXBu^9Fbqu=w+PgFSzXm=Z^Qe<^7B!a{ZmKz}f~m?GyZ8GWK@fJ5vLH?K(I9 z1B@?n{avrXk1+l^#y^AkNr&*TP=kk4_XM<^{1SWd)0PXqEmzjQ$lx@qGgiEf!_R8@ zP53vC+gswtATUP!uduWoT1+8{&XF7%fD9#XU@X;5$tJRi`-tv-?Z$7 zhmBUdo3W9;S-ibn18360`C;SDaW?k3`z!k*k;{CSxE-+U#s4|0-G8dVo3Zau_H&a9 zHSDF`lUBPT@9lT{FY+6f{AWBaBG=#ZtCadr$SKdyah_iX867_GM)H?gKIFQ-_!8+|8sEUvK7YpYT zb#RVbI3dk5g8j?JpH}91kmX%}WaN+IyQS%n{w`TKre9?oV=d#-qy)z;Z&nMI{}0yj z|9aJSo7FW7CtL@|za=29=(Ef_k=yfbonzeChOpw9(Lm#fIY>P$X_t@m)5(f2i*3ChYq#Y z+E>f~hxRQZH{&7eV6*C)cjL_0@$(g{-S4*;e0eX3>iYOIZg(U5Bkf8&s0ZEc8vk3> zko8_h`X%@bU%KxWe7C$=?XvpyjXM2WW%(n1?s}-2pUvvHg@39J{!&$(CN*7Som#T+ z|E3;(th%3Ff9jV1s2+~vwm1AYspB>LTxa3`v=07asycox9;Pk)3-$1ysD|IH7A>5A ztB3Qs>Nral&M)fVOg&j0r}em7{;N7T>JQxZW?b<6*23w)i|w1OBU3eSHd;8pUI%A! zs=7ZT7S2a&;E4Z8%YFmw={<-oS2(KmD+GIm_rmu3-}_l^zb}q!dCRi*$a{6W%ikCO zG27ekqmM)WL7z{_eb!2H1IKqEzYTKXCu79wCoGq7jIsO@uld4a?U^3JB;GpESLL*)olMEmdksJxB33-<1Clw z;UjGSX_iZzPqKV7$>G1;m#t!X3+~&roS*By#Pa)KzXI!&+@H#N5o3H=F9hdy7<9ot zV6AtLvAyi8#~J@g!Y7{j{$22&Vtcut>SFt8mdn0$gn5`JIr=5{M@hCn$@a4U6n?(L z_VPUK8OHf5wwLwv4#s&I@)bV3SGD$6uP}~0SKrAv7Z^v@sVT;JljMy)CHs>lmj4`b zid*}6)UO~Xe&qd#@Yc-ZCC?|XFwQ$6C;sLAAdA)eASeIr&r#gYIC4ML%N5p>oaT$Q zKkR|LTAUBDy*%eR#W9OT50Jl~Lb^WO?N z#$EQGezyMv04Z*5{4Wwd?B)5uTDE_h+m-tfiMOw^{jz+*{@Bl{ouKiUI6wE0);Gew8qasP86M=lmCW~_ zq1_d}@_rB1_3=ycpW=bn@f$3^1qKvX@?Pc&%ij$-@hta8B415%!r^s%k?lVMd9}Q8 zH{{j)4?s@iEAJU`sgJ)#f!eh{C$N>}@;QMGY(GqL_+#hiosd(zHvZ#~QyeaJ1vJ7S z<2=JSvW{=yJaB~N@?3O|?N6{=o)gZp{F^M7_kgPz=X;P>o7X>Jd%2HWWSrMY4j!gM z0WE1_oPS1~kX$~Oasv5N3A>qqv3KevbWqwzuc?Z?gaLomaY=BL5*O(sZGQ08s8g^CY?yCkWukbsG@ll&qgMk(L&r<=bU_ZBA1QebEmmnZ2et# zuMbBjVx@8{l8=-w;>kj8oN#jaY$7!ljykEllXYS$o=)ZOp8TZtBpx%sILD&ojLO0p zI>!>ZOfs@BoKC6S?nDMMG)|pckIvyV>Wn2)!zhQteFNeC`-0p0!lAxRo41F6DX$T`V0QOQuI7$#5*6&gR0A0@zAVWRgzaiFL29ZV`?rQi*URn}sjv zL3W>^BpfSDOzczX(MQ1&=m>{3Q$`#{mAtD?7zB?-K}95+4^KoAsqSQ2X^8d8(vZ=Z zbEisY9Ao9gbNh0MR6I>}$e7L$MI*^1Xvx!MA|1^qsSwv=OQnpH&8D;3HV;B{X-_lc+I@Ml&GON$p8w)4Km*!)892Ga!iQ zXi21q%P@3mw2(;VxeU;JB)U5s9p4>}M-rMCU}m$abU2xg>N&w4l}JKiA~%+ZO)67@ zk1E_Znb4iU@I~|4NJ_haq0U5;kz6hj-^b9o#8_%H!6T4$CNpL%q=b=*BomRG+8fQM zCq}7hAf}_c`Nke7bZI&lK`HDhQM28eQwr7}vuN zKAe0wKc00WF^strA;_2$j}(#!(|leVp>0r>iloxHq~qwpNJ{g2G2u#%r}sG7crv}W z(l3XJ(jCB*NjfIFO)(wUeBcxwhYQ?!svn8P^6|Sci=D^v@jG#zfJ<7CD~y^#JTaM% z=P0?MR7mCIBAJfla|y&U6jX-FTsf0@Cl!mu4PY*w`Is{qbuyF)b1+6%phh9gS)*@z zFLDM(wQ{19lNACzaUpU|Y}jlX+z|>6hIjP$-`_VJ9u9^E`v|JSOlrC#Qiov@mT)gT z9!bTLPPptPwO#HKu6XE@$Z0xm%zUTn`B=J;4;OOCtB7zS5og0H3LLfJXaT`&yrKkx zvdlW<4_xWih(4OGdPJ92u%s^b3E>d*p zQZ7eDvrbK-R2nIitsF!>pjBUnfJx*?k)#W?^rsvZ5Jn`SqO3C+uSK{tSq9fc7OqXQ zQ96&3HLcaum`(w_rg9k428N^B(fb{7PNzP?;u#dy)7uOO&(Crw=-41Ay+kNlK&4$1 zwY090zgQ;qPN3Q-F#nRs^?c1G9>)-dMW$!2mT?Lv2hDCm$z}i|8GHm=GO1K|Zr?;c zGKzCPYtG|R(j(lR#|W!#%8#l$o7OaRJL6^xq`LJm>m8G^F*_mEO?KT`Cy6Smdn^qE z{OZmmQ7{k)1=Wom*#tIPyb=CCp5BZ4v~}j&*3xqpKkujIzEYZz{k>d&z^X6zJ+iOA z_6K5V&VKi$`Y^65+n2wqTK9QlSg-w$aeW`-%HLlFxc(Lkz`9g-TlMAd6{f#p0GB!$ zm``$FBGS*GKqh#dt`B*BzOiVoZF{+v^H!L>6F*X4p5q7pm-Qb)UE)vb%l-EN*SGsC z?V^f4=zglQzWg282-lxxJ2^>xIVY_8^7m#}xV}7BtW`g2)h|C!C9m+gCv(zeeO)S< z0^QG&FVcT`PTSgGDz5vIyMoQf=b$Fbb#61XYwF)-$;F+@v&3Gdsh{hs+l`dQcpEAS rZ^8?1Z%e+r>VMR + * percent: 0-100 + * message: status text (max ~20 chars fits on screen) + * + * Examples: + * migration_progress 0 "Starting..." + * migration_progress 45 "Moving user data" + * migration_progress 100 "Complete!" + * + * Hardware: SPI0.0, DC=GPIO24, RST=GPIO25, 128x128 BGR + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define WIDTH 128 +#define HEIGHT 128 +#define SPI_DEVICE "/dev/spidev0.0" +#define SPI_SPEED 40000000 +#define GPIO_DC 24 +#define GPIO_RST 25 + +/* BGR565 colors */ +#define COL_BLACK 0x0000 +#define COL_WHITE 0xFFFF +#define COL_RED 0x001F /* BGR565: blue=0, green=0, red=31 */ +#define COL_GREEN 0x07E0 +#define COL_DKGRAY 0x4208 +#define COL_DKRED 0x0010 + +static int spi_fd = -1; +static int gpio_fd = -1; +static struct gpio_v2_line_request dc_req; +static struct gpio_v2_line_request rst_req; +static uint16_t framebuf[WIDTH * HEIGHT]; + +/* 5x7 bitmap font - ASCII 32-126 */ +static const uint8_t font5x7[][5] = { + {0x00,0x00,0x00,0x00,0x00}, /* space */ + {0x00,0x00,0x5F,0x00,0x00}, /* ! */ + {0x00,0x07,0x00,0x07,0x00}, /* " */ + {0x14,0x7F,0x14,0x7F,0x14}, /* # */ + {0x24,0x2A,0x7F,0x2A,0x12}, /* $ */ + {0x23,0x13,0x08,0x64,0x62}, /* % */ + {0x36,0x49,0x55,0x22,0x50}, /* & */ + {0x00,0x05,0x03,0x00,0x00}, /* ' */ + {0x00,0x1C,0x22,0x41,0x00}, /* ( */ + {0x00,0x41,0x22,0x1C,0x00}, /* ) */ + {0x08,0x2A,0x1C,0x2A,0x08}, /* * */ + {0x08,0x08,0x3E,0x08,0x08}, /* + */ + {0x00,0x50,0x30,0x00,0x00}, /* , */ + {0x08,0x08,0x08,0x08,0x08}, /* - */ + {0x00,0x60,0x60,0x00,0x00}, /* . */ + {0x20,0x10,0x08,0x04,0x02}, /* / */ + {0x3E,0x51,0x49,0x45,0x3E}, /* 0 */ + {0x00,0x42,0x7F,0x40,0x00}, /* 1 */ + {0x42,0x61,0x51,0x49,0x46}, /* 2 */ + {0x21,0x41,0x45,0x4B,0x31}, /* 3 */ + {0x18,0x14,0x12,0x7F,0x10}, /* 4 */ + {0x27,0x45,0x45,0x45,0x39}, /* 5 */ + {0x3C,0x4A,0x49,0x49,0x30}, /* 6 */ + {0x01,0x71,0x09,0x05,0x03}, /* 7 */ + {0x36,0x49,0x49,0x49,0x36}, /* 8 */ + {0x06,0x49,0x49,0x29,0x1E}, /* 9 */ + {0x00,0x36,0x36,0x00,0x00}, /* : */ + {0x00,0x56,0x36,0x00,0x00}, /* ; */ + {0x00,0x08,0x14,0x22,0x41}, /* < */ + {0x14,0x14,0x14,0x14,0x14}, /* = */ + {0x41,0x22,0x14,0x08,0x00}, /* > */ + {0x02,0x01,0x51,0x09,0x06}, /* ? */ + {0x32,0x49,0x79,0x41,0x3E}, /* @ */ + {0x7E,0x11,0x11,0x11,0x7E}, /* A */ + {0x7F,0x49,0x49,0x49,0x36}, /* B */ + {0x3E,0x41,0x41,0x41,0x22}, /* C */ + {0x7F,0x41,0x41,0x22,0x1C}, /* D */ + {0x7F,0x49,0x49,0x49,0x41}, /* E */ + {0x7F,0x09,0x09,0x01,0x01}, /* F */ + {0x3E,0x41,0x41,0x51,0x32}, /* G */ + {0x7F,0x08,0x08,0x08,0x7F}, /* H */ + {0x00,0x41,0x7F,0x41,0x00}, /* I */ + {0x20,0x40,0x41,0x3F,0x01}, /* J */ + {0x7F,0x08,0x14,0x22,0x41}, /* K */ + {0x7F,0x40,0x40,0x40,0x40}, /* L */ + {0x7F,0x02,0x04,0x02,0x7F}, /* M */ + {0x7F,0x04,0x08,0x10,0x7F}, /* N */ + {0x3E,0x41,0x41,0x41,0x3E}, /* O */ + {0x7F,0x09,0x09,0x09,0x06}, /* P */ + {0x3E,0x41,0x51,0x21,0x5E}, /* Q */ + {0x7F,0x09,0x19,0x29,0x46}, /* R */ + {0x46,0x49,0x49,0x49,0x31}, /* S */ + {0x01,0x01,0x7F,0x01,0x01}, /* T */ + {0x3F,0x40,0x40,0x40,0x3F}, /* U */ + {0x1F,0x20,0x40,0x20,0x1F}, /* V */ + {0x7F,0x20,0x18,0x20,0x7F}, /* W */ + {0x63,0x14,0x08,0x14,0x63}, /* X */ + {0x03,0x04,0x78,0x04,0x03}, /* Y */ + {0x61,0x51,0x49,0x45,0x43}, /* Z */ + {0x00,0x00,0x7F,0x41,0x41}, /* [ */ + {0x02,0x04,0x08,0x10,0x20}, /* \ */ + {0x41,0x41,0x7F,0x00,0x00}, /* ] */ + {0x04,0x02,0x01,0x02,0x04}, /* ^ */ + {0x40,0x40,0x40,0x40,0x40}, /* _ */ + {0x00,0x01,0x02,0x04,0x00}, /* ` */ + {0x20,0x54,0x54,0x54,0x78}, /* a */ + {0x7F,0x48,0x44,0x44,0x38}, /* b */ + {0x38,0x44,0x44,0x44,0x20}, /* c */ + {0x38,0x44,0x44,0x48,0x7F}, /* d */ + {0x38,0x54,0x54,0x54,0x18}, /* e */ + {0x08,0x7E,0x09,0x01,0x02}, /* f */ + {0x08,0x14,0x54,0x54,0x3C}, /* g */ + {0x7F,0x08,0x04,0x04,0x78}, /* h */ + {0x00,0x44,0x7D,0x40,0x00}, /* i */ + {0x20,0x40,0x44,0x3D,0x00}, /* j */ + {0x00,0x7F,0x10,0x28,0x44}, /* k */ + {0x00,0x41,0x7F,0x40,0x00}, /* l */ + {0x7C,0x04,0x18,0x04,0x78}, /* m */ + {0x7C,0x08,0x04,0x04,0x78}, /* n */ + {0x38,0x44,0x44,0x44,0x38}, /* o */ + {0x7C,0x14,0x14,0x14,0x08}, /* p */ + {0x08,0x14,0x14,0x18,0x7C}, /* q */ + {0x7C,0x08,0x04,0x04,0x08}, /* r */ + {0x48,0x54,0x54,0x54,0x20}, /* s */ + {0x04,0x3F,0x44,0x40,0x20}, /* t */ + {0x3C,0x40,0x40,0x20,0x7C}, /* u */ + {0x1C,0x20,0x40,0x20,0x1C}, /* v */ + {0x3C,0x40,0x30,0x40,0x3C}, /* w */ + {0x44,0x28,0x10,0x28,0x44}, /* x */ + {0x0C,0x50,0x50,0x50,0x3C}, /* y */ + {0x44,0x64,0x54,0x4C,0x44}, /* z */ + {0x00,0x08,0x36,0x41,0x00}, /* { */ + {0x00,0x00,0x7F,0x00,0x00}, /* | */ + {0x00,0x41,0x36,0x08,0x00}, /* } */ + {0x08,0x08,0x2A,0x1C,0x08}, /* ~ */ +}; + +static void msleep(int ms) +{ + struct timespec ts = { .tv_sec = ms / 1000, .tv_nsec = (ms % 1000) * 1000000L }; + nanosleep(&ts, NULL); +} + +static int gpio_request_line(int chip_fd, int pin, struct gpio_v2_line_request *req) +{ + struct gpio_v2_line_request r = {0}; + r.offsets[0] = pin; + r.num_lines = 1; + r.config.flags = GPIO_V2_LINE_FLAG_OUTPUT; + snprintf(r.consumer, sizeof(r.consumer), "migration"); + + if (ioctl(chip_fd, GPIO_V2_GET_LINE_IOCTL, &r) < 0) { + perror("GPIO_V2_GET_LINE_IOCTL"); + return -1; + } + *req = r; + return 0; +} + +static void gpio_set(struct gpio_v2_line_request *req, int value) +{ + struct gpio_v2_line_values vals = {0}; + vals.bits = value ? 1 : 0; + vals.mask = 1; + ioctl(req->fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals); +} + +static void spi_write(const uint8_t *data, size_t len) +{ + /* Chunk large transfers - SPI driver may limit to 4KB */ + const size_t chunk_size = 4096; + while (len > 0) { + size_t this_len = len > chunk_size ? chunk_size : len; + struct spi_ioc_transfer tr = {0}; + tr.tx_buf = (unsigned long)data; + tr.len = this_len; + tr.speed_hz = SPI_SPEED; + tr.bits_per_word = 8; + ioctl(spi_fd, SPI_IOC_MESSAGE(1), &tr); + data += this_len; + len -= this_len; + } +} + +static void ssd1351_cmd(uint8_t cmd) +{ + gpio_set(&dc_req, 0); + spi_write(&cmd, 1); +} + +static void ssd1351_data(const uint8_t *data, size_t len) +{ + gpio_set(&dc_req, 1); + spi_write(data, len); +} + +static void ssd1351_cmd_data(uint8_t cmd, const uint8_t *data, size_t len) +{ + ssd1351_cmd(cmd); + if (len > 0) + ssd1351_data(data, len); +} + +static int skip_reset = 0; /* set via --update flag in main */ +static int display_on = 0; + +static void ssd1351_init(void) +{ + uint8_t d; + + if (skip_reset) { + /* Just ensure display is on, skip full init */ + ssd1351_cmd(0xAF); + display_on = 1; + return; + } + + /* Hardware reset */ + gpio_set(&rst_req, 1); + msleep(10); + gpio_set(&rst_req, 0); + msleep(10); + gpio_set(&rst_req, 1); + msleep(10); + + /* Init sequence matching luma.oled exactly */ + ssd1351_cmd(0xFD); /* Unlock */ + d = 0x12; ssd1351_data(&d, 1); + + ssd1351_cmd(0xFD); /* Unlock commands */ + d = 0xB1; ssd1351_data(&d, 1); + + ssd1351_cmd(0xAE); /* Display off */ + + ssd1351_cmd(0xB3); /* Clock divider */ + d = 0xF1; ssd1351_data(&d, 1); + + ssd1351_cmd(0xCA); /* Mux ratio */ + d = 0x7F; ssd1351_data(&d, 1); + + ssd1351_cmd(0x15); /* Column address */ + uint8_t col[2] = {0x00, 0x7F}; + ssd1351_data(col, 2); + + ssd1351_cmd(0x75); /* Row address */ + uint8_t row[2] = {0x00, 0x7F}; + ssd1351_data(row, 2); + + ssd1351_cmd(0xA0); /* Remap/color depth */ + d = 0x74; ssd1351_data(&d, 1); /* BGR, 65k color, COM split */ + + ssd1351_cmd(0xA1); /* Start line */ + d = 0x00; ssd1351_data(&d, 1); + + ssd1351_cmd(0xA2); /* Display offset */ + d = 0x00; ssd1351_data(&d, 1); + + ssd1351_cmd(0xB5); /* GPIO */ + d = 0x00; ssd1351_data(&d, 1); + + ssd1351_cmd(0xAB); /* Function select */ + d = 0x01; ssd1351_data(&d, 1); + + ssd1351_cmd(0xB1); /* Precharge */ + d = 0x32; ssd1351_data(&d, 1); + + ssd1351_cmd(0xB4); /* VSL */ + uint8_t vsl[3] = {0xA0, 0xB5, 0x55}; + ssd1351_data(vsl, 3); + + ssd1351_cmd(0xBE); /* VCOMH */ + d = 0x05; ssd1351_data(&d, 1); + + ssd1351_cmd(0xC7); /* Master contrast */ + d = 0x0F; ssd1351_data(&d, 1); + + ssd1351_cmd(0xB6); /* Precharge2 */ + d = 0x01; ssd1351_data(&d, 1); + + ssd1351_cmd(0xA6); /* Normal display */ + + /* NOTE: Display ON (0xAF) moved to after framebuffer flush */ +} + +static void ssd1351_flush(void) +{ + /* Set contrast before first frame (matching luma) */ + if (!display_on) { + ssd1351_cmd(0xC1); /* Contrast */ + uint8_t contrast[3] = {0xFF, 0xFF, 0xFF}; + ssd1351_data(contrast, 3); + } + + ssd1351_cmd(0x15); + uint8_t col[2] = {0x00, 0x7F}; + ssd1351_data(col, 2); + + ssd1351_cmd(0x75); + uint8_t row[2] = {0x00, 0x7F}; + ssd1351_data(row, 2); + + ssd1351_cmd(0x5C); /* Write RAM */ + + /* Send framebuffer as big-endian 16-bit pixels */ + uint8_t buf[WIDTH * HEIGHT * 2]; + for (int i = 0; i < WIDTH * HEIGHT; i++) { + buf[i * 2] = framebuf[i] >> 8; + buf[i * 2 + 1] = framebuf[i] & 0xFF; + } + ssd1351_data(buf, sizeof(buf)); + + /* Turn display on after first frame */ + if (!display_on) { + ssd1351_cmd(0xAF); /* Display on */ + display_on = 1; + } +} + +static void fb_clear(uint16_t color) +{ + for (int i = 0; i < WIDTH * HEIGHT; i++) + framebuf[i] = color; +} + +static void fb_pixel(int x, int y, uint16_t color) +{ + if (x >= 0 && x < WIDTH && y >= 0 && y < HEIGHT) + framebuf[y * WIDTH + x] = color; +} + +static void fb_rect(int x, int y, int w, int h, uint16_t color) +{ + for (int j = y; j < y + h && j < HEIGHT; j++) + for (int i = x; i < x + w && i < WIDTH; i++) + fb_pixel(i, j, color); +} + +static void fb_char(int x, int y, char c, uint16_t color, int scale) +{ + if (c < 32 || c > 126) + c = '?'; + const uint8_t *glyph = font5x7[c - 32]; + for (int col = 0; col < 5; col++) { + uint8_t bits = glyph[col]; + for (int row = 0; row < 7; row++) { + if (bits & (1 << row)) { + for (int sy = 0; sy < scale; sy++) + for (int sx = 0; sx < scale; sx++) + fb_pixel(x + col * scale + sx, + y + row * scale + sy, color); + } + } + } +} + +static void fb_string(int x, int y, const char *s, uint16_t color, int scale) +{ + int cx = x; + while (*s) { + fb_char(cx, y, *s, color, scale); + cx += 6 * scale; /* 5px char + 1px gap */ + s++; + } +} + +/* Center a string horizontally */ +static void fb_string_centered(int y, const char *s, uint16_t color, int scale) +{ + int len = strlen(s); + int px_width = len * 6 * scale - scale; /* subtract trailing gap */ + int x = (WIDTH - px_width) / 2; + if (x < 0) x = 0; + fb_string(x, y, s, color, scale); +} + +static void draw_progress(int percent, const char *stage, int stage_num, int stage_total) +{ + if (percent < 0) percent = 0; + if (percent > 100) percent = 100; + + fb_clear(COL_BLACK); + + /* Warning banner at top */ + fb_rect(0, 0, WIDTH, 12, COL_DKRED); + fb_string_centered(2, "DO NOT POWER OFF", COL_RED, 1); + + /* Title */ + fb_string_centered(18, "NixOS", COL_RED, 2); + fb_string_centered(38, "Migration", COL_RED, 1); + + /* Stage indicator (e.g., "3/7") */ + if (stage_total > 0) { + char stage_str[16]; + snprintf(stage_str, sizeof(stage_str), "Stage %d/%d", stage_num, stage_total); + fb_string_centered(52, stage_str, COL_DKGRAY, 1); + } + + /* Progress bar */ + int bar_x = 10; + int bar_y = 65; + int bar_w = WIDTH - 20; + int bar_h = 12; + + /* Border */ + fb_rect(bar_x, bar_y, bar_w, 1, COL_DKGRAY); + fb_rect(bar_x, bar_y + bar_h - 1, bar_w, 1, COL_DKGRAY); + fb_rect(bar_x, bar_y, 1, bar_h, COL_DKGRAY); + fb_rect(bar_x + bar_w - 1, bar_y, 1, bar_h, COL_DKGRAY); + + /* Fill */ + int fill_w = (bar_w - 4) * percent / 100; + if (fill_w > 0) + fb_rect(bar_x + 2, bar_y + 2, fill_w, bar_h - 4, COL_RED); + + /* Dark red background for unfilled */ + int unfill_x = bar_x + 2 + fill_w; + int unfill_w = (bar_w - 4) - fill_w; + if (unfill_w > 0) + fb_rect(unfill_x, bar_y + 2, unfill_w, bar_h - 4, COL_DKRED); + + /* Percentage */ + char pct_str[8]; + snprintf(pct_str, sizeof(pct_str), "%d%%", percent); + fb_string_centered(82, pct_str, COL_RED, 2); + + /* Current stage name */ + if (stage && *stage) + fb_string_centered(105, stage, COL_RED, 1); + + /* Bottom warning */ + fb_string_centered(118, "Please wait...", COL_DKGRAY, 1); + + ssd1351_flush(); +} + +static int hw_init(void) +{ + /* Open SPI */ + spi_fd = open(SPI_DEVICE, O_RDWR); + if (spi_fd < 0) { + perror("open spi"); + return -1; + } + + uint8_t mode = SPI_MODE_0; + uint8_t bits = 8; + uint32_t speed = SPI_SPEED; + ioctl(spi_fd, SPI_IOC_WR_MODE, &mode); + ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits); + ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed); + + /* Open GPIO chip */ + gpio_fd = open("/dev/gpiochip0", O_RDWR); + if (gpio_fd < 0) { + perror("open gpiochip0"); + return -1; + } + + if (gpio_request_line(gpio_fd, GPIO_DC, &dc_req) < 0) + return -1; + if (gpio_request_line(gpio_fd, GPIO_RST, &rst_req) < 0) + return -1; + + ssd1351_init(); + return 0; +} + +static void hw_cleanup(void) +{ + if (dc_req.fd > 0) close(dc_req.fd); + if (rst_req.fd > 0) close(rst_req.fd); + if (gpio_fd >= 0) close(gpio_fd); + if (spi_fd >= 0) close(spi_fd); +} + +int main(int argc, char *argv[]) +{ + int arg_offset = 0; + + if (argc >= 2 && strcmp(argv[1], "--update") == 0) { + skip_reset = 1; + arg_offset = 1; + } + + if (argc - arg_offset < 5) { + fprintf(stderr, "Usage: %s [--update] \n", argv[0]); + fprintf(stderr, " --update Skip reset, just update display\n"); + fprintf(stderr, " percent 0-100\n"); + fprintf(stderr, " stage_num Current stage number (1-based)\n"); + fprintf(stderr, " stage_total Total number of stages\n"); + fprintf(stderr, " stage_name Description of current stage\n"); + fprintf(stderr, "\nExample: %s 50 3 7 'Extracting system'\n", argv[0]); + return 1; + } + + int percent = atoi(argv[1 + arg_offset]); + int stage_num = atoi(argv[2 + arg_offset]); + int stage_total = atoi(argv[3 + arg_offset]); + const char *stage_name = argv[4 + arg_offset]; + + if (hw_init() < 0) { + fprintf(stderr, "Hardware init failed\n"); + hw_cleanup(); + return 1; + } + + draw_progress(percent, stage_name, stage_num, stage_total); + hw_cleanup(); + return 0; +} diff --git a/python/scripts/nixos_migration.sh b/python/scripts/nixos_migration.sh new file mode 100755 index 000000000..7bc2463d5 --- /dev/null +++ b/python/scripts/nixos_migration.sh @@ -0,0 +1,247 @@ +#!/bin/bash +# nixos_migration.sh - Pre-migration: validate, download, stage initramfs +# +# Called by PiFinder app (sys_utils.start_nixos_migration). +# Runs on RPi OS before rebooting into initramfs for the actual migration. +# +# The initramfs will: +# 1. Save WiFi + user backup to RAM +# 2. DD the .img.zst to the SD card +# 3. Expand partition, restore WiFi + user data +# 4. Reboot into NixOS +# +# Usage: nixos_migration.sh [sha256] [progress_file] +# +# Exit codes: +# 0 - Success (initramfs staged, ready to reboot) +# 1 - Pre-flight check failure +# 2 - Download failure +# 3 - Checksum mismatch +# 5 - Initramfs staging failure + +set -euo pipefail + +export PATH="/usr/sbin:/sbin:${PATH}" + +MIGRATION_URL="${1:?Usage: nixos_migration.sh [sha256] [progress_file]}" +MIGRATION_SHA256="${2:-}" +PROGRESS_FILE="${3:-/tmp/nixos_migration_progress}" + +trap '_trap_err $LINENO "$BASH_COMMAND"' ERR +_trap_err() { + echo "{\"percent\": 0, \"status\": \"FAILED at line $1: $2\"}" > "${PROGRESS_FILE}" + echo "ERROR at line $1: $2" >&2 +} + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PIFINDER_HOME="/home/pifinder" +TARBALL="${PIFINDER_HOME}/pifinder-nixos-migration.tar.zst" +BOOT_PARTITION="/boot" +INITRAMFS_DIR="/tmp/nixos_initramfs" +PROGRESS_BIN="${SCRIPT_DIR}/migration_progress" +INIT_SCRIPT="${SCRIPT_DIR}/nixos_migration_init.sh" + +progress() { + local pct="$1" + local msg="$2" + echo "{\"percent\": ${pct}, \"status\": \"${msg}\"}" > "${PROGRESS_FILE}" + echo "[${pct}%] ${msg}" +} + +fail() { + local code="$1" + local msg="$2" + progress 0 "FAILED: ${msg}" + echo "ERROR: ${msg}" >&2 + exit "${code}" +} + +# Copy a binary and all its shared library dependencies into the initramfs. +copy_with_libs() { + local bin_path="$1" + local dest="$2" + + cp "${bin_path}" "${dest}/bin/" + + ldd "${bin_path}" 2>/dev/null | grep -oP '/\S+' | while read -r lib; do + local dir + dir=$(dirname "${lib}") + mkdir -p "${dest}${dir}" + cp -n "${lib}" "${dest}${dir}/" 2>/dev/null || true + done +} + +# --- Phase 0: Install required packages --- +progress 0 "Installing dependencies" +for pkg in e2fsprogs dosfstools fdisk zstd; do + if ! dpkg -s "${pkg}" >/dev/null 2>&1; then + sudo apt-get install -y "${pkg}" || fail 1 "Failed to install ${pkg}" + fi +done + +# --- Phase 1: Pre-flight checks --- +progress 3 "Running pre-flight checks" + +if ! python3 "${SCRIPT_DIR}/nixos_migration_calc.py" --json > /tmp/migration_checks.json 2>&1; then + fail 1 "Pre-flight checks failed" +fi + +WIFI_MODE=$(python3 -c "import json; print(json.load(open('/tmp/migration_checks.json'))['wifi_mode'])") +if [ "${WIFI_MODE}" != "Client" ]; then + fail 1 "WiFi must be in Client mode" +fi + +# RAM check: image must fit in available RAM during initramfs +MEM_KB=$(awk '/MemTotal/ {print $2}' /proc/meminfo) +MEM_MB=$((MEM_KB / 1024)) +[ "${MEM_MB}" -lt 1800 ] && fail 1 "Insufficient RAM: ${MEM_MB}MB (need 2GB)" + +progress 5 "Pre-flight OK" + +# --- Phase 2: Download image --- +SKIP_DOWNLOAD=false +if [ -f "${TARBALL}" ]; then + if [ -z "${MIGRATION_SHA256}" ]; then + progress 60 "Using cached download (no checksum)" + SKIP_DOWNLOAD=true + else + progress 10 "Verifying existing download" + EXISTING_SHA256=$(sha256sum "${TARBALL}" | awk '{print $1}') + if [ "${EXISTING_SHA256}" = "${MIGRATION_SHA256}" ]; then + progress 60 "Using cached download" + SKIP_DOWNLOAD=true + fi + fi +fi + +if [ "${SKIP_DOWNLOAD}" = false ]; then + progress 10 "Downloading... 0%" + rm -f "${TARBALL}" + + if ! curl -L -f -o "${TARBALL}" \ + --progress-bar \ + "${MIGRATION_URL}" 2>&1 | tr '\r' '\n' | while IFS= read -r line; do + if [[ "$line" =~ ([0-9]+)\.[0-9]% ]]; then + dl_pct="${BASH_REMATCH[1]}" + mapped_pct=$(( 10 + dl_pct * 50 / 100 )) + progress "${mapped_pct}" "Downloading... ${dl_pct}%" + fi + done; then + fail 2 "Download failed" + fi + + # --- Phase 3: Verify checksum --- + if [ -z "${MIGRATION_SHA256}" ]; then + progress 60 "SHA256 not provided, skipping verification" + else + progress 60 "Verifying checksum" + ACTUAL_SHA256=$(sha256sum "${TARBALL}" | awk '{print $1}') + if [ "${ACTUAL_SHA256}" != "${MIGRATION_SHA256}" ]; then + rm -f "${TARBALL}" + fail 3 "Checksum mismatch" + fi + fi +fi + +progress 65 "Download OK" + +# --- Phase 4: Get image size --- +progress 68 "Preparing" + +TARBALL_SIZE=$(stat -c%s "${TARBALL}") + +progress 75 "Tarball: $((TARBALL_SIZE / 1048576))MB" + +# --- Phase 5: Build initramfs --- +progress 78 "Building initramfs" + +rm -rf "${INITRAMFS_DIR}" +mkdir -p "${INITRAMFS_DIR}"/{bin,lib,dev,proc,sys,mnt,tmp} + +# Busybox (provides sh, mount, umount, dd, tar, cp, etc.) +if command -v busybox >/dev/null 2>&1; then + copy_with_libs "$(command -v busybox)" "${INITRAMFS_DIR}" +else + fail 5 "busybox not found" +fi + +# Filesystem tools +for tool in e2fsck resize2fs mke2fs mkfs.vfat sfdisk zstd; do + tool_path=$(command -v "${tool}" 2>/dev/null || true) + if [ -z "${tool_path}" ]; then + fail 5 "${tool} not found — install e2fsprogs dosfstools util-linux zstd" + fi + copy_with_libs "${tool_path}" "${INITRAMFS_DIR}" +done + +# mkfs.ext4 is typically a symlink to mke2fs +ln -sf mke2fs "${INITRAMFS_DIR}/bin/mkfs.ext4" 2>/dev/null || true + +# OLED progress display (static binary, no libs needed) +cp "${PROGRESS_BIN}" "${INITRAMFS_DIR}/bin/" 2>/dev/null || true + +# SPI kernel modules — needed for OLED progress display +# Modules may be compressed (.ko.xz); decompress for insmod in initramfs +KVER=$(uname -r) +KMOD_DIR="/lib/modules/${KVER}/kernel/drivers/spi" +if [ -d "${KMOD_DIR}" ]; then + INITRAMFS_SPI="${INITRAMFS_DIR}/lib/modules" + mkdir -p "${INITRAMFS_SPI}" + for mod in spi-bcm2835 spidev; do + if [ -f "${KMOD_DIR}/${mod}.ko.xz" ]; then + xz -dc "${KMOD_DIR}/${mod}.ko.xz" > "${INITRAMFS_SPI}/${mod}.ko" + elif [ -f "${KMOD_DIR}/${mod}.ko.gz" ]; then + gzip -dc "${KMOD_DIR}/${mod}.ko.gz" > "${INITRAMFS_SPI}/${mod}.ko" + elif [ -f "${KMOD_DIR}/${mod}.ko.zst" ]; then + zstd -dc "${KMOD_DIR}/${mod}.ko.zst" > "${INITRAMFS_SPI}/${mod}.ko" + elif [ -f "${KMOD_DIR}/${mod}.ko" ]; then + cp "${KMOD_DIR}/${mod}.ko" "${INITRAMFS_SPI}/${mod}.ko" + fi + done +fi + +# Dynamic linker — needed for non-busybox tools +LD_PATH=$(find /lib /lib64 /usr/lib -name "ld-linux-*" -type f 2>/dev/null | head -1 || true) +if [ -n "${LD_PATH}" ]; then + mkdir -p "${INITRAMFS_DIR}$(dirname "${LD_PATH}")" + cp "${LD_PATH}" "${INITRAMFS_DIR}${LD_PATH}" +fi + +# Init script +cp "${INIT_SCRIPT}" "${INITRAMFS_DIR}/init" +chmod +x "${INITRAMFS_DIR}/init" + +# Metadata: paths + sizes so init script knows where to find things +cat > "${INITRAMFS_DIR}/migration_meta" </dev/null | gzip > /tmp/nixos_migration_initramfs.gz + +sudo cp /tmp/nixos_migration_initramfs.gz "${BOOT_PARTITION}/initramfs-migration.gz" + +# Migration flag on boot partition (survives root format) +sudo touch "${BOOT_PARTITION}/nixos_migration" + +progress 92 "Configuring boot" + +# --- Phase 7: Configure boot to use migration initramfs --- +if [ -f "${BOOT_PARTITION}/config.txt" ]; then + sudo cp "${BOOT_PARTITION}/config.txt" "${BOOT_PARTITION}/config.txt.premigration" + + echo "initramfs initramfs-migration.gz followkernel" | \ + sudo tee -a "${BOOT_PARTITION}/config.txt" > /dev/null +fi + +progress 100 "Rebooting in 5s..." + +echo "Migration staged. Tarball: ${TARBALL_SIZE} bytes" +echo "Rebooting in 5 seconds..." +sleep 5 +sudo reboot diff --git a/python/scripts/nixos_migration_calc.py b/python/scripts/nixos_migration_calc.py new file mode 100755 index 000000000..8a0d36f8c --- /dev/null +++ b/python/scripts/nixos_migration_calc.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +""" +Pre-flight checks for NixOS migration. + +Validates hardware requirements before migration can proceed. +Run on the Pi to verify it meets minimum specs. + +Usage: python3 nixos_migration_calc.py [--json] +""" +import argparse +import json +import os +import platform +import re +import shutil +import subprocess +import sys +from pathlib import Path + +MIN_RAM_MB = 1800 # 2GB Pi reports ~1849MB due to GPU memory reservation +MIN_SD_GB = 16 +REQUIRED_MODEL = "Raspberry Pi 4" + + +def get_model() -> str: + """Read Pi model from device-tree.""" + try: + return Path("/proc/device-tree/model").read_text().rstrip("\x00").strip() + except OSError: + return "Unknown" + + +def get_ram_mb() -> int: + """Get total RAM in MB from /proc/meminfo.""" + try: + text = Path("/proc/meminfo").read_text() + match = re.search(r"MemTotal:\s+(\d+)\s+kB", text) + if match: + return int(match.group(1)) // 1024 + except OSError: + pass + return 0 + + +def get_sd_size_gb() -> float: + """Get SD card size in GB (root device).""" + try: + result = subprocess.run( + ["lsblk", "-b", "-d", "-n", "-o", "SIZE", "/dev/mmcblk0"], + capture_output=True, + text=True, + ) + if result.returncode == 0: + return int(result.stdout.strip()) / (1024**3) + except (OSError, ValueError): + pass + return 0.0 + + +def get_free_space_gb(path: str = "/home/pifinder") -> float: + """Get free space in GB at the given path.""" + try: + usage = shutil.disk_usage(path) + return usage.free / (1024**3) + except OSError: + return 0.0 + + +def get_wifi_mode() -> str: + """Detect WiFi mode.""" + wifi_status = Path("/home/pifinder/PiFinder/wifi_status.txt") + try: + return wifi_status.read_text().strip() + except OSError: + return "Unknown" + + +def is_pi4() -> bool: + """Check if running on a Raspberry Pi 4.""" + model = get_model() + return REQUIRED_MODEL in model + + +def check_all() -> dict: + """Run all pre-flight checks. Returns dict with results.""" + model = get_model() + ram_mb = get_ram_mb() + sd_gb = get_sd_size_gb() + free_gb = get_free_space_gb() + wifi = get_wifi_mode() + + checks = { + "model": model, + "is_pi4": REQUIRED_MODEL in model, + "ram_mb": ram_mb, + "ram_ok": ram_mb >= MIN_RAM_MB, + "sd_gb": round(sd_gb, 1), + "sd_ok": sd_gb >= MIN_SD_GB, + "free_gb": round(free_gb, 1), + "free_ok": free_gb >= 1.5, + "wifi_mode": wifi, + "wifi_ok": wifi == "Client", + "arch": platform.machine(), + } + checks["all_ok"] = all( + [checks["is_pi4"], checks["ram_ok"], checks["sd_ok"], checks["wifi_ok"]] + ) + return checks + + +def main(): + parser = argparse.ArgumentParser(description="NixOS migration pre-flight checks") + parser.add_argument("--json", action="store_true", help="Output as JSON") + args = parser.parse_args() + + checks = check_all() + + if args.json: + print(json.dumps(checks, indent=2)) + sys.exit(0 if checks["all_ok"] else 1) + + print(f"Model: {checks['model']}") + print(f" Pi 4: {'OK' if checks['is_pi4'] else 'FAIL'}") + print(f"RAM: {checks['ram_mb']} MB") + print(f" >= {MIN_RAM_MB}MB: {'OK' if checks['ram_ok'] else 'FAIL'}") + print(f"SD Card: {checks['sd_gb']} GB") + print(f" >= {MIN_SD_GB}GB: {'OK' if checks['sd_ok'] else 'FAIL'}") + print(f"Free Space: {checks['free_gb']} GB") + print(f"WiFi Mode: {checks['wifi_mode']}") + print(f" Client: {'OK' if checks['wifi_ok'] else 'FAIL'}") + print(f"Arch: {checks['arch']}") + print() + if checks["all_ok"]: + print("All checks PASSED - migration can proceed") + else: + print("Some checks FAILED - migration cannot proceed") + + sys.exit(0 if checks["all_ok"] else 1) + + +if __name__ == "__main__": + main() diff --git a/python/scripts/nixos_migration_init.sh b/python/scripts/nixos_migration_init.sh new file mode 100755 index 000000000..27af2f1dc --- /dev/null +++ b/python/scripts/nixos_migration_init.sh @@ -0,0 +1,374 @@ +#!/bin/busybox sh +# nixos_migration_init.sh - Initramfs init for NixOS migration +# +# Runs entirely from RAM. Strategy: +# 1. Save WiFi credentials and user backup to RAM +# 2. Copy tarball to RAM, unmount old root +# 3. Format both partitions +# 4. Extract tarball (boot → p1, rootfs → p2) +# 5. Restore WiFi + user data, expand partition +# 6. Reboot into NixOS + +set -e + +/bin/busybox --install -s /bin 2>/dev/null || true + +mount -t proc proc /proc 2>/dev/null || true +mount -t sysfs sysfs /sys 2>/dev/null || true +mount -t devtmpfs devtmpfs /dev 2>/dev/null || true +mount -t tmpfs tmpfs /tmp 2>/dev/null || true + +# Load SPI modules for OLED progress display +if [ -f /lib/modules/spi-bcm2835.ko ]; then + insmod /lib/modules/spi-bcm2835.ko 2>/dev/null || true + insmod /lib/modules/spidev.ko 2>/dev/null || true + sleep 0.5 +fi + +# Shared lib path for dynamically linked tools (e2fsck, mkfs, etc.) +export LD_LIBRARY_PATH=/lib:/usr/lib:/lib/aarch64-linux-gnu:/usr/lib/aarch64-linux-gnu + +BOOT_DEV="/dev/mmcblk0p1" +ROOT_DEV="/dev/mmcblk0p2" +SD_DEV="/dev/mmcblk0" +MOUNT_ROOT="/mnt/root" +MOUNT_NEW="/mnt/new" +MOUNT_BOOT="/mnt/boot" +PROGRESS="/bin/migration_progress" + +STAGE_NUM=0 +STAGE_TOTAL=22 + +show() { + local pct="$1" + local msg="$2" + STAGE_NUM=$((STAGE_NUM + 1)) + echo "[${pct}%] ${msg}" > /dev/console 2>/dev/null || true + echo "[${pct}%] ${msg}" + [ -x "${PROGRESS}" ] && "${PROGRESS}" "${pct}" "${STAGE_NUM}" "${STAGE_TOTAL}" "${msg}" 2>/dev/null || true +} + +fail() { + [ -x "${PROGRESS}" ] && "${PROGRESS}" 0 0 0 "FAILED: $1" 2>/dev/null || true + echo "[FAILED] $1" + echo "MIGRATION FAILED: $1" > /dev/console 2>/dev/null || true + echo "Dropping to shell for debugging..." + exec /bin/sh +} + +show 28 "Migrating..." + +# Wait for SD card device to appear +n=0 +while [ ! -b "${BOOT_DEV}" ] && [ "${n}" -lt 30 ]; do + sleep 1 + n=$((n + 1)) +done +[ ! -b "${BOOT_DEV}" ] && fail "SD card not found after 30s: ${BOOT_DEV}" + +show 30 "Initramfs started" + +# ------------------------------------------------------------------- +# Phase 1: Validate +# ------------------------------------------------------------------- + +# Check migration flag on boot partition +mkdir -p /mnt/bootchk +mount -t vfat -o ro "${BOOT_DEV}" /mnt/bootchk || fail "Cannot mount boot" +if [ ! -f /mnt/bootchk/nixos_migration ]; then + umount /mnt/bootchk + fail "No migration flag — aborting" +fi +umount /mnt/bootchk + +# Read metadata written by pre-migration script +if [ ! -f /migration_meta ]; then + fail "migration_meta not found in initramfs" +fi +. /migration_meta +# Now we have: TARBALL_PATH, TARBALL_SIZE, PIFINDER_DATA_PATH + +# RAM check: tarball + backup + overhead must fit +MEM_KB=$(awk '/MemAvailable/ {print $2}' /proc/meminfo) +MEM_MB=$((MEM_KB / 1024)) +TARBALL_SIZE_MB=$((TARBALL_SIZE / 1048576)) +NEEDED_MB=$((TARBALL_SIZE_MB + 150)) # tarball + backup + overhead +[ "${MEM_MB}" -lt "${NEEDED_MB}" ] && fail "Insufficient RAM: ${MEM_MB}MB available, need ${NEEDED_MB}MB" + +show 31 "Validated (${MEM_MB}MB free)" + +# ------------------------------------------------------------------- +# Phase 2: Save WiFi credentials to RAM +# ------------------------------------------------------------------- + +show 33 "Saving WiFi to RAM" + +mkdir -p "${MOUNT_ROOT}" +mount -t ext4 -o ro "${ROOT_DEV}" "${MOUNT_ROOT}" || fail "Cannot mount root" + +mkdir -p /tmp/wifi +WPA_FILE="${MOUNT_ROOT}/etc/wpa_supplicant/wpa_supplicant.conf" +if [ -f "${WPA_FILE}" ]; then + cp "${WPA_FILE}" /tmp/wifi/wpa_supplicant.conf +fi + +NM_SRC="${MOUNT_ROOT}/etc/NetworkManager/system-connections" +if [ -d "${NM_SRC}" ]; then + mkdir -p /tmp/wifi/nm-connections + cp -a "${NM_SRC}/." /tmp/wifi/nm-connections/ 2>/dev/null || true +fi + +# ------------------------------------------------------------------- +# Phase 3: Create user backup in RAM +# ------------------------------------------------------------------- + +show 35 "Creating backup" + +PIFINDER_DATA_ON_ROOT="${MOUNT_ROOT}${PIFINDER_DATA_PATH}" +BACKUP_STAGE="/tmp/backup_stage/PiFinder_data" +rm -rf /tmp/backup_stage +mkdir -p "${BACKUP_STAGE}" + +if [ -d "${PIFINDER_DATA_ON_ROOT}" ]; then + # Copy root-level files (observations.db, configs, etc.) + for f in "${PIFINDER_DATA_ON_ROOT}"/*; do + [ -f "$f" ] && cp "$f" "${BACKUP_STAGE}/" 2>/dev/null || true + done + + # Truncate log to last 1000 lines + if [ -f "${BACKUP_STAGE}/pifinder.log" ]; then + tail -n 1000 "${BACKUP_STAGE}/pifinder.log" > "${BACKUP_STAGE}/pifinder.log.tmp" + mv "${BACKUP_STAGE}/pifinder.log.tmp" "${BACKUP_STAGE}/pifinder.log" + fi + + # Copy obslists directory + if [ -d "${PIFINDER_DATA_ON_ROOT}/obslists" ]; then + cp -a "${PIFINDER_DATA_ON_ROOT}/obslists" "${BACKUP_STAGE}/obslists" + fi +fi + +show 38 "Backup created" + +# ------------------------------------------------------------------- +# Phase 4: Copy tarball to RAM, unmount old root +# ------------------------------------------------------------------- + +show 40 "Loading tarball to RAM" + +TARBALL_ON_ROOT="${MOUNT_ROOT}${TARBALL_PATH}" +[ ! -f "${TARBALL_ON_ROOT}" ] && { umount "${MOUNT_ROOT}"; fail "Tarball not found: ${TARBALL_PATH}"; } + +cp "${TARBALL_ON_ROOT}" /tmp/migration.tar.zst || fail "Failed to copy tarball to RAM" +umount "${MOUNT_ROOT}" + +show 48 "Tarball loaded to RAM" + +# ------------------------------------------------------------------- +# Phase 5: Expand + format partitions +# ------------------------------------------------------------------- + +show 49 "Expanding partition" + +# Expand partition 2 BEFORE formatting — sfdisk rewrites the MBR and +# blockdev --rereadpt can corrupt a written FAT partition if done after. +echo ", +" | sfdisk -N 2 "${SD_DEV}" --no-reread 2>/dev/null || true +blockdev --rereadpt "${SD_DEV}" 2>/dev/null || true +sleep 1 + +show 50 "Formatting boot" + +mkfs.vfat -F 32 -n FIRMWARE "${BOOT_DEV}" || fail "mkfs.vfat failed" + +show 52 "Formatting root" + +mkfs.ext4 -F -L NIXOS_SD "${ROOT_DEV}" || fail "mkfs.ext4 failed" + +# ------------------------------------------------------------------- +# Phase 6: Extract tarball +# ------------------------------------------------------------------- + +show 55 "Extracting NixOS" + +mkdir -p "${MOUNT_NEW}" +mount -t ext4 "${ROOT_DEV}" "${MOUNT_NEW}" || fail "Cannot mount new root" + +# Extract tarball directly to SD card (ext4 has plenty of space, tmpfs does not) +zstd -d < /tmp/migration.tar.zst | tar xf - -C "${MOUNT_NEW}" || fail "Tarball extraction failed" +rm -f /tmp/migration.tar.zst + +show 60 "Moving rootfs" + +# Move rootfs/ contents up to partition root (same-fs rename, fast) +cd "${MOUNT_NEW}/rootfs" +for item in * .[!.]* ..?*; do + [ -e "$item" ] || continue + mv "$item" "${MOUNT_NEW}/" +done +cd / +rmdir "${MOUNT_NEW}/rootfs" + +show 66 "Copying boot" + +mkdir -p "${MOUNT_BOOT}" +mount -t vfat "${BOOT_DEV}" "${MOUNT_BOOT}" || fail "Cannot mount boot" + +# Copy boot files to FAT partition +cd "${MOUNT_NEW}/boot" +for item in *; do + [ -e "$item" ] || continue + if [ -d "$item" ]; then + cp -r "$item" "${MOUNT_BOOT}/$item" + else + cp "$item" "${MOUNT_BOOT}/$item" + fi +done +cd / +sync + +# Verify critical boot files landed +if [ ! -f "${MOUNT_BOOT}/extlinux/extlinux.conf" ]; then + echo "Boot partition contents:" >&2 + ls -lR "${MOUNT_BOOT}" >&2 + fail "extlinux.conf missing from boot partition after copy" +fi + +# Keep boot/ on ext4 — U-Boot reads extlinux.conf from mmc 0:2 (ext4 root) +# FAT partition only needs RPi firmware files (config.txt, u-boot, DTBs) + +# ------------------------------------------------------------------- +# Phase 7: Migrate WiFi +# ------------------------------------------------------------------- + +show 70 "Migrating WiFi" + +NM_DIR="${MOUNT_NEW}/etc/NetworkManager/system-connections" +mkdir -p "${NM_DIR}" + +if [ -f /tmp/wifi/wpa_supplicant.conf ]; then + SSID="" + PSK="" + IN_NET=0 + + while IFS= read -r line; do + line=$(echo "${line}" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') + + case "${line}" in + network=*) + IN_NET=1 + SSID="" + PSK="" + ;; + "}") + if [ "${IN_NET}" = "1" ] && [ -n "${SSID}" ]; then + NM_FILE="${NM_DIR}/${SSID}.nmconnection" + + if [ -n "${PSK}" ]; then + cat > "${NM_FILE}" < "${NM_FILE}" </dev/null || true +fi + +sync + +show 74 "WiFi migrated" + +# ------------------------------------------------------------------- +# Phase 8: Restore user data +# ------------------------------------------------------------------- + +show 76 "Restoring user data" + +mkdir -p "${MOUNT_NEW}/home/pifinder" + +if [ -d /tmp/backup_stage/PiFinder_data ]; then + cp -a /tmp/backup_stage/PiFinder_data "${MOUNT_NEW}/home/pifinder/" +fi + +# pifinder user: UID 1000, GID 100 (users) on NixOS +chown -R 1000:100 "${MOUNT_NEW}/home/pifinder" 2>/dev/null || true + +show 80 "User data restored" + +# ------------------------------------------------------------------- +# Phase 9: Expand partition and finalize +# ------------------------------------------------------------------- + +umount "${MOUNT_BOOT}" 2>/dev/null || true +umount "${MOUNT_NEW}" 2>/dev/null || true + +show 82 "Resizing filesystem" + +e2fsck -f -y "${ROOT_DEV}" 2>/dev/null || true +resize2fs "${ROOT_DEV}" 2>/dev/null || true + +show 92 "Syncing" +sync + +# Final verification: remount boot partition and confirm extlinux.conf survived +show 95 "Verifying boot" +mkdir -p /mnt/bootchk +mount -t vfat -o ro "${BOOT_DEV}" /mnt/bootchk || fail "Cannot remount boot for verification" +if [ ! -f /mnt/bootchk/extlinux/extlinux.conf ]; then + ls -lR /mnt/bootchk > /dev/console 2>&1 || true + umount /mnt/bootchk 2>/dev/null || true + fail "extlinux.conf missing from boot partition before reboot" +fi +umount /mnt/bootchk + +show 100 "Complete" +sleep 3 + +echo "Rebooting into NixOS..." > /dev/console 2>/dev/null || true +reboot -f diff --git a/python/scripts/test_migration_loopdev.sh b/python/scripts/test_migration_loopdev.sh new file mode 100755 index 000000000..e27ffa461 --- /dev/null +++ b/python/scripts/test_migration_loopdev.sh @@ -0,0 +1,498 @@ +#!/bin/bash +# test_migration_loopdev.sh - Test migration against a real SD card image +# +# Copies a PiFinder SD card image, injects test data (fake NixOS tarball, +# backup, WiFi creds), then runs the full migration flow on the copy. +# +# Usage: sudo ./test_migration_loopdev.sh [--keep] +# image: path to SD card image (e.g. pifinder-mr.img). COPIED, not modified. +# --keep: don't clean up after test (inspect results manually) +# +# Requires: losetup, sfdisk, mkfs.ext4, mkfs.vfat, e2fsck, resize2fs +# +# Tests: partition shrink, staging area, format, tarball extraction, +# PiFinder_data restore, WiFi migration, partition re-expansion + +set -euo pipefail + +if [ $# -lt 1 ] || [ "$1" = "--help" ]; then + echo "Usage: sudo $0 [--keep]" + echo " image: path to PiFinder SD card image (will be copied)" + exit 1 +fi + +SOURCE_IMAGE="$1"; shift +KEEP=0 +[ "${1:-}" = "--keep" ] && KEEP=1 + +[ ! -f "${SOURCE_IMAGE}" ] && { echo "Image not found: ${SOURCE_IMAGE}"; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORK_DIR="/tmp/migration_test_$$" +IMAGE="${WORK_DIR}/sd_card.img" +TARGET_SIZE_MB=32768 # extend to 32GB +# STAGING_SIZE_MB calculated dynamically after we know backup size + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[FAIL]${NC} $*"; } +pass() { echo -e "${GREEN}[PASS]${NC} $*"; } + +LOOP_DEV="" + +cleanup() { + local rc=$? + if [ "${KEEP}" = "1" ] && [ "${rc}" = "0" ]; then + warn "Keeping: ${WORK_DIR}" + warn "Loop: ${LOOP_DEV:-none}" + warn "Cleanup: sudo losetup -d ${LOOP_DEV:-?} && rm -rf ${WORK_DIR}" + return "${rc}" + fi + info "Cleaning up..." + # Unmount any mounted dirs (ignore glob expansion failures) + umount "${WORK_DIR}"/mnt_* 2>/dev/null || true + umount "${WORK_DIR}"/phase_* 2>/dev/null || true + # Release loop device + [ -n "${LOOP_DEV}" ] && losetup -d "${LOOP_DEV}" 2>/dev/null || true + # Remove work dir + rm -rf "${WORK_DIR}" 2>/dev/null || true + [ "${rc}" != "0" ] && error "Test FAILED (exit ${rc})" + return "${rc}" +} +trap cleanup EXIT + +# ------------------------------------------------------------------- +# Step 1: Copy image and set up loop device +# ------------------------------------------------------------------- +mkdir -p "${WORK_DIR}" +info "Copying image ($(du -h "${SOURCE_IMAGE}" | cut -f1))..." +cp --sparse=always "${SOURCE_IMAGE}" "${IMAGE}" + +CURRENT_SIZE=$(stat -c%s "${IMAGE}") +TARGET_SIZE=$(( TARGET_SIZE_MB * 1048576 )) +if [ "${CURRENT_SIZE}" -lt "${TARGET_SIZE}" ]; then + info "Extending to ${TARGET_SIZE_MB}MB" + truncate -s "${TARGET_SIZE_MB}M" "${IMAGE}" +fi + +LOOP_DEV=$(losetup --find --show --partscan "${IMAGE}") +info "Loop device: ${LOOP_DEV}" +sleep 1 +[ ! -b "${LOOP_DEV}p1" ] && { sleep 2; partprobe "${LOOP_DEV}"; sleep 1; } +[ ! -b "${LOOP_DEV}p1" ] && { error "${LOOP_DEV}p1 not found"; exit 1; } + +# Read actual p2 start from the image +# sfdisk -d puts space between 'start=' and value, so use sed instead of awk +P2_START=$(sfdisk -d "${LOOP_DEV}" 2>/dev/null | grep 'p2' | sed 's/.*start=\s*//' | sed 's/,.*//') +[ -z "${P2_START}" ] && { error "Cannot read p2 start sector"; exit 1; } +info "p2 starts at sector ${P2_START}" + +# Expand p2 to fill the (possibly extended) image +# sfdisk needs ", +" to expand - just "," preserves existing size +echo ", +" | sfdisk -N 2 "${LOOP_DEV}" --no-reread 2>/dev/null || true +partprobe "${LOOP_DEV}" 2>/dev/null || true +sleep 1 +e2fsck -f -y "${LOOP_DEV}p2" 2>/dev/null || true +resize2fs "${LOOP_DEV}p2" 2>/dev/null || true + +# ------------------------------------------------------------------- +# Step 2: Inject test data onto the image +# ------------------------------------------------------------------- +info "Mounting image..." +mkdir -p "${WORK_DIR}/mnt_boot" "${WORK_DIR}/mnt_root" +mount "${LOOP_DEV}p1" "${WORK_DIR}/mnt_boot" +mount "${LOOP_DEV}p2" "${WORK_DIR}/mnt_root" + +# Add test markers to PiFinder_data (keep existing data from real image) +mkdir -p "${WORK_DIR}/mnt_root/home/pifinder/PiFinder_data" +echo '{"test": true}' > "${WORK_DIR}/mnt_root/home/pifinder/PiFinder_data/config.json" +dd if=/dev/urandom of="${WORK_DIR}/mnt_root/home/pifinder/PiFinder_data/observations.db" bs=1K count=32 2>/dev/null + +# Replace wpa_supplicant.conf with known test data +mkdir -p "${WORK_DIR}/mnt_root/etc/wpa_supplicant" +cat > "${WORK_DIR}/mnt_root/etc/wpa_supplicant/wpa_supplicant.conf" <<'WPAEOF' +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +update_config=1 +country=US + +network={ + ssid="HomeNetwork" + psk="password123" + key_mgmt=WPA-PSK +} + +network={ + ssid="Coffee Shop WiFi" + psk="cafelatte" + key_mgmt=WPA-PSK +} + +network={ + ssid="OpenNetwork" + key_mgmt=NONE +} +WPAEOF + +# Build fake NixOS tarball +info "Creating fake NixOS tarball..." +TARBALL_STAGING="${WORK_DIR}/tarball_staging" +mkdir -p "${TARBALL_STAGING}/boot" "${TARBALL_STAGING}/rootfs" + +echo "# NixOS extlinux.conf" > "${TARBALL_STAGING}/boot/extlinux.conf" +dd if=/dev/urandom of="${TARBALL_STAGING}/boot/Image" bs=1K count=128 2>/dev/null + +mkdir -p "${TARBALL_STAGING}/rootfs/nix/store" +mkdir -p "${TARBALL_STAGING}/rootfs/etc/NetworkManager/system-connections" +mkdir -p "${TARBALL_STAGING}/rootfs/home/pifinder" +echo "NixOS rootfs marker" > "${TARBALL_STAGING}/rootfs/etc/NIXOS" +dd if=/dev/urandom of="${TARBALL_STAGING}/rootfs/nix/store/fakepkg" bs=1K count=256 2>/dev/null +echo '{"version": "2.5.0"}' > "${TARBALL_STAGING}/manifest.json" + +tar czf "${WORK_DIR}/mnt_root/home/pifinder/pifinder-nixos-migration.tar.gz" \ + -C "${TARBALL_STAGING}" boot rootfs manifest.json + +TARBALL_SIZE=$(stat -c%s "${WORK_DIR}/mnt_root/home/pifinder/pifinder-nixos-migration.tar.gz") +PIFINDER_DATA_PATH="/home/pifinder/PiFinder_data" + +# Estimate backup size to calculate staging area +# Images compress poorly (JPEG/PNG already compressed), so use conservative 85% +PIFINDER_DATA_ON_ROOT="${WORK_DIR}/mnt_root${PIFINDER_DATA_PATH}" +DATA_SIZE_RAW=$(du -sb "${PIFINDER_DATA_ON_ROOT}" --exclude='captures' --exclude='screenshots' 2>/dev/null | cut -f1) +BACKUP_EST_BYTES=$(( DATA_SIZE_RAW * 85 / 100 )) # Conservative: images don't compress well +STAGING_NEED_BYTES=$(( TARBALL_SIZE + BACKUP_EST_BYTES + 209715200 )) # +200MB margin +STAGING_SIZE_MB=$(( (STAGING_NEED_BYTES / 1048576) + 1 )) +# Minimum 1GB staging +[ "${STAGING_SIZE_MB}" -lt 1024 ] && STAGING_SIZE_MB=1024 +info "Estimated staging need: ${STAGING_SIZE_MB}MB (data ${DATA_SIZE_RAW}, backup est ${BACKUP_EST_BYTES})" + +# Migration flag on boot +touch "${WORK_DIR}/mnt_boot/nixos_migration" + +umount "${WORK_DIR}/mnt_boot" +umount "${WORK_DIR}/mnt_root" + +info "Injected: tarball ${TARBALL_SIZE} bytes" + +# ------------------------------------------------------------------- +# Step 3: Write migration metadata +# ------------------------------------------------------------------- +cat > /tmp/test_migration_meta_$$ </dev/null || fail "sfdisk shrink" +partprobe "${SD_DEV}" 2>/dev/null || true +sleep 1 + +STAGING_START_BYTE=$(( (P2_START + P2_NEW_SECTORS) * 512 )) +show 20 "Staging area at byte ${STAGING_START_BYTE}" + +# --- Copy tarball + stream backup to staging area --- +show 22 "Mounting shrunk root" +mount -t ext4 -o ro "${ROOT_DEV}" "${MOUNT_ROOT}" || fail "mount shrunk root" + +TARBALL_ON_ROOT="${MOUNT_ROOT}${TARBALL_PATH}" +PIFINDER_DATA_ON_ROOT="${MOUNT_ROOT}${PIFINDER_DATA_PATH}" + +# Layout: header (4K) | tarball | backup +TARBALL_ALIGNED=$(( (TARBALL_SIZE + 4095) / 4096 * 4096 )) +TARBALL_STAGING_BYTE=$(( STAGING_START_BYTE + 4096 )) +BACKUP_STAGING_BYTE=$(( TARBALL_STAGING_BYTE + TARBALL_ALIGNED )) + +show 25 "Copying tarball to staging" +dd if="${TARBALL_ON_ROOT}" of="${SD_DEV}" bs=4096 \ + seek=$(( TARBALL_STAGING_BYTE / 4096 )) conv=notrunc 2>/dev/null || fail "tarball stage" + +show 35 "Creating backup" +# Create backup in tmpfs (RAM) then copy to staging +# Exclude captures and screenshots (user-generated ephemeral data) +BACKUP_TMP="/tmp/pifinder_backup_$$.tar.gz" +tar czf "${BACKUP_TMP}" -C "$(dirname "${PIFINDER_DATA_ON_ROOT}")" \ + --exclude='PiFinder_data/captures' \ + --exclude='PiFinder_data/screenshots' \ + "$(basename "${PIFINDER_DATA_ON_ROOT}")" 2>/dev/null || fail "backup create" + +BACKUP_SIZE=$(stat -c%s "${BACKUP_TMP}") +show 37 "Backup created (${BACKUP_SIZE} bytes)" + +show 38 "Copying backup to staging" +dd if="${BACKUP_TMP}" of="${SD_DEV}" bs=4096 \ + seek=$(( BACKUP_STAGING_BYTE / 4096 )) conv=notrunc 2>/dev/null || fail "backup stage" +rm -f "${BACKUP_TMP}" +show 40 "Backup staged" + +umount "${MOUNT_ROOT}" + +# Write header with actual backup size +HEADER_FILE="/tmp/staging_header_$$" +dd if=/dev/zero of="${HEADER_FILE}" bs=4096 count=1 2>/dev/null +printf "PFMIGRATE1\ntarball_size=%s\nbackup_size=%s\n" "${TARBALL_SIZE}" "${BACKUP_SIZE}" | \ + dd of="${HEADER_FILE}" conv=notrunc 2>/dev/null +dd if="${HEADER_FILE}" of="${SD_DEV}" bs=4096 seek=$(( STAGING_START_BYTE / 4096 )) conv=notrunc 2>/dev/null + +# Verify header +MAGIC=$(dd if="${SD_DEV}" bs=4096 skip=$(( STAGING_START_BYTE / 4096 )) count=1 2>/dev/null | head -1) +[ "${MAGIC}" != "PFMIGRATE1" ] && fail "header verify failed (got: ${MAGIC})" + +show 42 "Staging verified" + +# === POINT OF NO RETURN === +show 45 "FORMATTING" + +mkfs.vfat -F 32 -n FIRMWARE "${BOOT_DEV}" || fail "mkfs.vfat" +show 50 "Format root" +mkfs.ext4 -F -L NIXOS_SD "${ROOT_DEV}" || fail "mkfs.ext4" +show 55 "Formatted" + +# --- Extract NixOS --- +show 57 "Extracting tarball" +mkdir -p "${MOUNT_NEW}" +mount -t ext4 "${ROOT_DEV}" "${MOUNT_NEW}" || fail "mount new root" + +TARBALL_SKIP_BLOCKS=$(( TARBALL_STAGING_BYTE / 4096 )) +TARBALL_COUNT_BLOCKS=$(( (TARBALL_SIZE + 4095) / 4096 )) + +dd if="${SD_DEV}" bs=4096 skip="${TARBALL_SKIP_BLOCKS}" count="${TARBALL_COUNT_BLOCKS}" 2>/dev/null | \ + gunzip | tar xf - -C "${MOUNT_NEW}" || fail "extract" + +show 70 "Extracted" + +# Move boot/ to boot partition +mkdir -p "${MOUNT_BOOT}" +mount -t vfat "${BOOT_DEV}" "${MOUNT_BOOT}" || fail "mount boot" + +if [ -d "${MOUNT_NEW}/boot" ]; then + cp -a "${MOUNT_NEW}/boot/." "${MOUNT_BOOT}/" + rm -rf "${MOUNT_NEW}/boot" +fi + +# Move rootfs/ contents up +if [ -d "${MOUNT_NEW}/rootfs" ]; then + # Use find to avoid glob expansion issues with empty dirs + find "${MOUNT_NEW}/rootfs" -mindepth 1 -maxdepth 1 -exec mv {} "${MOUNT_NEW}/" \; 2>/dev/null || true + rmdir "${MOUNT_NEW}/rootfs" 2>/dev/null || true +fi +rm -f "${MOUNT_NEW}/manifest.json" + +show 78 "Layout done" + +# --- Restore backup --- +show 80 "Restoring user data" +mkdir -p "${MOUNT_NEW}/home/pifinder" + +BACKUP_SKIP_BLOCKS=$(( BACKUP_STAGING_BYTE / 4096 )) +BACKUP_COUNT_BLOCKS=$(( (BACKUP_SIZE + 4095) / 4096 )) + +dd if="${SD_DEV}" bs=4096 skip="${BACKUP_SKIP_BLOCKS}" count="${BACKUP_COUNT_BLOCKS}" 2>/dev/null | \ + gunzip | tar xf - -C "${MOUNT_NEW}/home/pifinder/" || fail "restore backup" + +show 85 "Data restored" + +# --- WiFi migration --- +show 88 "Migrating WiFi" +if [ -f "/tmp/wifi_test_$$/wpa_supplicant.conf" ]; then + NM_DIR="${MOUNT_NEW}/etc/NetworkManager/system-connections" + mkdir -p "${NM_DIR}" + + SSID="" PSK="" IN_NET=0 + while IFS= read -r line; do + line=$(echo "${line}" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') + case "${line}" in + network=*) IN_NET=1; SSID=""; PSK="" ;; + "}") + if [ "${IN_NET}" = "1" ] && [ -n "${SSID}" ]; then + FN=$(echo "${SSID}" | sed 's/[^a-zA-Z0-9_-]/_/g') + CONN="${NM_DIR}/${FN}.nmconnection" + { + printf "[connection]\nid=%s\ntype=wifi\nautoconnect=true\n\n" "${SSID}" + printf "[wifi]\nssid=%s\nmode=infrastructure\n\n" "${SSID}" + } > "${CONN}" + if [ -n "${PSK}" ]; then + printf "[wifi-security]\nkey-mgmt=wpa-psk\npsk=%s\n\n" "${PSK}" >> "${CONN}" + fi + printf "[ipv4]\nmethod=auto\n\n[ipv6]\nmethod=auto\n" >> "${CONN}" + chmod 600 "${CONN}" + fi + IN_NET=0 ;; + ssid=*) [ "${IN_NET}" = "1" ] && SSID=$(echo "${line}" | sed 's/^ssid="//;s/"$//') ;; + psk=*) [ "${IN_NET}" = "1" ] && PSK=$(echo "${line}" | sed 's/^psk="//;s/"$//') ;; + esac + done < "/tmp/wifi_test_$$/wpa_supplicant.conf" +fi + +show 92 "WiFi done" + +# --- Expand partition back --- +show 95 "Expanding root" +umount "${MOUNT_NEW}" +umount "${MOUNT_BOOT}" 2>/dev/null || true +# Expand partition to fill card +echo ", +" | sfdisk -N 2 "${SD_DEV}" 2>/dev/null || true +# Force kernel to re-read partition table +partprobe "${SD_DEV}" 2>/dev/null || true +losetup -c "${SD_DEV}" 2>/dev/null || true # Update loop device +sleep 2 +# Expand filesystem (e2fsck required before resize2fs on loop devices) +e2fsck -f -y "${ROOT_DEV}" 2>/dev/null || true +resize2fs "${ROOT_DEV}" 2>/dev/null || true + +sync +rm -rf "/tmp/wifi_test_$$" "/tmp/staging_header_$$" "/tmp/test_migration_meta_$$" + +show 100 "Migration complete!" + +# ------------------------------------------------------------------- +# Step 5: Verify results +# ------------------------------------------------------------------- +echo "" +info "========================================" +info "Verifying..." +info "========================================" + +ERRORS=0 + +mkdir -p "${WORK_DIR}/mnt_boot" "${WORK_DIR}/mnt_root" +mount "${LOOP_DEV}p1" "${WORK_DIR}/mnt_boot" +mount "${LOOP_DEV}p2" "${WORK_DIR}/mnt_root" + +check() { + local desc="$1" cond="$2" + if eval "${cond}"; then + pass "${desc}" + else + error "${desc}" + ERRORS=$((ERRORS + 1)) + fi +} + +check "Boot label = FIRMWARE" \ + '[ "$(blkid -s LABEL -o value "${LOOP_DEV}p1")" = "FIRMWARE" ]' + +check "Root label = NIXOS_SD" \ + '[ "$(blkid -s LABEL -o value "${LOOP_DEV}p2")" = "NIXOS_SD" ]' + +check "NixOS marker exists" \ + '[ -f "${WORK_DIR}/mnt_root/etc/NIXOS" ]' + +check "Nix store dir exists" \ + '[ -d "${WORK_DIR}/mnt_root/nix/store" ]' + +check "Boot: extlinux.conf" \ + '[ -f "${WORK_DIR}/mnt_boot/extlinux.conf" ]' + +check "Old boot files gone" \ + '[ ! -f "${WORK_DIR}/mnt_boot/config.txt" ]' + +check "PiFinder_data/config.json restored" \ + '[ -f "${WORK_DIR}/mnt_root/home/pifinder/PiFinder_data/config.json" ]' + +check "PiFinder_data/observations.db restored" \ + '[ -f "${WORK_DIR}/mnt_root/home/pifinder/PiFinder_data/observations.db" ]' + +NM_DIR="${WORK_DIR}/mnt_root/etc/NetworkManager/system-connections" + +check "WiFi: HomeNetwork migrated" \ + '[ -f "${NM_DIR}/HomeNetwork.nmconnection" ]' + +check "WiFi: HomeNetwork PSK correct" \ + 'grep -q "psk=password123" "${NM_DIR}/HomeNetwork.nmconnection" 2>/dev/null' + +check "WiFi: Coffee_Shop_WiFi migrated" \ + '[ -f "${NM_DIR}/Coffee_Shop_WiFi.nmconnection" ]' + +check "WiFi: OpenNetwork migrated" \ + '[ -f "${NM_DIR}/OpenNetwork.nmconnection" ]' + +check "WiFi: OpenNetwork has no PSK" \ + '! grep -q "wifi-security" "${NM_DIR}/OpenNetwork.nmconnection" 2>/dev/null' + +for f in "${NM_DIR}"/*.nmconnection; do + [ ! -f "$f" ] && continue + check "WiFi: $(basename "$f") perms=600" \ + '[ "$(stat -c%a "'"$f"'")" = "600" ]' +done + +ROOT_BLOCKS=$(dumpe2fs -h "${LOOP_DEV}p2" 2>/dev/null | awk '/Block count/ {print $3}') +ROOT_BSIZE=$(dumpe2fs -h "${LOOP_DEV}p2" 2>/dev/null | awk '/Block size/ {print $3}') +ROOT_GB=$(( ROOT_BLOCKS * ROOT_BSIZE / 1073741824 )) +check "Root FS expanded (${ROOT_GB}GB >= 28GB)" \ + '[ "${ROOT_GB}" -ge 28 ]' + +umount "${WORK_DIR}/mnt_boot" +umount "${WORK_DIR}/mnt_root" + +echo "" +echo "========================================" +if [ "${ERRORS}" = "0" ]; then + pass "All ${ERRORS:-0} checks passed!" +else + error "${ERRORS} check(s) failed" +fi +echo "========================================" + +exit "${ERRORS}" diff --git a/python/tests/test_software.py b/python/tests/test_software.py new file mode 100644 index 000000000..67e82311d --- /dev/null +++ b/python/tests/test_software.py @@ -0,0 +1,129 @@ +from unittest.mock import patch, MagicMock + +import pytest +import requests + +from PiFinder.ui.software import ( + update_needed, + _strip_markdown, + _fetch_migration_gate, + _UNLOCK_SEQUENCE, +) + + +@pytest.mark.unit +class TestUpdateNeeded: + def test_newer_version_available(self): + assert update_needed("2.3.0", "2.4.0") is True + + def test_same_version(self): + assert update_needed("2.4.0", "2.4.0") is False + + def test_older_version(self): + assert update_needed("2.5.0", "2.4.0") is False + + def test_major_version_bump(self): + assert update_needed("1.9.9", "2.0.0") is True + + def test_patch_bump(self): + assert update_needed("2.4.0", "2.4.1") is True + + def test_garbage_input_returns_true(self): + assert update_needed("garbage", "2.4.0") is True + + def test_empty_string_returns_true(self): + assert update_needed("", "") is True + + def test_partial_version_returns_true(self): + assert update_needed("2.4", "2.5.0") is True + + def test_unknown_returns_true(self): + assert update_needed("2.4.0", "Unknown") is True + + +@pytest.mark.unit +class TestUnlockSequence: + def test_sequence_length(self): + assert len(_UNLOCK_SEQUENCE) == 7 + + def test_sequence_content(self): + assert _UNLOCK_SEQUENCE == ["square"] * 7 + + +@pytest.mark.unit +class TestStripMarkdown: + def test_removes_headings(self): + assert _strip_markdown("# Hello") == "Hello" + assert _strip_markdown("## Sub") == "Sub" + + def test_removes_bold(self): + assert _strip_markdown("**bold**") == "bold" + + def test_removes_italic(self): + assert _strip_markdown("*italic*") == "italic" + + def test_removes_links(self): + assert _strip_markdown("[text](http://example.com)") == "text" + + def test_removes_backticks(self): + assert _strip_markdown("`code`") == "code" + + def test_preserves_plain_text(self): + assert _strip_markdown("Hello world") == "Hello world" + + def test_multiline(self): + md = "# Title\n\nSome **bold** text.\n- item" + result = _strip_markdown(md) + assert "Title" in result + assert "bold" in result + assert "**" not in result + + +def _mock_response(text, status_code=200): + resp = MagicMock() + resp.status_code = status_code + resp.text = text + return resp + + +@pytest.mark.unit +class TestFetchMigrationGate: + @patch("PiFinder.ui.software.requests.get") + def test_returns_true_when_gate_is_1(self, mock_get): + mock_get.return_value = _mock_response("1") + assert _fetch_migration_gate() is True + + @patch("PiFinder.ui.software.requests.get") + def test_returns_true_when_gate_is_1_with_whitespace(self, mock_get): + mock_get.return_value = _mock_response("1\n") + assert _fetch_migration_gate() is True + + @patch("PiFinder.ui.software.requests.get") + def test_returns_false_when_gate_is_0(self, mock_get): + mock_get.return_value = _mock_response("0") + assert _fetch_migration_gate() is False + + @patch("PiFinder.ui.software.requests.get") + def test_returns_false_when_empty(self, mock_get): + mock_get.return_value = _mock_response("") + assert _fetch_migration_gate() is False + + @patch("PiFinder.ui.software.requests.get") + def test_returns_false_on_http_error(self, mock_get): + mock_get.return_value = _mock_response("1", status_code=404) + assert _fetch_migration_gate() is False + + @patch("PiFinder.ui.software.requests.get") + def test_returns_false_on_connection_error(self, mock_get): + mock_get.side_effect = requests.exceptions.ConnectionError + assert _fetch_migration_gate() is False + + @patch("PiFinder.ui.software.requests.get") + def test_returns_false_on_timeout(self, mock_get): + mock_get.side_effect = requests.exceptions.Timeout + assert _fetch_migration_gate() is False + + @patch("PiFinder.ui.software.requests.get") + def test_returns_false_for_arbitrary_text(self, mock_get): + mock_get.return_value = _mock_response("yes") + assert _fetch_migration_gate() is False diff --git a/versions.json b/versions.json new file mode 100644 index 000000000..0cb9298fc --- /dev/null +++ b/versions.json @@ -0,0 +1,36 @@ +{ + "channels": { + "stable": { + "description": "Tested releases", + "versions": [ + { + "version": "2.4.0", + "ref": "v2.4.0", + "date": "2025-07-01", + "notes": "Initial NixOS release" + } + ] + }, + "unstable": { + "description": "Latest development", + "versions": [ + { + "version": "2.5.0-dev", + "ref": "main", + "date": "2025-07-01", + "notes": "Development branch" + } + ] + }, + "beta": { + "versions": [ + { + "version": "2.5.0", + "ref": "v2.5.0-beta", + "date": "2026-02-05", + "notes": "flexible upgrades" + } + ] + } + } +} From 7cf1b2c924bdc74d79e314649c400cf4522a91e0 Mon Sep 17 00:00:00 2001 From: Mike Rosseel Date: Mon, 25 May 2026 13:37:31 +0200 Subject: [PATCH 02/10] fix(migration): align tetra3 sys.path with upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit upstream defines utils.tetra3_dir as the inner package path (python/PiFinder/tetra3/tetra3) and every sys.path.append(tetra3_dir) site relies on that. migration had the short submodule-root path plus an extra sys.path.append(tetra3_dir / 'tetra3') workaround in solver_main.py and ui/preview.py, but solver.py never got the workaround — so 'from tetra3 import cedar_detect_client' fails when the inner module does the bare 'import cedar_detect_pb2'. Take upstream's pattern verbatim: long tetra3_dir, single sys.path.append, no workaround. Fixes PR #433's nox ui_tests failure. --- python/PiFinder/solver_main.py | 1 - python/PiFinder/ui/preview.py | 1 - python/PiFinder/utils.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/python/PiFinder/solver_main.py b/python/PiFinder/solver_main.py index e74135a62..5964e0e11 100644 --- a/python/PiFinder/solver_main.py +++ b/python/PiFinder/solver_main.py @@ -22,7 +22,6 @@ from PiFinder import utils sys.path.append(str(utils.tetra3_dir)) -sys.path.append(str(utils.tetra3_dir / "tetra3")) import tetra3 from tetra3 import cedar_detect_client diff --git a/python/PiFinder/ui/preview.py b/python/PiFinder/ui/preview.py index d47689664..bac4ce300 100644 --- a/python/PiFinder/ui/preview.py +++ b/python/PiFinder/ui/preview.py @@ -20,7 +20,6 @@ from PiFinder.ui.ui_utils import outline_text sys.path.append(str(utils.tetra3_dir)) -sys.path.append(str(utils.tetra3_dir / "tetra3")) class UIPreview(UIModule): diff --git a/python/PiFinder/utils.py b/python/PiFinder/utils.py index faa5d5d49..523228537 100644 --- a/python/PiFinder/utils.py +++ b/python/PiFinder/utils.py @@ -10,7 +10,7 @@ cwd_dir = Path.cwd() pifinder_dir = Path("..") astro_data_dir = cwd_dir / pifinder_dir / "astro_data" -tetra3_dir = pifinder_dir / "python/PiFinder/tetra3" +tetra3_dir = pifinder_dir / "python/PiFinder/tetra3/tetra3" data_dir = Path(Path.home(), "PiFinder_data") pifinder_db = astro_data_dir / "pifinder_objects.db" observations_db = data_dir / "observations.db" From 535d7de2c7d4cd4f08afb2c2c1a91e06ea0bcb9a Mon Sep 17 00:00:00 2001 From: Mike Rosseel Date: Mon, 25 May 2026 14:03:23 +0200 Subject: [PATCH 03/10] fix+test: cover migration UIs in smoke harness + coerce progress to int MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two coupled changes for upstream's new test_all_ui_modules_covered guard (PR #438): - Wire UIMigrationConfirm and UIMigrationProgress into _DYNAMIC_IDS with item_definition fixtures. Their __init__ methods use .get() with defaults so a stub version_info dict exercises construction + key handlers. - UIReleaseNotes stays in _COVERAGE_SKIP — its active() fetches markdown over HTTP and needs a network mock. - UIMigrationProgress.update() was crashing under the smoke harness because sys_utils mock returned MagicMock for percent/status. Coerce percent to int and accept status only as str; on bad data keep the prior value. This also hardens against a corrupt /tmp/nixos_migration_progress JSON file at runtime. --- python/PiFinder/ui/software.py | 7 +++++-- python/tests/test_ui_modules.py | 23 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/python/PiFinder/ui/software.py b/python/PiFinder/ui/software.py index 8b76985a0..f37b5c27b 100644 --- a/python/PiFinder/ui/software.py +++ b/python/PiFinder/ui/software.py @@ -464,9 +464,12 @@ def update(self, force=False): try: progress = sys_utils.get_migration_progress() if progress: - self._progress = progress.get("percent", self._progress) + try: + self._progress = int(progress.get("percent", self._progress)) + except (TypeError, ValueError): + pass # bad/missing percent — keep prior value new_status = progress.get("status", self._status) - if new_status != self._status: + if isinstance(new_status, str) and new_status != self._status: self._status = new_status self._status_layout.set_text(self._status) except (AttributeError, Exception): diff --git a/python/tests/test_ui_modules.py b/python/tests/test_ui_modules.py index 1e3888e91..a2b27a016 100644 --- a/python/tests/test_ui_modules.py +++ b/python/tests/test_ui_modules.py @@ -79,6 +79,7 @@ from PiFinder.ui.sqm_calibration import UISQMCalibration from PiFinder.ui.sqm_sweep import UISQMSweep from PiFinder.ui.sqm_correction import UISQMCorrection +from PiFinder.ui.software import UIMigrationConfirm, UIMigrationProgress # --------------------------------------------------------------------------- # @@ -111,7 +112,8 @@ # UIModule subclasses that are intentionally *not* exercised, with the reason. # Keeps the completeness guard (test_all_ui_modules_covered) honest. _COVERAGE_SKIP: dict[str, str] = { - # (none currently -- UISQMCorrection is covered via the dynamic fixtures) + # (UISQMCorrection is covered via the dynamic fixtures) + "UIReleaseNotes": "fetches markdown via HTTP in active(); needs a network mock", } # Bound on the auto-sweep so a handler that keeps pushing modules can't run away. @@ -172,6 +174,8 @@ def _node_id(node) -> str: "UISQMCalibration", "UISQMSweep", "UISQMCorrection", + "UIMigrationConfirm", + "UIMigrationProgress", ] @@ -214,6 +218,23 @@ def _build_dynamic_item_definition(spec_id: str, sample_object) -> dict: "class": UISQMCorrection, "label": "sqm_correction", } + if spec_id == "UIMigrationConfirm": + # Pushed by UISoftware.key_square() after a 7x-square unlock. + return { + "name": "Confirm Migration", + "class": UIMigrationConfirm, + "version_info": {"version": "2.5.0"}, + "current_version": "2.4.0", + "label": "migration_confirm", + } + if spec_id == "UIMigrationProgress": + # Pushed by UIMigrationConfirm after the user confirms. + return { + "name": "Migration Progress", + "class": UIMigrationProgress, + "version_info": {"version": "2.5.0"}, + "label": "migration_progress", + } raise KeyError(spec_id) # pragma: no cover From 626e1b1c35f7990499ade723e92443a85e61b809 Mon Sep 17 00:00:00 2001 From: Mike Rosseel Date: Mon, 25 May 2026 17:15:52 +0200 Subject: [PATCH 04/10] chore: prune dead code and move dev artifacts off the branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dead code dropped from the tree: * python/PiFinder/sys_utils_nixos.py (~591 lines): never imported, get_sys_utils() has no NixOS dispatch path. The NixOS-side system utilities ship inside the migration tarball as python/PiFinder/sys_utils.py on the nixos branch. * python/pyproject.toml dbus/gi mypy ignores: only made sense for the above; belong on the nixos branch. * python/scripts/migration_calc.py (~509 lines): described an unimplemented A/B-partition layout; no caller in the active flow (nixos_migration.sh uses nixos_migration_calc.py instead). Out-of-tree (moved to local notes): * MIGRATION_BRANCH_STATE.md (107 lines): internal hand-off notes, not user-facing docs. * python/scripts/test_migration_loopdev.sh (~498 lines): offline test harness that re-implemented an older design (tar.gz + magic-header staging) rather than invoking nixos_migration_init.sh — it had already drifted from the real flow (which is tar.zst + RAM-staged). Useful as a future starting point for a real integration test but misleading to ship in the repo. Documentation: * migration_gate.txt: add header comment explaining the killswitch contract; update _fetch_migration_gate parser to skip "#" lines so the file is self-documenting without breaking semantics. --- MIGRATION_BRANCH_STATE.md | 107 ---- migration_gate.txt | 5 + python/PiFinder/sys_utils_nixos.py | 591 ----------------------- python/PiFinder/ui/software.py | 12 +- python/pyproject.toml | 4 - python/scripts/migration_calc.py | 509 ------------------- python/scripts/test_migration_loopdev.sh | 498 ------------------- 7 files changed, 15 insertions(+), 1711 deletions(-) delete mode 100644 MIGRATION_BRANCH_STATE.md delete mode 100644 python/PiFinder/sys_utils_nixos.py delete mode 100644 python/scripts/migration_calc.py delete mode 100755 python/scripts/test_migration_loopdev.sh diff --git a/MIGRATION_BRANCH_STATE.md b/MIGRATION_BRANCH_STATE.md deleted file mode 100644 index 811339803..000000000 --- a/MIGRATION_BRANCH_STATE.md +++ /dev/null @@ -1,107 +0,0 @@ -# Migration Branch State - -Branch: `migration` - -## Overview - -This branch implements an in-place OS migration from Raspberry Pi OS to NixOS on PiFinder hardware (Pi 4, 2GB+ RAM, 16GB+ SD). The user triggers it from the OLED UI; the system downloads a NixOS bootstrap tarball, builds a custom initramfs, reboots into it, repartitions the SD card, and extracts NixOS — all without removing the SD card. - -The migration is gated behind a 7x square-button secret code on the SOFTWARE screen. The secret code directly triggers migration to v2.5.0 with hardcoded URL/SHA256. Regular users see the normal version.txt update flow (same as `main`). - -## Migration Flow - -``` -User presses 7x Square on SOFTWARE screen - │ - ▼ -UIMigrationConfirm (OLED: version, size, "IRREVERSIBLE" warning) - │ Confirm - ▼ -UIMigrationProgress (OLED: progress bar, status text) - │ Calls sys_utils.start_nixos_migration() - ▼ -nixos_migration.sh (Phase 1: RPi OS, runs as background bash) - ├─ Install deps (e2fsprogs, dosfstools, fdisk) - ├─ Pre-flight checks via nixos_migration_calc.py - │ (Pi4? RAM>=1800MB? SD>=16GB? WiFi=Client?) - ├─ Download tarball (349MB) with progress → JSON file - ├─ Verify SHA256 - ├─ Build initramfs: - │ busybox + e2fsck + resize2fs + mke2fs + mkfs.vfat + sfdisk - │ + migration_progress (OLED C binary) + init script + metadata - ├─ Stage initramfs to /boot - ├─ Set initramfs= in config.txt - └─ Reboot (5s countdown) - │ - ▼ -nixos_migration_init.sh (Phase 2: Initramfs, runs from RAM) - ├─ Save WiFi credentials to RAM (wpa_supplicant → iwd format) - ├─ e2fsck root - ├─ Shrink root FS + partition (resize2fs + sfdisk) - ├─ Copy tarball + PiFinder_data backup to freed staging area (raw dd) - ├─ === POINT OF NO RETURN === - ├─ Format boot (FAT32) + root (ext4) - ├─ Extract NixOS tarball to new root - ├─ Populate boot partition - ├─ Migrate WiFi to iwd format (early, before user data) - ├─ Write resume metadata to /var/lib/pifinder-migration/ - ├─ Restore PiFinder_data from staging - ├─ Expand partition to fill SD - └─ reboot -f → boots into bootstrap NixOS (Phase 3, not in this branch) -``` - -## Files - -### UI (python/PiFinder/ui/software.py) - -Simple `UISoftware` from `main` (version.txt checker, Update/Cancel toggle) plus: -- `_UNLOCK_SEQUENCE` / `_record_key()` / `key_square()` — 7x square triggers migration -- `UIMigrationConfirm` — warning screen with version info, size, irreversibility notice -- `UIMigrationProgress` — progress bar + scrollable status text, polls `sys_utils` -- `UIReleaseNotes` / `_strip_markdown()` — fetches and renders markdown release notes - -No manifest/channel infrastructure — that lives on the `nixos` branch. - -### Migration Scripts - -| File | Purpose | -|------|---------| -| `python/scripts/nixos_migration.sh` | Phase 1 (RPi OS): pre-flight, download, initramfs build, boot config, reboot | -| `python/scripts/nixos_migration_init.sh` | Phase 2 (initramfs): shrink/stage/format/extract/restore/expand | -| `python/scripts/nixos_migration_calc.py` | Pre-flight validator: Pi model, RAM, SD size, free space, WiFi mode | -| `python/scripts/migration_progress.c` | Standalone C OLED driver for initramfs (SSD1351 SPI, 5x7 font, progress bar) | -| `python/scripts/migration_progress` | Compiled aarch64 binary of above | - -### Other Modified Files - -| File | Change | -|------|--------| -| `python/PiFinder/sys_utils.py` | `start_nixos_migration()`, `get_migration_progress()` | -| `python/PiFinder/solver.py` | tetra3 path fix, `solution.pop()`, missing key guards | -| `python/PiFinder/utils.py` | `tetra3_dir` path correction | - -### Tests - -`python/tests/test_software.py` — `TestUpdateNeeded`, `TestUnlockSequence`, `TestStripMarkdown` - -## Key Constants - -| Constant | Value | -|----------|-------| -| Bootstrap tarball URL | `mrosseel/PiFinder` release `v2.5.0-bootstrap` | -| SHA256 | `d5e5dc7bfde57bb958d0dc55804af6fb14265f12d9e27a02da0385847f9ba742` | -| Tarball size | 349 MB | -| Staging area | 8 GB at end of SD card | -| Min RAM | 1800 MB (2GB Pi reports ~1849MB) | -| Min SD | 16 GB | -| Secret code | 7x square button | -| Progress file | `/tmp/nixos_migration_progress` (JSON: percent + status) | -| OLED binary | SSD1351 via SPI0.0, DC=GPIO24, RST=GPIO25, 128x128 BGR565 | - -## Architecture Notes - -- **Progress pipeline**: `nixos_migration.sh` writes JSON to progress file → `sys_utils.get_migration_progress()` reads it → `UIMigrationProgress.update()` polls it → renders on OLED -- **Initramfs OLED**: compiled C binary included in initramfs, called by init script at each stage -- **WiFi migration**: wpa_supplicant.conf parsed and converted to iwd format — done early in initramfs before user data restore so network recovery is possible if restore fails -- **Resume support**: metadata written to `/var/lib/pifinder-migration/` on new root so Phase 3 (bootstrap NixOS, not in this branch) can resume if interrupted -- **Data staging**: raw `dd` to write tarball + backup to freed space at end of SD (after shrinking root), then reads it back after formatting — avoids needing double the disk space diff --git a/migration_gate.txt b/migration_gate.txt index 573541ac9..e8b91e6d5 100644 --- a/migration_gate.txt +++ b/migration_gate.txt @@ -1 +1,6 @@ +# Migration killswitch — read by UISoftware._fetch_migration_gate() once per +# Software screen entry from the 'release' branch via raw.githubusercontent.com. +# Lines starting with '#' are ignored; the first non-comment, non-blank line is +# the gate value. "1" unlocks the NixOS migration UI; anything else keeps it +# hidden. Only the value on the 'release' branch is consulted at runtime. 0 diff --git a/python/PiFinder/sys_utils_nixos.py b/python/PiFinder/sys_utils_nixos.py deleted file mode 100644 index 5e9020393..000000000 --- a/python/PiFinder/sys_utils_nixos.py +++ /dev/null @@ -1,591 +0,0 @@ -""" -NixOS-native system utilities for PiFinder. - -Replaces sys_utils.py's wpa_supplicant/hostapd/file-editing approach with: -- NetworkManager GLib bindings (gi.repository.NM) for WiFi management -- python-pam for password verification -- D-Bus for hostname/reboot/shutdown -- stdlib zipfile for backup/restore -- nixos-rebuild for camera switching and software updates -""" -import glob -import os -import subprocess -import socket -import time -import zipfile -import logging -from pathlib import Path -from typing import Optional - -import requests - -import dbus -import pam -from PiFinder import utils - -import gi - -gi.require_version("NM", "1.0") -from gi.repository import GLib, NM # noqa: E402 - -BACKUP_PATH = str(utils.data_dir / "PiFinder_backup.zip") -AP_CONNECTION_NAME = "PiFinder-AP" - -logger = logging.getLogger("SysUtils.NixOS") - - -# --------------------------------------------------------------------------- -# Internal helpers -# --------------------------------------------------------------------------- - -def _run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess: - """Run a command, logging failures. Used only for nixos-rebuild and systemctl.""" - result = subprocess.run(cmd, capture_output=True, text=True, **kwargs) - if result.returncode != 0: - logger.error( - "Command %s failed (rc=%d): %s", - cmd, result.returncode, result.stderr.strip(), - ) - return result - - -def _nm_client() -> NM.Client: - """Create a NetworkManager client (synchronous).""" - return NM.Client.new(None) - - -def _nm_run_async(async_fn, *args): - """ - Run an async NM operation synchronously by spinning a local GLib MainLoop. - - Usage: - result = _nm_run_async(client.add_connection_async, profile, True, None) - """ - loop = GLib.MainLoop.new(None, False) - state = {"result": None, "error": None} - - def callback(source, async_result, _user_data): - try: - # The finish method name matches the async method name: - # add_connection_async -> add_connection_finish - # delete_async -> delete_finish - # activate_connection_async -> activate_connection_finish - # deactivate_connection_async -> deactivate_connection_finish - # commit_changes_async -> commit_changes_finish - method_name = async_fn.__name__.replace("_async", "_finish") - finish_fn = getattr(source, method_name) - state["result"] = finish_fn(async_result) - except Exception as e: - state["error"] = e - finally: - loop.quit() - - async_fn(*args, callback, None) - loop.run() - - if state["error"]: - raise state["error"] - return state["result"] - - -def _get_system_bus() -> dbus.SystemBus: - return dbus.SystemBus() - - -# --------------------------------------------------------------------------- -# Network class — WiFi management via NM GLib bindings -# --------------------------------------------------------------------------- - -class Network: - """ - Provides wifi network info via NetworkManager GLib bindings (libnm). - """ - - def __init__(self): - self._client = _nm_client() - self._wifi_networks: list[dict] = [] - self._wifi_mode = self._detect_wifi_mode() - self.populate_wifi_networks() - - def _detect_wifi_mode(self) -> str: - """Detect whether we're in AP or Client mode.""" - for ac in self._client.get_active_connections(): - if ac.get_id() == AP_CONNECTION_NAME: - return "AP" - return "Client" - - def populate_wifi_networks(self) -> None: - """Get saved WiFi connections from NetworkManager.""" - self._wifi_networks = [] - network_id = 0 - for conn in self._client.get_connections(): - s_wifi = conn.get_setting_wireless() - if s_wifi is None: - continue - if conn.get_id() == AP_CONNECTION_NAME: - continue - ssid_bytes = s_wifi.get_ssid() - ssid = ssid_bytes.get_data().decode("utf-8") if ssid_bytes else "" - self._wifi_networks.append({ - "id": network_id, - "ssid": ssid, - "psk": None, - "key_mgmt": "WPA-PSK", - }) - network_id += 1 - - def get_wifi_networks(self): - return self._wifi_networks - - def delete_wifi_network(self, network_id): - """Delete a saved WiFi connection.""" - if network_id < 0 or network_id >= len(self._wifi_networks): - logger.error("Invalid network_id: %d", network_id) - return - ssid = self._wifi_networks[network_id]["ssid"] - for conn in self._client.get_connections(): - if conn.get_id() == ssid: - try: - _nm_run_async(conn.delete_async, None) - except Exception as e: - logger.error("Failed to delete connection '%s': %s", ssid, e) - break - self.populate_wifi_networks() - - def add_wifi_network(self, ssid, key_mgmt, psk=None): - """Add and connect to a WiFi network.""" - profile = NM.SimpleConnection.new() - - s_con = NM.SettingConnection.new() - s_con.set_property(NM.SETTING_CONNECTION_ID, ssid) - s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless") - s_con.set_property(NM.SETTING_CONNECTION_AUTOCONNECT, True) - profile.add_setting(s_con) - - s_wifi = NM.SettingWireless.new() - s_wifi.set_property( - NM.SETTING_WIRELESS_SSID, - GLib.Bytes.new(ssid.encode("utf-8")), - ) - s_wifi.set_property(NM.SETTING_WIRELESS_MODE, "infrastructure") - profile.add_setting(s_wifi) - - if key_mgmt == "WPA-PSK" and psk: - s_wsec = NM.SettingWirelessSecurity.new() - s_wsec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk") - s_wsec.set_property(NM.SETTING_WIRELESS_SECURITY_PSK, psk) - profile.add_setting(s_wsec) - - s_ip4 = NM.SettingIP4Config.new() - s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto") - profile.add_setting(s_ip4) - - try: - _nm_run_async( - self._client.add_and_activate_connection_async, - profile, - self._client.get_device_by_iface("wlan0"), - None, - None, - ) - except Exception as e: - logger.error("Failed to add WiFi network '%s': %s", ssid, e) - - self.populate_wifi_networks() - - def get_ap_name(self) -> str: - """Get the current AP SSID from the PiFinder-AP profile.""" - for conn in self._client.get_connections(): - if conn.get_id() == AP_CONNECTION_NAME: - s_wifi = conn.get_setting_wireless() - if s_wifi: - ssid_bytes = s_wifi.get_ssid() - if ssid_bytes: - return ssid_bytes.get_data().decode("utf-8") - return "PiFinderAP" - - def set_ap_name(self, ap_name: str) -> None: - """Change the AP SSID.""" - if ap_name == self.get_ap_name(): - return - for conn in self._client.get_connections(): - if conn.get_id() == AP_CONNECTION_NAME: - s_wifi = conn.get_setting_wireless() - if s_wifi: - s_wifi.set_property( - NM.SETTING_WIRELESS_SSID, - GLib.Bytes.new(ap_name.encode("utf-8")), - ) - try: - _nm_run_async(conn.commit_changes_async, True, None) - except Exception as e: - logger.error("Failed to update AP SSID: %s", e) - return - - def get_host_name(self) -> str: - return socket.gethostname() - - def get_connected_ssid(self) -> str: - """Returns the SSID of the connected wifi network.""" - if self.wifi_mode() == "AP": - return "" - device = self._client.get_device_by_iface("wlan0") - if device is None: - return "" - ac = device.get_active_connection() - if ac is None: - return "" - conn = ac.get_connection() - if conn is None: - return "" - s_wifi = conn.get_setting_wireless() - if s_wifi is None: - return "" - ssid_bytes = s_wifi.get_ssid() - if ssid_bytes is None: - return "" - return ssid_bytes.get_data().decode("utf-8") - - def set_host_name(self, hostname: str) -> None: - """Set hostname via D-Bus to org.freedesktop.hostname1.""" - if hostname == self.get_host_name(): - return - try: - bus = _get_system_bus() - hostnamed = bus.get_object( - "org.freedesktop.hostname1", - "/org/freedesktop/hostname1", - ) - iface = dbus.Interface(hostnamed, "org.freedesktop.hostname1") - iface.SetStaticHostname(hostname, False) - except dbus.DBusException as e: - logger.error("Failed to set hostname via D-Bus: %s", e) - - def wifi_mode(self) -> str: - return self._wifi_mode - - def set_wifi_mode(self, mode: str) -> None: - if mode == self._wifi_mode: - return - if mode == "AP": - self._activate_connection(AP_CONNECTION_NAME) - elif mode == "Client": - self._deactivate_connection(AP_CONNECTION_NAME) - self._wifi_mode = mode - - def _activate_connection(self, name: str) -> None: - """Activate a saved connection by name.""" - conn = None - for c in self._client.get_connections(): - if c.get_id() == name: - conn = c - break - if conn is None: - logger.error("Connection '%s' not found", name) - return - device = self._client.get_device_by_iface("wlan0") - try: - _nm_run_async( - self._client.activate_connection_async, - conn, device, None, None, - ) - except Exception as e: - logger.error("Failed to activate '%s': %s", name, e) - - def _deactivate_connection(self, name: str) -> None: - """Deactivate an active connection by name.""" - for ac in self._client.get_active_connections(): - if ac.get_id() == name: - try: - _nm_run_async( - self._client.deactivate_connection_async, ac, None, - ) - except Exception as e: - logger.error("Failed to deactivate '%s': %s", name, e) - return - logger.warning("No active connection named '%s' to deactivate", name) - - def local_ip(self) -> str: - if self._wifi_mode == "AP": - return "10.10.10.1" - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - s.connect(("192.255.255.255", 1)) - ip = s.getsockname()[0] - except Exception: - ip = "NONE" - finally: - s.close() - return ip - - -# --------------------------------------------------------------------------- -# Backup / restore (stdlib zipfile) -# --------------------------------------------------------------------------- - -def remove_backup(): - """Removes backup file.""" - path = Path(BACKUP_PATH) - if path.exists(): - path.unlink() - - -def backup_userdata() -> str: - """ - Back up userdata to a single zip file. - - Backs up: - config.json - observations.db - obslists/* - """ - remove_backup() - - files = [ - utils.data_dir / "config.json", - utils.data_dir / "observations.db", - ] - for p in utils.data_dir.glob("obslists/*"): - files.append(p) - - with zipfile.ZipFile(BACKUP_PATH, "w", zipfile.ZIP_DEFLATED) as zf: - for filepath in files: - filepath = Path(filepath) - if filepath.exists(): - zf.write(filepath, filepath.relative_to("/")) - - return BACKUP_PATH - - -def restore_userdata(zip_path: str) -> None: - """ - Restore userdata from a zip backup. - OVERWRITES existing data! - """ - with zipfile.ZipFile(zip_path, "r") as zf: - zf.extractall("/") - - -# --------------------------------------------------------------------------- -# System control (systemctl subprocess + D-Bus for reboot/shutdown) -# --------------------------------------------------------------------------- - -def restart_pifinder() -> None: - """Restart the PiFinder service.""" - logger.info("SYS: Restarting PiFinder") - _run(["sudo", "systemctl", "restart", "pifinder"]) - - -def restart_system() -> None: - """Restart the system via D-Bus to login1.""" - logger.info("SYS: Initiating System Restart") - try: - bus = _get_system_bus() - login1 = bus.get_object( - "org.freedesktop.login1", - "/org/freedesktop/login1", - ) - manager = dbus.Interface(login1, "org.freedesktop.login1.Manager") - manager.Reboot(False) - except dbus.DBusException as e: - logger.error("D-Bus reboot failed, falling back to subprocess: %s", e) - _run(["sudo", "shutdown", "-r", "now"]) - - -def shutdown() -> None: - """Shut down the system via D-Bus to login1.""" - logger.info("SYS: Initiating Shutdown") - try: - bus = _get_system_bus() - login1 = bus.get_object( - "org.freedesktop.login1", - "/org/freedesktop/login1", - ) - manager = dbus.Interface(login1, "org.freedesktop.login1.Manager") - manager.PowerOff(False) - except dbus.DBusException as e: - logger.error("D-Bus shutdown failed, falling back to subprocess: %s", e) - _run(["sudo", "shutdown", "now"]) - - -# --------------------------------------------------------------------------- -# Software updates — async upgrade via systemd service -# --------------------------------------------------------------------------- - -UPGRADE_STATE_IDLE = "idle" -UPGRADE_STATE_RUNNING = "running" -UPGRADE_STATE_SUCCESS = "success" -UPGRADE_STATE_FAILED = "failed" - -VERSIONS_URL = ( - "https://raw.githubusercontent.com/mrosseel/PiFinder/release/versions.json" -) - -UPGRADE_REF_FILE = Path("/run/pifinder/upgrade-ref") - - -def fetch_version_manifest() -> Optional[dict]: - """Fetch the channel/version manifest from GitHub.""" - try: - resp = requests.get(VERSIONS_URL, timeout=10) - resp.raise_for_status() - return resp.json() - except Exception as e: - logger.error("Failed to fetch version manifest: %s", e) - return None - - -def get_versions_for_channel(channel: str) -> list[dict]: - """Get available versions for a channel. - - Returns list of {version, ref, date, notes}. - """ - manifest = fetch_version_manifest() - if manifest is None: - return [] - return manifest.get("channels", {}).get(channel, {}).get("versions", []) - - -def get_available_channels() -> list[str]: - """Get list of available channel names.""" - manifest = fetch_version_manifest() - if manifest is None: - return ["stable"] - return list(manifest.get("channels", {}).keys()) - - -def start_upgrade(ref: str = "release") -> bool: - """Start pifinder-upgrade.service with a specific git ref. - - Writes the ref to /run/pifinder/upgrade-ref for the service to read. - Returns True if the service was started successfully. - """ - try: - UPGRADE_REF_FILE.write_text(ref) - except OSError as e: - logger.error("Failed to write upgrade ref file: %s", e) - return False - - _run(["sudo", "systemctl", "reset-failed", "pifinder-upgrade.service"]) - result = _run([ - "sudo", "systemctl", "start", "--no-block", - "pifinder-upgrade.service", - ]) - return result.returncode == 0 - - -def get_upgrade_state() -> str: - """Poll upgrade service state.""" - result = _run(["systemctl", "is-active", "pifinder-upgrade.service"]) - status = result.stdout.strip() - if status == "activating": - return UPGRADE_STATE_RUNNING - elif status == "active": - return UPGRADE_STATE_SUCCESS - elif status == "failed": - return UPGRADE_STATE_FAILED - return UPGRADE_STATE_IDLE - - -def get_upgrade_log_tail(lines: int = 3) -> str: - """Last N lines from upgrade journal for UI display.""" - result = _run([ - "journalctl", "-u", "pifinder-upgrade.service", - "-n", str(lines), "--no-pager", "-o", "cat", - ]) - return result.stdout.strip() if result.returncode == 0 else "" - - -def update_software() -> bool: - """Blocking wrapper for backward compatibility (uses default ref).""" - if not start_upgrade(): - return False - while True: - time.sleep(10) - state = get_upgrade_state() - if state == UPGRADE_STATE_SUCCESS: - return True - elif state == UPGRADE_STATE_FAILED: - return False - - -# --------------------------------------------------------------------------- -# Password management (python-pam + chpasswd) -# --------------------------------------------------------------------------- - -def verify_password(username: str, password: str) -> bool: - """Verify a password against PAM.""" - p = pam.pam() - return p.authenticate(username, password, service="login") - - -def change_password(username: str, current_password: str, new_password: str) -> bool: - """Change the user password via chpasswd.""" - if not verify_password(username, current_password): - return False - result = subprocess.run( - ["sudo", "chpasswd"], - input=f"{username}:{new_password}\n", - capture_output=True, - text=True, - ) - return result.returncode == 0 - - -# --------------------------------------------------------------------------- -# Camera switching (nixos-rebuild + reboot) -# --------------------------------------------------------------------------- - -def switch_camera(cam_type: str) -> None: - """ - Switch camera by rebuilding NixOS with the appropriate camera type. - Requires reboot (dtoverlay change). - """ - logger.info("SYS: Switching camera to %s via nixos-rebuild", cam_type) - flake_path = str(utils.home_dir / "PiFinder") - result = _run([ - "sudo", "nixos-rebuild", "switch", - "--flake", f"{flake_path}#pifinder-{cam_type}", - ]) - if result.returncode == 0: - restart_system() - else: - logger.error("SYS: Camera switch rebuild failed: %s", result.stderr) - - -def switch_cam_imx477() -> None: - logger.info("SYS: Switching cam to imx477") - switch_camera("imx477") - - -def switch_cam_imx296() -> None: - logger.info("SYS: Switching cam to imx296") - switch_camera("imx296") - - -def switch_cam_imx462() -> None: - logger.info("SYS: Switching cam to imx462") - switch_camera("imx462") - - -# --------------------------------------------------------------------------- -# GPSD config (declarative on NixOS — no-ops) -# --------------------------------------------------------------------------- - -def check_and_sync_gpsd_config(baud_rate: int) -> bool: - """ - On NixOS, GPSD config is managed declaratively via services.nix. - This is a no-op. - """ - logger.info( - "SYS: GPSD baud rate %d — managed by NixOS configuration", baud_rate - ) - return False - - -def update_gpsd_config(baud_rate: int) -> None: - """On NixOS, GPSD configuration is declarative. This is a no-op.""" - logger.info( - "SYS: GPSD config is managed declaratively on NixOS (baud=%d)", baud_rate - ) diff --git a/python/PiFinder/ui/software.py b/python/PiFinder/ui/software.py index f37b5c27b..239478d4e 100644 --- a/python/PiFinder/ui/software.py +++ b/python/PiFinder/ui/software.py @@ -33,12 +33,20 @@ def _fetch_migration_gate() -> bool: - """Check remote gate file. Returns True only if content is '1'.""" + """Check remote gate file. Returns True only if the first non-comment, + non-blank line is exactly '1'.""" try: res = requests.get(MIGRATION_GATE_URL, timeout=REQUEST_TIMEOUT) - return res.status_code == 200 and res.text.strip() == "1" except requests.exceptions.RequestException: return False + if res.status_code != 200: + return False + for line in res.text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + return line == "1" + return False def update_needed(current_version: str, repo_version: str) -> bool: diff --git a/python/pyproject.toml b/python/pyproject.toml index 91c93f565..7430f3c0f 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -135,10 +135,6 @@ module = [ 'picamera2', 'bottle', 'libinput', - 'dbus', - 'gi', - 'gi.repository', - 'gi.repository.*', ] ignore_missing_imports = true ignore_errors = true diff --git a/python/scripts/migration_calc.py b/python/scripts/migration_calc.py deleted file mode 100644 index b9404d5dd..000000000 --- a/python/scripts/migration_calc.py +++ /dev/null @@ -1,509 +0,0 @@ -#!/usr/bin/env python3 -"""PiFinder A/B Migration - Pre-flight Validation and Configuration - -Validates the system for A/B partition migration and computes all -parameters needed by the initramfs migration script. Outputs a -shell-sourceable config file. - -Must be run as root on the target Raspberry Pi. - -Usage: - sudo python3 migration_calc.py --output /tmp/migration_config.sh - sudo python3 migration_calc.py --json -""" - -import argparse -import json -import os -import shutil -import subprocess -import sys -from dataclasses import dataclass, field -from pathlib import Path -from typing import List, Tuple - - -# ── Partition Layout Constants ────────────────────────────────────── -# -# 4-partition MBR layout (all primary): -# p1: boot (FAT32, 256 MiB) shared boot with os_prefix A/B -# p2: root-a (ext4, 3584 MiB) active root -# p3: root-b (ext4, 3584 MiB) update root -# p4: data (ext4, remaining) user data (PiFinder_data) -# -# A/B switching uses os_prefix in autoboot.txt with tryboot. -# Boot partition contains a/ and b/ subdirectories, each with their -# own cmdline.txt pointing to p2 or p3 respectively. -# -# All sizes in MiB unless noted. Sector size is 512 bytes. -# 1 MiB = 2048 sectors. - -BOOT_MIB = 256 -ROOT_MIB = 3584 # 3.5 GiB -SECTORS_PER_MIB = 2048 - -# Partition table in sectors (512 bytes each) -# fmt: off -P1_START_S = 8192 # 4 MiB boot (RPi OS standard) -P1_SIZE_S = BOOT_MIB * SECTORS_PER_MIB -P2_START_S = P1_START_S + P1_SIZE_S # 260 MiB root-a -P2_SIZE_S = ROOT_MIB * SECTORS_PER_MIB -P3_START_S = P2_START_S + P2_SIZE_S # 3844 MiB root-b -P3_SIZE_S = ROOT_MIB * SECTORS_PER_MIB -P4_START_S = P3_START_S + P3_SIZE_S # 7428 MiB data -# fmt: on - -# The data partition (p4) starts here; everything before is fixed layout -FIXED_LAYOUT_END_MIB = P4_START_S // SECTORS_PER_MIB # 7428 MiB - -# Safety thresholds -MIN_SD_SIZE_GIB = 16 -MIN_FREE_SPACE_MIB = 1024 -MIN_BOOT_FREE_MIB = 10 -MIN_RAM_MIB = 500 -SHRINK_HEADROOM_MIB = 200 # Extra space kept when shrinking filesystem -BACKUP_END_BUFFER_MIB = 64 # Reserved at very end of SD card -USER_BACKUP_OVERHEAD = 1.05 # 5% overhead for tar headers (no compression) - -# sfdisk partition table template (sector values filled in) -SFDISK_LAYOUT = f"""\ -label: dos - -/dev/mmcblk0p1 : start={P1_START_S}, size={P1_SIZE_S}, type=c, bootable -/dev/mmcblk0p2 : start={P2_START_S}, size={P2_SIZE_S}, type=83 -/dev/mmcblk0p3 : start={P3_START_S}, size={P3_SIZE_S}, type=83 -/dev/mmcblk0p4 : start={P4_START_S}, type=83 -""" - - -@dataclass -class ValidationResult: - errors: List[str] = field(default_factory=list) - warnings: List[str] = field(default_factory=list) - checks: List[str] = field(default_factory=list) - config: dict = field(default_factory=dict) - - @property - def ok(self) -> bool: - return len(self.errors) == 0 - - -def _run(cmd: list) -> Tuple[str, int]: - """Run shell command, return (stdout, returncode).""" - try: - result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) - return result.stdout.strip(), result.returncode - except (subprocess.TimeoutExpired, FileNotFoundError) as e: - return str(e), 1 - - -def _check_raspberry_pi(r: ValidationResult) -> bool: - """Check 1: Verify running on Raspberry Pi.""" - model_path = Path("/proc/device-tree/model") - if not model_path.exists(): - r.errors.append("Not running on Raspberry Pi") - return False - model = model_path.read_text().strip("\x00") - r.checks.append(f"Detected: {model}") - r.config["pi_model"] = model - return True - - -def _check_sd_card(r: ValidationResult, device: str) -> bool: - """Check 2: Verify SD card device exists.""" - if not os.path.exists(device): - r.errors.append(f"{device} not found") - return False - r.checks.append(f"SD card device: {device}") - return True - - -def _check_root_partition(r: ValidationResult, device: str) -> bool: - """Check 3: Verify root is on the expected partition.""" - root_src, _ = _run(["findmnt", "-n", "-o", "SOURCE", "/"]) - expected = f"{device}p2" - if root_src != expected: - r.errors.append( - f"Root not on {expected} (found: {root_src}). " - "Migration only works on standard SD card layout." - ) - return False - r.checks.append(f"Root filesystem: {root_src}") - return True - - -def _check_sd_size(r: ValidationResult, device: str) -> bool: - """Check 4: Verify SD card is large enough.""" - size_str, rc = _run(["blockdev", "--getsize64", device]) - if rc != 0: - r.errors.append("Cannot read SD card size") - return False - - sd_bytes = int(size_str) - sd_mib = sd_bytes // (1024 * 1024) - sd_gib = sd_bytes / (1024**3) - - if sd_gib < MIN_SD_SIZE_GIB: - r.errors.append( - f"SD card too small: {sd_gib:.1f} GiB (need {MIN_SD_SIZE_GIB} GiB+)" - ) - return False - - r.checks.append(f"SD card size: {sd_gib:.1f} GiB ({sd_mib} MiB)") - r.config["sd_size_bytes"] = sd_bytes - r.config["sd_size_mib"] = sd_mib - return True - - -def _check_free_space(r: ValidationResult) -> bool: - """Check 5: Verify enough free space on root. - - Excludes /home/pifinder/PiFinder_data from usage calculation since - it will be moved to the separate data partition during migration. - """ - st = os.statvfs("/") - free_mib = (st.f_bavail * st.f_frsize) // (1024 * 1024) - total_used_mib = ((st.f_blocks - st.f_bfree) * st.f_frsize) // (1024 * 1024) - - # Subtract PiFinder_data since it moves to data partition - pifinder_data = Path("/home/pifinder/PiFinder_data") - data_mib = 0 - if pifinder_data.exists(): - out, rc = _run(["du", "-sm", str(pifinder_data)]) - if rc == 0: - try: - data_mib = int(out.split()[0]) - except (ValueError, IndexError): - pass - - used_mib = total_used_mib - data_mib - - if free_mib < MIN_FREE_SPACE_MIB: - r.errors.append( - f"Insufficient free space: {free_mib} MiB (need {MIN_FREE_SPACE_MIB} MiB)" - ) - return False - - r.checks.append( - f"Root usage: {used_mib} MiB (excluding {data_mib} MiB PiFinder_data)" - ) - r.config["root_used_mib"] = used_mib - r.config["root_free_mib"] = free_mib - r.config["pifinder_data_mib"] = data_mib - return True - - -def _check_root_fits(r: ValidationResult) -> bool: - """Check 6: Verify current root usage fits in new partition.""" - used_mib = r.config.get("root_used_mib", 0) - max_allowed = ROOT_MIB - SHRINK_HEADROOM_MIB - - if used_mib > max_allowed: - r.errors.append( - f"Root uses {used_mib} MiB but new root partition is {ROOT_MIB} MiB " - f"(max usable: {max_allowed} MiB with {SHRINK_HEADROOM_MIB} MiB headroom)" - ) - return False - - r.checks.append(f"Root usage ({used_mib} MiB) fits in {ROOT_MIB} MiB partition") - return True - - -def _check_sd_health(r: ValidationResult, device: str) -> bool: - """Check 7: Verify SD card is not failing.""" - try: - with open("/proc/mounts") as f: - for line in f: - if f"{device}p2" in line and ",ro," in line: - r.errors.append("SD card is mounted read-only (possibly failing)") - return False - except OSError: - pass - - dmesg_out, _ = _run(["dmesg"]) - io_errors = [ - line.strip() - for line in dmesg_out.split("\n") - if "mmcblk0" in line.lower() - and ("error" in line.lower() or "fail" in line.lower()) - ] - if io_errors: - r.warnings.append("SD card showing I/O errors in dmesg:") - for line in io_errors[-3:]: - r.warnings.append(f" {line}") - else: - r.checks.append("SD card health: OK") - return True - - -def _check_required_tools(r: ValidationResult) -> bool: - """Check 8: Verify all required tools are installed.""" - required = [ - "sfdisk", - "dd", - "mkfs.ext4", - "e2fsck", - "resize2fs", - "dumpe2fs", - "cpio", - "gzip", - "zstd", - "blkid", - "md5sum", - "partprobe", - ] - missing = [t for t in required if not shutil.which(t)] - if missing: - r.errors.append(f"Missing required tools: {', '.join(missing)}") - return False - r.checks.append("Required tools: all present") - return True - - -def _check_boot_partition(r: ValidationResult) -> bool: - """Check 9: Verify boot partition is accessible with enough space.""" - # Detect boot mount point (Bullseye: /boot, Bookworm: /boot/firmware) - for boot_dir in ("/boot/firmware", "/boot"): - boot_src, rc = _run(["findmnt", "-n", "-o", "SOURCE", boot_dir]) - if rc == 0 and boot_src: - break - else: - r.errors.append("Boot partition is not mounted at /boot or /boot/firmware") - return False - - try: - st = os.statvfs(boot_dir) - free_mib = (st.f_bavail * st.f_frsize) // (1024 * 1024) - except OSError: - r.errors.append(f"Cannot stat {boot_dir}") - return False - - if free_mib < MIN_BOOT_FREE_MIB: - r.errors.append( - f"Insufficient space in {boot_dir}: {free_mib} MiB " - f"(need {MIN_BOOT_FREE_MIB} MiB)" - ) - return False - - r.checks.append(f"Boot partition: {boot_src} at {boot_dir} ({free_mib} MiB free)") - r.config["boot_dir"] = boot_dir - return True - - -def _check_system_state(r: ValidationResult) -> bool: - """Check 10: Verify system is not degraded.""" - state, _ = _run(["systemctl", "is-system-running"]) - if state in ("degraded", "maintenance"): - r.warnings.append( - f"System is in '{state}' state; some services may have failed" - ) - else: - r.checks.append(f"System state: {state}") - return True - - -def _compute_backup_params(r: ValidationResult, essential_only: bool = False) -> bool: - """Compute backup offsets and validate they fit on the SD card. - - Backup layout at END of SD card (working backwards from end): - [...partitions...] [root_backup] [user_data_tar] [64 MiB buffer] - - No boot backup needed — boot partition (p1) is unchanged by migration. - User data is moved (tar to raw SD, delete from root) so only one - copy exists at a time. - """ - sd_mib = r.config["sd_size_mib"] - used_mib = r.config["root_used_mib"] - - # The init script will use resize2fs -M to shrink to minimum, then - # read the actual size via dumpe2fs. For pre-flight validation, we - # estimate the shrunk size as used_space + headroom. - shrink_estimate_mib = used_mib + SHRINK_HEADROOM_MIB - r.config["shrink_estimate_mib"] = shrink_estimate_mib - - # Measure user data - pifinder_data = Path("/home/pifinder/PiFinder_data") - total_data_mib = 0 - - if pifinder_data.exists(): - if essential_only: - # Only measure essential files (databases, configs, obslists) - # Excludes: captures/, logs/, solver_debug_dumps/, screenshots/ - out, rc = _run( - [ - "du", - "-sm", - "--exclude=captures", - "--exclude=logs", - "--exclude=solver_debug_dumps", - "--exclude=screenshots", - "--exclude=images", - "--exclude=*.fits", - "--exclude=*.png", - "--exclude=*.jpg", - "--exclude=*.jpeg", - "--exclude=*.bmp", - str(pifinder_data), - ] - ) - else: - out, rc = _run(["du", "-sm", str(pifinder_data)]) - if rc == 0: - try: - total_data_mib = int(out.split()[0]) - except (ValueError, IndexError): - pass - - r.config["user_total_data_mib"] = total_data_mib - r.config["pifinder_data_mib"] = total_data_mib - r.config["essential_only"] = 1 if essential_only else 0 - - # User backup size (ALL user data with tar overhead). - user_backup_mib = int(total_data_mib * USER_BACKUP_OVERHEAD) + 10 - r.config["user_backup_size_mib"] = user_backup_mib - - # Backup layout at END of SD card (working backwards): - # [...partitions...] [root_backup] [user_data_tar] [buffer] - backup_end_mib = sd_mib - BACKUP_END_BUFFER_MIB - - # User data tar goes at the very end (before buffer) - user_backup_start = backup_end_mib - user_backup_mib - - # Root backup goes before user data tar (no boot backup needed) - root_backup_start = user_backup_start - shrink_estimate_mib - - r.config["root_backup_offset_mib"] = root_backup_start - - # Verify backup region doesn't overlap with new partition layout - if root_backup_start < FIXED_LAYOUT_END_MIB: - r.errors.append( - f"SD card too small for safe backup: " - f"leftmost backup at {root_backup_start} MiB but " - f"new partitions extend to {FIXED_LAYOUT_END_MIB} MiB" - ) - return False - - safety_gap = root_backup_start - FIXED_LAYOUT_END_MIB - r.checks.append( - f"Backup layout (end of SD): root@{root_backup_start}, " - f"user@{user_backup_start} MiB " - f"(safety gap: {safety_gap} MiB)" - ) - - if total_data_mib > 0: - r.checks.append( - f"User data: {total_data_mib} MiB (move to SD, backup: {user_backup_mib} MiB)" - ) - else: - r.checks.append("User data: none or empty") - - # Data partition size after migration - data_mib = sd_mib - FIXED_LAYOUT_END_MIB - r.config["data_partition_mib"] = data_mib - r.checks.append(f"Data partition after migration: ~{data_mib} MiB") - - return True - - -def validate( - device: str = "/dev/mmcblk0", essential_only: bool = False -) -> ValidationResult: - """Run all pre-flight checks and compute migration parameters.""" - r = ValidationResult() - - # Store layout constants in config for the init script - r.config["boot_mib"] = BOOT_MIB - r.config["root_mib"] = ROOT_MIB - r.config["fixed_layout_end_mib"] = FIXED_LAYOUT_END_MIB - r.config["backup_end_buffer_mib"] = BACKUP_END_BUFFER_MIB - r.config["device"] = device - - # Sequential checks (later checks depend on earlier ones) - checks = [ - lambda: _check_raspberry_pi(r), - lambda: _check_sd_card(r, device), - lambda: _check_root_partition(r, device), - lambda: _check_sd_size(r, device), - lambda: _check_free_space(r), - lambda: _check_root_fits(r), - lambda: _check_sd_health(r, device), - lambda: _check_required_tools(r), - lambda: _check_boot_partition(r), - lambda: _check_system_state(r), - lambda: _compute_backup_params(r, essential_only), - ] - - for check in checks: - if not check() and r.errors: - break # Stop on first error - - return r - - -def write_shell_config(config: dict, path: str) -> None: - """Write config as shell-sourceable file.""" - with open(path, "w") as f: - f.write("# PiFinder A/B Migration Configuration\n") - f.write("# Auto-generated by migration_calc.py — DO NOT EDIT\n\n") - for key in sorted(config): - val = config[key] - if isinstance(val, str): - f.write(f'{key.upper()}="{val}"\n') - else: - f.write(f"{key.upper()}={val}\n") - - # Also write the sfdisk layout - sfdisk_path = path.replace("_config.sh", "_sfdisk.txt") - with open(sfdisk_path, "w") as f: - f.write(SFDISK_LAYOUT) - - -def main() -> None: - parser = argparse.ArgumentParser( - description="PiFinder A/B Migration — Pre-flight Validation" - ) - parser.add_argument("--device", default="/dev/mmcblk0", help="SD card device path") - parser.add_argument( - "--output", - default="/tmp/migration_config.sh", - help="Path for shell config output", - ) - parser.add_argument( - "--json", action="store_true", help="Print JSON to stdout instead of shell vars" - ) - parser.add_argument( - "--essential-only", - action="store_true", - help="Only back up essential user data (databases, configs, obslists)", - ) - args = parser.parse_args() - - result = validate(args.device, essential_only=args.essential_only) - - # Print results - for msg in result.checks: - print(f" OK {msg}") - for msg in result.warnings: - print(f" WARN {msg}") - for msg in result.errors: - print(f" FAIL {msg}", file=sys.stderr) - - if not result.ok: - n = len(result.errors) - print(f"\nValidation failed with {n} error(s).", file=sys.stderr) - sys.exit(1) - - print(f"\nAll {len(result.checks)} checks passed.") - - if args.json: - json.dump(result.config, sys.stdout, indent=2) - print() - else: - write_shell_config(result.config, args.output) - print(f"Config written to: {args.output}") - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/python/scripts/test_migration_loopdev.sh b/python/scripts/test_migration_loopdev.sh deleted file mode 100755 index e27ffa461..000000000 --- a/python/scripts/test_migration_loopdev.sh +++ /dev/null @@ -1,498 +0,0 @@ -#!/bin/bash -# test_migration_loopdev.sh - Test migration against a real SD card image -# -# Copies a PiFinder SD card image, injects test data (fake NixOS tarball, -# backup, WiFi creds), then runs the full migration flow on the copy. -# -# Usage: sudo ./test_migration_loopdev.sh [--keep] -# image: path to SD card image (e.g. pifinder-mr.img). COPIED, not modified. -# --keep: don't clean up after test (inspect results manually) -# -# Requires: losetup, sfdisk, mkfs.ext4, mkfs.vfat, e2fsck, resize2fs -# -# Tests: partition shrink, staging area, format, tarball extraction, -# PiFinder_data restore, WiFi migration, partition re-expansion - -set -euo pipefail - -if [ $# -lt 1 ] || [ "$1" = "--help" ]; then - echo "Usage: sudo $0 [--keep]" - echo " image: path to PiFinder SD card image (will be copied)" - exit 1 -fi - -SOURCE_IMAGE="$1"; shift -KEEP=0 -[ "${1:-}" = "--keep" ] && KEEP=1 - -[ ! -f "${SOURCE_IMAGE}" ] && { echo "Image not found: ${SOURCE_IMAGE}"; exit 1; } - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -WORK_DIR="/tmp/migration_test_$$" -IMAGE="${WORK_DIR}/sd_card.img" -TARGET_SIZE_MB=32768 # extend to 32GB -# STAGING_SIZE_MB calculated dynamically after we know backup size - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -info() { echo -e "${GREEN}[INFO]${NC} $*"; } -warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -error() { echo -e "${RED}[FAIL]${NC} $*"; } -pass() { echo -e "${GREEN}[PASS]${NC} $*"; } - -LOOP_DEV="" - -cleanup() { - local rc=$? - if [ "${KEEP}" = "1" ] && [ "${rc}" = "0" ]; then - warn "Keeping: ${WORK_DIR}" - warn "Loop: ${LOOP_DEV:-none}" - warn "Cleanup: sudo losetup -d ${LOOP_DEV:-?} && rm -rf ${WORK_DIR}" - return "${rc}" - fi - info "Cleaning up..." - # Unmount any mounted dirs (ignore glob expansion failures) - umount "${WORK_DIR}"/mnt_* 2>/dev/null || true - umount "${WORK_DIR}"/phase_* 2>/dev/null || true - # Release loop device - [ -n "${LOOP_DEV}" ] && losetup -d "${LOOP_DEV}" 2>/dev/null || true - # Remove work dir - rm -rf "${WORK_DIR}" 2>/dev/null || true - [ "${rc}" != "0" ] && error "Test FAILED (exit ${rc})" - return "${rc}" -} -trap cleanup EXIT - -# ------------------------------------------------------------------- -# Step 1: Copy image and set up loop device -# ------------------------------------------------------------------- -mkdir -p "${WORK_DIR}" -info "Copying image ($(du -h "${SOURCE_IMAGE}" | cut -f1))..." -cp --sparse=always "${SOURCE_IMAGE}" "${IMAGE}" - -CURRENT_SIZE=$(stat -c%s "${IMAGE}") -TARGET_SIZE=$(( TARGET_SIZE_MB * 1048576 )) -if [ "${CURRENT_SIZE}" -lt "${TARGET_SIZE}" ]; then - info "Extending to ${TARGET_SIZE_MB}MB" - truncate -s "${TARGET_SIZE_MB}M" "${IMAGE}" -fi - -LOOP_DEV=$(losetup --find --show --partscan "${IMAGE}") -info "Loop device: ${LOOP_DEV}" -sleep 1 -[ ! -b "${LOOP_DEV}p1" ] && { sleep 2; partprobe "${LOOP_DEV}"; sleep 1; } -[ ! -b "${LOOP_DEV}p1" ] && { error "${LOOP_DEV}p1 not found"; exit 1; } - -# Read actual p2 start from the image -# sfdisk -d puts space between 'start=' and value, so use sed instead of awk -P2_START=$(sfdisk -d "${LOOP_DEV}" 2>/dev/null | grep 'p2' | sed 's/.*start=\s*//' | sed 's/,.*//') -[ -z "${P2_START}" ] && { error "Cannot read p2 start sector"; exit 1; } -info "p2 starts at sector ${P2_START}" - -# Expand p2 to fill the (possibly extended) image -# sfdisk needs ", +" to expand - just "," preserves existing size -echo ", +" | sfdisk -N 2 "${LOOP_DEV}" --no-reread 2>/dev/null || true -partprobe "${LOOP_DEV}" 2>/dev/null || true -sleep 1 -e2fsck -f -y "${LOOP_DEV}p2" 2>/dev/null || true -resize2fs "${LOOP_DEV}p2" 2>/dev/null || true - -# ------------------------------------------------------------------- -# Step 2: Inject test data onto the image -# ------------------------------------------------------------------- -info "Mounting image..." -mkdir -p "${WORK_DIR}/mnt_boot" "${WORK_DIR}/mnt_root" -mount "${LOOP_DEV}p1" "${WORK_DIR}/mnt_boot" -mount "${LOOP_DEV}p2" "${WORK_DIR}/mnt_root" - -# Add test markers to PiFinder_data (keep existing data from real image) -mkdir -p "${WORK_DIR}/mnt_root/home/pifinder/PiFinder_data" -echo '{"test": true}' > "${WORK_DIR}/mnt_root/home/pifinder/PiFinder_data/config.json" -dd if=/dev/urandom of="${WORK_DIR}/mnt_root/home/pifinder/PiFinder_data/observations.db" bs=1K count=32 2>/dev/null - -# Replace wpa_supplicant.conf with known test data -mkdir -p "${WORK_DIR}/mnt_root/etc/wpa_supplicant" -cat > "${WORK_DIR}/mnt_root/etc/wpa_supplicant/wpa_supplicant.conf" <<'WPAEOF' -ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev -update_config=1 -country=US - -network={ - ssid="HomeNetwork" - psk="password123" - key_mgmt=WPA-PSK -} - -network={ - ssid="Coffee Shop WiFi" - psk="cafelatte" - key_mgmt=WPA-PSK -} - -network={ - ssid="OpenNetwork" - key_mgmt=NONE -} -WPAEOF - -# Build fake NixOS tarball -info "Creating fake NixOS tarball..." -TARBALL_STAGING="${WORK_DIR}/tarball_staging" -mkdir -p "${TARBALL_STAGING}/boot" "${TARBALL_STAGING}/rootfs" - -echo "# NixOS extlinux.conf" > "${TARBALL_STAGING}/boot/extlinux.conf" -dd if=/dev/urandom of="${TARBALL_STAGING}/boot/Image" bs=1K count=128 2>/dev/null - -mkdir -p "${TARBALL_STAGING}/rootfs/nix/store" -mkdir -p "${TARBALL_STAGING}/rootfs/etc/NetworkManager/system-connections" -mkdir -p "${TARBALL_STAGING}/rootfs/home/pifinder" -echo "NixOS rootfs marker" > "${TARBALL_STAGING}/rootfs/etc/NIXOS" -dd if=/dev/urandom of="${TARBALL_STAGING}/rootfs/nix/store/fakepkg" bs=1K count=256 2>/dev/null -echo '{"version": "2.5.0"}' > "${TARBALL_STAGING}/manifest.json" - -tar czf "${WORK_DIR}/mnt_root/home/pifinder/pifinder-nixos-migration.tar.gz" \ - -C "${TARBALL_STAGING}" boot rootfs manifest.json - -TARBALL_SIZE=$(stat -c%s "${WORK_DIR}/mnt_root/home/pifinder/pifinder-nixos-migration.tar.gz") -PIFINDER_DATA_PATH="/home/pifinder/PiFinder_data" - -# Estimate backup size to calculate staging area -# Images compress poorly (JPEG/PNG already compressed), so use conservative 85% -PIFINDER_DATA_ON_ROOT="${WORK_DIR}/mnt_root${PIFINDER_DATA_PATH}" -DATA_SIZE_RAW=$(du -sb "${PIFINDER_DATA_ON_ROOT}" --exclude='captures' --exclude='screenshots' 2>/dev/null | cut -f1) -BACKUP_EST_BYTES=$(( DATA_SIZE_RAW * 85 / 100 )) # Conservative: images don't compress well -STAGING_NEED_BYTES=$(( TARBALL_SIZE + BACKUP_EST_BYTES + 209715200 )) # +200MB margin -STAGING_SIZE_MB=$(( (STAGING_NEED_BYTES / 1048576) + 1 )) -# Minimum 1GB staging -[ "${STAGING_SIZE_MB}" -lt 1024 ] && STAGING_SIZE_MB=1024 -info "Estimated staging need: ${STAGING_SIZE_MB}MB (data ${DATA_SIZE_RAW}, backup est ${BACKUP_EST_BYTES})" - -# Migration flag on boot -touch "${WORK_DIR}/mnt_boot/nixos_migration" - -umount "${WORK_DIR}/mnt_boot" -umount "${WORK_DIR}/mnt_root" - -info "Injected: tarball ${TARBALL_SIZE} bytes" - -# ------------------------------------------------------------------- -# Step 3: Write migration metadata -# ------------------------------------------------------------------- -cat > /tmp/test_migration_meta_$$ </dev/null || fail "sfdisk shrink" -partprobe "${SD_DEV}" 2>/dev/null || true -sleep 1 - -STAGING_START_BYTE=$(( (P2_START + P2_NEW_SECTORS) * 512 )) -show 20 "Staging area at byte ${STAGING_START_BYTE}" - -# --- Copy tarball + stream backup to staging area --- -show 22 "Mounting shrunk root" -mount -t ext4 -o ro "${ROOT_DEV}" "${MOUNT_ROOT}" || fail "mount shrunk root" - -TARBALL_ON_ROOT="${MOUNT_ROOT}${TARBALL_PATH}" -PIFINDER_DATA_ON_ROOT="${MOUNT_ROOT}${PIFINDER_DATA_PATH}" - -# Layout: header (4K) | tarball | backup -TARBALL_ALIGNED=$(( (TARBALL_SIZE + 4095) / 4096 * 4096 )) -TARBALL_STAGING_BYTE=$(( STAGING_START_BYTE + 4096 )) -BACKUP_STAGING_BYTE=$(( TARBALL_STAGING_BYTE + TARBALL_ALIGNED )) - -show 25 "Copying tarball to staging" -dd if="${TARBALL_ON_ROOT}" of="${SD_DEV}" bs=4096 \ - seek=$(( TARBALL_STAGING_BYTE / 4096 )) conv=notrunc 2>/dev/null || fail "tarball stage" - -show 35 "Creating backup" -# Create backup in tmpfs (RAM) then copy to staging -# Exclude captures and screenshots (user-generated ephemeral data) -BACKUP_TMP="/tmp/pifinder_backup_$$.tar.gz" -tar czf "${BACKUP_TMP}" -C "$(dirname "${PIFINDER_DATA_ON_ROOT}")" \ - --exclude='PiFinder_data/captures' \ - --exclude='PiFinder_data/screenshots' \ - "$(basename "${PIFINDER_DATA_ON_ROOT}")" 2>/dev/null || fail "backup create" - -BACKUP_SIZE=$(stat -c%s "${BACKUP_TMP}") -show 37 "Backup created (${BACKUP_SIZE} bytes)" - -show 38 "Copying backup to staging" -dd if="${BACKUP_TMP}" of="${SD_DEV}" bs=4096 \ - seek=$(( BACKUP_STAGING_BYTE / 4096 )) conv=notrunc 2>/dev/null || fail "backup stage" -rm -f "${BACKUP_TMP}" -show 40 "Backup staged" - -umount "${MOUNT_ROOT}" - -# Write header with actual backup size -HEADER_FILE="/tmp/staging_header_$$" -dd if=/dev/zero of="${HEADER_FILE}" bs=4096 count=1 2>/dev/null -printf "PFMIGRATE1\ntarball_size=%s\nbackup_size=%s\n" "${TARBALL_SIZE}" "${BACKUP_SIZE}" | \ - dd of="${HEADER_FILE}" conv=notrunc 2>/dev/null -dd if="${HEADER_FILE}" of="${SD_DEV}" bs=4096 seek=$(( STAGING_START_BYTE / 4096 )) conv=notrunc 2>/dev/null - -# Verify header -MAGIC=$(dd if="${SD_DEV}" bs=4096 skip=$(( STAGING_START_BYTE / 4096 )) count=1 2>/dev/null | head -1) -[ "${MAGIC}" != "PFMIGRATE1" ] && fail "header verify failed (got: ${MAGIC})" - -show 42 "Staging verified" - -# === POINT OF NO RETURN === -show 45 "FORMATTING" - -mkfs.vfat -F 32 -n FIRMWARE "${BOOT_DEV}" || fail "mkfs.vfat" -show 50 "Format root" -mkfs.ext4 -F -L NIXOS_SD "${ROOT_DEV}" || fail "mkfs.ext4" -show 55 "Formatted" - -# --- Extract NixOS --- -show 57 "Extracting tarball" -mkdir -p "${MOUNT_NEW}" -mount -t ext4 "${ROOT_DEV}" "${MOUNT_NEW}" || fail "mount new root" - -TARBALL_SKIP_BLOCKS=$(( TARBALL_STAGING_BYTE / 4096 )) -TARBALL_COUNT_BLOCKS=$(( (TARBALL_SIZE + 4095) / 4096 )) - -dd if="${SD_DEV}" bs=4096 skip="${TARBALL_SKIP_BLOCKS}" count="${TARBALL_COUNT_BLOCKS}" 2>/dev/null | \ - gunzip | tar xf - -C "${MOUNT_NEW}" || fail "extract" - -show 70 "Extracted" - -# Move boot/ to boot partition -mkdir -p "${MOUNT_BOOT}" -mount -t vfat "${BOOT_DEV}" "${MOUNT_BOOT}" || fail "mount boot" - -if [ -d "${MOUNT_NEW}/boot" ]; then - cp -a "${MOUNT_NEW}/boot/." "${MOUNT_BOOT}/" - rm -rf "${MOUNT_NEW}/boot" -fi - -# Move rootfs/ contents up -if [ -d "${MOUNT_NEW}/rootfs" ]; then - # Use find to avoid glob expansion issues with empty dirs - find "${MOUNT_NEW}/rootfs" -mindepth 1 -maxdepth 1 -exec mv {} "${MOUNT_NEW}/" \; 2>/dev/null || true - rmdir "${MOUNT_NEW}/rootfs" 2>/dev/null || true -fi -rm -f "${MOUNT_NEW}/manifest.json" - -show 78 "Layout done" - -# --- Restore backup --- -show 80 "Restoring user data" -mkdir -p "${MOUNT_NEW}/home/pifinder" - -BACKUP_SKIP_BLOCKS=$(( BACKUP_STAGING_BYTE / 4096 )) -BACKUP_COUNT_BLOCKS=$(( (BACKUP_SIZE + 4095) / 4096 )) - -dd if="${SD_DEV}" bs=4096 skip="${BACKUP_SKIP_BLOCKS}" count="${BACKUP_COUNT_BLOCKS}" 2>/dev/null | \ - gunzip | tar xf - -C "${MOUNT_NEW}/home/pifinder/" || fail "restore backup" - -show 85 "Data restored" - -# --- WiFi migration --- -show 88 "Migrating WiFi" -if [ -f "/tmp/wifi_test_$$/wpa_supplicant.conf" ]; then - NM_DIR="${MOUNT_NEW}/etc/NetworkManager/system-connections" - mkdir -p "${NM_DIR}" - - SSID="" PSK="" IN_NET=0 - while IFS= read -r line; do - line=$(echo "${line}" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') - case "${line}" in - network=*) IN_NET=1; SSID=""; PSK="" ;; - "}") - if [ "${IN_NET}" = "1" ] && [ -n "${SSID}" ]; then - FN=$(echo "${SSID}" | sed 's/[^a-zA-Z0-9_-]/_/g') - CONN="${NM_DIR}/${FN}.nmconnection" - { - printf "[connection]\nid=%s\ntype=wifi\nautoconnect=true\n\n" "${SSID}" - printf "[wifi]\nssid=%s\nmode=infrastructure\n\n" "${SSID}" - } > "${CONN}" - if [ -n "${PSK}" ]; then - printf "[wifi-security]\nkey-mgmt=wpa-psk\npsk=%s\n\n" "${PSK}" >> "${CONN}" - fi - printf "[ipv4]\nmethod=auto\n\n[ipv6]\nmethod=auto\n" >> "${CONN}" - chmod 600 "${CONN}" - fi - IN_NET=0 ;; - ssid=*) [ "${IN_NET}" = "1" ] && SSID=$(echo "${line}" | sed 's/^ssid="//;s/"$//') ;; - psk=*) [ "${IN_NET}" = "1" ] && PSK=$(echo "${line}" | sed 's/^psk="//;s/"$//') ;; - esac - done < "/tmp/wifi_test_$$/wpa_supplicant.conf" -fi - -show 92 "WiFi done" - -# --- Expand partition back --- -show 95 "Expanding root" -umount "${MOUNT_NEW}" -umount "${MOUNT_BOOT}" 2>/dev/null || true -# Expand partition to fill card -echo ", +" | sfdisk -N 2 "${SD_DEV}" 2>/dev/null || true -# Force kernel to re-read partition table -partprobe "${SD_DEV}" 2>/dev/null || true -losetup -c "${SD_DEV}" 2>/dev/null || true # Update loop device -sleep 2 -# Expand filesystem (e2fsck required before resize2fs on loop devices) -e2fsck -f -y "${ROOT_DEV}" 2>/dev/null || true -resize2fs "${ROOT_DEV}" 2>/dev/null || true - -sync -rm -rf "/tmp/wifi_test_$$" "/tmp/staging_header_$$" "/tmp/test_migration_meta_$$" - -show 100 "Migration complete!" - -# ------------------------------------------------------------------- -# Step 5: Verify results -# ------------------------------------------------------------------- -echo "" -info "========================================" -info "Verifying..." -info "========================================" - -ERRORS=0 - -mkdir -p "${WORK_DIR}/mnt_boot" "${WORK_DIR}/mnt_root" -mount "${LOOP_DEV}p1" "${WORK_DIR}/mnt_boot" -mount "${LOOP_DEV}p2" "${WORK_DIR}/mnt_root" - -check() { - local desc="$1" cond="$2" - if eval "${cond}"; then - pass "${desc}" - else - error "${desc}" - ERRORS=$((ERRORS + 1)) - fi -} - -check "Boot label = FIRMWARE" \ - '[ "$(blkid -s LABEL -o value "${LOOP_DEV}p1")" = "FIRMWARE" ]' - -check "Root label = NIXOS_SD" \ - '[ "$(blkid -s LABEL -o value "${LOOP_DEV}p2")" = "NIXOS_SD" ]' - -check "NixOS marker exists" \ - '[ -f "${WORK_DIR}/mnt_root/etc/NIXOS" ]' - -check "Nix store dir exists" \ - '[ -d "${WORK_DIR}/mnt_root/nix/store" ]' - -check "Boot: extlinux.conf" \ - '[ -f "${WORK_DIR}/mnt_boot/extlinux.conf" ]' - -check "Old boot files gone" \ - '[ ! -f "${WORK_DIR}/mnt_boot/config.txt" ]' - -check "PiFinder_data/config.json restored" \ - '[ -f "${WORK_DIR}/mnt_root/home/pifinder/PiFinder_data/config.json" ]' - -check "PiFinder_data/observations.db restored" \ - '[ -f "${WORK_DIR}/mnt_root/home/pifinder/PiFinder_data/observations.db" ]' - -NM_DIR="${WORK_DIR}/mnt_root/etc/NetworkManager/system-connections" - -check "WiFi: HomeNetwork migrated" \ - '[ -f "${NM_DIR}/HomeNetwork.nmconnection" ]' - -check "WiFi: HomeNetwork PSK correct" \ - 'grep -q "psk=password123" "${NM_DIR}/HomeNetwork.nmconnection" 2>/dev/null' - -check "WiFi: Coffee_Shop_WiFi migrated" \ - '[ -f "${NM_DIR}/Coffee_Shop_WiFi.nmconnection" ]' - -check "WiFi: OpenNetwork migrated" \ - '[ -f "${NM_DIR}/OpenNetwork.nmconnection" ]' - -check "WiFi: OpenNetwork has no PSK" \ - '! grep -q "wifi-security" "${NM_DIR}/OpenNetwork.nmconnection" 2>/dev/null' - -for f in "${NM_DIR}"/*.nmconnection; do - [ ! -f "$f" ] && continue - check "WiFi: $(basename "$f") perms=600" \ - '[ "$(stat -c%a "'"$f"'")" = "600" ]' -done - -ROOT_BLOCKS=$(dumpe2fs -h "${LOOP_DEV}p2" 2>/dev/null | awk '/Block count/ {print $3}') -ROOT_BSIZE=$(dumpe2fs -h "${LOOP_DEV}p2" 2>/dev/null | awk '/Block size/ {print $3}') -ROOT_GB=$(( ROOT_BLOCKS * ROOT_BSIZE / 1073741824 )) -check "Root FS expanded (${ROOT_GB}GB >= 28GB)" \ - '[ "${ROOT_GB}" -ge 28 ]' - -umount "${WORK_DIR}/mnt_boot" -umount "${WORK_DIR}/mnt_root" - -echo "" -echo "========================================" -if [ "${ERRORS}" = "0" ]; then - pass "All ${ERRORS:-0} checks passed!" -else - error "${ERRORS} check(s) failed" -fi -echo "========================================" - -exit "${ERRORS}" From 62844b57c96761b552b1a2cb46d908103e2f728d Mon Sep 17 00:00:00 2001 From: Mike Rosseel Date: Mon, 25 May 2026 17:42:04 +0200 Subject: [PATCH 05/10] chore: tighten migration error handling (timeout, except, sha256 hard-fail) Three small fixes from the PR review: - ui/software.py UIMigrationProgress.update: replace the redundant `except (AttributeError, Exception)` with a targeted `AttributeError` guard around the sys_utils.get_migration_progress() lookup (only needed when running against sys_utils_fake). The helper itself swallows OS/JSON errors and returns {}, so wrapping everything in `except Exception` was hiding real failures from the polling loop. - ui/software.py get_release_version: add timeout=REQUEST_TIMEOUT to the requests.get call (it previously had none and would hang the UI thread if GitHub stalled). Widen the except to RequestException so Timeout, ReadTimeout, etc. are all caught. - sys_utils.start_nixos_migration: hard-fail with ValueError when neither migration_sha256_url nor migration_sha256 produces a value. Previously the helper logged a warning and returned "", which the migration script then treated as "skip checksum verification". An in-place OS replacement must not run without integrity verification. --- python/PiFinder/sys_utils.py | 14 +++++++++++--- python/PiFinder/ui/software.py | 33 ++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/python/PiFinder/sys_utils.py b/python/PiFinder/sys_utils.py index e16aaafda..e577b1d60 100644 --- a/python/PiFinder/sys_utils.py +++ b/python/PiFinder/sys_utils.py @@ -437,19 +437,27 @@ def _fetch_migration_sha256(version_info: dict) -> str: sha256 = version_info.get("migration_sha256", "") if sha256: logger.info("SYS: Using hardcoded migration SHA256") - else: - logger.warning("SYS: No SHA256 available, checksum verification disabled") return sha256 def start_nixos_migration(version_info: dict) -> None: """ Start the NixOS migration process in the background. + + Raises ValueError if migration_url or a migration SHA256 cannot be + obtained — an in-place OS replacement must not run without checksum + verification. """ url = version_info.get("migration_url", "") - sha256 = _fetch_migration_sha256(version_info) if not url: raise ValueError("Missing migration_url") + sha256 = _fetch_migration_sha256(version_info) + if not sha256: + raise ValueError( + "No migration SHA256 available (neither migration_sha256_url nor " + "migration_sha256 produced a value); refusing to migrate without " + "checksum verification" + ) logger.info(f"SYS: Starting NixOS migration to {version_info.get('version', '?')}") diff --git a/python/PiFinder/ui/software.py b/python/PiFinder/ui/software.py index 239478d4e..5303b32f2 100644 --- a/python/PiFinder/ui/software.py +++ b/python/PiFinder/ui/software.py @@ -136,10 +136,11 @@ def get_release_version(self): try: res = requests.get( - "https://raw.githubusercontent.com/brickbots/PiFinder/release/version.txt" + "https://raw.githubusercontent.com/brickbots/PiFinder/release/version.txt", + timeout=REQUEST_TIMEOUT, ) - except requests.exceptions.ConnectionError: - print("Could not connect to github") + except requests.exceptions.RequestException: + logger.warning("Could not fetch release version from github") self._release_version = "Unknown" return @@ -468,20 +469,22 @@ def update(self, force=False): self.clear_screen() y = self.display_class.titlebar_height + 2 - # Try to read progress from sys_utils + # Try to read progress from sys_utils. AttributeError happens when + # running against sys_utils_fake (no migration support); the helper + # itself swallows OS/JSON errors and returns {}. try: progress = sys_utils.get_migration_progress() - if progress: - try: - self._progress = int(progress.get("percent", self._progress)) - except (TypeError, ValueError): - pass # bad/missing percent — keep prior value - new_status = progress.get("status", self._status) - if isinstance(new_status, str) and new_status != self._status: - self._status = new_status - self._status_layout.set_text(self._status) - except (AttributeError, Exception): - pass + except AttributeError: + progress = None + if progress: + try: + self._progress = int(progress.get("percent", self._progress)) + except (TypeError, ValueError): + pass # bad/missing percent — keep prior value + new_status = progress.get("status", self._status) + if isinstance(new_status, str) and new_status != self._status: + self._status = new_status + self._status_layout.set_text(self._status) self.draw.text( (0, y), From 231148b228f0623a2e6cd2b88ffe3745e43c74aa Mon Sep 17 00:00:00 2001 From: Mike Rosseel Date: Mon, 25 May 2026 18:05:37 +0200 Subject: [PATCH 06/10] fix(migration): hex-encode SSID + escape PSK in NM keyfile emission The initramfs WiFi-migration step generated NetworkManager .nmconnection files by interpolating the SSID and PSK directly from wpa_supplicant.conf into a heredoc. SSIDs containing characters with semantic meaning in NM keyfile format (semicolon, brackets, equals, leading/trailing whitespace) or in the filesystem (slash, NUL, "..") could break the connection file, the file name, or both. Failure mode: WiFi config goes missing after migration -> headless device is unreachable until re-flashed. Fixes: - Encode SSID as semicolon-separated hex bytes (ssid=4d;79;...). This is NM keyfile's standard binary form and is safe for any byte content including non-ASCII and special chars. - Escape the id= and psk= values for NM keyfile format: backslashes doubled, semicolons backslashed. - Sanitize the filename to [A-Za-z0-9._-]; empty / "." / ".." after sanitization fall back to "wifi". - Use printf %s instead of echo when feeding the parser, so SSIDs starting with "-" or containing backslash escapes are not mangled by echo's flag interpretation. Verified end-to-end with a sample wpa_supplicant.conf containing spaces, slashes, and a semicolon in the PSK -- files generated cleanly with the expected escaping. --- python/scripts/nixos_migration_init.sh | 39 ++++++++++++++++++++------ 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/python/scripts/nixos_migration_init.sh b/python/scripts/nixos_migration_init.sh index 27af2f1dc..7e656a128 100755 --- a/python/scripts/nixos_migration_init.sh +++ b/python/scripts/nixos_migration_init.sh @@ -244,13 +244,26 @@ show 70 "Migrating WiFi" NM_DIR="${MOUNT_NEW}/etc/NetworkManager/system-connections" mkdir -p "${NM_DIR}" +# NM keyfile helpers — SSID becomes hex bytes (safe for any content), +# PSK gets minimal keyfile escaping (backslash + semicolon), filename +# is sanitized so a hostile or unusual SSID can't escape NM_DIR. +ssid_to_hex() { + printf '%s' "$1" | od -An -tx1 | tr -d ' \n' | sed 's/\(..\)/\1;/g' +} +escape_kf() { + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/;/\\;/g' +} +sanitize_fn() { + printf '%s' "$1" | tr -c 'A-Za-z0-9._-' '_' +} + if [ -f /tmp/wifi/wpa_supplicant.conf ]; then SSID="" PSK="" IN_NET=0 while IFS= read -r line; do - line=$(echo "${line}" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') + line=$(printf '%s' "${line}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "${line}" in network=*) @@ -260,22 +273,30 @@ if [ -f /tmp/wifi/wpa_supplicant.conf ]; then ;; "}") if [ "${IN_NET}" = "1" ] && [ -n "${SSID}" ]; then - NM_FILE="${NM_DIR}/${SSID}.nmconnection" + SSID_HEX=$(ssid_to_hex "${SSID}") + ID_ESC=$(escape_kf "${SSID}") + FN=$(sanitize_fn "${SSID}") + # Guard against empty/dotfile filenames after sanitization + case "${FN}" in + ""|.|..) FN="wifi" ;; + esac + NM_FILE="${NM_DIR}/${FN}.nmconnection" if [ -n "${PSK}" ]; then + PSK_ESC=$(escape_kf "${PSK}") cat > "${NM_FILE}" < "${NM_FILE}" < Date: Mon, 25 May 2026 21:37:55 +0200 Subject: [PATCH 07/10] feat(migration): SSD1333 display support + JSON config gate ui: render migration status from frame 0, expose underlying error, and allow back/exit on terminal pre-start failure so the user isn't trapped on the failure screen. --- migration_gate.json | 4 + migration_gate.txt | 6 - python/PiFinder/sys_utils.py | 8 + python/PiFinder/ui/software.py | 119 +++++++++---- python/noxfile.py | 6 +- python/scripts/migration_progress.c | 237 ++++++++++++++----------- python/scripts/nixos_migration.sh | 26 +-- python/scripts/nixos_migration_calc.py | 111 +++++++++++- python/scripts/nixos_migration_init.sh | 53 +++++- python/tests/test_software.py | 74 +++++--- versions.json | 36 ---- 11 files changed, 447 insertions(+), 233 deletions(-) create mode 100644 migration_gate.json delete mode 100644 migration_gate.txt delete mode 100644 versions.json diff --git a/migration_gate.json b/migration_gate.json new file mode 100644 index 000000000..703d93f21 --- /dev/null +++ b/migration_gate.json @@ -0,0 +1,4 @@ +{ + "nixos_for_everyone": false, + "nixos_url": "https://github.com/mrosseel/PiFinder/releases/download/v3.0.0-migration/pifinder-nixos-v3.0.0.tar.zst" +} diff --git a/migration_gate.txt b/migration_gate.txt deleted file mode 100644 index e8b91e6d5..000000000 --- a/migration_gate.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Migration killswitch — read by UISoftware._fetch_migration_gate() once per -# Software screen entry from the 'release' branch via raw.githubusercontent.com. -# Lines starting with '#' are ignored; the first non-comment, non-blank line is -# the gate value. "1" unlocks the NixOS migration UI; anything else keeps it -# hidden. Only the value on the 'release' branch is consulted at runtime. -0 diff --git a/python/PiFinder/sys_utils.py b/python/PiFinder/sys_utils.py index e577b1d60..ea8f883e4 100644 --- a/python/PiFinder/sys_utils.py +++ b/python/PiFinder/sys_utils.py @@ -458,6 +458,12 @@ def start_nixos_migration(version_info: dict) -> None: "migration_sha256 produced a value); refusing to migrate without " "checksum verification" ) + display_class = str(version_info.get("display_class", "")) + display_resolution_value = version_info.get("display_resolution", "") + if isinstance(display_resolution_value, (list, tuple)): + display_resolution = "x".join(str(part) for part in display_resolution_value) + else: + display_resolution = str(display_resolution_value) logger.info(f"SYS: Starting NixOS migration to {version_info.get('version', '?')}") @@ -480,6 +486,8 @@ def _on_done(cmd, success, exit_code): url, sha256, MIGRATION_PROGRESS_FILE, + display_class, + display_resolution, _bg=True, _bg_exc=False, _out=_log_output, diff --git a/python/PiFinder/ui/software.py b/python/PiFinder/ui/software.py index 5303b32f2..93487704c 100644 --- a/python/PiFinder/ui/software.py +++ b/python/PiFinder/ui/software.py @@ -7,6 +7,7 @@ import logging import time +from typing import Any, Optional, TYPE_CHECKING import requests @@ -14,39 +15,54 @@ from PiFinder.ui.base import UIModule from PiFinder.ui.ui_utils import TextLayouter +if TYPE_CHECKING: + + def _(a) -> Any: + return a + + sys_utils = utils.get_sys_utils() logger = logging.getLogger("UISoftware") REQUEST_TIMEOUT = 10 -MIGRATION_GATE_URL = "https://raw.githubusercontent.com/brickbots/PiFinder/release/migration_gate.txt" +MIGRATION_GATE_URL = ( + "https://raw.githubusercontent.com/brickbots/PiFinder/release/migration_gate.json" +) # Secret unlock: 7x square button _UNLOCK_SEQUENCE = ["square"] * 7 _MIGRATION_VERSION_INFO = { - "version": "2.5.0", + "version": "3.0.0", "type": "upgrade", - "migration_url": "https://github.com/mrosseel/PiFinder/releases/download/v2.5.0-migration/pifinder-nixos-v2.5.0.tar.zst", "migration_size_mb": 292, - "migration_sha256_url": "https://github.com/mrosseel/PiFinder/releases/download/v2.5.0-migration/pifinder-nixos-v2.5.0.tar.zst.sha256", + "migration_url": "https://github.com/mrosseel/PiFinder/releases/download/v3.0.0-migration/pifinder-nixos-v3.0.0.tar.zst", + "migration_sha256_url": "https://github.com/mrosseel/PiFinder/releases/download/v3.0.0-migration/pifinder-nixos-v3.0.0.tar.zst.sha256", } -def _fetch_migration_gate() -> bool: - """Check remote gate file. Returns True only if the first non-comment, - non-blank line is exactly '1'.""" +def _fetch_migration_config() -> Optional[dict]: + """Fetch and parse the remote migration config JSON. + + Returns the parsed dict if it contains a usable `nixos_url`; None on + network error, non-200 response, malformed JSON, or missing url. + The `nixos_for_everyone` gate is enforced by the caller. + """ try: res = requests.get(MIGRATION_GATE_URL, timeout=REQUEST_TIMEOUT) except requests.exceptions.RequestException: - return False + return None if res.status_code != 200: - return False - for line in res.text.splitlines(): - line = line.strip() - if not line or line.startswith("#"): - continue - return line == "1" - return False + return None + try: + data = res.json() + except ValueError: + return None + if not isinstance(data, dict): + return None + if not data.get("nixos_url"): + return None + return data def update_needed(current_version: str, repo_version: str) -> bool: @@ -111,15 +127,19 @@ def _record_key(self, key_name: str): self._key_buffer = self._key_buffer[-len(_UNLOCK_SEQUENCE) :] if self._key_buffer == _UNLOCK_SEQUENCE: self._key_buffer = [] - self._trigger_migration() - - def _trigger_migration(self): - """Push UIMigrationConfirm onto the UI stack.""" + # Unlock: self-contained — uses the hardcoded URLs and does not + # require the remote migration_gate.json to exist. + self._trigger_migration(dict(_MIGRATION_VERSION_INFO)) + + def _trigger_migration(self, version_info: dict): + """Push UIMigrationConfirm onto the UI stack with the supplied + version_info (must already contain migration_url and + migration_sha256_url).""" self.message("System Upgrade", 1) self.add_to_stack( { "class": UIMigrationConfirm, - "version_info": _MIGRATION_VERSION_INFO, + "version_info": version_info, "current_version": self._software_version.strip(), } ) @@ -128,10 +148,15 @@ def get_release_version(self): """ Fetches current release version from github, sets class variable if found. - Also checks the remote migration gate. + Also checks the remote migration config. """ - if _fetch_migration_gate(): - self._trigger_migration() + config = _fetch_migration_config() + if config and config.get("nixos_for_everyone"): + version_info = dict(_MIGRATION_VERSION_INFO) + nixos_url = config["nixos_url"] + version_info["migration_url"] = nixos_url + version_info["migration_sha256_url"] = f"{nixos_url}.sha256" + self._trigger_migration(version_info) return try: @@ -372,7 +397,9 @@ def update(self, force=False): ) y += 11 - if not self._version_info.get("migration_sha256_url") and not self._version_info.get("migration_sha256"): + if not self._version_info.get( + "migration_sha256_url" + ) and not self._version_info.get("migration_sha256"): self.draw.text( (0, y), _("No checksum avail."), @@ -437,8 +464,9 @@ def __init__(self, *args, **kwargs) -> None: self._started = False self._status = _("Starting...") self._progress = 0 + self._terminal_failure = False self._status_layout = TextLayouter( - "", + self._status, draw=self.draw, color=self.colors.get(255), colors=self.colors, @@ -456,13 +484,39 @@ def _start_migration(self): """Kick off the migration process in the background.""" self._status = _("Downloading...") try: - sys_utils.start_nixos_migration(self._version_info) + version_info = dict(self._version_info) + version_info["display_class"] = self.display_class.__class__.__name__ + version_info["display_resolution"] = list(self.display_class.resolution) + supported_displays = { + "DisplaySSD1351": (128, 128), + "DisplaySSD1333": (176, 176), + } + display_class = version_info["display_class"] + display_resolution = tuple(version_info["display_resolution"]) + display_supported = ( + supported_displays.get(display_class) == display_resolution + ) + display_supported = display_supported or ( + "SSD1333" in display_class and display_resolution == (176, 176) + ) + if not display_supported: + logger.error( + "Unsupported migration progress renderer display: " + f"{display_class} {version_info['display_resolution']}" + ) + self._status = _("Not supported") + return + sys_utils.start_nixos_migration(version_info) except AttributeError: logger.error("sys_utils.start_nixos_migration not available") self._status = _("Not supported") + self._status_layout.set_text(self._status) + self._terminal_failure = True except Exception as e: logger.error(f"Migration failed to start: {e}") - self._status = _("Failed") + self._status = _("Failed: ") + str(e) + self._status_layout.set_text(self._status) + self._terminal_failure = True def update(self, force=False): time.sleep(1 / 30) @@ -532,7 +586,12 @@ def key_down(self): self._status_layout.next() def key_left(self): - # No going back during migration + # Allow exit only if the migration never actually started (e.g., + # pre-flight refused due to missing checksum or unsupported display). + # Once the bash script is running, going back is unsafe. + if self._terminal_failure: + self.remove_from_stack() + return True return False @@ -573,9 +632,7 @@ def _fetch_notes(self): self._loaded = True else: self._error = True - logger.warning( - f"Failed to fetch release notes: HTTP {res.status_code}" - ) + logger.warning(f"Failed to fetch release notes: HTTP {res.status_code}") except requests.exceptions.RequestException as e: self._error = True logger.warning(f"Failed to fetch release notes: {e}") diff --git a/python/noxfile.py b/python/noxfile.py index 86eb7291d..df27d3607 100644 --- a/python/noxfile.py +++ b/python/noxfile.py @@ -47,11 +47,11 @@ def type_hints(session: nox.Session) -> None: session.install("-r", "requirements.txt") session.install("-r", "requirements_dev.txt") # First run populates the cache so --install-types knows what stubs are needed. + # success_codes=[0, 1] here is expected: missing-stub errors before stubs are + # installed. The second run (with stubs) must exit 0; real type errors fail CI. # Targets PiFinder/ explicitly to avoid broken tetra3 symlink in the tree. session.run("mypy", "PiFinder", success_codes=[0, 1]) - session.run( - "mypy", "--install-types", "--non-interactive", "PiFinder", success_codes=[0, 1] - ) + session.run("mypy", "--install-types", "--non-interactive", "PiFinder") @nox.session(reuse_venv=True, python="3.9") diff --git a/python/scripts/migration_progress.c b/python/scripts/migration_progress.c index 82bb2b4b2..34bd729a9 100644 --- a/python/scripts/migration_progress.c +++ b/python/scripts/migration_progress.c @@ -1,19 +1,19 @@ /* - * migration_progress - SSD1351 progress display for PiFinder initramfs + * migration_progress - OLED progress display for PiFinder initramfs * - * Drives the 128x128 SSD1351 OLED via SPI to show migration progress. + * Drives supported SPI OLED displays to show migration progress. * Designed to be statically compiled and included in the initramfs. * - * Usage: migration_progress + * Usage: migration_progress * percent: 0-100 * message: status text (max ~20 chars fits on screen) * * Examples: - * migration_progress 0 "Starting..." - * migration_progress 45 "Moving user data" - * migration_progress 100 "Complete!" + * migration_progress 0 1 22 "Starting..." + * migration_progress 45 10 22 "Moving data" + * migration_progress 100 22 22 "Complete!" * - * Hardware: SPI0.0, DC=GPIO24, RST=GPIO25, 128x128 BGR + * Hardware: SPI0.0, DC=GPIO24, RST=GPIO25, BGR565 */ #include @@ -27,8 +27,8 @@ #include #include -#define WIDTH 128 -#define HEIGHT 128 +#define MAX_WIDTH 176 +#define MAX_HEIGHT 176 #define SPI_DEVICE "/dev/spidev0.0" #define SPI_SPEED 40000000 #define GPIO_DC 24 @@ -46,7 +46,16 @@ static int spi_fd = -1; static int gpio_fd = -1; static struct gpio_v2_line_request dc_req; static struct gpio_v2_line_request rst_req; -static uint16_t framebuf[WIDTH * HEIGHT]; +static uint16_t framebuf[MAX_WIDTH * MAX_HEIGHT]; + +enum oled_controller { + CTRL_SSD1351, + CTRL_SSD1333, +}; + +static enum oled_controller controller = CTRL_SSD1351; +static int display_width = 128; +static int display_height = 128; /* 5x7 bitmap font - ASCII 32-126 */ static const uint8_t font5x7[][5] = { @@ -194,156 +203,165 @@ static void spi_write(const uint8_t *data, size_t len) } } -static void ssd1351_cmd(uint8_t cmd) +static void oled_cmd(uint8_t cmd) { gpio_set(&dc_req, 0); spi_write(&cmd, 1); } -static void ssd1351_data(const uint8_t *data, size_t len) +static void oled_data(const uint8_t *data, size_t len) { gpio_set(&dc_req, 1); spi_write(data, len); } -static void ssd1351_cmd_data(uint8_t cmd, const uint8_t *data, size_t len) +static void oled_cmd_data(uint8_t cmd, uint8_t data) { - ssd1351_cmd(cmd); - if (len > 0) - ssd1351_data(data, len); + oled_cmd(cmd); + oled_data(&data, 1); } static int skip_reset = 0; /* set via --update flag in main */ static int display_on = 0; -static void ssd1351_init(void) +static void detect_display(void) { - uint8_t d; + const char *display_class = getenv("MIGRATION_DISPLAY_CLASS"); + const char *display_resolution = getenv("MIGRATION_DISPLAY_RESOLUTION"); - if (skip_reset) { - /* Just ensure display is on, skip full init */ - ssd1351_cmd(0xAF); - display_on = 1; + controller = CTRL_SSD1351; + display_width = 128; + display_height = 128; + + if (display_class && strstr(display_class, "SSD1333")) { + controller = CTRL_SSD1333; + display_width = 176; + display_height = 176; return; } - /* Hardware reset */ + if (display_resolution && strcmp(display_resolution, "176x176") == 0) { + controller = CTRL_SSD1333; + display_width = 176; + display_height = 176; + } +} + +static void oled_reset(void) +{ gpio_set(&rst_req, 1); msleep(10); gpio_set(&rst_req, 0); msleep(10); gpio_set(&rst_req, 1); msleep(10); +} - /* Init sequence matching luma.oled exactly */ - ssd1351_cmd(0xFD); /* Unlock */ - d = 0x12; ssd1351_data(&d, 1); - - ssd1351_cmd(0xFD); /* Unlock commands */ - d = 0xB1; ssd1351_data(&d, 1); - - ssd1351_cmd(0xAE); /* Display off */ - - ssd1351_cmd(0xB3); /* Clock divider */ - d = 0xF1; ssd1351_data(&d, 1); - - ssd1351_cmd(0xCA); /* Mux ratio */ - d = 0x7F; ssd1351_data(&d, 1); - - ssd1351_cmd(0x15); /* Column address */ - uint8_t col[2] = {0x00, 0x7F}; - ssd1351_data(col, 2); - - ssd1351_cmd(0x75); /* Row address */ - uint8_t row[2] = {0x00, 0x7F}; - ssd1351_data(row, 2); +static void oled_common_init(int mux_ratio, uint8_t remap) +{ + if (skip_reset) { + /* Just ensure display is on, skip full init */ + oled_cmd(0xAF); + display_on = 1; + return; + } - ssd1351_cmd(0xA0); /* Remap/color depth */ - d = 0x74; ssd1351_data(&d, 1); /* BGR, 65k color, COM split */ + oled_reset(); - ssd1351_cmd(0xA1); /* Start line */ - d = 0x00; ssd1351_data(&d, 1); + /* SSD13xx 65k-color OLED setup. */ + oled_cmd_data(0xFD, 0x12); /* Unlock */ + oled_cmd_data(0xFD, 0xB1); /* Unlock commands */ - ssd1351_cmd(0xA2); /* Display offset */ - d = 0x00; ssd1351_data(&d, 1); + oled_cmd(0xAE); /* Display off */ + oled_cmd_data(0xB3, 0xF1); /* Clock divider */ + oled_cmd_data(0xCA, (uint8_t)mux_ratio); /* Mux ratio */ - ssd1351_cmd(0xB5); /* GPIO */ - d = 0x00; ssd1351_data(&d, 1); + oled_cmd(0x15); /* Column address */ + uint8_t col[2] = {0x00, (uint8_t)(display_width - 1)}; + oled_data(col, 2); - ssd1351_cmd(0xAB); /* Function select */ - d = 0x01; ssd1351_data(&d, 1); + oled_cmd(0x75); /* Row address */ + uint8_t row[2] = {0x00, (uint8_t)(display_height - 1)}; + oled_data(row, 2); - ssd1351_cmd(0xB1); /* Precharge */ - d = 0x32; ssd1351_data(&d, 1); + oled_cmd_data(0xA0, remap); /* Remap/color depth */ + oled_cmd_data(0xA1, 0x00); /* Start line */ + oled_cmd_data(0xA2, 0x00); /* Display offset */ + oled_cmd_data(0xB5, 0x00); /* GPIO */ + oled_cmd_data(0xAB, 0x01); /* Function select */ + oled_cmd_data(0xB1, 0x32); /* Precharge */ - ssd1351_cmd(0xB4); /* VSL */ + oled_cmd(0xB4); /* VSL */ uint8_t vsl[3] = {0xA0, 0xB5, 0x55}; - ssd1351_data(vsl, 3); - - ssd1351_cmd(0xBE); /* VCOMH */ - d = 0x05; ssd1351_data(&d, 1); - - ssd1351_cmd(0xC7); /* Master contrast */ - d = 0x0F; ssd1351_data(&d, 1); + oled_data(vsl, 3); - ssd1351_cmd(0xB6); /* Precharge2 */ - d = 0x01; ssd1351_data(&d, 1); + oled_cmd_data(0xBE, 0x05); /* VCOMH */ + oled_cmd_data(0xC7, 0x0F); /* Master contrast */ + oled_cmd_data(0xB6, 0x01); /* Precharge2 */ + oled_cmd(0xA6); /* Normal display */ - ssd1351_cmd(0xA6); /* Normal display */ + /* Display ON (0xAF) happens after first framebuffer flush. */ +} - /* NOTE: Display ON (0xAF) moved to after framebuffer flush */ +static void oled_init(void) +{ + if (controller == CTRL_SSD1333) + oled_common_init(0xAF, 0x74); + else + oled_common_init(0x7F, 0x74); } -static void ssd1351_flush(void) +static void oled_flush(void) { /* Set contrast before first frame (matching luma) */ if (!display_on) { - ssd1351_cmd(0xC1); /* Contrast */ + oled_cmd(0xC1); /* Contrast */ uint8_t contrast[3] = {0xFF, 0xFF, 0xFF}; - ssd1351_data(contrast, 3); + oled_data(contrast, 3); } - ssd1351_cmd(0x15); - uint8_t col[2] = {0x00, 0x7F}; - ssd1351_data(col, 2); + oled_cmd(0x15); + uint8_t col[2] = {0x00, (uint8_t)(display_width - 1)}; + oled_data(col, 2); - ssd1351_cmd(0x75); - uint8_t row[2] = {0x00, 0x7F}; - ssd1351_data(row, 2); + oled_cmd(0x75); + uint8_t row[2] = {0x00, (uint8_t)(display_height - 1)}; + oled_data(row, 2); - ssd1351_cmd(0x5C); /* Write RAM */ + oled_cmd(0x5C); /* Write RAM */ /* Send framebuffer as big-endian 16-bit pixels */ - uint8_t buf[WIDTH * HEIGHT * 2]; - for (int i = 0; i < WIDTH * HEIGHT; i++) { + uint8_t buf[MAX_WIDTH * MAX_HEIGHT * 2]; + int pixels = display_width * display_height; + for (int i = 0; i < pixels; i++) { buf[i * 2] = framebuf[i] >> 8; buf[i * 2 + 1] = framebuf[i] & 0xFF; } - ssd1351_data(buf, sizeof(buf)); + oled_data(buf, (size_t)pixels * 2); /* Turn display on after first frame */ if (!display_on) { - ssd1351_cmd(0xAF); /* Display on */ + oled_cmd(0xAF); /* Display on */ display_on = 1; } } static void fb_clear(uint16_t color) { - for (int i = 0; i < WIDTH * HEIGHT; i++) + for (int i = 0; i < display_width * display_height; i++) framebuf[i] = color; } static void fb_pixel(int x, int y, uint16_t color) { - if (x >= 0 && x < WIDTH && y >= 0 && y < HEIGHT) - framebuf[y * WIDTH + x] = color; + if (x >= 0 && x < display_width && y >= 0 && y < display_height) + framebuf[y * display_width + x] = color; } static void fb_rect(int x, int y, int w, int h, uint16_t color) { - for (int j = y; j < y + h && j < HEIGHT; j++) - for (int i = x; i < x + w && i < WIDTH; i++) + for (int j = y; j < y + h && j < display_height; j++) + for (int i = x; i < x + w && i < display_width; i++) fb_pixel(i, j, color); } @@ -380,7 +398,7 @@ static void fb_string_centered(int y, const char *s, uint16_t color, int scale) { int len = strlen(s); int px_width = len * 6 * scale - scale; /* subtract trailing gap */ - int x = (WIDTH - px_width) / 2; + int x = (display_width - px_width) / 2; if (x < 0) x = 0; fb_string(x, y, s, color, scale); } @@ -392,26 +410,36 @@ static void draw_progress(int percent, const char *stage, int stage_num, int sta fb_clear(COL_BLACK); + int scale = display_width >= 160 ? 2 : 1; + int margin = display_width >= 160 ? 14 : 10; + int banner_h = display_width >= 160 ? 18 : 12; + int title_y = display_width >= 160 ? 28 : 18; + int subtitle_y = display_width >= 160 ? 55 : 38; + int stage_y = display_width >= 160 ? 76 : 52; + int bar_y = display_width >= 160 ? 94 : 65; + int pct_y = display_width >= 160 ? 116 : 82; + int stage_name_y = display_width >= 160 ? 145 : 105; + int wait_y = display_height - (8 * scale); + /* Warning banner at top */ - fb_rect(0, 0, WIDTH, 12, COL_DKRED); - fb_string_centered(2, "DO NOT POWER OFF", COL_RED, 1); + fb_rect(0, 0, display_width, banner_h, COL_DKRED); + fb_string_centered(3, "DO NOT POWER OFF", COL_RED, scale); /* Title */ - fb_string_centered(18, "NixOS", COL_RED, 2); - fb_string_centered(38, "Migration", COL_RED, 1); + fb_string_centered(title_y, "NixOS", COL_RED, 2); + fb_string_centered(subtitle_y, "Migration", COL_RED, scale); /* Stage indicator (e.g., "3/7") */ if (stage_total > 0) { - char stage_str[16]; + char stage_str[32]; snprintf(stage_str, sizeof(stage_str), "Stage %d/%d", stage_num, stage_total); - fb_string_centered(52, stage_str, COL_DKGRAY, 1); + fb_string_centered(stage_y, stage_str, COL_DKGRAY, scale); } /* Progress bar */ - int bar_x = 10; - int bar_y = 65; - int bar_w = WIDTH - 20; - int bar_h = 12; + int bar_x = margin; + int bar_w = display_width - (margin * 2); + int bar_h = display_width >= 160 ? 16 : 12; /* Border */ fb_rect(bar_x, bar_y, bar_w, 1, COL_DKGRAY); @@ -433,16 +461,16 @@ static void draw_progress(int percent, const char *stage, int stage_num, int sta /* Percentage */ char pct_str[8]; snprintf(pct_str, sizeof(pct_str), "%d%%", percent); - fb_string_centered(82, pct_str, COL_RED, 2); + fb_string_centered(pct_y, pct_str, COL_RED, 2); /* Current stage name */ if (stage && *stage) - fb_string_centered(105, stage, COL_RED, 1); + fb_string_centered(stage_name_y, stage, COL_RED, scale); /* Bottom warning */ - fb_string_centered(118, "Please wait...", COL_DKGRAY, 1); + fb_string_centered(wait_y, "Please wait...", COL_DKGRAY, scale); - ssd1351_flush(); + oled_flush(); } static int hw_init(void) @@ -473,7 +501,8 @@ static int hw_init(void) if (gpio_request_line(gpio_fd, GPIO_RST, &rst_req) < 0) return -1; - ssd1351_init(); + detect_display(); + oled_init(); return 0; } diff --git a/python/scripts/nixos_migration.sh b/python/scripts/nixos_migration.sh index 7bc2463d5..c6cc4196e 100755 --- a/python/scripts/nixos_migration.sh +++ b/python/scripts/nixos_migration.sh @@ -10,7 +10,7 @@ # 3. Expand partition, restore WiFi + user data # 4. Reboot into NixOS # -# Usage: nixos_migration.sh [sha256] [progress_file] +# Usage: nixos_migration.sh [sha256] [progress_file] [display_class] [display_resolution] # # Exit codes: # 0 - Success (initramfs staged, ready to reboot) @@ -26,6 +26,8 @@ export PATH="/usr/sbin:/sbin:${PATH}" MIGRATION_URL="${1:?Usage: nixos_migration.sh [sha256] [progress_file]}" MIGRATION_SHA256="${2:-}" PROGRESS_FILE="${3:-/tmp/nixos_migration_progress}" +DISPLAY_CLASS="${4:-}" +DISPLAY_RESOLUTION="${5:-}" trap '_trap_err $LINENO "$BASH_COMMAND"' ERR _trap_err() { @@ -73,29 +75,25 @@ copy_with_libs() { # --- Phase 0: Install required packages --- progress 0 "Installing dependencies" -for pkg in e2fsprogs dosfstools fdisk zstd; do +for pkg in busybox cpio curl dosfstools e2fsprogs fdisk gzip xz-utils zstd; do if ! dpkg -s "${pkg}" >/dev/null 2>&1; then sudo apt-get install -y "${pkg}" || fail 1 "Failed to install ${pkg}" fi done # --- Phase 1: Pre-flight checks --- +# nixos_migration_calc.py is the single source of truth for whether this +# system is ready to migrate (model, RAM, SD size + layout, free space, +# WiFi mode, supported display). A non-zero exit means all_ok is false. progress 3 "Running pre-flight checks" -if ! python3 "${SCRIPT_DIR}/nixos_migration_calc.py" --json > /tmp/migration_checks.json 2>&1; then +if ! python3 "${SCRIPT_DIR}/nixos_migration_calc.py" --json \ + --display-class "${DISPLAY_CLASS}" \ + --display-resolution "${DISPLAY_RESOLUTION}" \ + > /tmp/migration_checks.json 2>&1; then fail 1 "Pre-flight checks failed" fi -WIFI_MODE=$(python3 -c "import json; print(json.load(open('/tmp/migration_checks.json'))['wifi_mode'])") -if [ "${WIFI_MODE}" != "Client" ]; then - fail 1 "WiFi must be in Client mode" -fi - -# RAM check: image must fit in available RAM during initramfs -MEM_KB=$(awk '/MemTotal/ {print $2}' /proc/meminfo) -MEM_MB=$((MEM_KB / 1024)) -[ "${MEM_MB}" -lt 1800 ] && fail 1 "Insufficient RAM: ${MEM_MB}MB (need 2GB)" - progress 5 "Pre-flight OK" # --- Phase 2: Download image --- @@ -216,6 +214,8 @@ cat > "${INITRAMFS_DIR}/migration_meta" < str: @@ -66,6 +77,37 @@ def get_free_space_gb(path: str = "/home/pifinder") -> float: return 0.0 +def get_root_source() -> str: + """Device backing the / mount, e.g. /dev/mmcblk0p2.""" + try: + result = subprocess.run( + ["findmnt", "-no", "SOURCE", "/"], + capture_output=True, + text=True, + ) + if result.returncode == 0: + return result.stdout.strip() + except OSError: + pass + return "" + + +def get_partition_count(disk: str = SD_DISK) -> int: + """Number of partitions on the SD disk node (excludes the disk itself).""" + try: + result = subprocess.run( + ["lsblk", "-no", "NAME", "-l", disk], + capture_output=True, + text=True, + ) + if result.returncode != 0: + return 0 + lines = [line for line in result.stdout.splitlines() if line.strip()] + return max(len(lines) - 1, 0) + except OSError: + return 0 + + def get_wifi_mode() -> str: """Detect WiFi mode.""" wifi_status = Path("/home/pifinder/PiFinder/wifi_status.txt") @@ -75,19 +117,41 @@ def get_wifi_mode() -> str: return "Unknown" +def normalize_resolution(value: str) -> str: + """Normalize a live UI resolution string to WIDTHxHEIGHT.""" + match = re.fullmatch(r"\s*(\d+)\s*[x,]\s*(\d+)\s*", value) + if not match: + return value.strip() + return f"{int(match.group(1))}x{int(match.group(2))}" + + def is_pi4() -> bool: """Check if running on a Raspberry Pi 4.""" model = get_model() return REQUIRED_MODEL in model -def check_all() -> dict: +def check_all(display_class: str = "", display_resolution: str = "") -> dict: """Run all pre-flight checks. Returns dict with results.""" model = get_model() ram_mb = get_ram_mb() sd_gb = get_sd_size_gb() free_gb = get_free_space_gb() wifi = get_wifi_mode() + display_class = display_class.strip() or "Unknown" + display_resolution = normalize_resolution(display_resolution) or "Unknown" + display_ok = SUPPORTED_DISPLAYS.get(display_class) == display_resolution + display_ok = display_ok or ( + "SSD1333" in display_class and display_resolution == "176x176" + ) + root_source = get_root_source() + partition_count = get_partition_count() + boot_present = Path(EXPECTED_BOOT).is_block_device() + layout_ok = ( + root_source == EXPECTED_ROOT + and boot_present + and partition_count == EXPECTED_PARTITION_COUNT + ) checks = { "model": model, @@ -100,10 +164,25 @@ def check_all() -> dict: "free_ok": free_gb >= 1.5, "wifi_mode": wifi, "wifi_ok": wifi == "Client", + "display_class": display_class, + "display_resolution": display_resolution, + "display_ok": display_ok, + "root_source": root_source, + "partition_count": partition_count, + "boot_present": boot_present, + "layout_ok": layout_ok, "arch": platform.machine(), } checks["all_ok"] = all( - [checks["is_pi4"], checks["ram_ok"], checks["sd_ok"], checks["wifi_ok"]] + [ + checks["is_pi4"], + checks["ram_ok"], + checks["sd_ok"], + checks["free_ok"], + checks["wifi_ok"], + checks["display_ok"], + checks["layout_ok"], + ] ) return checks @@ -111,9 +190,19 @@ def check_all() -> dict: def main(): parser = argparse.ArgumentParser(description="NixOS migration pre-flight checks") parser.add_argument("--json", action="store_true", help="Output as JSON") + parser.add_argument( + "--display-class", + default="", + help="Live PiFinder display class name from the running UI", + ) + parser.add_argument( + "--display-resolution", + default="", + help="Live PiFinder logical display resolution as WIDTHxHEIGHT", + ) args = parser.parse_args() - checks = check_all() + checks = check_all(args.display_class, args.display_resolution) if args.json: print(json.dumps(checks, indent=2)) @@ -126,8 +215,20 @@ def main(): print(f"SD Card: {checks['sd_gb']} GB") print(f" >= {MIN_SD_GB}GB: {'OK' if checks['sd_ok'] else 'FAIL'}") print(f"Free Space: {checks['free_gb']} GB") + print(f" >= 1.5GB: {'OK' if checks['free_ok'] else 'FAIL'}") print(f"WiFi Mode: {checks['wifi_mode']}") print(f" Client: {'OK' if checks['wifi_ok'] else 'FAIL'}") + print(f"Display: {checks['display_class']} {checks['display_resolution']}") + print( + f" initramfs renderer supported: " + f"{'OK' if checks['display_ok'] else 'FAIL'}" + ) + print(f"Root: {checks['root_source'] or 'Unknown'}") + print(f"Partitions: {checks['partition_count']} on {SD_DISK}") + print( + f" stock SD layout ({EXPECTED_BOOT} + {EXPECTED_ROOT}, 2 partitions): " + f"{'OK' if checks['layout_ok'] else 'FAIL'}" + ) print(f"Arch: {checks['arch']}") print() if checks["all_ok"]: diff --git a/python/scripts/nixos_migration_init.sh b/python/scripts/nixos_migration_init.sh index 7e656a128..6697801f2 100755 --- a/python/scripts/nixos_migration_init.sh +++ b/python/scripts/nixos_migration_init.sh @@ -56,6 +56,12 @@ fail() { exec /bin/sh } +if [ -f /migration_meta ]; then + . /migration_meta + export MIGRATION_DISPLAY_CLASS="${DISPLAY_CLASS:-}" + export MIGRATION_DISPLAY_RESOLUTION="${DISPLAY_RESOLUTION:-}" +fi + show 28 "Migrating..." # Wait for SD card device to appear @@ -88,11 +94,12 @@ fi . /migration_meta # Now we have: TARBALL_PATH, TARBALL_SIZE, PIFINDER_DATA_PATH -# RAM check: tarball + backup + overhead must fit +# Initial RAM check: tarball + fixed overhead must fit. The exact user-data +# backup size is checked after the old root is mounted, before formatting. MEM_KB=$(awk '/MemAvailable/ {print $2}' /proc/meminfo) MEM_MB=$((MEM_KB / 1024)) TARBALL_SIZE_MB=$((TARBALL_SIZE / 1048576)) -NEEDED_MB=$((TARBALL_SIZE_MB + 150)) # tarball + backup + overhead +NEEDED_MB=$((TARBALL_SIZE_MB + 150)) [ "${MEM_MB}" -lt "${NEEDED_MB}" ] && fail "Insufficient RAM: ${MEM_MB}MB available, need ${NEEDED_MB}MB" show 31 "Validated (${MEM_MB}MB free)" @@ -130,17 +137,45 @@ rm -rf /tmp/backup_stage mkdir -p "${BACKUP_STAGE}" if [ -d "${PIFINDER_DATA_ON_ROOT}" ]; then - # Copy root-level files (observations.db, configs, etc.) + BACKUP_NEED_KB=0 + + # Root-level files are preserved, except pifinder.log which is truncated + # while copying so a large log cannot exhaust initramfs RAM. for f in "${PIFINDER_DATA_ON_ROOT}"/*; do - [ -f "$f" ] && cp "$f" "${BACKUP_STAGE}/" 2>/dev/null || true + if [ -f "$f" ]; then + case "$(basename "$f")" in + pifinder.log) + BACKUP_NEED_KB=$((BACKUP_NEED_KB + 256)) + ;; + *) + FILE_KB=$(du -sk "$f" 2>/dev/null | awk '{print $1}') + BACKUP_NEED_KB=$((BACKUP_NEED_KB + ${FILE_KB:-0})) + ;; + esac + fi done - - # Truncate log to last 1000 lines - if [ -f "${BACKUP_STAGE}/pifinder.log" ]; then - tail -n 1000 "${BACKUP_STAGE}/pifinder.log" > "${BACKUP_STAGE}/pifinder.log.tmp" - mv "${BACKUP_STAGE}/pifinder.log.tmp" "${BACKUP_STAGE}/pifinder.log" + if [ -d "${PIFINDER_DATA_ON_ROOT}/obslists" ]; then + OBSLISTS_KB=$(du -sk "${PIFINDER_DATA_ON_ROOT}/obslists" 2>/dev/null | awk '{print $1}') + BACKUP_NEED_KB=$((BACKUP_NEED_KB + ${OBSLISTS_KB:-0})) fi + MEM_KB=$(awk '/MemAvailable/ {print $2}' /proc/meminfo) + TARBALL_KB=$((TARBALL_SIZE / 1024)) + # Keep a conservative 150 MiB for tools, page cache, and shell overhead. + NEEDED_KB=$((TARBALL_KB + BACKUP_NEED_KB + 153600)) + [ "${MEM_KB}" -lt "${NEEDED_KB}" ] && fail "Insufficient RAM for backup: $((MEM_KB / 1024))MB available, need $((NEEDED_KB / 1024))MB" + + # Copy root-level files (observations.db, configs, etc.) + for f in "${PIFINDER_DATA_ON_ROOT}"/*; do + if [ -f "$f" ]; then + if [ "$(basename "$f")" = "pifinder.log" ]; then + tail -n 1000 "$f" > "${BACKUP_STAGE}/pifinder.log" 2>/dev/null || true + else + cp "$f" "${BACKUP_STAGE}/" 2>/dev/null || true + fi + fi + done + # Copy obslists directory if [ -d "${PIFINDER_DATA_ON_ROOT}/obslists" ]; then cp -a "${PIFINDER_DATA_ON_ROOT}/obslists" "${BACKUP_STAGE}/obslists" diff --git a/python/tests/test_software.py b/python/tests/test_software.py index 67e82311d..ba73e33d3 100644 --- a/python/tests/test_software.py +++ b/python/tests/test_software.py @@ -6,11 +6,14 @@ from PiFinder.ui.software import ( update_needed, _strip_markdown, - _fetch_migration_gate, + _fetch_migration_config, _UNLOCK_SEQUENCE, ) +_NIXOS_URL = "https://example.invalid/pifinder-nixos.tar.zst" + + @pytest.mark.unit class TestUpdateNeeded: def test_newer_version_available(self): @@ -79,51 +82,70 @@ def test_multiline(self): assert "**" not in result -def _mock_response(text, status_code=200): +def _mock_json_response(payload, status_code=200): resp = MagicMock() resp.status_code = status_code - resp.text = text + resp.json.return_value = payload + return resp + + +def _mock_invalid_json_response(status_code=200): + resp = MagicMock() + resp.status_code = status_code + resp.json.side_effect = ValueError("not json") return resp @pytest.mark.unit -class TestFetchMigrationGate: +class TestFetchMigrationConfig: @patch("PiFinder.ui.software.requests.get") - def test_returns_true_when_gate_is_1(self, mock_get): - mock_get.return_value = _mock_response("1") - assert _fetch_migration_gate() is True + def test_returns_dict_when_gate_open_and_url_set(self, mock_get): + payload = {"nixos_for_everyone": True, "nixos_url": _NIXOS_URL} + mock_get.return_value = _mock_json_response(payload) + assert _fetch_migration_config() == payload @patch("PiFinder.ui.software.requests.get") - def test_returns_true_when_gate_is_1_with_whitespace(self, mock_get): - mock_get.return_value = _mock_response("1\n") - assert _fetch_migration_gate() is True + def test_returns_dict_when_gate_closed_but_url_set(self, mock_get): + # Gate check is the caller's job; fetch only requires nixos_url. + payload = {"nixos_for_everyone": False, "nixos_url": _NIXOS_URL} + mock_get.return_value = _mock_json_response(payload) + assert _fetch_migration_config() == payload @patch("PiFinder.ui.software.requests.get") - def test_returns_false_when_gate_is_0(self, mock_get): - mock_get.return_value = _mock_response("0") - assert _fetch_migration_gate() is False + def test_returns_none_when_url_missing(self, mock_get): + mock_get.return_value = _mock_json_response({"nixos_for_everyone": True}) + assert _fetch_migration_config() is None @patch("PiFinder.ui.software.requests.get") - def test_returns_false_when_empty(self, mock_get): - mock_get.return_value = _mock_response("") - assert _fetch_migration_gate() is False + def test_returns_none_when_url_empty(self, mock_get): + mock_get.return_value = _mock_json_response( + {"nixos_for_everyone": True, "nixos_url": ""} + ) + assert _fetch_migration_config() is None @patch("PiFinder.ui.software.requests.get") - def test_returns_false_on_http_error(self, mock_get): - mock_get.return_value = _mock_response("1", status_code=404) - assert _fetch_migration_gate() is False + def test_returns_none_on_http_error(self, mock_get): + mock_get.return_value = _mock_json_response( + {"nixos_for_everyone": True, "nixos_url": _NIXOS_URL}, status_code=404 + ) + assert _fetch_migration_config() is None @patch("PiFinder.ui.software.requests.get") - def test_returns_false_on_connection_error(self, mock_get): + def test_returns_none_on_connection_error(self, mock_get): mock_get.side_effect = requests.exceptions.ConnectionError - assert _fetch_migration_gate() is False + assert _fetch_migration_config() is None @patch("PiFinder.ui.software.requests.get") - def test_returns_false_on_timeout(self, mock_get): + def test_returns_none_on_timeout(self, mock_get): mock_get.side_effect = requests.exceptions.Timeout - assert _fetch_migration_gate() is False + assert _fetch_migration_config() is None + + @patch("PiFinder.ui.software.requests.get") + def test_returns_none_on_malformed_json(self, mock_get): + mock_get.return_value = _mock_invalid_json_response() + assert _fetch_migration_config() is None @patch("PiFinder.ui.software.requests.get") - def test_returns_false_for_arbitrary_text(self, mock_get): - mock_get.return_value = _mock_response("yes") - assert _fetch_migration_gate() is False + def test_returns_none_when_payload_is_not_object(self, mock_get): + mock_get.return_value = _mock_json_response(["nixos_for_everyone"]) + assert _fetch_migration_config() is None diff --git a/versions.json b/versions.json deleted file mode 100644 index 0cb9298fc..000000000 --- a/versions.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "channels": { - "stable": { - "description": "Tested releases", - "versions": [ - { - "version": "2.4.0", - "ref": "v2.4.0", - "date": "2025-07-01", - "notes": "Initial NixOS release" - } - ] - }, - "unstable": { - "description": "Latest development", - "versions": [ - { - "version": "2.5.0-dev", - "ref": "main", - "date": "2025-07-01", - "notes": "Development branch" - } - ] - }, - "beta": { - "versions": [ - { - "version": "2.5.0", - "ref": "v2.5.0-beta", - "date": "2026-02-05", - "notes": "flexible upgrades" - } - ] - } - } -} From d1b9ddb37d0e93ea01e075d3a19e3abbdb463d93 Mon Sep 17 00:00:00 2001 From: Mike Rosseel Date: Fri, 19 Jun 2026 14:10:07 +0200 Subject: [PATCH 08/10] fix(migration): chown /nix/store to root after tarball extraction The migration tarball can carry non-root (uid 1000) store paths; tar preserves them, and NetworkManager then refuses to load its wifi plugin so the migrated device comes up with wlan0 "unmanaged" and no wifi. Normalise /nix/store and the nix db dir to root right after the rootfs move, while the new root is still writable (it is mounted read-only once NixOS boots). The fix-nix-store-ownership boot service is the runtime backstop. Co-Authored-By: Claude Opus 4.8 (1M context) --- python/scripts/nixos_migration_init.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/scripts/nixos_migration_init.sh b/python/scripts/nixos_migration_init.sh index 6697801f2..dc82ebec5 100755 --- a/python/scripts/nixos_migration_init.sh +++ b/python/scripts/nixos_migration_init.sh @@ -242,6 +242,14 @@ done cd / rmdir "${MOUNT_NEW}/rootfs" +# NetworkManager (like other security-sensitive plugin loaders) refuses to load +# any plugin file not owned by root, so a /nix/store with non-root paths baked +# into the tarball silently kills wifi (wlan0 ends up "unmanaged"). Normalise +# store ownership to root now, while the new root is still writable — once +# NixOS boots /nix/store is mounted read-only. (The boot-time +# fix-nix-store-ownership service is the runtime backstop for this.) +chown -R 0:0 "${MOUNT_NEW}/nix/store" "${MOUNT_NEW}/nix/var/nix/db" 2>/dev/null || true + show 66 "Copying boot" mkdir -p "${MOUNT_BOOT}" From 5d131c020374ca57de0d4c049e2d0e259314c9a5 Mon Sep 17 00:00:00 2001 From: Mike Rosseel Date: Sun, 21 Jun 2026 00:41:33 +0200 Subject: [PATCH 09/10] fix(migration): keep initramfs progress display readable and flicker-free - nixos_migration_init.sh: after the first stage, drive migration_progress with --update so the OLED framebuffer is overwritten in place instead of resetting/blanking to black between every stage - shorten over-wide stage strings to fit the 128px display ("Loading tarball to RAM" -> "Loading tarball", "Validated (NMB free)" -> "Validated: NMB") - migration_progress.c: render the "DO NOT POWER OFF" banner as a solid bright bar with black text for contrast - migration_progress.c: auto-fit the variable-length stage name (scale down, truncate as last resort) so it never overflows either panel - rebuild the static aarch64 migration_progress binary to match Co-Authored-By: Claude Opus 4.8 (1M context) --- python/scripts/migration_progress | Bin 80440 -> 81392 bytes python/scripts/migration_progress.c | 30 +++++++++++++++++++++---- python/scripts/nixos_migration_init.sh | 17 +++++++++++--- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/python/scripts/migration_progress b/python/scripts/migration_progress index 92ba6dd8f97403fdbb2fb75d098fb0303c3e974c..ab937680eed1fc3b98c444b442c8b2c15b979332 100755 GIT binary patch delta 19016 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 delta 14986 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 XK1Pij9opk1bf9w20PTYkTJZk?Id3KR diff --git a/python/scripts/migration_progress.c b/python/scripts/migration_progress.c index 34bd729a9..02c94cbcf 100644 --- a/python/scripts/migration_progress.c +++ b/python/scripts/migration_progress.c @@ -403,6 +403,28 @@ static void fb_string_centered(int y, const char *s, uint16_t color, int scale) fb_string(x, y, s, color, scale); } +/* Center a string, shrinking the scale (and finally truncating) so it always + * fits the display width. Used for the variable-length stage name. */ +static void fb_string_centered_fit(int y, const char *s, uint16_t color, int max_scale) +{ + int len = strlen(s); + for (int scale = max_scale; scale >= 1; scale--) { + if (len * 6 * scale - scale <= display_width) { + fb_string_centered(y, s, color, scale); + return; + } + } + /* Too long even at scale 1: render a head that fits. */ + int max_chars = (display_width + 1) / 6; + char buf[64]; + if (max_chars > (int)sizeof(buf) - 1) + max_chars = (int)sizeof(buf) - 1; + int n = len < max_chars ? len : max_chars; + memcpy(buf, s, (size_t)n); + buf[n] = '\0'; + fb_string_centered(y, buf, color, 1); +} + static void draw_progress(int percent, const char *stage, int stage_num, int stage_total) { if (percent < 0) percent = 0; @@ -421,9 +443,9 @@ static void draw_progress(int percent, const char *stage, int stage_num, int sta int stage_name_y = display_width >= 160 ? 145 : 105; int wait_y = display_height - (8 * scale); - /* Warning banner at top */ - fb_rect(0, 0, display_width, banner_h, COL_DKRED); - fb_string_centered(3, "DO NOT POWER OFF", COL_RED, scale); + /* Warning banner at top: solid bright bar with black text for contrast */ + fb_rect(0, 0, display_width, banner_h, COL_RED); + fb_string_centered(3, "DO NOT POWER OFF", COL_BLACK, scale); /* Title */ fb_string_centered(title_y, "NixOS", COL_RED, 2); @@ -465,7 +487,7 @@ static void draw_progress(int percent, const char *stage, int stage_num, int sta /* Current stage name */ if (stage && *stage) - fb_string_centered(stage_name_y, stage, COL_RED, scale); + fb_string_centered_fit(stage_name_y, stage, COL_RED, scale); /* Bottom warning */ fb_string_centered(wait_y, "Please wait...", COL_DKGRAY, scale); diff --git a/python/scripts/nixos_migration_init.sh b/python/scripts/nixos_migration_init.sh index dc82ebec5..5de6c408e 100755 --- a/python/scripts/nixos_migration_init.sh +++ b/python/scripts/nixos_migration_init.sh @@ -38,6 +38,7 @@ PROGRESS="/bin/migration_progress" STAGE_NUM=0 STAGE_TOTAL=22 +PROGRESS_READY=0 show() { local pct="$1" @@ -45,7 +46,17 @@ show() { STAGE_NUM=$((STAGE_NUM + 1)) echo "[${pct}%] ${msg}" > /dev/console 2>/dev/null || true echo "[${pct}%] ${msg}" - [ -x "${PROGRESS}" ] && "${PROGRESS}" "${pct}" "${STAGE_NUM}" "${STAGE_TOTAL}" "${msg}" 2>/dev/null || true + if [ -x "${PROGRESS}" ]; then + # First call resets and initialises the panel; later calls reuse it + # with --update so the framebuffer is overwritten in place instead of + # the display blanking to black between every stage. + if [ "${PROGRESS_READY}" -eq 0 ]; then + "${PROGRESS}" "${pct}" "${STAGE_NUM}" "${STAGE_TOTAL}" "${msg}" 2>/dev/null || true + PROGRESS_READY=1 + else + "${PROGRESS}" --update "${pct}" "${STAGE_NUM}" "${STAGE_TOTAL}" "${msg}" 2>/dev/null || true + fi + fi } fail() { @@ -102,7 +113,7 @@ TARBALL_SIZE_MB=$((TARBALL_SIZE / 1048576)) NEEDED_MB=$((TARBALL_SIZE_MB + 150)) [ "${MEM_MB}" -lt "${NEEDED_MB}" ] && fail "Insufficient RAM: ${MEM_MB}MB available, need ${NEEDED_MB}MB" -show 31 "Validated (${MEM_MB}MB free)" +show 31 "Validated: ${MEM_MB}MB" # ------------------------------------------------------------------- # Phase 2: Save WiFi credentials to RAM @@ -188,7 +199,7 @@ show 38 "Backup created" # Phase 4: Copy tarball to RAM, unmount old root # ------------------------------------------------------------------- -show 40 "Loading tarball to RAM" +show 40 "Loading tarball" TARBALL_ON_ROOT="${MOUNT_ROOT}${TARBALL_PATH}" [ ! -f "${TARBALL_ON_ROOT}" ] && { umount "${MOUNT_ROOT}"; fail "Tarball not found: ${TARBALL_PATH}"; } From dfd4d3334dfde7aa48236323076410cf95e467d8 Mon Sep 17 00:00:00 2001 From: Mike Rosseel Date: Sat, 20 Jun 2026 15:33:55 +0200 Subject: [PATCH 10/10] fix(migration): keep the OLED progress display alive across stages The flicker-free --update path left the screen black. migration_progress was spawned fresh per stage, and each process re-requested the RST GPIO (driven low = panel reset) then skipped re-init, so the panel was wiped after the first stage. Replace it with a single long-lived --serve process that initialises the panel once and redraws in place from stdin, so the display updates without ever resetting to black. - migration_progress.c: add --serve (read ' ' lines from stdin); drop the broken skip_reset/--update path - nixos_migration_init.sh: start one --serve process fed via a FIFO on fd 3; ignore SIGPIPE so a dead display can never abort the migration - nixos_migration.sh: drop the percent from the 'Downloading...' status so the screen no longer shows two mismatched percentages (download progress is already reflected in the overall bar) - rebuild the static aarch64 migration_progress binary Co-Authored-By: Claude Opus 4.8 (1M context) --- python/scripts/migration_progress | Bin 81392 -> 295896 bytes python/scripts/migration_progress.c | 60 ++++++++++++++----------- python/scripts/nixos_migration.sh | 4 +- python/scripts/nixos_migration_init.sh | 41 ++++++++++++----- 4 files changed, 65 insertions(+), 40 deletions(-) diff --git a/python/scripts/migration_progress b/python/scripts/migration_progress index ab937680eed1fc3b98c444b442c8b2c15b979332..4d87f175879138c1360a27677736c4f5fe50c60d 100755 GIT binary patch literal 295896 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

<#< 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Ë(9>xx1I+$YpJnt-Gl4&qZ0x@Ww-|>0 z25Nw!+d<4vlls@w*=5-|3a$m^ZCyM%ho6i9y6Hbb{VM4Ax%v%Usfa%t8&S$0C@i28 zZiOMNjsA?w1@-rk8u@a1C@+|k7Ei3gbR6O`el6_i=SvDBDnO9|+GhRi^lw~4vbV@h~v`hmsN zPM(~2-?a{)W~mPV{`m{tvnOrR>FSVKlMv#cJP=%Dr(Qymp?w&r4FLJ5sP}Lsv4QZdoC{jOpY@mO*%VSq{^o&IdCO$+ zUKph3oq0l?=ZUuv@d3H<(4a2qm}hFC_R{Jlw?r}>E5=vnNkds*ez|TA=*%;J8_MrE zFxc%!6lUT_V(q0pNh`&1!xR=ZV?J~N#Tn>6mOvH~gMQ8MU4a3Y1_G3B#dDM2Oi+JG z73qFHy&gHN+49fg(sAu%Raiok=1+OoUqOlKlVOyX`AHD#%N9Obk%whf1ulh9O9xX= zz)VMA8SLuXDzw}2V|sd>u@@}^JHzsbFXmC^=G6}Xju#1aIeG}Q*Qa`h`Xk}f#(~nx zvP#BwT4^qEsbG30WV@pP8dl@H=R0#%fitVrP?vy@Z({U9zFz4ffas^wJyLbuOz>;MMEp~0)a z0$SPur1YENc)LjXheZ7Zd>R;jz`X%$<~0JJw-o)1wecWFz5NVu2D~Fh6@-ew^5AM3 zcXM%X&qOH|GUNQOkM+5K3qU4O0q+K?9xeMq^W+M-fnCl+5T5R)orxgvuW%5$d(*T; zIZd^XL<8CJFXfyGzE9EH?x!@=`Na7sH?#pex#kzv0Wrw#a{Fc+4NgwDdwaPN)NXAa z>$J3)LY&120l;<|s|o7nk(Pp_>7bWVFp8m{Rq;N&G;z$Y#$QZ(+?=0tpi>xqraqvJb;K^832f?=c%dRJ5H~dH2F;TFbnd0v+J2_NWe!VTlHnxyc37syB z%7iF@_FgD9f%G49j)?modZ_fMg#{8I~7l~f;7R`^~fQ3^M_i*Z>6d7 z4p3re>7^L<1;eZF!KR5*o7Q`MGvrkeET;5ck}W1oE%RB#b}+X)imy;2Vn@k@`C-?I zY;xBe%V|zW-tP};GELjn{IO3I%Rh=ACgr~ud|xomOve2EAGhE13v@) zwF#hmX^^Z^x2N=}k#DTaz4(?RGqbj4%~-Gj3w+KlAD z$0?|sGBuM6-SI>{VuUDnL9=Wx}b6QS=ty=Iea#CzAN2_xs;U=*;<>U=>ecn`~8`C7^un>c5SQ0 z-jDK}vj4%8kuX;o>+cAxgN32L0RHxPf9U3^Up!y^tq8Wpk!NoGb$@bg9y%B82|sWO z@1ZV81s??_fq!v4`E!!qC^YkoFR-3xGJE&#u9G|4VMABY)*m3~$VD!+%+JuQ!ml6F znu~KGtDxF($u)0>Yi7oHU;f8BZ|I!$Ig*BUn8y|uSL_xiknh=MzVzq&qJP7g|4_Wk zUQaYHhTJ~kPR*Ch{7HCsajVwP+iFAfwq$e}DFduc&)NIF;P= z;#H=vyNDB(#9mO4&V}6H325_j*n~uXGBXb%8vo!9LE1~a?_7%zI6WXBLh%!!lUbik zZW-ne5q<$YEYr?fDyymNy}zDGi%&qB$S%>0!Vb2t1&m7#kL0TQZvds2|SJv)3+l(%dP@9GYyc zkk)Z0pXOYm!LAQno=f{pL;8*NHg;b}-EWIEmKauyW?z2s9nIKR_Zh#uC^%5?i-r`< zh}J~=e3kzq8elWGGR+qY7iti+K|2RB250%)8P&{cODh3kj!Mtiae9(#Y%wlIQk*Qb~G@kO?ok@gPMOg_=IKzavS8hS{&!WV-;GKtMvg?_9{0v zC8Y}R?}98c{qpzvPXk`SGHzWW$NS8u2E~P6UHO{ny6-!&Xruk`x~Ii?N0^v z!?rB4GEJA|BM%gG`R3m>H*mD}n3I8itsrs+MaBmgj8973kch+d0^xhd{gZWQCj7l#779c4Moo{=#|y1ZkYoGE^DOB#C4BE$k>L7?|iWu>8)AJSCWE zKhKRwSd69M#@@HgkG-RQ{ms#b6l`c_2sq6Svob9!5y6K1iu1WyUH5WR^Jo)tck`OK zHA3(>t0Kg~-Sg~QY60QB)2>TULjO40x^#3aV^fuX+L1Q>ade=`pF6w_x4dnj80aSP zFJikx9J78=5H=c|?s*S0`7{^N4tQNTT`QhSvP8`=-qu>}jfked&x1(jf46POCTkz6 z=^w6$&zhgCkN@TbK(xE_kAbQ^#IP>{%+k>N0L=A8Xf(skC*fogqE<%rLTLMcM%2RGazN#(0}!cu zBQ`AWnHHcVzm>8O`tr=`8jys-0RtQJrR>hT)J?LgNucF~7p3s-qy?S~hdIBG#r}+F zyc9{8!?e(p3M}ly9soFPUi-Ypm5j$rfR=@9Q&%s6P?Y7PCv8~;-Sc|3jNp2`yKdTx zM2cZIXfpZ((%FZ^c-<|&UqE;PyHbuxOQyS_qIT9@OA=b>SS=737cZio>UZ9*gVZE( z;Da$U2}0IP*5Y@4r3{cnOomvL{X^~9;C(Qdd`AO5PW|ko3L%i_RbJ4=mqiK#ACpc0Oh>$9Sx=>YOhzBXNGh=b(#O+{F{mBJr+Wye zp&FxVk(+Lk5Fe~!hnzp-7K1GQ&T~^>H#EMk%q{sPRoH2!TuqwJBu*KJ-vk*mMaaem zNG9gjSP^$!c0!k2%k}Eb>;pB+Khv5(^r=wmui$gC&w@Qqha-ry^sV7_57gH4J9-?+ zLCv8HX;ssLUwdKjMskE;_BGg!2p=H~*q{rse{D%Yv3WBUsmZ6JVLUYODEN<8h*mAZ zvrqot6i9@4zD9*qo`Z&~zkaf6`2Yx_ut-nP4ZnwW-rpVX5ey??x?srpVnVz%d~Z&1 z-aBXe;)Pvb)fU>40OVVW=fphlrRb-x^OT6!3KX{7A3Tn)2uA(%N7Dbmgz%7s-yIui zbSMc@LsmNy$^T1N>P^*?gW-xxm4sqYEyh^it(dAr4-y1Xg6jVF5M;&Jh0z+=35fL)ZC+oG9LqFN$wk$OF~2nm8Fa`){^%1-~B z{JaM_w{^bal{>)v@;J=2595FneVP`s4RWXIrojvBTJ5{f&J&jX6|f9AU+^Hk;xy z;@2oI@|#s!Ny$)yCyFD3I7)ZJUU!b65yf#Dh!@v9wB$rQ#Cc>s@Osj8Aj=j|?j<}e zm!g(cCO6ujkx-yuGBXl|8RL)~s@)J({Q+yQqfiXT*2qN08wJqbo&*lHZ?ymxXzg4+ zwzjutg@cdHoQA4oh*oOO!MRHqv6On^{2!V5u;zFp9H?PZPq~bK3IQWYnhf(;P}g!^ z56f>fbZ+Rn&08h}Iwp9t`_ZU9A%RhHAX=7K(zypw4%zgaIH_>ml2t%XOH}!AVB1Ri zPH!?&ve5L~(b16Pf-2&K$)>9~t9sfxu;LQ^nx2C@Y zz-(*di6BTi;7#K`b4e=V^H3rfP(4F4SNJheTvm=>(w}dLOy9BZft!T@ZR5_hE59D# z%r$k^Onma-U@P#+_-xB>gi^dQ2RG|X2tC8%*?5Ecb-3WdW9U2e$Q!DU0qo*p3WJxn z&eA}$p|L!uIi}6*fZ$4zVY-Pe@Ybu|fxRta1Vk1z!%1U%Lq_Es>t{}n_W`+KnYibm z+jllhC(j>^RrcO4ql6-f(2uNMOEPjfZbO{!2QZ+SFNA4ANbGvT)HeX~qCdN66X(c^ znETwT?Z`1AU|8|n<-LsaDivt1Qtr>{$i|n zL)41ylHytj8mny|%lDRfTXKCPTh|%VHGJJ$L1NWvrit4=$D!PB}gv%o++9L82;ywD9=@U*t&eF5qkf&jLy`4 zF1P>r;c)-U_1H`#G9-$>CRpj{S%0+^g?IR?!03D{@ zpz(7!k+uLq%}&K&%aMqg9SdJ@ok7i+T}g{<$$|`_=OP--A2`(qI7UCcQS(zq!`}8H zC{vkH8{adPrqe_&+YdfI34{<$se9JDjIu&Q;}CYHW>MDSy^j9S+o zm7iY^lU$-U76u{lR?H4Cp|v5#1gVhLTinLtho8d|uM+e|>B9z-dU}NsO%`G+=T4PI z8=eNWL8+2(xJwU%R3bgoB}4Iv$fq|9Qmt`@5StMDIrLCYmz>7H=-I47D`ohIeD4r% zV(2f#Z@U7VyLJ>^air7D-AdX;%TtjckK%BmOJUF`Yqi(BE6Z!Wotb;u!fD8muUGGU zSH;bY0lKU>ApUWyx|WH&5BN7aH^@?Mq3#3DpcP%k%73u}`L+6uQyQRCcA`YZ1=Z&G zu%BO2`wOnWTV1VE%;ea|FI7&h)S9~}6YJ1H{+uywI)J#=@7RxF$dQPwRhx%SgN%Shca#(uw|*`nT-HhTEA?75!AGr0 zD52IZp{l@=8z_Ro{N)*!y!&34_sdRrABDMKqGi8T5S1qwHx%Y=qqqBu>0c7~h80_? zm{Ofyo&Yum&(#sM^c*-{-gc~kctl&8ZfLu2$CS+3@&e?Vm*JxV`7iOZ2Lp>9_&?a3 zKFY%fc%@`VprbZ;1u^mFx%c&%sk@Bq3Frw~kCBm>A1+MMbzmdGgK`=g2D!lcCTb-i z#B*1$)*vf6vVd(t@be^3@L#!B|OEc_9-d@*n~l?K&2CDqS+<7$!yKPACE)|Ike;-pp% z%Q;3}O<4W&>sSGV`ee;xsUm@esM#_|P^1Z}7Ff2*Tu^nR5{0%;d*}OWyYU<5_Uk(3 z)-J5ANcCDWssc?#QIH76b^~Gl>iU3RhRMgtpI4g7&b2@XU&M-`X_bEpX9SB%4u2Pj z<@FbozCSU7qm^dtF4|2_Y6`EAC5%2|*6w^2+b>$0?s`z?_qwo)j#4_zvBRuEi%up}#^w=yh`WhbLB~t-&s2?d z%oOjy{>et>EioEB&;|rPn1IDP6)k}mbd#l=@{j58|5+tx<$|v8fCbR#C!B5w&`DyD z|LDBF^VKQEOUeMY4GN96vYQD%?daH4;Lv$|99`FLknI5Gm)C?&OY;@H;{dXzd?@tS zMfeV^v=`0Vr7{Z-%uA_Rh>%KlLG{Z+AE6;gT7pAh`C+(%J9@n8xmzy&F<4kPp-MKe z)K$VPQUI4A40)B`@dKfSm7jVG?cSWEEAXQd7!7C}q`>K1#@+IilO)&>1U6DmEi4@q z*{xgC5~>FFw1CqN!f|9N3o&_a@VOaS`o=<`Ga*r>7-Bw2WmmwPqm&Da&R(Jn;cs91 z!GL=imb6WosG+MSegQlkCf5$Ao$|y-^2|XgZ8-~NDc%|QGrpvikHdpnCP^VnbT2O( z+Up*a?%26D$X~1eN2YCR73a_Ss*$C9!K*#tzjVJ8t6>$!QLdAyMwQRL(B;Jd2p}@2 z)NAz0vjISR;p3Z&dR`JWXx40=QidP=3FAhQhq>V|&qdtKurTLeKpQn{h_ZpcHym;}#{q5Wu}% zQD82Bo9eqYy)_Z@>pJ}^v;u}`NFdtl{~Xw8m~b*E++^iixT3vk#qF^snS{i7AcN#6 z5ZqUhi%pV#kdu}cdsTs!9F~8~RXg>v+`Ei zQ1{a@ZTHI-Ec9kFnI%9fK@)}pGeWLY3elsJa}RXI4;3cRzQxfTCB+vDhd0l~HjVI0n?cH@+RCw}BZIjHAlDhh9c=Sup{SIcCRtQP!8zqAT zsEo!Y8cBaQ)i5Jtqg|5yRA=Pp2#D1FA#1(IK;?m7yZI4VQ?q6lWshzH6KPfU8j2TT46zkTd5r8txca1ACA-F(Fz3seeU22JV~~ z7$fu5Xpj>#{Ueq9tDzhuZ2#iEo`6j%7k5__BzQszupdP2w2+iqA?C~cT82tRBKfOD zbIu+IiGzn@mw@zwZZQP0E!Atnqh&ndEXvcW5?>fiy~{F&iIPkSQqw%4iDe2OuUOsm zTlMsv&pxT(pSiX5%4!rrR@4R{Xr%APYoXJqL+-x^ z{azkE*R#+M*W?j!Wpf8%(-hXLp@8`lSRB?i;&+~)z1mS6!Z*CjI|A@*>S)zz4WWGQ zqh+O@Q%?Vwo42BHT0<@;808U&7h&5u`IHWf4H4r}RA=M2H)g6gIW8)q6hTKvS_^3O z`mAkwV~@!HI^qPqg&x>kdgBDbX8m?B-#5yP8QdGDn2{+9bm65Q?y6GHwg%D=E$7;V zby13#e}k7Jpl#3xHBgTjZhS)Yq*pg)Ny%XJbSSZa6ocB8m$mm@ixEvoPF$zqk6vo!~|n9R(|5y zkkjxPLy_eL3{CR#kCZ1&k~=4M?lLXGbko=mdhKUTv!8iu5h+#jB@~y_^N~I*$0T14_4szQIJ9{^UhYBw*>x!y7h=dj)JvaBpy&6?<>!(uS1{hXs|I29@epgb2t^tq5k5%G zBXzuD#%<&mO;gc;c6#?UC%D(SMAaoM2bvTec3EbfH~doci@Wi5+@I%4aolS*0_gudY16tBv_NdeDhovq@&qt^cx3)q za_taswBvMN_e2A%??iEsn#bI(|ALz6{YU0^amaF{^MlvZu!9AhY~4BFji357Q~`fg zWi*O69yvJUP083*XC(SJubsDT-qdHQcvDJ!k64qf`YXdpElE#5ih6R6b!6U5v4`h3 z>4YOkXMUx%mbFk?;$}3p*LDZd1qyJ3+wJh5E*d_c^b6azOG#|m7#Zln2fw|n^pMCp zAX0r>$Vxp-U~{`UrBfCz+eKrw;97#wQa{XQMrSPV1B@2Z7n$QtHv)|o#Za-l^sC(EoPL5)| zp<9sA&!6(>6ntGIePmHEwt_xOutIrqJ-&k2u(<6HyY+|fWPY9K6Qd1tPkIfp^W0vd z{H8?o(UJZ{U6@bfrq&beR}0bXUlpZou5P&?BxD{BD&F*|gD1P+&;qL^6KMTEa=5@9 z?*mlB81gu~I-e^D>h2nP$Q!7W#C}um&8FH3b$k3%Y_9TgNZENRKKhKf8WnL;2W7UG zUXV$Pr!lcyr~Troa}&{rtBXxg13o%*6F+bi4uPl96|1+z)@z>+f#mwqc#Rp@RvTa* zSZd7*YiQ)ckoP~2SfF~e#Ez}sbP$U%F>v_kxc1iH*g_hVP$CyceVW88jtgAjzL-FK z*O5jnB`+i<)M^*JD`C3qtGcgF`BsHm&*I$f8TKubsv;?EShdMN*oq%)67 zD*ylgW$}U=iwn5Lrh#o>Wng72YG_&5V(2uDqBd$-YRjOR3Ag~IxwL3gGq{zuqHX9* zjiMRaG@&^S_Guc#CA1A~18W-j`8~h;$NL-(=iGBQ&VBQGKVOf>#ridAw6Gj9`tQNz zyDC)IfvYryv9WuQhbV9;kVCw_)eXip{U!Wo6dt44sRW%hapBsL3SCq$Gh>Icp5CXS`F@LMfXvGi9?9g7c6rsQ! zaH}~xytfcnsK>pIf~Zhbb>vDjJ~wyAWAJFxQ1kW0p-HGtaLa(y*7nY@K9$`x_4q{O zr70^S;auS|?Z{t^DS1)Bd*qN?ROhHy8YhQY%w0Dezp>`?*aa z$?A;vjGfFP#-7As5$e|Tz{&0SwuK=MCg(UYDX5Cek~va$Uyl2Q?-8cWVOP4R1XM9_ zDjr$WKq~okwD({EYu9rcdpT-?`|P7k_!Z`h*f|#SCFjBsqGW?WS{6eEJEtK*@pO@2+?Rwn;YODkq`-`*Vh3?dB?CgY@s;N%WTU2pjkh5gWHNeMJZh!e8+8$LkHhSnan5=lrW2+9N;Pj#-c z#@daVgvgS(heNcdcGL1b@vu(CU%qg#7T5%KKy&I55E!&Q4VtCUENvA}WJJFn@VMNc zHuJFoUXJ_Z9-Vm*+8K$wee9N<#yP5m((8twlHM17VnGnJI?$oWnTL@0I=;EVFjRh+ zU$S)|Ibeu%v6$Ow{P|UcP~G|!{RC2b5<tEY;*2#;y=)2#oV9&^e*s-T`u_O%25 zW8vJ9i#eT%etSr%l?R2RX~5?-I~HJSW5FNtfJlS&9i0iZVenz|zOMp1|OelicLarYFy6 zjp4$l2@E^N7=WZXEX<~~ZjZ47PQ7)C*=@jPV+05~xp8LD*I)fy#_?ePfw{g26LTVc zWcy&F#MrYbJj$0qenseBQA{a#NYH@ow+76TCuYIlAtf?cvlJu*&aTFP9;a&0R%3u* z4hS_R-i?>)(^Pyo6)7S>+kY+P3;xt#+zzw8-HkJf_5o((><+2c@|O#7(>~SH(Zx|a zJA@vyWxM({_YoJ{>Y(ku{DoQqd-i^turi@Kv#xv@CQP9i#kXt$@5VG||0{3|@?{I4 zZ@3U+x%M@j)wytIfZE;&;_`f?Qtskyu_&0UT_b^F~_vPk6uYaFL5xRj>6D=EV6!m;t*$l6XLn3j+t|>WQ00A0#25r(c`AQwRTRp^qM%vT9#1om)P?%72>er7q08i)xGi z#J&6YQOg;s7yjO%os0zSbTgVpI%+lGNwgGzeQ=-m>RTE?<^Nj9S8qsJkeN=bea-d3 zYEjR7Yy7+SbxcUEptlvN^-J8+|8wg;H>zTz_L=sxE&Egnk?(N%Ta`=xZl-yi;eZpG zTX!0bRUPQ=IJf1Sj#)$(uzBJ)Sq+5{0QCP@TavcD97aWEw1u_ZuXE`1D~&nT8(3;f zH*t}M$J~?C)(!gqp06$Wgzh%FH*EtmOP0bD#V1x7e>(YuTrk#~J)_z=vNkgZ*5-&a zUV#Yb)7%ld$U{JPmNjAL;iQ(M9~y3<$TKZ!zqoF)n%7^jG}u%(YwRzkvII;Qv4ES; zU=_nSsu+0yLcnu%=pU5dJE0v6E>y4cI#--(%Z6OT(=rGJ}OQ z{JteG$5?UeoU(pvx(3Wmgi59)(TQ5}>uM_G_KA{Ko2jJIbcg*^G}AE0yoFsk+dfBr zh^!nj?Bz&EZ6welS2#H&p9E+81qU2_S2r748vaw1?Rv^ej8Nr0%T^@x8xSoQ5Ct&* z=2m0!_ORmZR93Dp?`Q9{!nO_JDbe_U)5@M|9bECNxZ19`9Q3OWpexZhT@Pv%NRQNM z9eH}|rgK76VTjB1pQ;(0rLQBG?n#`4!7d*^I=G1l!xw&ZMoPQ@>0nTh)ISSL%l$Ub zOV{KKIqcO>gtk2Se%IQtkQIjnla7wA*9b!qXn>}%J%3Z_G*7>xpuoOb+vDj#+ffJ) zDPSt-54oYNS`BD8vCsFYonEWPIX%n$C^t5W)i7-MW^k7K>%Eb`=W5Fd$ss*R(_>3xI}KLm0kKbQVrxDYzO@Gg6yR(JH`m=-KHgrHVZv^YX^!*nIfu{ z>5FKJEl(wJH$HW#A%p?is0K|DGall5F8#KofYkSV*QGzHRHJ?B{is9feEsW5Tx{lF z@RHq=Wc0xfkB-)-gT

_X73Csk#=Y^z-sKYI~Y5HKZJcl3ixWhJ;9l6n3MkpBP3S z+PpV3xC2`i2RHr0UMM;w>L`Y6?rk~cHeiY3M6P!E87Zh<>2owUeIJhIo_Y({OmARs zG6)vZ5L#EQ+x z57~|@a@m2Pp@^57nR8X@`C_l<+@SJ_i{)t$k_?_pxaQHYkN0baykFXT>|9}Yo1>Ku zD6y3X=W;Y5Nj%R#B`0&bmSx0|;Q=*S;{px;G zqUAA%zN9PlJA&7&7fk5Y+HGmdit5iAa~nvmj`(7E+oZx-agPw@sqTzxL8hJRX8Fp6 zxsE>bK%yAk(Zc;MVWKvpUcH`^7oH1hoF+z+9en)277D+R9e=hJWl2ZzG5WzDS`19# zs)1+R+B!r`asXZmGn;FIvmHQU8G3*SVK4cIIVJJr4it=@K(xF~Klhmj$eZNyJ2kg zaUEL!so+n&OIO!AF>&J93~n?3vBm`k;fBPCt(YEalz0WnR9&*1W2&+EWUg+f*76Qk zUxT1khy+t5*r}@j@0VsKge`{b?x=ydZkm?NqEOZUI>To)+FfGY!9gI%Wvi$8Ubf@N z+tn4rXC4G?^6Kav+2D(w6}+6^r$zLtf?YOrE7EpeswygP_Hgq#I)M5+v;Sk0!(=_! zrftE;1jiE=JhQjGRnImg-GHF*QWg}sHZT3)#=yEJM?Thojmex&%xvkcRTra$Wd?ZG zc9+~`VCHO6lYl}32($|_Vmf!Pituqcp1XdAjU=Za~&o=p#0vY(BsPI-NU+eX7p?X{$F!twm zEB%=$8;+baMJE)yCW=5WckQ!y)NOO|TF^;zCUa)Bn|FE(N7@m~++oE`inr9Z;|l70 zwiQxG&E`riVhZ#TLuUPQb8m>Urwy0$#b(88d_V$Sm`9y47jAcqNj1ijJ2cjfN6Iw? zvB&hFA9iyKah`iz-An}jK3X*lqsRpKNlmAn zpJWm?cPGl~rH$%pS6lL=OBrOUykf-Q@7ZfZMBetzTBxgkZp?`Q%iVJYLI@ zJ+$!bqCYLW|A*mg{7b!=7(LX3VYt>k@>Dfq#a)tiZ}YbC+ut+6x(S(JiAlu;Cq`c_ zR5pqQQbOon(_*#&rzOMnG6Kp87t9hUv^Dk4k+U1RvLQa^-3jY_uWU?1@0CqA+rT|? z>CVk$-&6yq36MFYHi6_tH$R2UCAx>I{s2c5O@z9#`HQPfvh+~hlSK$&?!NTP?AwHq z0(-d4*=qs5dZaYsQtf1u{YaW~*I853$Q#JHuzW<5RC|)=<>Y0t&h?2i`B%npo+UK3 z`HM11msRCvCMlFv+%@k)1@hTPyr zi@&VsY}l=Hkj&B~DfRcJR^YTWEI}*Ho-BAa*N?s`VY^^#w?(2@2(Spa>ML=dR#yTY zISEpj^ZlS=8JaU2y~~R$Jt0U8((b)oQ||n_5!dp}vsn1h3kIs2aswJ%GB)Svc4wIE z7wLydtd;o>&{O={vxlcP)aHSc9XHLLCSco_6|OV)$mccb`5~m<2mburAabryhh?k#Y-`EEHrhYJz0IQL|nJ+wM|8Z*?W+c#5^ zma0W8N_c!6OJ~o4JTjnuCV)M32P2J9hyqpXa2aIBW07m+ zuR9u-XjTO6gH*`#T^Dz&6NJ}C0$lbO3(&});l0^8xhCZm!ebOo;0!~l)iFhp`C9Hm zf%9cm27eT?_Nl+CP`m(J*Klo8MKbG*m*LsBHs@wf<367KwmvZ zxJoiL!Sn*|MjbU@iXK-VH~KpX|E^U@H3tbOM_ppMD2CVGR4|&*qZK$-k0XTjCErkm zijlphu{c(p^z4m-=HpK^kM*~xv`-_?YY(cye{b7wqiH{U#4cgoU?rI$>`~O_+2GX6 z)`GB%qG!s)`s4Q4wsx3LyiUB^xFk5cM!p}7!zp^J08UoRU;Fp7&ZvhZ2v-lnWW{s5 zvmisGZZd0-hx(*#rP%{ z({DAK!7|-5ZN?s);radEUyVzT0hti2ReX?YTJWR48fJ+k33pKcWoTBEC2n*yf6}ig zg$}#5=E=W?^n(t0JYX4IGT<#3BDLRo6n0d(9RYp%jK@Q zkgih)73UOTbGoVBY1EX1HxXI(0(omU@5(dVFF1RflQVDIT+STtr%baTT5NbQCuwSA z)^kNOmyY^>Gd)S%`DZSrPE=7Rl$nSXY&-R2XkCr zbq(hcx2~5~AQ!Q#XgcnE^>K5s8TY&973ijh>)zafPhISWVg!#TGA-W6z4{0`PC-<6 zCT;_aF&-?f4=R|mzqS|K3{y1drHjRT|DHvKT`GvURE}c!%{O5ts-2@2yXb`vKP&N6 z&DjI*JkeTCo0@9qTh>{D1$m)0t8%76|y)EBrJ+#H{dB0yL~(RP~%oxWl##fl@f!hnR-Vot4f?k zqhud^n$_Y!cyU74Q z2RVX2_vU{mIUJvT3tn|x5tGIgJbJq*Eb+!ORH ziG3yEvG#39^3wN{)FySD^iO3=R5|M5sLFCLZZwgRILUB zQ0`-*M;j()q0*L_rr3_J_7f;4uSw1?`l-hByEw#T!6)1d7gB+fzoeBZ#*++0wP`FJ zQn1^kDIzFlVJ^Ko1k!rJ>E7A8Cop;P4lmG9uL9(svuS02MoKLW@WyZlj68M%WV>|X z%C*u2-F;JN&GW?0t~%dU@HFFWIji9EM_xcMAc#|YN*B5aAJ;7;#z)+J1=_Ian@Zap5&rX4q}$j)zCIi%_b=%xs3&?eMyOD8lt_o=h67+`j`g`4IG za-9`WTNr~dY=TfpfB#WL^wb}h24yElA;3}dGSCt#SRn(*sn-|x7rq&YZ0m#USa(9S# zh977hv{SP=G2=wYrI)`gJw?te2g${AcJgJtx@{TCc0)L?Z3$dVq8PPk&~Glem(OM^2dtOn20F^Yu9;#ZM)It#cnNeYMUMI z^;yZ@ZyKB)G@BF|OW+oqGp6*=>0 zzt9w!czYzg`cjs5KGWF!k9W}poS2?Qu0F5I7E@CakPVK()E6#)Qk3er7D>QQtJYa| z6&$F8E>Z_et@T8IAaWqcTDBR;x3pK|3$|r1Ae2)+v?(~Hr+IgJN4*j|3(w@{5^4)v z#Um@Ce=RUsv>Im}VPT~Gr>RG<)49DNy@osjS^SYd%q6rIq5)ic?+oT==q;T!>5 zSL=&=Xg|_k94^L%8&U#x`uWW93m-Xl6Dyjr<wn z!rFXtQ^l?I;1emYxI>rYy0}uNAHCx4-JP9NTk&1&Z>{vZ_NhxP_%&U4VO#1XV#~IX z*1;!*olOnv-L^QhayU`(*Z-n-mvY)=e~GIL)g0Tcx5tinR3$ScTvWre&SO7RIthQ^ z1PG%(NO5<(+>fqJ2-}P2c2$#l<;i~`GIH-fbJ2-8fGe285uH57l~Xvbuxo4Tu(L-w z?)*!OtWeL$SqbN2{05j9x(^WyTj%RkaKLdt?3!yV0?n3pqLQbaVCf zVDYd%`!D!XkQlrW5>(k6$18Tr^94TF`jpUD;IqC*HAY@^rm)YOBee^#dV(6dD$KSM zRIbYln%)c*bmkM^#kbcQ<)gFEjDZUTEC|?Zf4Vh&Qgaj9+*2g$K}_&qm36`H{f-x) zpXPqZYkYH?0$^FEHx;#6YU11Gm)8Z6&8T3ABmtr$B}IvDT5leMJf$U2{-7HcdW17T zTg_-EiQ$x-7&?9`Xgwixyl`5YfanGnhl_7&sx!mK*ZdvgQ0XtjU%C*Mvw0hOUnb}cvmS^1+li<(~GvoIcQlIcHp-P z>Y=AbFMQQ>T<4Wt%n;VgpKfprPsFD!QOab*{6xs5!a&_S7nMVz8|+Y@^a0h_PzKW*mVPdzJmy#w*J^^X4u7+R<~IiA(rQN3QdZ+(&hYsEZ&ln=rmgAjhC+n zPsJ%h5&QogbwE%25*nx+Wl>>G7~c+-&`TBfE;Vqh%G9@+Wi!2-aJc@R1BXlF>_Po+ zN8N%k>A$0-6Nr-QW6(pG_YGqY$2{HxKL)goVbUNza)%LNEJwm=;wC`6#siLJBcdwn z!P!d3>N?UKGK;+#!?A~hj}N=gQ~u>K$-Nbz1zN+HdG+4qb~E5-x19`3sXyzmbr1kC*1NYZzkddoSBQ44D? z{tP^)holA~c|49BO#vPka%#{Y9{VL5P+McI+v40q`h{_XaImT}Ztg%1k8gOud%ts> zbTl+dApJ1@=zLY^3B}Hdo#Q2;$Z_n8ncrm_WPUv4>+;I2>t3*#FJw;L{hXPWE$^#f z<1_-9M7T5lenTHGKnZ^zcp~!oiz6?1ysr^E;pjW#EDU|w1L?mK*to+c&Y|&f-u@E- z-7hY@2mv27w!aAOkVW!1eXT;^9JztSA))y$v{10T&A2pl7COZw8G2OE0|No05$FY(rKc0xXXgahR15IT#OdL17M+Rb0 z)2lXN9Adp05SX85PYiJs${4FWx#6_*@x+NsQK$&g(kj3GA0|Y7Xs2Yz++}9WY2fBg zc(ukf&L#9+c!8N5DWNc$2gjd{gBG@1GGr%51HXdvPppHX1g~-QJqG!a@!nxF8w?BHE`Qo>U zij9}HY{CrTjnjcgV5)5n*uqW*R{%UnXTC&@L*6rAN?(2~oTXDun*Y7j@9I4dX92kSca2Gha6Jz{X|KFISm2`>r4{ zY8%Dt2Q*VnYqFR8K_NQW)yy6KPNR_(r(V!r^g$jRHk*KT%OT4i_&=y7o$mks)Qh^| z!^w3dHYKNv=6R^kvD>`!WhCz>W!lD9jC~$aA1YrS8c!|^!4+qgk-CGAF0whS8b8D=TCWL&q6V@2JXGuM69s1k5P(i5*QBnI`PJaTwkO3yruioi;F%tv+ZTHyHH`G<*{6A;87Pc$47QyYZ@}eaBhsdT zmT%%FUF5S1yC|mbmh2dAd%`~}V*Ry?I&69Wh8e_PPS+3OF!#A_OxC}1&@@=fa(Uzd z@Q~4-ZM%&~@MJ6bEcDzOI*(jOT9vu28g#w7t-j#8aDGa0y)ciMs2Md_Z@Hc zgdl{Jc$=>F1XghTCWbVB=Ks%Gki8G$KFk6j-J(Ywcx}urJ-wu51H*Hnex$ncZ@_L3 zDE6GF)mXcjav)T<5M0sU&n&yZm7meX3_DuD9Qft*vZ=_rJ>*z0&?V&AQw zSyORRzbq;7X9N7??2In+;xShQm1uomSKwC8blaiv>9)qG&l!|VK@6nyUQG+F^&4(D zjsH!JT#L$96%nm1Nv=X~7qE?5FTi(cwIGqb(=OsF-a)$J3VfNKZ%@HdmxwicFwn+C zd-H=gp8TY(G?A5|@m}JRbU8w0yT4L8rM|-d^vZ&wiGE#xI{krom>OqWHz;m2XFdV5 zwlf_Q;-_u`Kn!8jVw(rx?rz6z4csrXW>P>|@f#ZE!4(qco42x1NK*y6Pd3YZVXZp#;;}X{a`Sq(K%Q}N?Ye%BUP1E7X zMT)uHoVNpMiNL~*&TxWpt=<{oReAJluFMM+oapOHwRs@vy{&CG;-7s)gd}cE=@e z=fyge&*frvV~^{9?~rtt&u<^_r*NJJ3J?)3F*3nmOkxY@_9Kh)eE;UKn>fCnL|H>& zilaI>M1F<#sPYx(vITKr+DR6U8k?n5)Y#V+bC8$4Y%^`zRipP)xeE`3lNdv{3E@(X z2-WJeHeyb0rwP+yhpM+oP%Gz8-dcn5Hb)&ajCk%`JZnrZghWO}_b0O7-Ii^HunB`G z_2m)34sj?wJ_UH_Dn)_&EBVEQ$OEM99~*8BmZ!>~9hH2$1VGW9X3(5(B_??8SD;+G zh5`HM6xm+mUMz^{S;&o>{Iq1p%QL-f_s|_7u5tv^Uwm_Gt{@`D#gfD=8f?}K#9E$yID~9;t|%S|uBP9aeztobwPj3|o8l7D!y8T) zq)|qJVm?=(%mzQL#}=MdV6YBHW1c$4jAN_Dp1z2QIe><^fMmLc2ytW(u`#6Sq-M%u z$9($~p39}iBvu0egHn*yHy9K3#rf~6Fw`5(Vx-xo+>MO+yjvaa(_DEpCi)8REPZ-I zf940<4+!kLQEtp66T7r}e3&28^>;8{T6Ytru1i?JWxTrq#8%96?O|r;7#Saq&8_d_ zGU%@zZX1FkFMZ?{y_`anU)rgmAh|0!b;^)OHajwP5};9Vz^%lzIpe5cAV?>4eN{Iu zM^PUhRSJG!UDI4k-R%+*!m-OKsK1u*2WR!WMtFg#6cv<=#WE_mSroC?vrh5>0|a$c z8A;C4JTA=aUg_UZ0L`jZREwnknUC~**4ko7Le?USGAUmAX3Fk0?M5uiG?>QhHt9-= z%OHMO&P_S*?X7ix{sFd?GHQ@WA{SgQ0U!R)-v35D8uX)#9VsgLxbVeM+;5Dxa*CO%}o8?!iSB^Y9myzzm+o;%JRX(k&0j(6`g=Nd$ zOTsn;Izuww02O?obLpfON2EtKts^Y`BRd$~(u%!)|1vR>M64*he?PMAFMFHBWcuz| zLN97Gvw`CiWKV54(tR%MYi5H3^!WFq@`qxV*T;+)eI<@2-Z-e7VVf$D$cVt{js5y9 zXU^@e;Pe-#++6Mf?n%X$R-N7$?0#$=;jW%Kn0xdjna;d;=MlL=Su46WHd&!$`KLfp z$}P?JM)75gJ_r`Kps0rx>S)Qfr9i9Pzs^sLXx>*eXif1its}W#S}%qTeTTW~5JwOd zZb}M6oh=T+zSFgox)eqQr8Whqx0VD^T8uXT?VV}HaCUfL6*Xi%0jt4HZ_j&twrC=^ z#%q&1nbMX}<=3EW6NUCi^tATRwAF&rchX&S1#0n1>vbqNXcG1{Y`z-Qk_v!lnn6*8agVoVA=>#~wav8sC5=>Igc)Rz6#LE4E7N=q`<&K0iCOrizpLHV?L?pUd>gmeRkzl*SxjH zpTN(W#@0UUJ0wk3Z$3;pw%Bz6$R(8=SU9nuE?ln6t(kyTDans=?!gSouC}u3aK01u z{FaVZ(FI%$Icx#Nd=tr{5bWkeM)5Doi39zll-O+TmMM*`eZ`zWzOxdD4P4tJ%9_il z&Rh&ijdkiRcxagLsAsn9O3H;~BL9UGO@7=ET4)+mM5U}+s;3dB9Qp2MQXGffH)>K& z8Wv392y0GGQ!KmMwIxC;Il#10M1-N7xRg;>J#K+zN5qaL{Li|DQ=k$bKIlZ$D+{OR z`k1F?A9Iw;Ta`BYuNZN~>>i*KEUl(i9R&oL!%E0CtcqF5kN8k667~D~=8_QFRT3oh z@!D63^v$ke?)Bn=ZmXlXOIv=KK%MEk)ii_dq!b)VojFD=gutTre*C%F^TBKYKP(KX z>1$7Tbarh=$~1js-Z!f>xH{p;FN-NcM)e<4H-EWAo6ml(BRme*qiMDqoC@Vos~~Jw z%{|vxEDmp2lu~tmKCVDkCeualX0wm=_!f%wvO7Ivf_zkYG0Ghg*2WKR8XbdOpH6@| zO$5FHPK`MqD$Z(dBHgijyHI~@k=_6q)E_acb4ajGsniku4>z$(y@2QbID)wVmzw(K z#Je&bME!dI^8Hb}%BOVhz*L!9A-R^l6-Xk+ybwccqLJ*5vAp8_w$xNt zS4#ErXej{zgdL$b?O@wcSZJpg%%&B~~VtC|cE>(%QuFX<9Z ztnjXCbG8>En=f`zG4?<{+PJ;m55*8v3{rp}gXbRPm*Rzem22)HhWYasl`v1t)LRs< z6lKflnfB-{M}9q1n}ix*M;E1@E>#lwCJW8!u=ZJ4KGPOc-Mca2A(+tJwZ7?{Yvv2H zC+0g07=hcJa&OxRxK5r9mct%$7e3$9xyXQ8w$rsO5$T;NHsLn0o}7Q)Ywk9eL79;!)x#0!;QxmGi&3P&{n zv#vFSYz*r=*EAXZh-=O1O*@zK>KyoQi*grWjWW-Fvz#8+Tw|+SjyRpetU;Y~6(7X3 zy%iim)gPg|7H+I|UZQ0Wy1JG=l2OO^VdmK(wRKnw^y)j<>jI{1%cXo3)aDv=tSc2L^CmeGue~EgHBQ4g>(J0W#3l_7@JDCN;ceIoW6}$SiRrX=^ zICd`i`C$(($SKtN$A9*g(;xb`m_`=%j#P_!oiAqNP{Q6P6gSb2A81VxViKj<8cvHc z|22XhfR#nliA1DGSx2^4c}TStC>qP@u=gXQTzM%%Ip?{mfz^* z63Qj4^WwldNw>b>C(Trw$bQ?odn=eTOaP1G$L<}x(S%FwanQzYBxC?ei8Bv-oowe( zhtCBMk_u2$@fi_4`O^~3rp1~0Q`*k72LUkxuB+A;=;>V}xaK$nErmDT4&!0wJeE&( zCjAN0A)6EScmHql7U<6JDOJuZxGYPZLtd61K3Tjmk$uUeK!V{x>3~>s6^02zp#!L+ zPPIu{bFbS){Pe8Oba=x_0ml|~_013r>SCQ@%|Ve}hv~~*FN~hWZ=@mxzk#k{y9CRP zoqwcZf1gv}Hg`FP0rU=g4WDuT3xxjUJ*~5&vVGd=@<=ar!>z7Fj4^lROpGHbV#jTI z8amy1)ska!Ni+6ze2~z{MMa{*{bAOjD}(qI!ae+^lT&G|VamG|vn`VRajUyHWtszI zd;~oFoSl7gfH>46MYF;w6IqQ&9hVQVd>%O+nXtUb+dWfEj%{M-uaJ7r7QfLGL<^cP zWLkdqFoK!b3(PtC9i#-B&F;7y+SB#KKuxWKfBNnXnYcNEP-teYKfb~c#Ssuj7~CDW zXrkR7vezEmE6wOmt5J*-Z*6g}F(-=-6(Y(X{xMgvQZgqS!wHKy(i4c2K2ff1BO1aD z%s0KWEa8?VM_q?V&MgmRrsnI_&d6tTh}>yl+o@fk^zz5d|L>?#!Gn`4Z$RUcZe z;?GZeBDmyUay$hdDU3>=?!rfCBb;!V@&RtLXQ=)PjwL6?*dOKNtg1=X`^hN<~8~&@s zI$Jm4j(%(@joO>t$#K6fYMv8cM{+JI{{lI7rE(jlt+Rb@jO1veuHrfsE(!l_|?TIc$HkY7Ct9MgpqO7KX*>woryC6=4NAmzpYDhwjsBEoF= zHIf%-s3+dXi5D9eRYBzR!Zh;!cx;mp$(|6kQoM2`Sro5V97WDWwRFMah)EzSPe3Xp z7ZqexMbQx-VdG6mIXjb*<=;%$zn+Waz=)n$m;Yeyz>2gaY3TTX)dXN&mbl6jD=1!h zjaGcuM^WDhy&e@w*>d+7p5%h~W#c3gcW;>D<>Wd+M9NY3CeB!&L*7dnebAJIE=c&d ztaiuVQE7b_K<)L-M!Yv6bwdz@%e-o9?}hs45g*$`!9TC0^}UP*CHJC2Pa0yP5)dxE z3v6YeEOeQzZRf{^pYZD5V*?1$MD;ywB`id&R)miKASt)%tPoK;>x7LHKMML6b0n@d zmajkZOM<@ds%?c&Cj()APE6|XScL1EsKbAxJ(PHEzaJ^Z=woD!_QTb5{9gYYj1}5Mh{83``q6$`H=A>96PIwDb@!0J--9*wlZJQIRV7 zyFXwa|9iDZF{rvNz3Y_D;o*v&jLb_(l?dcLBY#QU`DT6V95u<9;QQ-Z55|!5dyjt; z&WB+wGX{Ig>WBNQ>+3s)0uDMK*R2B2KUf&B%J7u^%jCRum{gnx)o$V4OK(yo271lE z`>!w`rGb@HiNS<~8N|o+@L_l2zeRfW8* zlywb+&P-g>5PEg7i5+F~eaNEO%$h8puruLt{R!wutyy(*2wgb>bBe38^>=+A(yAEB zf&}Fb4<+a2%p{@6vvT%h>4;RWO09)Xg%6$Yx45DxzmcDQ+w5^U24*DP4|-0ksY(;v zCJ5>GS0WHBMcaQt%S&7n8+5$Gq7U-oHZYfz#u7GSQKg9C+JS?wzl+fCQm2!jW9Nq$ zjZUcFgcgXQt>t@W$~(J#mVZ7Uc`gm4&T}~De*|{Ufg4cTBaJ9cecJ|iN3kzRGw%&Q z<3UixUu%zHa4**$qOgn4I*bi#`Q`Qh*<+tGgMEf807o?HvX~GKlJrYfK2pR>F@+?hT*O@TK9N1oHK-h3^3YD2I{kbFukH1{8@oVGhr?`oG z=3ErMbj>`ni_I&sxCZZnHxD~ZU66P%I{arE6KbGGMMcXa%ULtMt}`CW2ORm~3!6w$ zYLTA5hI?M1*+H+8(@WYlEbXFiXA(uw$V2dKDa<+>yTcKY!}gL(zRyO^v!ZGv)5u-e zI2Kqn5?p7})tKgwjQY|fNq1fRXLa4}fg`ECDD0+ECTh|nX;&J-uw{KW2#x7}^N)9L z=I|e`KrF-S!i>C%R%w;M(9gD9gU#@M$oqCS4WlpaxZsk2hym6`tbMET$Y(wM(%bQFs!UOT? z(?-tQT2m|T`e@#v8S2UzqZEwMsAVh`B*|Wm^UWEB0;Q5$guSjz;lbwXs9+*)S*#i| z+<}*(xQTYl=Xo_fDdpDL5j$}oR3;$s?0+mJb=zY*5tZ?K(zj)mGe@sgMys<%R=tc^ z`eT}3+VS^Y+;wGvElH>@U>W%G7F8|%au?4rsL&C))p;a;9<5SBFTtAbsXEa$5xiHZ>n@1U!-r&)2H8% zI;&%Eezwb%o+kf^6%jwtdxbiM`vr{1W^_ho%Z!g~rE}}O|9u|ILso*}X|}a9PKZ~Z z?PH%~s$g)hsnO5H9w_;LoL}s^aBrt!$PH9baZR#y>i{4p#JKaVoe)BJs@E6Wff*09 zd+D2Qic@dg%shw>BgN*)fM=NWZ{XI%o6tz$@N#W*3f2}G>nDPSTDa2_TQz6>S^;X) zxKAl{aSDK_n04ObKdKjZKzK!{=344}sH#14`-5X}v0sQoRibv^iadKmHwUjAMga$8bgJv0PpkV>pe>_WD z@MwG%ksLm}h5gl-vb~Rp_MMPU$^*<(k6bKVY8)q}c{S>xi7%=3&gqE``LovV3QG4+ zH^!{FG6HRqRtyi@QVTb_?`WQ={4$(abB_~H)Y2h3d|<|Ydhgv+@>*yj_t`I9ie#A2 z1a~!ed9UDyUet}17lGJV(9h9f$|2N1ipJ+OdfjxBb8$cFRHI4oT+-4U zXURsmKB*|P!Ln$UyJ&YpSW_Y+<9=Vq7{Tici!&V>)t5HZL4{uxr}J|vDzlHbFdu74 zeP=U||FsE{fG`@UyjPZ<-0v;-&h|1Pg|YMe%} zqJO$LZ5Nn2VL$7b)T7=7D(8deiq?-dd7DAFEJBm>zvpGHX2;h$*wSI^AGk?xoyTkxNXdsTH{FB?QgPD&+rdmI z*a*wk3E)yg9nf+gt9(4%Q9@|&2^%g3FKA<`o(4qq4;26K)&!~v?p_G=P9jSR z+53#6_L-wV#ZF}R*%ux1GUz#|J>0n|AU!qY|1or?VM!%k7{BZ&3a)_W0B)dWV5MM9 z0WRTIXl7tGgG+_Bh}OT*sRnU_Ry1?8X#re7dWwcpBv_@?iwV6_< zDfxT9+~aYwr<9cI*U;1bzCDOY3?z zbaSpvw-ZVCk$#45iD@y?&QW)y=T3+<5gjXm%%kQDwCIN>>T_YENCV2Z|H*D%jU5d$!eK;n zbp!2$iFkzqf>-3#v54i51K_cCm#h_lRYf=f^+(w4X>KPUkp_0zeu0*}cOQhlFqRmY zOVk`5Qn?FjiNc-k?{C>9!8r>&!%~V~E}4c$W5gtDfI>{r0k%GZKNFo1HVr2i`z{>( zKDYv*-RWjS8ftQ{W_cr`qNBec->oyw_buqN(W6=7oasZrzOf!ZikoB1&~Q7P@Z|9K z4E>>xfCt47Qr}#-wYOZHBZfn|e$l(8F}H|W$Dvi<|4-1G>=>2@WiIetNrii{d{+3* z!taK=v2_*u#l{S<={Mpizw+q-o>Yeh{;hujyX>UJLJmOx2H2o)UIeWH0ghK>PI8B* zdiwF2rc^EIveQ>lt*vW3?|S%-{p&=25d{3Qxa*4lhQW`7zokK~ogayxqP+iRcYge3 zH@>?B1_7!E0LW@JCWaF~+BV(n#Jvo`F7UD5`x8KVnY`qQLtex5i>0<{W$@U40SA8p zd#$~)=katskBu8)GX!8^<0x{;_-pBG?DOcr981ItBM9A)hL$t=#&-SHcR;OqE`0*I1 zHreCg?-2!M=+4_RFF)6{vfGyVU!?TK;@cXN6Ptcs20HGp{?;gD*+zJ4;NT;V&T&0p zTPi$~!Cdn%(JQmei~rS?^d`c*azod;~iHx53^?UB^(;1^gqax8%s*JB!Yp`Un>YjWdhI#3m!F$brQff!Vn7sJ|Xp_o6&R-BzPqx*%~^% z%Gj{zdz_li?yQ%N$AbryCH>FcjJ!D}K*}OWQ-^wF?K+d6eOcLWLNl9`O*kEqwlCh3 zA`vPnCp}j%rT@e+5Qk~FUf-ns89NYkg)=Jm9)MzJ?neq zXa~F<>?Dg}J)%Qimx<(sGY{)x1P~r4eNX&o6pyse;>u5~5b@pusAB1RwhC@nib017 zOFEAWA>cC3A=ZpQx!`pY!Ljl+8{?{@WLqx$v0W3*pZ7y3U(` z|7#%R^AR)KP&L%!dth8IuW!76$or3~MRdt)%u;Y-o1HO1Ci2c!{|5;UrgVS4Tk&SH zG}|Wnc#`8jy0+6mQ)fO20F}Os$?A4kTzWAV9Y+tfbiEm_I(cDUmykTOOdOV3&Fa8o}RFEfuOuhI}3Q zT`#l%a9``@Nv{{hgEyW}elPZWS2r!Nb3_*odF&TkFmBezpK-S@93>xuU+m&E4JmGm zyyzFC7`883#;CZeDPu{O?8bQ&1bQ9H>QxitM(@1|#q;&O@Y#yy33^&Ku))G0w~RrV z0lWq@s0b0YJep?&bv*&t=hm&WOeSofv%KT(8oUWLwm zxWumxzARq$J9gOugN8bil2G&c zd50Dr%!ceo#}3bR*g!TaS?Q6qL_{kmq_(MzpA12>`o=7^B_LO|VBX_X`=tV3q@4HV z0;3O$^6Fl;fpTi15pj5E{f?o@Ci+CT_|)092beiyhPi&RoZJM#b(BAAWkVL(9KT}X zzom{8m&BRieBRS+ggVBxlqkn)w9=W8xKmW`u$j$q9q0KjA^ARI|AAHutwf5Kzqo9J2j&7MK zRbpbi-cfz(p(EN8N$~qD_8`TR(hUtz%fw3?#UrZ}a@g+aRfYB-_8icl5lDgF7}{!!+^M-6dV?&bOVs=wB84 zkva$tH*o-cvN3D9I9Cl3#UydMuYtDVHJQf4e=UMtqK5VAzO;^Yrq{p0EO8<|ePAld z{d=8f$AkpzwRg+Ve7?qx&WA*?bH`fg*7~yT#8GQ01Ssi1T!zu9s)d`kHco!Bv*s)N zzw=PY&jBaaO)9s(_}+b9@7iDqL-pIcqpNYSD*Um0wV+5wt2h>zJ@MZ*^CwYJIYIG} z**VjDfex-(7%>8ZC)hvHygfJy(Qxb&HV#&<_zTq_CSMhEKVyM^tdvD4n2RtfEh# z6;M1unZ+eO!ji+2y>9Hf-8W(O!Lu3{Q@%q;KtC}B%YX~#@s;;pVD%gkCDDynCy_sN zH!td1t4d3&%O(+4$idcn8&^P`^PiH@6?$_$IH|;~l+&+goO(EA`w>AjBjFk%M6nol zZ~|6$ro+d`09-77qEk)LO_ChWJe~bE47kbD?Mb=Rj;*DpX5T7(XkF99IeHAAI6MfD zn6W~*{S>&32}_#U9KAiO!>=DE9jGEMdhTt7Z_fC6sDK-I^-mP+0re!6?bta^cYn6Z7 zc>sJ=YZvx~ad{tiJoa-kdB<+Q-5@iGn{`o&WhI-asP=Y?Q>FzDb2=J6dexVn9oL0D zWOCk)*Hm>Za^9o9c^KBwhh64&RG~}Z$3NX4x6TtqjVN{7elsoa-KO>1R^Wq1tEWi{4U{jdjsbaD$MKzgRH`Zn*sN>?|`8uhE>eW;lSH*_IGhY~7^CHy`IR~Sk)kGK2 z;4-sp-ZwxD{wXsqe9xg!hfzjT`X_C>P@*=)9R5=?zAXZYPU5S)FP-uirA)edNwbqR znPhIp@4ww;jk4FlpJZ*w7dcHXCKvi_v%19zDico!?o=oRUZ071dm6PaC0jQlF+|-M z#=iRXM1>d3uR+aLI?h=nL)8%Ua6PSbhK5#3 zx$hPAPB{bWU?(D$goX3z*od0zL~&csp8eSRY+*X|6K@pUIuI4d?vNsuX8!0_Kg0*} zti!NPvH5ae0jR!~8lVf!(=CFq(*CDBD;N@06;G~uDT)d$+-2u>hF?Q+CfL%{ljdY$a1yJ*aJ zNMS9xSUeQeE;}$_a_CA0D|W3WG>mV}7fXy5uLay?muV~76tuuNwyH;=fNR#TGpC5K z6e75k_7)H}#>@c7+1Z6~TyEypss4HX!gc%YQ9=p*hB>cPdsPxM>B=P*Y@!z?p{qN3 zoQxhxVbeY%*>*RrE$|UmPvpETDr39Nv)xtR*PD15Om1dS@|Df(KU|E)khtk3#@4R7 zCf*@*!zGh*W@i?K2}zQ}P83ep4dWy{SKh|c%~f6GjwZ?9jcJ*wDiE)}(yGSu&>Brs z>mOmgQj1kmdz>K|j)s;o>Z+XyX0&m0;Vt#K2XH6j2FjJfA1oONy+R0iW^&#KJ^4BI z$OU%3pH=gDh&kkZi!F5wzeK8>V1hqf%hJ1MLZEx|p&xcmp-QAk+6v!4m-OQ~@;lN& z&ck8BdL*@7+|qzE&OMAj3i2m&PR7p`rzra}!E?g;1~h{eKL3xo4&2XMBb68j`6B0RB> z0VtiQ86=-*&eDU}Jsz-zt5lpE$x##l1T~#oLRx_C*>7n;O%I{+vr1+P7pltdMz24} z>-N#+IoxN`Awz{M;PWb)7t21mDduLm2|XfB9a$Cip;^;7V6r{0)cW8MWAWwFmQ{Ac zBHXr>4J{ZYC}Eh4kadDM%VJwG(ghB+>n<3o0YEMszRt7(aMGx3hk#MyK-;(Qj2-6ZQO8(q z@UK73n+Wxh=Qd1%6H5NNh$R4Cwd43HzTaQBW1eP9#POE757*ir#TAtnUW|;Dh zXGpz)DV*rGI)FjR|Kh(SE0@Vlw4s>Nq9ck1Y9{NKsj?7y59hM1+yB#8L^ih48rVvt z^X_~nGY5t-r*#b!Uc3^?&h6e`cqvS0uy6uzW`Mt!HF2APHvzZn=;Sf`^@@g-#8E{4 zO|)SYz|A1<*mTo0MkSfmPzJwEve`D{kqSgeLC? z_d<;$)9KY2^~Mps^yephxN9`K-ka>s7bG6PY1Isqa*7_-$q1Z4#brWt@8xS!tddLr zvU#*Qod2d`32-3A;UZhGw^r$YE~-b=!^NqQG3W>tsD4O8<*(m#d=4;P`XM0(spo`km== zL3})Dgm)Q0oa%GSl(5&b2X+p*L1bSw%PRL^8O*|XzLP@lwRgD=@0xzT&UzJ;j8V*6 zmwK2zP0J1vnx|CuOH=DQ0f_1MsjuJ!2$L40vbST#UUgY7j2%0bwIsa7`@)) z-|+H0byKRY&~12J$=K@OR4~~;PTqMJePv&nD(NT5>mT8d>*f?7bGorcmUSp zP!sF}h2DfgclrgBa_#yZ;;eN{nEnTye+K>rTx+<(g@UC$UKE$CwNPFR?Q>Y%SKxw+J%JefH3jmu2u{589Qf)vUk)I>Y;Q z?erLhUjbl~S*7f!u9~GeZ1#7@I1NtgICES~?Y5#Wm(ug<$f{i+yS|uCPj^7SH&Es2Yeqrq6=3A(OONdwb#E=^N)KGYQ<}Y9*gyeQS2*3dHBn z-vC`}O4`C=+?a2B@)kLl52>P=9K1h*qU?st)4~(8r%&ffF2e`gii>T`60{Z(2FQ#& zdJ0H7;74U12Hrl8;WgHg?#3d}vF3?2O0b(J-j%x8km7#6jbWA>8yosZ_7c8VUY_&i z5YkOVGNL%QSnJxJQ|iikQ~bBS(tFjxAA|^<7a4AVfMUGSTL(#{1NK@UWb`BmDhUg1 zucvh?5@8?@lkzyJ`_T{-OKm2yao$(0y}%z{HKX7&KKG&P`UpIBU3?mfL(nbLSKo9k zII_hee1uBFGaoS7JLZ9P;3dMMuw58a$WVEO4VBJ4Z$_!Q_GTG2V*FFiaF5fB8EKq zBg~8`diqr{hfyr}wk(x82FJJl1ZDr=;dTq9Xu_0$Tzp)}1=}}Br!}7sRE$7H`)?40 z^^&sVEF}-us@~{%YHP;;!{P`gkGm)9gs6>wSDN7E<@^g3U-bTOByJ0A!G-F3q(}o! z{_9Bc?Ye(F%OZNF zo(&RC>LxRK%cbrm8-0$igCyco0KK@BFaaT59sP*B>F^OsHL4&8 zyP@_^osF6-8OTQNMJlY@pBB;-trNAec)Ozx`R~EU#Gs9TuLOK;gA;mFq%1BAA!IvVz0CN8epR-1hy%nk$80 zJm-8n+&n_@;N+4khzU)nsN=5`cExqvMN#P~x|!qudSS`5g#1jyeO4OSVpL8NR=ZpP zg&**pCa(KU(ze1A4uXU{m(kG@v{OVW+%&OY<2IJPBMngi6kC(2qz4Xk2b3-5$PNtR zq#Olz);q!Bh~eZ>2>r!$qk|M9-m`c2qGtLmsqCFH`YRq28TKl36V>@0G4hROulMii zi&U025Z4Te7WuS)zZBx!iJWzGn<}D2z*;R z;a6+wbHYZD)sMymZ8%DMSTcAK1%8l<5;zhNELeTy!*U!K=wjE@)9j6ovW&BbG5pNP zPW3hdsMUAJ%C>+f0=r~vO=RORQ3B+Ie)*%VuB_zw|In+QBY_=a&h8hs20a8KgD_0o z_A?j;iu2<)E#*8fEtb5d2W%WGE?`0aeHSJ77ghO-BUAn!M<`d>nL!JMk3*H~Hnxh# zcnwLdzE))LV#e#om`#Ygc6m{M>R66Rg8N{u-yT~yK%~v}0heLC;a&L}84*t%;|l_j zpBYjOW0anJh*u_EuJ>`yCA{2qFvSZU*HZZ&m4dZlfRcL`hV{+;GEU56fh{y4%cx8? zEr0%xAd47CthH6wvM1<|9n+4dl4YIBce_^LXBai7ppgWFVORSmmqDQ%XY9fvp5S6KO1S#_unmls!@71Bw8n5g@0Nrdast&UO*MIt9L zAg!D8NcMVS_Q*I)L%cyx4uh;TfqktyK5y~UDfC(Z$TS)45&0BT0v}wjEJ}Fh0H6Gz zn1A$bfUW+wiyYiB3YrjE0c4zQ2IhCAUVrXz*;dH%jCqRM-KOQ)9_9IQ56rEI( zAbos@ktd_|p1CwUxw;z+H?&HZ6;q0OSE~qcN1Jq_XpnWVa6$SPjX~h+;UxN!xtEf9Cq zS|rj5s$!`DJRM`(dgdj|=WiVNd-h;Hzavc6lt0oT1ShFh%)is(h>1 zxmkWyAC~xsM&VarucUw8_5sjgt20qe93+y}vCzbm8Y($nD;Y5&PjJ(555zvliD&BpH;kcB0X9^nKbMd>+_)rS;euoel zMng6i8Meu61F1mCzD|`C_6hs+r-nA%2njj=$N}V$Qo-v_^o_S-Bdk{fetab{awRL@!sIihZ!#&Sq79to0eoyx!N!h_q{YO(rc#adzC$iysJDR#h)eQz zwSu0iv<>eEvn#JbxM1TrdRZzej9`rgZ&*%P1Q62pzq(y*wAQc+->oC8R#Wyu8!hXA zJx%M0j4R#Iq+;z?g=VGXvBlw{W9~MWsvjIiNsd|l9!D8tNbQs*Z0wG(2+i&+DZqw> z9ok?d3o+~I{y+1R2h@S`Lxb zZbR0awEeQ&5+|fqDrCq#k#dSrUj9Sb5$SabgD>)=mbmPTjY8^O5%qK2z$j)@&7L&UB?YHgz?W_6<%$DI0 zr|X;t*4Jw|u{@8RZ;jGP!X;bEMGM8grT*+GXToDK*6cVsE{+>_^BDiEi2WrKn*1v8 zubNgZ5Z3F~tC<5C2s8VjXUfnMV#K*Z*`WFO(=XcKd?3fU@-INb^K2O?xQ+Dr6;ph6 zAHcf3EDT=1XH&k;8vGW zv&I{H0|=c%Qur`=$?x-iNk$6OVtr3-!SbojUf=80hb`?bu~`u2gREqy%d0Bp1|8`g z^CC^1mNOX;i_O`0=`~q!$DlmTUj26Q3gNC`$DP#r_oO@CFVOvxzHwAxjKK2PeabR4 zOc=oannKc1Z`Uza0aL0)*mafn%X7e7bA`s&-Tb z4efF}t$(f$g$~(NymURvv~MQ3#^Af8H4`o$A~%Mu+C5U6{YI(^50H@ z1fjgIbf5uJr(I*=(z} zuCX0ECx~r>I?1h7w*MViMLCrNcXCZiy(;gnDgt&dW{Y!_A;=h)VO_lIS@XZ_dfqGl zM>7iy3#vcn+_`YoK48bANT$lUJd4gG2iLDBE3-GMs+I-xeMW9A@Hv-YV>p5A(3C)D z-%TYUcdzIL^QEWEFXFS@{F%Z{2lu?S9E4aFvECGS2C*0N&~!yV9v*a;*BH z#}lj$>phe*lP$rTPLM08y5iKsJ$06EDrSbrJE`R^b;|-m-eMn=>!rA=Rn-O5EV|V8 zS+Bcf?hpWUp6|GlK(zJY^U$N&!uX#Nnm*%_QoC%T=u-dY3=sXKEDJPRK5ob!Abszr;U(5Fip zNsHON#EwHh`bfPOOOsg+htqqp*yByo`RU>^rk}=n%sEm?*adF-HV)t+|H{i-iWEYQ zKl{~M<-cTvgSt`AA>N6Xl$WWZ8`IHozovsYoS33#rdA&RBpvf!NR7w$on*~b7v{zj z;6ZiGf_YG;reb%;4=V7Ly~yW=g5qVpm*eP=7(Ae)Cg!{iQAt}Yy8exg*Q59}@2%jHNmT5EbLdIYEF>Mplz2Grs9zyB^Iq(M zA)SLL4~su6e<{=T=k4gIoai>W7Dtiej=|Y5r4D*7$#1QtLYeuJanX}Yjl8gdKcf z6^w)g`6dU%bmcgM?ji1OMz%PEN}X20KwXZYzdcHvK_^pdI6QlBf?uj=RmHkAd!I9? zD>#7)lGg^5Z?#i!(Z)tQ3=Ptz)>wgh5=aaA_Jlp=mL@Tk=7<3Q?6GRAy`#LLzUE2z zrW@KPyu?Ss?8V#5c%~X_ZH@gN_NDB+CUxB$u>OF5yJl;O($ ziq0z16>+Ta?CaEH!fzLNY4i(zJl~4Kyb5%VwZHG{$hoY!qixLLU&zPGU_28Q`*TfQ zO|#At68JJzd40I6;Jjw{HEpYIM8pnu3rRcvN_j{0^2^$qX6wj>+N=g&>wlcO#MHCZ zbZkiAvZ9ocdU`-@%}&IyjTv)#v6Ft!m#%hvn#WQ1>YP$GxzdzN@w`Vr!>O8$0Y{H>WB=^Az;hr$g04Fp51Ptftxc9Xud1kisDxN_+%NQN|u1Alm5oqLNEB zPe7Jx3Ly~l6%qo1_C7~NJ&0K`Hk>d~+q~4@U95S6s&b&!*1QAV!GPuiU&f$+`mYZI zI#?}_r2T;bfmXeM*UB;t5T}bZv@WYZH{ru+fRVkD*2+ydkV^vvxTg9an#QWPXam0L zW^@)NKKfF_MS~c!Z!l{@e;q}Gr1Pxggpf4x5G(x4!le8ea&1Hm?HT6OnGC6>h!jy=b7Ke627V9KCGJtTL(?|8h7p0Qm z1Ri8Kv)>iIq{Logjt}I zKyfa~kn(PW!jkM3=VGaQt@+jLh*ZEZWqE*L5OB^a9rs*rnHXIqn&gs#li^#4cO0Sv;euf&%3U zx=S_sqoCDJ9Wv!ZQN!)QH}G4J6b?OA`w2sX>=QIQ6B}eyLs!{^O!b;b=AAyBRvX)J&=t#n`$r{jwS3H z4IvH(K=4@-ZT-)4w2D#KEWOkD9UQ%YNvKht|LDj=NqYC5$3XZFL3R@`SCvqM2%Hsn zu6PFzNj>*7b%FMAO0)dsZgK+7Uqne^pAW!Aff86snhQu(<9_7s&;MW;`z)rw%LRal z0d!Z&i$QQM(D7!!;|`gSU>^-~bL-rMs|%{|kH)@GUxpgKoOX;YKhId2r#g&{vXfV& z^qrd_ETXSU89*dLv|Y-faBJirqqfH$cGn7X`_a7%JD1_!d!{xW5s2nJm5u3vUPO&; zEM?v-8h!YSx4BN{`=D%69qut+|h9N`2y(cMrD8qdZzD8;!;!7nO^+| zNYg#Cv+-rUGRo3r@uPlM`i<@o#M7MP^?@=twEFa>+{NmsQ}>Dq_*{}-DUWj&&Jb-W zANpL33z7L1Rd-1nK}yf3G79qP?(04iJ~59K9l5~j)U^{WW}1A&S4y~UF1on#)IQwN znbzr+_U%FTnTA^&_UCU5OWP?kK**%rMtuwIc+*UnL=$Tmga54j-0IQ_TZB7D1nh>P zEt%MkrfVz*EZQo1Q1K~X;)&D5kHt$wg69R}j^7~ArmxQ5v0{|sA}}qhsa{q!OJ=|N zhSl9HL-ic0f++3aX@p$|f}myqtaMslI?+2hGI&GyqKbh!7qRcHotSm7*(~d8je2OIRU0F}s;V>Josn5xBSK^^C0nw=xhO{WV-*HbN@V&kTrpslVT>gZ79I=VX- zd{w3Q^0tA=aZrTFyzyfgSD*b;@*xxQO^N$@GcC(~|C%VEMX|xBj6nLDRsasG_|0PQ z<+(UosM0>e3t#;|P!YK=-{7;!>q2V%BC>~Bx5r|39$YGL#d{FlBM`oe%uZH=eG=C` zv=~a%KbLfG47@S|sq$6evSI66Vv>z9kK~voirBH^v#$zWaPT_uUMT;~)f(Z)vWRbx z5x34~94{}k58fM&^%;G%C*Cjzc~b3h_3A)tNA0^+)u3~5_cVMebLPb^>n{GIK=&C) zc3n4!;xQdB5j|;m)L+w{HH>p{pshoVSWROO-}16e*<{q?aY1rl)1!g49 ze4a~TzRda&QNW#Wd8bIcpv&Qwz;&EP>F|LGuwJ2|UW^qRlW1Ys<_m#KHV8H8dI?rS zyxB9lXlXiVzpZirCSmc*-X6o)=Whn&_fO0xU)m!Fn~$JzS+Ndl?rj~7&4R59bf~zf zEYPB6Jg|?C+Zl0@>~}wcE5nn)%4lld`RrPPo;ZGLG9pm|QO1DUdd^x5QJ0AY=ymUF z!|(%^(XwAH{Z7F2X6S2s!mZZzbpduDy&ElW*p>k=r zk11ozCh{`g-rt_~jSQ}L=Chqo&6>WoV`i?S65PHpmrW%{fh#EI z)XgmBwCF``v%Q2DxKx=`&r8fSVEQVx-AbH(_B1=5a|TkxybXjMkuRgcxG&iVDx6YM zb>RLn6fKv~Kq}r;abywBC|czH&+Qz1u0&kaxfE^25W(dO+MOGrQLQ1Dt_)eVkW)Ii zYOL66`wx8FsqJ+5EdpcS^sBuN&awA0UQ3_V|I|#{gjgJuvJfoy+0xA_-l4%0EnB{s zDLo`S^aHyCJR=E_zc!RtTf=*2vQc+WzN)JF+~tDz0!8}$`nU^fWjlOswCw#Czd-dp z6vRMT$2Xt>3Vbi1PWd6rK$_=k*>sPr^YEVQ4C>t6u7K=2^BBb4;A3__Oah5Usqxrj-(&9!C#_LCRXN|? z?BPZ}(z|qwa2m244aioU(_91u~P5X6g6!!MxD;bEQv-dGl)MmlmxrPjifhK*j z-+6{Q;5J-_GHhUcHXi?;c}p@m-;g~E5ByvLpA7gJJvVRu$@q2d>H~YCZxK9m8^GB= z*Mx@>d#efIx!D5ri}4|X9HPj%y!KojtA{rRx)bi6iLeEY1hsm|pKnobx2KJ=w;N6@ zZF+CnUXM61S-)$gC!(Q94w|_&7PN}&SL8R~A9r;1=47^I3ucgdbhdJWZ2W08XmpAM zsVEU9o$?+Ud^MM4Bo{4(13>FjRr?OaKl;)R&h@D|7-$C%LcVbQy_#i(O(T#S&cbm6 zNj;E@E_%=nRndr4==eei6FbU&xZG$!kHCYp-0lCO=Np?p&lePFmc;QR%sMTfkN&DK z&(XEy)XTh~fIuA&#|zS zk+%}@S3W0aQE|tX{Tr9rHRX5o*!F)J_q%-^NP#arRKDw;`YM{lRSmcJp9#RVtuu=4 z7_i00(n_DYvc_=akM?(#VT7gBRTrvV(sle%~^z6M|6k z!Cw-psV>a9C9M+KscFSdPy1hK3WQUkJk<@$^Q6W0_V8UCdd)lftaWZD0H_wv*V}$~ zS*sZ!8UwpPMq}x)J^)(RZ2+xjPi)LZQC*O9IDI2>a%u=Wy*2tnjeY((deXCkdT{j@uQTFkI|M^f@s#0XW=Z%^U|^1I{Dt-0`b#qY@OHHuJwZ4H;9kvvUK(S$)9? za~%JKE)D%bANvbb9e#Lo;0>7G^1wzJF4K|ked`K(6f#i%od|llq=#4cuAA2v`5JC7 zd2C+l_kq{=5Sa!4=sa+75u2WVIMEKN*y`45y^S+w;r2f55Xu0o4v2-|X;Sw3z?ZG_ zNv2e&P6DDbP2W~mEE;$uos$94m|sz0=h=UU7n2EpaEv=#L>izx;JCjHEs9P~Sc^?I zq^fe3&PW6)J}dGG@LUR`TAj&5PS6od3A)C+6vxrS+x@H4Ddy4O1V|9KE)Di-Vy<(W z)P;yWUw5ZwXQXTqUJeOnh9+O5a1-`GajWgM)os!Gx<@~xlNFt|Wz;0^;=)9GAo_W2 zKyH;C`EnyLbgW31ctPL1RiI*2;7IV;W zX>NJ}^s|QmV*ZqSsjotq19~kpaCNd%KSc1CA%G&+IQVykc3q;ZdOtkbIO%?a>J&#= zT?Hw3S=YM6+?3aUo!|qma!@3nJN2NS?NEn(wWVh5{O5x;@4lxATdOEk)7xsW(n80s zT7raL*1+e~`DczH1x`+I7t9V{TTK}n?|k76Zzl%^4+VvA7Rf#ntiYG7;7H{)@K@8@ zYeFiBynABdrI-z6TU4S>&fVYyj7H^~IPA^$SFoGm0z<<={^e_dCN6yWPRHL%4T1c< z{^*b7%4urP!Nk<@)@UnfsqGzat*L8slg&#ucf2|X=VwnB^LQcUvO3Gm$d#oZ-|k(8 zfNx2sCPb%w{j}Xh^V&rqfe-Yp!#Fj*oBRX+y6KNNI2MAJMc57&u6v`&0tC;8>lgmT z*GT40))cV6W`IruV*K9d!ONr#`uQve#8mp`JBzLM0w0(4hmS3(ue5n6 zcxAJUuqS?P2Yt+n6ufp#cvLEybOdgnf}NYkFeaWI_I|XNGi?SIdfbH);ttS$P>jtH z;fp=+F{p5&4l67@|9xuQ%YGwoF%R&1L#e~70B;y8MiVHgmi*86`Y^&L- z8CWsccX&?}b2!!o{4L5t5yJ{g?@^~YA42Aw|G6I1DBDYE~K}Wd0`=oOS2MigB<5kk)fq7$j8G z3cZT*uRshcgy+-00MES;Za!RN63;Q3ot85Uuxrim*}95*!;vL#YKJkV04p>ExO%7P zH*y4Y%1$o`Bg9lY7R)Em|DcgfH0vFl@J(MT631wnv!~Gv)Lw9Ix5BicdV3=-!5Uv3 z(tc70Jzv*~^De(jldO>~+jy((A;@DiOIfAdm&bMhWWML{gv7p zRM^O*P2O@$P`l87!1ke@q=E@!IDCzW{?~%ztVg?{G1duhJK<_-R{ zZjilnY-g|D_7sdc&e03joz{*Pt)&28gPmexTf)qDN4*~nNxy?)Sk2x^F_jQ6UyuJl=W{%bFlrH6= zu{(2NOWHN@R)PPmNmjgVj{TeFj?q5X37Bm3<}6lL=r~^Z+Opy4!;}dz*8A@(Ppq}0 z<7eB>RPcsXFJN)UW(Ka9gH2D)bmQl1@m@J4{Q<;dpwh-Tdo=p#zFxV1i3>W#cOCH> zd>^;(gHP=f1)P6814z5~T09WV{lP{j?6<|wAiH+k&|b(f@w@W9U2{uRJ)0Kzm-Modoa_*r zy1s@Lr_P%Eh~}QcY$hx>1bKlRbR8uI40uEF(dk!W!xb9pl^mYw+O>AG-a9$|cI!`s ztxnK^yjw+~UO6Yp10^@nL(wiX^7vK=&5^SLk>~v`&F0TwXDUfZAHP_&NL=eyKC3|z zV;i5O%->k@o_|xAiE!Ej`evvignCX7h8Jh=kG2`%RBgbQuJqp4D#wg;!o(spmfZh* zNB8#sCDt4_Qd@V>XZLO=!$SQ=NWNrebUQ#j1rFQLBb%Sb zCh|j1lT3DHb3MOzdIYNIJN-@iM6GK3WWOluPLDN$ZMDU`{69tK;+N$4{_*GG>hniW_nSZe}mpk-iX=sEx%Qah=w64yb%18AkFnRM6$%cNQp+e)o9#lx_}re+OV z>no^Dt(917YOTqS-(PS)uls(z?&rR)>+^Ykem^Tyev1Ioc2%(X`qlE&nY4wX1g4Ly zNC-XoX=dBN5`Fg%cCAAX+0r^aPTH*;i5`Q(YHW(FM6knWz{hN}loCpUenQCCbuGXt zm7S~W*9QQ%nEsnqzD9(YKoXW8jTraD9arrQ?J7m^txEXqC%=@3S%vSiUbKI&oZ-Vo z@?*mn-a=trt(!kIhgZX}4`1XnDLv_Zi$wifuk?a~_7~IRy0TadnxmWyI^Nmv>b1i5 zSGz~iaa~*B;w966H%kG7skPPKp2gcz_6K=Z%F04NTt9wyml5)%MZ$>c*}S%{#ps`Re~L_5AA z)s92zwii*;UWgY&>It%Z)&Ban@wbByBf_YNUa@s+YtQi z9EmT3=6@Uu{a3-l%h3BP$6DsO6zm$gn`&=q=Z1H#iFpIc zARuyZH|*Yb;V+yx8bOpXyz7)jh?EO#Isz2_tp?U? z_V=XzCeozK9*tekp5vR6zK*1asNg2Ge@zs?-!R%cZr1D6hUAFASN zZj0W3?!?oXBGKB=L&#rhX-nxF@rW0bmI_OgnNP5#Z7j476=&WjQAK?->% z)bgk6Bp@Ff`8Dh;B|W!2elC7%#idkRP8Gr}0r8a25Z5=EX>RT`IHq!17tPpGAE4=C zLL84CO$b)$^PGbjvh%(?ku|r~lD-vyanP;_G{XRSUKo4~6~qIb{_1gr7v}JTvS`i< z2c~zm*S#aOs;_9Zz13};F@eL%UU)q2;vrTc{yT557J4bjQi&ZZAB@^d4}^z&{k8v+ zE;{S@T@FcHio3&I)s&p8o8wgTlrcXI-zH5I)#v&LA^X7y`&aAN)a;-K8)#NYC6CMB zv%7uq6+{~+l%y+`zB8T;7Uiu7K7KZtXp_(%;P^bo8gus}vZRYTM!M2lkYTaC z%xDOH+0FOQp|9PSYwe(cxg6Gqrh|(i{o>!bV+^B1X*G%edqrG!wEjgj?oY|nq5D%4 zmP5L4roHi+7d)0K+mA@Eigup%`>ABPU216mQ&8?HVyb#hg$B*iOFYQsz8xcmaEaxaB!ItY9q z8uqOcMGNQlE;DbsVLUhzn=MS(nFK{xa2iIAC2l}u*X^8k=TzRD3EEh+Q0<9{pN zL3qqu9<#xg&OBJ;q-O=+G^jnfA{3gL^{k-b_(l)X;H!$dWv-qrcVK zjuRNov*{0%>JbMssphB9BWdaeKki1qs6N||eshZ+EMgq)mvBYRHi@%qX8 z!KSXSXfI1 zi?`dT_s_;?bC=q~=KV7`Nb~?9>=4lABvp0mYPWp0j#=g#^4N!W_;no;gh@eI^&hSA zNRz}GhyMlzCz(11c-@w|D_qyx8 z;yYAXevM1|84yj>j_HL?oYjvSU1w{t3VG7<;MB;@IH{f|>S5Cwc8nY|eNo$wCz-uL zFV6j^!OHBtR`IN}AGZw=(){FqS$U*(LiRRNU%{pg!z6?or<$}4Ouq-2T;{vwL`Yba zwk}Oyl_npxgJr+wFwQxFgvzlrn&SnRU@?0o`kgK%mnA0;>_=qH{lzlbuRu674>$loNGQC8V z7PwNL2c@Sifzl%Ap6Zs194P6~x>NT;=q9htPim!o@!>g$2xNixo!-q8e3lpcZg6_O znmV*MVaTddY4eTt!Q&IbmT0I1|_wGW3R^} zZ2LCC4z0?)NCZiToHN}v>aG&U9{*Kk)?C2hn!hC~jy!kQ8F6MY$Gvh&7}E)gn)uE6 zG4qV#(j5`#7Dzj}6m>>1DNI^*OXETU+2c)`gi2Q{g6NEZnDsFW@nTta6=LA9Q`mEy zbCw5DkUsPq5=LFQfQ??V)<=bNnhJDsEtn>E!%Dr~CVk$24lQJG7z4~o`L|B~RVL;) zz<6+EggH$pnF(7)Ppfz01n@Gk4cG5;zU$16F)LdjE-XmV=$y!dTM zNenpgS`dL@A9Y>BP0#9QVaQW#pk0j6-Uuh)iMw0L#a$^0c!KYcio1kOdK@+T3hJcz zD+bgvlSl9?7h%2hw_(Sh9tyK_G1}iixPA#W^xqa)dsxFO5(Z>1CJ&M3XMH< zW&Qx!A=%VU#ymQK=Wf$$TnA*|Ak$tpNB5Ig6u#n;a4hQ7nn^)DKRe|B*U95sr?1fd z6(*34K~7*!>KixkncFaiSv!f~f*E8p(QFe4heg_F`Q06EQ|sL&b7(yFrr|xm{mR+8 z#I`m!;_6T`ADAG4$Q_w17R^+y3fMCQpkcxntzNF}JkrpUu%zhp;oqQvR!7=xUX2QS zQ|!!0_m-qVDgIgHsgqpZA`HtQFMT7dU<66-@dHZ#p#l?N#ndhyd`{+v=-wEydhd(VPZ7Ri!l8{d;>i;q{U~q*~w)ib; zsK#@2MmzK06+)6F`>~zQfE2g%8kI*{ipwJI+O)4yk{%ev8cVrrYyJ=1jOrN#qv-9O zP;0)UwlqKjIX5?P&O{^V9zz^4aywn&gFzhwu=+`_YuWRRo9N5?Y$(zo~8YFyS zRSaAT^T?!(8<56He;2=}tE>3a@$SXVnRao4vCJBR~6syeg{-1-^gFwn@5nC(<8$BqIh$8UCO=9wrN!sA}6JZ=mx5!!Gw(ugctvma10P($i6G zy^beG={?9`md`1D-w4%Eavrj;Ge7OrA6^#IMsSNROpQi;DZ?w37hPC_n|)Q-_d$E% zag(mHU}919G8ArSelK)dpg?wDliTRw;jQaUtapN4PL81X^IP5*y&?qlv|}uE;&6&5 zISBvca@{F%m+jtAVa+2%o$=O4lJ+#iHp7B>VHC0@tJl0tHqx#@nMQ+W>EwdyKR#OTm` z-y!@Q-#M?RYouK@>;fC)H-a%ElrjM6y+(#N^9zb#~AgS#VOLmiY9(fOinoEPvT+4DUNDsIl z8fq|GV~3d=Nw)w@;9o4$1c{1$zYL_G`|o=#8tL8e_RX%f@d#tR&zbk=J8j=qjpZIC zy3K|AXQ?BI-;;I1;DpzGS@FDQ(K1M7l*YBjZ;#UtTG9qow z9DVjo--))JSs6~*2ANL{2JquMUurKP?KD1grZUBA(XMfGX^8)ozFnnZ!SnP7(SC;6 zE?b)MFktdRv`w=TtlKH%g~^f8An1*QG8Ob(y-djKpF!;FVahN4O9!kp9lc4 zush57>Hrvms=^@s^BM9}$S&#}spn(ZX0s^(TnY~JFp?d43_#IdNBJiu_i1Xq-+G>90i{bkRD98EPn z&X*OUq4igl`WO`h@GQa|OI0*>O+4Fp=?pyE=h;5cO^t8S237u2|8}Ci{zhnoidS0* zAN;qLXIw+J&J{gYrG)yWcH&HF#+vYm%D*+ChSA(yD0p)6+_rczV)(=M16|<+P6MuV zv&lan6E;z7x)9oo5a04;HB!%$v*^VJYjZuFcT0CP$A=^YUDV{Ny`i?(CJ0&$qF5gW zB+A=eF+u=^J2|MAq1vJwWRFT4>4E|o?$8|PUcCyW-P|yqupnEx*0FOJ$;nr{hk&5a8m#jShS>-x~`9l2xP%X+hQ9#;Y{QmI58bO2%$hgbd!%sm85 zuVg<4U_enrj1krvI~!PCpLf3lRLt^wJt4wSObds+LyRmCrCfErzD=-FLH%8 zlWZ{25XE|kbvvb5ySgKOoWS~^BfP$G*V}LL%3Dd?%z|YI-7ZGg{-?e425J&}cQ7Xi z)T~$){M>Uwe;WeRR}6+(w2+|=&M2`^26ye4<%Kn^XlrPR3?;8M&;#lRBG$l6d#SAP zUN1#jgz!%%nk*3fVWj=D13iGl;dm%4vC&6@o<&4H%+Eg6nM-TmwKlJxK4&aGz4AFC zyCK9md{eV96Xx?KJpgD4v)9Ok0k--9mmB4PJ|%8#s^Jhq>gvp_>O6;D^>;kkg{z-S zLAVeq6jJZ0w}u9_)?-Pim4f2-xFa~Zc?gVR>o((6n*?`=;v<4&+4tnKD!UsjM32@euyv>T0$gPZLbQl@qN9JXjISY+UibH?ojK?DR!36tGeo}2zK=br&JG}l*JZU}JI1$g~M zehc#RSo!WFfkCq>HDmbc{y|1ymbzWS+{2Aapt} zXWubn08Dh1t{L*E6Oo&N4~xHd&r$YM_iUQq>jWfA#HlsT5z14kjPDx3&+P!mvxcJ< zK-CDA41`CE+lElLg+>0bklcXUN|H$n32|@Sz4ksLoAjnwYbJfNXHTuWk}P2XgIAh< zmSwB%9s*j?{RhdY(lBf*;zXR_WH3Z)`k8DI$Lk zyjvr{Py@f0oq0L&$&#J@`Yw;kr}|@G=0UEdc#GC-PAmX*NyXR=1H#zA$MXdTYt^cK z&54(u8h#itkNfpiDA^SLf*oG7yD(2#VmM&UEFuM5f52>AxVVQT1z@*=TqDm5=Y*Fx z{8Wg5&R(W4S8-Csxzv#b@xMzuxC5Rj&y%b8FT=G-BvQ|CETNNyD#HNB^&?_~D9E=9 zKPQ6#$yx|+xUf(65**x=-~+X-b&r?Xd9iB}#{JM1JwAx=_BlnvWVd$HDRb#kbFI+7 zo(%vWCZnbv)TlAQg(}{#`ICx29M$a_5)yo`>+7a8iw`mLm+7urg-gn1&b2KO1^pWB zjNuiC5XL73=bKFQi*I=%HTcjDl*T;vXvKe>%|`KWX=BgY7|S$!x8>K7T2gzB&;_+i z%5P*UFcH&y_=M5h-az}hHTq2qjJ7ER^b!r%;Zi4VYc4)g!WpqT*6MZRZJE(aU7suI z!7UCGPUu~)%6qB+Lz;DRZdyVDQVnk4M zhA|sfw>BGX&kn2(U1s&F6umDl+fS-0?JK>Jg>-gJOqHln5`_~b2zka1BcA9}dqhJ? zXQE*g&xO{^Us#6_b6)C2`(F}rG(Y^tp@=b*detqb+UOK@;r1A)@g_2!wPfR5m)onK z+wI#mO#psnJ;c60$r5NTR=go0&OMi{hz0? zS1k}1C-LbSKm1N{UF zszbzu#F#QXLr8T)=VZ}LRcT=2^At$YW{z^AIc19=c|#C=8D;BLD(gcgO?CrsTU}sA zC$DlXxMkBkTO!51acs1CI)s^*XjXbfPls4uNE4eBhkv>Xm6im@T~`#DfwD7BRfcNA zMNL;=04mQ;D@1`fWUc}Bs4w7AKUi2%n~RPoq$9Gk1e@o{+KE_2p-(W`;}QP}QR;gY zc4eKW>-&Fp=h1H|Ads7BUgKd|kp5pn>8$qmE8F3sk_L>+t7vKSg1>R#gltZ|BD9R2 zaFs@k2-URKP!Mw{A7OnSLIzK2KHUb%^~~w?qzF<0&;GDkWPT;Yw^L3>idFz+#OZ@P zY1qvfC%_Kd;k%WQyTddEjQn8YqubhUZ9W$>)c%9lN#ivZ5k>!cUB(KV1@MV-$0#atkLoAXLqJKG2E zB*2WYpfx~`8Kx?D=WD=&jb=flcw+nN)YRIC6Zk9mGl_G*z?c0{_RZimmQLf7mw+lj zEz|Sta#;2DVl-d6+~5_YJLqxSb%U4B?xKsyZP9fp)Es4elxN|JmqpI^q9Z6h(eXGM z{rA$Nh-{ln1Y#mD`|tKZSu1rQbgm0uIP_q2Z9hY)y;ECmo(zyF>8;zJA!r83P|g!= zh+t+lU<|Z7O|p7XD$*4gW=RI><$@p|aYCiW)nxymKnO0-dhKMq^Z$b{LJmZJ1aJju1V~Pr(3tZxIvWow2{?#8B?fCUTJ%%e_qMuYJDc2~J zJZ)JS9QXCG^23d{BVr^7B@&F^rxgPQL%Pd+I_a)`ls5K&7utgI`|?5@i%qP8Urh}S zklhxXo8QvITlfK@TIK69%)h{t|A2bqAS3CAo^EdR$)Y5MU+4FZ1PIe716>9BiPUYo4y7M|0FmKoTyDA6q}0 z#sy%G;0y^rhXrW^!dp6jaU1vNMc21=%V85do`2ZXM<17guWD)D*V@IT)nC5+r2RBX z0+MarCuP2A%!O&!vPXS}BBZ!zez70#eP z&4cAGj#9KG`Mu|bm_tGRVffAJ;ELRfcHu92xbW}?gl}2jT zdLhm{Nv5$zJX^Z;e<1`Oy79q9*O)e;8-&4{RvTvL=<;@$EH(?lLi>b!Xhw)WiY-J% z@oH8n%1h^$PhoLpM>uy&0|6t4I+~cOX-o0GQ$3ThB>O&W%ilsKfW=2y(jWH{OlL{_ z)36!3jPSF9m^JIMF4sFFTGdZ4uQE-d&?UXcLlOlvlK7K4hv1|zqiPi)}!L@ zozDkZ(eDQ4F$3a9a2>0F)J}wvd&LEV(W{0-uaJdDs4HDuVV=q`TADIn5kej@gTTN) zQRlfRoVsFboYcC#47bY*3VB1;+9;Q%w5ZH;8woprN42v<>q zJS@Z-{w|a3|Ccp8yuO#NQUkwkFUoxM?_+nhzrc-5NiIZ|IXGMIcf!kGHNAJlf;mLvoV*U_0Lk6;=3rs z3E~C|{3><&cETzD!L4akSl55k$;v^@HOGw9yzR)JjpjRYMr7-bMj|Fh2(yttkj`8v$; zS6{}M#bHf_Xaw)C1~03bW09GHYlwQPAdb~oJr}5$8SO;)EtLW%(FX@Zv=C+D z*7~;vY={J^y+T>3R79xOd&IvJqwwVh?WX=Wh_V~_V=1%X5bJhVgyBs%w_eF+Mg5UR zckEgL?#QLX*~%FcZa_TzmFXu|xJvWbgt`6KwI2LHqFc~*xSVg??=l00t$C71-;sPT#%Ek z9sLw(wvu`R6_FPb`+Cb0UN+ylqBJ00xX4m59D(b?*mk+M%t95?faq5D&MBC{moom_ z28(nA0a{B+lX)5OfnQd2;in+>bcp-)_+EynP5OP=rV^YGzyZktrhP7Eb)^NPxCj~$ z)_1UaPy%(!dex2T)L)+qHMyZn2Mtzoofcx3YL9lWn<%1nzzKqRqZ9>(v|52r4N_My zFEhfYVNfAYF>Y8k!r0VuIK0M)jBZ=$#@yKSwro0rJN8{%C@GHgrR?)V@rn33a&y2s z7rkr$de6IPM=R|bzT8J|C|MFbRpjZJ=aDkR>H!_y7{E;sj^uzZGBXo2T%@M;x7~bz zD+ct}(#ZjcdeH}m%{7rk%lqB~zu9ucH5|LcM!VEsSblJ( zIg_5QgUs*mAh%^rcf-}+=Lhh~uI5Ii3%pCE%t*7ntzk$dR>yjiCBL604vI6J&BaRG z4p5GBv1GcorBeTKICF|BN~);BWsdoPJIMnHz9*654_b=%DfIi&`ca?1XpJ=wHzWbj-ah&G zw)d0F(4Ks(YOV@P5xee^sw#M1$Tl0p3e8S~W`GQ$rJSPw}F z#rlyuQ&!`T^s2gE&4Iwr(}7=ow0}g>Mc&K0;JZu7%;x9X9%~a&G|huJYqV%qn61EP z2Xvr_c`>VN#_4@mZ!~~s}x>Gj;9zyj{$zoiuW0y zOYmKV@fesWh7m%tSa8D9>8t)NaO|~>(YI@FFI(Q-c-M&bmDKFE_Yvnq#-$YPf~MRb z%N;w)`u_3z%usGl%<@Tg-DvAK|Ax>Y)N%peQ(ep+@|iV3QH8&&Iq~-bTP-#ZeE3<7 zFxXZt8|8S8Y+l2$`Lxv_n3X$#CAL_bB?{ zT3FL~#Bxpa4ZtlJ*EK$atwXb%Tjo8kts@ddbmv-koI>`whtbVzVhKC^Zpd+}| zZkLrj%F#54!{77;w@)Hv9nGb=vU>?}5HsdsemcCFBRk)eJ>i*q5Wsp|nO!vnudL}2 z&>leu!TNruSlNoN^^!|znNp)2v!z8*K&1GdOB@s|8qV|?q)!J~f6P0$fTznkIpW~7 zMvS!KVnYCt8c}zW-avGgGHQi+FznVN>;=1SNzi&O!!Ol|YH#SgotYXuvDxM|dZ-VZ z6p)f^I3vT%e!$R;LGWd3#m%|~SHI<>1#<|TqtBw3`+@5jpQGOcmbre`TspKYf-&j3 zf6vqQgD_Fr>?V?LEDJi?(EL_tr7Z87z}!nBmsZ(8Ohqus_}E<|+v1ocWZ%jWo39m| z5q3s~-3o=95RN*8uiWr0(X2lE_ze5549FUsicflKr@UZzofE5&X^4vuzN;E)Lt&+o`E@;>dWiI$aHSWv&ZjQ2+ZjbaN3|C;LF&Vc2JihHdDJ5N z>Qbt@!1HqKrFMdlM8?LI4&dsxL?ISDaIY?>)KIpBXU`wj}Sv9#C~jp6ixD>ep?|8>qmk(;WSUs z5RzgW-AdkpN2WQ!dCwm5N$evHik8tuT3#VU!JYl|VV+L>T5!P^JuRxR?gyOHciWhxBv+D}L=$1?j7`2`cB4CA9sMj^j z^qcB*)!<_{`(PAp#l1fxM27!bx2<2DUn}5QN53fU_c+C+u2v{367!(-g)Kj&@#Aso z2o6|`Kw)xQ(sC0c+T<=(NZ;O18mFKaN2+45s#J${_M(mAG2&1e#%m%{cGF`LPv|@U z*1e#F^+8m+s=%9>W##zN5Wg@QhJ$MUIc{*7J?O!B?s%}X35$aG;0*b?MtHVmo$NFU zY1~G_9>7q)7?F;O$0`DNM6JiQLd)p=WKmq7dPcb6J+oD4obA7tvsoFQ>Se~vbDo` zp8jCfNfMd!Bl=R)WeAP7xS#_a^1ZN~jI!XZEB8gUEp<_ff_e;AN#1Bt+~S2*3)YL< zkH`^-y@K9L6bSJ(!jSd)46H*mjb710e1g>qqfg0qOfj#c6gQOeQ}2qawCF6zhqDFRw^DA<~&@Fhzoyfl>FUPGj=PJD3YFUk{Qs&-wnN`{s)Z>75$* zvgs7<-Cihq zUI7NgHLg3kR7c_0AuJe;o(i;xIeI+KMMR2MQ=l4Z;w7EZo3NemU1(& z6+Z|4G9t`ytuFoSCM^Wj?fe`QB-_R$rEZBmx6md}+;MN@Po6x{@0lDif{5;$`(9p8 zz5VBdZx-LB z#_5EP)f+Tzf|N*&=4I2MTLoDC}#y7Zp)mYB*9S|>6BdKu8!+R7mQd0v23)+zu*xx zCu=?k!B`$nNOjnVkr&%4u=drpOK-=)Gbtk+$ZT-+$v=>pXQQyCDU34L=XCcyOj7Oz zluJs;e$U3`N@_WN$T8%n-KV1|)OvJ~!HkPKw9UYi4y!4_trbXEy-ZoTR}h3CCZFyo zF6zPc#nmkF&+Q`uUO#U-=dhM8Tmu57nG+yC;H{_FRWY0H*L12xAXBX?YBc)mASw`K zE;V3_>Gi^7*;|Jx@w_H|t#f#l4`Lms5#)#~G&FCUmRQ=wYON?+CUn1j z^tSg60{+{C{%q%#06+%8eAz(E%pOaVP2q>a0BOBXPN&)E#}h^;bmO zY8I1}-+N~nMa+Z{*+2i*WDHJZDNvL(p*VRb*^#o0f-R>+?K0-E!Y@}1P~z^TIpL1q zJMtMdtQajST6phqXyde(X_sL|grfeRMG4=iDZF5W{CrsaMSrGJITg9_?xw3M*gbjb z{%l(5LKPVSH2AFoy{b3Jw=*m!8{Ke|lX8Zq!2r;_uKk96?<qwfblTVCFa4j?0+7`=!{(q zowvcMdgK*i(^2LNfC_)b!geN(HTSB2WVL=V>o__ktloco(=alR9#;)<4-;nP7Fgac zp02k^9WMA=K;F8?|2swDS!&H0k9STEWJpz|lt~%oChC;39x07i_jaVj!W{%#k;$z1lcQdt!Iq4$;uY;vbwC?EH)^*&b~kRbjbeGKLdbT@X%c zdpo!RW->B_z>15l6+lR)SPD%4|6!8iAOSJ07C2I5M3_no(uhm;4xv0vOY|B#TX|W| znMGx@0~E%6Y5TNP*h1*2zqdj$@5927-uADkP_&d(`#7wkXk(;ps8txh!abdqXBuQ&NVp`Y~(GoHbyK#n5J)XOYPk36c z`Y=vh-oy5oc7GxNt}76<&%%OBe;cI;ygc75qI~#UxVrwo)b5(#f)Wuz5V)a$$=J*G zKp-tH1@nt3Z%cq0O9l+6X-7b6Q!tvl-QAC~Hgz81ldCn`l$B5`~MNaU$&I~Sp|YH*P!(k`tRAQV7TN%b=X zE|Ob(2k>9TMFx5!B*Tk2OdF%e^?MqgfuF$+lyyR=7oMeUhgEFGX}`40uhtg7NA`b; z0JA-uHi~2IlUe;vDXh&>@O2L4G-#9>*`OT zl1>ei1%Adup0;={Oak9R8pF7$;@|d3EHzP%K)g;wO<{(ry5#(oxvj;l08IiXRHlvz zM|`Qr`@17kjkf>YZ|3wP_ewg1&p3Gur2TO3N;HxBu=CI;#N{J>Px?IWt762-)~;Y( z+KGbsT_AFT6JXe94S8z5w=f#hOw}_yl7bEsBk71t|5!6~4lz2?G@-s9Wxy3^{P_+_G+edcVMu^~Bg7uHVb(uteHzbjW^{s5^51TJ0uf`J$6w z5R@uQx~#)VD(kP$cb+1NrXjzt{lmdq`G^KwsHfa4gN6ypFIbw4ggybl()NQ$cr9|8lOY?9!%t0pQ6FO76_gVs4MH-X)iq|L_e4hduXXNeL`liuDYc0%n~94V5OC~b z7ow#;h_L>M6>=!RLoeO)!+wO=G%fW(|AG`Ok^S7(NzI&RD6a&)!6!KP*Y3S~G8~ar z^c$`9ij#M-f!y8CUUu=ahQ#Q*k~k$Nhv2Kb;0#G((qo-_U$@|3XLpI;W~ScA zLO7elr`i8er#CBEYr@yZx5L-Z3dJ-z9og|e`bb9U!5*OCLg4Ua4>6?4xZg`*} z@{sUmTyculksH!$YM-zKw5^8}vRo;2KiF|zbI2X!5UlmHqQBL{3kvGvE!dWD6Znel zW^ORh7bf13Mia#v6WOg8AD@d+L6$3@k()$}|WaP&BO zmu(hf9U;BQd-?`OlQBy%vJ}~)C41QI=$i_h5+-D)Xfb2Y!jDB(19iG}c==Y(7_`!k zd|fdn_hibe0X~~QbFSKBtJoEv*8R=e*PWxo2WRU??Kj?$Q*4$zo!jRTMb?ytnd2p+ zV7S1m`JEhV2=Q;6jaii|H(z-sJHkme2no|vl|Ve| z+$CEB;?I8mO*Isz>2`U+2=r2pHXk=v59|E4G80pCX4Ngy(0|1quO`saT67ZZ705eyo(M9h5op{t#Gei;Qc}?l ze{5&!!9vM3|9)4#T@xZrIZJjp5Na_PnF_V0%Fm%zNz<8&H-Xd;b1vBU{qW#AzON7d z7=^TR6@XwDnJxAt6!u!O4T&tkyORVIJQ&*eN9Ut2YqR~I@2-{S-V#xi)?M^GslD(t z^hWSn!Sukj7TGiD1N0lg!2t-77*n_voAsQ64zFcIG(SgT7gfYqyZf~sS(ti)fUv*| zfcEph3q6iz0q*t~?8&}#zLfn9c?Cv#@>$%QDYO)2-{z0Mbwzq_>9ay_0jX(ksguZe zS!p9p0ko#$Q_22u8U;Jt@VL`l1HaB|&r?Nga>nyZM^_$t#{er5p=4e zgce|dKPs+&A#=%#P#(n=1h5Ikrndc~%fPCD_hl!urFluvr*Pq|I3ENCETwvXq3hIk zl!2@28Q`^1V$6CrbM{w=@M2rR%@t)mXi_Ld9nkaOi954-52rY4z~64T;X(DMBZ+?$ z?kuG&&cz(m_i*RPZCV^~Go=fjnj|w;xwkquvZ_4ly%1V}8)Ghab}nR$oUC=1DRcFA z3f1T76Qn4wC@N`G=jEC;OS4jTE+bF`12*o});+#Y8Djso5?tl3WB~W56L;6C5eM(; zo`DrriJ%Z^_OL_g#!%|n@j_5i`oOK z3_I&ZT%yl{TvxTMM7XOlL(F6~pH~DY;j(tKRPh`N!wK7op#CDMMRPIXsJ? zHN}0L1-sUnh=~bWw$UhtE?FFkHpd%L#knp%E^Bba;6om<6VwQmGkt;QnF!Pj62d~9 zNyHFbU2|*s_>lFax_hbt__KW?4Ii86Mcgz94t`gnY_yr`(TUgFMgl=+z<~`b_fKSJ zYqLuH*X~A`lygmGlYz4HW6h3qJ5c*=0C^Jbx4|IsN@_tm(pVHEcYbVY=YXGh43%XA z8e{2HDBwxbnE$PRAIwd(5!AtY4!6&I@Wc8xDm|eo@qMpL+Eq}lboomJVMw>nPQ{4e;ieXy+_#pu# zRAnKF4}_iIf7}t{qp$imN1D2YrxftDWLLq~tIZd*{upG}WBSOwzQ(JYFt1uqb)!v) zkJiSLyxHWd;(5D%qUUhTtOSXlz-;{=5J4f?B^cZ2&W%}P4P$h6bN6%i#JlhbPp;+8 z(Lp-TN}u7`SSu^y-eS1olFth34V*vegFdVC@32EBLjvBjtT7B#O3-RBdX2Vi(uMfe ze#;NH*UCpO*YlUX1bf=;$e3cp5|w&2{#r@+YpEVHX+0G^Lq?|9n8Tc6H;L7EuIKXp zC?+q2=6Uep9K6e|Zo%-r4FWz4!CV8c^kYQ3%9}!Lidg8y%h^-yn+D8`V}qNM=Vq9a zf9*C(sk`4He$@WZsgW5|0l&Woegdl|>Ary+90<`f+U?|+e8%Twi?ZIvEgK0%4xBD} zZ27bVmD^6+ay1-YoIjXN3KIVOGn=wl*|{1Xml(&lIs&@0`tTI=OsZkLp`X}4?t#OZ zr02=-dK=9iuk`XF1|q^-DU;@qv}F%=tQ9#84^a3TpK_L1o6^fDuEad)EQKJk`QuVe zQN@xr?JnKLX78&jsp~Fx(Vb|ic@X!+7wv=T#SelQ;LfwCc;v*$q(?pF08X)F)S!q~ zPih{sJTQZQ+IFv9x66a+wg!wi*C)&Anu$?tu|r9-we$5CEwU5CSD<^NqR^t^wsnl) z(HswzCDhL~twsK0<99T}=k%JKxG?Myd}l0c^-DzYBmdZ>)!&7TV(8#nFLd;u8Qpee zJ>cY=R*G1HSUj?e{KrNQ9(_o<3>~w^Fk=8#D;;cCmN+R}SK94E6_Qk}VQK6n5O#&U z^z6nl{9Jr@#s4vMCVolZe;ogE2zY^4U}_v5P-f^#(Jn3(EDbCRT{S>0wQOjs&{Y%g zK+Qt!z*hYft?-zkZHm^Kq87Hbp&i54Hb5=3+|af{J0?H*6L`Sq`~AG%uh;X5#97Q( zP~P$$3VvB>Lsvh2al>JcgOeyy<19F7?FHKJw@-oVpxef`EEU6Ao+uAvEBnVupdXCY zw9gYaflSfUplElz+ggH4f42dO-4X502Cxb3bMsarwlUspXj-LP#9}E|Hh|7o$GH6k zwXwf5w)?|aR#A5c*+uD=-rRO}^>b!h8zgS!tv2Rbr&RO9<~G%?0Udj~+2f{A)c_F| z^BvH<1K6V^X5k`YN$Okt2Z4Z@%|W#PxRma;hS4!x+6PSF66+Cmh2DA}xuP({(hu!- zAa%QDmqAR<=oj{T5tIX0JO`beG{}=3AC{qEP~cto^{f|YCnqh8P=$1K5m=DgLf{wzXDqf92ZGR`255Gj-rLLTvNFz!)*VQ3E*fxYwu# zJ+YAJH{goZzSRMJUin<*9RB>k=DI>6-a~14cU~3b%8^dpjCdhe(zq|<+}-QdrQM%x ziw-eg35*Er;C|*Z-aUnfn60tKC=mD0jnCu;rA=jxh{!k8&6ktljmk60;SFY|FqU^} zKr(oN_t(aLMs5%Yi(Log)m8RNK{l8z&=QUc*hEy7T%x7F_RS}ukGx+hq}O>=tDM8I z>fua7I&W7A-BTVlGLW7kB8AUQlr}Emy%PN5i+euV8Bv7$dIS(AD2P3;ZVQFvY{FK& z@rlt;j2IrO6E!=zZRUonC%I<^IC#pA*!r~*F&j@$lY>fpA4u;~YDRk`{r#oUF z{%#F;S;u2Ab%p;&e(t7)*~mprrZ`Y}IOG>eX3-)*Kt$pm^)Tg6?}n7O!4pw;=6Bf5 zpOz**u^8tD5~@0A*u+gMyx-#`^?vlcQww%RHGKp>=&VwILT?$5|aL@H`?+0AJRYjS}Xh%X&I;7?K+Zjy+=} zk{-Py7S5zl`KO-Yv(*eJU#5r9_zdW0FCox*IsIg?i;V&nh~YrARMY3Q_{0Fo{ni9h z)XU`|>OW54RpNbWSv53Z)daF%ysUjj9f;*nNQkLOogdvWHn9O8av&RtfCoIZU9Gpd z+uXFQk^Lu#3kAv7AOH)3&MG^iT4N$p;AaKNrJ+I|CxdG4)g7}K72o!2mk{Fvf5U%6 zy(>=DtA7~#s>Dixw@qb`*{Q&En2JaeX9LmUmpo4UUZrO^7?Q#5dNPVLtJnQ5?&E^&a7ybyRPSqQ z-Af|LI*T|IY*&sO|{2_7BtEr4}iJT$L zOIvB8J2frD-U+IXtSvBqd(>09nWly0GIRQ~Tdqpt6fZ6#TBo>j|5BueZj&s}Lhn5o zY*4D-WF1)Qxy{nOiZAmfT%GKNp4&beCGM+cJ*ZOQ=?zrljw}j+WJ0N>nsG{Opt8pezh9y5y{xX@qoO+Rm2x6rg;laj~|a@}@z{sYg38 z(e5lf0zNd)GO19+z$R-KRq!(|9=j>9UO<(>1SNA};@*0Js1fWr6ywfKccy_8-pjeS zjs@mjdjpHCKa*IpAmDynxml_P!6eCmhv86+fVb$qiDEEmRW8w>vXpz>kYOjxapo}S zsZ285oxm2@x5Uh@#@yJir4b9IwIM8us zw6~!j&}aJ+Cvxp2cQ2FPbHbZTqwmygk=t|s`f$*F^SSx~>fKUHaI-!yHs*?kf_g3_ zxR~o+rku>bM4*Y*A*RM|Z=fI7@K{Cb!O$mlL@LOqU)gdV3TM@A+f*cRA<3%1#XXiY zJJSPa%O6gj`-BUl{iV(8WHKfJ%+0Jnyd(e+X~w4r;Nh}^N3IwD`b6#51jVfZwZx<4 zPnwXc_%yU1%X^vx*a~+|3W;^+VIDD|PjM4>+<8n~UzAjHq~*2t1UVb`MsWn(aLJs; zhmyw~3+JEgN5(k{x9)y2zZlNQ_9$8eSH*bnMUmZ<(#XC6UC7&|acs9AXg3RKUFV~% z(MR)$Z_{52dV8i1ficvRjYdgWSp8lah(hp*MWMnWy*@m#{_fZ| zgn2daPIubl#|BCaLeCUvGeW-dB7e{VQLRDL9J#6rddVa(1pbUZ1%)xxe^MgO)^qP8 zw^(j?j`41;so1w@#dcaoKkBn2fw2q7&)udvDjvyxYzeDydiW26uGTLL`MhC9W(r!| zz4k6e>BZ<30-%^}mhu`~f~p zu+uRV=&_K^h;MXH+<*9bA-}_;9rR~W!^~yl zPtDFf187Fjufc2F#pF%i^|tS(rfGQ=k>ub;1Ty6aI+k~&NOhT-!$_u56F`R_A1@d9 zhvtn6n)pKM7?A%d&~YQ0KQ0POMT3?B8XY7kLXK{p@K7Mu^l9IP*W&Lm91wc>px5Zj zOnZG(#ja%YMJCind(-h_(l}?HQU@%7@e68yfa$xNkOt|c`_!!!0X}RYY;tb1wTHsM z*%wzxYW@Qwn8#cHS&F8usJ-t<$XAv1(JZgm?4ROUM5o<;|9sJFN-((3C%pVCehVoB zOZTsSa4G(|3Lx?E(Tk_@BTLH_b&)&D$>Si2xcGv;o(4q!2r3W?J#H~^@Q=)h9MU^1 zhTTN)J@eO12OeZW4gB-;D~bxqEyjh*12KuXz1hHt2j2Yd49HP;$ph9DvB>f88NEz2iFd!OP_F0H1$q2RSYe?o?o*2&&3plM zz{5TJjt6W7$pk-MZdjM zf{6Budk3JwY|fZ|yOF6kERT6|4l7OeI`4;_C7b2NkH>kK@Fk40| zU){R_DwiHStF-g8T z5@XAe?FT!1dhmLr{FCHh?XaBRo}&^XSqsWL?E5Rf0&JjAsL6|69U^~EPrVSl4;qdM zJNfVt*qWLI^MbeTe2l9i2j_M=29x$3PTI{N&X3!lUUo5j5) zpx#t*^EFXCL;%L%WQA*6qpj4G>I36j%9X0by%1@l#-38~=sr}>n#tTYI3>4T_xr1z zrWOX)UhfEbFrxA@E~O8gzXPqXRmn*%Zz%@OQ)P+Xg^4c8naRp$Ex}S}V3og?3Ja)h zO`UI!l~C@CIMYg>ZRjPIBFBr;KQBC2KL7UFfr|=q=1SJO)92-N#?-=^3f8hkFi>%7 z_Tr`IwgSaq&no}17%?my|9Q|sWRa28>KP&XfvU$E2_(0)Y*>5e(Ql1Si@RWA)L4MDi#`8WfAENXiZer3P;pET(O&%z^IHi|96GX$FPTeCODsxE_U+ zrvI`^_h?ql&fGrGKyv?`<0LT_%e$Lkr(b^;iNVfK1otG~HEA25z+u3*ykVN!Z^0I= zP4#Ze>L!5>#7T!spx1BMN!9tk{-%V3xh$V_VKh#mEB2s76Cb24>5d|Le;myAWrK^4 zeBI$&!t(%7U(VJ!W58N6nX-NZ^OOC0c(ym1=YBPcwO%Z0LcaB;)=JKDg{w+-;RLCB zH)BhcW1Z~xeNClA!UIMJp2cG6s^!{+&x`Hr_x1a?;RvedPs^QQ<#Y(p9d3Ec>8ufv zBELOdm30ELOt9%0{yL$9mhu67`)c{fGgCuf*w#|;yxH$$q#yTAPXaoYkqgS|7~ekx z;!~N3JNxRm8!CN^HGJRc*e%xFV=-C@IIS|P9{+&V8wo1;a+v>~^NnlF+Zy?S# zWWQz{9;^Ww%U8@UX-Ad<4R4}z^3<}t#-FqVzLBuks>28=|ygSxVK6^A*uZr zucQ5_UlEtc@ zKv>OBuey?#g}pl}xY?lirY92+y6i+c2(iSx>L5Zr8#oq;k}*~Ax| z16FZhAZKR_9?>C#GG*3|77(V7+}&_|-NMMa*n@NBG{26w>}!1$?*)AhuU_c2b=>O^ z%R#OMwr)52Qa1`si>pYQtRCA9?3EEaVJN^Ewgc$#FLrj!;&L{`Ojxu1h$o8}CJw!v z23G7Ig}&pCS7uMlEpqgP!5g3dkQI(g!6RYl7Wse@%^h_1H1uKME{B0BTa?@AC7ta>;aZT4KGpcOL^ZVTCC#;Ln_<+028*mv1kHI(l|H`!V?C&V!RR zu;?jBqGx6iN7A`3CtB|#ztji`YDL)e#L^AF2*c;+;d=@%y*G0rhfUR^Xz$zD?=k-s|$#2nsb{H#RmUX97#>N1rnEd-8pNKKl|d zlv2tOp<)L7!g2Jl=025_98V8Li*li+6HqjXILwD%9Y~1R>$Bv|08X)?9HGcF;$A*z zNt`k+mmh9Gpy9JtIdTenBm3SzzieY4#-OQ`A?dx&=#fhg6TBTT^1ZvK&%fb?IQj=k zUkqf!;vvorUIoQJ2Ubzi5B(%l)hh~MrylkftFLFTq{#6}pL^lpXGa4!ajkggl2|?L zEBSt*nl}6{kaZ5xpC)TQM_a-+;B+wF4$>ZZXEU>R@OyYV7Pv zQJcHq%Q*N!!5vAn7Kl{=7Sc(>Lbki6MN^+nMq+BvH2e$hTOC9t6B z(kPQ}3T%f4xN$UZAy0v<29Labf09CcjXV&>&2z0Rh8KX?%pR*ZYgY!BF^%^Y8JX$p zW2a1Wa#yH`POO6V?qELW?uWQqw?tqU`<<>wlmdNf*F@htwn3u3b`H{hLlT;;?y1mFT-sRHp`_l(%(OEB;Eh4^qS^bE`AWYc9ZVeZ$@c2Mp6ia-k3m&Mxi#~>6G z2vVL9{0~J#rY!k?#>$J|CTf^3 zU4acqI88;hVUtqW=M>3zD}}Qn^5~pwlvnh<6YTZf5;wI5E%P;FYDJq4YQB&nZ_wuO zqn~HRi}G`La1IvjYwJFSf-g;ckAp}Ldhy*%3r$zHvT#Hn;FNw6Qit@kz(ytFJoFFi zAOE%J$m3hHd4h$^jAtO#6?B2vxHZ{z%SygzK$MEu0l=(eCSiY(=g#r z(!SS6FJxAEi;1o?FBhGB!s2pF^EJo+P9cKyrG}t={=58VK@SYBRpy^|lYvvHn$k-W z5G1>WzU%R-3Cf}?&-KZdPcLy>Heyda4fd&pT~jQr3cF?aA{&v8O?ZGUhVo2Q;1+!d zgZ6jAhZ?W&q>TYEF2z1y?Jr9EPelNbD ze>V%FcH9J7Q6ag_u2fjH$E)ty|HM=?a>hWYxwK!N%ev>5^-a><5(3M~q=S9xpk4=k zwjk(Rj@ zKz{VzRz5lD=pMc-41-HIVw>uc_PRiZizMR%*O9T5cps$XQLbq@1#6^EA+G)yY`+>{ z-S7-{$Yn_yL9u(%3*1w;@=?@t1yGX_DRua?0SS8)f@ShqYfC>TT>xH9MET5MPm{Mk z<4B*CPdqfjMP^v|BIfev&DE3swaqM$k2|mtNF{Sfpc`wfBd4F1_Pb!JnoyipO%;K> zuXW2GX!q82%6pFnmT5eRN9vDkVQxccYtCuUkQ|t-_l)7YpKSTa6D}5W9 zn!-fnNNaJtF3#xO4arC>xmLA@gep@)HCA3x_xGU9PlP{lfSc?I=x!L-csKkZ!xm|m z;)Kz$#oH?!bsG1RKra2(Qt{ts5Sf($NwRE@s5Mn z$p)*K*+bwAuUKq-DA*yT^6+bbuCayOKXa;ZKpuM@gm=+B9pau7Q<-dbS@<7g#&nVC z=-i>+QQJwr{mO>>y6?&7VXDipiWrL*FT?&M(K(iCRP6oP?bCbcfDWH@&q{pi#CiD> zWSmJk7;z=Izz3XKM#F?5hsj>+5hzZJF9J4Z&@!f3X`7~`B2d5+st(CEw^Cs8(_K#H1ESrj7!|bT4 z#&ZA<*r$76OO(uamV%DgvIU(ooV$?G>K52wOLsoK%}^gU;54qer9nhkHlye!$IkiV zIwaFj{KNb}TpeH_Zh8Wu2H-dko)|!vk>^@qfoj?TsHOCK5;!qB{Skw^|1hRT)QXiP z|Cx7a_*>R+Cvvva3tC=H8tpiJLMbQHmbU>5K{bgey>C3oX8?`%L9MN{&0_s|C$6rk z?H_kRo?FQo*Wy5|Ja$<1xOvMyuUrx*KMNJlW4t8cEe&Zo*^8WN$S1Xp=pCc?%hlF* zYqy;Gmr4~PL$K~SWr+ei?(khdlqgG0TSlGmbf8<sVjtDV6hf|(4P+mg{d!tbNdf}_;q49U{nwnX-E^;A)@w_&p1GhH1@zmeW z3v#^C*L3<^p6klaT>?vBUsm}*oL5m@pW&@+0q`au9ulh$~q5b z%q!}YoxxCEd*e$OX37y&;9Z9H4@H+@h8tYKp+viknDcO-vrknIek(fknW2B?ZuDGH zEHtmjwaMplM?1uHnLa)kZvESQ*Nf@PG9EpX`;pk5k}a0Hr{yA8;@q&Dml}oq=F=P( z`>7iut={*WY%)g+nIHTkp|y@8Izcx%y9Pz}`HFWTa7(eD@W!lQ;<<%KiY$D8jE?&f zz=BR`X68_+I$MVLtf&PAZ*4oU@6_R}R%#RXXV&H4IT|ee9hb#!P4!r<&RSaUo9hpJ zQ#o5xa3W~l=P*5RytAyoZRtIz=fK6X^VB@ZNw-5-B}>Y`Ek*$Id@BQKvC&>m@M>ETw+IeN^Nq4)*!5STJDHSQg44p! zC*Wg9k_YZU(UhzC((Z@!8qs3+?L6B!B{fezG3e;V>#Tq>TkTJQozXFXRbw*!NP5J&PV3c&f36{8P(7A z5J_pJq9NlIqy2W^uV#E5H4q{{-?g@nQWw!Yc2bm{0iqsddt?@4icjL!wqU}qh9nPw zS?YfBHzOp_0t#t)=7_#JM%lmikaP!G9<#EHMGjv~SA9ro7pd=691~D($#XsB zv`boEr@Yqlvo!qvRsQ36_*FRks%!K-7ng=j-uo}5IdfIK7@nY~(P6-<07EpJHFe7A zrF7O(U-++I1ZZ}sZvduy+C-@&(So|SobE~PYQdx6#r#nt}$XMM=aJxyr&~r@jt4zhO@2Ae~F@}Wmo7jDzs9mtzb`itBl;Ndz z**gB#s>*mNp+{lPfw;u&wM6N|rH%L#e$@Xt6@i^W+N|&hk<6x-Qjpd~kWIi3{;}tvU z>7dGMN8K0K_q*@Kx0`s-RJ5~zvcG}y9^{mi#C@fd4r_N$@M;VEULuc5XJ5g}XrPtM zi~Hf$ok?AJDMc3J7Ykmc{^U#DE_vTO1?YHIO$#{ltY56W?>}LDN(Xx?=&~0@rJ1HK z#O2~#7xD(SWQVPDl#Ql%pw(W6sI}9nt}Rg(2Xj5WD4qK(LJ%(9un9ozF9*h+rV$8c zA;$SD?eAoYcYE=2roq7JI-#$IZ?ozFM8MOFm&>7Mh;YttIFtN>Vvh)}ESWM{XCdn- z5%yWl6l_g!&Nce%>)Dqs0L*OKfCtaj%&ioNgq^|9&A0^BQ1e5BBo89je$YPUe^C*zBFX^RBfO)jZ#6v$);DhYIrf6{J};Vj+keTeXa~6`bTodO`%UcZ ziqX1u8Ywt^xZ#uDL2(x)&{Q9o)(QzOiS7`WM&--l>cobLbt9Trv6AN->F>$=(2g7SGm4yJZ|bnG>#B%277tE52|szf|0EP zN*v2h2wM@-iN@dWS2h0{SlirBK!2@v1EF@WY#7WQ{s4CJ8cPql8h?SfQIksgu=U_} zVXAhg_LZZ1toGqQwE{Yj%3bRRDDCW(zY^9qWM8AR6>g%@)L?xc;uPWx4CcR?^8$%XcNTbQ`8Q6$AW$8YiayaE)5Zpv(Z-EFYDjLuQ)(F^x4 zQ_;fNh|aZZM>z074LL(q-&o-H^vNhI+X;p~-&@W<`B7NW0Prn;3MknjCCLP22)WQ@ zutD8an7J?#AfH=7d2Ss?Egs8XwTVZZpgvz$dhc%*vevn$Y^U#E3*;`4FToPn^(N5-Z$I z##I;KBO-s$dr2>0KEQ4U^Og3&T=_26t*zkz&DWT`pGiPmbtmCJk*nV8D&aRaFNSF^m>y+@Ul(D8L8s8au7nsDk4jm9!pRV)~>-7FZf3AwAs(8%gxoM6i%)p<9{~uqycFrw_W%_xMgb$?WO3 z<(;M3ctyEjFt{S$hBd5mChv#UELJa@S;u{hq7bCsZ%e&W-gD~Z8}0XIiqaMF)$g%r z1Z?Cc6SojVgfr8a$i;@_2l_MS7E)4K@?9s)sAt7heFOB&k&8tBwsgcH^7exin0J5?a-Eu$?OY5_>VQuXsBs#23Q{)s&G;s`u1rY8vUx8B>EQN z;?YOwi1l!PlD&t5y}YX>S22#A7Co;*^n1T`X>A20HAo38Wi@D}4ykoZBfC$7E0Nj{ z)M)^%j>}TCxR3AAT1*QASGO6={7Gu;t;BvA(y{zDMB45SYSdCk3`|-{ znX+5=hBP8L>gQ_4gWeoy&lLIj=Q{0b7_-R2f`tr71w0oZMCT{#`%ZdwamkhR%O!>U zXkvL>v`wHmP_{z=($Y_e7cfe4*m{8OGKJV@tR-5?nSrJiMGomACc4C|TIAqA;3yL# z>oxmhPA^1GE}iuA0EqXAJH(Im4>@Z`X_@{n&fk;rY9ws$Q!+n~Rg{#(7~)_iEmMK1<(QS8Zb(JuFH6CcnW?OBVqKe6mAasvLa|Jcx`rN7IA4=DB_h;`P0-!Fse-I;ugTi(GYDBj6_SA4FjouX`YSV`IuHfevQtp)H^+ z(#nOx#OV4Vzwx#rl{ChC6;7P28*4j>8ouAwjM)kzn^!9x+9L7h!p{(SD{3y z9X{^@&jF}iyf5(`(_4vA6RjuVV1h zq6Pl*Jx5hr^vWVok@V;_O%jrgtJ)BDWnaChs&yXhW!g8%ja9kstQ&lDuJ?vRzx>NnAq%oi)#|wmVLdSeu-UmzdXp%e%0? zUwi!5jK2IaMXd~**z(utiN{3z;m!_NDh6#(1U`xEvZLLkV@~LRss^~-bJX)}QA!{< zM!}DChG!N;GnO*LxTqxpaI0Gar6_3zdl?4b+Os)gP2#5f{&HTk4cKYkHKLyc{*uqz z{t)hVf9UAO{UOJ>MEnqM{_6z&9p?Bm`R_*VfarX%7ti)Cc8Xni$Dojm8|-Z3KspF4 z;!b}H%%j8pnwP`!^BF7k3&!DfPyH2kUSg}%9gVelEtGJrZjpL~+suz1d)HjYl1MCh zq|PV%ep)1Y>3-&%8^=^vvVD)sU&3h1bE3MaaNkvf@ta_y{F3_pTUnY(t()ae3G&k= zES?t>YmAh<-2|C;V$mZq}^D{!I#UmOzA` z7GvB1r%3Qz5pX!X{*7|9eEgCH&#jB7)l@=%={3&^a&%bclYIJ+Y@Wx3h%(&I_r=eeZ|Bg&LAt zaVIzJtv%9*L_@d#qw-^=sz6^2fWBdmSwykD4U?6~@W@0SZ;6GkDl*8HHbR4V#F z_c2hY?=$-=c1*%AU*tj?778o`DG7(Rk__>aZCNMClj443?C>{#QAd3idCCe(qo;dO zi{ez4BI-=>T(`z0)QJMhaWEW+$Cs{4{V!h^OT8~QPoMY#(|9f-M{&=fb`N_UXs1?H zlg91j44BIzNyfd0=(W3_mf00Rg_BMB_G&khYF z+4UBzFK0$H4hgHT8bvD?XRy+%h?wwl{;}S3;#JHF9?_X_?Vk>c)QqiqROvDJ&-;J2 zAjZFYJ#YU<+W80i@$^3vMXOH!cv)S8c3H%qq+e!Lkv9?ck(-LZntJt$Q@!a`vh!hwVU6b12)UcZ8~1P-j>%pU1s za!fwpyi_Sre<}HR92?+?T!T!DW-!mW^f{{TjtFe$1ai5l^|Xl7)psz}LZ{_3aYC9I zxh3IHb?U`fFRUE`7eKd6RBp4wLJf?C%7!tgP}3w=0OUhD)(es4O<3s1?^VS+LMkNN zT0N%q>+P;9HSb-;k>DKv6k!-E!`112S%~lcdTzv52TK(exkFO)d@j}*788SrCV5oc zU1nX1?S0iLTkLYwTe(~0@rADO+K)(O{!lUlZrjh3OV>_ouj?1x(%<~1y)}Ux7z#uh z-Z!rb2*o2&=!Xw=Hyzd2POGYL;gJ*A<}slRiqNXNnrpl!+==t)UzO%lj2{aE14=~f z9nuH0Lf)!ZzucY@D{G|TaX!t#*oF~{b5Zd>{$8j6*E8)iwC3tKbn>B6Zq{L=s~gfd zW=FM-4|NPYE{qn>ue05wxGtfWa+3{Z)h|Hp?iM5q%%d*@>YO>(bjX}=6 zXR}QBOI`Zq4e4X3(f=Kq1Vx*q+hxH~2$3I|d&c(9RSC?ra+Zbpog?8MdHOm6V$Zcw)?GT0<^4~C8v zB|d(Sr{HaHSOs8_rWv$D?oQ8KT`31yEwQ~@Gb#jY4zZkG@Ds~ zRBU@}!&dcro0=2D9#wx7z50$*!Y)7U@mClNi*D@j3gA*@2xyh;x@7CE9zE0}{{7d8 z??DAi)V>?s8yCCx<<84YJ9Ulj|3}@J&f$Y z2M|$CAS|G7R4Gb;ugB+Yl$z<-=Daf+O96`cW+9L@Wk;R^cdaC{L*S?glO_bFa{vD9SB#>u@6YNLW% ziB9m95*wjYHGTmARcPhVD^{mcG$FUUt_5ZJj#OqMWHfth;Y|EDeZl?`z^yqJyxxPW zd*g8XQiUj>zjx5jev{l+aHY||>=LV>W9GNfy0uo(#<>6_0P$TSjJ(oDftDfV)Wp^t zLaen1TemZ6aMVTZ5Uu|>V*1}dI*~-MdUFcgouGL7tB2(ZflidLtR#ut~pyLbT&3G zR+j!y63@D3HnJmX2khB(F43FMMfr+}f@@R(5VO5TF(?eVBSw43{Pe@ggG>3>tH7;q z%U+<%3#`Ogm&=Ed$gSY=gWelXKJr8PRZYe7T_C^I9KJbXim_5!R>DrNHci@I_{8_d zD}x1#+DjRma_heHqY&TLgY}G^CV;`~a4C zLul?^t_vB)xGY1{pTJtCO^gh2b3bG%`lOL^WAE_RpOUUN3_nZrTCG;f7WRb0ia<4N zR8#XPZlc2ny@z*G`bzyic6q||Vuk>sD~`w?Hf&FGc10z->a*Ci#M$ynzA5X|m*rI#_ z<{iuGnwOdjN5d!Idb6{>VQ8C4kiN?CHJ;_j{6t-b<~`k>4L*+GiyzUzqaehr%Hh~K zkROSl_4P-9fs$<4XqN$Mis;6l!4$w;$&=j@ChV%qS$@`082rGW&H!d;oZ$8|3an8P z^DBnZ9VrL{L|20J%HF9@fo|M4amw5iRqEeTEFXt>kF5licr>szWTJ^(3POOXpJT^_4o#@^Z35y6vENY!SaR&pIkDoF9$*keoOY2+1Ckq?klQU z-Pztr%EQ}wQVW9dt_8^F)UD6xE+a7Se&YO|9ZM~pr(sOj;Zo<=%TwfF_$Mr-yg1@D z_hr`Nkwx?Un`yhx)B44+Gv^%z@H96swj;NzRIcO2+Rm$YQyv(DDKZ2)d2NM3o!*pm|a#G9;<49A=qa5*)4c z1+jye_Si*yEfHw&$an88VpE8)(ciobV$;`$0#fWRB>1nBezG088&`V1X^sDJ+q3~f&$U1;UL zT%=c%4^)dgxFvf*%@zMi9zGMsPQim>bFo)PR5s}97zbVK@bft25NASn(t0X(Yz}&x+>p07}(r>w$r`P(p*f~~@ z0A^!WI&#Ip=p}<;Vp=u6a)jt+`LWA~LiGA~rqoYs=VtOt53-!n>TW39vi|Y|ofdI! zHD^LamJnbhh|lm{Khu;2b`0yug`2acC_c*wyHh=s&MBY?x52v&5F9kL(nAAuxIpfA z!%4GEoi#@Ip~>X=j=SEq);AyaO;^$&bZ?pvxh>XzRp+})YpLm$MTJ7u?DmaXM;sQp z??u!Qsl{zi;%5}-cUd9NoSf_IXfSUb`tBvD0j%V}hN>IOFrS@wMve*VIAzOAHJ8`xTkCZ(F!4R^!!E-2i z-=~G24n8K@6Lrg!02N*G4@f^;;3e`d)YTRy^|Wg2jA! z^4J!ggpyAEORmb0*Vciu|A#2joIjf{I08&KN0d%?Qs=z@oT&i$k>jy}*L8usUG|1`UVtV~F8 z_T8Ht?$FAZGm-^)6j^4H3lX^9&nTSl{M-!b!$h1s>&m0S+y!Lugz2+_F^0|u_hIC$ z!yt_R>@20IhjjQh-&xs_Vs?W~R^XIbVGq=<-~cx^f6X4P9Rd);?>mDpc|*BF5pJ0#U{-7{=la`VOV@qIg zdNyHQNO+!eISFLc&xH6!s=o}kf4i(Ahg&)_`g1m29>?Z=y7WJWEa4>-RmTC`yd`!` zLWjTy2U?hOPBizoFi#c6P!^GdY*lwt{BWRIw*2c2dRp@$O5F&^;9A(hbyLx^G)qXX zhgIT+B|y!6Q(bEh-)!0`hABhJfMyP-l0I;Qbe|Gq&V{5*nr25E<^r2RMSpqK4mwq7 zsckeztd{`lA?q-Ued(V)+0cHAiw$k@-K|q)>qW=PWr5E6M(VEQ?-BgSJ6X3trEq5o z(Ld%UfZI_wNk~6QgyVb<)=0@#fhft79<@czKws#FA&9nA-7-sEZc!vmMB@W}?dJu?>^I8Chc@pOhbHyE&O$W9$zhfcld zGVCg+t*jpHbbT7Wo}gt)l#M=Ni~}O9M%r0?B_W$N)TYukusLk&vTvcTS^f4uWSm*P44IQvzX$0udtnV$ z>dS*`5W05tNj}xwquOub;-~`8jspj1H9fpHj7Lms6LokO+V)jo^}8u5f(f9=w7!J{ z6NePy@$9o@u86$%N{@VXI1ah0k>=a{)#U~>r+whmc@bGD6n=7zVrHZIzRwZhjU}-8 zC$^6c-@)Gtd#I142^b<}Fi;V*@cE2-IjMzchrx>D#min3ap5JjPhljY%5_0YDkec8 zI3=c-k5(L+ZNvHGU-M9IxkEv+#LWeR=?Ndn(3#L#Y_F4QBRZ9>fAbLQj-h;`t-uj2 z!9?=4O063wtB$m0g~d1&-4A>GaF8;BXrzPX1mCP57wYLtxp3xkv6Rmxcda^ZY;OI;E?K3X{YfoM-T3>zfLITO*O-d)(`T1Xy~+ z9l}e85b&0xW_5BHUQ|5v^sFXpo)hhxl083-`#(cx9+yP^#_{1A@B&XTEx;qNGPE+Z zV>vuPJJ75}w_3y_v#j+Oh2(v@2HMF)6kHy+{(T*RwZHrf{9lL04 zi*_vhn1ANKnb&LPH8bCNp3n1s7kpnzP^tg6RqEs$-lZEsijYJzYX$6|6zE(B%yB+) zG#WV<74@9K;0^na$hE5xE@X%;8J7ju#8R6u44z z6B9q`Rx(+Kcuwb=QYw1z8g%?PREzoVxtPJd$&QnQ&^8j&;Yu@scwV8 z{S)*`U!p`G9e0Qa(kr}lCkvn`dRt3^Irf-q&R<~K5)Z}?jswtM{}Bt6@ZR8GFM|cD z>z9ubxzMcKR4p}7VsBH1IV~B(NxqmJU|t{h2R6frR@!95<|nb#7k^Go({HRakC(f( zu+Wodd*_0$qYMAMfN7cjDslD?m3tg;JydLa)O3>QVJB>LzZ-8Pv|s6g&$PTjSTKZF zSW-sPZ*Z&?*76FBMqom0ZS<8;OT0*9U=RLIj^K!9l4hds~uDA;@G4{cOU*2cDeD!MKu7~HzGr8@I`|B`omf8337>~!K zEKh$!lPg^wL5@pSPaq}9(*;8kRnEdy`%0Y=QGumx79&fa(bDwoM{NBC|K(q~*o9Kl z4sLZ8H=iGOeE;;#5bAR2)#V}-1U?-=K2E$Wp&{PjrN0NkC{ErW2|#*B)H}Md5Jv79 zjjU2$I}#f+rwJet9zPZKrkw%JM-;5r_D!!fOnjH>F|_3By9N5ncn(K|+P!$vSZ+hO z>;B-Lxn%_>wv4>%R5er$g?`5W`HtS_WW|}QAtkPIN04?}j4=^@e^&HdVbjGP@cpsg zaAKK*q)*%rv2zqqb+VVF;hp-kJx*k`ao3Zprmvk&HrTs8r+>ZvP1CRHLs`1#cvol+ zV1q4b_V--U&FT&fCF)q08l77jNUnST4O2RkKvwIi$C-a!f0wn*`Q|!X-|1gev?#cH zp_3Us_O3A#au@m$*WKEE0rG9g@uIJJYntews7U-cWb7<%dp@N9(HUB1HGFMY7XAK{ zk6Ae1!Bgy{RE=Of9C!sqn)crdygu$y=lq&;eR~@II{g!$-3}=n+a%$}JDSVF@9ES0&*QTwD;a)^s_p zruEY))E(b$T<(GI!L@ZFUcJgKzQ&vLe4Eq8Tq}B^WgNOp-Kf;6H|Dv<5IQB;;r)xu zL55#c)znq8Rs1$Ft)gT#0!!kgzyv-}2caM-ddFS7*Kehd^NrjZY2|$WKZ1h|K2;H7 z9A-fn%8aOju1u_YdOvBYM$nb{rf{Hb?zH1~JY~57|UBVQlC+8;Pp>V=$9jlMfCFg-}M`{`cIZva#wfI1{ zpjwd#&+s+*pJe#jNVhG1<@=g$Fu>dWthj~8;|t&h$X;`gv)lCP) z5Y2qS`qofTTH>*ZWIVjtxx8kI2UQp&4v>t7{Vz+YEVc8q+KB`JZwpHKUsEUpV|=@* zr`@pI4dnG4(cYoerd`8(5C~0shKXE1Pu=#K3(A zZ7fH&+(qyaX`TA}dMJ{-2Wynz<+_tBHhK$vDrO8a1lVuj;i<9iGl=Hf^)Bq^e?|Uj z*Ji-->*XD4D|}A0tw;%-8c=IHBV-S!l-hQ!qzC7CkN{S&Cg^a4o;vfx^zkp&yX6yN zdFe=(c(;Yx#N7UpxI?Y!oJR8bbLZ(V6RSGo`z6M73@3%@oAeO!vU&aHhVRJJ(?5Rk zhY6oXC-`U5r|@nA`v(cxbiC?qAQ2+cxMnzC(1oR>6k+{*>k_wtAW4en(mGS3yLWvC z#_VUhp@g}WenY_`81>dneo3D~^uJmS?UuQKoOc6@9j?=~A=w_%*SdVRAk`%>&yn5n zFEO2gis9I%;b_K2k->{dLY?Y3Y~(@+<(t3yEmu3)dp_CIlyDoH`g-4&6motn<8j;u z>}U8xDkO;bRif>q`*Mhg^T6$Ji}MtN#Z+VwA>C#mU_9%TQWLz5|60^qU{l{Ip}qqW z7wRD84!%ISwyo^?uJ;4Dv~ML)aOV3dQy^=rV$^*ksSWVK0qc)FVPY>nC{S%>r92s0 zqcs-#Y@Io}YHrN@kNl9QjzubQ$tDk`{|~#qHQm#61PY!bF#}m^Rkrq1LzbyLQ(8jI z`F|_$xFyWMs#MnJe^@j`rgZ9a*9BYk;UhU79DcM;^{GguC7F(*<&StnN2k!|ISB#z zRx$;=e80KOh3Ea%Z%6CQ5*W9Tb#%}@8R9prGRr)7>itc^9P`2xzSqnihbZ_<3?IZ! z;cEvRzrg2=4aNeX&NP+W4-3L{l@g`dW2p!5t>#>K&(Nf8-99We@$t6z_>?W4_th7> zEkny!?cmTCavLLEL(^^fJQ*UO%y#pz{05goedJjpshTN;fAw=zBJS! zrRzetZIE0V&hiKtY^y9v**yqUu*wIYK~C%?|05XW?EXcV2v`o9&qE>8HAmx=@1iwl z#bd`4|0`aX8qhhCktYCcJdOyvr1r;+}-By|~6Dbp1L z8&;f3!LRbq^BdoLXkO3;x$I9v|NBEtGb#Eqq$}4ZmguEK@=yqx z+1%#6v?h7hQklqMU2QXzK$L7D6e1fPenXk~0CuXM zL0DZ!U-Z?UqK^^XNzotDa#=q1WIs%mPP8IoZNUEEF?hM5>aPvYS;}nsQXsc-5o8UpE!d&-R`L1=S$( zVGooO4`xrjfp6R^M8ZkiU2jO0a^$8wpNo2$i3HW{S+0IS%R-;gb}!{ZumKE@m1ywW z&k)6v$nLiFN}w7G^>BRK!{aw)J9v9r^y2(t-1TNqXc!yYdNvlH!(scP;9~{#_aj4?nFmSgvWv%vD0{Nn70OJz9JCTMO%)JT)~LlLh*NHt1**g_(*<9`csmBYQH zp#oMrx!(x{vFPy1rvJ5dKQKf;n2WjU)s^zp0=4>1ca%lfz{-EWF_JF)FhR0GPZHW| zD_aB5b(=QqC6N)m$4QR{mz;hH7`fJaBH?O^@)JD?MWsl=}88S@fqz!rNlo$<>Vwm z3RWAlqh!8dPC_(F)9)0VV6-TI+t*v#e)#}Xc`Q4Rrpa?vHOe!F$ zh-JdX4T&sjp1ZkzO2!MO&P*tR@nY*O$6ST;0$M2uf=E+oO12v50j9|t(E-6<3#Bwh zXZn{Ur&8+N=+JNyQy$$}^7?%-Xq_@t^vLU7+*)b?MWsM=DT%+@-&|rX0m}9VndRcN z3m?CjqeOzBQ133$mxTIegU!kQZd8^MtQWfcuX;>KON@7hF0;rV4EeXKh)FaYrsap~ zbT_s2&pIF5Pu)~-sV@xa8QxCpBUWiCi;j3k|40#cWev7NY|^!Z>R^U2+U0F6ZOv4@ z@*sG>$;8mXto|(UC4p)F%KdyTll$cm+PAA}Exu4Gqa<4cVQBeb8m_2)GE{>t-hLy? z_j4|gZH0W{uFsHr?T*%#W{aS@(rlC+O+dLH=rJ0x#UsHmg(UE?zFI=lrCrX;b;_Gb zTUrQ*J|16%li9wW?C`ZBWrp2q5~s(kVhH-O0e_)nDP5)-i*&9xJf=aQ<5h1F1Fdxn z{W6$CrNs*HH_6|K(O!tPW`BUUS(3kKCE8m4beMmXMdA_t4kQ;%;TG@ettgZfu?#&( zw5#e9XQ4K)D*`YVYj{Ycr?N-@rlrDC7fi88S0P{+-eDl}ggVJpU|f--4*<`8DP7|- zKwbL*5#f0%P#dc&01AQ>GS^ENY-jcPznjE4QW1sU zPL$JhzieHMSWPk>pf3Z+Z<3Y9$B6#{`4b!l`tp}BydK;|G3EB$VSoY&Rs5$uu|i5w zic2!!xAP)G$W!b}EEZ_(ka}~=&ZWqNjVb1S_tI&Ksc6}8Zz>~kj^jRN+aY#e`kAfzZ=HA;>39UA0gnODXwZ&9?p+#_ zIItIE1Yj3IyJXHeC-~SX1Q=f8;+qehfdU-)6M%!Vl`=b8<8RYd63f5g1*TP?z^`I? zNF*9GUPnTw?FzOerb&fsat@kp?|HY}YPhuT#MLdLMtps-LsGN7HQ)w>XR+Zz8~m(i z`^}Ref{f#D2$;?G>R6BQkMY@+ps!_*VbV8q6+2_s$|(D1y|~o{VWfsTKXw?sGdHVzH3F}3|ZcFc+(=h>5@@UKMvcS z-l?Pk02l@LRn4FpFU-x9*5`y!kPX%?MWq6xromRk0RlZ)at@3c@|0&^!3eD(9Ol`X}Rxt`iQ~%r_Y{kP5#DnPF zodemQg~1UnJAs+j_H#Gq&>45W2o;+%)2Pr7+CU z_%LXC9?5FCjEY3T@6Xsnf1SOZfWb>4ZH$c&YY%$ZlhisXE_i+d!oz4U7>xWQkY^~h)(T5=2#oFja0N6NsX#qYl>tORC%ZA zgA`qsQc79CXoN==N`O$Om!;ZkKLVj?76x8M`Ws965P_zaEn=(?k5MPAUq!L245&gq ztgR{LRW6BdRJh$JH6UENO-78dNOlTgg%McDb>U zmmT`Dm^Wr%*#{(Z__f`aG?%z(SU$Rm8zjgusffqgcUCJ&>u&3dlbGC{xRm=L_plqb z*Cf7>hF@$S?*4oE=r9N40NfPmz+8>%ueEJ-FIc^ z&r1+a<*QnMkOTE&dQ7X5h-{_uBGNtEu%1Y&kdJL6boIsvS%qziwX?E9mVcYRCKS)= z1EKkHPxn`hC#^qKAYE~#6k*LK_yPmE^vy?{#zQ&Bi6<>)Tz9eNU;z7SHCgor{r;iPOB=iKD3$x>4^m!>_vM%fJit zmO}13hA?*=1w2@!Pz1ud3yC!RPB}G#urx6D{(@PlvA6RwrfB8LZ-O$@)xRa35nFm6 z>sPPp>LI1*6(76W7N(lC$(&N z)WZKV`04OS;33coE*zv4p`!cLu83At!}?qOLp-n*>v(ejon)?0OYUCP6kp}qp9Cvi z$0~g~=H)F~%|fw=@5P<7(KVGcg=DU>ORKBaMe7&PY8lHO9xG);eq~O7`nkDxJ`D2; zRv-QrHV3OPKLG|0n(B+~_=+!rzH3HIY`hfx1c+QyR}t7+GtygI9ZBL$jC%!-=32*B zRHI+(^kIbc;l4Z5yb2RY_oTj1;lI1f!5bLJM@ zYv|4dg(Ihi>$QBa8xlEIs*|2|+c~0n?))q3x zBru_cfo^NVPZP7ntJ7mPdXgEmv(TvDp5?TWftOMSxxdi~PGe7%Cr>!XlULggyVnBt zhyv@oiU&lEP^{Z zm;Xjjn7q2BuB-Aat>lrc6_Z@XoB(bePFgm{eQQ8!`PS7|qCa7^h_ zi8w;^4*H0hYxnkC4;kn@6!^BMN5_xehJ!#77HqzX>Z>)rp5V5q8k zyPlti6C)S3kg=YF+l$B|h>FwLv!TeHi>>;Lwcz5^n|2%*`g7XgKkM7%NmE;rw=y=| zn*C?}#!Z{A-!p8@@!x5m%C3hQ>3&5Lxdv}e@)@qEr&!$w+g%tReuum0>l8*Lj5GLz zHV8+6Ei4!u5#j5-9yA`E<287t?pr8oq{uE#f2->>fRnP%OdHLgI+4BDqQd)##jt{5EbzpI}3AZgv?R&vY8e6iA z;Yz`-snv%f8|#8gG3{%!S5KOQJSru>zl4|-tI4Dk<3Ly9RZ zlJDMqpMvW7t!7`_71Z%~7jO~*&x3e5$pgM}y>LCTAjAOZD{BG4CJve;7AwU`Uk! zjQaqNZUVkEj1mr#(u-DNWBM5_iBvx5turw8dgf5|wmiC{w#sj=)@&O3yP2zYQ*MO= zVUQ1u0GwCLFyDP3bk)-5k{--J9|^vK-z$TFiVcA~y{P5WWoOSxXjZ|>LtUpyGhZ0U z|H@b-0cR*(c_{@%Xgv7J>CP=g(`7>yc;m|Az8Pbz0E-(*o*K1b-96^pMS2R|IckR) zJazhU{I&P?z|Gg*WnBB|UD4^k-YL_V`XFO3`t+&CNa&sa^t)Q$ja{b_4S(V@AdgU^ zz1%Uw!zC9t<0;qu4#%QH6nauuStC)4Z;*Pp(RM&-Q53x~`dHt3H|}V6p=$ROLsVwOP zqkx)*>P(qk`S7vYxyHhGu*P@JfVoIR~!v&{SSz5(+t{}_ERj2eN?luca~4m&e8|Yyp>`D#!}MV&p$3&&t~2s zvq!mn;bPYKhUpV^cb0tqW0(nNbQgjMdqOEtJ-S*phDb4nhMvO>n>yXh53pE#5k^wo z3p$C^PBXr}tBLl&;Jc8zPFMl+!%Xzk$4mdfSMXTh;&fa~ zqK{H2Z=QAwdQobybwN@2Xzat4Iw&jYTFm_!E*w3S2W=KR{K1|)-#3O`Tkn9EYjq1w zdqIMxMW-7jFp)^P?T)7iD((XRbyS3s1chI|$|yR8J;@dZgv9I{M^8|n47rG>eu5%S zno#9*!Ur^;4qV)?AweRT`0{2QRz0u%UlXQa{N~$ zoDv-k1tP}2?h#_PZ6U0Ny2s>(`T5F`TMh_e2;Lk*^dU1fW1f)3>Nln{7ZR1KZ{i;d zvk5}A=#~m`1tq9fVJM z0qOB-#a^OVoEwYYpCJJi6LwP!8UTi6zO36N2N#ArVET5ay}$c*t2_@Y(ZYuY_u$rN zt5zqat>U5$h|s2r(7z0vBNg%i?44FI|C`(D2 z*?bt}Q^ogu;Y6;cJ!3QsYKx#xw9KA=teo8)THn5vJ^Kz7Kw>X3GG%A3xjo>dNl`2F zx@&l7zpI|7v;DB9TXQ)PsaWVoRu zp$+ng6}<=kQn-|=i_gC>UzMq+R3+L2y}Juk)_)w}+tDvVP5VMNYt_F+jwx4@}@*6}m zUXer5l9lE0dA^bV1Z7|GT|gn@$_*D&l<9efLeY3w_-vEc9Qs?Lt?r0Zk5i|40#kr^ zRYWi|fXae5Fyz!}lIPC0n$pJV9sLjjR_Ye4wJ<((x`)mZQ*zT@gfQ^H>Qkp;E4}T9 zrcuajT+LPQmsE{(cPm;v>^Lte@mUu%&;r18b!_tp+w9ITY?A1kk{27D|+9#vZsVY~w}x z&Tf-r(qF)U{GG1z{7GA;B(iiU^=V2aBnyImf=cRUSN87NJ=BJ5D)N|?2_(U%T61poJ z@9X>9{e&9TE@*DCIV0F>PIqfcwB7e)2*FFraVRBm17s=5sdbHV>b1Nq7dHV(*;w~c z9)I(cKXDLOR+T;udY zGVkTpLR|Qb*FU9?W*6)k4*da!KXSa%4X5HTVoFJ~O9rs>W03JJ9A~Z&LHsJx3YqVu zEOon^q&Z7jr>m7o@|0Qw{QZ4*Z+=kp+dBdPf zfJ4@WP6~y^_?)u`f=%Uqx4m#~QOkLxc(?7=gM0kZ1HD6ikO9)^6`Q`T)4D`Zp#=PV za~`IhRdrEEhbcMnvQO-bV(fqRh-PB!Q-4FVLW}4GFmpv4Vsb1ijebav(hhHNCU*%l z7)@OHw3q_>5`74}j`PLy#T%!&=KNikvtDtZS>T`UAW11+96r>5FUkw2QG_>d0NSY= z>Gn0JDv61?kQ4btTK!c1jOIG(fIc1>RlqGxR@+Y~~4cJz38hdKV3~nQN zPq;6h+wfH8F=%uNCSD%y{C6f@f^$>mr$v~8ExHArahJY+1kOLkW`bm?Y$aCv;m?^G#&StrSl-#qJ0;Wqg`rA=M|mvi$Ko}tZ@m_j2Tbqq zhQ$sHKCC?38X%aCFLC><@Pd>h2}%89A`A&dk>|Q3O{%6uDqKK}C(`6DM)jVh2RMKo z+DKRzr$Jfs$$o2fQk=^u#gv=g}eLlaVd--jF(o2v;npeVdyP?L2*XG_h%q zX#SjPd{4%75I>XY!m8Y$thGwHK>=l(HJ-%CA@@^mY-j)CA#NS8#tGX>!Ps+tfsa|C zNIQt>CnsvIr;XfcD7kB2Io{iAv=DhmH8dA0q1{e)gOXe&0_+RrTUD&U3Vji-8pY|! z!5uZQVWb)wT;cQQOsa!Q9yMw$ud$CY7q861R!E3ilH)v(g0JeuKnKKn=+F z@YMT8AenIUt&wxZEd zx-2OKC6auQD303S?4O(r`K~Hq=zx zohEQ$APF6vQ^7Kb)&B7<5$=N({0ee96sm2Ap4jKzD6}vTtx~nd$?L^?yaN`$pjvn9 zfsVE)Rgas0xArSn`5C=4g(D9F0&a=RLoL=lKVR-&uUlt}zuY0Mqn3fsA>mWPL~Bof zABg^I9c%Tz5GWcgT5{2gMNv@4S4!K2lQ6%E(_vm+FRWA7Ub?@PsBAK*uXnGohSm<3 zEk$n=r7AzUEBKgAPN*3(vNbJx;{yF+V#U#2`u|LR*%0rU0l&6GvI8@lG?Nw^RJO{q zlvaiTt;V^1ROOLK_g@Tp)D98&}edEIsdxc3Ki1-en@rXtE)0O3E}}hx!a$ zX3v#kptXL6zYG_qPKB0uJeKwfHT&)bKqxb(xL3!4x9gZYdhq^B`e)cTy&iOL%=aP9ePDmlvc#O%3o=AP6?0xn1k;{T=vn*vCv-J2K>0! zh8F$E;bU9FLIwN7#S-*g_G028QxFAW*sl-$td=s$_Y2~kP**K)8Y#E;$3wKr^d)cV zNni2;XhHi!AQbu{{njF?GRPqSJh@k?Y=HE{_E=e!RAERMlD9oIpkuCP5~^Po`O6%> zu5v(BK>~#7lZ@cjUDw$-XKv1xrq|YPeq99 zTo2)K)$D)T1II}}6&H#;Sx!Q9-f{N}P$Q!VoS^8AdO}^xE0T&TYojRu={N9hY}n4^ z%HQdnkKLr1QqS$aWDXabtR6uqdLg{&s}c~yel{(o&B9SmK_>w^-bu}GrMW3gL?lA>EqJIT{wzj{-xq}ap zs0X!Uk_&GZqT4G$1>9kbMnw%eAScJ`hU$4mO4Bs&Psd4}MAnjTb4?Gk-R^FLh!@La z%SZRFYps_lu2P#7L`gpd7G7P3Y_3O)rO2;B!`UA*OvZIxeJC8Nd=i|-%F6cz>f7k< z=um(nd;ap^EKBSZlrsZ+NS5sGbTf?0cpXw}GXBqFmH_4m{(UYY7XnM!u(O^`&<;%04lMwu z(n>fRVH#~r19ieHjcEI8N6Dr4MY(@rDSCM)w+_FseAUq};bU0kBU?EW`{rYZlpt@x zTBCj)n{i6PUQC-82z{#~$WH|-P4MhyV5)xp&=Ubz-5!n?jp^kn)TUpoaKMs4i1Ti( zkHo2uGG@}K_R#pKxlK3ek*ct_y$1WCd z`tvi-B!R=QjZ#zOS8RsX*wTa0d#$vTc@jw}{##PpBW+1H9aG*#-#Ke=g!9DjJn~hYazws0t$Ds zw{yP$7~MD(8$Uza>z}{L8bcAH_-@WG9gFEmdey2D0LguoGMR|iXTf+YK>IrA7~Rr zUY1qTu>9fZ-tpj+E{Ir$`hA!Ximn4Tx@CS%O`DL^7q*ssvTOBjqg7*@VzXhWG|06M z63ueg)$QVd&?;k+5#r=R_p>rTq2g)3I#`gnrg|yt4pv|4A2u62S{Ltnun5T_EG>k# z6>gV~onX04ZEp)l^$a|z?#T9`J+2+*+FY+vBQE>`909P({S8Pz%yu3a4-q5pm^W<#qiDCh;en zY2TRd5FQ8bmxRt>dwqv0X?S5#lrvQXxu?7qWgCaDP|ua&#$k9AE(md&05qIx_MhB{}DJ zr~n>Pjt^X^lHiPGwff!Nu(g#~OleO$4D=*S-&_%%m1~M>DjQ5g4!-=$AH^f-?F0JV z0iJAEdce==aPOpEMeDP$Nd6eA{l(z(h)&p4)^9a*>(xSev+Z^fm= zHA^SN+2*Ov-p6tKW4c!zp}yZvUW%;UobeV^M0RMDL$o${Rn+DE0|CG6&U7A3XHWQg zUf4HmSNqVGkOj-LvXr@r{MTRZpIi;8?vz6JyOyCaRN;*)g=FtNkf2%Y#llEJ#5auX zb3a{X?;M3e?G)6?@%Bk0dC4Ch{S~W#52dKD>nj39Of!(jBziVWgdTgisw5}kWuGvq z=9vTyV7DqyyLENL$qSaj=7(Ex@O!M35Cqx%)C&&?h;Ep5Iur@*_7GIR<Je}w{i0YKt?E8P7$dqGzedrxlU6Q+g1@Ch^P z3Xomld1-jpDHyyY4##sVJXp4;i_Ah3FksOKSFXic)LcbWNYe&Zo6r;1!}t%yMnyP3 zLaPdJ;=_17MTx%Op5gW?FsgJojnC zH;QUH{7{^>mnZt*iTL1E&ODSoVtdNvW;f8XC#j{9?+$~Ts0QO&;MP1WzATwAiSPX> zA2x2|+B04I{W9yk=XxbH98;{?9E@@$3+mD$E6>3r?b}{Ywrmn@W^uTQ+wy!}9UA)1 zQ{O}N5^cpOx&?q*jnOa5!p{dI0H}@G!#2k_23VsixA*6cl7G)5 zfz6~s*b%0Q(z}T*-S(jWLv$q2)E`i1L5$UJO62h2lf^uPH%InX-F z_pz;~Ho_CR5TF49zv*J&dPI?7bTItBymdg`7x^YRelPuIO9&R7y9C86gz{r<=av_3 zcTrW`kNz&5N^AFf%&4;Zo~d(X!13-pM^d1AiWV@x;kKdH5nEmDuR=M%#As4nZ}4kl z20pcD#AX7K&v`3aUjS&O|1=;IN=qVb)VWPS?P&V?&{CGSO5As!Vn|w|cWx3p!%6>*2RSJgkm1>$Ya>NSm|ojwp+b z=;nHyQYhRryD6G#U{5`npWSU~#HPFB*S9s8kxCh>E?PVJY;kDAQXOB#xBIA~kyb z;EBsPa-W)NgI|jWaRSESS zHuV|WPFd3Doo{~xgYn9M)Qa28b>We7lV$#i#?r-{ijexUT|)WdO(VDUlZ(s$4$F?Y zJIyD$3)q?B7`OFad1+ip@rtri1Fr`4je9ISEIZ-H%_ZDGf7yLT#;_`ck>4X;&DWP! zE&qf1dGx+w*!NO>0ps74NXHDde#k4>1ScIxcr`7&vVZJSo}Ab8`|gCq36u5pJ^s%Y zC3#5VyhmwectPJNf3T#6O7f<(R?=}<8)@ZcR1Dk0CYycW71wTqKv0bldgyQenKOU9g&GH*M@gS7lXg z`1l%l=7Qby#;O6Eo^Y-{u(VhtZ$;2$z_BZBsFus}Yq`FV_Lwx<)T*Cl#jPYNBH+OO z_!bI-N2-X;tfDfahWEVXJMu9na+xzsq!FmwVLf^((cNWA0X?&FeK0QbOg`vK!mO*| zB`zWw8J~U}uKw8UmkxV^hO+NGV0~Tx6MDcO{fd%SI(6lN{!^M4>~;x0fpTJ> zYspWB!2y5ej-dtI$jiC70;KE!1)YH<0&o!%D+!hk?t0A*^9cBg502T0id#$+R`+IM z?l*NpNO?jKrSUyDFuooEoBubP- zF{zsD0t8B23+?9Hp%&7%sZr*_h~zpdTj@?4Ja(qZ+2+#htKPCw^s@RlbMzKOMw|lM z2nOC-$+awgC2iz!;u&##vw5Gvlb;eE3hcL4>(FeEIk;wM36~%5( zT*3o8cjUx{Yx6~3l%o+IseU%fg{ZH8;jtJl*^a>uv#=eFN~{LiRD;4iZnqVdvgZ6GTYJJrY`-%&Pbg zt9p%nrp3vlSs6|}6WQ4|$pS=A3J4xRsLLr5(0|qJSHG~87&B>q?!;r+Pt&3X8Jgc) z$_-r`1_{T^)FBp42 zBqPP#6Ud#Y%zBcY&SQ&hGM|!ov;N16HJYMYQ#d2)r3i+0pkKBlS!laX#Vuyo44S&Bxy|60d;?U-)-lH8Jv65lUwbNw;F4%af55OMT^Ue6|NaxC z$%0rqt*Sr5-cb;~1+A%Ys3_>V)B7RbIQ62c34FWfitDGTuNO2r&Pu$o=qIIh~r^^?fRto;gMe;1m43LX$B0gm2_6 z?$as2qQC-CVj@7?h<5p*l&%IUl3a$yRw6q~oMI+{fC1y`UU44`neX`PCA7M2>v#{_ z59yA$qi@L?tS-`e6YmD!9prOmv7QP=hOK35w z-F;67HuJud#C!^b0+rJe6M>OcX^W29cstXMbVo15+HkJNZwBzB!t*^){y?3BT?|b< zOY6K)zxSo1-H&uTt=EsiFaaf74i$3F8jo%4ugx`Pm91LKaFjeI{Q8lcci93!NaQ&< zB;7J=%VkJgx8kg-H%1DoZzSdp3lPVUXGVf=Q?tfz+~=X_|(=g`ur0 z(fx~p#cpe4s7n{75lO)f@stwMN8T%D2Bj5DfP~b?2IF){37I%!;-7ujen1mBv@S+Z zx0C6Po8XArZX?hD^;LS@anh^ z3v)3ipqFGnlO0|(B)F$K&sFr3b;`@KajOWyxSnFz!%t9{uE*HoSN;wv`*O(hY!q^H zFm00C7BKIjbN)_=nLkN=sN|m)fxgs+vjxvKe1T_al&_H! zeR&*%n=#RyR=|tnY;a?+I%Y@o3q&B($C+{IG!M&s5O5%%Q2;NrIx~pljPET9Lg*v| z&8(PRj^dfFBPdg?eGAgG9Rc2M<<`U)ZTX`9zQI$F2ky{gZ9fhhRH#p z6B$Iptc-Y2S2!fv1*DNsxn5OyDRwgZG4gxqL2_qzPQquUy{;&>NU{a^!`iSP@*^D9*IQ z-o%BM)V9Wv{`HneUZ!mQi10>~8?I-yw>Q{W0VfO^oIy#1^8n{k zv{7IgLO~!QvZ7EWlNQ34Zn3?gzJ_@gCDesYfk~-Fmsw}zDr8i_Y3nbxSX#++|1G5q zF&NGA9U4nD$Do%bEKQAU<*7INca2fF<59%BfdihHq!YNI;S)6e{`$pGjScJmC+j}@piceE1Fnc|nnP}o zlnnIsu&+w-ZquRPQZ;h*7pIx4d5_G(8*a}t zRJTU4=A_?d^~Kp!#Me0xr4QoTa&3iq^Q7zO5Qpj8z2x81CGgogb2-ytnO$fw!@Qx* zFxjOgR?WX1|DKrQ4@15D@{(NhF%VJSuSeKs5Niw|xah}bi{7cMq=4{La|$OJ<=R>c z*ME_p9etW{Pw9~t)-rLf^%^~~yAv7%z#s!{ zF?TlP6k9t2;Q(=)q#$M43GSur8f^t zDu4g~4~qxfz%4K{zzwtvoKmz65Dhd7swuT)0JYFIv`rnS5M02r+$t@bK}^N62CWor zrU6_AZ5cHyY^p)5M%xB$-let-eEIyY-+$*EuIs+8b6tmXpV#yCyq^zlJviBg4sdcWGBwcSRSNm{?Vo5LO;B0NPTL56Jy3KHR0Eh z5X&lBNBxv<90Dt#tJruqZgp%p1QmBiIk_&q zFPvYtd`GAUlfyMLhQWnYh#K*=)v5b8l==HAKLp258@~Z^^$Y0dQtY@zXcM=E0as06 zaQl%evhac9(+6RcY*F0+Sa!(`Ok@x%hR@tLmg&~t%F+snDyN>}z43@@5*YTLewL$H zzv{1jTVRnO3UfdFfNQd*lqtN3D>)10m{9G(X-;6MM#SrNn|ozOVJ!T)N387P-VLQm z7kT{B-1rJk{`oWad7Ttw3rDlD*r47*P8ezlr_DnhkoJzil9o}li+z3A)f;G_W8?Da zDb0im1D5L927o1ZDAN&spLd=XsXQlsZ7s{Ozo%r?0oWUdZb_;Uc4O5E}}35n*Eh+xp7Rzb&x`WcY%xC*Sa(tNd(oSz?%1s&lliyjgXIS0OL zYZgKW2RfSnMTYHQV-TbI=Kf*a5NYzA^P9XAn27}rPV4XJ5)V-RB364UNrTLuO-+r0 ze7ah*iK>169i8yciTxZx!NPojw&z?N@p>wqzNtjHAbj)?4%oT)gVC+0*gy%jtp z25?ut%TV}ZIdaz*p-5EdYoR&&ju+Go5*DbiSxJ`#s|A|qe%8n|zFx`Ixl*sA|910HVYj-f{XJR)MB#d!S zwtt|yP`@jHcr~99Z}XXmxw*6qvKn*Kr(dXql{FiqH1jWQ9>u~`QemX?lMPD6?4a>U zyH@UBov30UIwYOtt@X0#qU1w!DaLhgTP_<4g!^^`nd#NhLhxnuZPqNL!wzR>4K`eO z_y`(FOpb$=rp&Dn+(v{^eBav zFcSkFF+q47Z>ZDLIc=0?$@pIm#smop<;!ob|AGcYbPN)>`j*givoqzevC}#8vjc{-65NPr?CV$TFKZ0^T@p+J2At>lw z)to-RCUNQDBE(4o3WCcqF3@i+P_?Y|@upSxM{Y#z5vbCQm%EXvb_98^tU;0=jXQIP znCTNzjIay7!BMT8da$OBy|<)XX}Z`cZb)?ie7j?A(zi^ly~$9vOv3U~R*xwJi;X43 zRQWIbFS%J!)8i^W)CqqO!WgKm`QDLdS|oUqEfG;UwN zHiY3>GWx$2Jsz(nXrhsMug;eVXn4#k#~x-mMyh6b)48;l$K}Ps@-=H-f>UGn3T6EQ zEP|5uuW~I(LZvbvMNjyW=FRV0JPBEpxowe8L2I4jU!)U0t!!40Q;wIXFY4Sd28GXz zCOf})!^38xj#aoI%B^!^KsGN_=jZ%qu7w_bxHU{XTL`I4npt~^xPkm3`%_j$(H_zN zWXad_Zm8!noi8JD!_?a-DO&FAvG`&4Z0if0^W%Lo2ofC-{C?+2nw<@lm67V2NCsB; zy^>-I*puI^3wMu9;EwpnvFPhQJ}WpXc`-6~wr*t&91E#ST|jRe?`A@%INXO`KgEDiHiEL%)J1aW4`;#) zIAmV9Y{^Z}cW0P1mhF~U`XAwVr5erhBtIH9=df!pLF(m$%WZ9k!1UzABz6)Yz}9l*QHMz^pCo$M zDB}}!G%k7+TRAP+mg5yUK1}eQ&Gw#sc%`cfH#M`Ix7A!+yq>J_==ZDzqSpu&*E}^V zWP{l}GAAy7kC4;jgej%Vk??&Z7_y>K?{`wjENdGN#HlaR0t$B>vs}DxF)A?(eUk^i zXF<hFTu ziFOOzwICsoT?aoH`d{GMI=GWQy1t2TO(Z*6CX#6YbL1I4ihK$RT}V^hu;HQ|IUK2Rq(Ok1`Fje?hGnv%lLd3)j`n4V zl13UT(KA@nBut1clv0Gtt!JLqBVEgum;FVX938ImZ_Xe?)N59|9ImU!tq5JxO|(qD zlZI^u6i%+yJrkOt@8w@mDD}`$sD5M{IJ&HN|IUCTb%T5TT}LsPo)~lYb))YI>uZOv zOR3$K>JQ3`zfO|2* z@YE8rRd+@W^^W;myqLhWVi1=hNx2NBiZdRWBM}fh5!9l^_Kja+H4L^Gl7ir42pOKm zy_|g5S?RUuNt8hvv#hXxkZhsd-n9X5&B5?$PNkVq%ZXETr=qei*zGJ7%FDW$@cJ(} z<>Go+c{O6kVE%|7sPxkksB(Qh7d-W|lpS6<*ZEFr( zCF9ILVZ8}ib%|`pLEj5gS#&xN&aNwkIr=PE&gm!{XLF*HzxUdD)pg^SPQH-Wg4Jm7J{BzBusyc?kbvZpu)a!v9P}=npvf ztL(bJEeobty4-*{6iiVLc6}FSn2XUydS7?~kQh%$63Er~nr6xqFs!^8c&gaStC^cQ zNa(>v(#;V{isfVgygnvhx;3vSZ?-Ml)dC?AO-lzmKUjJLU^FN|Zzg{Iw^wQ!qcWiY zHaU9^hF7eygWk-n1?sFYHI?H>Z)J7wry9O52dQ{*4DyFF9@Rc_DujFJZi82i-i4p^ z$F%73Ytek@@Q4>^m}H{XHNUsb4?P3cVD+G)z1uOl(ynuNg}2SeV8HK1dYzO`!vN^H z{N5w%e~9s|9@Zf2MCj2u$qzZ|?28@=QS7=%n1Ml56K1gs9&oMjq z0u(@Bq!u5BcuC$~i=&U;!lmqEoOO%nLNRG3g*^x*ad)bo(G=eSt;pf3w0({a*$B?4{Qg^xawun|B?DLo% z;zsfP;9a-0GF$z;px(V%nJ-8w*}R{%D7~$CU+`w!Fk0!jpm)=8d``IXtlL`b*yna= zbOFV_6UW#~XAL`i}RgKo7&8 zt?UmQpPAh?M-dfVb~+q`VzGKQG_I^dxqz6uZ$7p!)u3XDKM!wY;IUB30s-A&n< zV+bsFIa(cXuD_2sy~JFIOt122Y1bwWUz@+-m8pU}`J+ zE|SA*DhMR?*C7(@^W@JL^w+KkERsbVA8xFoO|VK3JTDE+Gk(P0@#Sezg;8!Sm57<^ zEaWGwe|$iqrr^@HU5atw!&-6#knL-2|N3UM)Cjo!$Snl5wmhB0)%(TaFMb*8a^7}$ zO3lHuPP|$wP3NlIqH_1jsIekH*#akTjU2K!jRJ+XcJo_klJdpeAc3^?c_D0^gq)uN zOwBa_^Kb%Ei#mIKpwKb!N zu1D!6Xkt*QyWRF9)Nr!{>V4l>Pq~& z-aDzeIpkj=U6%>5cDF{t=TuKhXfg!1u5m4*xmap^i-qzE&|SDI4P=3?bFtRv-+`?| z`Sa$FY`c({v>2W@9+iqx6P@a%ctYBb-&{i?+J}tt&A*W;<+Uo4VnpN$*v{7$jbjZrW$0sq4 z!!7IjH{jKv9P8>VORFQ1>+Zcrl36UO{STkqbh-gdZ00DZ_j_=y=A4d zA{(+xZ%^A0th^{)hR+9edgvgef0>6!fY%W%TwWlUSqt(cwA6kGqf)HMI?vx&s@w;psI z0cYn>g{6z%p@fZ?1f|+$^jlR>6t58H+cp+BBpI?%)KcJZP*n2p(W-Li7;oP}6|eXD z^(G~Id>_ohCQTZf-~lX-UwYgVI0iGn)HL72=h8*gz&m&NE%J`NDY=>->DI8?eScy6 zavzno*SldjI5G42bJz#rjg_QmYjo5d{TuNSqLq;q5L3p6_4zz3BSVwn0j9jDPZ8=yzKIV(#q#a;I2T+R+UFUZ8j&HeeC#QMq1vgdZC z=l&!0j?2z|u1V;28T-OOAWG-5KY`3S#1FzU$ajVPz9zN*4>?uNUkN#eu?6MV`qFu8-?}^NTo01 zs8YvVuBT)dD2G|O*mS5d30VEdr59kcwtY|#Na$+f$c)zyEUd`(AAI-YV6%A0oHJC< ze-@n+f*-<*j)(JMILF3VoQuqiFWo6;P7+(a&nXqWKuge z!ush1lw*)g>Cdf%n?2D8QX;#;N%!TWOOI1hPDPo_%c3CU4X>@6#0FUFpqrt%KbvjZ z!y|TmQxz`7RgC&$TnI<|4=ZY1AES^63F>)(&IyLix}F2HYz4LI=LoM5$wc?GXzfyK zbGK(G@Dso>`Z1-Hp>C4~nG6}9NbG?!MBlyD$$(Kvn&U#S&B9B_SkF=9v+C~yct1s# z5_Y_1Fu<(#F)>Xc_;&fnG~tMP4?AK7j8;Zi?T z5fW#`V&${EU$qftas9*(o(6Z-#133R?Ez%z(}@$=di_F);vCcoIWW)%q62pTOVWYS zC_>R$BKfoTRZ1_>nWJ=g1TjVP0%OlQQA@3%wqGta`LU`?A)%q+{OY4|FCx~C$>!UK zsRIvqz}4;!*H9>$wX&eb5wY-e&kr-!d<|SLD>h&h(YT)FF(+KS+%YgKyxztrs96t+0hn`Nn*BQi8;g?0 z$jiL0DOa;ecp|F4gwFOudn}rMF!`?R?X?qu&p=w2bj3fR*CrH1hALX>%MwbghEvl! zze+dN?>)o3Ff_{WJ3v;rdM76 z*)IvV6vfLgjy?usjnhY!Nf7-^IDtW^)PHPmU7&Ct&W%bpiOvoO_v?*Q2!`m_{Hwss zlrzwd?S>KxHQsgxHa6}Z*o~_G-rsEu*|d10>`<8ltehc)z@Ef_)_i>}B+2aOc(o|Z zBI>FAr6Lf=mfTFmGz|KaXSxlL@^KVQmwM~&pi^Q9Z8yY`dcq+pQ*~3{`u!Kt$W(w! z>LhgUnp0M(ig~o<0+Af&3}ILm?j9~FZlf~BFH7W;GpiW5T@0l zx)2bT)-{Is`xq%(dlNQTNB(%CPtKe%Cq?RG%=ZZ8`s?f?(7e)2Gun+>&bZ`UpQ#sp z-0!l}V({2#bP}=f!Ueg<4dgP(t^H>8Ec$Anrb#oHK3$#Ho~W1M7KguIk|N}p?+3?) zvZ4{T*@C`U1Am;BP*7>r>AX%(-7nWMC97HwiO)MV1aP?qVMYLv!$yLTcXyS~5Vy}zWNoEO;|Kk%ajiY}nHc{p+t0aqlo^4kGW zuAh*4K3?YxMvjeLj-iz^Xl51gBuAropNGuFJRsvB>wkd`+RZ`O7PqtFvC+EQ$;E#Y zR&<_49q+1xc24-R94N7wx;Nu}F`VHF6A<41LV?44LG)#~OV=i07Co@qIsimK3bxm$ z{;$xQO(aN4$-KH6Ts1Q#&7NR3Ala&2k!OoHHqIMd;$YmQ$#Ig#LZwVE%N0lo<^-;a~-!v2oh`sv1SY)J`|1 zoC}*@t~Ky@cck6Ve?Z*^d!c2V{VGwz%rF9^O&4$0&+7rfHbbsNUj6n+@_O6jEA7Ac$NU~q>e&t#@DR#am>K~0UNcuf2%^EP-p2pg3w zPeoh7`-;zoi|jT~InAGF*b~*&YXqmU%0!Q%+IziS1?^j#*}ZK)8ZEZ2G4P`n_^PWH z<~_;1kD0-T`-7FV7_!|H_b)aP*uCxuRzkZS zHpw&U_V~|n(aj9$VJD+9eRAo;-&kBT8gXYdTdSk$3h~pN{9GaVQf<3qlGAj`(1Bx6dI^Qp&BO zvn~Ng-*df(ijV3}=mWlyK@YEoHqX+%bIO%GO`=h`?RXd3Lia)u`<(NJ2W3d#n7RpX zuGatkiX3`yCOuS|SmTcviwOdvHdqlEqr(i&|>R4ol zyqLhE|7J${ z7{n6`gd4If*_XU_37X4p&No6axaK#i!vZaq>wSeV9dnuPD2QESV`MfQ$j|P8Set+_ z!A`Y>!G+qLNGsN6Kt?^r+9VJK9HmP>r$0>|gh%c0u)@+4Nz!sL09lG1VSM$*0}hpt zxC*4OasWTc`nK8l}w z!_fhKTAtY}ewp~(t2af8r}B~x`sDgef^|r*^ndoM?ttKYzYM`+E!aK1k?$TVJu|m@m>JOzxm*LAw?351 z2q|B*H@M4_@u_C#nDhC9aiAYIZ)teJPKr!|ghoi&Ih3MQ|Cc|c&AJ>*3hOslzj7fS zi0!D8{Pb1%kUmkcrJ^GEKfAYcqGv>T&>tWP%4ZI4{WFW*YH%YLp{AgjN)*s0Y=%6k zFPu=>Zm_(zS-WF09;+E#V7Rk;~MPQp}L;BiUI;+U~RHaDvtEG!8To~ zNLO8j{CpCo#))HB#hHvnf(H)WABGAv9?>C1@$|90fQU6@TcOu|Q`2ug1>39XO{?m+ zFh7GuN2^Ey)RSmU6d;Lo0Q}=*dOR}CDr-bzLSi5%i`6%46feYH{?lJ!2%lvydJInb z;p>`V_ajOCT_#T8l1smRr%HnxHcY^CvIsX-iej;Y9gGt%M2XZ|#Hj>PwSuKggFf5_ zPjdG;=^tUu0m8TDpd%{V!G^#+J43`)$impKJY_k6u5WXSJ-R+?BSb^rzLyWAk2K9pj3}G%T=SvH9{d)kkum#ZSWyN3$-AO_Az$}DoaEaiWZ>0!3@B^*T`H0iShHtwIT?FJRH zwR@#VRh3emGos&yA`#9d??!^ZmacGea;A2!nuZ5I8WTTCgrG}VnjB##>wmy0`KeLx z25J`PocpR;ldocN@KiSqo(o`oWWNWDcg&PdZF}&e1V4 zlOV7CR^)V$k6JJJ5MX4wcwfjt=0Wzz0-40b&8aqR;oC^F40EvoLw&OAT^Ps#KJ~ZV z-xk9e>`)d2jYEkInlpU1K4l7F34yfW*UpV-z7Xnc=OzQ8-95ww9xAmFTW)(MbjUK4CsKcL~ zIoYq$*BX86rmMS>3aIj@`L-A2)U)9i@nsJdn9oS3gmg_Qqh{OiosWpPL`CRrLQnr7 zhjQ+5Ssr8t_h;AjTkJBkDL{ERU#lZ4z1MH1D1co4XSSoB>8c*}*V5~`9^)Rcn*>%e zI#t^I$Dqs}xX}}@Ut*mI+)*(t+)m|jhxlJ7{VmMH-X8vE#O=u;;eha~>8N8?e_D^6 zsOrilo+-)?CF-ww4d3BA_ zft|Ov?`=6iC2T)3cTuy}^2!s1PXuzhYmNl7(kY$GoLIh0Wr`+V zbz={TdV7IJl)045v1^(W^Aa2ws35e%u{%}n<;0zZbjL|Uow|q*r~znSPRGd6>ENDL{E5eJW+f;T6q52gGb~;KFFYAXx62 zVj}9}yX1(Y#~z4xRSZ_@^7leVx(H3Oi6zcVXB^kjA#kaNd?%woo+B)9j0inc4WiZ@ zk@=J{>m0&pZ7!2U{tppOJO0cn=Y7t>w{8-e_3ae!!^)n4HV^Uy$RG#S?!JiyBm9dI z3(LJK(}@bp)D;c`tN)7?=ee$2kKKA4E-+(iU?&&&;q{#Q2>4>UHEThezoL~Sjf&aW zpWP5Q@2gt!I)|DVGZ`f5zi;{0Gy%L^|7OXX@MQ+41sNG6Fq+J&IQ}x zjY6P!+t&a19zbMG^BlPN$`a*hL}hDp@r7kBNvw_r?uCNU2y9IPV~aONb(yncn_j{w z0}VdY!)=3~E|kB$cUd3l=Ooj&{gPScFU-Neha#wU%rBx4s_x}zP=*wlIe1fzIzT8` zlM$lT4pk}QL~#6{v$0Sa0ff0llBN^Aai1Wda`2W*%4pgYD>Dw|KQUH5KHk*W+zXm_ z9Y{&SG5)d{$dE`!vQk>(sunCQ7Ks@`ZLtfqJn`a6|C5kJB2{$YTy3#~sz1 z!6yYT>W$S#Kje{X8Sk-d1{HOgA$dm<3Xqv>XgFaGd}@oB$~`W1)f6!0WlhQZQVx`H zBMXtbx2xS9IjBNngJ|!t1FnInu6S(d?x5j%#(e(JM<~)%k|3uPFft|I|JmQ)*jb0z zZ1hhH!pM;FXEg6;eoK>t8&K($$|XUQ45aNUoC@VO0wWZJ5>bhmaQF7@$My??mODcl zeRJvrPU9O7I+r0Ln=BTJ)iL&G*e58RJu-2S86uDdX!3SFI1O(^PfAf@+Q)``2le zqh3Yz6kRXh`89_VS-P}f2nKZenUjGeq>NBDYm0!~*%?Xk@(JF91u!JvL@}v0K5cEY z9|p)+MW)RZ(3ms5JCLPG>gd(y#Gj@Z==pZb5(04Z-Gg2iDFIU_7_sB7HD`N)GbAJo zev9*AtSE;dGUTe+%|1IfUv_e4E=~t&PnZE*7&aCn`x7LiLBWXSIdvmD0~sADZjIU< zS5jo*pL07g!n@eE|FiA>SK^s`)HO)w?YQ-s>Fm{W6-djzG6F(^y&TY7UVpaju_|R7 z<>q0>J;hXo1S-dmR5zBxfbuGu=k*^3jfcS082wL~^Tt>i<3D)r4W7s>m@L{7QITn^ z!+)x{L~mA}vfshp?ln$hJHrRzql=*Faed$H;=u#pUKL6Fed7#&wfbe{NnlqxMm>P^ zJ$l;d#)VYOF3UC`Wgm*w{`~UOB&diP{JT#oCh|G8F!pcd7%^rzsDuU#!yJ!Mh~_2A zRu2wTHeQ>VTS7SIJP0I4ELHC!GxO(FyItKU=d_|o1#oR2P$9WS|1~f*e`;{#8B3T_*!Oak7 z%luZ4MKf2(X6S-L85fGymVIm8+KOA|2&CT4_K5YEmhH5d;$Lq|r$W8g7C9?@B_b6r zMu)H=*6bBtE%2x9KEmx7JS=hR9~A?V5nUG1R4^eLe=BNKgx7aI!G`b%mzp^np@H2%5LJU2)!w2eW$ zysgQ6Ndfzlu72-7XXsRK887C1`2^r}H+h?U%HS6@Ut|!fi`j0aT3A$MZ0o>T&Yu3Y zl7OOlMp(G)_b|Wq8J6>$_s{i*d1ZrF9?x84$Xty}?3p>2$Y1+A7z(n?HGWCy_de(m zZy~jKaoQS#)uHoLY!Ith?0A(E6dx4s68%?c1r5*r&agpDoCIxsKhTT3>kyWcRC#Lb zf=7yc(FnJA#vpVy$XKWjr86Va;g+tk`X%)724~F7&zjWoyObihEY=m2Y1Fa}JR)ig zK*cN=y)Y#)YG2$+TT4tt-CdKKl`^xF=yG&>#*{0?P`JVuBx1Ppa~;t(&1*)UQQ$z5 zNn9{z7nOGcOSk#RuhCAOqBSD=#XddwXMw80o0Gw+3x%Bzk_Tm*gN@xpwV>y6lhzcn zg^E)Qs=z*8kt8g#7(NQ$S8}R9r%{|We6vYKA?;Qey4lg66UV&E8;$83))?RzmDsp& z^R@}Nuy0~~Ka6dKSrUzJKW1M>${k5%VIZ`yZALYH<^V$Te8wc2WegL!PNqT%khqJfYO>-5|*lse;pq z0mZPwNNWe~(y8=k&k~z?@ID6!&nQ3E#ZKd-&<)LS?Z+=DdwA5)$3h$YN1!X)0Reii z@b(TMi2|JhJZPDY=nNq>li^PYoH9P$mSEL360fdB5QnerdKVa(zv}kG2dV`0DaTO1 z2`R)WMh#L+I6{K0)nc^Z1egkcI8gDrh zz2N;tKN*H2L9cR*N}x=1e0KAe=dUh;cyTAUf39B9dXJs=?PKwbc^Hd{D#}S1Ww?!^ z;7m6xH_$BDIum%SfRo9918;0X|6p8|4lb4%T~JPX-{^~OXWRhFb9SH?vXRs52!t@2zFllP`c)B4gAgQyE<)JAeIv?pKa&q!-mhElZgT$SD7vII zU6%@Z+^*z*)$zOUmu=I9@fYE{oqN~J@p%KdTiux31AJ|%XRp`41A@A*?x6|ru~^nL zO8G&p2%JHopvZ+X1*v*^;o~eFO^FG-6)aH_Xqta=(Mw&__MF>yF>$$D?C^7f@kPF? zH}1oR7Q=^_7UxT>lR-1qj6??ALkKt}=y(A7eq&ul^M2HwGxHORP0wZ1DE;9 zNDm1)5lR|c3C-gsXzc&kiCuojrrdgZYk4nCeu*)J7o$KuZf|z1MF4>x87>E`9q(YS z`3@BCfgoKSD#!soV}LpA{o+2j%4Ja@?uOFor^aD&07ekBdPlrTi3et#|ANxU<2p>?NiPd3%1vvF>Ne|NtShCdr>td`jC zi)>*0{!^^AOz21pOg_)Y^xgL8!_xw1>pJP3Dc);1V$4+FwBBKlaKn27jvW`Z%_w+3 zL1B1r#doQRI<$MJ?)tx^SUQ`SJTt_kMAe-XCL^I!cee~B<70j)h zN!YM$-Wt?bRxFiG^$s6e#-bD~RE7jzdl^V@ulQKJ1oyZ&@lqu}qC4lkzz0)KyOZq~ zsA&@a!9bh#*t2wzR!5vD7knasQIQbmezF8NQN*%;Xe%*IXSMD5U}bMxxJkz#FBM(t zVTAHl=wNa|MK_j)SGw%2T)U1Wc0sQDg-_#&f5EnIQN2uR$yfi`{(BJzfXD}pF8=cv zeE(Sdsi{TPo9N*R_#Lh>WxAqwT-XmR`Np~Q$~3eSRWuXn5mu76Z|P`7@qv9rV1 z66304UpFZr?%x)eVAYJ^?|1+E0_zu%>!)E11Mca6+$LQ8Ma>+GMoas7)r0Fio|^YP zqG||m_Nnj*)W(FAK<9!$ndV7cZV(ybR-VB8^HJLe>ge8#b>F-~jzNh=RB&z_l!iN) zvU34K#b^%MJ-w5UU_CZAOeW3P*-xt--u*p2v+?>Y)=BmYL|-x~UY$`I0WVs|qt<1F z1s`6b(GWw;FU?^VEM>|wKKC7V3?ky8j%GknqA}M!Anw(MPQUl8uT?pDh;mam2FY7u zt@$n{Io?GZ>n}-JLB+p&&#RE(5;3Hefxker!%d3K@&=ZjnV2%8pLqB_LOo% z303djrn9^*Dr0xUF;LWuC#F-;@)k8@_l1tVj7PEiCGMq9ZBX%pl{i*vIm7u}XNgO@ zS&Y+pom#iO>`gK0UcG1Tr0IjsING_P8r5#ghPq`kpS<Fn{n}lj8AXR;D{9cmGu`tE_!SJ_e;Houv^S;k{W^%E1H#!dMhCE zQ9kRv50lHOaqU5`JtTwD_r$M1>0)m5;VL?>SOY)&etH9tE}*pLCv9_ZlJ|F}uch28 zUWl{J=5XFpPRz?9p`N^ote?!vdJs_X%*F=z$mCNfTvg7yAEh@kg_;vKnjF#ZxMJKj z(qiUQb0GacmFjU*gtwxyitf9&lg3)nKIZ?F*4Eba z5=AnK`h!*p1T^PN0-JZycXUie!q{kR*=7MfU3BhH%G37H^vFzX<`N_}xjCs7n!C!p zZWQ9yoa2vSBFbnji@_|(266RpUiSj*7(kT4V%82~&Ck(qI+UmIBqua~|M2-fl)~6P z@XvpJ@3UVyP!0 z?ZM;0NhIjx+nFD?UJ2%Gs}6=^voX<@W~;!KRRyI46T+mzf?>H!g5KIJTEA$ zsM$Cb0mFe<8DHwicuxKYEU$~3u5y7P>6ZVb-%O*8eGU~Ges*ic5llAO;a^78pFLif ze2yrGH7+~z`^Dbs9F6y{(ZM;0=4qR4ys$hazZ;ceXx7=%3n+pfgIO#?um*eQ;$ zjr%Y;xFO|DSP2zQntG-`amA=35ytdE3mNFdemI*ka zm6)6|nOLy+x8eR_na(AA00Qvng<8fRK>EbKTZ{)OR7`Hg!OOQ8oL8e@xaJgclFx&_ zE0hX4JI;LjQ^q_ih+0fdpl*cJ%VcHvR*1Q-W)t8=56MN$9;O7kGm8)XSO0rmh?Co4jhX(|=A}Tqz1|HvJ$sHC4BjEm8QD-Ir|N2Zp~XRWF2$7a5@}3 zLH0=*--@8#6Mv)qxp5lwU|kNQW)*BGeXxi5$jhK3f7y>MqG%q8vqPjCOCO3^{BIaX z_e0U8d$VJBH>CeP(e$6L#h%=ZYuNyPBOW~FFc11(hbnL^RUE}!I4Mow#VF|+W2KFl z-yxzjvRYDBtDDm;flmAm!)+?CFFGZP+Cgc&r-Az z6+g9Q4U6|CzLwz&oBo)Cy1)IKG$C~XZ%cEs9ImX*{+mpvHYv+P6!!|(5w%kj;uE!w zQ0FTYmdlm5Xa{_LL(dr#cM}x_Z>LIs0QbuQg=tq|-} zO`Wch?80Yf)10(x#QiP3`}H^&8Iu z;iAR3OseXGD8s!)ycXh|;I{9J+r_d+=$LO4s8qQ?+ag>C_ma|9qaC z&Z4kwEaJ`J{PB0`!;trJ>-R{nX5$j{nVSup(;<#wJyk}_;th==O99QlEZ(i%6!rOd ztu&~6R3yRd6TGApbc+m!y86 z2Tm^#on%JW&^eN2==4Qy;O_vtd~s1Fs**p=Au^*2eas(#jq*?qmwI9`WK7Ch+{b~4 z1VJYqF1~j)?fvK{U%XF7xLd1}b^qGPS;=`#BR=-Tz{%MtWCLW6W-$KHNY*|ybP>7$ ziyNK}flZcc1OLvVeXS1h>#2J&bO8QVZ7e3^=#SuKR5d1O(XieOKc}&c`b9ExU^{AN zUtrR%nV%`}lVYEybZQBQ`no!DDhZN{G~3!W*G@*9tcBqMMq;WwXAF?DGx6#WcM9)u z?xqb@nqW&Om+FVQ))Zb`+d$4-)(+y+hxkOJ0AkNZujXX+Qso2JXu`oLj3;d2UJ z0g*2MSK+rwZ(kY#wAYyZ|7)-q;Qtl=|EeVaUtxQdjD6idC?@Sij>_~H`z2E!IB;+H z;p6}N`I-6Ut9Ntuqv;QGUnP*`*lky~@mmR$_$xmv<)r(LUuA*Q8oy`jmm4Cos@pD- zg*T34L1Jer$9?yXHHGAARL#E?#d2zAurq7HAiTM5QOcm= zy%)(+P~npHZDvFdE_E^u(&d4k7&cHmC!l*_qIEmaWjRXGbeckWG5Ql$X8b*j|$q|k+jJwkgvxEQlfe8+3mx;iKRCL;<& z`g)89y$Cg7e2d7g7M+$j{wH7n2OZ7p=8J$X&8^l6H}7Tf`OPJ8n-l2#IX-iI=pS-M zl8n(MY}YY}tQ`=it;THWGS^PO8Pl1H3jNO3KT#e^Y7+QkXP{#mOndUX_TlVGQxzQZ zQg+Xydih_`dHT%OCqCh>}{pNQJEB79&c z5lGBifD%2cZY!{o<_p`}q?l~vLMIAzGgZCY_W!tg&!{HS_y2!VNJ2|OOQ=a`p$G{A zCWw$iNkAb%N`jz-&;klcqy%gvbPH0{1uLN_C@P>LSg8uGx?o$t1_51kEr7c&U?abL zzW-*lmI4U)%i9_-g*Vbdpp4^$TW7MjPv`61~pw;PUL8Ai@DI^+EMU*Ae*G{*z zkT6q-!J!VCeM?v`v5MXHOjP+q_&Z=JXdBvJSclx)yjN1_n2&hJ#z;Xc8|7i`!;N2% zxscezyg1u}=~H*k+VqXy-050F4ZG7xabz4LQNoCmU z4ucVaiLB-OrY`H1HETcv+t{}#;El7tzN`d+1^ZTEqFs`H2C?LKAZqEt^Qt;9Vm47? zPABx3wR)Df1E^C2f92mI$xQ+_7tl&A;$R+=g`3aSHc!SYRZQ)+y+loLT~2I6hQ}&i2f53-DSM+1VlM&b5tMoC8!Jh zq2|h05&xhARV#O>Z}M;0IsL;Wa*k|^D^063TogZh=wq4?y+|_fjNaozOUyr0!G~zO zNY>K}ISk!JA1;xTm;j^Om6;wMA|a;>K#YnCd2ziBud(eD&!%Z*Z(|L(G~jT+E4fRx zeF|U(_XKkaHGU9zP)>ZPM12h>1(2NCin(P~8H{@dfRV3!#vr%3_L5q7po=pFDgS=L zn~5!bMi!akTxb|T%cJliK@-~V=AUgKfhA{BKT@Ip-D?quX)$G>TlEwz%B`}*M!NkMtWZZ{KS4gct9HUDALhXS<1ck& zw~OOk+YCv>d}zJ0E+mc0BPhBUKr2+ZEM}zE`;g=2pcnQKF!>@h16%ZTh=POak+D#P z;LIEAD_DldjIvn~x=yVH@U0>@ey9r_OGGC7Mg%R;5|DzLGYzn{>D^d|_3yj9E9_uI z&*e!6VxB$H7H6}}2IVdFSz)YKWcWk|XrrFx^a7R3ZY;WoBfiBqyoPUcMowYYFMcpAZTYULcE!_RYR22@Zd`Z(fuD9<%QP^!X*N%9@$5;v%4 z)*PV_g`dXyF1+P3-9@g=rqgVa2hFY<(Uq`ane!s(Ou<%90{7 zi+^SUtfi6msxm$l;%f?@oJVMERUk&S%Y25;XdJU&5+NJ7UkYkFr#&!jqr-|jVz2FM zR|^c-C#6$Q`m@{-zs_|LI zKezlZ1Ivbj8228`^g4#7N6e;HaZ&txMT$W|spn5BgpBWB0MISOmd7rAcuptr7~`IB z?NG1skvQIB{<}I-X+|zU$g;)jhGscy+AGQNcQzf0t)OxewbtbhksUf0;RWU=1~@Bw z%bM8vZ(s>^1wV>6YLzCUh?Ojn0qM_cy|qZUOkG_Hc|OJmUQNbKH+L!g{1?6E{^$#X{SWWrKJMdk5`T(kg=)VL( zI9i#hIeJ`(JEoQk`QG@hIh8_GA#brGgTz~S_0uN`&ByJy~2 zL6^=Ar67w)?}Wh{MJf4Isd@~JtSH-Rx7=FON1Z0R4?Q{kB4=OBx9)<0SwN#57d3Tb z8(^lI{q+Kb7?!MD3GJn{|?F!Uy*y>+b-4WI&E z{N~>kPedp`0vuLk$Ukmqp|vWDO{eFK?=)XBqJ6`$nTzJmOX0tl+d(S`jME=?NHU>(JXmdNJ`!imC03*)D+sD0&L705J(@R8O(k z`kCJ!O1TX6e6uPy~k-ZM7roT(Fm;ip?Qt z=mg{&7hf=qyzeb31iSrwvtm6~fPs7)t#BbcYad2$4eD3UC%wJFV2pJ&|z>{aCx8-nz=6Nf6bPP5NiAQgf;b(cG^X#RtG#NUCb zIK}M0M4l*CU;^(T|HBoQv{w)lXW zB08XxoIC_TdO4IagK~27klETcxvk!v!8lAx{ny}+wn+9H4c!-)KX({p`lVSO6i2)E z(OZF2p+e$NcN44;JfYr5g?V0V-y7PTuP&>H!kANZpPBN^AyIbd_JA5qhOes&tI(zL z(~ccAVv?c_9+rx(+tdz_tWy~n6w}0vH(s;vOXJzAE3!ji)}a(4TIX}uea_P^G4kHN z&hSM~>{KE~r5%l10?&fhYsBFkWdGG*D>5C928fV;E4-6xTqF`3hi<+%rgBC-4KrAP zF}nU07EE?DD`+VfszJu`GTrQFy$ZFaMfPmHM}x!tl}#o<9+X{8TYCi@hUoGf%|BwQ zQ^|+V;L7R^98sl?DS{u;m9hcSr?}%IC}z#FwTehL=@zvp>Syo;&J}dVm5cr}Ju98M zUc1?>|8PbSneegi`-(*>z7L0FqYElmxG0tO6T*_3{C<<-axYh<3bv+IV2Jg_295!v zWgeGG+C{3V(+Xs4y+|1^U!(gcJhz?#axDyRp2)E1-H!~rky%@X)>veKpz%R{pV&~+ zYzXkhpLDMeTFvcP{Y4f2rUpuEc~X3D-zq&?X`v)wZ^0;w0DaL(JZPnaewhj@C1|kz zhSxCDyb*^}HD6M?G>#|LM-F&g>so-{Kbu+8*sDQ*>nKVO=;A{MxEKH)!^pFxHY$uC z*Pu+%Xi{(A4@dxH_yA}Kzb-H0n6fVaFb0Oixhj4)%3aQ1=ydI_;RSqJQ(V<=L`brnp zG#7x6Ji;gnT@E#WBH%FQgH>l`*2^h+n?*&nM@Tw_phvj1*kfkzAC1i|%HjNVfxylk%Iw!rtPF1FQSVv zU77^?f7v$LSd)VPFJacC(3%wde;Kh4Lz6uJZ!RoSlY;+mPLn`ul4sA*66u^gUjEcG z^r`%RTc~CvkN@AMIpV3=$^UqXcXcHK zWqtJ!$=H*z1as1wv5o_-#SXMe4Q}Y{a8%`d;-WC$)QH|i6aHjA=)HFgqOY9Nf>gzR z3hf^`Iwd%8G$tA;3RIAW)sR{;1HS(EvJNWN|1r39W3K-bC=|!1mVLLg-1cK6sp9V8 z?jRSVTVlG&qq2*aRzxRN9b?1JWF|=D2kj^PW7!q~Dg&cV+Dj@FP}_Q3_*DLq$ju+#3UhPt}#%+Un`yxtMrg(RK36eNi5x-}w zh)5??f*th}AXD=%J6w9A4m&>4zjA>^Kr4>Rk19ERMmLY()q_szeW^+2U6P6T8PlcqVJcQyHV6K9 z5LJD(rOfs0SX_&2<|eGxAl9!2@3H>7#zMehvIJ@TV47U?m9TpwsTEmhJ{_u|$oM-} z&l7swg)ky@tz1`CZZ|GR^xo<7D3P6RF8Xr6a0$kC`A~CXdXTv^g9`lR4Rw@ZjF1Vi z9oH|uEbM7cLC#RgV|^E+B&4o%)60? zyFGFU&RTJ(|4BPCtT)5B4u!$Bx(7Uk4L2KB{^lsLV9T|Z)%5och>Qo;V$oO|52g0rJtD;Cyr23c<(_}}ZJG!MUd zgUkghrenK*`ae)2as3T;3riT~ro-gl?^qDX(;6errXTjV;I9q{RJEgw#y_wMJyAQ3Z5}?z@vAn$SWfA=*Jb zg(&JDs2E3GLOZ%mjgz3xLEEoZ9}%(Lyu7@Y-VMKdXaFl4treNzy8NJU`MIPrD?u3( zG-lpcD4(CatQEaCcMaf>r0yO+woU4zE?R{j2BE#4T`K}t(~AsDYxSc@tK?F2coCF;sT3?(p7PV{&{#w;pcezx6kg9=>BN?j104C+4bpj2G=D zYp9HKJ6;r`C5tRJQ(y00&`6Cq!XGrw5I|y|N4~RFu6Z7labIaJ0ILGh;4}0igt{f@ zw*4?5lC?KF6kjtusj!Ru5#8b%cRzb-g&cm9i(}8Ksp{4(}T^>WpBQ!X$jl~}?v!g1q zPr22*#W_@a6BH|_UH)d~3F@?j4kz&(GiSg;sLCIP-m!Vqy#R|bc!c1gY|BfK#p4^1 zw%acy)i#s@w|ku8`bY}hp^~`zk*iv(fN^sD>#$>GoJu)(^T9iicU-LltNem!V|_H! z(i?kEFDhO3lpeuTlrH<12^H_!lzm{>^~nMR5I{Ly9vxQ(p0ARYao`-iD(f%$?QGa( zto~4Wozku)>dqo_Q3xw*g9cr+xx7MekB+YTt&-^N*toxKXy{$zMk*kIwWhYILvQIq zNU)%vR7!ckQi*c-IJLRCbnJzpOyenS+R01)LC-NUEI_@3>LAw?jZN8oquN~Fa;Nn4 zGCtlU@keO@mM*@16iPR;lNTaEzC|{bos2ecj)Fzyf%g5H{0Ugm~ zR%1lR2161ouH(Xb6H;}MVc#EM!ZHi;F&+&!x+m)3edApnxg(ohF|M~NLFKPNh6{9p zU~Zk_&CmND*Yv;zXPfIZ7}k{p%v-b}g{N($Z7IvE+~fECw-vw<5;0f*=aa4xJT2E& zBT+*S^)%Y7y>Kznp@%O0lHWf(c|>PYho*%9;fd|0-$P#^FDsFwZ~Fa9arh6zT)Q^m z986#pzXvgw*xYc{9@=f>EF*RpDmdCH=HsNpleXrAPSjQJv~8PDQx`(ttg2NSsyivS z4%Ja>hn}fpw#_Koo1sX^29!+Rd+R>l$NHOEslZuzIHk-K{J9UHa z@wKVzX`=w1xr8kPK7shyR&GaNAxP96RO=G+%$+}EgkCPK)GAAKUd(d#e<~;6_U^2N z&_?8n-h5sDK(M$EZ*sBk5Fj3UGpO8yCXMn7ZSZg20M&{5hC&C*^JoK^M2Jm6# zfN}ikCdI%6eT5N)%jyP6p-tK2xke^exDsDe`SxDc?+^&20pzDuElwzVGp;-6*SRzl zpA}uWGLG;2@w(Ra1u@w9yk_VAF07B&56v${}u4ypd)X+0*?LQ`s3OC`Ux z7MgcKmr9wb{4&8Z{Hp%xon0WrJnPPhQy+q1^Zi=m-^FXH75zm$ZOuS2RzU)lQWomq zc2y0DZ3uY)u@y8MS@RVpI!ix7kNBZpGofb_HcIilwoO6QrM>BSH50&Pku6%;cr_$x z17i0$$Gz{8Y8aVTscmF+v{|orT38P_lr+_f^fI#QSV^>tdp6mXBtU z9D0X)yIXSjxBz-rKrWiU1kL1D;QP6OJ1w&<;;>&cwhoTt!L)U7`dld?WTZRSmqyJ_ z3m1}myK}fTm2^jWB`du{&w*@nFKCzx-&H=nn!7@0pnrCh*p2*-0tM2t=+i%S283k} zJ?iDbwzHilEh?5qJVSrZ{5O7i_BbnM(E359;Q+jjfI|A>eCcKsHhppJ!hL{Zt?O{& z6a>#X=U@#V{gDDC1_$mwbpwM}{*QmoC(`NGuPgyVc z$QY^OdgBi))x@o@$!z61sx7ZZE2&aa0NE$yE0J{z1rh>kie7)$ud%LD_qk#*8zluS%i%Y8QR9#(nlIc<} z@lKB3&AGOB`;j70@0pR3>q-fkuR3{i3A#eLnDQ=nQ&E`B6!fcW1B=jmj*V8+0g7w| zr2vWTW-YBU$+jlmhg%FOxJHL$g_>W7jaD^%A&}bfvb@um9@ORO2!a@@|LCtKe-7ze zhE0%vASHk*%^x$hP+X#^zaQ(y+2;YdJ%j8BD`@l1MXgtR??5ceA|RGS7KY{6v2jXV zI4?nG$?$yZ-ajiETR``=16*#cp={fVgNiN)o(?wXBA$5ROHwxdVq>sQ`f&SO!8Y?? z^rK=mUJ$ceH$o7W^G}0Cot`%GJ7E&hYjUn=+lo}&(Bt^N#d@VcFEt#NM_tZ+)*hl7 zQg?vV&Ozn|r6j*q!GD*6&QV_pzgn||i@p8!Kt-ueQa++(+sy5rf@as_i_OeMu#|+B z=N#S3Sb$2@>Q26WgLa)#(BHkU1OdQA7aeh{Q|8KNmBv1xDgNn>6{GJg*yqlmvt8X8V)7Jmc(0hZ4G z6stF+y=jucanq$*pi{wY$WiC$?YYLwlyb}6#0Bsed%sQS8yiOOL4~I<=^x zw+fcPQ_VNciSECbOM#QFUbFc^d@-x$Kl9kVeGoUNbzlak1qaG>b)&68a6M&SfZ7!a zA3dtgA0UjtQ1Bb4Y?6>zs&}-@-$>ap+U$Wvxc||eoL$W_1>ApY>8>8wx~b>`6A3kt zT1;zA6W}vZOB`Y$ylA4%Jj>X*M6vSwnDK!$r0VA|2>5<hc;T&A^ z4G&wG&}9SeOYnt=YjN@)6+H>Z-H7|{@lmx~w{&2KU>!n2pRt6OODMcJD4N1`c|%$W z;{S#c5qzE5t6-X>m@lQ12)bGQvj+d9l77f!P4TZ&w9w?1Ra){(Q&77g(9Ktfghc|Rw6wgoH0`FjX^5Cd~G9_OQ265x#37fED9q)|${CrcPNjX4kyV0krA(5}SU~@|JZ0 zhA?U4VY8e*!^;6p{Z`O)WKvL$Jhbz_m0CODYBU{;+^eOE=g+mT&eLYlI8f{TVN?fp zbb)O^E-(5Oq2)g^{I`~VCpNIa9~nTie!L|`9Znc2`zs#Wzl_2i$jNT^y<~mYL zq7N$`HpFvyDjmgRpc8Tn{Mb+R)m{Xz8Y<|{(~n1!TZ9$Nco_>ZF8f}4ckuF)ubA=^ zg1(Ev%XnZ7oaP&tSIDnO5=OTpFF8!%z$ycZEvcIUfFxjRXbZ|aevKY)+JDj-VSId6 zQVU+#H`MbR{I4`HTl8)O%&Q+7^Rhcw9fvgBqzD`C&L>#9__wgwXepJmR`3a*fEqBM z4}+YOU2Q1zrPJpTda%-em|9_A^W>IAlf;6ZL!91Pa36>;+|X;kXOT}yZMc@?dw|MA z%|$|rE!w+|o$Rp$^W5`?#9xC|Cbodp6`k`JTqyB2pp~<59OI|2}#2D5_m?)pFJ8B(r!)`1_K2; z?ar)n87>M0cG=a|8Xwz?@V>v4aujC!PyNO0T2#q0;KRv=Ib@AIgKANWIW-m0_`m9h z;$dV)qOamR=5H-51?;Nv#_j=Emsxu?E~qxt=2kss-tr#dr$k(e_f4I>LP@|#&_|(` zequ79V!ZKl;P-4`aM-gIvVi(M0K>k<7!K>}O|8`Wm@sJ^Z+dHJw)sNtiT9+5$>ftD zMmw3hX3f(PV`SMT0 z8iWW#i#790;OgpSSWEiP@TZE{s{}ju_Tt%_5C-(Rfk%&GyT)0HlIqf0NzK5vFR=WU z24p}>-F&3RE5+?><=_2}hv@CvNe&!7#Faj zXQ*zGyg+d#v?VSzZIwWw5gq{bs}DWF!gP~_#^ZdO)08ieR+l4q*v4iGQv`?!gJdzq zs=ohPTz1ztit;2*WYevn8rl|q)s9n)6Pe4m0#c^)Pf6y_BH}^wTq52IBdZ70z5F1( zmdgsbQtAAr?-0e0_4VXnUUBW{3S_JaY-R?$XTO8#Y~HR;BuUuO$I49{h=1Q}c>c8| zUvL#XeK;OKHjLz8Jl}i950~0eMK(@{UHBY^3SN($1e*wUo-5&hj}tdhAkci|AgDUG zk5#BId3CZSm*4z#)RIr6i=MaWbJ=u@R$ag3^~!ND>_mK}IlHv20N0M0a<7AS%o1I{ zUM#D74`3+`Fz$UkNo30`uqjbv8v?do>UIE;&scQYRiM7<0$ZOUi^%lKPQ~GaGJp*^ zOFF+;hFqmp*{;Xw+RlKKjksn{VezUmrz6K-ltUeW`EZl6Ft2ws!J#sTy%G^Rz%(YV z?K^PiXvHi(6>y1n_?_+7X)i9IqY{9ntC%|ReP=W1rf8^`upJJIOhljLJRyhc+6q z5dnaWHJ{>ITVeqaETTYLF4Ff2=1m?p7$`JBs@DFyU{qp+ijSGf;!#hhoNb;vOr9`h zIE^ZIN);?Dp!ZmN_j*B1r*H<^G5o~Xk^AHxnQx%0(8?#-qOrG%9lFQzvzCjr&2l%*q8(x~g~ z9|YqR)b2~60U{_g9}*VZs}tGTQ6y@2VomHkk~0PSYEX=p@2z_lmWV@PiRFYxedC%Q z-7*K^Pz3i7ukm#I;(NodW5?z}% z0F~52*Or#vIT~d^{@gk7In1R5qcZ;L&btmi6Cs(^3v0jD^5@(U^pQu@##Tw;hn?_95ur-WY_;Z2%k}1_wF+NN2 zu06eB3_#+iRqe+*W&xs2p6BxGHH-MMsVX=vgk^ z$%UP{yB(zz*!ecP;fUV@Q5)$O%Gi6ULI!_10m=SG6mm^1LR&u3OIhl?vsS4I#;`7@9kcXI+^!{K@> zA7QYYH`8Nq+#l&?&z+Uc))VtC&CS^#klbU*<#9tDS-3o~O<5WaLx^VHLGmlXL&vb$ zF~glo^ce%4-i5;;JFK9_U&r}Kftu3U(V^JD>ztiX5EW&8$k&+P;3;29r;{!oSy3F1 zb4gWc^XR}_(m`Ge1^B|qKBA!@qx)OG!(fe;)#@6WhmcujPAX-r(Z2PivWpJdwCND} za>t`4NvZczr)L|?0k4QlvirbNFL|i{RxrT=+l9{fz|ofLgh++Von@z} zZdbxd5U<;U!ykOA4r(oWfT?RB70X&QKT743#+9JDf)9{yR(kL@hj^?J9+s5RpF0mO z?c82UFIEv3kZY>9dYOe2@TDDw#@8xYZMs}HO{d{u-zsf5Pfi6_Xjk4~5b5$kQ@<#` zp7`BWUm^uYn*+EynRot!S9;Zha=$XlqT=7-8}!QS7v=P7$>!rjrq(SFn(S$%3R-Wc zgZaRLG&r|DZz}W+RvDiR;`VtLWQyXPH=Fs#jbM-`hK!1Vk%RzK>tF4CI*h4PIjfLnR7Bl#|u2-Ibo$0v){H!f^s$0UlqtTX2UwW%(Sq%kZp|J!->ir56^7 zBqrV%c(=gc2-Y#r0XD4fJ03s}B+eCA8u6Z52#aR`pi|zvK%bCuE0i<%FVib(cf{sG z2~`_KdddX=oPO$FI*@z1_$u7S<4_{0lNDiS4_6=^>BaX+mmC_#{3Gc(2iX*O# z7Pc8MRgNUVMeTabFr;5cI68H;^FO0rakZ58Y_zncy&RF-Pj77Hf91gbLS z&4{8}0;|c(cn>Y5HGFS%^C`qqWqR~H1$(?6*{k6~PkH&gM^yN+r&)ScxItV!vF=N! zqjg*xG8hK%>2aQ%$g$?=mQ{^C*9Gr3KzT{?YPEbdJ|j((Da=ljhyiw3Fjv5H7q9~( z9ffCq2||K7fdWf^AGKPItxyB?SEGOcE`h_1B!qGKf#C!$iv@7f^0+4!<)0#ePC7BY0;-m{n@wo)^_Kd|vX}a(-!BH#;2v6KL;*`6sVWMA>Y5isXwb}7K{leKH z5p>1plzZ~>f5~}wNpWUDUqs+G`Sb~rOq#t{SrM}&niHwpX^5fO3I zD}R#110%R0QTDQkfYl!Qozf`)SWTtm!R}?EE%o*%i6cXj1}-tI3kx?V=_Kbe^2+Q0 zc+FW(m%`X=O0BkYmOC5T8X=MrD8W%SVSpCI+tTLq3BCGPOgn_Lr85Gc@gLbmYz^}S zjK|bbd4};_1T+E)rfm!kqyaytjsyR|gvKmuX5fa^rpAVH^jOGl4@+RJ&-&otYub%D z^Dk2_0~8p{2?tOpw|LqJb?gZ zHQZ<&XXf;l_+QMf{i}=RP&$vE@&3Hnn}Cy@=4ds*1E0<^+RcRL`0_7kA=fU{PwbX3fG7FEbUMsap?ppQRuGM37EYWZwMy$hTGCWdHOULiP^n zg^L`_ugqJP3C4zdEZ5<7khHT?a~kPK8A*QW<#_k(wQFCMt+kV`KAJ_Yyx_6#asq0b zk65(!*z+IH-QAQzif2k5QslUGGPvin(eEia>FHB=apb~ z5oz2Dl1J@%yGX&)JwUC_hwOsYpMHGSqT{z)4r?6)nLQ4~ZuywC`03YcHnwbJiDz=j zXr0!!U_%3te5vE590l^Kc%y|}b7VX$&VOp<$EO|o4?%ivdV^Z;8xN3fAapU8t4dza z3l?0Gtab4^QeU0%Cd9tIf#Ae0%+l?b$sCVY|PWJ_-(I@YHz9q790gvyX4ZB9uy z&ms^BVc}c9c7T7f1T1cNByv+J+Tg|EEo5gvM>dxxeef(Gk7Gy3X_RJ1co}AUf7SuG z_F`dfoaKbEqi4cGxF|yb^7n64(kff3}mfcEjKD zno=YHi=9~K3V06d|0QB(cU%9zWAQOE7XnD zUDHohN&jLEaP1c8xMlyR{^na&vW|*>E#r@}>NPs9Asb@k0@i(Qu{b+~m%tXmn;~)m^)riM?yc`lWwauD{m%fyKb}BjD zQ1=gzyv*vY(R-j7biR z;-EmszkYH7-hT8At*T_{PpfNRGl9IFWk3aR68NoeN-OBkdzYhofg#{wuZ3ZH9q@c{ z!VH`Q-UimZpB8<=M&QNJO)KK%&{o|vAp6d1pUEI^hT_d|7|55ZU{&R}`UwT}WF15bnfp&OuMU25TI zU|3*ys1An60Q5`{K9Q_zhHJ?^v^&)W&legW;^kS6Fe3e z5NIBkV{rn=7)fMQa&x+g~8GSUP2%CM7Wfl>JK?H}1T84fEgmYORLfc;02XM!p z&%-UlnH)BT$!5CW3=7kFII@pT;ypxWZhCoBy>c^x>z^JP!Hx)yi3y4XZwgy@`C`|9 zW;&6spQ6ITM#d)1D!y)w#0UN6GjTQ7Jd#Cbngs-?lJ#{kmVqV-Oi6*NekS-Wrze5W zF<9qypg)R3L1AJ7#pw@vUi$yTVfqyZ256A*1seWr`MQ0*s=wi8C?hnaUvJa=g4;w?NQA@w8Owlmla;t%p>e@M^KQ(@ z$z6h5%(N36ZfIV(F3-60pa+-N#d7C6Wg}qPhxcFp9s;qoFfEDnTf+{Dzt$h#laTKu z4vSyyZK4Rj8sg4LXDPK^#AuHokzRNN6x@HFv+kMcQ5zbH|L8@rGcnApX!T3-fz~SEY*A zKadN9K%<&x$U{OV56aSiC$IOHj{@s~#oK92|90nYi`!%pWwY0Mxf=*5bA9VaAm^qW zjJoCzlSs4FD;8hWE;(5QUtfYika#3fmMxG9Gqcma(ESo3p(V1>#TLRZn}QO{>Wn3GFYbET1AgNE=mc@o~sLi|%2MwC@Q; z3B_mXp9pkF5<;psbS~B}oa&O8nB}W}y>nq`!@SLz6g|iq&a6_+)q+IW9wYMpbR(%ynvjz+5uWpY%#M2bMdDCe_6|Rz^f2T=rdW_u zVu>sFZ6Ulf?}NVBdtZ?87Flq;=%%BilXqB~Xj=t_SrTHsHQaF9H_8uP3-u~NT2|gp zsWej?_G`1F)T#Zp=TM&|;bbJw@cxF|_?)%Hd85eEa%9HIDuJdaTj+-Bmr76oxJ-6r z^Xnu5I>6<>W6xb_4@aVL*{P3upNk@UXm5NpWeahpuZ?~E#_{DO8=8(!Yt}VE=iYkN z13kgxH=_~U!V=x)ojEoHv#$Q5tp;P*I&|mKg}iJmnWiHlWc^$q7-zl7tt1z${d-H6 zPsFK!sEMQNjtU4Ha`=0kt9>$)xkZch*ES{M+OpXN{R#StJUZ9rQJjn?)eBT>{C)E} z{4=Dc{lRC(W{R?M5)Nj)E?QTtnCcmPIkDGNgShyEUw?Ggn9onL^@JLqw<7C8)9%I6 zQ!R6(JO!6^!b%oE$UYRGFqbS7SkRMhJ?r}6ywkODo4Ho3p=W>X?I@E|Wbok~(pC({iug^$R? zWP_AdkBlW~@s5{S$NJ5EOoy@)1we1-ovPBOgMzFG=v#vOd4d%m0V*_*H%X5+kh1@C z{8N3+%`aZLB{Z71fZNGEDUGU}+@r(BCuQzniJh({NFG;~(I363H#Lo)@dq9&SHncIXi!73ND>0L|n ztZi}dEf+M9Z8yU1-GMy8HcP$LFg4HN`K3T&n;nsW_et6?Ky>RPNMwEQmm9W~$NOxP=M24?# z6KdhgIChnO1sx!(63JoUUeJEBEQ2hOvCKnQ>6RI3u%E|wv>Ce1v_15ZRxjT%1?S z!T#Haa3k_(>RNFEVHF>gP!CjqJQ3bo18fS)%*%}L`r4pQ^8*FRiG(Oi%ju(T@&<6H z$*$&Yd)l3=!1P*zyK#ekf}ppl2~p9O<%D`*W_`&+zzwzdbAQL0>UDyKohH$?1jlV% zH~jjY094i{$RE6p_D81Ko!P&5(P9zbl~!=Oa7$7As6dkb_=U}I?(gO)QwY>n&zv`l zZ#;Aow{1??=EM!N@#>(bXi$*GTiq&^#qkFSf9jRA%b|`K7u$JEJ#Tpt@K^LrOCceobY$-Z<~D-@q`I`g1@5jBLY!kElc|n-k)Lar@5<#v)w=D z)rKB$4BZ<>{FU+BD{VUiSf+qq49d8gG1`pid-CxvUTPI6eRfxRK*~VxExFmdDYA;qd3W)QH?j2_xJv@=1_%Yxpp@{ZH&!A*{nB%# z)f`4t@a}4@gjfM8BRRv1lXEO1`LOKDruSd{qS_PvETXVCga>iepQb$sN@MZx$GkFU>;ouo7 zK^^^qE?5GTHV3B-z5XBvw97#W!Pz>P*K{~uR324FPvYQ0ld{o4t>c#)IWFE zwS0qlrDS(}auiW$qfe9SUzw)9+Fd7Nz^Wc1(lQb<(Xe>X4eLJ!ld`g>uAu)e+(Z@;${mfNxXGLwiBZ1Z<|#JolMi}q0d->t5}EHC z<`3N-iy=X@2g%cJEyasWjtsXM$YTE&5*#T=c&V}B8dM2P936Ares=b3zxwycN7s3_?0CUUCoOh!^W zC_U0u;s!u?3!0!LrQ5R`pHJ1{f`Xg8mEM%Lj5{ zwZ(Us(y2p_WiptT4*t=s8Eh${J3@J1>r*x<8O-{;%*15w))c>8PI}OZsl;urt@>g{))(H+%1_9aa=krx!HnK|N| z+Wu_}Hz{0DSfUCqT*e57Z6{Qj?c_}sXH*IyIqOijHgYYa4C3sw945`Y)C^Sk6$+#f7L(-CDEYX2 z!@$YUEw3(b4fgutT*f*q0~te)F&T;?#rQ0OGx*EI5YGLa6WPG#X&L}1t#b~6{f}jZ zT}hZTzt^EDsr*&|Cr(ESHIUj(dVEvzbg(~RNaYvB-iNWjYz>Tl8F$D=4+sP?nL#PH zky0DS)S2E_md&qGl9ZuQDQ9Tvne+y4x2r5EOF~NZ+2?mvnMx5nuRY>Fe{@hoZzDgabe35A+Z+bCMmioCxCd zJZ6!}@iZ})<;IQ>tfu!>9XnI;T#)$UDS~q%PnJxEj3vFaEye8+*oqcHs38FYBUGbN zHnlU{cvB=ZG6?VIgRr^Rnaknwr|^+dow=^R@&lP0@Y&TFcVc@yK)|*cH7+Vz;7+2D zJVuK@>>qjbO~4m`K14>vjS2vKPF~T5@)CUzBoOrFdijDWOYkTLI9akEU`D)W?&`o8 zGou_M*@4c8{s}{aH9O+nfR!M=KQnBX33eEY=Y|KftE|FaaNFx+`arDk7pU-% z?w-r8O3Y8FI6{J4mb_Ne7)nFR{W^o3&w`v&StB3Up4Wy7?eYVumV)5D<4!7o@hmfMVS3a$i3T>H%AfOPK514lr~O zQQG_qf%+0m>DcSyDI*olD&hfggh)CfMg}Hd5|%PLZmS@8gCNGKoG=j*5D3ZX8~5?g za=<(dWW5m17;=qsJ94iZ0B2sGz1gSXvml&iO znlrQe+}AB=eN{<3MR`5gUCtkR7)jaeqauN5lTb;-Gx7x_ILrb@D5sTlUA(;5C{c*^ z7!5@6UQM7kh38QUdA!MkB2A}}~ccjdoOdt+h5QLqdDActiAqAd@t`{7# z3|zPA?dttxf1+huQi zKt&K70ju5vdgdbH@{;oZq))rX=zT#U93pZ*GR)>(E)i@hF9aZBh)-e)c)mHT;+@Hs zr>TPo#1j4i2Jb*dmQzk9vRNmR0XZWYZ%rtkp+tW`Y@wm27ZG9g(3Z5_LS9hh-eQ4f zybFb-jI1OTi!-r?skp_M>I`LaO_dm7I%$FTlCPzq)`UT^GpD4w>1zL@W7?QfPcItun^ zSwg(X5X1Xv7Z@nUZ~Zd6g?qHdABjY{uPLSlWSptJ5K$hX;ks%VwGplNbQS<+&6>m% z1AO-dV?gnpC9$ui;L6ANgTy5MuGzk5bbu+p#z}r;UCs2tL&EN6S%EX9t$F8$3MS!x ze+{G9MO!s-U4?Lv)dLYfPGxvA0fW<)31t{fk0A9e3ww5?vDPTsit$F1&jB>?q2}1+ zY16YIQe^(9h94jX~`DJ zX3!=i&$?{nv=;7)i-Ccj&*Pa?YW*D9u+NtAP117WPDWZFWM%_EQh?NxX7!B1>=;TH zkn=PmcOF6|E*fOQg*koai+1*Yto3&7-k!3GJCCb`LaiG zsb*ote&fWs^oLbXlTuH+{fQm{zD5iw<~GwhTvbKa)ZTN>Ft(@gJGTvCW=ONM$6YVm zL?<{3>2BjY^9>33`*tGzkX!5B#B!o0>J(I8?QN~7%pHyWJ2lW?B3L<%^Qa$HIgHTc z<>EU(d>Apg30@B>jS&Hc5Q9Ql7QJC$B4UC%(5uixa2O#mxWm-`4Pgv%od)S39`P7q zrJa)xtO#sP$nXM8jQ4qjQg~Pv0HL{FVy}uX$NLPoP3@LNwmWI4g%L5Nb~|hQAgPn3 zw1C?p`54}8EJGRa%khc9cSi!OOu%&{S|7E{JV?sH=~lr6VZf~Dau)#uwpf;|1LT`R zOc+Zzm_!_-+K3tHfpENJq~r?o09_41gu)!1y+RY1leke+u9<6{8``3R1=3pt&2bi@ zkZ0XRb^+ovhPV8loh(w7dxbbtp{Pnp5uYcNiDYzsnyEOu#E*JX!=N>7(sm~7wxSv! z6Z})EzqffqLe%7=h#JYA82Ma8p}hW@;3Q*Y?hMctd&R~qAtoxs&&$QZ0Akh9=|dv1 zuW(k}+TeLvs5FCcs#hUF8pZ|ee+sZt)6qhMtU2XwR0=WFn{Dhr2@bX65nIRjJ!Q5k z>#*>l+B|EMXL=mxp>I~reARs8eDC?XuG4+_KD3KcMFjy2{MYT^7*HapL{(1MFU%jr zj|c4s-G59ks7dL#+9UAzCBBb63S>#We0gOg>1 z1l%A}GV&ctd;&tiy{F(a2NNZXh>Qpz1!N>SoGciKAwvEnWRQr}%P{oR2rVrgJp*GV zh=ZAuYUl&l9?V2UeP2>h&iH_s0(MNshzAvKRl-vv2Tc);CcLWdadIZa2cMGRSFiF? zQc;7KBnPfiI)c#Gt~bDI3zmQ{`Oux@dqP6&1%Gf3BTHX&uX(lq-ud{e#vVB%W}+}RSKQ@ zgl@bzbysiGz(ggbE&>G^jc1js={0>J=(A1R6!^IRndA?oxEMVx8KD~p>GCXo28}$! ztg(-X3&50>rm;aJM|G04GdxcQF1jd`tl|g1Eia%6@trC6c#kQ;x`60SABDiI z&$IWo!YYnIzG@wUC}icpNWeWsh)PgHpN*4sH($q3m=Pbo*`m@=$7{-3b1da zmUwZYx(qe*juG2MJAuZGB=;$Q+l^L7Km4;kaQto%{VSQ9BHbZe%VpfPuCsfCJT}9D z^&*Xcdn}!SfP<(z8HN3{E`mZVpo6rB2*J`WJ_zYqM+`Ajv{*`Tl$@cOVUImv@zR-3 zJ`th(kDaPw8uMz2m^om6^+&5PiwaYsV^-LP;L2c)Y86qXY&e?EDPu#2=)srB9yS$N zetQnl4o9yjsAmDjSI{bU49EZ6e)0EO z{TW+<5nKYXD~JB4Q^zaFKNf@_F!--8wqfAPBk)()igrOZx~R~hDE;=!C1&=wG=#(2 zbL2bugQ00SW;~)RL{lN?FaJO#TO;R3<$%3uUUn`7^K;Qlv)-SCTR5h$`S965t4*nd z#l5Q^1w>bGd6*iK99@wAHnMV_e`{&NF^>d;+-WXE4Du=L`@NdmG=k!Ki`%u?>a=u= zysGen+Ci5wmzgvir0GXZnM-nWgY$G!F03Fhwx$L6gh?P!$o|Zp^MQYI`4D@GF>CRi z*-<*;8(?7b0U51flp)ii@CORbS&Abm_q0pj%_xp@jWeKiT`*y)yi|zowUsjWXQvgn z3{JCCsa+r`FEIIz%e(S7LV4o(J9Pjc#Phn`&a2;fSe-H}_`W^C7F$1a4NhsU#dNA+ zsn>*Ew2PfNsC>It*PmY zjpv7#xWo3vb8rkt_7!B$rzkqo`)j(4=laWvEyXEk^{gthXCd#p+kF#oOvoKg6}K+& z^eyYRwM`e&6_+_+^TXkC7QNn7({VL=H5?Ob4|3%GeX6JZkez6)UPjMiBuKFDdmUL( z?O=WYqs}o94*+^!=Im8`^X5sMTlM5ML`I)A=R1ha6EZI>0mR?~UVQA+-;ktn^Ca0) zVbR?HjG~@TKm4l4sUv!TIZFIb5u)xl=@~M)mD@E|exF2x!s4&hXHEB>KkPAkDlGN5 z7qA~oU#9-KFi%a-l%f}rH{|tqPJjI;0Ep(gXJ3!q6G^PI**CZ1{%{y`xl!_avlK3e z6WixQS0n`kRiMBFS(J)zfsJkqDiN5u_-^Vxs101G6F= z7Sz;X<6Zm5{Il~4V66YQF6GI!wy3)*qIxjQK^9l5`Q7nn_nngh?ck(L^|(TxtIEg+ zxu2O7Pa@U!CGB(n0081Ioei?pcOF9gC99dYqv!kGaQnDX>o0rS=p*2K;JW|Fc+twj za!s=elNMCg#~jKyQ#@@j^mI+$fp-s>3MG&0=nptLuoCV4WKf1}QP%M?IrTEZ?wDpo zBU`mPO;SZ8;^W2L(+{@Ve?@k&z+dHg7)I0pekf-4Q(jydxiD^Gru*TOIT;qE5CVMx*}*dZpxiQ#Gx@|| z+h*(B`3qkmY~T5J|J2?+`h`7D*5s-#RJc!t=dE8@IUzJ*_<`{0+WK+LVFt2yp=kJ~G$%Xj$fH4FuUv2@4N|#%BEd@K3 zu)Q;wNGzrQn~5Myo&Ub-iDIDXB_tsQ)JmHyd7jgRVr?`4h_Nz9(%&fPh_Gsw?)jR; zp-#eImb>k%wT_9kj>P1D#xd{rE3FHnmW)irI44LFn2A3%{}LR_Qn@V~f9-nNP#}S0 zj9~njbkuJG6Xv)VHE;b`?iYU4j(bg)EqAk0@!VJoJN^m(zLHBR+?AsGup~q_StKEL zfOPBNC2qw%2Y`a)^oS`6{}7!i@AIof!L&O6od$zc*Yl#WII&^-Pbv@Ae!kA5 z?r~|$7?)nl%-*lw{Up0t;rc>#XL1(Dr1UA|sRPd0lfQFriPEn)lCYGI-*LFE=THif zGZPSk0zfMu&xoLlRx@)RY*10qMZlQ#+CyM zUP8JBqr<~bI@gl_^Emg9(5FF<-b9`q@I`km{wepax|-V|P@14wf9D+^DEseaA6OZAZwjG+9DoE=NUvHzKxdMZCj` zY~LO@$3jcG9yX2R7}>q2VVBU|X{Pr%!qQt$O~+|u;upeiO#QRN2}p?StFcpYbC#uN z=s<-`OSscU?k#+Idbf7!R30zoo}MoDbRkgirKB?bFz>|(vbA8zcXEZBWm>Rf=Q1%{NeV#T*5Pvw;kTpm-VVD720R^hdeq{yV$t|Al3{Q#VUhDC<>Pb7xt?d1X}VR zC~%`=ov8JKN+NOUW##5%8WjI~Jl7?&BMq$iHMm*Q6!tEW`#ezw zC=Xpwk_xVUVtNHi3maHb@xHZityb8|V)wY1C2VbZU$Jg)&Hp@wm55+_iu5%!&ZJ4N7#s&*)7%T4`+h1lRQDkiOo**j_DkvW zOxi_;(Ilmb)ePc^6?TkHfa^^*SnS*u7^NHH@ zTvia1(Ms*;;^5tX9^uWLDF^2IWf`bzV6_({vYJJ-W+VOg&X`)b{4ZQZ63e>EMMdf+ z7-jcY&PZhi=ZB(IWd(U*$Y;GZ_+)S!9P{Ez`rBwX3;!$QKbE8pN}OyTQqTmhdaCNJ zj3LiG7YhNb2Y)e_aq42ldbap7VAu06z(Mz~>RV*%kt7u#&T?`s!QM#d@$zEIp|!4) z29#Q00WoKs<*WmNW2aqjnM;*#yXd)Em_CdYXv8&nqK6pQdKG>O(|GBl@CYjM7da0e zaSi_YCa80B@G1U(w&PMEjR?G(MK1p*_TjM#FAdvoAI%`)y!-FZBDR=V`~T}6l&`c* zfJ*^4TUjgoT|xN*S}Lm`Zau%8sQT1 zggkQF1@HydLb6KK=4?fZ!+#-J!Tax$?>xW8Z#;dI78|zvR9$U;vHOzTRAA(u<@7Cq ze!zwX^U8B*!Oo?h*qo6rF`g4A*``YxKPce^hH=YD%|l&Yqi|VOQQ_qa ztZ~HJ0&n5G6SM3qTT_;R@FtzokrvAuyb&~iC(Q6f$K9WJ z`2sr*%Ek+-9Xl?9OEX|6FBI=TNE>~T$8rPsyDRh8bNrV00O>jUs?_a~;bbC&lZg>J z-Ws4?#%Mko?oaA%eENO3yEc7Fl%BHE#pv}k(-MyP<{hX``VL0?!~00TJx6_{slRI=u<&;E;i^%kEZ=w4P0nJisV zhwfJo*VLk5ZXEN4Cpu7Z?gKri|7VY^>DHis9}oMa4q_L?37gd0D+(!aOc;aW-K$_m zbkmeE{AJB=4ckL9Uo`~PDix+TqF~Wr499fPBvV93g2oFeugdGpLg}7Emlv+;q`VWF zKg=k}*!kla@k2c)z307L_)!g&Yj%ylCM_P?$cr~Hv%Pa8#6O&PjALf`pvDsE$}b(H zcs2NUxCe4DQXO}@m!Yd>rcXi3s1_Xa_j9dGw2k9fV&iw9Pt@X57lr3Tn&#ESzRF;v z{wxTHVQ2w9Tx42UWoG2J3+syh)!O=eA6Jp!DG>0(|D-=`&r4@De?uDP}l!!=PDKwbzvr+#>4c+7a{Io|9<>#$cMBSu9NJ+3p>UQ>)? zL^_|y+_shR0ZE&sQ+=;h`Id7p|Aiw)sq&>#(C2`AeK>}E*5mT+kI&ImdV-(7KHWuP zrV4Kt+E?9M@|>4fzf(Y`c7}B}id@j7ydbK<#V>PQ7-XdN{)-VQi$Nj37sa3tRCdzr zSUK1mvb|7oiSnmXNshms&Z69hcSIH2q3NM*QbV*gB`E;V1AKM}EwWkOzSAl8#I8^N zw;x`UrpI;H8@#|Gp_UK#_X}dUg>Qx1()BsLw~>OxgA^X3HixXl=~{aj2RdDF92A{UjB+NTIQ-i(&s0V z583n|=WSP{=AmqKaB=(xi+38PGcC8I>o*KEF^bxMyNJob4C3xkL7aSH8`+a(SAvg? z4+lE_FxtdKKUIePn6BRDxrHA3X7?l$$0VKZtZ&6%oRiS$kM8L|Z-zhT5yJ?!udd!q zt_F`tE#jEB$%b;p8w5-DYIIkT6{cFjcRR!GA$Q{ZR`we^+MmF%As=9gnVe5rKfT|U zY?GKmw*5X=aGff?<{T)1pA0`sv&PH3{g|-M95GOi6L@m-L)ck=N7v0lL0^ZN|8F29 zeg1rfJ5jkb??lg3p8&_O5FhMHrEV~=kZr+f_^-6Qnbal!^|WcrZh_t9HABPzY%({S zAN!M9E#u=Xxf+TmVsX(ZYKgRWS0~pjb^wIUL%%;wrW=&|HVJGv7F6CTd|^t$f^;DP z9@5^}_1|>3y$U?QIGB5EKwRd(0x{s5G}UUtAM+$R8n-eAB`#`oG%cw3+E(7KOfX^lqK0xRMFFB|6prFf3#AK_h3ToD-nu7`EylFR)~x=Q(FEP*g$m+5bC(r1LZ6yd9%Tg< z$}x7(g{7=D^qA5j_CecR4LPi_70Z^`|D*A5iB%sGr>`CRJ0CmCD*?gAJZG@bYNIJ+ z3bW9Y(4@T|?xZt!%@cJF$QK>>u><|1Jdbc?Cs<>=JO#+?p1620wDP*f+;KZ+dzHz^3HvR@?rKmCYVXX8k8O7eNm^=>Tia42qBu>O<(tTQN998@XJ6@sxn&=Tim9(z$5 z2EPV)k*$Jb{@w4k*b3xk5wCB1=N|vQ@Ecn@{g*Fhq!$avHMzxU|Jge0zGwHd=(j2@ zNwo4JEx3zjEJXM`_+MpOCEq$pAf-T5#v_4-){g&&B$9C2T$hysc z9!&f9(3q3?n^Q;ZO(3Qi< zoY6t0>kXqfXP_{g3(&$28HoPRK&mJ0i%oJT3Yr|##tyfJf3$Zj3qJ&aCpYrio-5N) zA4R*r&$j6&PzlQrQw1#cJZ?UZ#wN9`M%6s|t_x;e(BW^Rv#T(CiOTVB$P=tqg6(oMu&*7hygmz6liS}| zo6nxcgY7u$__`elw((7RAcipY2T`)Eo6=HkQ7K&lf|~~Hg{#O#y-@L%Gw64ocmKx* z!}m3zUuRv+wlNEIciIQT??+;@D*zyCg&C|rb4DLDL8!>_Y0@RN=YNf_IDMKWM4tcK zj(dcK|Vxsl`uap|4DcVYmKV zkJti5(f{?%TKnSF>#2X=)q~J+;U06LEc|27uS?C`wH~fAv?3eMal&6!N`7M9Bd7Nu zAhn|X_jmD%E$7#7C`U$s0+B$fBI*u~$*&Yl$(DZfMutMzql;uVZPEMoXmp^v*dv?y zZh6lpc{nyih$rF|J?6Mi-Y?y4rp{$`PH5GK9YH`+DnGt!x4LliCEa7H1rD83S>w1e zsl}*e`zoB|YQvYwkIS}RfvYwj*u9$&Z0XCn{Vo959@=5=@l8dU z{UK9b8HHtC1;}`{S!Y#{e2)ss-HiXQ7r`od_!NF4=`r)upx}~kgjtGv`-ZMm#&VfT z-<~^eKyj*cEh&@2xKW^L*?TmuQcNYrAT?FO?<~$sR#gJnVq|)>)LoYSi4Y&D#P3!| zutbX@@7L!ta;K5@rcUAid#*NdKzFmJ!=$j)m`W!t*o>``LAh3m^|NMf0;mksgo{QZ+%J?MDISE&RC zVkLbl7dATAhQR;&yZhJ&NovX_vjY1hO9`h9p$;$oy_c8Ur@xYkZK8 z7y0Ezv!}?c{>g@G?TlV(Qe%Jms{<-rD;^8cFkRip4twG*<-0lYwzIBARolBH(UR~x z66@d^+&Umo8!*Ht_ebeXfZYQ{#e^KS(|-n0PZqah^HEoIF5TE0;juq@Lfg)hE!`il zv0*p9mvtBMp`Ts(fzAL-I23kP;m(;%%PjweBFfS70s@5=GS`Ug+cEnl_ zdRa3ku;mC-Hrd$?imP_28R@>B{Q0PdIzN7SD782@H4A=!(FhD|LC>OkEwn`ELMTX6 z!0kPHegi=l0Fo25AWK}nVs5L9HvRP#qmR>2^!p>bnnI(b&6&Y3w<;4Nb4i&rW(JPE z=-05=B2@=CV|}2U-fdKVSrS3G6;OE9uq= ztsJ1>O1RFMY7l~P_>I%GB)Vaq zfnW!5!Tttv-~1&lLrb85r!|&dfBmzi|3hJ<-usPW_T;3RbaK=4_M_#uZ&#xKc$<1+ z37}}-q2Hb$4hsd$94V zqB(B_rm#%IS%xnsT(3M(GIrTEO!O36C9J9=-Dh*NAM!8bu$qJ)(bPox>H8lWe}Ocpk2u!% z0j|=2t_=0NhM{aF6AR_&zn4ASZq{&vt1T+C{e}RGO2&4VXs?IEJ?i9$^Tpz8-gCc5fbpd+F@zg>IW99jDkY=m*@wo>5+&&1;? zZL&LwAkBS_Csi!|RP+d-R9WRu=KDM1jKbJ*KuY`Y#9D%Ewg>E8xs?0VUX@4rNU`>t ze9DW$PQiQOECV|LpmObyKvJ%IKXW}SpDymfI-#t}OwccmTCPHzgy){JA4_>_69J9g z?CH3KMSttj7UE zAo_>S7Zsn$*b>1X=A7mw3C$FBVejFXIdT}Ng_jlinv*iu2UG_OWEDs5+ zz%I;Z0ou}s`xJPtN=?jF@Gs|#)UQ{Cx}TpMgT=8{MfUpcS~g9dcsng;jY`W=%4!E> zT|HE;W=0AfjMGz7*Y`66{;=V@wDWKGoIdwdy}%wPj$rk-P?;aQ7j>zO$iD%ImL3uz zk#*7mVIpqC+OWr!u36K#xezO3d+cf5P{W?{p!POLH2%?dCKrV|rYc+mexpjT1MnKh&C|o~`hO+#&dF7-E`a zN^(tJ*NAm;0H7-U<-U!yvC``<6W<`|a?i`j_=}z6!Kvn=--tf5(wqAAGoUWaH<4I8 zE(4`hU36ns=y=te4FGKtc@aB*Fsz8MGPL@GRP-lE)7w~(3pSqxYLCcA{pY{;7{8Ay zWApSO4I*0pwQ-{Fd76Rm5=?R()8m`W^uFW`NpDB1RimWlAvV|@_$n9i%PrhM`G=1H zKdsBZ=nqzG_jSD2%^5c*|I6xYR;M~CpeGs3?*jw7)XYCzvx8R^)Os5iNeXmBx>cC^@k+9H3zwon`6-{{aU@J5~_oBY2Tk~yO*uH<*JonZ52{4{wuwYulW%F1()!ETb{5wOI>;1B5 z1O9jvVB;*5`2M}eV|iH`htbae-1HDNJecK#f;PhUeT+iOn z@8#cp{9nzEnmPU^gl3Ie!;k{UEfIQov(|ewS zuWS3c*M75WHC0uecW9oOfKhlha0F-sXlh<~y?(C3U;JT+YV!BO3NQD%WiZw;$J9LI zVe{V%vsat!#3Yry8as~E+p?tE&6soxYQ*jhj`!{tf2XMBBe_}&?mVV!c%KerXzeOd zyeL&!POHdC3SpTV#c2o!&Mklcsk%P5ZJP5_2%VwhZSxgzX5zI`z4tcT#pN-MF?j>p zGt%E9@!Ku?j=TZ;*l^<)9!NYj--X`#yBGZojAOzG?|Gr`RoE?es7x&1IH~9Ks5}cn zfJ6JYF+8m#Fd7`Q!kr)*I+1mE&UBLDE|Nuc!v%_jT5swk*V(MhhtV8i*U|oNM`v-p z(w7>kZAIL)W3zH>MkX$j7uW;&dA@4@6R)Uy-8fxi(*4V3PUSa4+llimYpiSEEJJQX zL!bGP4A$Pw|2n3%+Zpzb%-4RpSo6OegsnB@1y!?r!nFT~&fX3Fm!|h9eF(@8@olv4 zpoA&u$#GLVaOMl7L9$;c_-iPJ%iX(BxB8fjtvqgxZNQwyNgjwP(xcb^kW`Z&leAA1 zh_JKuX~lYBK!pP~si$@ zNyCA#U4oBnE?hNPIk8y#%8hzL$)IJ&|E-6p=BL3BF4#Lx4HWmZ3^E?45=vJ|6Pk_6 zB~U*k&G5=By0my42QW}x$V(Q!+g!`WLsG`!n^~P$<>3BBTUKAix(8>zvPlkgCce7=pc03xzKex$e*zaAYE-z?W1eUW7fg}4ml2zL)Zn6nj#AsOcb6)1n)~e#8 zgZC1x5%_adJ1lCkZXs&_SzVTkz)oJy1_H!z;?1`4s&F3f?K(m2!VQHMJwKUPeq0~} zFekDy!OfMSVa$g`}L+?lZ{yke~L zz#4>ToiRzh^oJRDp8qLxJ7Z;Up4Lq%mu%9n_^Q&*wJCeLzHx!>)e_sEZx1OoW>6e- zu&uyWTlaoryJFW@csrh!7jAU#zT$q7rcmR%FHe(hnWF%nh!bSh%t>L*9C|AYS5b+vcs&H^4TER0}lh2`^pBk*|+c&6~4 z*02&MK2=Gia#gVQ$VT1Oqv>32+sY0?SO7b;+T}ZH9CVJhHDpN%an{Q z1@;7+nR#t_2{zxz)ZA2h6tam%?Mu#)-qo1)Q1oiwW5h8_sFQcfIn6u63#km36!zI9 z?T9C&433@DiandYDwY6tkOG~RZ=$l(y&pQsDqHUasS-*)(<9z+ef54|Um05(bN6BN zgK>rYzUPM`=YP50u33NQlJFe4MNj6TgM#M&=f5mv)m8K7bi}rY5$u{wiOmoBGyZAi z#q7Rlt0Q-#OaFtL3u}T9regx}HD^VF5hVaBO*H8X-TBc1bK1b^`5`XLXybH04z`tY z_D4`QvLZ8E1kJj+#%kdF@z!t)F1rE__sia!ta059|05;i!?<-((Dk<&SvT;R{-Yx3 zT=C{jGBzJJo#1Y-w4xljo^}C+l_Wo!=h`sCR-L@KjMhpqdv{z$xtJ&C@%379Gf<*& zUMj8>gOhCEk~YA?$cI11Zbl-}&DogD`Qvj}663d0m3!K0d`fya=GI6u&0WE{Ps8ii zQ>^@VrfZdp^3FB-jax=X_M*~GeQ``hyy=pl^;CS{?l%9IlSTeG|C3dBaAW{Tr8UYs zznGd2za?(I?#&{n~7gFER? z_fl5aEyJ1_qFi1N+S;a${g>zBFOl}l4^Zr%s-%|aONs5VFS;}27aW|d+5I@{YI?hf zTN!u*pAsG#bXL#*&i$o%gi{qSR(e!|t8w~8zHTV?u|A_Lu?}}@d0fcfwP2*mqRiT9 zVY?O~Bb<)Fdi%w?F8`AHKI_7h(rkOb`s*KKaju_gP4mMB2XZa|x;Tpx6Nh;l3=gQo zgt20^g4Hn?yY>sf)SLU{-b~Zg&Yv*i4^$lArhrE(O~h^b(c?P4ir}*?pT1Dgo;t(u zOINx`Zr$0-d=Ibip;~QJvU{V1%|`(p74z*qtC(PR1}>6I_$OD*0AH4ZDPp`~>^}tn zr2Y0GAs6)p!}6@N?QTlqaVOE5Hn-Tj>wbf||IlIATm^v!j_EU~;+Vqu=GCS4 zOx;c+%_#XB`{2l#$cZSXDC;tRYRIdtpEt)?BchSpjJNU1?;ny1%mqCke}}RkC#lo? zFEQ^3Dy<|dicn1a{-$9ob5I5QDMkXavNNO5p&^Q5#n&^qCiG$bJA_fL#oG=tWrH~a zWJb+P4cj7hvE8fNUkyF*eDScB!Owo^AJIm*Q(xT5RhQa@`sZTq4<9hjIBPp9ylBVy zwmoRCeVeMLcBA;T{r*>rDpk!TPTmk5O&Lf9AjArhrD|dm%gBDFp00-7T;UKCmync_ zmXVc{M>)H=y19GY_2ydydVD_kqXX^*1_g(NhJ{DmN2;mMkZ%GSnp)aAxQ~Yz8c~|9 zuO26Vu(GzXy<=x$tY(bVdwm$OZmbRSBJy|7R#4mT{_+K8!l2DzgMvx>Mxu|2L}A3@ zctcr_4=bqf%PF5`xr^wEY$A@({)lYZSXsQsZsFLQeh8XrPehdK15u@V%&BdHm2l3` z#8$$|5?=ZZwau`f6RJ)g2H4j=JrfAHZ#>q0K}_^Ag41A?ke9rPGCD|A_VEm|Val~w zH1RLNndFBSAuivkPaY9~OagM@UK`lWB1Y(6epR_OV>DyzjD*t_`0X;LnKCh{CazN0 zEr#n+tcXg%B+*n(^xVgE3}>9|r}&}uQPF^qU_?zYea<3&LQJbcY~=P^!N5nD_#qFl zTA)V=OF--fs;xwnTT0AR`74pabF!;eR9QE{2rZYZO+}Ck+q|C;S3t1jl(jEmC`-Jn zncJW_C4Zm~p%6sWJir>|s(NYY|Hcox(B3sh84glxts0WTm5 zUW>tGaUHR~?>H=3?`D!c7VmQ;5~Us}%aqoEki@76yUY18cAKL8@9Ay0d0f#~$kjW! zeRAZab49zN{e2^f5nB}-7(O0#dLPw*!BDXzyd>lIffXp)#xT#wCN1vc91Y;-)pLMp59PEh79gLa6&%OM#7 znXtXAO2*A*}G`z$nK;OgHRo>D7LG8^rC?9Oh1q!gj*f?0=f385ONPRvQNXQ0=8H0MD@ znTjMl)AtF7tTX?s!krG+j=InjsClU;myqmd><`NEofiy^) zj|AGcqLFH@)Hy+J*q0VS@fH|BAMe&(S&f5O1^7vv%h&{^O!}vVpttKN#yeNLM(19h z=z!gQ)6KJhPIGMwQ_why&NIv9=fGzure~JJx@7R}TS8q1cUspDB9eXqLSvNQKeX@?QX6`@t8Nv`D!XN zbAB7|QJn@1i6*Jh^za~V_?0gwk0?6Cr)cwbjG9qyFnVa4d=sD4Koi17kW@}gA35Zf ziCQs(PUyGAIHTLy-GAIW@jQx%6zP%Q?X2Le%L9{GrYe+u$U8 z;tq>A2qizgQ`=4R-NLpjuZbXZ0hDa~JlF2vZ}2 zW9aLY5|f<1)WZW}+K}K#YO(W0WjyZTI*xKm4UkSvq#M3tah8WM30T>}360?}&v)^B z?~Nh>5Z-7G)h8?sXYQ7-_KY=4#R~{8&6m=U9ny24n)C2%d#P8iL4? z%9ccUsG!?$7Xt0U^|SzAFBi9Iok0?$qALV&7fXkYg#@8=(LupXF<`o$=+Hz z%UI9$+BL~5A-~Wm(ikmYHN}`fMd}>rM)#A?qjcRI~EX)ovR>C0rRt z8t_VNhH?OIt}Wu@g)q{@$5(6!=;84EyWUPpSBxzJ&YeBwEt5%DyWC zB@{i$T0d&Pxi+=bH5Wu0Sy^gzo12?hUpUYQYH1iCjZM$buVn^}%vG(-E%dCl^aVfz zHbSqTScJaeBytYX!*||uquP=h8uG!HtJO}SCg7UrqUMK+K`hZ|Wos5jMIAIz4rERR zW=(K>eH+g2kycD;@%9M}iPS*80X-3nL8^%gG>JW?p6iSd!#H;RMj{iCUs-Zn-@C9p z4G0L?5!gh{y+lv2MlSY6SaE$G>5d3wTcsnyBRh>h)xE0#6W%6?=FOW?gq;0p`ZwCmNNjn0g{P8Py&fB z%G4)#AP!(J1PH_bPQl}Ge)A3>!k+||ZuUQbRXKsr;ZRGw&@W5J_}EvaHpKSAMMcYJ zR4ELc_Ck7+@DJ|#qr*mWDB~aT<+dlb(>lR)6}2`*Ot7yqyK#1yt%J3|v8ruy9?yyH z@4#iW-GzF|gpcO(uVn%f1D27w$g1{5J~L>{!)v?Ee}I=i{N7z?Bp-DJ^tdOy&auS- z0d_ubYD8q4Q&@dOBtoFUmfyGn_~_%Z5QAA)7rN{j8ROY>cWe9bf^J^Mk<@7 z5p0^emCGirQG3B#Is(|Z!5sK*pcjZm zApR^N!M}aRC}q%R;-qJMiEwdLkcVwUa5LI$OwKPY=~&@OJN4TbHGp3LYnvfWq>I8q zZubNs0{SrtbwhX`Cy~m!rabW)hz;6BxWR7|(_QfsGe?xacyBIf7$MG*W1%j+D58T( zq*Om%x8-pbkSlPaQ_Lx1vkd@ca=d$Ee?Vg=kGZfeZD|5Bq{y}4gG7o6tj4()bH480R7xPfVHQfHdL7{Vv zY*5ewd!47V_}jr8DAM-?l*wt?%;gR_sXZaRgjC-dew^*B2sVA~8_5L%P1joS2XHE( zE2QHSoQjod;M-!|qI z41q%RAt@l3i>NNDVk2D!9mpT3i*Y+K5!wJHCi~kVplVKUHT6{t1)wI*U8dqe9uHh7 zvk=O;9Ex|^Nq9p1nY#k^O=G zHHmB{As)RJ=t?|*3!ud?szcsHA8nm(UWkOcB<7<-2g9+(uG){Ysl3ki68dvGw< z@3spEeQg%I@A0-?|Nn)JhrW`4({4ZW}7LJ|hBq8E*5e2unP>CkK zx9`*s4c~Ti@Hh#I74jhJ7?u4<$V22v<>vYdoYvQ7;N=f14=v$k^)LxWM-g;0y>y?9 zd-)vB7)!uefKOxo5T1Uj;iTD^nr)#j_Mv`NN7dAnCVb{*2trOfB+e4M(M7|qr9s7a zKP}u5X=MSBWWlZ195cmgMH4+xdF1@Pnyw2xx{$<4x^1o`Dh3+i*_b2@|0X)Of^no{ zsqG2Xy6RVc2TenyE78d+@?XD&^Kos%LOHfwHgYox%hl3uQn~4Tta#w!?QmPnhcU%iTLG~n=Klm1Pn-A z!i&QDXrD{Rt~vaOKtGHC5fhPfL?|CKwZ;%#UW4)bok4AXRIhCT)oYchi=0nHhxI^f znS1AwDsJiJ8u(LE7TQLkA`?TQx}>bSB>83f!5DChV->(A{-U?4?S_D94qo43d2Xn}Cll#g@YH_CKzr1RUZlN<{ZP8FvlY=}Bv6l?Eyc59IVW#5n}1v*EE z{*StM4~O!5<9}xc&x~;xau}r=#Ec|E5tWQ_K97(QGDD6zjGXD|1)#VpZy>7`GEftoD zUr43nT9ys}v>Gf##@zY%iWE9vuk7z8(zLZmRKlc^B*q9C2K3j zmr{(=*+ z5^@wL`*CYFd*4=aLrmzEj5l6o(X6E(-LyB19$6mzX<8@9#K?Wgat(90@mQgean-uh zhmYK4O;5=Y#t0N?|E<}MQ+lIIb>uUeN9)I>g5SNh@p0Zn)@rla&E(Jz=;u~d-IXHA z)ce}uaK>4U&WR7D>$NPMq7p>ARb9Twt;hRnn7{sHXB{Hw&|}to!D+v-4xAik8`7As z;$&_WaJ&eH6cgr()vH(EprG{=^V-7}t1VUwaJZY1j*YmYRaLBGZg+Jk7}g}ikhx2> z_EWkZ9~o?uCVeQmG)(F;&HgNDjf}f{ZvWj z!basg9Q_xipxSBgINpW0TaXxQ$8yB8^Nztzm~vQwWs{c^_!~1ERqfyS`ZV480tFRWDALYr#hIu-D^oF>faR^H%&0 zF7*9}kCUI^LO)M^`T93#)8D@Tn3;7_d7u9C7xOn^SytC7O6&FG!f+l-M(3}L$;|a6 zs$+(!)gL*W95u5Ai*9{r?Xfvl+$Uwx*`0^JiZ4dK z>DmZYs2t?1o{E$0**34TR2YB-Zu+#t$)c2p@g-7~sWRKJ?K?8Hd^VV6N^6X{IZS7fFI7Rw zaIa$((Vt&9o>S{XGd~bq80kvEyN?=?@|NTM@;$ZuR7F&;QOej5&X(#xwJiv4D&%_R z3yhTuw`?rwiQ&r~;84?3m)As7Q>YP{HFBX@tr5p%U&ljMdU{q_rdXY{I5g7NW_itpe1)RA6+$ zhmzE~j3BSf+``q|;@22P<#C1`Ap>41CKYsvP-w`d)55oR}@w2k!~uXi4Tx zg_|a$9g}IHyyy3fQaCID&4H0~o=gp|a;h?|D>aH_&eW%vXpKg&oF>Qo zNAN|b#&uRjvpl%=W+r1EGjXj3>bC|xdGRz(|JJ8@wltsAz*4@DUz;ozZjo~3L^fQ# z_#s4;RO?-GzrQ{uvC5XSIYzBZEsEMacrROt70l|haW6E_^wrGKs`7V?G?VXOIoD>L zR4*`RZP2%gT_3^X89dF_-so7&A28$UR8g;8P}8DN+w%NK<_xM)%7%EuQ5~Kk{d#>QaM1el4HTJ`W#nNyBTa)aJz!@yt|~PvB$-O*@svbvUgQfTyn8 zSGYx|Dp9;4Sv$+Fr@>1h_Nl#yS;R+u5YGqL%G&7HNPG|ZqB`|+1E~hv{X7Y-TzX)PKE;Y=$(3*Hud!k|T3xrGlk+#!dOy8)PQIbahkNuU zpK8zGw7NgK?rz_+o@(n&qBL5~ZWAub}y4&f13o5-vXCh(Xh zfm+mVq;<$uktxF#SCF=hJ8ZAV4v`aNZ2Q8OEi?l#6lU$ zOhFmSPC;2xGL)e#6_llCu%XPx7Rpev3d$g1h3Mi6Wp3^sP{#3G4`p87Q0BA27s^l~ zEB?}ii$9I3W`kocmx;}>jaWu?FDpnAaM!Tpg*aE*x>1$$N-FDUS|~gVRxnqh1_(4Y zgXTa@L@(Hyy}HX7wMrY)WCb+KN>eI&hmvbs(L0OP32pRqnZ`^Lw7blT|MRiBq@?WC z=5>3FRYxN^!8R6+cIGrT&fl$hmw5;)L0nOg{7Mu*8jrg%*tT>gj+QKUWKa zUCrHTjscT7a+@P!YMC)1eYki@9F5v`EHF7DHDXh)Fr;TRqdqjUki~F!lxvu9@kz$z z*>5$ijcKLy-_71l3&&BeA1quH+Zr2-3z!TM#kTU)W5)Y}W;Qc}m#q>qqPfiOV}Xq; zn^&$Z$nI6=&MZ}xzQ&+(6Rw_Yh(>j)Pp0T4&_Wq9cy%(m_GWuEGZsQv*n9XTI|Hc^9!!To+d ztv&6&xoYBkd{}Pq9y|*lao0E3t~L{2aB<9-w)L)Wb_bDzU(+~dY@S&XnD8l;ijura zV>!5wzuO($$Cm4wf02n3}JF{|?q!+d4SIe+O=8&2S@V zfxM8g$Y{{wwkD-!fVLxRSAHRA2M!%ADLVlgx~z-n>tKb`e_eZzu7G#|k6!7Osg-y< z_H5uKT;ABkyN{n)Xbt)AA9(z4OMwI7yfj9VhYFQYvn+xNwR9N9_R@ZR(>UB8m)}6L{ z_aB4yY+&fsYta7lesb#Xezf&{_8$p>gc@Z8H4Zckv>Cb-ttFNM*?07}n4vN3Z)&lQ zZ7=Y!?s0Ns`CxufS0Fz$G8!}ix|U2h4{DO4BPF1fSE0rLt>LfcTdknAcRzmG{{pmE zumAJ@lVC~R^w(nCPwd}H@ggg{v^plMpaiE@p=xUBL0cIxO;=l?f=XKlSI-Uppaq45 zZ;1m9PMF&>y|}7ipR^Hnw6p^1tv++Et`W|8<$6n7C!F)aoH@*$6U+g$$j!P{<;d#^(8M1;eVP7|?yZsbb3GALgc7xd zOAJgvvtT*6du@Q@HbPf}7MGN^EfX~8>4Qf=D*+!UGp-cZUA~6ij-118g9m{01ew|> z9QFp8+Sli3>)Xt)fAK^*y7?z)ZEPNOA+&=weVMTtw9y)tEwV0XBX4d1FBG(>xWu#_ zpylN6J9uo)rYcX>T&M@_uWL73J3zbtR=$x@@0 z79g&(adcY`2l@tvM8<)Zl#!LU?=Wbk6{yQVt7}AE23lL^!`}WO&_*ZTf1W4m93LIG>sQGFgQFK?ms>mnFcBm?>TU!^aN<9 zYU`S=-2zQ`??F!=XfHtd_lfJpT7<)VU_iWF~vFSh3a+R2p zI_$qtN8iX4_FrS|;N}JU`vrw=5hQ|^x;-nm0JMDvkx_$IedauJA<%BMws$=Q4Yknd z8_>ibr>1BAnZEGVQX&WsO%!*QJT-W#}}fx`rcq7uu9EwS;5=_BLQDd`HIvCJIkgWbq?4nhAw z{H#6={Zn(kuD%JhE6q1=3qfn|ypP-mw0>|O&?dlrs?gT-kDtFK1VvQ8V$EFWf|ElO z)q;~Rp)WOL{$InWi@PUi-o6`ANQVnUVLfgu9EZaCjx5m7XoE%?(2kXsSDpmzOznkB zjiAAB(|Q*)K{C6E_$RiMntg%_E%A#zmNv|>*(n( zGlKnLv{`2j8X9ffIG}lL@Cyh6EjSb!479|Q^zE6TjVia`TkIC~DX zi}g)cnn6Q7+1?2n^kn}aXb=d+pv?us#P8{q(38r4l;I|n=U#9Va~p6Ia~uC3Y4hgI zo3@;?oQEl?sHgK9vwtnbXmIIs9SL!Lt|RCDfy+btq8D8H+y-3w+{XW!hIU_z{Xgl- z`N!wF5>7*zr>3C^r-7a{Snkw2ZHamky$=|~pesRgn(Ioqy1A}|!{)jYw6wXd1Z}P> zL7VGJ(9X?uC1`V93EEs&f;Km%iLKD)Tvw``5U1DwQBhG*MSUv%?|ecP?UwjQ6<(+v zdl0Y{W>5*tZtS2EN92~?zHkC?%P`QuEt7W1SoR>d{2RS*!RGuDBs2>wul#_vPJ&;m zR*FxEQSBJ=OfW4N2A@Fxz(nvBnBCk&a0(Wc{|3*jyeLo}Qv0K(CQcWptEpis>aY{? zPSl}L!Q4C$DnQ<8>i`<^PUM-O1q30_1P#Jj3TVhXk!OO2g4xj$&>)!AfQGyic_wIR z{?*Y18uHHP1E3-AM4ky68l=C?m>v2FVf;Lgn4-N3y3f<3^!`wkQV zAx{82it+^W(-RU)n<2S7+106(F`0T4CC zHy|VrfIolz^BY>^-#-$(G-}Iz1oSn=(6|_pBnu?V$twaW=-Y>ZDkyaTq-xAx0MuN# zXt9TfLnTdGOIybSy_V?e(SZ#8r3S!dhRcoAUnqGnDQNFu3Ow)&$H7Ctz}0KkS_0Qu zv8;h?8(TXdJn9RC$9;kB9-Q?+FK?d>KzQ;O=pPUm1mtarP5`23hW7#YqpkuTL0ttb zDJ!o8R-s%D5IP759Rx%jbQ$>9m8&;_ki`MEqAU*Z;iKNCz-P|~M8Lrp!>@p--CqMC zRRetY{>xV&YUl6nc_%7ne{y6(L$MYLL90qj>&TISP%R2c7NzfxkmZz=f%8z40#So% z0#Sp~v@abwqNStTbN~MRSPGqKhIoIK`RX-5OFKs=pfisj3=G+P^4xV<;r+TBZQ2Y* zvt28NAcHZ`3w50WP!V+#KsAL@B}vyoC}El$=?RtzGG)5@ZjO2y(SA8 zjAwmQItdBo!=pMB>Jsp-RhFS)$%tE`2VayACmmX?o2g3y0of)7M7Bu=q8n5Mf^h=D zIDufCKrl`qO-p;OVqJ8}@E#sD2S%Vv21cVx2Es$<@PY@=f$;D-5JiQZz|5@d93V<0 z<^fT3*b9U_BCrS<9T1ET2)RTc3qVLG0vk{|5r~ZN8W5!v zZvw#>fnbcl_Ktgqfs4-6iQDfEuQDa^NLT(laxmh4;%w`~J%v(Un&jL}Xy$6KsED*A@ zKon}90#T^#2SSDx2pQVqd0fq{|41kk=!HThHZk#IVkz#A;LjiQK+eP;iVOxW4E6~G z`vg)@^a6T%`D_6CayR+|0|I$`Abj`%7#1EGH9zmjkuA}ZA_;_N^nA$wjS~~AR!fk@ z!zDoM0HUvypAQ7r1};Vx4@9Y4Js=%LW1sp)mz2f`;XfG8NR1EOHe2BKhW z4}{NM0O7M2Ko7Kp4TNY6glG&zwy;Sn(0M#4u@80qKgxItY9z1%88n^%Wl>N9DkxHv zfXe6&fbgaGKokQP0#OXq0&1f>0P3MT04_y$09=ml00_eg5QY;V3@1PsPJl3+0AV-* z!f*ma9pVXGkL~~nHvohi00yDp3WQIk08u6&3b+M@3gF!5Q(yyy3IKdg1^9nVAhNRI zJv?&&{NE=MC{)1v11MAg(PIb4fbiG>u&lh|zmek9e>Yc>^E}XFxP<_z6VQhJSNV z3;mHaIygAgF#FL9&1UQ=$n?Q*$*6;YC@`x4RZ;%}p?`s3pg=HCAQ&hR3={|k3IqcM zf`P&d*{2oI8g(qt&cVeMh%zf2peO22AVeo1L?Lf06$Z3G9t}kO&H_Tm1L3n^K=?cu5DWn5i98wzlNKOMT7WQV0m7sO z2$L2dOj>|2X#v8d1qhQCAWT|-Flhn8paq0M3kZW25C$zE3|jDlK??|j77zw4APia# z4)nWEL_Cp5gnY*V;s13mS^@R8e;_ScNU);ggi?;NRQXORishI3;+^QPFx+F+V#7wE z6!v+5Am?ViiGILDPfs1PrO!{H;yMRjPtd^32|KtG?&r%g-Q#$ za4a9=EWCUK$Nb?#97ZEfy}_P76t&_qTMm%!Zox_7sdeZj5%)9$tMUch3PcuzG6NIi z(XNIPU?w_#ADXJ8<3)=$?=!hma{%`p_iwPc4rku}t0VvOavx! zx)4X~z>RljWHA(B*5f~dYFM!X&F)YQE6}U8wkXvYk>|*yenBs|0w7#L_f5?&tPS|v zd+KI*(ij#&#^iNAx`zssuz9H&DjNmrm`3J8Y;jq4S$Dop5#4T}yt~8zGpzbfzdT}$ zt*BjvS?sAZKh$7}Sv8~MZrfmXtxlK=I#Q?01M}?h*;6M@$9(<#M?nq3_~KA39Mq{T zSjUT0p6m6}&z zm3AK%#N8U66aBFpRY4VO+rOY;vVK6M$=)?4WbP`V){%&^Nj+7Xk(TswyOL=hPUGy z%#ve`rH}Xw*l-;%Cl~m?-pvC|bPPVGSEBX^%(q1Q^NUb(M24ZY$t^K)@u)`khNR}S zkBF_?yRxtxZ&Ybd9#$aQ_n_=S26kZdFsiUzjM`!fwZ*aTHJ4{lTSy9S)S08UAl93g zBwwDTT*1O_->ADry@^2z?lDb>-Rh*pFaKfl>Z1?DwCN?=5@w5HM-13e{RYK&GLOgC_#{9%_qX3!Hj5IA5YS}~eF5;(9*j2gnK7oFPL#?GO)thbElLh%4n zy=cBbhCee1h*Ij|k?VTO9g$(g#YehFx^t6wu$Ji_fy^Q+C-)oL2CFzAg;xERFb`u# zi%VgRs;r{2`qUX%qpCTF%mEgn8ZTeDHkV$z*>XpCw?q6ma$P48Splr6Jn4PbH!!!> zIW&y!9r|Sg-TNeHpTGS5?Z@oxyW;d;zyC;mC4h+T9Q_*Cbj$opiY4iRa2VKJ+=~7_ zE9!&V1N1VAPxfz8niHO|bZb#JPsK>N!#v_5p$H&ZG?3wuhHuyXv z(VvBSexzcc+;JS6;Jm{=MBRRWh5H5jI~wiB`+?0y{Ql_V;2}sOB>D?HQZbB_Dc@86 zJOfkAher7LIBK0n3F`Q1OMyq#Tq|5bwHY)UVaC&Mue@zF?Xtkuq@yBB5wwB_s@6g5 zGKCoJG25XG(2((>ggC-92TFDG%!uSW}b#+Y)4Jk;fUNIH%}!% z5W?Gw?Ta`#P(GM}9Tp%LpkEv5NJVatd8)~*={t5N|6I}SJ(tYuu=)}d4KAVTs!%j& zagVvZrvbS@YbR30SoedTCs=RDNB0;}A2u+WfodMYhQ(vpcn7L_>Mize<~yo+68kLq z8~cVj;x8hK2*3XzZ+SrjKOl`f8ki=_J0dr@hKxp4U1I?@s?m(?VF$#%gLmA#v$QfpO%U_laN&1L`7`V}#H%xqd9J}r<6NSXY z070BNvIo|W&hnQT*sSAk44S|%YOV=l(M_SBs8d2`e4MI^p(Y6EnSNU|*97Rc6Khc% zFfGLXZXEnBiFDmYDPr zX`CZ!fh1Himcm0VfC?&iW~Jt0d2YC|^n$%b2c}zOaV5|IxKf<3w4s7ez^OD>x1N#3 zx#7Cb^;DYUv+6{R*ySrYp)^i>9fz)dss%&wK!}`~*oh3`5h}vdkW=)bE1i!sM|LoR zN`zya3GD4Vh$tT>QAmJ@g2KYD-#z77oqvL%0YHP_=u-l19+&_$;ljmW0R-W+rM`h7 zoR5lzNYhp5d`p>g7J!Nfj&>NjeN=?wp#joMSWXc7r21k01hkcp21xY2yad@n?G_BQ zm}b-rpmn8SX`qQvU=ZibX}L26*j_1NxfJmLcCZv}P>*88j50-BB92*Uj(ddXpJLS* z6SG!3Uf|R(#J$CFFY|C{ziSaUvrs3&IC-}tt0Ttn@o?(3hs3dE!8HNT_ zk~ZFhd;*n-KTL|hV5p!H1xd*-ce^*~Xn5F2z@r8Hxgd$^RADM&O7m1voyv|H7>bgp z2+xD~fJ(CTQ^7A#N!p4c6eUrKc#UPPHHHc*(df|Xf(p_xG)jt4fjKybKh!yeS|ubD zm8cLSF*Ht!(^ukY30P7JD#=Ql3zVoNEr){!N>n1=i=qUE3MvmDEzT@M1?dVU?fyA;fpoZ4(;|>X3Ubz^a#YE_C=23C^MEm zf(#`v2o*f$gwk)smfW=cvC9>rCrWe|6I6H4b%a;SN-d>0&TZ`M-08{+`;aE-LF zo_R5=Y#!Rpu`(U)#5u*S!Op?)XmZhb8R8e3T%dpo@eBD+XE&-4CRWX|s5iiUU~=K! z^WT{j90#+D&v4w_?BbU@|4sVuKkWZua)GR=QW3e0IwQ-R!CinYj4(%AT9{5M9jio} z`uX5D=qxavKrF3*`L?x9A|VZ8smP51_J}5zQz(?8M=xMpMVBB?nX?hNPMBRLz_lc$ zAPa$Off-faTzU-6s16GyysWJIPy)tCx9ULU@Cp%CQ6(K zrBwLxlZ1jNdMC3~EdFVC7f^hXZ*jxMHWU5|r9}ofB;cO>aVzu=PNF2oB{E-nzean+ zB`U`a4>i7sQ^92>EGnwJ9X_LZz?kV0EO7`6`DbOeEG_s5J3fIH5!)8C!G)(7!%B{5 zotGs|TaM#w{^K?#&GHcZWf!)2+Y;yY*5tlB3CfQSY=4P=vN_DBtxav~@$WyGjMKbkNoa7#(R^5+hCZc)(YVx5R;1&# zac1@5T=a)GJV%BC>~sURwFA^s4LDEfdKuIL=xU+mekd@*c&4LirpH zZqQhjQ^oX{BFCj_NbGoj^{GU?`1# z_ptrkwfJOQ6(vtDPAZ8SD+n8H91=FN1guC>laQBRgeejIIcyy3Q(pFdnw~(5svDSa zKP1d)ljgO@J$ji`Z^w=AY?KNc#CpBzn%7dP>73?3HF-mhGclgqwn3@Ezf122zUQBK zy(GN|EH+$G&qHz17i7tyA&FXgk<5fuU!pQKY?te0%=6i-XNQ&Uuw6D!D}^R~Q(8-j zd0Em*NYOGeU1hdHE6T(oPRqg0Hcrx>;xff@@v${wdfTp8p-aKfGP#Z`jFo@wIjExj zI4Z;Qm$~vWTc$wEHSF-hVs#tuD7WgqxMP!s^P-cCb_!OlH+Z)<&Vp^FW${CCa8%#U zb-BqZ6Eoijago6(lOc@}Qthbgl_v7b15H=bR&qMD zx>uM5Wm*d-Ulbjj0`+>p8a!Z}>( zG6Q|-)a2NYfp~%xTOtu(3r5RDPZw!G!UmExXjM>WYrroFBoQHj2+2d{=#n^B39L+` zHQi4|Bd((>I8T|71F0xYEP~x9iHTrIs6O`PpGv>`jbJ zL5W|$hrVnWy%S=~>`!HF+JY(Gg>KD3NXN{U~@WTxVbMhkMn>!L0gjWKPKBNBU09sI_$ zG9p*0&TAa}m*Q!mrIpWr-}WHr4G&keKFYAC@~Q9J<@VV^`IY-PRWVm_PGnD8iuQTe ztukkqEBgrpwk41`_>sJ80UwVgBu*u}l%?RUDnjgXwS+NTi+l*aW#8S?clbhuw$NTc|wsIyrScyFA z2eEikd|2FfyD0gscj6XXT_!O8PF!x4!CSKS=KL*@!7)79HFxqu_pb+V%h!$0kK?I**GpbQ(+lQD?R%{iClCqJtcB4%^b2lz2UieD z+n5g0-up7iyu*rWB(LC@Q2p#P(x+P&2{*?se$jOFV7)HQ-R=u+>9QuAF)k}XnPBOY z^T77PDz67C*RR&TbVXHcO?KI{zMNoRq{B*zykf+c!DSa2g}W4L(E_^ zH*amo)*u9KZe725Q4DiWS4`9g9YbNj&4{SD1KV!)W!}3E4F3<*~s52|XwD%mR718|pYYS&c-d_7U{IKb^^;weRd9v>rsy z^Mx~$4|~bt<>*dDG9~mnU-tcR->?MPC@L;SmgdeZh*ww|yX&8a^Sv6-x7D0HnBufR zHZ@>D-$r*W{r{8=hNQ0~Q{JmQE0tTq?+TNdPu1UHVxu3W*NUAv>hHx1lQ!3nk7)I~ z%{D%4nrgr+J*jv4ac#e+IFD_r&3S z?NiJ%6i@16qZCaW>-;mD7hF7W&@Dwv|B8pxt}{&F>X;#)LHdNj^J{SEJ=FLWQ++nKr+i5~dTAej}?*=^1UXjgcD~xqa>O{cSfoY);p-rh>Bbn5cSyV* z_SyDj3a%RaRif8tJv;Z>o2?ol+`|kFvki@z`qa_sWSbCnKFM&6{wvDDb-FhS*<@Cc zu+OR2toHDo#si1g%%JLyQ-S|(t}}3P*p+H@J0&h3Jxu_76Po z%V5i8Qog02ACc*<%aUQA@T}yJJ&wBD9dj#o4U`Ero+Q5-3DafdeO_!g)E5*;^S;cY z9c;^CGe_gjR7)Mq=^HH${qe@i;SrtYnd+r(JuqoY>h)lF)StYa^*OEISKr(zk9zQB zE2}=Mzl+*?_$A9F?x4&>BcIh^wCf+&k!mZ86V=j3@lG!f&)`<)zrEt1OF1alJ|n2P zp4mPV&)R0A*gGMW_Q108zNO8v*RMZGIxUOuS=t`ywmjOkprpexb)V>&l{Jb})dMNp z4ca8-*Q`&FDv5WhNk?S}{`j?)Su18nzKIk$v12cJU8k^Osq2`gibKg}&xJQDxw#FJ z5vguT+{EA=DM2s(!~*n9lrJmihimBT*_Ei4s@NZ&SEf9EN~2iKwp^)V&7a&HtGt)P zFNRnppPEfZG=>jc#|htlWzG^#=vOlC=oT-rKSDpMcYgDy=GVrIor8~p8qqhgmk{_T zQ|VY02KV*2SxfwY+o<$Oy8FAU!N>H7cPfTEbIkXN zEKlfEYICW9*F<@$uU7dQQWQRbnMS*g%Wb9>~T^Vu^g*-ePw~cgV5K4&6_?o{rNqfr;)1Z zI<$E}{|#MUL8@+1y$^#n5d8W;UKX8O&srtl<9q$hjKrts^#^&n!~JQO*V3&0?x{cC zz3DOC=j(3AN$nbK`uCc~eXC)7v&{*tQs#QpnOL${-7!K@^XtPi;k^eI+Vh(I-P!_T zSgLqt=4e>c{&CHJZR9x05gG2E&faO92q)3@pUh2K6C1}lP1g)jU@GJ@eGC4>O^wRc zX412amOa`mOd<%XPQD`-w@`m)$;KSV%IeFdE2N67b!Hz@d@^9Fi|j~KC&@>NA+uqVlDz84 zo~eh%)>Pk8Bh~9Sy5xI|17=TR_Qm)s!D;V;lC^9(>#6xGryD&^S~>8haMj$r&iKzY z_&a~6w-W5n+Z-D2&f0wQNCWHM*s&DSlp?KKntfuSlk%UJm6{$4j?Q5`&8up_1pdW&@UKHY%Cv1s5te6FSUH^ z3U|B70*|+zWD$FZohXOn5GSJJzE)|@r|ywF@hA!2+MZSGCvhMPJcWO2i`L!tvvfbD zE^?7Qc`kDB)%pe9GFNPCM0fvq)^nfP4ZUES-wD)7kor-{mLu7yIv$_+$N0zZYu|98 z8f~9^?GN5mbrfz8xt$Pxo|L4SoR&Rie3odm-d<#H{^pHVkA*o~#NnL0OIcMq-CUGL z>mClA2?*-FQUlrw5IZ zT9;VGR#;@eC*Nj|ZQ=ML{dlQ#e3LxwK&e@~Tzf=~#%xW|q6e4STAFS4QmA%AS<5XV zZ^|DEVlPfSI_#*Fva8zD;lAB;eX?w2NqRZ&gB^8UWOIHXHvCRXK}yS+K{qS;F*eVs zD?rJC%l;xv8+%Mw4>TVvUh2PUA)(~;@S8J@@0Q8ePY`TM@fih!PLT&r4o+IzwZ6M% z%pQ^=%THL^e6v+#XR643(RK{iT0L8?5n?B(ypZ5NWwnnUFZ#z}->XwMpSkzj%&cYi zFSrvuZuO)it|dQnVcy9QJBl4m%4m;q@CF4=-Ltz5t1Q}PC6xD@RBTf)zEMTIRA620 z0Q-&JS<(_asm1H|#b*k8aqUE{FIYW<+do4KbUJKbC} zgff3ueU*Aa`z)pOYFEdBPSp;NeO*F-U1vexiN%$RijQih$jmY;7ZlGAEYWZm1>bPh zTCB2c(pzjxqB-#Ib>*@RXoAI$vcx{y>66$@@M*`hkLx`T5ET25rKu%pB)PFTo^I^@ z&lY(T%NOm`JbWA98#EgFs6PHbgZWm{yuX@LZI4My6>2nWOh`U=Nj0M+x7+DSilyl- zd;=}_bhAfHQgX5Ek`=`YskOOE8gFt@C|I_igpw)ju_SItr;1* z962CYBszJv#HOY`>SVyA#|BaVKo7@VX_>w;X^RJDZyT}H?xF3>+g(usZ{nZMiluW$ zZ#B1nxfWovMOoD@CW(A}Nx|t)cL{$MTz?itc*_ zq3XM0e9UH|>cVpk>S+_&+1FkM9VK?DSLfWSzvcYgLmc%garos~B39qF+=kgz>}9WE zs9;`Te(7fYGK~b=L$<-*hkW^@q1StD;@rE!+p5h5@3YB=Y)Q5+)A-#7mU6|5Y+nwi zwO8)Idz#68@?Pt(PPFs%I{o37Q7yJ|4oNneF{N$n;H?UdHXrYq>S}1WvZGE}Xvo#N zE(s=@Jmn1gU3pzzo_(tRAjS5RDrXb9y&eCu-@dHhWg?_%#8bxjAA(Q6Gl?EZaQSz6 zTP7y`W|rsKw#&%5D9`5GbVgWFvy-oGHy*cKae+hCTN}1YOK&y%UJY5*&fI=O4Dof^ zFEd{@Rh;;$5F52&vele7(iV-6={&vVah+YupB~Ab;1kTBo|za_I#(P^RH1N}Fj z1TtH;?zlE4<}`0L9>X{LaMbOGHwAs^Q5}&f=;}O6Ja(^EX87~|Wlj9g)oeM;lt*m1 zQa}4HkabOs;@x^w%Ddc?dT!5{KeZw(L4z_wla9PI8K?C@UT}o8H4AIJJKz5KRy!(* zzkXlrvglM@?nfK_gWpoKHp^1{@)w&Y4o(eCXM3;= zr2o!V7DpZ`m5V zjJ%QxEa%I4k9$ya3c`inwvXdx?THF^53F(KbbZ^o(5Tz)%ePsHahDS_%A=)AgIeP- zIfGYwtz2{8wtx^&xaYO&eo6S)_o^oPj}|_Ccsu$&mc}koNOP`(#Sae=Mi4HFdg^%P zYucrsLDy#@Yn5t@IL+(5TOH2xCT5A^WXl-Z!kQziI>=dGXIZKm%XEhn7taPM$p{u( z#b-arU2IqE_{7z~@UG0lZ9iVR&DYQ=H62Ra&{nJD;Jqp9=UI7&ui45W=g6DvIP$mT zA~r6*GWcopN?uxS*F5>~SXF5+uE_ph!Od^KtQ@l9TK+viboV31@2bhnOQEO4H`l9OlDzs|*K=6uuYIIWzf+)SEqG)8m3-%?q!{Obv+zAG zy6C*yT{taYAHi=3AM9;CO5t&+*R2$(vTHSr0;n;hx?iiU_V@)J$*&#BJ8vJUK_@y0 z@|WVu8m@QR5tLdn(YKaIbn7(c3$AbQnj8;XC#Lz>5Ks7f!N^aEC2wM_->b=HS1pca zu_LisPvw&&|6^LEL-+60Z=(Hsk|Q0Con^)c@9o|0TOl<#C7HzyKQ(u?_R(cS?#tbDOwpI(tYxdYXjE{6y_S+x z<0M<_)HR(~%dQPPawA7<7E$3L9FA~tT(s8LkK?ZrRq1y5XLxI0T%_DP+{Q28oi2v9 z@_u_5%Xc+Bu%m>yaeR6F&eKwci|*+Ep@|mg#S3bAm?wiTv(sU_{LhlE$nzf7HmCM2 z^{}Rr?7B_A5H@WjW0M=`t-aVtG2e*?9-z~j2-rUG?C8v-YPrp~2qJl$a;t>KDd)2m!g ziSx#k!E%S{pT4FlPNEl6G8${oY~iYuNJw(b)G;U3pOV0)&Bvls-$ipas2j_0 zOo>t3UfrceoSm@?b)WW^ri)lvLDhqDY4`i2&iOWMF0T-F8uzmoy&jd1rcQO_S*ZV% zkVLzu>!$|krj848OLKOe%k$>hIe!_D8QJck!6h26yraBB?;Ke(xl7H$;+_rfMMhTB z6xrPAfDd(akLT8;27edxjQi7d`M#xn*5|wxgjI}UjZSWRtKu$3!?|z^jh~WXj?~ZU zK#zuy>pxmB+F;N`QkT6)p{D*~SK_Gm&(0BP?auk!=1`}nA%V8eFKw5OPL)-rTE@l4 z5IyOS4!=#*4%J^^UFljWPV@2GSM9UiDE(mdLQV9QnL)dnuh2D;YH!|GTIkj6kU3@{ z7@zg!Ynp$f`+0@N?-o^$UW{&#`o&r3+3QizGd1Q=bRplp$j-xJo%TZE!k?|aelmX5 zV!9tn-;?`<+uo_s?A(u#juP!ZDwQP@+*3<;{;g>#z7AMZjo&QYz`&h+A*}#{>Zd6y_c~(x_^3j+3_S~{w!0B8_kGGf|=#kDp zDDR*xQNWq3B7VA1tP-f*NQ&FL^QBjj#`dCOYkO01WO3+{E&8I~tdf5tMEw00PDPB0 z@JjReibg>reOSpTz0$Q~&~ngN*>}6NZdc}{Qy@1hUYF5G>e~26keThta{fH5`!j;% zceR6YKE*JT`udi^Q*A!YDNdCh*!}!$CL>TOezzs{Z~0u3RH2%87O%a* zFTB3d4mEm>ylV5mAbV_M9q~{+Tl_H0A;w|km88kzHg4S`TUKcmgNUu&|cIFf#ag zFRRcy!ll+kn3}6@>br2+wQgktk>eK|b*^h=tsE~j?Vzo)!LdKKzNts|wM?W34Hl3u z__U=SjjNLQS{jzD>G5)s(48tok&ju;lScd!yERGzT3u&9>96`~H;OyAWnz(OrmlWn z+?nMUGW2qkMl*YS?B(regY=iPHb(cQb9>Sp(wI`ATZHTjiv#XSsHFdmqHB+5>i_?C zHrs4ta~*OmZIm`XxvUbqZB|=w zIC?(E74D9;vrOaYblR|(2djLoASf;eU#%!L2G_c~9I)>mt9UdNfsosu#h8QcRM%f% zshovC=8}ee8lRfwe6;vPuh2R`%Ie*o{tp@~E})F1qkZhAn8cHCKXdk~OLSsyFOZu_ zN)HMSZ;3v;(TG|#lm%e5F9r+Oi9G(wSS!1A=i?Ub9WP&S)DTM??)~H$t@iY-m7miqR;FpqYBVtfl zxbr_@<^a`zGjLJeur@LUeNk}Tz_d5SznfE^jneMkguCjCTsJFo6zXvQ<&6A6=37|A z(x%kG7uKX}AMk7T9HrGzpuL)2x6QfbK#=jeTrZU&oc0SeOTAEKyapSK_0wF{D@^R6 ze~n1ho>nwmC}uCPgIXKujsl!9?N6NeosGNd1+Am1*kJtM_zTQDu<#=MhhNG5oVDMM zt#S~clnQ0fIq7jpV9NYj>A<{5OPn#g%DhftET;F?b+K>m_=a)%8abIVDy@NC?2>0UIhi$TU-a&f-t zvF%`jAW141MCi66@T(C<$=OWZ-^Y_q1t!J6e#Od9^aou%$Np?ZI-B*6jao8lE^RQrVK@* zH0lYI6>YM__QM3%716z7gLkw%Ix;B zM=w@jV!l{=094QIw(x$BsQ_6f!@YZ48NoRjd=U|<4`7$}ZWiVTf3~iZO)NS@l9Iug zROS?vU%rQ~w)v~>JA%NDv1D1cxA6q#9N#I%%9I-KC#Mx;8dj77KMTlvYVq@?n2o>% ze4t0oSV5@UyJTcgWyP^qc zIPhk|GV{X)0Q37gHz|6~j*d9ov0UyF-3qA3n3WN_e=|Cag?s*JPQ8!pu_9qISF@5D zvn3lJax(E_kwwy^v0JW2H&=P(3;q~|8|3Kgtwl#LYPuCGc>!J*morsvYebhDcA_u( zVaVwhQxBk-95b0E4?kTTobn6grjD#bnX~(X41g{1T^f=US4551Dtsk9h>F(#&#g_u z<81G8;mAK?oO{nDbf&^ZJxNC*GB3KQR`-f_TF@ncmtG^x#qwLWgGid0Atq>GQ?mSh zcW!Gx27%t9Z0$tsAvNl{yxqxWh3>Z{yb{Cc#9 zlKdzs@0X=kU21-jh3f}WRjEE?@?OaziJG5S;;6{N8*F$|P%LlS<4Crr7s@*wp;$8W zb!_j^&8mn{lIm#pvEIVx9?7JFqCam$wJT$?+f!NhB{;)*UGp0#^_IfnwX2MJiqA$Z zBF~gsxXu8yA>X3YD_AEFHeE6D^(zWGc8~jze^=+WQSHDCaM*LRzPfJU$CmY*zXxIu zIa(h$?PHpo+JVk;5H1nGPtNqF05t8au-*wn(5utVj9=3A6$ugNxaL$rkRIl~Rt+N1fHn>@y&BX{n z5>-6se%M`!7K!Bv99+i19YGVbx00 zNJy~$<)Kh*(%`$f%nuQrx*f7F%V*1!{X})IX2y>YgYUM-UMl9)R^(m^PR|1GbW3xk zyvt%Ri<&e+;0g#6b*X$FNwg@3vI-pb|EIpll~+@;w3Prgz~`CsGtS<)*RR$xb++gq z-Mt-q!^)_4MMuxX%vD#+3g_9A9nFFdJc_|Q@>&Fy;Um6^{wpvaOLeh_nSWq+PRiU$VsYv8Rt!52LP(W)X& zYI%(tslh2!@f`z_hCkE;pMck_JI6${?2wH_2+;$8L)J=!(B@uy=0r3 zW(70H4)DjFQ>Q6{*R&1fJ%D`utS!|eNlTl8q5l~mSD!c8PoLV7={02%b!-Yu?6x?a zddW&2N|FiKZgS))%jHWDjm)fD36dP{$xElaCeD(Ad!+r}Nmq47NChKV$0(Y_@y@+}qHH%b(_O+(1`S|%x(zGVG3#X$( z3eWwcx#8-JGB^*tEA02UvFJ=_vIb}mR#*-H0u3t3{kz_3Um`B*%piG(hi);VtDwRI z9L0{LJNbHKMwW6c(f9a|M zY!#IIUF_w>al2X|6xPPq+N6iUU?NE(^$7JSwX;FhnS>{5x~V z=u0Cp?RQ?%6B1B&1=W1i;zH|=_TSb4d}(GLb8Oqb=d=-@6Vw%EuQ{;8K;o^~T}!qM zeiX{x{>plL%yasO9aiyC5DVpv{~S7Yu-%_g_9_(}%$_Sck*9BxLqK(@>oFY2e!ATE zSXoxrMJg~LuY$RYACXEifO<#LNdI4JB3>#Xvw#`i@1RgSbb8YFzUP%?%u>O%JEONL zwh`o5JXe&9(XG?+yt)(tgR`g&D%msl0mDknf>Y+_qI_4I}6c`hb1A$GFQ|LYK9vp5#CnM zyqup}bM-p=a~!miv~lM-ACrH+A23D)XN;IZk57Zi0!}sJIFVVVeNw0MFPsBqKgO*~ zbkj$1xbrs<>k+w_FS|pJlAe*3vKH>PK>^o1!FOOkK7{8gwhEpWs(cc{|F zz-UE7N5o|fw;zDVwTY{D$7R>pc!9lV8p3c--AfNDq4P;i^gI74f5N;^{^!5&l2qm+ zbIh?K8&HtOi_yk-Z36T91&y)~ujXQoT+Iu8ULKSZ!|_jV)B{t7U)Q&M$4EiK$d}eT z)g2!;0MxLcTgio)243NBl&1N0UCp*$6RD7813ZwRsGa=QG4CoHG_J!lPT{Jt+=46k z^8DjBe}LBvEA^^$YjkS0fiM#s;eC9*G|&D+>ZQY-67pTk5tCJ>wak+iQTU_A@>^i^ zyaYUoL#2~M|5Mk0QV7xc0dEUNWk@jde_)2SZN%LBAsq#ht@*@0V9M6J>Eo2oD9nR7 zMLRPU7 z-zN$CNOL{mxanV$aRV38#Md4Upq+SQE0ehBZq@mD5{5YKQs)d%w*fkTa2{4Xmrq+S zQm0e2QqAcdF1R+!qd-FTiB|>ZbyPs$b!1SHkMuNTxC0x6ay#A`7jeywZzBmzY;d{O z^shVW$CWOWKlhcMH_q&pH!42Tb&9dx9(!HeaP0^Jzgyp9m_PjOSId-ksK;x94Av%@ zU|PPU)(0tSNb60cDUiX-R-CRKr#QSDy{&n71DELge zag8B!kn#RZt?#WhC91Em2%cl62IH+KcD}gt=y&BXm_nUf0zl-#Xi8i}}O+#)-40KsW9_bK=p)J2VH1Umz|6Fv{^I{qLf_ z*#xl@5I@OjI7WJz>vEiG{5~bV;D%~7j%|?e3J};!#EOGbk=I(*G6i^#XI!y47`7d1 zDJsEinW%Ghe7k&w9X6!k&Kdo1NKD&R0kGO)6Ur~m>}QXE_&6@VIF@j21}z`q z26bA-Yr?HuEuFn8wg~IT`TrOd$FCcTI(hsW&w_I~ytE6D|78okU_=n@WPHsF6AHYx zb6uu{jw%!)21frp3uU0ak^I}JR)Zl0 zCu#eM!w+O3^x^A6x~b|K;q53io`v>pH8Ry#dLE3i!aP1aoakg^)rV|U92J{%$#d@U zaYKM)^>t(O+Ek)V_q^%#yi4f3ZlXgM5i>9&qJ|wH#%Kpnq}ta9x{1mpmYHCVDEii5 zL-Y-NoeoE}+gc}z{gl(WouDfHp72M-cYw6qMRzpwf5iYk9D@zFk*hzJ4cc&;R&URY%-u zHBtd>rpc%(>XyHG7|kwin#9~0J-Bnh`E_#$EdX^h5f4ObTPNK7v_Hr*HT$U5+mF9B zj7cdwP#f+G(KwO-^>U|Gz3_EZDx&$rhVn8X(y!|B@Zl|K{;OwuO9}Xcz6e;56&2wg zP9b1iAGF*V5|F3+T%64bSm5gZAFCgyY9H$jZBJqj{CB9f2_X(7eeMANwperC*Al2k ziEbjAcKZC!=gmZF_c%hw_j+W=YqKkMX)dnzfA&uz!#5d`Ki%9Q^N+ zRv$4}5FJaH-Syir$;(Zt?!8VtWbFJye{J*T?H32~NFiM7{kZoF9X(B@@dDcKtrSH5 zXg8@6IQWv=5OxULimR4`iIN0N>b3p(Sx8ZG!ta=?0@-%;{jy-O#viTWxUo^sAqB)R zsmpkh!yOy#Vtc!tVUYq&n74O(Cg79jf8GeDeY`dJY%@JYGGUy^VN#?$bJkIa1PCq|( z@_Eqqu6-rkPxqnm_(FrAvYNZb8?j700-HY=K{JNG0M}EZ(|e1Oo0mJ`eDi`I!~ZNz zWp%0VHH^LI`ub;t^!@rcVm;6&t4RO6=5+LVE9}XxJdG?Z8IaSl&a6j}@g>m8NJDiD z{#;$Jq!Sc(4p8b7J(6WWHxO@->+pJdW#+wC`Ma}GD8J}{!Z4z>oN3`5^FDWgiwdB< z>bQAT^)4s;Pb#SZ=*yetyA9sxVr@>FJRVrtm+`1rkycz-_?Oauztf+EYvNgc?3X2Q zR%7&CKttEjci7!XO~>vj@ZCtNX~9g|Z!n==|KyB#xk^0n8}wImkTPWMn#wTztQ_Pg zAN&pSuw5wR1%4gp7H9=a3g=gWrBf%i5siBqPz6&r6m3*cY&Vy*bhuQ}mx)^Iunve9C1f1V{eDt=xM9VJ+wumVJ>$*QL`R_Njl^;UzS)zsZ z*1#gZu=wkU0lJl{6I4%hKbw>~8FB2X_3e?@ul%NrEa0aGoSA5E%$D@2R$#9kbd1CP zvSZE%Fac76ZK1oZm>RreYGd^wq8+vUuXrj`!|?DMP76gJl%97t6!q41n8}oW|je~ zvPABU-(p$XZ6LFaR7yRA#%3NkxlEOwv_GeBn4xQQNEe^3YkW}GvH$%dhXHnO49Wf>+%wHeRk{e6LbafK1h9}0di^i7(=8H5|1=SnjlS) zE0AVLbEE~5i}XZ#A-$12WY0#!ZZ;n&klXL%-}tS{rD|e7es9a!-v{!mHj3ZLD1ro% zEOuRO0Z{>bI{1BmmiODPGXC^U-~UXJ^+#@KTR439cdgOuh+7n2RBZ9Q$G__zp8pG0 z5zMG|EIJSuj6Q)tiFjZ zKh;AtRzWDqTJskn^Fm)T`7G^{^IJnvzCld*K&&hat^>`tDv>(sa)fJjXM9476+;|$ ze-xkdyTn2kX)wb^jWR<$%Oov>|POQb|e50vW3 zCf*=wP&MmJl^l}Gn+f@cy6*nM_r~|@EpA2x8#X;u3+12bGGh?w@NsDkV9m#a|5^>; z`33hQf{EXog8Pl-34~HxrLPJDZFXFE;qJkt~zjhf0C~<=7l2tCaq(D`)D{o zVQ19g-xJLO_Ug``#I*dywtPFS7`Ou5h~Gs!x6&1i1DdyQa(~h;vB1XP#J#oqA&`~m zG-#bl3=SAf^R62N{UKWlf0#W}C%!GM@;fjCdQ`(qU zGNOEhFn%NQeBokAmHewSF98{C9wp~t0wngkuFuHZ<88QgzAe$f8b-Y?iy=a8$WVoRW=N^8BT32Eiyyjj?gTfaqgOL zw=w={l}p^HnY!{;S1Oqadz$a}RJHr_!5fkIq8zX@mcp9tX;p`kL>hhbO=s^OQ*SQ{ zci50~e|MC+foSZb_)hwHl|Z4C{1t$s2yd%-rP;E8xc7$3>2=DCQ7GQ+4QBtC?%fW2}%>>mFJy{saU>)F7GFTBlI97d5dW(dMYL#i)!RMKsl`76A@2SqtO> z=(WnhTjUk+@|#)qgR#Wik#ucxPqYJ@r+nD@T=4vsPFbx7!+C$HPU2(6;&4DG!W`JM z*1y(dLt{;&zjspGWNpOq0=VP zNz|OO?zH7in>K}&pQH{wFwWGB>@5UelW^W8UX6DW#WP~xg{i}LV`r!=&#mg4$k4L} z#uDCwI@<)ybK~QD7C@n;eQt6j*B5*ir}!T3up`sG$DzsV_vG=IBDAS$lv8srwwJ^1 zuHY_1(1kMPM&BNBv%^N46Ngj%#Yo5N0eVz_VO@f?{B%JsC!u!;$Os96kU&tq#_oG@ z`X0+2go0(hj9airS2{_7e@sn(WCYT)CtTRij40gD(?xe0zMjA&bWe~yN4!=qodCpX zwsV;`H3~;HxquMP-G^BgP}dv+#&wpV2I2o39KrEJ5GUlyDY5V zFQ+B`dkRkTq;aU-IiV{+TiiC+;NaIwL**l$dRD7=&Gyj(FHLhQSbo8FKCv@@LIm~+ zWjvMlnAq@+G2v6*?I0iL`qkISVBVtw*^z284kSnl2S60?7*BFIk`5|0YP$%vb1G>} zZDViXkzJaExHUuGcuGseicI@`Uym*jAIesmf9hb(xDCqJ*u>iUWW-*fv z+m+p9<_l>qK#FCQuiT=%Etajd2lxMZwg5VI-*6m7y ztv2;}ExbF_=t}LxC&+2V^l`P~ebcdQB`}0*|1}xA! z&N8V0fBN=Z+^Fu>Os~o}6L+&cPFvO6r_{Y(0IBt1@F%yZgiySqAxLuNwpfb|Ah$#H zE4}bFly7n{#p=xhh!mHRG@7AuJ)@7D@RgRWC zd(P)6`;!=hVw2!d=;qQm7o*$P&i2-xk#R7mPLK0Y95mCKu9>^mOfYDm5;6M*CtYEG zdG*Dw!qNTOJ=ed}g>ZDey}OQ2qqZ?`b*Q$=s3AYhQSUg_XaP!&=9TM?Gfr~EF;>E) zZ;w(f-dac<>A)F;qm9C(@H;C_StX|hPk_U`GJC&eov*m>!o#PZ7{DKH;sOY8rH~+r{Fe!h%)aadG z3CMQ%eN4stNaQ5;5|qZPT&7Bde4qcJ>WbP@QyIQ!w4;xFOHC_mMoq|xMouk&=?Lm! zf8bDIIqQ<f~ZaK9(xTBe+7=}vOD{%Mx+^Lrh9o@uYwu};$)`Jc3z z4r6LQ-5&}^oz8T%(m^vHvtPW_-SzSXiX_vId=a=k^bz`FfQv1*}0Q!3kcUOdqkBJ0R) zt@QdQd|3)*bhEJ<^jq&;o^t8pn3Cc4wLe*)^UX6z)71sjU*WHVEmcKn!HZExAlo!Y zS{!&bK8|7i{|00*jJ!g99lZWgI;RXAq68^-)R$)Fwg2AzZV?RE;DE9j_-Cq*@^fE0 zoQfhZ|GPkhXPo=%Eqk6y3RB%l-G9!}o>Dhmiby@Ug)&0jn99VxZXV@^a@)eS%%n1{ zB7(;NSG7$41{PCi+_qbR?0Ke%SF1)PxGiVPcaja%03^BkLglv9$y(CAxU+S_vy)!3 z@W#XX{Q>fDIb=TiKOumHWd zF)K@&$>>AQq=hLP*k8(^hzdHz;Fo2?aUGew-{lmqcl$+>tNU~@1sg&M+hsfVMpm~5 zwm2soWpoHfnHPp;M%VhGMm^jP2d3-m|G+JR!&rZkt==wo{4-&KNS}*25N->Sy4$dI zgFk(0NkW4X67$)rY+u0hTfZFVwTz7#E~=zDOb??w0!f{tGZLSSB7GH@A;j+hN&J6$NG%?iC={MJ= zk%Axf8QTFM7M)`^v95!$ z1oH)RZ)5@EgQklDrzx!t^c2Z9;?SK{R99gpt&lI4e&0d|6B{s)a^re)dfK{Y8q3dG zxNPicqzWVUO4hiYSgRNEGJs_9L!yzW@MlGF}J!mEdVhoA7 z=Bi8k^cM&JVTlU5p?)y)-Ij=*Q_y>lUe`*t8LuDk+}z%Zfo1lK&+zq+&Z!t={12Rdt}MO^t08fiMwG?liw@t)S^+Y)LUcx1wEan1Q8me`0snKL5j z68IahSrkt^rJ8T;NaxGq)bAut9m_0^%pX1%l~ne(?U_CN(zW01TLzYi7TtrWyuZnQ zf=4h@w~WOjt&4NU5+j{={bXwDxhBhiAP4c$&gE1;c#N9y@rn@`&0R;0RRuIJ_LEPi z8b0G>mW3Cnvc`8}16!`WT(xs0nK15Z+wQ>EzMIO3TrZs*JW0P}W?d9UyI)dB|EKWN zvih*&n7(NzE&mme+L~)CrjGL*7Oz1OK+yWT2sG>$C5AT2tjI#Q^hgrVYWA(P7V1wv z7Z;dN_D{-+zoaQeRnCYJ!u+$8u>O*1B2b$e=CMea>EP=|=NlXg@)yM3?XN1#g+l?v zk1HVG!DZ2Mi+sHcNU}GM(AR5njU9Gb1jmj&Z*7Y?bhq-L%t|jPur|8W3-zG0Z<#67 zPfsYvsXUkih1>GufgoMr;QZ#`N7@}<>Tnwu)%1d=F&zfQy!LlN85VoO-fgfZDBX-}YGB-6TZ~477SYyXUdGEkptjZ_KI|7l>!_96V-IAMXm`O=44=op$G#$W}A2>GsC^ zfF&6zLKqnYvF@ks>(**~ziVHnytk%;SV^c_eafbKRgH#&)$f(H)<9qkUi$G0SUk`8 zfZY?iF?67Jfli5nc(@D4SU<^WfA0f*SxZ4?$d-tn;xgu9rv892hG1U7_BcO;yPms? zp*#iVDo4{(Y*^{_)&pxW1Fay1`Tj7A>i_z{`h3$_Ln>u_2YV4j);q7bwa0M)AEe!O zR2J58e$W*K7Ta^80Za2kRzo$Yx{ZCWyavAw`w0D?vS&ZhAvO5}JWoh%l!}iC7~~zx zC6ilRI`}u7HfoveAab(=df8XKI9pdQwF}G$IPXQW;$arFFJTbaJ^;}+yOZ$dpI@Zb zWjV4B#4Kpimnb33@{RN=&BU?%tO2_`+wov_%}S@Qj?B~0LdaD8+vybp72_h3Rz8w; zIvZSb|2L#A3&M^n%Ph~RI8>Rw!e;=iI(X_pby|(2HuX?G#M$&J@&rGwq6pyYcAXnY z+-YQlHbN~JRmpzpuYP+99`?c$>HQ-7Uf-%AUY;<15tL%P2DG{b~5SThW{P#r2y}|GcPjfj$WlQ)DWb@GjIdbg1Lu)iw26fSO zz75nlJ0>ny9o?XRh@?ub-dAoQX0f(3`m4(IxJP3yq2U_fj8!(fVG~lkW@^heiW=o8 z=7o#ie{V-uE)%yxA*9Wl>6Xc57Kcj$XP{W>y}k&9mvQlN_L*bPywQX=b>`e>QT%^}QZ$GaqYotkj|I!cxTIK1ilYlYTn1Zrfqw zKVaroog&Zj=ffeDfmH#g{Hy(Hw$%CpxA440Q~SP_in5ADgC&ob%6vn5CC-M5-%^1JZ zp=*(8RNxZu4Kyt@q2(Fp<7qiYxd*cA{{0>Bn)q0(4>TiBSzIXCce`l^X9hZd+Wp4vu2*%N^DMH=tIZCtXgAI6Z}%>&8>{XrFp?VR zVd&iqrtR;Kam$A2a6gn+F|EmAMvvNwkm!(MC-$u{0mtUQ`>e0NIzLrF_;785%r3B{ zD|he-^Nv03#w!Y^`HFkk%QTbBVgIh{5-(kd;JI&tM$eY2;mnCQZ`@pP6AY3&|#W8g>rp_Tj7g? zlWgMPYWTN;sH|Vftu$8wWk-)rF7A9Sr!j4n*)#rDt3UkLbsE6kjxl=g+7p5tHuCmN zqjMQp^Y!*(2ojl(1<}h6n*^Jy@;y&#Y@LHGKk&Q9HJddALUwb6Am;q$ZcG)mSKwrC z0MrdrQUy`-p~DHGsy!pY9SM+T5j{k|AZQmUci zt7*pTZ?eUbqPI@)j@`Fju9EeEHmAe;iya1JVKY+<9g!xpA+k~LLSe?A%5fnr+&cXv zV;Egde*k0JW^+It*noIKGO!313}K?8pdRGG@AgPAygODFG#IBYyWcUrK|?dkqBR(# z+3fC3oVoV}5KAvY=6-Bo#T*A3IAnOa7FcI7JRv+MU91n~cc$SHN%hI3?s)?0t#q=8 zWOd&o@dEcJyuK!-+z8Cv2AX)W$_*;?>6b?y=~wEpYPD;0fU?yjY34CC;;&2A-($>^ zywNH^!+cAQxIo&;UIEtskx#8#sagaRK$2vp#yEFm(v3t_ECxZ1)!Vx=nk!plU$?Z=;rv_Y(Uj+|r#*TS6;JyDI#h$o)!=J) z;j=MknBI)0K zy7?nhrLe-)wVW`NBUn(PAWcp$A{Y!wWIF!ka950B7{xIkb|&R>=D8{JmF>m^gHXfw zmVr}*3lCrw5XOv&*|4Y$Ord;9TBDgi0nQJ1La&`|l2>U8HQKIU@hY>d>|8ZR8+#!- z`A~L;x^y31{@|h3B{5}<$lrVg`|s>(y;SB}0G*Q%Htn2-z-lyv~`pMaBG%jcL(Q z*2RD%VW&KT_$c8Gsxq`n^R918u9K6C->VHt(%^4@J#cY+6oHXYDO}Vq&$_sp{GiQl zWQ%_d3|Xj)XnG<2%QXPKEwlFAgQJdzDrRF4M{gWIQW&yOlF6IzQA8`+Ao&TBa-)9^ z^&{opOyne(^UMkQYoc7&~k6pS)o3kojAU8E$b-DsKW0!elL1i z)yEyINJgEDN+0CTO(4n5jW!a-Nu7;ky3_)wW9)W)9H2qJhEJ)oPDai@z?N4&B0~n0 znx=kn0_)_9w;uXBb98*xvCbW{a?2Rd*@E1T+8_Svbp$4PG9dJ(+q@&m0<=bak+S-u_)HQ9Qx=*Oj%!4(lqy+0o`mcZ8l@g&9v(W09}$9fHD z+xQOw-bP-!S^K{p?Rxw`+ixd}+=lG0d0zhP5n6vTNyRNlee_WX6_ImHTuY1|Y-|M? zT?rd~DgmyN+p13uo_YHqe7S?ga^I|KQg#9X%IgL~GuU6Mh7)SuoK$|r3UsGnU`U=< zt~$JTL&c_)Zb;f#E{`Sm%|SutQ+y<`KME?+t&wbmy4#AS3^cCf%S=w1dRl6MU=8n- z5u;lFlmV+&w?+>r_5q^i^CA5s&s$a656sF4k6@R)dE^oxrYqvyrm0LC?a=Ijrc1n< zI5C*$%O(Cwt2RvErJOtH(!Hq&r{jO6$H(3Foj$CCW{TM~-{gCTL|1wf8M4D2{VNdu zt!>z)r2edDa>;`_`niJDDgO(=v2mZs_*Hu*$})llZ3JMM_bp0_80a`M@SW5>J9iS8 zy@hOvBY)wS;+FIPdRC?;9ct-SfLrRY$##neNewo$Tm#V6P1o4NXe(|B{30*PouFuL z-tU{?0~K-={I~=E#6bFqV9E}?CAyDlOWj-ZSOWba zx`x?z5R;c)Dk8PH2U)t1NTl;!I&>gBcxR^Z1t*^AJ$61mketIlG6V(&J0&Vg4K0IU zKYKoB#f0b~=CJ|w;I816zxZZG>K=!XKJ5oaOEGC-)lMw&vSN4f&e56hfFS;{fQEyF_1fTT&|4r?z73dFalSv-_ zd7_t8@y4c6KXu^quHJ-jD^nR+`p_`XD4dj19$&g9or$Kms?R1SC27c8cw&B9g$7*> zvPZIl4$X|C@eL^Bvyi$orzU=2;jHPG(6Yw=8Xy(*zsXJoK{-{YAOsx6ZI>~Z^wSa&tSJ#Lbx{PSrL zpE~bvw*>A`(|6>uw*PPD{G&d%QX^d@*rC&?Z5y6~BUd7ls%9*c*>1qE!186#AW^gq zUr>0S8sVwd?Vk!OWv3SbI$UlM7U5{j!{pNGUC4ruH3QM(t=5zZ-rc7wdqYH-kJWhSxMIj7$iZUGl z{DDhPo}83DzcYe*yIYfGOM z-t|{1nr`r}`VDf$$cf8#h}k2MxQiHDY zJmxV*s(3d1O&pn362;W=-V}4zr7bytd4jEOzT-9zAurp7 zZC|s7A1PO8|9nY$u`BBzr-AXT;BVa@X#5qQHhgu|2)Z$6CypWL}3?^t5&b6PQ!* z%X#4SNsk!=(5pekIQ^vPvE&g@_7Gr{Kai!E$pEn(Pv240I{>EG#DYhr4n?*15Db## zqsXc$b0|+yGXn<3$}!S0Xx1o{o;?CR{M_OhbZOb0uk0z*tMUo7>hahkX!S#A%>!ud zeW)gPxaM*66}ab+*P!=3-hj_tOMkHozh7_%8b8{9S9WE&#=8UO!v%02+z0Lj_l9%f zoQ!%`^X}fYo0$nhpv;4#<}8ONhf`;1Tc;cmUi#jsorgA~F#=bx{NSQ;L$`B20A> zYG+?3yiN8fQB2ac*|xA32{HPVXmlUF-ch<-73T5ctt<>(dNfiSo&bD}jOL%zt}St@ z=vnU<0nHju8nhAiH+vITSr@@GfyOd}41F=z1-5%B0`q#M&b22~yV7)mG=d~U(KFj{{wE*J4W9h2M4B>KY&P>pn_(dcFBih3;lXIFWb&d&i~92 z+l1#vTE2Z!+p^j%C=_pV>$Of|x_grROHjOI;#|c~Vc-{=Yp}7k&2tKiuM}kYes2yl zqVu5I>nH$Wmu4uC1{&3S(_?zbpK${&at|>|T*~FVqygNtiJ{$tr z>lX}Mb1nHVUeYZ$nr>|LSxhxXUC{EN3?%=7Petyo$EJI;*;RQM9%lF0z6WekkPMa_ z)Jk6;OH|&dX=uWgVY5yPXuY^KVa$`Xcw63xb|R$VnOXGyZ4-uG!A%RDJf#+6{<+(iVKQc#E0Xp?#WdORSyj zY(7C~A(0zYWGAw==h&*`M)~lSX4)C`_YwOPro};x*xtda`1@f>;BltaH{U%E7uKIV zzw`cqp#7@q@UtB!iT9VMFWYxW+ZM*+tDU(v*QM+)WoP#BaL7xD-<)c?+ZdP{(%+)Mvh z(((J6w)jUs9t$HA$0+vg+(qvF4cBBIc)QWZTW#sQe8TnI6D`{yc;4Kx!E1RCk5du& zI>7Yx(z8E&qVSsJ8Ey+$KQ69!PP(AnK)1F^IK}kJxk%26LTDGJ3!R2(=(gFDonmMI z`ZBpNo?G_T+sE$8#umdD@^E4~JZ>y!4#8$T}(TI|1`9$On$*ph=}=&*CyC-5Q1FaEyI*c5j0AIr!y zq1N`>{*jc;q&0{0La7E3uC=RJ2t0c6t)+5#PW^G&4efqbyr#+)_mo5Ahkv&~;vXD8 zCEozzG&UQYlwWFWtUT8L%S1}}c|_)@g8VKF^GGLKNH#F}>}VHhqu&SDNOj8D_QokH zS<9}llE0+cIQc|qf10xiVbv9MxKtie5l(UUL7DIECk{G`_UeQ7SDOB^83C<9x})#Q zDTgcVPF=n`lqKby*v<=f4!^IwQqE;DCqVm(%CG;E*Y?@TKgkZ)&d@7&9VZ|9h2-$* z=|12Kk4<=WA^?Qg0ss{NfLYbWH+<)4APPpah}?aB`y67j zMyN-Vw;6txjKZO?Gol?rLfQ}z;HfT+UKPQf2z_G|9E3dS>zoGQMSKJdTF-}BYu~V` zkq;vhk+p9oWFj~@jPDWh&(Y79q=%CQSSTE|H1)^^&kzY76~XU3vw@0>l90OHJeal5 z_s!fc)y-r!T99Otc_PW)* z9^|x!tKBIPQFr@X{s=yRMy2B+^wwoCe%f@i-fu+M zaB}~QEcn!*2=ReUcZAPJGdVs#z$z}u()I~7^OJZOi5iXoz>Q%r(Hb^C!e%mT?Hd_O zj+DSfq49z#S@7ci7PJKZi)*6Cyi62G@al-LfHyBg7P@vJLugo5t-k++9`=8k`63p`1Bc_{_c_swQN)} zv@^t1egtkohthECa?vUnp9piU>e|({EA@mGV8av9Lg@Hlc%9O?B#noMlj;OryYQ-3 zc04XlZP)<1&Mk^uWN!v$_H_=5N+xJQ$thKTZfQY*agNPUz-Dx6QF2M0nJTqI#tA|n z6V<_!Z-$CZmda0=X$x_Kz|NhaI<~=4$teUxA|V9ixJ%SJWDce~`H0NeyUEfzm6;A1 z=bVY$lKor#JN?dsGJ;-m{iN`6Xf z{2>C!*W@d~pr2tJXrldm>D3OdO2ms0E}KZ@sQAd{z+L&(pmrTESQGr}w)V3~ydQ~5 zHtZOg4uyH~0Dvx+bibB9#M?aA5!#QyLHwar3M5x&o-~PV$q3PHdNC|5k()6ltLsiF zx7t-3Th>JieOAA3w;4xOEjoQVAkYA>mG&Ovlv>Icu)F!GU88s#9LH0wlN4{}8{|SC z*vRYTsBKZ0Z`wHBV6IC9s5W55Il?@c&R?&nO$5ROy@Q)9KzwZSKPnK1Ej0YI-)Nap z2+aO0VuJ&17}aM9dJDdOBIddpS0+YEYuN{)Yup$Bdd*yLu7M z6*5H9fxXL!?eBvVvfe2Latc$zEoS3Tb!q#}bVz5MEbx6ZF-LW#zsCjk^a*;8?;9mb zd~bMSqUqi;Vq8usHD+A*5tt}Cbe2fzCg*^~!mW8U4b1l8FRuAbelBVdAnYYiU0r($ zZt3bb+YXV-*@Zx-w$;Fi@|L57rS+QtmKhOLZ!Tu;RS{vaW?H9I4apjGs=2`>8c&4( z-arz~W|d@`n52Jz!afJg2BOkw>5*=h1YF|?G3g_$kz#B|FYrXmJ7QEVY7yIazR44xXvW7P48Mv{`NJS7YO zWG~veSL%VX6(1g+Zmq?ztK1R0C+!zuep{%7OaT*tFZ1dLHC8fNqQTfY=ySq=q^6{3 z3}9Byp47(X2|UfapeQHZJ1?&gAOT6eH*tC!NF1fT(vV?YxJQ!bU6{Q`isnetveA^x zBHi{rJfi?D$S5kw?Q~y?57)0R&ly+<=s%c8_?~2;&!=F2Tb94uf zmEy{_<{4a+Pb)K8Qgbpe;Ky?mF(TfZWz0!6Bq~1y5JP43-|o33nJI-u0LWVhf{OHg zVSCchLUvI3n53YnmIS|Uq^cCDdkgp3g0VMIg#-YTyPil>(iN_kc2y~aqT3;1n!p^2 zV@XP98daQeVbA^y2QnqqASY=aSl=4U$~`6fq#$iUdrJ!XDtw*EwQdS=@ukz$n_v6< ztI{y7O?B6-c8*FAghXy;*?gsni$QWdcjO-&FVSEZsz2dw+43wAQAkpiiSem4~h^KPvYgI^z=36j&74g^Nx5;PZirl@rlH*cR zuTuNbU5%Dq-Y}o8aj=kjD7Gr7+S{K#Fr4069)U~#y__t{5nF$3pLQvy5Ig6vJ%MC! zmC%=zp}Mptm$2$=e9&Rsum}}Ig$rU(Q~>MZ_=}^tb=~J1JWujaGH{pt<`Wu}Bv*FnE zXvF8IVfH)upC+z6^7HlGaGk>+9y1f4m1G`s7uAYj{;}{CNPtoTCI@xm{T5q9@}0Fe zVvt#X&$DNQ@?YU0I9dY!oF`~&L7jOj2Sg3ckbOwe({ha8FiBl5e4~)^4xQOhDnej6 ziLc0rsZ~S9yF=ok5Uyel{#?=y!KctkBzl^j8L(1Y7K666OslQdzR`hFBoS%(IaAV} zPIzFROnB#Xonv|6NQ6nK)Xb%G)p`d`T^Slxn~42>E-b%Eige9lgKk|>biO1*ac%sj zbjYHOTFymcYDO*XFcYrBLVbec6+V5E67Dd?!62b##I&(9Vl<9cpEHb`xWcEh%lZ;@ zP9ZkX$6R(+88Tvok^{ZAYR_8sw01fOjwUT3!eUXiNDP*rKVqj8cD1Q;_V?`K@wuKb zTid2yl$?(lghP1=BYCh?Q;mtZ{Zp1JoiXD7q2E(|$A)qY3Nc`i_nt+Jk3(v)euwCx zjdB21k_eyjvMmdgQ#7torb?AYC6fprw^o}f*%#+|hL={+#CZmD?lIgM=m{kXj5a8j zL<8V0GDRt34OY}D9TWN3X=O<-PW7mzLwsMbtorB?>s zko{TP{-#GEM!3W=Is`6nnwC03<;Fu?6VXR6^Bv&BzFol8YpAu%Orb zl%YixOn zo{w;Io9oiG!Hb0SSXk+;st)rGDjg7VrO~iYs3f?Y+qH$OL{(Uba4Y`}aMII(&+yNP*|O#*(E0#Hq>(J7=1n>g(#fAE2O+S z&rrb=UOf|KS0|*$OmAh-PqTUaP@x0k?vdksX_(R3i7erVsCmQ1``pz+x-X5H_f)4+BQ*PV!}v{RIO@&Z7r3uI@9mfIl=Ku%*Ckx1c8<93rH0K}SxO0IuKsTlV$6|?F+lXrh0$TAyz%WSd zB@Q_T@{<7|z7faQxB<%he?p{zKOx}VJ0cLthWOn^3h-{NfIaXDQSNtWX)8#Q$MRu- zKItZpC8~M|kmdqX0Kk6?LcIROai%4aPfOe&@%aD&u_EIZigO87fIebB#tq^cBV;7o z$nB|=|CXPJ057>Ezk2}!jwvBv0C;)eI0692o)S8a!N}Y=!Iy^u$I7hv-4(w4wH}20 zNlM6^S;seoZskPTF(Qu>$@((!-l+`LCd^u5gRw2|4+uV|50MR;wQ~%DzHcc_^)>L zD030?X9T&Dfqb)Ev&>(KvD_;yAztMvN;lmKZC20x;o#+&a;$s@s&T zXD`i{ph9nZh-^bGcORws`5h~z$)P*|W3e}85h6P*?w6egO=FWFtuv6DO%2JWtVJ@~ zIgiZ3zk}{$l(M9EsT8c|`vt(+Eez;tD2rv&=zEwa5i>#-%G;zE@{XyX4Y&Xcd{Qd{`747v#&FIg2 zk)_uEXqOg4tk;h5&U2|`-O8OSdPG|AcFSwHZ5!VEf>bqJanvYz^@ zcpio7*#I_=;~Tir0!bRTFe4|En^BVSezMyVb3JVd5SV9Ju*agn{IWx?e}z5>pNYP=++OHkqxhdC(905B#s9q?mLb}GhPyFg*WLTMHB-9n&KXC^%OmdtyHkzPDl zlhc#RHNpeWSs1L8s`c1maazzav3pQj(y@6S6I{`m*SKpIs-e_?!S1vHLqo@-2wGfw zQiAf^+~!>9id!F_5(r)FX&ie7k&DSR&?__0Q2(lP@92w0G&S-YXb}r)jm+bA>m3ZV z*Zc<%&~X@A^YzlvVjbhkY;gGs3MSQG`nCD`gkKsz`(^!|+MTk}L)YHMf#=`yGW1`n z3tQzR`T8r7jKF{#HMpX&54rxG($CNo_-e+G3jx_m>!Y#2imwaM&DmoOsGzhb+0STk zr;z2(+!MSrkm`OGJA~AG@m_p#jt0FDpFqkWsd`1+WH#&m{iI!F#N%aKMBBU^rHzH5x> z#;BQhWiV)=;Q+MNh~YyD4pf9A@cn(4j51uMP{?UMmkhzHd0GCZ1`9$RlKF>nf%l;9el< z_#8_p?pwZ;pOC8?&SPaqFIGJ64tk3?j~DCPByHu^^(iOVVCLDUr0Ey3YkAMIdTQNW znOr9|p8KFdeC4#|VAQ9ak1rLRrY((I^6_Mz$z#MtUS&_sPNd0wif3dol#Wn_@K8eB zboa#@H2q)#Jih14zN?MM04yoHvz2?`(g{DwTSLtk_L`pQ%}j1b+B9b$$+3o}Z_kR< z59h%03`qyTsCsoVd--F^^gt(g)RED5Zc3+vYP(rg`H^HPd$ zQQ$msqL>ykYuY+bYj9sN;n?}AH>m+Ufd##&VLg&%DP$|kDRJD#HXl?;5#NE|pI1V% z(X2TZsSuf<5U1P#rkSrsn_n%&C`z5ZuJM_7@@50HoW(J-);V1rzh?WbSu3cq#CX}Z zTm7Sp*UkIsf4WrGNcj_Wrasml3h+NbQtwc9$XEQUGpkh2T{+~!T!BCO>)4~2yqM{! zPmVV-;~Nud8EoNEWR0NDR{xqfN;uf^bMoU3rgqRVixsP0GyB{4UvZo}CpN-!`zALw zfV3tASmO0;*X-dJC%PXb8R0-q=L05%lJH=~)8`wO>=eWj_)0Y9=M{&7f8f&mxttiA z8)ZJl%_~yQ?o9BEFJ`b;xC6mHvlCp7_D=9w?!`yBD&Q?8xs+F1`s?@7UHTMS8b`7) zk_3IXc%X*MNqHSANKvDF_ruaw^V&L;GcykQFm!d6Y)^-V-;R(c(Gdu9peoi zM_^{FVE^dH|BT2Aj?B&e?aBVp;HX?l+$KR-1c^1t3)kFh=}8hlwS`HoZ;P8QynFg( z%;PW)i$Yd%L$e{hU5%1MX?&~{B+dDnLYPhZ+CN@3PPrHmEH%0wHO?-+y8P;&cNiKU z$^>o24d`OCU&45rKxH)Uxfy47rzdvaIBNX%D@ILV_6xs=)JIq31*IgvEV6!>ZiA%t z^4Qeu=4U}ZkGU+)t3AzUBE6D#1i=4-@2zs!{oU1s%e4Dq+cV?z*CF5e-N(JEj@*0h zQtZACZs7)iyC!bzJV^v}nw>B*mEqcvpVE28Mc2slzx6*Ag zE_-hw+yY#A5zMfQr~!IDu+ zbOpJl3tmSX+dZ9d4;DT=sfQ;L(Hhvr&pY_x)6a*3sZLA9~Xio=5g?dVJ;Z(AL6tk%C zG1HX_zm&~NA|3wgdfrJhq0Rjn-{wc4yXy1jAJ!aC7<<%`JxyUh9G^~NT>Q{|Y%U9= z(5^eWcnZ2<3bA%MHoHUo#|yZ@o-z>1d6q`sUCs(#V8NvI+EQA~I89xDhFHQoLj6Fe&;SGxA$8Gd>u$qJNQxDzqPEiJs$Y{panCYFDQN;Ezi%VbkKK~l;)vMV8iF9AV zo-A<@+f(o%d-dA20jqp{d;|VpXnaOWt~Wa*Cx;zt&Q-{c%_z($kY%KMn`Hr(!vL_G z(AE3~Zc|I^aiG;Be*5rGRTE?dES}pKa!+p z>Lu+dId&Q1AR=;WpOv;Xy?yZK`Lqma5o*kFD+(`4Ei62bo~8{vNh#Q|?=UlR)?|Ne zx~Wo9WCb6*cjGz=Z4#ogi-qn%Pxg#B9Uuz|_oi;jBMSAlu^sX~3w+Wfxv_)6t^df& z1ZkEOQq8?i2~r30VZ;O7>MgrWMwVh%uE=_#wz5}pLXqhwcc*lit|h_Vk}4S;&0{*l z0hZe>5jn~B^36^@EW}q|K5?Q3Q!1tC#4T?b9a)d@Azkm;f7l^m#g-@^YoDTZht}zRjMR^8bITj89(&PMz&2kY;DwabGORYQd4I{C}% z06OtX;t8wX62^2or}CN!YBP>Bjo+OzVn)CpJ9FA5SB1^blf!rCG~pfCB<{+%`%gPe z1YGyIb#c$eXxJKC?8Xh)hTX&x7w0rR{lN~av&0@@m3l^U91&iGt?`1(DTlxb%ycKb zpuiM0gCA>mrQ&QXh<#lb_TW?uK9A|LW6GIRd^Qe)n2&8zjt>{(;O8MI35MFNjF2wye%-P@$kQ6Qa)=C=DG?(fONsUF{{a(R B+sps} literal 81392 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 0) close(spi_fd); } -int main(int argc, char *argv[]) +/* Persistent mode: initialise the panel once, then redraw in place for each + * " " line read from stdin. + * Because the panel is never reset between frames, the display updates without + * blanking to black. Returns on EOF (writer closed). */ +static void serve_loop(void) { - int arg_offset = 0; - - if (argc >= 2 && strcmp(argv[1], "--update") == 0) { - skip_reset = 1; - arg_offset = 1; + char line[256]; + while (fgets(line, sizeof(line), stdin)) { + int percent = 0, stage_num = 0, stage_total = 0, name_off = 0; + if (sscanf(line, "%d %d %d %n", &percent, &stage_num, &stage_total, + &name_off) < 3) + continue; + char *name = line + name_off; + size_t len = strlen(name); + while (len > 0 && (name[len - 1] == '\n' || name[len - 1] == '\r')) + name[--len] = '\0'; + draw_progress(percent, name, stage_num, stage_total); } +} - if (argc - arg_offset < 5) { - fprintf(stderr, "Usage: %s [--update] \n", argv[0]); - fprintf(stderr, " --update Skip reset, just update display\n"); - fprintf(stderr, " percent 0-100\n"); - fprintf(stderr, " stage_num Current stage number (1-based)\n"); - fprintf(stderr, " stage_total Total number of stages\n"); - fprintf(stderr, " stage_name Description of current stage\n"); +int main(int argc, char *argv[]) +{ + int serve = (argc >= 2 && strcmp(argv[1], "--serve") == 0); + + if (!serve && argc < 5) { + fprintf(stderr, "Usage: %s --serve\n", argv[0]); + fprintf(stderr, " read ' '\n"); + fprintf(stderr, " lines from stdin and redraw in place (no flicker)\n"); + fprintf(stderr, " or: %s \n", argv[0]); + fprintf(stderr, " draw a single frame and exit\n"); fprintf(stderr, "\nExample: %s 50 3 7 'Extracting system'\n", argv[0]); return 1; } - int percent = atoi(argv[1 + arg_offset]); - int stage_num = atoi(argv[2 + arg_offset]); - int stage_total = atoi(argv[3 + arg_offset]); - const char *stage_name = argv[4 + arg_offset]; - if (hw_init() < 0) { fprintf(stderr, "Hardware init failed\n"); hw_cleanup(); return 1; } - draw_progress(percent, stage_name, stage_num, stage_total); + if (serve) { + serve_loop(); + } else { + draw_progress(atoi(argv[1]), argv[4], atoi(argv[2]), atoi(argv[3])); + } + hw_cleanup(); return 0; } diff --git a/python/scripts/nixos_migration.sh b/python/scripts/nixos_migration.sh index c6cc4196e..740928318 100755 --- a/python/scripts/nixos_migration.sh +++ b/python/scripts/nixos_migration.sh @@ -113,7 +113,7 @@ if [ -f "${TARBALL}" ]; then fi if [ "${SKIP_DOWNLOAD}" = false ]; then - progress 10 "Downloading... 0%" + progress 10 "Downloading..." rm -f "${TARBALL}" if ! curl -L -f -o "${TARBALL}" \ @@ -122,7 +122,7 @@ if [ "${SKIP_DOWNLOAD}" = false ]; then if [[ "$line" =~ ([0-9]+)\.[0-9]% ]]; then dl_pct="${BASH_REMATCH[1]}" mapped_pct=$(( 10 + dl_pct * 50 / 100 )) - progress "${mapped_pct}" "Downloading... ${dl_pct}%" + progress "${mapped_pct}" "Downloading..." fi done; then fail 2 "Download failed" diff --git a/python/scripts/nixos_migration_init.sh b/python/scripts/nixos_migration_init.sh index 5de6c408e..385f972d9 100755 --- a/python/scripts/nixos_migration_init.sh +++ b/python/scripts/nixos_migration_init.sh @@ -11,6 +11,11 @@ set -e +# The OLED progress display is driven through a pipe (fd 3). If that process +# ever dies, a write must not take a fatal SIGPIPE and abort the migration — +# the display is best-effort, the migration is not. +trap '' PIPE + /bin/busybox --install -s /bin 2>/dev/null || true mount -t proc proc /proc 2>/dev/null || true @@ -38,29 +43,37 @@ PROGRESS="/bin/migration_progress" STAGE_NUM=0 STAGE_TOTAL=22 +PROGRESS_FIFO="/tmp/migration_progress.fifo" PROGRESS_READY=0 +# Drive the OLED with a single long-lived process. It initialises the panel +# once and redraws in place from stdin, so the display never resets to black +# between stages (a fresh process per stage would re-assert the panel's RST +# line and blank it). fd 3 is the persistent writer; keeping it open stops the +# server seeing EOF until the migration is done. +if [ -x "${PROGRESS}" ]; then + rm -f "${PROGRESS_FIFO}" + mkfifo "${PROGRESS_FIFO}" 2>/dev/null || true + "${PROGRESS}" --serve < "${PROGRESS_FIFO}" >/dev/null 2>&1 & + exec 3>"${PROGRESS_FIFO}" + PROGRESS_READY=1 +fi + show() { local pct="$1" local msg="$2" STAGE_NUM=$((STAGE_NUM + 1)) echo "[${pct}%] ${msg}" > /dev/console 2>/dev/null || true echo "[${pct}%] ${msg}" - if [ -x "${PROGRESS}" ]; then - # First call resets and initialises the panel; later calls reuse it - # with --update so the framebuffer is overwritten in place instead of - # the display blanking to black between every stage. - if [ "${PROGRESS_READY}" -eq 0 ]; then - "${PROGRESS}" "${pct}" "${STAGE_NUM}" "${STAGE_TOTAL}" "${msg}" 2>/dev/null || true - PROGRESS_READY=1 - else - "${PROGRESS}" --update "${pct}" "${STAGE_NUM}" "${STAGE_TOTAL}" "${msg}" 2>/dev/null || true - fi + if [ "${PROGRESS_READY}" -eq 1 ]; then + echo "${pct} ${STAGE_NUM} ${STAGE_TOTAL} ${msg}" >&3 2>/dev/null || true fi } fail() { - [ -x "${PROGRESS}" ] && "${PROGRESS}" 0 0 0 "FAILED: $1" 2>/dev/null || true + if [ "${PROGRESS_READY}" -eq 1 ]; then + echo "0 0 0 FAILED: $1" >&3 2>/dev/null || true + fi echo "[FAILED] $1" echo "MIGRATION FAILED: $1" > /dev/console 2>/dev/null || true echo "Dropping to shell for debugging..." @@ -193,6 +206,12 @@ if [ -d "${PIFINDER_DATA_ON_ROOT}" ]; then fi fi +# Preserve the pre-migration hostname: Pi OS stores it in /etc/hostname, the NixOS +# image reads it from PiFinder_data/hostname. Bridge them; don't clobber an existing one. +if [ ! -f "${BACKUP_STAGE}/hostname" ] && [ -s "${MOUNT_ROOT}/etc/hostname" ]; then + head -n1 "${MOUNT_ROOT}/etc/hostname" | tr -d '[:space:]' > "${BACKUP_STAGE}/hostname" +fi + show 38 "Backup created" # -------------------------------------------------------------------