feat(16-fagor): Fagor CNC FTP support + validator gating
Phase 16 (v0.6 milestone) — supporto Fagor CNC via FTP, riuso pattern Siemens/Mitsubishi (FtpWebRequest + ASCII). Modelli supportati: 8060/8065/8070 (FTP nativo), 8055 (con opzione Ethernet). Modelli 8025-8050 esclusi (solo RS232). Code: - Cnc/Fagor/* — FagorConnectionConfig, IFagorFtpClient, FagorFtpClient, FagorMachine. ProgramExtension configurabile (default .nc), ProgramDirectory /Users/Prg/. - Cnc/Models/CncManufacturer.cs — enum + Fagor. - Cnc/CncMachineFactory.cs — case Fagor, porta default 21. - Cnc/NcProgramValidator.cs — refactor IsRestrictiveDialect(): block-80/no-tab/no-lowercase/nested-parens/comment-32 ora applicati solo a Fanuc+Mitsubishi (Heidenhain/Siemens/Fagor esenti). Fagor name rules: max 24 char, no ñ (escape ñ per safety codepage), stem alphanumerico + spazi (estensione separata). - ArgParser.cs — case "fagor". - Program.cs — help text manufacturer/username/password. Tests: - FagorMachineTests — 12 unit (10 mock + 2 validator reale). - FagorMachineIntegrationTests — 5 stub-based (riusa FtpServerStub) + 1 hardware skip via FAGOR_TEST_IP env var. - NcProgramValidatorTests — 12 nuovi Fagor (8 + 4 extension coverage), 6 lock Siemens/Heidenhain exemption, 3 esistenti Siemens→Fanuc. - CncMachineFactoryTests — Create_Fagor_ReturnsFagorMachine. Docs: - README.md — sezione "Controller Fagor" italiano: modelli supportati/ non supportati, regole nome, env var test. Build: - *.csproj — 4 Compile Include main + 2 test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
60ce1c9d57
commit
b3d7402498
16 changed files with 965 additions and 32 deletions
|
|
@ -31,6 +31,7 @@ namespace NcProgramManager
|
||||||
case "heidenhain": inputArgs.manufacturer = CncManufacturer.Heidenhain; break;
|
case "heidenhain": inputArgs.manufacturer = CncManufacturer.Heidenhain; break;
|
||||||
case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break;
|
case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break;
|
||||||
case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break;
|
case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break;
|
||||||
|
case "fagor": inputArgs.manufacturer = CncManufacturer.Fagor; break;
|
||||||
default: inputArgs.manufacturer = CncManufacturer.Fanuc; break;
|
default: inputArgs.manufacturer = CncManufacturer.Fanuc; break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using NcProgramManager.Cnc.Fagor;
|
||||||
using NcProgramManager.Cnc.Fanuc;
|
using NcProgramManager.Cnc.Fanuc;
|
||||||
using NcProgramManager.Cnc.Heidenhain;
|
using NcProgramManager.Cnc.Heidenhain;
|
||||||
using NcProgramManager.Cnc.Mitsubishi;
|
using NcProgramManager.Cnc.Mitsubishi;
|
||||||
|
|
@ -44,6 +45,16 @@ namespace NcProgramManager.Cnc
|
||||||
Password = args.password
|
Password = args.password
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case CncManufacturer.Fagor:
|
||||||
|
return new FagorMachine(new FagorConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "Fagor",
|
||||||
|
IpAddress = args.ip,
|
||||||
|
Port = string.IsNullOrEmpty(args.porta) ? 21 : int.Parse(args.porta),
|
||||||
|
Username = args.username,
|
||||||
|
Password = args.password
|
||||||
|
});
|
||||||
|
|
||||||
default: // Fanuc
|
default: // Fanuc
|
||||||
return new FanucMachine(new FanucConnectionConfig
|
return new FanucMachine(new FanucConnectionConfig
|
||||||
{
|
{
|
||||||
|
|
|
||||||
18
Cnc/Fagor/FagorConnectionConfig.cs
Normal file
18
Cnc/Fagor/FagorConnectionConfig.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NcProgramManager.Cnc.Fagor
|
||||||
|
{
|
||||||
|
public sealed class FagorConnectionConfig
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string IpAddress { get; set; }
|
||||||
|
public int Port { get; set; } = 21;
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
/// <summary>Directory dei programmi sul server FTP Fagor. Default valido per 8060/8065/8070.</summary>
|
||||||
|
public string ProgramDirectory { get; set; } = "/Users/Prg/";
|
||||||
|
/// <summary>Estensione applicata ai nomi programma senza estensione. Default `.nc` per Fagor 8060/8065/8070.</summary>
|
||||||
|
public string ProgramExtension { get; set; } = ".nc";
|
||||||
|
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Cnc/Fagor/FagorFtpClient.cs
Normal file
71
Cnc/Fagor/FagorFtpClient.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace NcProgramManager.Cnc.Fagor
|
||||||
|
{
|
||||||
|
/// <summary>Client FTP per trasferimento programmi Fagor CNC.</summary>
|
||||||
|
internal sealed class FagorFtpClient : IFagorFtpClient
|
||||||
|
{
|
||||||
|
private readonly FagorConnectionConfig _config;
|
||||||
|
|
||||||
|
public FagorFtpClient(FagorConnectionConfig config)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException("config");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifica raggiungibilità server FTP listando la directory programmi.</summary>
|
||||||
|
public bool Ping()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var req = CreateRequest(_config.ProgramDirectory, WebRequestMethods.Ftp.ListDirectory);
|
||||||
|
using (var resp = (FtpWebResponse)req.GetResponse())
|
||||||
|
return resp.StatusCode == FtpStatusCode.DataAlreadyOpen
|
||||||
|
|| resp.StatusCode == FtpStatusCode.OpeningData
|
||||||
|
|| (int)resp.StatusCode / 100 == 2;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Scarica file dal Fagor in stringa.</summary>
|
||||||
|
public string DownloadFile(string ncPath)
|
||||||
|
{
|
||||||
|
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.DownloadFile);
|
||||||
|
using (var resp = (FtpWebResponse)req.GetResponse())
|
||||||
|
using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.ASCII))
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Carica contenuto stringa su Fagor.</summary>
|
||||||
|
public void UploadFile(string ncPath, string content)
|
||||||
|
{
|
||||||
|
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.UploadFile);
|
||||||
|
byte[] data = Encoding.ASCII.GetBytes(content);
|
||||||
|
req.ContentLength = data.Length;
|
||||||
|
using (var stream = req.GetRequestStream())
|
||||||
|
stream.Write(data, 0, data.Length);
|
||||||
|
using ((FtpWebResponse)req.GetResponse()) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteFile(string ncPath)
|
||||||
|
{
|
||||||
|
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.DeleteFile);
|
||||||
|
using ((FtpWebResponse)req.GetResponse()) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private FtpWebRequest CreateRequest(string path, string method)
|
||||||
|
{
|
||||||
|
string uri = string.Format("ftp://{0}:{1}{2}", _config.IpAddress, _config.Port, path);
|
||||||
|
var req = (FtpWebRequest)WebRequest.Create(uri);
|
||||||
|
req.Method = method;
|
||||||
|
req.Credentials = new NetworkCredential(_config.Username, _config.Password);
|
||||||
|
req.Timeout = (int)_config.RequestTimeout.TotalMilliseconds;
|
||||||
|
req.KeepAlive = false;
|
||||||
|
req.UseBinary = false;
|
||||||
|
req.UsePassive = true;
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
218
Cnc/Fagor/FagorMachine.cs
Normal file
218
Cnc/Fagor/FagorMachine.cs
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<CncError> _errors = new List<CncError>();
|
||||||
|
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<ConnectionState> StateChanged;
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> 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<bool>(() =>
|
||||||
|
{
|
||||||
|
SetState(ConnectionState.Disconnected);
|
||||||
|
return true;
|
||||||
|
}, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> 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<CncResult<bool>> 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<CncResult<bool>> 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<CncResult<MachineSnapshot>> 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<string>(),
|
||||||
|
OperatorMessages = new List<string>()
|
||||||
|
};
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return Task.FromResult(CncResult<string>.Fail("ReadActiveProgram", "Not supported via FTP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return Task.FromResult(CncResult<bool>.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<CncResult<T>> RunGuardedAsync<T>(Func<T> 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<T>.Fail(_errors.ToArray())
|
||||||
|
: CncResult<T>.Ok(value);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { return CncResult<T>.Cancelled(); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "FagorMachine", ex.Message));
|
||||||
|
return CncResult<T>.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"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Costruisce path FTP completo. Se name è fornito, append `_config.ProgramExtension` se manca estensione.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Cnc/Fagor/IFagorFtpClient.cs
Normal file
10
Cnc/Fagor/IFagorFtpClient.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace NcProgramManager.Cnc.Fagor
|
||||||
|
{
|
||||||
|
internal interface IFagorFtpClient
|
||||||
|
{
|
||||||
|
bool Ping();
|
||||||
|
string DownloadFile(string ncPath);
|
||||||
|
void UploadFile(string ncPath, string content);
|
||||||
|
void DeleteFile(string ncPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace NcProgramManager.Cnc.Models
|
namespace NcProgramManager.Cnc.Models
|
||||||
{
|
{
|
||||||
public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi }
|
public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi, Fagor }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,34 +45,39 @@ namespace NcProgramManager.Cnc
|
||||||
errors.Add(new ValidationError(1, "First line must be O#### program number", ValidationSeverity.Error));
|
errors.Add(new ValidationError(1, "First line must be O#### program number", ValidationSeverity.Error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool restrictive = IsRestrictiveDialect();
|
||||||
|
|
||||||
for (int i = 0; i < lines.Length; i++)
|
for (int i = 0; i < lines.Length; i++)
|
||||||
{
|
{
|
||||||
int lineNum = i + 1;
|
int lineNum = i + 1;
|
||||||
string line = lines[i];
|
string line = lines[i];
|
||||||
|
|
||||||
// Tab check
|
if (restrictive)
|
||||||
if (line.IndexOf('\t') >= 0)
|
|
||||||
errors.Add(new ValidationError(lineNum, "Tab character not allowed", ValidationSeverity.Error));
|
|
||||||
|
|
||||||
// Lowercase check
|
|
||||||
for (int ci = 0; ci < line.Length; ci++)
|
|
||||||
{
|
{
|
||||||
if (char.IsLower(line[ci]))
|
// Tab check
|
||||||
|
if (line.IndexOf('\t') >= 0)
|
||||||
|
errors.Add(new ValidationError(lineNum, "Tab character not allowed", ValidationSeverity.Error));
|
||||||
|
|
||||||
|
// Lowercase check
|
||||||
|
for (int ci = 0; ci < line.Length; ci++)
|
||||||
{
|
{
|
||||||
errors.Add(new ValidationError(lineNum, "Lowercase letters not allowed", ValidationSeverity.Error));
|
if (char.IsLower(line[ci]))
|
||||||
break;
|
{
|
||||||
|
errors.Add(new ValidationError(lineNum, "Lowercase letters not allowed", ValidationSeverity.Error));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block length
|
||||||
|
if (line.Length > MaxBlockLength)
|
||||||
|
errors.Add(new ValidationError(lineNum,
|
||||||
|
string.Format("Block length {0} exceeds 80 chars", line.Length),
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
|
||||||
|
// Nested parens + comment length
|
||||||
|
ValidateParensAndComments(line, lineNum, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block length
|
|
||||||
if (line.Length > MaxBlockLength)
|
|
||||||
errors.Add(new ValidationError(lineNum,
|
|
||||||
string.Format("Block length {0} exceeds 80 chars", line.Length),
|
|
||||||
ValidationSeverity.Error));
|
|
||||||
|
|
||||||
// Nested parens + comment length
|
|
||||||
ValidateParensAndComments(line, lineNum, errors);
|
|
||||||
|
|
||||||
// Forbidden chars per manufacturer
|
// Forbidden chars per manufacturer
|
||||||
ValidateForbiddenChars(line, lineNum, errors);
|
ValidateForbiddenChars(line, lineNum, errors);
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +94,12 @@ namespace NcProgramManager.Cnc
|
||||||
|
|
||||||
// ── private helpers ───────────────────────────────────────────────────
|
// ── private helpers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private bool IsRestrictiveDialect()
|
||||||
|
{
|
||||||
|
return _manufacturer == CncManufacturer.Fanuc
|
||||||
|
|| _manufacturer == CncManufacturer.Mitsubishi;
|
||||||
|
}
|
||||||
|
|
||||||
private void ValidateName(string name, List<ValidationError> errors)
|
private void ValidateName(string name, List<ValidationError> errors)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(name)) return;
|
if (string.IsNullOrEmpty(name)) return;
|
||||||
|
|
@ -116,6 +127,33 @@ namespace NcProgramManager.Cnc
|
||||||
string.Format("Program name '{0}' must end with .H", name),
|
string.Format("Program name '{0}' must end with .H", name),
|
||||||
ValidationSeverity.Error));
|
ValidationSeverity.Error));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case CncManufacturer.Fagor:
|
||||||
|
// Estrai stem ed estensione (solo ultima estensione conta)
|
||||||
|
int dotIdx = name.LastIndexOf('.');
|
||||||
|
string stem = dotIdx >= 0 ? name.Substring(0, dotIdx) : name;
|
||||||
|
// Lunghezza: regola applicata sul nome completo (incluso eventuale .ext)
|
||||||
|
if (name.Length > 24)
|
||||||
|
errors.Add(new ValidationError(0,
|
||||||
|
string.Format("Nome programma '{0}' supera 24 caratteri", name),
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
if (name.IndexOf('\u00f1') >= 0)
|
||||||
|
errors.Add(new ValidationError(0,
|
||||||
|
"Carattere '\u00f1' non ammesso nel nome programma Fagor",
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
// Stem deve contenere solo lettere/cifre/spazi
|
||||||
|
for (int i = 0; i < stem.Length; i++)
|
||||||
|
{
|
||||||
|
char c = stem[i];
|
||||||
|
if (!(char.IsLetterOrDigit(c) || c == ' '))
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationError(0,
|
||||||
|
string.Format("Carattere '{0}' non ammesso nel nome programma Fagor (solo lettere/cifre/spazi)", c),
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,6 +237,10 @@ namespace NcProgramManager.Cnc
|
||||||
ValidationSeverity.Error));
|
ValidationSeverity.Error));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case CncManufacturer.Fagor:
|
||||||
|
// Fagor: nessuna restrizione su contenuto programma (ISO + linguaggio alto livello)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NcProgramManager.Cnc.Models;
|
||||||
|
using NcProgramManager.Cnc.Fagor;
|
||||||
|
using NcProgramManager.Tests.Integration.Stubs;
|
||||||
|
|
||||||
|
namespace NcProgramManager.Tests.Integration
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
[Category("Integration")]
|
||||||
|
public class FagorMachineIntegrationTests
|
||||||
|
{
|
||||||
|
private FtpServerStub _stub;
|
||||||
|
private FagorMachine _machine;
|
||||||
|
private const string ProgramDir = "/Users/Prg/";
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_stub = new FtpServerStub();
|
||||||
|
var config = new FagorConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "FagorIntegration",
|
||||||
|
IpAddress = "127.0.0.1",
|
||||||
|
Port = _stub.Port,
|
||||||
|
Username = "user",
|
||||||
|
Password = "pass",
|
||||||
|
ProgramDirectory = ProgramDir,
|
||||||
|
ProgramExtension = ".nc",
|
||||||
|
RequestTimeout = TimeSpan.FromSeconds(5)
|
||||||
|
};
|
||||||
|
_machine = new FagorMachine(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
_machine?.Dispose();
|
||||||
|
_stub?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConnectAsync_StubListens_ResultOk()
|
||||||
|
{
|
||||||
|
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteThenRead_ProgramRoundTrips()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
string content = "G0 X0 Y0\nM30";
|
||||||
|
var prog = new CncProgram { Name = "PROG1", Content = content };
|
||||||
|
|
||||||
|
var writeResult = _machine.WriteProgramAsync(ProgramDir, prog).GetAwaiter().GetResult();
|
||||||
|
Assert.That(writeResult.Success, Is.True);
|
||||||
|
|
||||||
|
// ProgramExtension ".nc" applied by FagorMachine
|
||||||
|
string storedPath = ProgramDir + "PROG1.nc";
|
||||||
|
var readResult = _machine.ReadProgramAsync(storedPath).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(readResult.Success, Is.True);
|
||||||
|
Assert.That(readResult.Value, Is.EqualTo(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeleteProgramAsync_FileExistsInStub_RemovedFromStub()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
string path = ProgramDir + "PROG2.nc";
|
||||||
|
_stub.Files[path] = "SOME_CONTENT";
|
||||||
|
|
||||||
|
var result = _machine.DeleteProgramAsync(path).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(_stub.Files.ContainsKey(path), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_FileNotInStub_ResultFail()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.ReadProgramAsync(ProgramDir + "DOESNOTEXIST.nc").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_InvalidName_ValidationFails()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
// Fagor name max 24 chars — 25 'A' triggers validator failure
|
||||||
|
var prog = new CncProgram { Name = new string('A', 25), Content = "G00 X10\nM30" };
|
||||||
|
|
||||||
|
var result = _machine.WriteProgramAsync(ProgramDir, prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Category("Hardware")]
|
||||||
|
public void Hardware_ConnectAsync_RealMachine()
|
||||||
|
{
|
||||||
|
string ip = HardwareTestHelper.GetIpOrSkip("FAGOR_TEST_IP");
|
||||||
|
var config = new FagorConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "FagorHardware",
|
||||||
|
IpAddress = ip,
|
||||||
|
Port = 21,
|
||||||
|
Username = Environment.GetEnvironmentVariable("FAGOR_USER") ?? "user",
|
||||||
|
Password = Environment.GetEnvironmentVariable("FAGOR_PASS") ?? "pass",
|
||||||
|
ProgramDirectory = "/Users/Prg/",
|
||||||
|
ProgramExtension = ".nc",
|
||||||
|
RequestTimeout = TimeSpan.FromSeconds(10)
|
||||||
|
};
|
||||||
|
using (var machine = new FagorMachine(config))
|
||||||
|
{
|
||||||
|
var result = machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
Assert.That(result.Success, Is.True, "ConnectAsync failed: " + result.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -61,12 +61,14 @@
|
||||||
<Compile Include="Unit\SiemensMachineTests.cs" />
|
<Compile Include="Unit\SiemensMachineTests.cs" />
|
||||||
<Compile Include="Unit\MitsubishiMachineTests.cs" />
|
<Compile Include="Unit\MitsubishiMachineTests.cs" />
|
||||||
<Compile Include="Unit\HeidenhainMachineTests.cs" />
|
<Compile Include="Unit\HeidenhainMachineTests.cs" />
|
||||||
|
<Compile Include="Unit\FagorMachineTests.cs" />
|
||||||
<Compile Include="Integration\Stubs\FtpServerStub.cs" />
|
<Compile Include="Integration\Stubs\FtpServerStub.cs" />
|
||||||
<Compile Include="Integration\Stubs\Lsv2ServerStub.cs" />
|
<Compile Include="Integration\Stubs\Lsv2ServerStub.cs" />
|
||||||
<Compile Include="Integration\HardwareTestHelper.cs" />
|
<Compile Include="Integration\HardwareTestHelper.cs" />
|
||||||
<Compile Include="Integration\SiemensMachineIntegrationTests.cs" />
|
<Compile Include="Integration\SiemensMachineIntegrationTests.cs" />
|
||||||
<Compile Include="Integration\MitsubishiMachineIntegrationTests.cs" />
|
<Compile Include="Integration\MitsubishiMachineIntegrationTests.cs" />
|
||||||
<Compile Include="Integration\HeidenhainMachineIntegrationTests.cs" />
|
<Compile Include="Integration\HeidenhainMachineIntegrationTests.cs" />
|
||||||
|
<Compile Include="Integration\FagorMachineIntegrationTests.cs" />
|
||||||
<Compile Include="Unit\LicenseFileTests.cs" />
|
<Compile Include="Unit\LicenseFileTests.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using NcProgramManager.Cnc.Heidenhain;
|
||||||
using NcProgramManager.Cnc.Mitsubishi;
|
using NcProgramManager.Cnc.Mitsubishi;
|
||||||
using NcProgramManager.Cnc.Models;
|
using NcProgramManager.Cnc.Models;
|
||||||
using NcProgramManager.Cnc.Siemens;
|
using NcProgramManager.Cnc.Siemens;
|
||||||
|
using NcProgramManager.Cnc.Fagor;
|
||||||
|
|
||||||
namespace NcProgramManager.Tests.Unit
|
namespace NcProgramManager.Tests.Unit
|
||||||
{
|
{
|
||||||
|
|
@ -81,6 +82,18 @@ namespace NcProgramManager.Tests.Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_Fagor_ReturnsFagorMachine()
|
||||||
|
{
|
||||||
|
var args = BaseArgs();
|
||||||
|
args.manufacturer = CncManufacturer.Fagor;
|
||||||
|
|
||||||
|
using (var m = CncMachineFactory.Create(args))
|
||||||
|
{
|
||||||
|
Assert.That(m, Is.InstanceOf<FagorMachine>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Create_FanucHssbNode_TransportIsHssb()
|
public void Create_FanucHssbNode_TransportIsHssb()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
212
NcProgramManager.Tests/Unit/FagorMachineTests.cs
Normal file
212
NcProgramManager.Tests/Unit/FagorMachineTests.cs
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Moq;
|
||||||
|
using NcProgramManager.Cnc;
|
||||||
|
using NcProgramManager.Cnc.Models;
|
||||||
|
using NcProgramManager.Cnc.Fagor;
|
||||||
|
|
||||||
|
namespace NcProgramManager.Tests.Unit
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class FagorMachineTests
|
||||||
|
{
|
||||||
|
private FagorConnectionConfig _config;
|
||||||
|
private Mock<IFagorFtpClient> _ftpMock;
|
||||||
|
private Mock<INcProgramValidator> _validatorMock;
|
||||||
|
private FagorMachine _machine;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_config = new FagorConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "TestFagor",
|
||||||
|
IpAddress = "192.168.1.2",
|
||||||
|
Port = 21,
|
||||||
|
ProgramDirectory = "/Users/Prg/",
|
||||||
|
ProgramExtension = ".nc"
|
||||||
|
};
|
||||||
|
_ftpMock = new Mock<IFagorFtpClient>(MockBehavior.Strict);
|
||||||
|
// Always-ok validator so existing write tests are not affected by validation rules
|
||||||
|
_validatorMock = new Mock<INcProgramValidator>();
|
||||||
|
_validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns(ValidationResult.Ok());
|
||||||
|
_machine = new FagorMachine(_config, _ftpMock.Object, _validatorMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
_machine?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConnectAsync_PingTrue_StateConnectedAndResultOk()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
|
||||||
|
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(result.Value, Is.True);
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConnectAsync_PingFalse_StateFaultedAndResultFail()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(false);
|
||||||
|
|
||||||
|
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Faulted));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_FullPath_CallsDownloadFileWithPath()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.DownloadFile("/Users/Prg/PROG1.nc")).Returns("CONTENT");
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.ReadProgramAsync("/Users/Prg/PROG1.nc").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(result.Value, Is.EqualTo("CONTENT"));
|
||||||
|
_ftpMock.Verify(f => f.DownloadFile("/Users/Prg/PROG1.nc"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_ShortName_ResolvesWithDirectoryAndExtension()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.DownloadFile("/Users/Prg/PROG1.nc")).Returns("DATA");
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
_machine.ReadProgramAsync("PROG1").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
_ftpMock.Verify(f => f.DownloadFile("/Users/Prg/PROG1.nc"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_ValidContent_CallsUploadFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.UploadFile(It.IsAny<string>(), "CONTENT"));
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = "PROG1", Content = "CONTENT" };
|
||||||
|
var result = _machine.WriteProgramAsync("/Users/Prg/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), "CONTENT"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_EmptyContent_FailsWithoutCallingUploadFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = "PROG1", Content = "" };
|
||||||
|
var result = _machine.WriteProgramAsync("/Users/Prg/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_NullProgram_FailsWithoutCallingUploadFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.WriteProgramAsync("/Users/Prg/", null).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeleteProgramAsync_ValidPath_CallsDeleteFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.DeleteFile("/Users/Prg/PROG1.nc"));
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.DeleteProgramAsync("/Users/Prg/PROG1.nc").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
_ftpMock.Verify(f => f.DeleteFile("/Users/Prg/PROG1.nc"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadActiveProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
|
||||||
|
{
|
||||||
|
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectMainProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SelectMainProgramAsync_ReturnsFailNotSupported()
|
||||||
|
{
|
||||||
|
var result = _machine.SelectMainProgramAsync("/Users/Prg/PROG1.nc").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator-real tests (NcProgramValidator with CncManufacturer.Fagor)
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_NameTooLong_ValidationFails()
|
||||||
|
{
|
||||||
|
var realMachine = new FagorMachine(_config, _ftpMock.Object,
|
||||||
|
new NcProgramValidator(CncManufacturer.Fagor));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
realMachine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = new string('A', 25), Content = "G00 X10" };
|
||||||
|
var result = realMachine.WriteProgramAsync("/Users/Prg/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
finally { realMachine.Dispose(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_NameContainsTilde_ValidationFails()
|
||||||
|
{
|
||||||
|
var realMachine = new FagorMachine(_config, _ftpMock.Object,
|
||||||
|
new NcProgramValidator(CncManufacturer.Fagor));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
realMachine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = "MAñANA", Content = "G00 X10" };
|
||||||
|
var result = realMachine.WriteProgramAsync("/Users/Prg/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
finally { realMachine.Dispose(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,8 +32,8 @@ namespace NcProgramManager.Tests.Unit
|
||||||
[Test]
|
[Test]
|
||||||
public void Validate_LowercaseLetter_ReturnsError()
|
public void Validate_LowercaseLetter_ReturnsError()
|
||||||
{
|
{
|
||||||
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
var r = v.Validate("PROG.MPF", "PROG.MPF\ng00 X10");
|
var r = v.Validate("O1234", "O1234\ng00 X10");
|
||||||
Assert.That(r.Success, Is.False);
|
Assert.That(r.Success, Is.False);
|
||||||
Assert.That(HasErrorContaining(r, "Lowercase"), Is.True);
|
Assert.That(HasErrorContaining(r, "Lowercase"), Is.True);
|
||||||
}
|
}
|
||||||
|
|
@ -41,16 +41,16 @@ namespace NcProgramManager.Tests.Unit
|
||||||
[Test]
|
[Test]
|
||||||
public void Validate_NoLowercase_NoLowercaseError()
|
public void Validate_NoLowercase_NoLowercaseError()
|
||||||
{
|
{
|
||||||
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
var r = v.Validate("PROG.MPF", "G00 X10 Y20");
|
var r = v.Validate("O1234", "O1234\nG00 X10 Y20");
|
||||||
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
|
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Validate_TabCharacter_ReturnsError()
|
public void Validate_TabCharacter_ReturnsError()
|
||||||
{
|
{
|
||||||
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
var r = v.Validate("PROG.MPF", "G00\tX10");
|
var r = v.Validate("O1234", "O1234\nG00\tX10");
|
||||||
Assert.That(r.Success, Is.False);
|
Assert.That(r.Success, Is.False);
|
||||||
Assert.That(HasErrorContaining(r, "Tab"), Is.True);
|
Assert.That(HasErrorContaining(r, "Tab"), Is.True);
|
||||||
}
|
}
|
||||||
|
|
@ -223,6 +223,32 @@ namespace NcProgramManager.Tests.Unit
|
||||||
Assert.That(HasErrorContaining(r, "First line"), Is.False);
|
Assert.That(HasErrorContaining(r, "First line"), Is.False);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Siemens_LowercaseContent_NoLowercaseError()
|
||||||
|
{
|
||||||
|
// Dopo refactor: Siemens esente da check lowercase
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
var r = v.Validate("PROG.MPF", "g00 x10 y20");
|
||||||
|
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Siemens_TabInContent_NoTabError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
var r = v.Validate("PROG.MPF", "G00\tX10");
|
||||||
|
Assert.That(HasErrorContaining(r, "Tab"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Siemens_LongBlock_NoBlockLengthError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
string longLine = new string('G', 120);
|
||||||
|
var r = v.Validate("PROG.MPF", longLine);
|
||||||
|
Assert.That(HasErrorContaining(r, "Block length"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Heidenhain rules ──────────────────────────────────────────────────
|
// ── Heidenhain rules ──────────────────────────────────────────────────
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -259,6 +285,138 @@ namespace NcProgramManager.Tests.Unit
|
||||||
Assert.That(HasErrorContaining(r, "First line"), Is.False);
|
Assert.That(HasErrorContaining(r, "First line"), Is.False);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Heidenhain_LowercaseContent_NoLowercaseError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
|
||||||
|
var r = v.Validate("PROG.H", "l x+10 y+20");
|
||||||
|
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Heidenhain_TabInContent_NoTabError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
|
||||||
|
var r = v.Validate("PROG.H", "L\tX+10");
|
||||||
|
Assert.That(HasErrorContaining(r, "Tab"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Heidenhain_LongBlock_NoBlockLengthError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
|
||||||
|
string longLine = "L X+" + new string('1', 100);
|
||||||
|
var r = v.Validate("PROG.H", longLine);
|
||||||
|
Assert.That(HasErrorContaining(r, "Block length"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fagor rules ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_ValidNameAndContent_Succeeds()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate("PROG1", "G00 X10 Y20\nM30");
|
||||||
|
Assert.That(r.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_NameTooLong_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate(new string('A', 25), "G00 X10");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "24"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_NameContainsTilde_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate("MA\u00f1ANA", "G00 X10");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "\u00f1"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_NameWithSymbol_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate("PROG@1", "G00 X10");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_LongBlock_NoBlockLengthError()
|
||||||
|
{
|
||||||
|
// Fagor esentato da check block 80
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
string longLine = new string('G', 120);
|
||||||
|
var r = v.Validate("PROG1", longLine);
|
||||||
|
Assert.That(HasErrorContaining(r, "Block length"), Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "80"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_Lowercase_NoLowercaseError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate("PROG1", "g00 x10");
|
||||||
|
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_Tab_NoTabError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate("PROG1", "G00\tX10");
|
||||||
|
Assert.That(HasErrorContaining(r, "Tab"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_NestedParens_NoNestedError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate("PROG1", "G00 ((NESTED))");
|
||||||
|
Assert.That(HasErrorContaining(r, "Nested"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_NameWithNcExtension_Succeeds()
|
||||||
|
{
|
||||||
|
// Regression: default ProgramExtension `.nc` non deve essere rifiutato
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate("PROG1.nc", "G00 X10");
|
||||||
|
Assert.That(r.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_NameWithCustomExtension_Succeeds()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate("PROG1.pim", "G00 X10");
|
||||||
|
Assert.That(r.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_StemWithSymbol_ReturnsError()
|
||||||
|
{
|
||||||
|
// Symbol nello stem invariato come errore
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate("PROG@1.nc", "G00 X10");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fagor_NameContainsTildeEscaped_ReturnsError()
|
||||||
|
{
|
||||||
|
// Test con escape unicode esplicito \u00f1 per evitare codepage issues
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fagor);
|
||||||
|
var r = v.Validate("MA\u00f1ANA", "G00 X10");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "\u00f1"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static bool HasErrorContaining(ValidationResult r, string fragment)
|
private static bool HasErrorContaining(ValidationResult r, string fragment)
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,10 @@
|
||||||
<Compile Include="Cnc\Mitsubishi\IMitsubishiFtpClient.cs" />
|
<Compile Include="Cnc\Mitsubishi\IMitsubishiFtpClient.cs" />
|
||||||
<Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.cs" />
|
<Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.cs" />
|
||||||
<Compile Include="Cnc\Mitsubishi\MitsubishiMachine.cs" />
|
<Compile Include="Cnc\Mitsubishi\MitsubishiMachine.cs" />
|
||||||
|
<Compile Include="Cnc\Fagor\FagorConnectionConfig.cs" />
|
||||||
|
<Compile Include="Cnc\Fagor\IFagorFtpClient.cs" />
|
||||||
|
<Compile Include="Cnc\Fagor\FagorFtpClient.cs" />
|
||||||
|
<Compile Include="Cnc\Fagor\FagorMachine.cs" />
|
||||||
<Compile Include="Licensing\IMachineFingerprint.cs" />
|
<Compile Include="Licensing\IMachineFingerprint.cs" />
|
||||||
<Compile Include="Licensing\ILicenseValidator.cs" />
|
<Compile Include="Licensing\ILicenseValidator.cs" />
|
||||||
<Compile Include="Licensing\LicenseFile.cs" />
|
<Compile Include="Licensing\LicenseFile.cs" />
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ namespace NcProgramManager
|
||||||
_log.Info("-azione=");
|
_log.Info("-azione=");
|
||||||
_log.Info(" l'{AZIONE} da eseguire.");
|
_log.Info(" l'{AZIONE} da eseguire.");
|
||||||
_log.Info("-manufacturer=");
|
_log.Info("-manufacturer=");
|
||||||
_log.Info(" Il produttore del CNC: fanuc (default), heidenhain, siemens, mitsubishi");
|
_log.Info(" Il produttore del CNC: fanuc (default), heidenhain, siemens, mitsubishi, fagor");
|
||||||
_log.Info("-tipo=");
|
_log.Info("-tipo=");
|
||||||
_log.Info(" Il tipo di connessione utilizzato dalla macchina: 3 per ethernet\n 2 per HSSB (solo Fanuc, legacy)");
|
_log.Info(" Il tipo di connessione utilizzato dalla macchina: 3 per ethernet\n 2 per HSSB (solo Fanuc, legacy)");
|
||||||
_log.Info("-ip=");
|
_log.Info("-ip=");
|
||||||
|
|
@ -318,9 +318,9 @@ namespace NcProgramManager
|
||||||
_log.Info("-nodo=");
|
_log.Info("-nodo=");
|
||||||
_log.Info(" Dato per HSSB Nodo di connesione (solo Fanuc)");
|
_log.Info(" Dato per HSSB Nodo di connesione (solo Fanuc)");
|
||||||
_log.Info("-username=");
|
_log.Info("-username=");
|
||||||
_log.Info(" Username per autenticazione (Heidenhain/Siemens)");
|
_log.Info(" Username per autenticazione (Heidenhain/Siemens/Mitsubishi/Fagor)");
|
||||||
_log.Info("-password=");
|
_log.Info("-password=");
|
||||||
_log.Info(" Password per autenticazione (Heidenhain/Siemens)");
|
_log.Info(" Password per autenticazione (Heidenhain/Siemens/Mitsubishi/Fagor)");
|
||||||
_log.Info("-compatibility");
|
_log.Info("-compatibility");
|
||||||
_log.Info(" Attivazione modalità compatibilità");
|
_log.Info(" Attivazione modalità compatibilità");
|
||||||
_log.Info("-commento=");
|
_log.Info("-commento=");
|
||||||
|
|
|
||||||
54
README.md
54
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# NcProgramManager
|
# NcProgramManager
|
||||||
|
|
||||||
Software per il trasferimento di programmi CNC da e verso controller di macchine utensili. Supporta Fanuc (FOCAS), Heidenhain (LSV2), Siemens Sinumerik (FTP) e Mitsubishi (FTP). Funziona completamente offline come applicazione console per Windows.
|
Software per il trasferimento di programmi CNC da e verso controller di macchine utensili. Supporta Fanuc (FOCAS), Heidenhain (LSV2), Siemens Sinumerik (FTP), Mitsubishi (FTP) e Fagor (FTP). Funziona completamente offline come applicazione console per Windows.
|
||||||
|
|
||||||
## Requisiti
|
## Requisiti
|
||||||
|
|
||||||
|
|
@ -25,13 +25,13 @@ NcProgramManager.exe -manufacturer=<tipo> -azione=<azione> -ip=<ip> -porta=<port
|
||||||
| Parametro | Valori | Descrizione |
|
| Parametro | Valori | Descrizione |
|
||||||
|-----------|--------|-------------|
|
|-----------|--------|-------------|
|
||||||
| `-azione=` | `Scarica` / `Invia` | Azione da eseguire |
|
| `-azione=` | `Scarica` / `Invia` | Azione da eseguire |
|
||||||
| `-manufacturer=` | `fanuc` / `heidenhain` / `siemens` / `mitsubishi` | Tipo di controller CNC (predefinito: `fanuc`) |
|
| `-manufacturer=` | `fanuc` / `heidenhain` / `siemens` / `mitsubishi` / `fagor` | Tipo di controller CNC (predefinito: `fanuc`) |
|
||||||
| `-ip=` | indirizzo IP | Indirizzo IP del controller — connessione Ethernet |
|
| `-ip=` | indirizzo IP | Indirizzo IP del controller — connessione Ethernet |
|
||||||
| `-porta=` | numero porta | Porta di connessione Ethernet |
|
| `-porta=` | numero porta | Porta di connessione Ethernet |
|
||||||
| `-tipo=` | `3` / `2` | Tipo di connessione: `3` = Ethernet, `2` = HSSB (solo Fanuc, legacy) |
|
| `-tipo=` | `3` / `2` | Tipo di connessione: `3` = Ethernet, `2` = HSSB (solo Fanuc, legacy) |
|
||||||
| `-nodo=` | numero nodo | Nodo HSSB (solo Fanuc, connessione HSSB) |
|
| `-nodo=` | numero nodo | Nodo HSSB (solo Fanuc, connessione HSSB) |
|
||||||
| `-username=` | nome utente | Credenziale per Heidenhain, Siemens, Mitsubishi |
|
| `-username=` | nome utente | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor |
|
||||||
| `-password=` | password | Credenziale per Heidenhain, Siemens, Mitsubishi |
|
| `-password=` | password | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor |
|
||||||
| `-pathprogramma=` | percorso file | Percorso del file locale da caricare sul CNC (richiesto per `Invia`) |
|
| `-pathprogramma=` | percorso file | Percorso del file locale da caricare sul CNC (richiesto per `Invia`) |
|
||||||
| `-path=` | numero | Percorso programmi sul CNC; predefinito `1` = `//CNC_MEM/USER/PATH1/` |
|
| `-path=` | numero | Percorso programmi sul CNC; predefinito `1` = `//CNC_MEM/USER/PATH1/` |
|
||||||
| `-workzero=` | qualsiasi valore | Se presente: include dati work zero offset nell'operazione |
|
| `-workzero=` | qualsiasi valore | Se presente: include dati work zero offset nell'operazione |
|
||||||
|
|
@ -63,6 +63,12 @@ Scarica programma da Siemens Sinumerik:
|
||||||
NcProgramManager.exe -manufacturer=siemens -azione=Scarica -ip=192.168.1.200 -porta=21 -username=admin -password=secret
|
NcProgramManager.exe -manufacturer=siemens -azione=Scarica -ip=192.168.1.200 -porta=21 -username=admin -password=secret
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Invia programma a Fagor 8065:
|
||||||
|
|
||||||
|
```
|
||||||
|
NcProgramManager.exe -manufacturer=fagor -azione=Invia -ip=192.168.1.150 -porta=21 -username=user -password=pass -pathprogramma=C:\programmi\PROG1.nc
|
||||||
|
```
|
||||||
|
|
||||||
## Controller supportati
|
## Controller supportati
|
||||||
|
|
||||||
| Produttore | Protocollo | Porta predefinita | Note |
|
| Produttore | Protocollo | Porta predefinita | Note |
|
||||||
|
|
@ -71,6 +77,46 @@ NcProgramManager.exe -manufacturer=siemens -azione=Scarica -ip=192.168.1.200 -po
|
||||||
| Heidenhain | LSV2 (TCP) | 19000 | Testato su TNC series |
|
| Heidenhain | LSV2 (TCP) | 19000 | Testato su TNC series |
|
||||||
| Siemens | FTP | 21 | Directory programmi: `/_N_MPF_DIR/` — richiede opzione FTP abilitata sul Sinumerik |
|
| Siemens | FTP | 21 | Directory programmi: `/_N_MPF_DIR/` — richiede opzione FTP abilitata sul Sinumerik |
|
||||||
| Mitsubishi | FTP | 21 | Directory programmi: `/PRG/` |
|
| Mitsubishi | FTP | 21 | Directory programmi: `/PRG/` |
|
||||||
|
| Fagor | FTP | 21 | Directory programmi: `/Users/Prg/`. Modelli supportati: 8060, 8065, 8070 (FTP nativo); 8055 (richiede opzione Ethernet) |
|
||||||
|
|
||||||
|
### Controller Fagor
|
||||||
|
|
||||||
|
#### Modelli supportati
|
||||||
|
|
||||||
|
| Modello | Supporto | Note |
|
||||||
|
|---------|----------|------|
|
||||||
|
| 8055 | Solo con opzione Ethernet | Richiede attivazione esplicita del server FTP via parametri macchina |
|
||||||
|
| 8060 | Sì | FTP nativo (sistema Windows-based) |
|
||||||
|
| 8065 | Sì | FTP nativo (sistema Windows-based) |
|
||||||
|
| 8070 | Sì | FTP nativo (sistema Windows embedded) |
|
||||||
|
|
||||||
|
#### Modelli NON supportati
|
||||||
|
|
||||||
|
| Modello | Motivo |
|
||||||
|
|---------|--------|
|
||||||
|
| 8055 base (senza Ethernet) | Solo trasferimento RS232; il programma non implementa il protocollo seriale |
|
||||||
|
| 8025 / 8030 / 8040 / 8050 | Legacy, esclusivamente RS232 |
|
||||||
|
|
||||||
|
#### Regole nome programma
|
||||||
|
|
||||||
|
- Lunghezza massima 24 caratteri
|
||||||
|
- Caratteri ammessi: lettere (A-Z, a-z), cifre (0-9), spazi
|
||||||
|
- Carattere `ñ` NON ammesso
|
||||||
|
- Estensione predefinita: `.nc` (configurabile via codice)
|
||||||
|
|
||||||
|
#### Funzioni non supportate via FTP
|
||||||
|
|
||||||
|
- `ReadActiveProgramAsync` — Fagor espone lo stato di esecuzione via DNC/OPC-UA, non via FTP
|
||||||
|
- `SelectMainProgramAsync` — selezione del programma principale richiede DNC, non disponibile via FTP
|
||||||
|
- Lettura runtime stato macchina (`ReadAsync` ritorna sempre `Unknown`)
|
||||||
|
|
||||||
|
#### Variabili d'ambiente per test integration con macchina reale
|
||||||
|
|
||||||
|
| Variabile | Valore | Default |
|
||||||
|
|-----------|--------|---------|
|
||||||
|
| `FAGOR_TEST_IP` | indirizzo IP macchina Fagor | (test skippato se assente) |
|
||||||
|
| `FAGOR_USER` | username FTP | `user` |
|
||||||
|
| `FAGOR_PASS` | password FTP | `pass` |
|
||||||
|
|
||||||
## Validazione del programma NC
|
## Validazione del programma NC
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue