using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using NLog; using NcProgramManager.Cnc; using NcProgramManager.Cnc.Models; namespace NcProgramManager.Cnc.Fagor { /// /// Fagor CNC machine โ€” trasferimento programmi via FTP standard. /// Supportato su Fagor 8060/8065/8070 con server FTP integrato e su 8055 con opzione Ethernet attiva. /// public sealed class FagorMachine : ICncMachine { private static readonly Logger _log = LogManager.GetCurrentClassLogger(); private readonly FagorConnectionConfig _config; private readonly IFagorFtpClient _ftp; private readonly INcProgramValidator _validator; private readonly List _errors = new List(); private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1); private ConnectionState _state = ConnectionState.Disconnected; public FagorMachine(FagorConnectionConfig config) { _config = config ?? throw new ArgumentNullException("config"); _ftp = new FagorFtpClient(config); _validator = new NcProgramValidator(CncManufacturer.Fagor); } internal FagorMachine(FagorConnectionConfig config, IFagorFtpClient ftpClient, INcProgramValidator validator = null) { _config = config ?? throw new ArgumentNullException("config"); _ftp = ftpClient ?? throw new ArgumentNullException("ftpClient"); _validator = validator ?? new NcProgramValidator(CncManufacturer.Fagor); } public string Name { get { return _config.Name; } } public ConnectionState State { get { return _state; } } public event EventHandler StateChanged; public Task> ConnectAsync(CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => { SetState(ConnectionState.Connecting); if (!_ftp.Ping()) { SetState(ConnectionState.Faulted); _errors.Add(new CncError(0, "Connect", string.Format("FTP server unreachable at {0}:{1}", _config.IpAddress, _config.Port))); return false; } SetState(ConnectionState.Connected); return true; }, ct); } public Task DisconnectAsync() { return RunGuardedAsync(() => { SetState(ConnectionState.Disconnected); return true; }, CancellationToken.None); } public Task> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => { EnsureConnected(); string ncPath = ResolvePath(path, null); try { return _ftp.DownloadFile(ncPath); } catch (Exception ex) { _errors.Add(new CncError(0, "ReadProgram", ex.Message)); return null; } }, ct); } public Task> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => { EnsureConnected(); if (program == null || string.IsNullOrEmpty(program.Content)) { _errors.Add(new CncError(0, "WriteProgram", "Content is empty")); return false; } var vr = _validator.Validate(program.Name ?? path, program.Content); if (!vr.Success) { string msg = string.Join("; ", vr.Errors .Where(e => e.Severity == ValidationSeverity.Error) .Select(e => e.Message)); _errors.Add(new CncError(0, "Validator", msg)); return false; } string ncPath = ResolvePath(path, program.Name); try { _ftp.UploadFile(ncPath, program.Content); return true; } catch (Exception ex) { _errors.Add(new CncError(0, "WriteProgram", ex.Message)); return false; } }, ct); } public Task> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => { EnsureConnected(); try { _ftp.DeleteFile(path); return true; } catch (Exception ex) { _errors.Add(new CncError(0, "DeleteProgram", ex.Message)); return false; } }, ct); } public Task> ReadAsync(CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => { EnsureConnected(); return new MachineSnapshot { CapturedAt = DateTime.Now, Mode = MachineMode.Unknown, Run = RunState.Unknown, Motion = MotionState.Unknown, Alarms = new List(), OperatorMessages = new List() }; }, ct); } public Task> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken)) { return Task.FromResult(CncResult.Fail("ReadActiveProgram", "Not supported via FTP")); } public Task> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken)) { return Task.FromResult(CncResult.Fail("SelectMainProgram", "Not supported via FTP")); } public void Dispose() { try { DisconnectAsync().GetAwaiter().GetResult(); } catch { /* cleanup โ€” intentionally swallowed */ } try { _gate.Dispose(); } catch { /* cleanup โ€” intentionally swallowed */ } } private async Task> RunGuardedAsync(Func body, CancellationToken ct) { await _gate.WaitAsync(ct).ConfigureAwait(false); try { return await Task.Run(() => { _errors.Clear(); try { T value = body(); return _errors.Count > 0 ? CncResult.Fail(_errors.ToArray()) : CncResult.Ok(value); } catch (OperationCanceledException) { return CncResult.Cancelled(); } catch (Exception ex) { _errors.Add(new CncError(0, "FagorMachine", ex.Message)); return CncResult.Fail(_errors.ToArray()); } }, ct).ConfigureAwait(false); } finally { _gate.Release(); } } private void EnsureConnected() { if (_state != ConnectionState.Connected) throw new InvalidOperationException("FagorMachine is not connected."); } private void SetState(ConnectionState next) { if (_state == next) return; _state = next; var h = StateChanged; if (h != null) { try { h(this, next); } catch (Exception ex) { _log.Warn(ex, "StateChanged handler threw"); } } } /// Costruisce path FTP completo. Se name รจ fornito, append `_config.ProgramExtension` se manca estensione. private string ResolvePath(string folder, string name) { if (string.IsNullOrEmpty(name)) return folder.Contains("/") ? folder : _config.ProgramDirectory + folder + _config.ProgramExtension; string dir = folder ?? _config.ProgramDirectory; if (!dir.EndsWith("/")) dir += "/"; string ext = name.Contains(".") ? "" : _config.ProgramExtension; return dir + name + ext; } } }