using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using NcProgramManager; using NcProgramManager.Cnc; using NcProgramManager.Cnc.Models; namespace NcProgramManager.Cnc.Fanuc { public sealed class FanucMachine : IFanucMachine { private const int AlarmSlots = 17; private const int OperatorMsgSlots = 17; private const int TransferChunkSize = 1024; private const int UploadBufferSize = 10240; private const int Param_PiecesCount = 6711; private const int Param_CycleTimeMs = 6757; private const int Param_CycleTimeMin = 6758; private const string DefaultDownloadFolder = "//CNC_MEM/USER/PATH1/"; private const string SeedProgramName = "OSTD"; private const string SeedProgramBody = "%\n\n%"; private readonly FanucConnectionConfig _config; private readonly INcProgramValidator _validator; private readonly List _currentErrors = new List(); private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1); private ushort _hndl; private FanucCapabilities _caps; private IFocasDialect _dialect; private ConnectionState _state = ConnectionState.Disconnected; public FanucMachine(FanucConnectionConfig config) { if (config == null) throw new ArgumentNullException("config"); _config = config; _validator = new NcProgramValidator(CncManufacturer.Fanuc); } internal FanucMachine(FanucConnectionConfig config, INcProgramValidator validator) { if (config == null) throw new ArgumentNullException("config"); _config = config; _validator = validator ?? new NcProgramValidator(CncManufacturer.Fanuc); } public string Name { get { return _config.Name; } } public ConnectionState State { get { return _state; } } public FanucCapabilities Capabilities { get { return _caps; } } public event EventHandler StateChanged; public Task> ConnectAsync(CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => ConnectCore(ct), ct); } public Task DisconnectAsync() { return RunGuardedAsync(() => { DisconnectCore(); return true; }, CancellationToken.None); } public Task> ReadAsync(CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => ReadCore(ct), ct); } public Task> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => ReadActiveProgramCore(ct), ct); } public Task> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => ReadProgramCore(path, ct), ct); } public Task> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => WriteProgramCore(path, program, ct), ct); } public Task> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => SelectMainProgramCore(path), ct); } public Task> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => DeleteProgramCore(path), ct); } public Task> ReadToolDataAsync(string path, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => UploadFocasFile(1, path, ct), ct); } public Task> ReadWorkZeroDataAsync(string path, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => UploadFocasFile(5, path, ct), ct); } public Task> WriteToolDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => DownloadFocasFile(1, folderPath, content, ct), ct); } public Task> WriteWorkZeroDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => DownloadFocasFile(5, folderPath, content, ct), ct); } public void Dispose() { try { DisconnectAsync().GetAwaiter().GetResult(); } catch { } try { _gate.Dispose(); } catch { } } private async Task> RunGuardedAsync(Func body, CancellationToken ct) { await _gate.WaitAsync(ct).ConfigureAwait(false); try { return await Task.Run(() => { _currentErrors.Clear(); try { T value = body(); if (_currentErrors.Count > 0) return CncResult.Fail(_currentErrors.ToArray()); return CncResult.Ok(value); } catch (OperationCanceledException) { return CncResult.Cancelled(); } catch (Exception ex) { _currentErrors.Add(new CncError(0, "FanucMachine", ex.Message)); return CncResult.Fail(_currentErrors.ToArray()); } }, ct).ConfigureAwait(false); } finally { _gate.Release(); } } private bool ConnectCore(CancellationToken ct) { if (_state == ConnectionState.Connected) return true; SetState(ConnectionState.Connecting); ct.ThrowIfCancellationRequested(); short ret; switch (_config.Transport) { case FanucTransport.Ethernet: short timeoutSec = (short)Math.Max(1, (int)_config.ConnectTimeout.TotalSeconds); ret = Focas1.cnc_allclibhndl3(_config.IpAddress, _config.Port, timeoutSec, out _hndl); break; case FanucTransport.Hssb: ret = Focas1.cnc_allclibhndl2(_config.HssbNode, out _hndl); break; case FanucTransport.Library: ret = Focas1.cnc_allclibhndl(out _hndl); break; default: Record((short)Focas1.focas_ret.EW_ATTRIB, "Connect"); SetState(ConnectionState.Faulted); return false; } if (ret != Focas1.EW_OK) { Record(ret, "cnc_allclibhndl"); SetState(ConnectionState.Faulted); return false; } if (ct.IsCancellationRequested) { try { Focas1.cnc_freelibhndl(_hndl); } catch { } SetState(ConnectionState.Disconnected); ct.ThrowIfCancellationRequested(); } _caps = FanucDialectDetector.Detect(_hndl); _dialect = FocasDialectFactory.Create(_caps); if (_caps.Dialect == FanucDialect.Unknown) _currentErrors.Add(new CncError(0, "cnc_sysinfo", "Unknown CNC model, falling back to Legacy dialect")); SetState(ConnectionState.Connected); return true; } private void DisconnectCore() { if (_state == ConnectionState.Disconnected) return; short ret = Focas1.cnc_freelibhndl(_hndl); if (ret != Focas1.EW_OK) Record(ret, "cnc_freelibhndl"); SetState(ConnectionState.Disconnected); } private MachineSnapshot ReadCore(CancellationToken ct) { EnsureConnected(); ct.ThrowIfCancellationRequested(); Focas1.cnc_setpath(_hndl, 1); var snap = new MachineSnapshot { CapturedAt = DateTime.Now, Alarms = new List(), OperatorMessages = new List(), Mode = MachineMode.Unknown, Run = RunState.Unknown, Motion = MotionState.Unknown }; snap.CycleTime = ReadCycleTime(); ct.ThrowIfCancellationRequested(); snap.PieceCount = ReadPieceCount(); ct.ThrowIfCancellationRequested(); snap.Alarms = ReadAlarms(); ct.ThrowIfCancellationRequested(); snap.OperatorMessages = ReadOperatorMessages(); ct.ThrowIfCancellationRequested(); var stateInfo = ReadMachineState(); snap.Mode = stateInfo.Mode; snap.Run = stateInfo.Run; snap.Motion = stateInfo.Motion; snap.Emergency = stateInfo.Emergency; snap.AlarmActive = stateInfo.Alarm; if (snap.Mode != MachineMode.Edit) { ct.ThrowIfCancellationRequested(); var prog = ReadProgramInfo(); snap.MainProgram = prog.MainProgram; snap.SubProgram = prog.SubProgram; snap.ProgramComment = prog.Comment; } return snap; } private string ReadActiveProgramCore(CancellationToken ct) { EnsureConnected(); short ret; string identifier = _dialect.ReadMainProgramIdentifier(_hndl, out ret); if (ret != Focas1.EW_OK) { Record(ret, "ReadMainProgramIdentifier"); return null; } return UploadProgramByIdentifier(identifier, ct); } private string ReadProgramCore(string path, CancellationToken ct) { EnsureConnected(); if (string.IsNullOrEmpty(path)) { _currentErrors.Add(new CncError(0, "ReadProgram", "Path is empty")); return null; } return UploadProgramByIdentifier(path, ct); } private bool WriteProgramCore(string folderPath, CncProgram program, CancellationToken ct) { EnsureConnected(); if (program == null || string.IsNullOrEmpty(program.Content)) { _currentErrors.Add(new CncError(0, "WriteProgram", "Program content is empty")); return false; } var vr = _validator.Validate(program.Name ?? folderPath, program.Content); if (!vr.Success) { string msg = string.Join("; ", vr.Errors .Where(e => e.Severity == ValidationSeverity.Error) .Select(e => e.Message)); _currentErrors.Add(new CncError(0, "Validator", msg)); return false; } if (!CanDownload()) return false; if (string.IsNullOrEmpty(folderPath)) folderPath = DefaultDownloadFolder; string fileName = ExtractFileNameFromContent(program); if (string.IsNullOrEmpty(fileName)) { _currentErrors.Add(new CncError(0, "WriteProgram", "Cannot extract program name from content")); return false; } EnsureSeedProgram(folderPath, ct); ct.ThrowIfCancellationRequested(); if (_dialect.SupportsPathBasedPrograms) Focas1.cnc_pdf_del(_hndl, folderPath + fileName); short ret = _dialect.DownloadStart(_hndl, folderPath); if (ret != Focas1.EW_OK) { Record(ret, "DownloadStart"); _dialect.DownloadEnd(_hndl); return false; } try { if (!TransferChunked(program.Content, ct)) return false; } finally { _dialect.DownloadEnd(_hndl); } if (_dialect.SupportsPathBasedPrograms) { ret = Focas1.cnc_pdf_slctmain(_hndl, folderPath + fileName); if (ret != Focas1.EW_OK) { Record(ret, "cnc_pdf_slctmain"); return false; } } return true; } private bool SelectMainProgramCore(string path) { EnsureConnected(); if (!_dialect.SupportsPathBasedPrograms) { _currentErrors.Add(new CncError((short)Focas1.focas_ret.EW_NOOPT, "SelectMainProgram", "Path-based selection not supported on this CNC")); return false; } short ret = Focas1.cnc_pdf_slctmain(_hndl, path); if (ret != Focas1.EW_OK) { Record(ret, "cnc_pdf_slctmain"); return false; } return true; } private bool DeleteProgramCore(string path) { EnsureConnected(); if (!_dialect.SupportsPathBasedPrograms) { _currentErrors.Add(new CncError((short)Focas1.focas_ret.EW_NOOPT, "DeleteProgram", "Path-based delete not supported on this CNC")); return false; } short ret = Focas1.cnc_pdf_del(_hndl, path); if (ret != Focas1.EW_OK) { Record(ret, "cnc_pdf_del"); return false; } return true; } private string UploadFocasFile(short fileType, string path, CancellationToken ct) { EnsureConnected(); if (string.IsNullOrEmpty(path)) { _currentErrors.Add(new CncError(0, "UploadFocasFile", "Path is empty")); return null; } long programNumber = TryParseProgramNumber(path); short ret = _dialect.UploadStart(_hndl, path, programNumber, fileType); if (ret != Focas1.EW_OK) { Record(ret, "UploadStart"); _dialect.UploadEnd(_hndl); return null; } var output = new StringBuilder(); char[] buffer = new char[UploadBufferSize + 1]; try { while (true) { ct.ThrowIfCancellationRequested(); int len = UploadBufferSize; short r = _dialect.UploadData(_hndl, ref len, buffer); if (r == (short)Focas1.focas_ret.EW_BUFFER) { if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested(); continue; } if (r == Focas1.EW_OK && len > 0) { output.Append(buffer, 0, len); if (buffer[len - 1] == '%') break; } else { if (r != Focas1.EW_OK) Record(r, "UploadData"); break; } } } finally { _dialect.UploadEnd(_hndl); } return output.ToString(); } private bool DownloadFocasFile(short fileType, string folderPath, string content, CancellationToken ct) { EnsureConnected(); if (string.IsNullOrEmpty(content)) { _currentErrors.Add(new CncError(0, "DownloadFocasFile", "Content is empty")); return false; } if (string.IsNullOrEmpty(folderPath)) folderPath = DefaultDownloadFolder; short ret = _dialect.DownloadStart(_hndl, folderPath, fileType); if (ret != Focas1.EW_OK) { Record(ret, "DownloadStart"); _dialect.DownloadEnd(_hndl); return false; } try { return TransferChunked(content, ct); } finally { _dialect.DownloadEnd(_hndl); } } private TimeSpan ReadCycleTime() { var ms = new Focas1.IODBPSD(); var min = new Focas1.IODBPSD(); short r1 = Focas1.cnc_rdparam(_hndl, Param_CycleTimeMs, 0, 8, ms); short r2 = Focas1.cnc_rdparam(_hndl, Param_CycleTimeMin, 0, 8, min); if (r1 != Focas1.EW_OK) Record(r1, "cnc_rdparam(cycle ms)"); if (r2 != Focas1.EW_OK) Record(r2, "cnc_rdparam(cycle min)"); return TimeSpan.FromMinutes(min.u.ldata) + TimeSpan.FromMilliseconds(ms.u.ldata); } private long ReadPieceCount() { try { if (_config.PieceCounterMacro == -1) { var p = new Focas1.IODBPSD(); short ret = Focas1.cnc_rdparam(_hndl, Param_PiecesCount, 0, 8, p); if (ret != Focas1.EW_OK) { Record(ret, "cnc_rdparam(pieces)"); return 0; } return p.u.ldata; } var macro = new Focas1.ODBM(); short macRet = Focas1.cnc_rdmacro(_hndl, (short)_config.PieceCounterMacro, 10, macro); if (macRet != Focas1.EW_OK) { Record(macRet, "cnc_rdmacro(pieces)"); return 0; } long val = Math.Abs(macro.mcr_val); if (macro.dec_val > 0) val = val / (long)Math.Pow(10, macro.dec_val); return macro.mcr_val < 0 ? -val : val; } catch (Exception ex) { _currentErrors.Add(new CncError(0, "ReadPieceCount", ex.Message)); return 0; } } private IReadOnlyList ReadAlarms() { string[] buf = new string[AlarmSlots]; short num = AlarmSlots; short ret = _dialect.ReadAlarmMessages(_hndl, ref num, buf); if (ret != Focas1.EW_OK) Record(ret, "ReadAlarmMessages"); var list = new List(num); for (int i = 0; i < num; i++) if (!string.IsNullOrEmpty(buf[i])) list.Add(buf[i]); return list; } private IReadOnlyList ReadOperatorMessages() { string[] buf = new string[OperatorMsgSlots]; short num = 1; short ret = _dialect.ReadOperatorMessages(_hndl, ref num, buf); if (ret != Focas1.EW_OK) Record(ret, "ReadOperatorMessages"); var list = new List(num); for (int i = 0; i < num; i++) if (!string.IsNullOrEmpty(buf[i])) list.Add(buf[i]); return list; } private struct StateInfo { public MachineMode Mode; public RunState Run; public MotionState Motion; public bool Emergency; public bool Alarm; } private StateInfo ReadMachineState() { var s = new StateInfo { Mode = MachineMode.Unknown, Run = RunState.Unknown, Motion = MotionState.Unknown }; var odbst = new Focas1.ODBST(); short ret = Focas1.cnc_statinfo(_hndl, odbst); if (ret != Focas1.EW_OK) { Record(ret, "cnc_statinfo"); return s; } switch (odbst.aut) { case 0: s.Mode = MachineMode.Mdi; break; case 1: s.Mode = MachineMode.Auto; break; case 3: s.Mode = MachineMode.Edit; break; case 4: s.Mode = MachineMode.Handle; break; case 5: s.Mode = MachineMode.Jog; break; } switch (odbst.run) { case 1: s.Run = RunState.Stop; break; case 3: s.Run = RunState.ExecutingIso; break; } switch (odbst.motion) { case 0: s.Motion = MotionState.Idle; break; case 1: s.Motion = MotionState.Moving; break; case 2: s.Motion = MotionState.Hold; break; } s.Emergency = odbst.emergency == 1; s.Alarm = odbst.alarm == 1; return s; } private struct ProgramInfo { public string MainProgram; public string SubProgram; public string Comment; } private ProgramInfo ReadProgramInfo() { var info = new ProgramInfo { MainProgram = "", SubProgram = "", Comment = "" }; var prog = new Focas1.ODBPRO(); short ret = Focas1.cnc_rdprgnum(_hndl, prog); if (ret != Focas1.EW_OK) { Record(ret, "cnc_rdprgnum"); return info; } info.SubProgram = "O" + prog.data.ToString(); info.MainProgram = "O" + prog.mdata.ToString(); if (_dialect.SupportsPathBasedPrograms) { byte[] mainPath = new byte[242]; short pathRet = Focas1.cnc_pdf_rdmain(_hndl, mainPath); if (pathRet == Focas1.EW_OK) { string str = ASCIIEncoding.ASCII.GetString(mainPath).TrimEnd('\0'); string[] parts = str.Split('/'); if (parts.Length > 0) info.MainProgram = parts[parts.Length - 1]; } } byte[] commentBuf = new byte[100]; int charsRead; short cret = _dialect.ReadActiveProgramComment(_hndl, commentBuf, out charsRead); if (cret != Focas1.EW_OK) { Record(cret, "ReadActiveProgramComment"); return info; } string raw = ASCIIEncoding.ASCII.GetString(commentBuf).TrimEnd('\0'); int open = raw.IndexOf('('); int close = raw.IndexOf(')'); if (open >= 0 && close > open) info.Comment = raw.Substring(open + 1, close - open - 1); else info.Comment = ""; return info; } private string UploadProgramByIdentifier(string identifier, CancellationToken ct) { long programNumber = TryParseProgramNumber(identifier); short ret = _dialect.UploadStart(_hndl, identifier, programNumber); if (ret != Focas1.EW_OK) { Record(ret, "UploadStart"); _dialect.UploadEnd(_hndl); return null; } var output = new StringBuilder(); char[] buffer = new char[UploadBufferSize + 1]; try { while (true) { ct.ThrowIfCancellationRequested(); int len = UploadBufferSize; short r = _dialect.UploadData(_hndl, ref len, buffer); if (r == (short)Focas1.focas_ret.EW_BUFFER) { if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested(); continue; } if (r == Focas1.EW_OK && len > 0) { output.Append(buffer, 0, len); if (len > 0 && buffer[len - 1] == '%') break; } else { if (r != Focas1.EW_OK) Record(r, "UploadData"); break; } } } finally { _dialect.UploadEnd(_hndl); } return output.ToString(); } private bool TransferChunked(string content, CancellationToken ct) { int len = content.Length; int startPos = 0; while (len > 0) { ct.ThrowIfCancellationRequested(); int num = len >= TransferChunkSize ? TransferChunkSize : len; char[] chunk = new char[num]; content.CopyTo(startPos, chunk, 0, num); short ret = _dialect.DownloadData(_hndl, ref num, chunk); if (ret == (short)Focas1.focas_ret.EW_BUFFER) { if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested(); continue; } if (ret == Focas1.EW_OK) { startPos += num; len -= num; continue; } Record(ret, "DownloadData"); var dtl = new Focas1.ODBERR(); Focas1.cnc_getdtailerr(_hndl, dtl); Debug.WriteLine("FOCAS detail err: " + dtl.err_no + " " + dtl.err_dtno); return false; } return true; } private bool CanDownload() { var odbst = new Focas1.ODBST(); short ret = Focas1.cnc_statinfo(_hndl, odbst); if (ret != Focas1.EW_OK) { Record(ret, "cnc_statinfo(canDownload)"); return false; } if (odbst.aut != 0 && odbst.aut != 3) { Record((short)Focas1.focas_ret.EW_MODE, "CanDownload"); return false; } return true; } private void EnsureSeedProgram(string folderPath, CancellationToken ct) { if (!_dialect.SupportsPathBasedPrograms) return; short selRet = Focas1.cnc_pdf_slctmain(_hndl, folderPath + SeedProgramName); if (selRet == Focas1.EW_OK) return; Focas1.cnc_pdf_del(_hndl, folderPath + SeedProgramName); short sStart = _dialect.DownloadStart(_hndl, folderPath); if (sStart != Focas1.EW_OK) { _dialect.DownloadEnd(_hndl); return; } TransferChunked(SeedProgramBody, ct); _dialect.DownloadEnd(_hndl); Focas1.cnc_pdf_slctmain(_hndl, folderPath + SeedProgramName); } private static string ExtractFileNameFromContent(CncProgram program) { if (!string.IsNullOrEmpty(program.Name)) return program.Name; string content = program.Content ?? ""; int start = content.IndexOf('<'); int end = content.IndexOf('>'); if (start >= 0 && end > start) return content.Substring(start + 1, end - start - 1); return null; } private static long TryParseProgramNumber(string identifier) { if (string.IsNullOrEmpty(identifier)) return 0; string s = identifier; int slash = s.LastIndexOf('/'); if (slash >= 0 && slash < s.Length - 1) s = s.Substring(slash + 1); if (s.Length > 0 && (s[0] == 'O' || s[0] == 'o')) s = s.Substring(1); long n; return long.TryParse(s, out n) ? n : 0; } private void EnsureConnected() { if (_state != ConnectionState.Connected || _dialect == null) throw new InvalidOperationException("FanucMachine is not connected."); } private void Record(short code, string source) { var err = FanucErrorTranslator.Translate(code, source); _currentErrors.Add(err); if (FanucErrorTranslator.IsDisconnectError(code)) { SetState(ConnectionState.Faulted); try { Focas1.cnc_freelibhndl(_hndl); } catch { } SetState(ConnectionState.Disconnected); } } private void SetState(ConnectionState next) { if (_state == next) return; _state = next; var h = StateChanged; if (h != null) { try { h(this, next); } catch { } } } } }