pro3_control_panel_v2/Services/Uart/PythonUartSession.cs
2025-12-23 14:12:21 +08:00

239 lines
5.9 KiB
C#

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<string>? LineReceived;
public event Action<string>? ErrorReceived;
public event Action? Exited;
public bool IsRunning => _process is { HasExited: false };
public async Task<bool> 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<string> 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<string>? 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();
}
}