ソースを参照

add splash screen

tuanchris 4 ヶ月 前
コミット
ba0376297f

+ 124 - 14
dune-weaver-touch/backend.py

@@ -1,6 +1,7 @@
 from PySide6.QtCore import QObject, Signal, Property, Slot, QTimer
 from PySide6.QtQml import QmlElement
 from PySide6.QtWebSockets import QWebSocket
+from PySide6.QtNetwork import QAbstractSocket
 import aiohttp
 import asyncio
 import json
@@ -65,17 +66,15 @@ class Backend(QObject):
     screenTimeoutChanged = Signal(int)  # New signal for timeout changes
     pauseBetweenPatternsChanged = Signal(int)  # New signal for pause changes
     
+    # Backend connection status signals
+    backendConnectionChanged = Signal(bool)  # True = backend reachable, False = unreachable
+    reconnectStatusChanged = Signal(str)  # Current reconnection status message
+    
     def __init__(self):
         super().__init__()
         self.base_url = "http://localhost:8080"
         
-        # WebSocket for status
-        self.ws = QWebSocket()
-        self.ws.connected.connect(self._on_ws_connected)
-        self.ws.textMessageReceived.connect(self._on_ws_message)
-        self.ws.open("ws://localhost:8080/ws/status")
-        
-        # Status properties
+        # Initialize all status properties first
         self._current_file = ""
         self._progress = 0
         self._is_running = False
@@ -87,6 +86,24 @@ class Backend(QObject):
         self._auto_play_on_boot = False
         self._pause_between_patterns = 0  # Default: no pause (0 seconds)
         
+        # Backend connection status
+        self._backend_connected = False
+        self._reconnect_status = "Connecting to backend..."
+        
+        # WebSocket for status with reconnection
+        self.ws = QWebSocket()
+        self.ws.connected.connect(self._on_ws_connected)
+        self.ws.disconnected.connect(self._on_ws_disconnected)
+        self.ws.errorOccurred.connect(self._on_ws_error)
+        self.ws.textMessageReceived.connect(self._on_ws_message)
+        
+        # WebSocket reconnection management
+        self._reconnect_timer = QTimer()
+        self._reconnect_timer.timeout.connect(self._attempt_ws_reconnect)
+        self._reconnect_timer.setSingleShot(True)
+        self._reconnect_attempts = 0
+        self._reconnect_delay = 1000  # Fixed 1 second delay between retries
+        
         # Screen management
         self._screen_on = True
         self._screen_timeout = self.DEFAULT_SCREEN_TIMEOUT  # Will be loaded from settings
@@ -108,6 +125,10 @@ class Backend(QObject):
         
         # Use QTimer to defer session initialization until event loop is running
         QTimer.singleShot(100, self._delayed_init)
+        
+        # Start initial WebSocket connection (after all attributes are initialized)
+        # Use QTimer to ensure it happens after constructor completes
+        QTimer.singleShot(200, self._attempt_ws_reconnect)
     
     @Slot()
     def _delayed_init(self):
@@ -127,7 +148,9 @@ class Backend(QObject):
     async def _init_session(self):
         """Initialize aiohttp session"""
         if not self._session_initialized:
-            self.session = aiohttp.ClientSession()
+            # Create connector with SSL disabled for localhost
+            connector = aiohttp.TCPConnector(ssl=False)
+            self.session = aiohttp.ClientSession(connector=connector)
             self._session_initialized = True
     
     # Properties
@@ -167,12 +190,89 @@ class Backend(QObject):
     def autoPlayOnBoot(self):
         return self._auto_play_on_boot
     
+    @Property(bool, notify=backendConnectionChanged)
+    def backendConnected(self):
+        return self._backend_connected
+    
+    @Property(str, notify=reconnectStatusChanged)
+    def reconnectStatus(self):
+        return self._reconnect_status
+    
     # WebSocket handlers
     @Slot()
     def _on_ws_connected(self):
-        print("WebSocket connected")
+        print("WebSocket connected successfully")
         self._is_connected = True
+        self._backend_connected = True
+        self._reconnect_attempts = 0  # Reset reconnection counter
+        self._reconnect_status = "Connected to backend"
         self.connectionChanged.emit()
