main.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import sys
  2. import os
  3. import asyncio
  4. import logging
  5. from pathlib import Path
  6. from PySide6.QtCore import QUrl, QTimer, QObject, QEvent, Slot
  7. from PySide6.QtGui import QGuiApplication
  8. from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
  9. from qasync import QEventLoop
  10. from backend import Backend
  11. from models.pattern_model import PatternModel
  12. from models.playlist_model import PlaylistModel
  13. from png_cache_manager import ensure_png_cache_startup
  14. # Configure logging
  15. logging.basicConfig(
  16. level=logging.INFO,
  17. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  18. )
  19. logger = logging.getLogger(__name__)
  20. class ActivityEventFilter(QObject):
  21. """Event filter to track user activity for screen timeout (linuxfb compatible)"""
  22. def __init__(self):
  23. super().__init__()
  24. self.backend = None # Will be set after QML loads
  25. self.activity_events = {
  26. QEvent.MouseButtonPress,
  27. QEvent.MouseButtonRelease,
  28. QEvent.MouseMove,
  29. QEvent.TouchBegin,
  30. QEvent.TouchUpdate,
  31. QEvent.TouchEnd,
  32. QEvent.KeyPress,
  33. QEvent.KeyRelease
  34. }
  35. @Slot(QObject)
  36. def set_backend(self, backend):
  37. """Set the backend instance after QML loads"""
  38. self.backend = backend
  39. logger.info("📡 Backend connected to activity event filter")
  40. def eventFilter(self, obj, event):
  41. """Filter events and reset activity timer on user interaction"""
  42. if self.backend and event.type() in self.activity_events:
  43. # Check if screen is currently off
  44. try:
  45. screen_was_off = not self.backend.property("screenOn")
  46. # Reset activity timer (will turn screen on if needed)
  47. self.backend.resetActivityTimer()
  48. # If screen was off, consume this event (don't let it through)
  49. # This prevents the wake-up touch from clicking buttons
  50. if screen_was_off:
  51. logger.info("🖥️ Screen wake event consumed (not passed to UI)")
  52. return True # Consume the event
  53. except Exception as e:
  54. logger.error(f"Failed to reset activity timer: {e}")
  55. # Return False to let event propagate normally when screen is on
  56. return False
  57. async def startup_tasks():
  58. """Run async startup tasks"""
  59. logger.info("🚀 Starting dune-weaver-touch async initialization...")
  60. # Ensure PNG cache is available for all WebP previews
  61. try:
  62. logger.info("🎨 Checking PNG preview cache...")
  63. png_cache_success = await ensure_png_cache_startup()
  64. if png_cache_success:
  65. logger.info("✅ PNG cache check completed successfully")
  66. else:
  67. logger.warning("⚠️ PNG cache check completed with warnings")
  68. except Exception as e:
  69. logger.error(f"❌ PNG cache check failed: {e}")
  70. logger.info("✨ dune-weaver-touch startup tasks completed")
  71. def main():
  72. # Set Qt platform to linuxfb for Raspberry Pi compatibility (Linux only)
  73. # This must be set before QGuiApplication is created
  74. # Note: Startup scripts may have already set QT_QPA_PLATFORM with rotation parameters
  75. if 'QT_QPA_PLATFORM' not in os.environ:
  76. # Only use linuxfb on Linux systems (Raspberry Pi)
  77. # On macOS, let Qt use the native cocoa platform
  78. if sys.platform.startswith('linux'):
  79. # Default linuxfb (rotation handled by QML transform in main.qml)
  80. os.environ['QT_QPA_PLATFORM'] = 'linuxfb:fb=/dev/fb0'
  81. os.environ['QT_QPA_FONTDIR'] = '/usr/share/fonts'
  82. # Enable virtual keyboard on Linux kiosk
  83. os.environ['QT_IM_MODULE'] = 'qtvirtualkeyboard'
  84. else:
  85. logger.info(f"🖥️ Running on {sys.platform} - using native Qt platform")
  86. else:
  87. logger.info(f"🖥️ Using QT_QPA_PLATFORM from environment: {os.environ['QT_QPA_PLATFORM']}")
  88. app = QGuiApplication(sys.argv)
  89. # Setup async event loop
  90. loop = QEventLoop(app)
  91. asyncio.set_event_loop(loop)
  92. # Install global event filter for activity tracking (linuxfb compatible)
  93. # Create it early so it's ready when backend is created
  94. event_filter = ActivityEventFilter()
  95. app.installEventFilter(event_filter)
  96. logger.info("📡 Activity event filter installed")
  97. # Register types
  98. qmlRegisterType(Backend, "DuneWeaver", 1, 0, "Backend")
  99. qmlRegisterType(PatternModel, "DuneWeaver", 1, 0, "PatternModel")
  100. qmlRegisterType(PlaylistModel, "DuneWeaver", 1, 0, "PlaylistModel")
  101. # Load QML
  102. engine = QQmlApplicationEngine()
  103. # Store event filter reference so QML can access it
  104. engine.rootContext().setContextProperty("activityFilter", event_filter)
  105. qml_file = Path(__file__).parent / "qml" / "main.qml"
  106. engine.load(QUrl.fromLocalFile(str(qml_file)))
  107. if not engine.rootObjects():
  108. return -1
  109. # Schedule startup tasks after a brief delay to ensure event loop is running
  110. def schedule_startup():
  111. try:
  112. # Check if we're in an event loop context
  113. current_loop = asyncio.get_running_loop()
  114. current_loop.create_task(startup_tasks())
  115. except RuntimeError:
  116. # No running loop, create task directly
  117. asyncio.create_task(startup_tasks())
  118. # Use QTimer to delay startup tasks
  119. startup_timer = QTimer()
  120. startup_timer.timeout.connect(schedule_startup)
  121. startup_timer.setSingleShot(True)
  122. startup_timer.start(100) # 100ms delay
  123. with loop:
  124. loop.run_forever()
  125. return 0
  126. if __name__ == "__main__":
  127. sys.exit(main())