diff --git a/ArgParser.cs b/ArgParser.cs
index 3c54303..0b26dce 100644
--- a/ArgParser.cs
+++ b/ArgParser.cs
@@ -31,6 +31,7 @@ namespace NcProgramManager
case "heidenhain": inputArgs.manufacturer = CncManufacturer.Heidenhain; break;
case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break;
case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break;
+ case "fagor": inputArgs.manufacturer = CncManufacturer.Fagor; break;
default: inputArgs.manufacturer = CncManufacturer.Fanuc; break;
}
}
diff --git a/Cnc/CncMachineFactory.cs b/Cnc/CncMachineFactory.cs
index ae12c39..f394404 100644
--- a/Cnc/CncMachineFactory.cs
+++ b/Cnc/CncMachineFactory.cs
@@ -1,4 +1,5 @@
using System;
+using NcProgramManager.Cnc.Fagor;
using NcProgramManager.Cnc.Fanuc;
using NcProgramManager.Cnc.Heidenhain;
using NcProgramManager.Cnc.Mitsubishi;
@@ -44,6 +45,16 @@ namespace NcProgramManager.Cnc
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
return new FanucMachine(new FanucConnectionConfig
{
diff --git a/Cnc/Fagor/FagorConnectionConfig.cs b/Cnc/Fagor/FagorConnectionConfig.cs
new file mode 100644
index 0000000..5e5eebb
--- /dev/null
+++ b/Cnc/Fagor/FagorConnectionConfig.cs
@@ -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;
+ /// Directory dei programmi sul server FTP Fagor. Default valido per 8060/8065/8070.
+ public string ProgramDirectory { get; set; } = "/Users/Prg/";
+ /// Estensione applicata ai nomi programma senza estensione. Default `.nc` per Fagor 8060/8065/8070.
+ public string ProgramExtension { get; set; } = ".nc";
+ public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(10);
+ }
+}
diff --git a/Cnc/Fagor/FagorFtpClient.cs b/Cnc/Fagor/FagorFtpClient.cs
new file mode 100644
index 0000000..1b81bc7
--- /dev/null
+++ b/Cnc/Fagor/FagorFtpClient.cs
@@ -0,0 +1,71 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+
+namespace NcProgramManager.Cnc.Fagor
+{
+ /// Client FTP per trasferimento programmi Fagor CNC.
+ internal sealed class FagorFtpClient : IFagorFtpClient
+ {
+ private readonly FagorConnectionConfig _config;
+
+ public FagorFtpClient(FagorConnectionConfig config)
+ {
+ _config = config ?? throw new ArgumentNullException("config");
+ }
+
+ /// Verifica raggiungibilità server FTP listando la directory programmi.
+ 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; }
+ }
+
+ /// Scarica file dal Fagor in stringa.
+ 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();
+ }
+
+ /// Carica contenuto stringa su Fagor.
+ 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;
+ }
+ }
+}
diff --git a/Cnc/Fagor/FagorMachine.cs b/Cnc/Fagor/FagorMachine.cs
new file mode 100644
index 0000000..9ce0357
--- /dev/null
+++ b/Cnc/Fagor/FagorMachine.cs
@@ -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
+{
+ ///
+ /// Fagor CNC machine — trasferimento programmi via FTP standard.
+ /// Supportato su Fagor 8060/8065/8070 con server FTP integrato e su 8055 con opzione Ethernet attiva.
+ ///
+ public sealed class FagorMachine : ICncMachine
+ {
+ private static readonly Logger _log = LogManager.GetCurrentClassLogger();
+
+ private readonly FagorConnectionConfig _config;
+ private readonly IFagorFtpClient _ftp;
+ private readonly INcProgramValidator _validator;
+ private readonly List _errors = new List();
+ private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
+ private ConnectionState _state = ConnectionState.Disconnected;
+
+ public FagorMachine(FagorConnectionConfig config)
+ {
+ _config = config ?? throw new ArgumentNullException("config");
+ _ftp = new FagorFtpClient(config);
+ _validator = new NcProgramValidator(CncManufacturer.Fagor);
+ }
+
+ internal FagorMachine(FagorConnectionConfig config, IFagorFtpClient ftpClient,
+ INcProgramValidator validator = null)
+ {
+ _config = config ?? throw new ArgumentNullException("config");
+ _ftp = ftpClient ?? throw new ArgumentNullException("ftpClient");
+ _validator = validator ?? new NcProgramValidator(CncManufacturer.Fagor);
+ }
+
+ public string Name { get { return _config.Name; } }
+ public ConnectionState State { get { return _state; } }
+ public event EventHandler StateChanged;
+
+ public Task> ConnectAsync(CancellationToken ct = default(CancellationToken))
+ {
+ return RunGuardedAsync(() =>
+ {
+ SetState(ConnectionState.Connecting);
+ if (!_ftp.Ping())
+ {
+ SetState(ConnectionState.Faulted);
+ _errors.Add(new CncError(0, "Connect",
+ string.Format("FTP server unreachable at {0}:{1}", _config.IpAddress, _config.Port)));
+ return false;
+ }
+ SetState(ConnectionState.Connected);
+ return true;
+ }, ct);
+ }
+
+ public Task DisconnectAsync()
+ {
+ return RunGuardedAsync(() =>
+ {
+ SetState(ConnectionState.Disconnected);
+ return true;
+ }, CancellationToken.None);
+ }
+
+ public Task> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
+ {
+ return RunGuardedAsync(() =>
+ {
+ EnsureConnected();
+ string ncPath = ResolvePath(path, null);
+ try { return _ftp.DownloadFile(ncPath); }
+ catch (Exception ex)
+ {
+ _errors.Add(new CncError(0, "ReadProgram", ex.Message));
+ return null;
+ }
+ }, ct);
+ }
+
+ public Task> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
+ {
+ return RunGuardedAsync(() =>
+ {
+ EnsureConnected();
+ if (program == null || string.IsNullOrEmpty(program.Content))
+ {
+ _errors.Add(new CncError(0, "WriteProgram", "Content is empty"));
+ return false;
+ }
+ var vr = _validator.Validate(program.Name ?? path, program.Content);
+ if (!vr.Success)
+ {
+ string msg = string.Join("; ", vr.Errors
+ .Where(e => e.Severity == ValidationSeverity.Error)
+ .Select(e => e.Message));
+ _errors.Add(new CncError(0, "Validator", msg));
+ return false;
+ }
+ string ncPath = ResolvePath(path, program.Name);
+ try { _ftp.UploadFile(ncPath, program.Content); return true; }
+ catch (Exception ex)
+ {
+ _errors.Add(new CncError(0, "WriteProgram", ex.Message));
+ return false;
+ }
+ }, ct);
+ }
+
+ public Task> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken))
+ {
+ return RunGuardedAsync(() =>
+ {
+ EnsureConnected();
+ try { _ftp.DeleteFile(path); return true; }
+ catch (Exception ex)
+ {
+ _errors.Add(new CncError(0, "DeleteProgram", ex.Message));
+ return false;
+ }
+ }, ct);
+ }
+
+ public Task> ReadAsync(CancellationToken ct = default(CancellationToken))
+ {
+ return RunGuardedAsync(() =>
+ {
+ EnsureConnected();
+ return new MachineSnapshot
+ {
+ CapturedAt = DateTime.Now,
+ Mode = MachineMode.Unknown,
+ Run = RunState.Unknown,
+ Motion = MotionState.Unknown,
+ Alarms = new List(),
+ OperatorMessages = new List()
+ };
+ }, ct);
+ }
+
+ public Task> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken))
+ {
+ return Task.FromResult(CncResult.Fail("ReadActiveProgram", "Not supported via FTP"));
+ }
+
+ public Task> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken))
+ {
+ return Task.FromResult(CncResult.Fail("SelectMainProgram", "Not supported via FTP"));
+ }
+
+ public void Dispose()
+ {
+ try { DisconnectAsync().GetAwaiter().GetResult(); } catch { /* cleanup — intentionally swallowed */ }
+ try { _gate.Dispose(); } catch { /* cleanup — intentionally swallowed */ }
+ }
+
+ private async Task> RunGuardedAsync(Func body, CancellationToken ct)
+ {
+ await _gate.WaitAsync(ct).ConfigureAwait(false);
+ try
+ {
+ return await Task.Run(() =>
+ {
+ _errors.Clear();
+ try
+ {
+ T value = body();
+ return _errors.Count > 0
+ ? CncResult.Fail(_errors.ToArray())
+ : CncResult.Ok(value);
+ }
+ catch (OperationCanceledException) { return CncResult.Cancelled(); }
+ catch (Exception ex)
+ {
+ _errors.Add(new CncError(0, "FagorMachine", ex.Message));
+ return CncResult.Fail(_errors.ToArray());
+ }
+ }, ct).ConfigureAwait(false);
+ }
+ finally { _gate.Release(); }
+ }
+
+ private void EnsureConnected()
+ {
+ if (_state != ConnectionState.Connected)
+ throw new InvalidOperationException("FagorMachine is not connected.");
+ }
+
+ private void SetState(ConnectionState next)
+ {
+ if (_state == next) return;
+ _state = next;
+ var h = StateChanged;
+ if (h != null)
+ {
+ try { h(this, next); } catch (Exception ex) { _log.Warn(ex, "StateChanged handler threw"); }
+ }
+ }
+
+ /// Costruisce path FTP completo. Se name è fornito, append `_config.ProgramExtension` se manca estensione.
+ private string ResolvePath(string folder, string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ return folder.Contains("/") ? folder : _config.ProgramDirectory + folder + _config.ProgramExtension;
+
+ string dir = folder ?? _config.ProgramDirectory;
+ if (!dir.EndsWith("/")) dir += "/";
+ string ext = name.Contains(".") ? "" : _config.ProgramExtension;
+ return dir + name + ext;
+ }
+ }
+}
diff --git a/Cnc/Fagor/IFagorFtpClient.cs b/Cnc/Fagor/IFagorFtpClient.cs
new file mode 100644
index 0000000..8a6e231
--- /dev/null
+++ b/Cnc/Fagor/IFagorFtpClient.cs
@@ -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);
+ }
+}
diff --git a/Cnc/Models/CncManufacturer.cs b/Cnc/Models/CncManufacturer.cs
index 9cbe135..c340863 100644
--- a/Cnc/Models/CncManufacturer.cs
+++ b/Cnc/Models/CncManufacturer.cs
@@ -1,4 +1,4 @@
namespace NcProgramManager.Cnc.Models
{
- public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi }
+ public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi, Fagor }
}
diff --git a/Cnc/NcProgramValidator.cs b/Cnc/NcProgramValidator.cs
index f75e8fe..3df49c0 100644
--- a/Cnc/NcProgramValidator.cs
+++ b/Cnc/NcProgramValidator.cs
@@ -45,34 +45,39 @@ namespace NcProgramManager.Cnc
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++)
{
int lineNum = i + 1;
string line = lines[i];
- // 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++)
+ if (restrictive)
{
- 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));
- break;
+ if (char.IsLower(line[ci]))
+ {
+ 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
ValidateForbiddenChars(line, lineNum, errors);
}
@@ -89,6 +94,12 @@ namespace NcProgramManager.Cnc
// ── private helpers ───────────────────────────────────────────────────
+ private bool IsRestrictiveDialect()
+ {
+ return _manufacturer == CncManufacturer.Fanuc
+ || _manufacturer == CncManufacturer.Mitsubishi;
+ }
+
private void ValidateName(string name, List errors)
{
if (string.IsNullOrEmpty(name)) return;
@@ -116,6 +127,33 @@ namespace NcProgramManager.Cnc
string.Format("Program name '{0}' must end with .H", name),
ValidationSeverity.Error));
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));
}
break;
+
+ case CncManufacturer.Fagor:
+ // Fagor: nessuna restrizione su contenuto programma (ISO + linguaggio alto livello)
+ break;
}
}
diff --git a/NcProgramManager.Tests/Integration/FagorMachineIntegrationTests.cs b/NcProgramManager.Tests/Integration/FagorMachineIntegrationTests.cs
new file mode 100644
index 0000000..9b15d73
--- /dev/null
+++ b/NcProgramManager.Tests/Integration/FagorMachineIntegrationTests.cs
@@ -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);
+ }
+ }
+ }
+}
diff --git a/NcProgramManager.Tests/NcProgramManager.Tests.csproj b/NcProgramManager.Tests/NcProgramManager.Tests.csproj
index 0180faf..b04e80f 100644
--- a/NcProgramManager.Tests/NcProgramManager.Tests.csproj
+++ b/NcProgramManager.Tests/NcProgramManager.Tests.csproj
@@ -61,12 +61,14 @@
+
+
diff --git a/NcProgramManager.Tests/Unit/CncMachineFactoryTests.cs b/NcProgramManager.Tests/Unit/CncMachineFactoryTests.cs
index bec48c3..9849e6e 100644
--- a/NcProgramManager.Tests/Unit/CncMachineFactoryTests.cs
+++ b/NcProgramManager.Tests/Unit/CncMachineFactoryTests.cs
@@ -6,6 +6,7 @@ using NcProgramManager.Cnc.Heidenhain;
using NcProgramManager.Cnc.Mitsubishi;
using NcProgramManager.Cnc.Models;
using NcProgramManager.Cnc.Siemens;
+using NcProgramManager.Cnc.Fagor;
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());
+ }
+ }
+
[Test]
public void Create_FanucHssbNode_TransportIsHssb()
{
diff --git a/NcProgramManager.Tests/Unit/FagorMachineTests.cs b/NcProgramManager.Tests/Unit/FagorMachineTests.cs
new file mode 100644
index 0000000..121b578
--- /dev/null
+++ b/NcProgramManager.Tests/Unit/FagorMachineTests.cs
@@ -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 _ftpMock;
+ private Mock _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(MockBehavior.Strict);
+ // Always-ok validator so existing write tests are not affected by validation rules
+ _validatorMock = new Mock();
+ _validatorMock.Setup(v => v.Validate(It.IsAny(), It.IsAny()))
+ .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(), "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(), "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(), It.IsAny()), 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(), It.IsAny()), 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(), It.IsAny()), 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(), It.IsAny()), Times.Never);
+ }
+ finally { realMachine.Dispose(); }
+ }
+ }
+}
diff --git a/NcProgramManager.Tests/Unit/NcProgramValidatorTests.cs b/NcProgramManager.Tests/Unit/NcProgramValidatorTests.cs
index ac8c2f5..6c64bf2 100644
--- a/NcProgramManager.Tests/Unit/NcProgramValidatorTests.cs
+++ b/NcProgramManager.Tests/Unit/NcProgramValidatorTests.cs
@@ -32,8 +32,8 @@ namespace NcProgramManager.Tests.Unit
[Test]
public void Validate_LowercaseLetter_ReturnsError()
{
- var v = new NcProgramValidator(CncManufacturer.Siemens);
- var r = v.Validate("PROG.MPF", "PROG.MPF\ng00 X10");
+ var v = new NcProgramValidator(CncManufacturer.Fanuc);
+ var r = v.Validate("O1234", "O1234\ng00 X10");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "Lowercase"), Is.True);
}
@@ -41,16 +41,16 @@ namespace NcProgramManager.Tests.Unit
[Test]
public void Validate_NoLowercase_NoLowercaseError()
{
- var v = new NcProgramValidator(CncManufacturer.Siemens);
- var r = v.Validate("PROG.MPF", "G00 X10 Y20");
+ var v = new NcProgramValidator(CncManufacturer.Fanuc);
+ var r = v.Validate("O1234", "O1234\nG00 X10 Y20");
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
}
[Test]
public void Validate_TabCharacter_ReturnsError()
{
- var v = new NcProgramValidator(CncManufacturer.Siemens);
- var r = v.Validate("PROG.MPF", "G00\tX10");
+ var v = new NcProgramValidator(CncManufacturer.Fanuc);
+ var r = v.Validate("O1234", "O1234\nG00\tX10");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "Tab"), Is.True);
}
@@ -223,6 +223,32 @@ namespace NcProgramManager.Tests.Unit
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 ──────────────────────────────────────────────────
[Test]
@@ -259,6 +285,138 @@ namespace NcProgramManager.Tests.Unit
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 ───────────────────────────────────────────────────────────
private static bool HasErrorContaining(ValidationResult r, string fragment)
diff --git a/NcProgramManager.csproj b/NcProgramManager.csproj
index d54bba6..fb608d8 100755
--- a/NcProgramManager.csproj
+++ b/NcProgramManager.csproj
@@ -122,6 +122,10 @@
+
+
+
+
diff --git a/Program.cs b/Program.cs
index eecc95a..9e246c7 100755
--- a/Program.cs
+++ b/Program.cs
@@ -308,7 +308,7 @@ namespace NcProgramManager
_log.Info("-azione=");
_log.Info(" l'{AZIONE} da eseguire.");
_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(" Il tipo di connessione utilizzato dalla macchina: 3 per ethernet\n 2 per HSSB (solo Fanuc, legacy)");
_log.Info("-ip=");
@@ -318,9 +318,9 @@ namespace NcProgramManager
_log.Info("-nodo=");
_log.Info(" Dato per HSSB Nodo di connesione (solo Fanuc)");
_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 per autenticazione (Heidenhain/Siemens)");
+ _log.Info(" Password per autenticazione (Heidenhain/Siemens/Mitsubishi/Fagor)");
_log.Info("-compatibility");
_log.Info(" Attivazione modalità compatibilità");
_log.Info("-commento=");
diff --git a/README.md b/README.md
index 1900432..697ea8e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# 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
@@ -25,13 +25,13 @@ NcProgramManager.exe -manufacturer= -azione= -ip= -porta=