+        self.backendConnectionChanged.emit(True)
+        self.reconnectStatusChanged.emit("Connected to backend")
+        
+        # Load initial settings when we connect
+        self.loadControlSettings()
+    
+    @Slot()
+    def _on_ws_disconnected(self):
+        print("❌ WebSocket disconnected")
+        self._is_connected = False
+        self._backend_connected = False
+        self._reconnect_status = "Backend connection lost..."
+        self.connectionChanged.emit()
+        self.backendConnectionChanged.emit(False)
+        self.reconnectStatusChanged.emit("Backend connection lost...")
+        # Start reconnection attempts
+        self._schedule_reconnect()
+    
+    @Slot()
+    def _on_ws_error(self, error):
+        print(f"❌ WebSocket error: {error}")
+        self._is_connected = False
+        self._backend_connected = False
+        self._reconnect_status = f"Backend error: {error}"
+        self.connectionChanged.emit()
+        self.backendConnectionChanged.emit(False)
+        self.reconnectStatusChanged.emit(f"Backend error: {error}")
+        # Start reconnection attempts
+        self._schedule_reconnect()
+    
+    def _schedule_reconnect(self):
+        """Schedule a reconnection attempt with fixed 1-second delay."""
+        # Always retry - no maximum attempts for touch interface
+        status_msg = f"Reconnecting in 1s... (attempt {self._reconnect_attempts + 1})"
+        print(f"🔄 {status_msg}")
+        self._reconnect_status = status_msg
+        self.reconnectStatusChanged.emit(status_msg)
+        self._reconnect_timer.start(self._reconnect_delay)  # Always 1 second
+    
+    @Slot()
+    def _attempt_ws_reconnect(self):
+        """Attempt to reconnect WebSocket."""
+        if self.ws.state() == QAbstractSocket.SocketState.ConnectedState:
+            print("✅ WebSocket already connected")
+            return
+            
+        self._reconnect_attempts += 1
+        status_msg = f"Connecting to backend... (attempt {self._reconnect_attempts})"
+        print(f"🔄 {status_msg}")
+        self._reconnect_status = status_msg
+        self.reconnectStatusChanged.emit(status_msg)
+        
+        # Close existing connection if any
+        if self.ws.state() != QAbstractSocket.SocketState.UnconnectedState:
+            self.ws.close()
+        
+        # Attempt new connection
+        self.ws.open("ws://localhost:8080/ws/status")
+    
+    @Slot()
+    def retryConnection(self):
+        """Manually retry connection (reset attempts and try again)."""
+        print("🔄 Manual connection retry requested")
+        self._reconnect_attempts = 0
+        self._reconnect_timer.stop()  # Stop any scheduled reconnect
+        self._attempt_ws_reconnect()
     
     @Slot(str)
     def _on_ws_message(self, message):
@@ -617,12 +717,13 @@ class Backend(QObject):
     
     async def _load_settings(self):
         if not self.session:
-            self.errorOccurred.emit("Backend not ready")
+            print("⚠️ Session not ready for loading settings")
             return
         
         try:
             # Load auto play setting from the working endpoint
-            async with self.session.get(f"{self.base_url}/api/auto_play-mode") as resp:
+            timeout = aiohttp.ClientTimeout(total=5)  # 5 second timeout
+            async with self.session.get(f"{self.base_url}/api/auto_play-mode", timeout=timeout) as resp:
                 if resp.status == 200:
                     data = await resp.json()
                     self._auto_play_on_boot = data.get("enabled", False)
@@ -631,7 +732,7 @@ class Backend(QObject):
             
             # Serial status will be handled by WebSocket updates automatically
             # But we still load the initial port info if connected
-            async with self.session.get(f"{self.base_url}/serial_status") as resp:
+            async with self.session.get(f"{self.base_url}/serial_status", timeout=timeout) as resp:
                 if resp.status == 200:
                     data = await resp.json()
                     initial_connected = data.get("connected", False)
@@ -651,9 +752,18 @@ class Backend(QObject):
             print("✅ Settings loaded - WebSocket will handle real-time updates")
             self.settingsLoaded.emit()
             
+        except aiohttp.ClientConnectorError as e:
+            print(f"⚠️ Cannot connect to backend at {self.base_url}: {e}")
+            # Don't emit error - this is expected when backend is down
+            # WebSocket will handle reconnection
+        except asyncio.TimeoutError:
+            print(f"⏰ Timeout loading settings from {self.base_url}")
+            # Don't emit error - expected when backend is slow/down
         except Exception as e:
-            print(f"💥 Exception loading settings: {e}")
-            self.errorOccurred.emit(str(e))
+            print(f"💥 Unexpected error loading settings: {e}")
+            # Only emit error for unexpected issues
+            if "ssl" not in str(e).lower():
+                self.errorOccurred.emit(str(e))
     
     # Screen Management Properties
     @Property(bool, notify=screenStateChanged)

+ 160 - 0
dune-weaver-touch/qml/components/ConnectionSplash.qml

