diff --git a/pi/ui/assets/.gitignore b/pi/ui/assets/.gitignore new file mode 100644 index 0000000..74318c1 --- /dev/null +++ b/pi/ui/assets/.gitignore @@ -0,0 +1,4 @@ +# Ignore font files (may have licensing restrictions) +# Keep .gitkeep to preserve directory structure +fonts/* +!fonts/.gitkeep diff --git a/pi/ui/assets/fonts/.gitkeep b/pi/ui/assets/fonts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pi/ui/assets/images/.gitignore b/pi/ui/assets/images/.gitignore deleted file mode 100644 index eb6e97c..0000000 --- a/pi/ui/assets/images/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Generated/symlinked images - created by prepare_assets.sh -rei_default.png diff --git a/pi/ui/config.json b/pi/ui/config.json index df5c21f..6e045a8 100644 --- a/pi/ui/config.json +++ b/pi/ui/config.json @@ -1,4 +1,6 @@ { + "assets_path": "/home/pi/smartserow-ui/assets", + "navigator": "rei", "overheat": { "threshold_celsius": 75.0, "trigger_duration_sec": 10, diff --git a/pi/ui/fonts/.gitignore b/pi/ui/fonts/.gitignore deleted file mode 100644 index 245c75a..0000000 --- a/pi/ui/fonts/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Generated/symlinked font files - created by prepare_assets.sh -din1451alt.ttf -NotoSans-*.ttf diff --git a/pi/ui/lib/main.dart b/pi/ui/lib/main.dart index 5bd2738..272c3ca 100644 --- a/pi/ui/lib/main.dart +++ b/pi/ui/lib/main.dart @@ -21,7 +21,6 @@ class SmartSerowApp extends StatelessWidget { ), useMaterial3: true, fontFamily: 'DIN1451', - fontFamilyFallback: const ['NotoSans', 'Roboto'], ), home: const AppRoot(), ); diff --git a/pi/ui/lib/screens/dashboard_screen.dart b/pi/ui/lib/screens/dashboard_screen.dart index 1a732df..8f1c334 100644 --- a/pi/ui/lib/screens/dashboard_screen.dart +++ b/pi/ui/lib/screens/dashboard_screen.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; +import '../services/config_service.dart'; import '../services/pi_io.dart'; import '../widgets/stat_box.dart'; @@ -46,6 +48,21 @@ class _DashboardScreenState extends State { super.dispose(); } + /// Build navigator image from filesystem + Widget _buildNavigatorImage() { + final config = ConfigService.instance; + final imagePath = '${config.assetsPath}/navigator/${config.navigator}/default.png'; + + return Image.file( + File(imagePath), + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + // Graceful fallback - empty box if image missing + return const SizedBox.shrink(); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -127,14 +144,7 @@ class _DashboardScreenState extends State { Expanded( flex: 1, child: Center( - child: Image.asset( - 'assets/images/rei_default.png', - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) { - // Graceful fallback - empty box if image missing - return const SizedBox.shrink(); - }, - ), + child: _buildNavigatorImage(), ), ), ], diff --git a/pi/ui/lib/services/config_service.dart b/pi/ui/lib/services/config_service.dart index 9f54fa1..4e64af9 100644 --- a/pi/ui/lib/services/config_service.dart +++ b/pi/ui/lib/services/config_service.dart @@ -16,6 +16,10 @@ class ConfigService { static const double _defaultThreshold = 80.0; static const int _defaultTriggerDuration = 10; static const int _defaultShutdownDelay = 10; + static const String _defaultNavigator = 'rei'; + + // Executable directory (for fallback paths) + late final String _exeDir; /// Load config from JSON file /// @@ -24,11 +28,12 @@ class ConfigService { Future load() async { if (_loaded) return; + // Config file sits next to the executable + final exePath = Platform.resolvedExecutable; + _exeDir = File(exePath).parent.path; + try { - // Config file sits next to the executable - final exePath = Platform.resolvedExecutable; - final exeDir = File(exePath).parent.path; - final configPath = '$exeDir${Platform.pathSeparator}config.json'; + final configPath = '$_exeDir${Platform.pathSeparator}config.json'; final file = File(configPath); if (await file.exists()) { @@ -66,4 +71,19 @@ class ConfigService { if (value is int) return Duration(seconds: value); return Duration(seconds: _defaultShutdownDelay); } + + /// Path to external assets directory + String get assetsPath { + final value = _config?['assets_path']; + if (value is String && value.isNotEmpty) return value; + // Fallback: assets/ next to executable + return '$_exeDir${Platform.pathSeparator}assets'; + } + + /// Navigator character name (subfolder in assets/navigator/) + String get navigator { + final value = _config?['navigator']; + if (value is String && value.isNotEmpty) return value; + return _defaultNavigator; + } } diff --git a/pi/ui/pubspec.lock b/pi/ui/pubspec.lock index 5928308..e4bb9bc 100644 --- a/pi/ui/pubspec.lock +++ b/pi/ui/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -71,26 +71,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -180,18 +180,18 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -201,5 +201,5 @@ packages: source: hosted version: "14.3.1" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/pi/ui/pubspec.yaml b/pi/ui/pubspec.yaml index c1e5787..9459960 100644 --- a/pi/ui/pubspec.yaml +++ b/pi/ui/pubspec.yaml @@ -18,17 +18,7 @@ dev_dependencies: flutter: uses-material-design: true - assets: - - assets/images/ - fonts: - family: DIN1451 fonts: - - asset: fonts/din1451alt.ttf - - family: NotoSans - fonts: - - asset: fonts/NotoSans-Regular.ttf - - asset: fonts/NotoSans-Bold.ttf - weight: 700 - - asset: fonts/NotoSans-Light.ttf - weight: 300 + - asset: assets/fonts/din1451alt.ttf diff --git a/scripts/build.py b/scripts/build.py index 64a3f0d..dcacfe6 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -71,14 +71,6 @@ def build(clean: bool = False) -> bool: os.chdir(UI_DIR) - # Prepare assets (fonts, images) - prepare_script = SCRIPT_DIR / "prepare_assets.sh" - if prepare_script.exists(): - print("Preparing assets...") - run(["bash", str(prepare_script)]) - else: - print(f"WARNING: {prepare_script} not found") - # Initialize elinux project if needed elinux_dir = UI_DIR / "elinux" if not elinux_dir.exists(): diff --git a/scripts/build.sh b/scripts/build.sh index b1b6318..4de7eb1 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -36,15 +36,6 @@ echo "Cross-compiler: $CXX" cd "$UI_DIR" -# Prepare assets (fonts, images) -PREPARE_SCRIPT="$SCRIPT_DIR/prepare_assets.sh" -if [ -x "$PREPARE_SCRIPT" ]; then - echo "Preparing assets..." - "$PREPARE_SCRIPT" -else - echo "WARNING: $PREPARE_SCRIPT not found or not executable" -fi - # Initialize elinux project if not already configured if [ ! -d "elinux" ]; then echo "Initializing elinux project structure..." diff --git a/scripts/deploy.py b/scripts/deploy.py index efc2a3b..44e822b 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -17,6 +17,7 @@ PROJECT_ROOT = SCRIPT_DIR.parent CONFIG_FILE = SCRIPT_DIR / "deploy_target.json" BUILD_DIR = PROJECT_ROOT / "pi" / "ui" / "build" / "elinux" / "arm64" / "release" / "bundle" CONFIG_SRC = PROJECT_ROOT / "pi" / "ui" / "config.json" +IMAGES_SRC = PROJECT_ROOT / "extra" / "images" def run(cmd: list[str], check: bool = True, **kwargs) -> subprocess.CompletedProcess: @@ -77,6 +78,20 @@ def deploy(restart: bool = False) -> bool: print() print("Note: No config.json found, using defaults") + # Sync images to assets path + if IMAGES_SRC.exists(): + assets_path = config.get("assets_path", f"{remote_path}/assets") + print() + print(f"Syncing images to {assets_path}...") + run([ + "rsync", "-avz", + f"{IMAGES_SRC}/", + f"{ssh_target}:{assets_path}/", + ]) + else: + print() + print("Note: No extra/images folder found, skipping image sync") + # Restart service if requested if restart: print() diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 5d3d92f..c9991bd 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -28,6 +28,7 @@ SERVICE_NAME=$(read_json service_name) SSH_TARGET="$PI_USER@$PI_HOST" BUILD_DIR="$PROJECT_ROOT/pi/ui/build/elinux/arm64/release/bundle" CONFIG_SRC="$PROJECT_ROOT/pi/ui/config.json" +IMAGES_SRC="$PROJECT_ROOT/extra/images" echo "=== Smart Serow Deploy ===" echo "Target: $SSH_TARGET:$REMOTE_PATH" @@ -55,6 +56,18 @@ else echo "Note: No config.json found, using defaults" fi +# Sync images to assets path +if [ -d "$IMAGES_SRC" ]; then + # Read assets_path from config, fall back to default + ASSETS_PATH=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('assets_path', '$REMOTE_PATH/assets'))" 2>/dev/null || echo "$REMOTE_PATH/assets") + echo "" + echo "Syncing images to $ASSETS_PATH..." + rsync -avz "$IMAGES_SRC/" "$SSH_TARGET:$ASSETS_PATH/" +else + echo "" + echo "Note: No extra/images folder found, skipping image sync" +fi + # Restart service if requested RESTART="${1:-}" if [ "$RESTART" = "--restart" ] || [ "$RESTART" = "-r" ]; then diff --git a/scripts/deploy_target.sample.json b/scripts/deploy_target.sample.json index 64a7dcc..f0aa055 100644 --- a/scripts/deploy_target.sample.json +++ b/scripts/deploy_target.sample.json @@ -2,5 +2,6 @@ "user": "pi", "host": "raspberrypi.local", "remote_path": "/opt/smartserow", - "service_name": "smartserow-ui" + "service_name": "smartserow-ui", + "assets_path": "~/smartserow-ui/assets" } diff --git a/scripts/prepare_assets.sh b/scripts/prepare_assets.sh deleted file mode 100644 index 7b99f72..0000000 --- a/scripts/prepare_assets.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/bin/bash -# prepare_assets.sh - Prepares fonts and images for Flutter build -# Run this before 'flutter build' to ensure assets are in place -# -# This script handles: -# - DIN1451 font: symlinks from extra/ if present, otherwise downloads Noto Sans as fallback -# - Dashboard image: symlinks from extra/ if present, otherwise skipped (handled at runtime) - -set -e -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" - -UI_DIR="$PROJECT_ROOT/pi/ui" -FONTS_DIR="$UI_DIR/fonts" -IMAGES_DIR="$UI_DIR/assets/images" -EXTRA_FONTS="$PROJECT_ROOT/extra/fonts" -EXTRA_IMAGES="$PROJECT_ROOT/extra/images" - -# Noto Sans download URLs (Google Fonts) -NOTO_SANS_BASE="https://github.com/googlefonts/noto-fonts/raw/main/hinted/ttf/NotoSans" -NOTO_REGULAR="$NOTO_SANS_BASE/NotoSans-Regular.ttf" -NOTO_BOLD="$NOTO_SANS_BASE/NotoSans-Bold.ttf" -NOTO_LIGHT="$NOTO_SANS_BASE/NotoSans-Light.ttf" - -echo "=== Smart Serow Asset Preparation ===" - -# --- FONTS --- -echo "" -echo "--- Fonts ---" - -# Ensure Noto Sans fallbacks exist -download_noto() { - local weight=$1 - local url=$2 - local dest="$FONTS_DIR/NotoSans-${weight}.ttf" - - if [ ! -f "$dest" ]; then - echo "Downloading Noto Sans $weight..." - curl -sL "$url" -o "$dest" || { - echo "Warning: Failed to download Noto Sans $weight" - return 1 - } - echo " -> Downloaded $dest" - else - echo " Noto Sans $weight already present" - fi -} - -download_noto "Regular" "$NOTO_REGULAR" -download_noto "Bold" "$NOTO_BOLD" -download_noto "Light" "$NOTO_LIGHT" - -# DIN1451 - symlink if available, otherwise use Noto Sans -DIN_TARGET="$FONTS_DIR/din1451alt.ttf" -DIN_SOURCE="$EXTRA_FONTS/din1451alt.ttf" - -# Remove old symlink/file to ensure fresh state -rm -f "$DIN_TARGET" - -if [ -f "$DIN_SOURCE" ]; then - echo "DIN1451 found - linking/copying" - # Try symlink first, fall back to copy (Windows compatibility) - if ln -s "$DIN_SOURCE" "$DIN_TARGET" 2>/dev/null; then - echo " -> Linked $DIN_TARGET -> $DIN_SOURCE" - else - cp "$DIN_SOURCE" "$DIN_TARGET" - echo " -> Copied $DIN_TARGET (symlinks not supported)" - fi -else - echo "DIN1451 not found - using Noto Sans as fallback" - if [ -f "$FONTS_DIR/NotoSans-Regular.ttf" ]; then - cp "$FONTS_DIR/NotoSans-Regular.ttf" "$DIN_TARGET" - echo " -> Copied Noto Sans Regular as $DIN_TARGET" - else - echo " ERROR: No fallback font available!" - exit 1 - fi -fi - -# --- IMAGES --- -echo "" -echo "--- Images ---" - -REI_TARGET="$IMAGES_DIR/rei_default.png" -REI_SOURCE="$EXTRA_IMAGES/rei_default.png" - -# Remove old symlink/file -rm -f "$REI_TARGET" - -if [ -f "$REI_SOURCE" ]; then - echo "Dashboard image found - linking/copying" - # Try symlink first, fall back to copy (Windows compatibility) - if ln -s "$REI_SOURCE" "$REI_TARGET" 2>/dev/null; then - echo " -> Linked $REI_TARGET -> $REI_SOURCE" - else - cp "$REI_SOURCE" "$REI_TARGET" - echo " -> Copied $REI_TARGET (symlinks not supported)" - fi -else - echo "Dashboard image not found - will use empty fallback at runtime" - # Create a tiny transparent PNG placeholder (1x1 pixel) - # This avoids asset not found errors while keeping the build clean - printf '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82' > "$REI_TARGET" - echo " -> Created 1x1 transparent placeholder" -fi - -echo "" -echo "=== Asset preparation complete ===" -echo "You can now run: flutter build linux"