Coverage for integrations / remote_desktop / engine_selector.py: 88.1%
126 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2Engine Selector — Auto-selects RustDesk or Sunshine/Moonlight based on context.
4Selection logic:
5 - File transfer needed → RustDesk (Sunshine has no file transfer)
6 - VLM agent / high-FPS needed → Sunshine+Moonlight (hardware encoding, <10ms)
7 - Gaming → Sunshine+Moonlight
8 - General remote support → RustDesk (full-featured AnyDesk replacement)
9 - Fallback → HARTOS native (frame_capture + transport) when neither installed
11Also provides unified status across all engines.
12"""
14import logging
15from enum import Enum
16from typing import Dict, List, Optional
18logger = logging.getLogger('hevolve.remote_desktop')
21class Engine(Enum):
22 RUSTDESK = 'rustdesk'
23 SUNSHINE = 'sunshine' # Host side
24 MOONLIGHT = 'moonlight' # Viewer side
25 NATIVE = 'native' # HARTOS built-in fallback
28class UseCase(Enum):
29 REMOTE_SUPPORT = 'remote_support'
30 FILE_TRANSFER = 'file_transfer'
31 VLM_COMPUTER_USE = 'vlm_computer_use'
32 GAMING = 'gaming'
33 PERIPHERAL_FORWARD = 'peripheral_forward'
34 SCREEN_CAST = 'screen_cast'
35 GENERAL = 'general'
38# ── Engine availability cache ───────────────────────────────────
40_availability_cache: Optional[Dict[str, bool]] = None
43def _detect_engines() -> Dict[str, bool]:
44 """Detect which remote desktop engines are installed."""
45 global _availability_cache
46 if _availability_cache is not None:
47 return _availability_cache
49 result = {}
51 try:
52 from integrations.remote_desktop.rustdesk_bridge import get_rustdesk_bridge
53 result['rustdesk'] = get_rustdesk_bridge().available
54 except Exception:
55 result['rustdesk'] = False
57 try:
58 from integrations.remote_desktop.sunshine_bridge import (
59 get_sunshine_bridge, get_moonlight_bridge,
60 )
61 result['sunshine'] = get_sunshine_bridge().available
62 result['moonlight'] = get_moonlight_bridge().available
63 except Exception:
64 result['sunshine'] = False
65 result['moonlight'] = False
67 # Native fallback is always available
68 result['native'] = True
70 _availability_cache = result
71 logger.info(f"Remote desktop engines: {result}")
72 return result
75def reset_cache() -> None:
76 """Reset engine detection cache (e.g., after install)."""
77 global _availability_cache
78 _availability_cache = None
81def select_engine(use_case: UseCase = UseCase.GENERAL,
82 role: str = 'viewer',
83 prefer: Optional[Engine] = None) -> Engine:
84 """Select the best remote desktop engine for the given context.
86 Args:
87 use_case: What the remote desktop will be used for
88 role: 'host' (sharing screen) or 'viewer' (connecting)
89 prefer: User preference override
91 Returns:
92 Best available Engine for the context.
93 """
94 engines = _detect_engines()
96 # User preference takes priority
97 if prefer:
98 if prefer == Engine.RUSTDESK and engines.get('rustdesk'):
99 return Engine.RUSTDESK
100 if prefer == Engine.SUNSHINE and engines.get('sunshine'):
101 return Engine.SUNSHINE
102 if prefer == Engine.MOONLIGHT and engines.get('moonlight'):
103 return Engine.MOONLIGHT
105 # Use-case-based selection
106 if use_case == UseCase.FILE_TRANSFER:
107 # RustDesk has file transfer; Sunshine does not
108 if engines.get('rustdesk'):
109 return Engine.RUSTDESK
110 return Engine.NATIVE
112 if use_case in (UseCase.VLM_COMPUTER_USE, UseCase.GAMING):
113 # Sunshine+Moonlight have best streaming quality
114 if role == 'host' and engines.get('sunshine'):
115 return Engine.SUNSHINE
116 if role == 'viewer' and engines.get('moonlight'):
117 return Engine.MOONLIGHT
118 # Fall back to RustDesk
119 if engines.get('rustdesk'):
120 return Engine.RUSTDESK
121 return Engine.NATIVE
123 if use_case == UseCase.REMOTE_SUPPORT:
124 # RustDesk is the full AnyDesk replacement
125 if engines.get('rustdesk'):
126 return Engine.RUSTDESK
127 return Engine.NATIVE
129 # General: prefer RustDesk (most complete)
130 if engines.get('rustdesk'):
131 return Engine.RUSTDESK
132 # Then Sunshine/Moonlight
133 if role == 'host' and engines.get('sunshine'):
134 return Engine.SUNSHINE
135 if role == 'viewer' and engines.get('moonlight'):
136 return Engine.MOONLIGHT
138 return Engine.NATIVE
141def get_all_status() -> dict:
142 """Get status of all remote desktop engines."""
143 status = {'engines': {}}
145 try:
146 from integrations.remote_desktop.rustdesk_bridge import get_rustdesk_bridge
147 status['engines']['rustdesk'] = get_rustdesk_bridge().get_status()
148 except Exception as e:
149 status['engines']['rustdesk'] = {'available': False, 'error': str(e)}
151 try:
152 from integrations.remote_desktop.sunshine_bridge import (
153 get_sunshine_bridge, get_moonlight_bridge,
154 )
155 status['engines']['sunshine'] = get_sunshine_bridge().get_status()
156 status['engines']['moonlight'] = get_moonlight_bridge().get_status()
157 except Exception as e:
158 status['engines']['sunshine'] = {'available': False, 'error': str(e)}
159 status['engines']['moonlight'] = {'available': False, 'error': str(e)}
161 status['engines']['native'] = {
162 'available': True,
163 'engine': 'native',
164 'description': 'HARTOS built-in (frame_capture + transport)',
165 }
167 # Recommend engines based on availability
168 engines = _detect_engines()
169 recommendations = []
170 if not engines.get('rustdesk'):
171 try:
172 from integrations.remote_desktop.rustdesk_bridge import RustDeskBridge
173 recommendations.append({
174 'engine': 'rustdesk',
175 'reason': 'General remote desktop (AnyDesk replacement)',
176 'install': RustDeskBridge().get_install_command(),
177 })
178 except Exception:
179 pass
180 if not engines.get('sunshine'):
181 try:
182 from integrations.remote_desktop.sunshine_bridge import SunshineBridge
183 recommendations.append({
184 'engine': 'sunshine',
185 'reason': 'High-fidelity streaming (gaming, VLM, creative)',
186 'install': SunshineBridge().get_install_command(),
187 })
188 except Exception:
189 pass
190 if not engines.get('moonlight'):
191 try:
192 from integrations.remote_desktop.sunshine_bridge import MoonlightBridge
193 recommendations.append({
194 'engine': 'moonlight',
195 'reason': 'Viewer for Sunshine streams (4K@120fps)',
196 'install': MoonlightBridge().get_install_command(),
197 })
198 except Exception:
199 pass
201 status['install_recommendations'] = recommendations
202 return status
205def get_available_engines() -> List[Engine]:
206 """Get list of available engines."""
207 engines = _detect_engines()
208 result = []
209 if engines.get('rustdesk'):
210 result.append(Engine.RUSTDESK)
211 if engines.get('sunshine'):
212 result.append(Engine.SUNSHINE)
213 if engines.get('moonlight'):
214 result.append(Engine.MOONLIGHT)
215 result.append(Engine.NATIVE)
216 return result
219def recommend_engine_switch(current_engine: str,
220 context: Optional[Dict] = None) -> Optional[Dict]:
221 """AI-native: Recommend switching to a better engine based on context.
223 Args:
224 current_engine: Currently active engine name (e.g., 'rustdesk')
225 context: Session context — {mode, fps, latency_ms, use_case, ...}
227 Returns:
228 {recommend: str, reason: str, current: str} or None.
229 """
230 if context is None:
231 context = {}
233 engines = _detect_engines()
234 mode = context.get('mode', 'full_control')
235 use_case = context.get('use_case', 'general')
237 # File transfer on Moonlight/Native → suggest RustDesk
238 if mode == 'file_transfer' and current_engine != 'rustdesk':
239 if engines.get('rustdesk'):
240 return {
241 'recommend': 'rustdesk',
242 'reason': 'RustDesk has native file transfer (drag-and-drop)',
243 'current': current_engine,
244 }
246 # Gaming/VLM on RustDesk → suggest Moonlight
247 if use_case in ('gaming', 'vlm_computer_use') and current_engine == 'rustdesk':
248 if engines.get('moonlight'):
249 return {
250 'recommend': 'moonlight',
251 'reason': 'Moonlight offers hardware-decoded 4K@120fps with <10ms latency',
252 'current': current_engine,
253 }
255 # High latency on WAMP relay → suggest installing engines
256 latency = context.get('latency_ms', 0)
257 if latency > 200 and current_engine == 'native':
258 if engines.get('rustdesk'):
259 return {
260 'recommend': 'rustdesk',
261 'reason': f'High latency ({latency}ms). RustDesk has better NAT traversal.',
262 'current': current_engine,
263 }
265 # Native fallback when engines available → suggest upgrade
266 if current_engine == 'native':
267 if engines.get('rustdesk'):
268 return {
269 'recommend': 'rustdesk',
270 'reason': 'RustDesk provides better quality, clipboard, and file transfer',
271 'current': current_engine,
272 }
273 if engines.get('moonlight'):
274 return {
275 'recommend': 'moonlight',
276 'reason': 'Moonlight provides hardware-accelerated streaming',
277 'current': current_engine,
278 }
280 return None