@@ -0,0 +1,160 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+Rectangle {
+    id: root
+    anchors.fill: parent
+    color: "#1a1a1a"  // Dark background
+    
+    property string statusText: "Connecting to backend..."
+    property bool showRetryButton: false
+    
+    signal retryConnection()
+    
+    ColumnLayout {
+        anchors.centerIn: parent
+        spacing: 30
+        width: Math.min(parent.width * 0.8, 400)
+        
+        // Logo/Title Area
+        Rectangle {
+            Layout.alignment: Qt.AlignHCenter
+            width: 120
+            height: 120
+            radius: 60
+            color: "#2d2d2d"
+            border.color: "#4a90e2"
+            border.width: 3
+            
+            Text {
+                anchors.centerIn: parent
+                text: "DW"
+                font.pixelSize: 36
+                font.bold: true
+                color: "#4a90e2"
+            }
+        }
+        
+        Text {
+            Layout.alignment: Qt.AlignHCenter
+            text: "Dune Weaver Touch"
+            font.pixelSize: 32
+            font.bold: true
+            color: "white"
+        }
+        
+        // Status Area
+        Rectangle {
+            Layout.alignment: Qt.AlignHCenter
+            Layout.preferredWidth: parent.width
+            Layout.preferredHeight: 80
+            color: "#2d2d2d"
+            radius: 10
+            border.color: "#444"
+            border.width: 1
+            
+            RowLayout {
+                anchors.fill: parent
+                anchors.margins: 20
+                spacing: 15
+                
+                // Spinning loader
+                Rectangle {
+                    width: 40
+                    height: 40
+                    radius: 20
+                    color: "transparent"
+                    border.color: "#4a90e2"
+                    border.width: 3
+                    
+                    Rectangle {
+                        width: 8
+                        height: 8
+                        radius: 4
+                        color: "#4a90e2"
+                        anchors.top: parent.top
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        anchors.topMargin: 2
+                        
+                        visible: !root.showRetryButton
+                    }
+                    
+                    RotationAnimation on rotation {
+                        running: !root.showRetryButton
+                        loops: Animation.Infinite
+                        from: 0
+                        to: 360
+                        duration: 2000
+                    }
+                }
+                
+                Text {
+                    Layout.fillWidth: true
+                    text: root.statusText
+                    font.pixelSize: 16
+                    color: "#cccccc"
+                    wrapMode: Text.WordWrap
+                    verticalAlignment: Text.AlignVCenter
+                }
+            }
+        }
+        
+        // Retry Button (only show when connection fails)
+        Button {
+            Layout.alignment: Qt.AlignHCenter
+            visible: root.showRetryButton
+            text: "Retry Connection"
+            font.pixelSize: 16
+            
+            background: Rectangle {
+                color: parent.pressed ? "#3a7bc8" : "#4a90e2"
+                radius: 8
+                border.color: "#5a9ff2"
+                border.width: 1
+                
+                Behavior on color {
+                    ColorAnimation { duration: 150 }
+                }
+            }
+            
+            contentItem: Text {
+                text: parent.text
+                font: parent.font
+                color: "white"
+                horizontalAlignment: Text.AlignHCenter
+                verticalAlignment: Text.AlignVCenter
+            }
+            
+            onClicked: {
+                root.showRetryButton = false
+                root.retryConnection()
+            }
+        }
+        
+        // Connection Help Text
+        Text {
+            Layout.alignment: Qt.AlignHCenter
+            Layout.preferredWidth: parent.width
+            text: "Make sure the Dune Weaver backend is running on this device."
+            font.pixelSize: 14
+            color: "#888"
+            horizontalAlignment: Text.AlignHCenter
+            wrapMode: Text.WordWrap
+        }
+    }
+    
+    // Background animation - subtle pulse
+    Rectangle {
+        anchors.fill: parent
+        color: "#4a90e2"
+        opacity: 0.05
+        
+        SequentialAnimation on opacity {
+            running: !root.showRetryButton
+            loops: Animation.Infinite
+            NumberAnimation { to: 0.1; duration: 2000 }
+            NumberAnimation { to: 0.05; duration: 2000 }
+        }
+    }
+}

+ 26 - 1
dune-weaver-touch/qml/main.qml

@@ -59,6 +59,17 @@ ApplicationWindow {
         onScreenStateChanged: function(isOn) {
             console.log("🖥️ Screen state changed:", isOn ? "ON" : "OFF")
         }
+        
+        onBackendConnectionChanged: function(connected) {
+            console.log("🔗 Backend connection changed:", connected)
+            if (connected && stackView.currentItem.toString().indexOf("ConnectionSplash") !== -1) {
+                console.log("✅ Backend connected, switching to main view")
+                stackView.replace(mainSwipeView)
+            } else if (!connected && stackView.currentItem.toString().indexOf("ConnectionSplash") === -1) {
+                console.log("❌ Backend disconnected, switching to splash screen")
+                stackView.replace(connectionSplash)
+            }
+        }
     }
     
     // Global touch/mouse handler for activity tracking
@@ -91,7 +102,21 @@ ApplicationWindow {
     StackView {
         id: stackView
         anchors.fill: parent
-        initialItem: mainSwipeView
+        initialItem: backend.backendConnected ? mainSwipeView : connectionSplash
+        
+        Component {
+            id: connectionSplash
+            
+            ConnectionSplash {
+                statusText: backend.reconnectStatus
+                showRetryButton: backend.reconnectStatus === "Cannot connect to backend"
+                
+                onRetryConnection: {
+                    console.log("🔄 Manual retry requested")
+                    backend.retryConnection()
+                }
+            }
+        }
         
         Component {
             id: mainSwipeView