main.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import sys
  2. import os
  3. import asyncio
  4. import logging
  5. import time
  6. import signal
  7. from pathlib import Path
  8. from PySide6.QtCore import QUrl, QTimer, QObject, QEvent
  9. from PySide6.QtGui import QGuiApplication, QTouchEvent, QMouseEvent
  10. from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType, QQmlContext
  11. import qasync
  12. # Load environment variables from .env file if it exists
  13. from dotenv import load_dotenv
  14. load_dotenv(Path(__file__).parent / ".env")
  15. from backend import Backend
  16. from models.pattern_model import PatternModel
  17. from models.playlist_model import PlaylistModel
  18. from png_cache_manager import ensure_png_cache_startup
  19. # Configure logging
  20. logging.basicConfig(
  21. level=logging.INFO,
  22. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  23. )
  24. logger = logging.getLogger(__name__)
  25. class FirstTouchFilter(QObject):
  26. """
  27. Event filter that ignores the first touch event after inactivity.
  28. Many capacitive touchscreens need the first touch to wake up or calibrate,
  29. and this touch often has incorrect coordinates.
  30. """
  31. def __init__(self, idle_threshold_seconds=2.0):
  32. super().__init__()
  33. self.idle_threshold = idle_threshold_seconds
  34. self.last_touch_time = 0
  35. self.ignore_next_touch = False
  36. logger.info(f"👆 First-touch filter initialized (idle threshold: {idle_threshold_seconds}s)")
  37. def eventFilter(self, obj, event):
  38. """Filter out the first touch after idle period"""
  39. try:
  40. event_type = event.type()
  41. # Handle touch events
  42. if event_type == QEvent.Type.TouchBegin:
  43. current_time = time.time()
  44. time_since_last_touch = current_time - self.last_touch_time
  45. # If it's been more than threshold since last touch, ignore this one
  46. if time_since_last_touch > self.idle_threshold:
  47. logger.debug(f"👆 Ignoring wake-up touch (idle for {time_since_last_touch:.1f}s)")
  48. self.last_touch_time = current_time
  49. return True # Filter out (ignore) this event
  50. self.last_touch_time = current_time
  51. elif event_type in (QEvent.Type.TouchUpdate, QEvent.Type.TouchEnd):
  52. # Update last touch time for any touch activity
  53. self.last_touch_time = time.time()
  54. # Pass through the event
  55. return False
  56. except KeyboardInterrupt:
  57. # Re-raise KeyboardInterrupt to allow clean shutdown
  58. raise
  59. except Exception as e:
  60. logger.error(f"Error in eventFilter: {e}")
  61. return False
  62. async def startup_tasks():
  63. """Run async startup tasks"""
  64. logger.info("🚀 Starting dune-weaver-touch async initialization...")
  65. # Ensure PNG cache is available for all WebP previews
  66. try:
  67. logger.info("🎨 Checking PNG preview cache...")
  68. png_cache_success = await ensure_png_cache_startup()
  69. if png_cache_success:
  70. logger.info("✅ PNG cache check completed successfully")
  71. else:
  72. logger.warning("⚠️ PNG cache check completed with warnings")
  73. except Exception as e:
  74. logger.error(f"❌ PNG cache check failed: {e}")
  75. logger.info("✨ dune-weaver-touch startup tasks completed")
  76. def is_pi5():
  77. """Check if running on Raspberry Pi 5"""
  78. try:
  79. with open('/proc/device-tree/model', 'r') as f:
  80. model = f.read()
  81. return 'Pi 5' in model
  82. except:
  83. return False
  84. async def async_main(app):
  85. """Main async function that runs the Qt application.
  86. Uses qasync's event loop integration - do NOT call app.processEvents()
  87. manually as qasync handles Qt/asyncio integration automatically.
  88. """
  89. # Register types
  90. qmlRegisterType(Backend, "DuneWeaver", 1, 0, "Backend")
  91. qmlRegisterType(PatternModel, "DuneWeaver", 1, 0, "PatternModel")
  92. qmlRegisterType(PlaylistModel, "DuneWeaver", 1, 0, "PlaylistModel")
  93. # Load QML
  94. engine = QQmlApplicationEngine()
  95. # Set rotation flag for Pi 5 (display needs 180° rotation via QML)
  96. rotate_display = is_pi5()
  97. engine.rootContext().setContextProperty("rotateDisplay", rotate_display)
  98. if rotate_display:
  99. logger.info("🔄 Pi 5 detected - enabling QML rotation (180°)")
  100. qml_file = Path(__file__).parent / "qml" / "main.qml"
  101. engine.load(QUrl.fromLocalFile(str(qml_file)))
  102. if not engine.rootObjects():
  103. logger.error("❌ Failed to load QML - no root objects")
  104. return -1
  105. # Schedule startup tasks
  106. asyncio.create_task(startup_tasks())
  107. logger.info("✅ Qt application started successfully")
  108. # Create an asyncio event that will be set when the app quits
  109. # This is the proper way to wait - qasync handles event loop integration
  110. quit_event = asyncio.Event()
  111. app.aboutToQuit.connect(quit_event.set)
  112. # Wait for the app to quit
  113. # This properly yields to the event loop, allowing:
  114. # - Qt events to be processed (handled by qasync)
  115. # - Other async tasks (like aiohttp in Backend) to run
  116. # - No CPU spinning since we're awaiting an event, not polling
  117. await quit_event.wait()
  118. logger.info("🛑 Application shutdown complete")
  119. return 0
  120. def main():
  121. # Enable virtual keyboard
  122. os.environ['QT_IM_MODULE'] = 'qtvirtualkeyboard'
  123. app = QGuiApplication(sys.argv)
  124. # Install first-touch filter to ignore wake-up touches
  125. first_touch_filter = FirstTouchFilter(idle_threshold_seconds=2.0)
  126. app.installEventFilter(first_touch_filter)
  127. logger.info("✅ First-touch filter installed on application")
  128. # Setup signal handlers for clean shutdown
  129. def signal_handler(signum, frame):
  130. logger.info("🛑 Received shutdown signal, exiting...")
  131. app.quit()
  132. signal.signal(signal.SIGINT, signal_handler)
  133. signal.signal(signal.SIGTERM, signal_handler)
  134. # Use qasync.run() to properly integrate Qt and asyncio event loops
  135. # qasync handles all event loop integration - we just await the quit signal
  136. try:
  137. qasync.run(async_main(app))
  138. except KeyboardInterrupt:
  139. logger.info("🛑 KeyboardInterrupt received")
  140. return 0
  141. if __name__ == "__main__":
  142. sys.exit(main())