tuanchris 4 месяцев назад
Родитель
Сommit
ba0376297f
3 измененных файлов с 310 добавлено и 15 удалено
  1. 124 14
      dune-weaver-touch/backend.py
  2. 160 0
      dune-weaver-touch/qml/components/ConnectionSplash.qml
  3. 26 1
      dune-weaver-touch/qml/main.qml

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

@@ -1,6 +1,7 @@
 from PySide6.QtCore import QObject, Signal, Property, Slot, QTimer
 from PySide6.QtCore import QObject, Signal, Property, Slot, QTimer
 from PySide6.QtQml import QmlElement
 from PySide6.QtQml import QmlElement
 from PySide6.QtWebSockets import QWebSocket
 from PySide6.QtWebSockets import QWebSocket
+from PySide6.QtNetwork import QAbstractSocket
 import aiohttp
 import aiohttp
 import asyncio
 import asyncio
 import json
 import json
@@ -65,17 +66,15 @@ class Backend(QObject):
     screenTimeoutChanged = Signal(int)  # New signal for timeout changes
     screenTimeoutChanged = Signal(int)  # New signal for timeout changes
     pauseBetweenPatternsChanged = Signal(int)  # New signal for pause 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):
     def __init__(self):
         super().__init__()
         super().__init__()
         self.base_url = "http://localhost:8080"
         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._current_file = ""
         self._progress = 0
         self._progress = 0
         self._is_running = False
         self._is_running = False
@@ -87,6 +86,24 @@ class Backend(QObject):
         self._auto_play_on_boot = False
         self._auto_play_on_boot = False
         self._pause_between_patterns = 0  # Default: no pause (0 seconds)
         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
         # Screen management
         self._screen_on = True
         self._screen_on = True
         self._screen_timeout = self.DEFAULT_SCREEN_TIMEOUT  # Will be loaded from settings
         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
         # Use QTimer to defer session initialization until event loop is running
         QTimer.singleShot(100, self._delayed_init)
         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()
     @Slot()
     def _delayed_init(self):
     def _delayed_init(self):
@@ -127,7 +148,9 @@ class Backend(QObject):
     async def _init_session(self):
     async def _init_session(self):
         """Initialize aiohttp session"""
         """Initialize aiohttp session"""
         if not self._session_initialized:
         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
             self._session_initialized = True
     
     
     # Properties
     # Properties
@@ -167,12 +190,89 @@ class Backend(QObject):
     def autoPlayOnBoot(self):
     def autoPlayOnBoot(self):
         return self._auto_play_on_boot
         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
     # WebSocket handlers
     @Slot()
     @Slot()
     def _on_ws_connected(self):
     def _on_ws_connected(self):
-        print("WebSocket connected")
+        print("WebSocket connected successfully")
         self._is_connected = True
         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.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)
     @Slot(str)
     def _on_ws_message(self, message):
     def _on_ws_message(self, message):
@@ -617,12 +717,13 @@ class Backend(QObject):
     
     
     async def _load_settings(self):
     async def _load_settings(self):
         if not self.session:
         if not self.session:
-            self.errorOccurred.emit("Backend not ready")
+            print("⚠️ Session not ready for loading settings")
             return
             return
         
         
         try:
         try:
             # Load auto play setting from the working endpoint
             # 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:
                 if resp.status == 200:
                     data = await resp.json()
                     data = await resp.json()
                     self._auto_play_on_boot = data.get("enabled", False)
                     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
             # Serial status will be handled by WebSocket updates automatically
             # But we still load the initial port info if connected
             # 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:
                 if resp.status == 200:
                     data = await resp.json()
                     data = await resp.json()
                     initial_connected = data.get("connected", False)
                     initial_connected = data.get("connected", False)
@@ -651,9 +752,18 @@ class Backend(QObject):
             print("✅ Settings loaded - WebSocket will handle real-time updates")
             print("✅ Settings loaded - WebSocket will handle real-time updates")
             self.settingsLoaded.emit()
             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:
         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
     # Screen Management Properties
     @Property(bool, notify=screenStateChanged)
     @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) {
         onScreenStateChanged: function(isOn) {
             console.log("🖥️ Screen state changed:", isOn ? "ON" : "OFF")
             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
     // Global touch/mouse handler for activity tracking
@@ -91,7 +102,21 @@ ApplicationWindow {
     StackView {
     StackView {
         id: stackView
         id: stackView
         anchors.fill: parent
         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 {
         Component {
             id: mainSwipeView
             id: mainSwipeView