using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AmebaPro3_ControlPanel.Services.Uart; public sealed class PythonUartSession : IDisposable { private readonly SynchronizationContext? _syncContext; private readonly string _scriptPath; private readonly SemaphoreSlim _stdinLock = new(1, 1); private Process? _process; private CancellationTokenSource? _cts; public PythonUartSession(string scriptPath, SynchronizationContext? syncContext) { _scriptPath = scriptPath; _syncContext = syncContext; } public event Action? LineReceived; public event Action? ErrorReceived; public event Action? Exited; public bool IsRunning => _process is { HasExited: false }; public async Task StartAsync(string port, int baudRate, string? pythonExecutable = null) { if (IsRunning) { return true; } if (string.IsNullOrWhiteSpace(_scriptPath) || !File.Exists(_scriptPath)) { PostError($"UART bridge script not found: {_scriptPath}"); return false; } var candidates = pythonExecutable is null ? GetPythonCandidates() : new[] { pythonExecutable }; string? lastError = null; foreach (var candidate in candidates) { if (string.IsNullOrWhiteSpace(candidate)) { continue; } var args = $"\"{_scriptPath}\" --port {port} --baud {baudRate}"; var startInfo = new ProcessStartInfo { FileName = candidate, Arguments = args, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true }; try { if (!process.Start()) { lastError = $"Failed to start {candidate}."; continue; } } catch (Exception ex) { lastError = $"Failed to start {candidate}: {ex.Message}"; continue; } _process = process; _process.Exited += (_, _) => PostExit(); _cts = new CancellationTokenSource(); _ = Task.Run(() => ReadStreamAsync(_process.StandardOutput, PostLine, _cts.Token)); _ = Task.Run(() => ReadStreamAsync(_process.StandardError, PostError, _cts.Token)); return true; } PostError(lastError ?? "Python not found. Install Python 3 and pyserial."); return false; } public async Task SendAsync(string command) { if (!IsRunning || _process == null) { return; } await _stdinLock.WaitAsync().ConfigureAwait(false); try { if (_process.HasExited) { return; } await _process.StandardInput.WriteLineAsync(command).ConfigureAwait(false); await _process.StandardInput.FlushAsync().ConfigureAwait(false); } catch (Exception ex) { PostError($"UART send failed: {ex.Message}"); } finally { _stdinLock.Release(); } } public async Task StopAsync() { if (_process == null) { return; } try { if (!_process.HasExited) { await SendAsync("__exit__").ConfigureAwait(false); if (!_process.WaitForExit(1000)) { _process.Kill(entireProcessTree: true); } } } catch { } finally { CleanupProcess(); } } private async Task ReadStreamAsync(StreamReader reader, Action handler, CancellationToken token) { while (!token.IsCancellationRequested) { string? line; try { line = await reader.ReadLineAsync().ConfigureAwait(false); } catch { break; } if (line == null) { break; } if (!string.IsNullOrWhiteSpace(line)) { handler(line); } } } private void Post(Action? handler, string line) { if (handler == null) { return; } if (_syncContext != null) { _syncContext.Post(_ => handler(line), null); } else { handler(line); } } private void PostLine(string line) => Post(LineReceived, line); private void PostError(string line) => Post(ErrorReceived, line); private void PostExit() { if (_syncContext != null) { _syncContext.Post(_ => Exited?.Invoke(), null); } else { Exited?.Invoke(); } } private void CleanupProcess() { _cts?.Cancel(); _cts?.Dispose(); _cts = null; _process?.Dispose(); _process = null; } private static string?[] GetPythonCandidates() { var explicitPath = Environment.GetEnvironmentVariable("AMEBA_PYTHON"); if (!string.IsNullOrWhiteSpace(explicitPath)) { return new[] { explicitPath, "python", "py" }; } return new[] { "python", "py" }; } public void Dispose() { _stdinLock.Dispose(); CleanupProcess(); } }