sat_updates_view.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. # -*- coding: utf-8 -*-
  2. """
  3. Satellite SubView: Updates (F5).
  4. Bietet Buttons fuer verschiedene Update- und Steuerungs-Aktionen:
  5. Wakeword-Modell, Voice-Recognition-Modell, Script-Update,
  6. System-Reboot, Asset-Updates, Plugin-Updates, Test-Funktionen.
  7. """
  8. from __future__ import annotations
  9. from textual.app import ComposeResult
  10. from textual.containers import Vertical, Horizontal
  11. from textual.widgets import Static, Button
  12. from trixy_core.tui.views.base import MainView
  13. from trixy_core.audio.player import BlockingStreamPlayer, PlayerConfig
  14. class SatUpdatesView(MainView):
  15. """F5 im Satellite-Kontext: Updates und Steuerung."""
  16. TITLE = "Updates"
  17. VIEW_ID = "sat_updates"
  18. def __init__(self, **kwargs) -> None:
  19. super().__init__(**kwargs)
  20. self._sat_data: dict = {}
  21. self._mic_test_active: bool = False
  22. self._mic_test_chunk_count: int = 0
  23. self._music_playing: bool = False
  24. # Audio-Player fuer Mikrofon-Test (16KHz, mono, 16-bit)
  25. self._mic_player: BlockingStreamPlayer | None = None
  26. def set_satellite(self, sat_data: dict) -> None:
  27. """Setzt die Satellite-Basisdaten."""
  28. self._sat_data = sat_data
  29. def compose(self) -> ComposeResult:
  30. with Vertical(classes="section-box"):
  31. yield Static("[bold]Modell-Updates[/bold]", classes="section-header")
  32. with Horizontal(classes="sat-button-row"):
  33. yield Button("Wakeword-Modell", id="sat-upd-wakeword", variant="primary")
  34. yield Button("Voice-Recognition", id="sat-upd-voicerec", variant="primary")
  35. with Vertical(classes="section-box"):
  36. yield Static("[bold]System[/bold]", classes="section-header")
  37. with Horizontal(classes="sat-button-row"):
  38. yield Button("Script-Update", id="sat-upd-scripts", variant="primary")
  39. yield Button("System Neustart", id="sat-upd-reboot", variant="error")
  40. with Vertical(classes="section-box"):
  41. yield Static("[bold]Assets & Plugins[/bold]", classes="section-header")
  42. with Horizontal(classes="sat-button-row"):
  43. yield Button("Assets aktualisieren", id="sat-upd-assets", variant="primary")
  44. yield Button("Plugins aktualisieren", id="sat-upd-plugins", variant="primary")
  45. with Vertical(classes="section-box"):
  46. yield Static("[bold]Test & Diagnose[/bold]", classes="section-header")
  47. with Horizontal(classes="sat-button-row"):
  48. yield Button("Wakeword simulieren", id="sat-upd-fake-wakeword", variant="warning")
  49. yield Button("Test-Audio abspielen", id="sat-upd-test-audio", variant="warning")
  50. yield Button("Test Mikrofon", id="sat-upd-mic-test", variant="warning")
  51. with Horizontal(classes="sat-button-row"):
  52. yield Button("Musik abspielen", id="sat-upd-music-toggle", variant="warning")
  53. yield Static("", id="sat-upd-status")
  54. async def on_activate(self) -> None:
  55. """Aktiviert den View."""
  56. self._update_status("")
  57. async def on_button_pressed(self, event: Button.Pressed) -> None:
  58. """Verarbeitet Button-Klicks."""
  59. btn_id = event.button.id
  60. if not self.connection or not self._sat_data:
  61. self._update_status("Keine Verbindung oder kein Satellite", error=True)
  62. return
  63. sat_id = self._sat_data.get("satellite_id", "")
  64. if btn_id == "sat-upd-wakeword":
  65. await self._execute_action(sat_id, "update_wakeword_model")
  66. elif btn_id == "sat-upd-voicerec":
  67. await self._execute_action(sat_id, "update_voice_recognition_model")
  68. elif btn_id == "sat-upd-scripts":
  69. await self._execute_action(sat_id, "update_scripts")
  70. elif btn_id == "sat-upd-reboot":
  71. await self._execute_action(sat_id, "reboot")
  72. elif btn_id == "sat-upd-assets":
  73. await self._execute_action(sat_id, "update_assets")
  74. elif btn_id == "sat-upd-plugins":
  75. await self._execute_action(sat_id, "update_plugins")
  76. elif btn_id == "sat-upd-fake-wakeword":
  77. await self._execute_action(sat_id, "fake_wakeword")
  78. elif btn_id == "sat-upd-test-audio":
  79. await self._execute_action(sat_id, "test_audio")
  80. elif btn_id == "sat-upd-music-toggle":
  81. await self._toggle_music(sat_id)
  82. elif btn_id == "sat-upd-mic-test":
  83. await self._toggle_mic_test(sat_id)
  84. async def _execute_action(self, satellite_id: str, action: str) -> None:
  85. """Fuehrt eine Satellite-Aktion aus."""
  86. try:
  87. result = await self.connection.satellite_action(
  88. action=action,
  89. satellite_id=satellite_id,
  90. )
  91. if result and result.success:
  92. self._update_status(result.message or f"Aktion '{action}' erfolgreich")
  93. else:
  94. error = result.error if result else "Unbekannter Fehler"
  95. self._update_status(f"Fehler: {error}", error=True)
  96. except Exception as e:
  97. self._update_status(f"Fehler: {e}", error=True)
  98. async def _toggle_music(self, satellite_id: str) -> None:
  99. """Startet oder stoppt die Musik-Wiedergabe."""
  100. btn = self.query_one("#sat-upd-music-toggle", Button)
  101. if self._music_playing:
  102. try:
  103. result = await self.connection.satellite_action(
  104. action="stop_music", satellite_id=satellite_id,
  105. )
  106. if result and result.success:
  107. self._music_playing = False
  108. btn.label = "Musik abspielen"
  109. btn.variant = "warning"
  110. self._update_status(result.message or "Musik gestoppt")
  111. else:
  112. self._update_status(f"Fehler: {result.error if result else 'Unbekannt'}", error=True)
  113. except Exception as e:
  114. self._update_status(f"Fehler: {e}", error=True)
  115. else:
  116. try:
  117. result = await self.connection.satellite_action(
  118. action="play_music", satellite_id=satellite_id,
  119. )
  120. if result and result.success:
  121. self._music_playing = True
  122. btn.label = "Musik stoppen"
  123. btn.variant = "error"
  124. self._update_status(result.message or "Musik wird abgespielt")
  125. else:
  126. self._update_status(f"Fehler: {result.error if result else 'Unbekannt'}", error=True)
  127. except Exception as e:
  128. self._update_status(f"Fehler: {e}", error=True)
  129. async def _toggle_mic_test(self, satellite_id: str) -> None:
  130. """Startet oder stoppt den Mikrofon-Test."""
  131. if self._mic_test_active:
  132. await self._stop_mic_test(satellite_id)
  133. else:
  134. await self._start_mic_test(satellite_id)
  135. async def _start_mic_test(self, satellite_id: str) -> None:
  136. """Startet den Mikrofon-Test."""
  137. self._update_status("Mikrofon-Test wird gestartet (TTS wird gesendet)...")
  138. self._mic_test_chunk_count = 0
  139. # Audio-Player erstellen (16KHz, mono, 16-bit — wie Satellite-Audio)
  140. self._mic_player = BlockingStreamPlayer(PlayerConfig(
  141. sample_rate=16000,
  142. channels=1,
  143. sample_width=2,
  144. prebuffer_ms=200,
  145. ))
  146. try:
  147. result = await self.connection.start_mic_test(
  148. satellite_id=satellite_id,
  149. audio_callback=self._on_mic_test_audio,
  150. )
  151. if result and result.success:
  152. self._mic_test_active = True
  153. btn = self.query_one("#sat-upd-mic-test", Button)
  154. btn.label = "Stop Test"
  155. btn.variant = "error"
  156. self._update_status("Mikrofon-Test laeuft — Audio wird empfangen")
  157. else:
  158. error = result.error if result else "Keine Antwort"
  159. self._update_status(f"Fehler: {error}", error=True)
  160. if self._mic_player:
  161. self._mic_player.stop()
  162. self._mic_player = None
  163. except Exception as e:
  164. self._update_status(f"Fehler: {e}", error=True)
  165. if self._mic_player:
  166. self._mic_player.stop()
  167. self._mic_player = None
  168. async def _stop_mic_test(self, satellite_id: str) -> None:
  169. """Stoppt den Mikrofon-Test."""
  170. try:
  171. await self.connection.stop_mic_test(satellite_id=satellite_id)
  172. except Exception:
  173. pass
  174. self._mic_test_active = False
  175. # Audio-Player stoppen
  176. if self._mic_player:
  177. self._mic_player.finish()
  178. self._mic_player = None
  179. btn = self.query_one("#sat-upd-mic-test", Button)
  180. btn.label = "Test Mikrofon"
  181. btn.variant = "warning"
  182. chunks = self._mic_test_chunk_count
  183. self._update_status(f"Mikrofon-Test beendet ({chunks} Audio-Pakete empfangen)")
  184. def _on_mic_test_audio(self, audio_data: bytes) -> None:
  185. """Callback fuer empfangene Mikrofon-Test Audio-Daten."""
  186. self._mic_test_chunk_count += 1
  187. # Audio abspielen
  188. if self._mic_player and self._mic_test_active:
  189. self._mic_player.add_chunk(audio_data)
  190. async def on_deactivate(self) -> None:
  191. """Stoppt den Mikrofon-Test beim Verlassen des Views."""
  192. if self._mic_test_active and self.connection and self._sat_data:
  193. sat_id = self._sat_data.get("satellite_id", "")
  194. if sat_id:
  195. await self._stop_mic_test(sat_id)
  196. elif self._mic_player:
  197. self._mic_player.stop()
  198. self._mic_player = None
  199. def _update_status(self, text: str, error: bool = False) -> None:
  200. """Aktualisiert die Statuszeile."""
  201. status = self.query_one("#sat-upd-status", Static)
  202. if error:
  203. status.update(f"[red]{text}[/red]")
  204. elif text:
  205. status.update(f"[green]{text}[/green]")
  206. else:
  207. status.update("")