239 lines
5.9 KiB
C#
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();
|
|
}
|
|
}
|