nc_program_manager/Cnc/Fanuc/FanucMachine.cs

739 lines
28 KiB
C#
Executable file

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FanucProgramManager;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.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<OSTD>\n%";
private readonly FanucConnectionConfig _config;
private readonly INcProgramValidator _validator;
private readonly List<CncError> _currentErrors = new List<CncError>();
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<ConnectionState> StateChanged;
public Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => ConnectCore(ct), ct);
}
public Task DisconnectAsync()
{
return RunGuardedAsync<bool>(() => { DisconnectCore(); return true; }, CancellationToken.None);
}
public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => ReadCore(ct), ct);
}
public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => ReadActiveProgramCore(ct), ct);
}
public Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => ReadProgramCore(path, ct), ct);
}
public Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => WriteProgramCore(path, program, ct), ct);
}
public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => SelectMainProgramCore(path), ct);
}
public Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => DeleteProgramCore(path), ct);
}
public Task<CncResult<string>> ReadToolDataAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => UploadFocasFile(1, path, ct), ct);
}
public Task<CncResult<string>> ReadWorkZeroDataAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => UploadFocasFile(5, path, ct), ct);
}
public Task<CncResult<bool>> WriteToolDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => DownloadFocasFile(1, folderPath, content, ct), ct);
}
public Task<CncResult<bool>> 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<CncResult<T>> RunGuardedAsync<T>(Func<T> 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<T>.Fail(_currentErrors.ToArray());
return CncResult<T>.Ok(value);
}
catch (OperationCanceledException)
{
return CncResult<T>.Cancelled();
}
catch (Exception ex)
{
_currentErrors.Add(new CncError(0, "FanucMachine", ex.Message));
return CncResult<T>.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<string>(),
OperatorMessages = new List<string>(),
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<string> 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<string>(num);
for (int i = 0; i < num; i++)
if (!string.IsNullOrEmpty(buf[i])) list.Add(buf[i]);
return list;
}
private IReadOnlyList<string> 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<string>(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 { }
}
}
}
}