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.Heidenhain { public sealed class HeidenhainMachine : ICncMachine { private static readonly Logger _log = LogManager.GetCurrentClassLogger(); private readonly HeidenhainConnectionConfig _config; private readonly INcProgramValidator _validator; private readonly List _errors = new List(); private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1); private ILsv2Client _client; private ConnectionState _state = ConnectionState.Disconnected; public HeidenhainMachine(HeidenhainConnectionConfig config) { if (config == null) throw new ArgumentNullException("config"); _config = config; _validator = new NcProgramValidator(CncManufacturer.Heidenhain); } internal HeidenhainMachine(HeidenhainConnectionConfig config, ILsv2Client client, INcProgramValidator validator = null) { _config = config ?? throw new ArgumentNullException("config"); _client = client ?? throw new ArgumentNullException("client"); _validator = validator ?? new NcProgramValidator(CncManufacturer.Heidenhain); _state = ConnectionState.Connected; } public string Name => _config.Name; public ConnectionState State => _state; public event EventHandler StateChanged; public Task> ConnectAsync(CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => { SetState(ConnectionState.Connecting); _client = new Lsv2Client(_config); if (!_client.Connect()) { SetState(ConnectionState.Faulted); _errors.Add(new CncError(0, "Connect", "LSV2 connection or login failed")); return false; } SetState(ConnectionState.Connected); return true; }, ct); } public Task DisconnectAsync() { return RunGuardedAsync(() => { if (_client != null) { _client.Disconnect(); _client = null; } SetState(ConnectionState.Disconnected); return true; }, CancellationToken.None); } public Task> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => { EnsureConnected(); string content = _client.ReceiveFile(path); if (content == null) _errors.Add(new CncError(0, "ReadProgram", "ReceiveFile returned null for: " + path)); return content; }, 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", "Program 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 = BuildNcPath(path, program.Name); bool ok = _client.SendFile(ncPath, program.Content); if (!ok) _errors.Add(new CncError(0, "WriteProgram", "SendFile failed for: " + ncPath)); return ok; }, 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)) => Task.FromResult(CncResult.Fail("ReadActiveProgram", "Not supported via basic LSV2")); public Task> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken)) => Task.FromResult(CncResult.Fail("SelectMainProgram", "Not supported via basic LSV2")); public Task> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken)) => Task.FromResult(CncResult.Fail("DeleteProgram", "Not supported via basic LSV2")); public void Dispose() { try { DisconnectAsync().GetAwaiter().GetResult(); } catch { /* cleanup — intentionally swallowed */ } try { _gate.Dispose(); } catch { /* cleanup — intentionally swallowed */ } } // ── private ────────────────────────────────────────────────────────── 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, "HeidenhainMachine", ex.Message)); return CncResult.Fail(_errors.ToArray()); } }, ct).ConfigureAwait(false); } finally { _gate.Release(); } } private void EnsureConnected() { if (_state != ConnectionState.Connected || _client == null) throw new InvalidOperationException("HeidenhainMachine 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"); } } } private static string BuildNcPath(string folder, string name) { if (string.IsNullOrEmpty(name)) return folder; string sep = (folder.EndsWith("\\") || folder.EndsWith("/")) ? "" : "\\"; string ext = name.Contains(".") ? "" : ".H"; return folder + sep + name + ext; } } }