Compare commits

..

No commits in common. "mazak" and "main" have entirely different histories.
mazak ... main

16 changed files with 5 additions and 909 deletions

View file

@ -228,52 +228,6 @@ Enhancements and known issues discovered during execution and codebase analysis.
--- ---
### ISS-021: Mazak support — requires EIA/ISO option active on control (Mazatrol out of scope)
- **Discovered:** Intake/online research for Mazak integration (2026-06-16)
- **Type:** Enhancement constraint / known limitation — pre-implementation
- **Decision:** Mazak support will transfer **EIA/ISO G-code (plain text) over FTP only**, reusing the Mitsubishi/Siemens/Fagor `*FtpClient` pattern. Target controllers: Smooth family (SmoothG/X/Ai/EZ).
- **Hard requirement — EIA/ISO option:** modern Mazatrol controls run both Mazatrol conversational and EIA/ISO G-code, but **EIA/ISO is frequently a license-gated option** (paid activation) and is NOT guaranteed present on a given machine. If the EIA option is NOT active on the target control, this feature cannot send programs to that machine — only Mazatrol (proprietary binary) would work there, which is out of scope. **Operator must verify the EIA/ISO option is enabled on the control before use.**
- **Mazatrol CMT out of scope:** Mazatrol programs are a proprietary binary format (CMT) with no public PC-side authoring/transfer API. Supporting them would require a licensed vendor library (CIMCO/MazaCAM) or a DNC box (e.g. Mazak LAN Connect, Calmotion LANUSB-MAZ). Explicitly excluded.
- **No FOCAS equivalent:** unlike Fanuc (FOCAS `fwlib32.dll`) and Mitsubishi, Mazak exposes no native DLL/SDK for program up/download. MTConnect / SmartBox / SmartLink are monitoring-only (read), not an upload channel. Transfer is file-drop (FTP/SMB/serial) only.
- **Config variance to expose (not hardcode):**
- FTP port: **21 vs 23** — some Smooth Ai units run FTP via IIS on port 23 because port 21 conflicts with Mazak's own boot/shutdown data-backup. Default 21, configurable.
- Credentials + NC program directory: undocumented, site-specific (NC dir often surfaced via `MC_DIRECT MODE PROGRAMS` share or a user-created exchange folder). Configurable, no safe public default.
- File extension: `.eia` default (configurable, like Fagor `.nc`).
- **README note pending:** add Mazak row to "Controller supportati" table + a `### Controller Mazak` section documenting the EIA-option requirement **when the feature is implemented** (not before — avoid claiming support that doesn't exist yet).
- **Validator ruleset (VERIFIED online 2026-06-16 vs Mazak Matrix EIA manual H740PB0030E + field reports):** Mazak EIA is **NOT** Fanuc-restrictive. Do NOT reuse the Fanuc/Mitsubishi restrictive gate. Verified facts:
- Program number = `O` + **up to 8 digits** (`O[0-9]{1,8}`), and is **optional** — programs may lead with `%` (EOR) or have no O-line. A 4-digit-required `O####` check would reject valid programs.
- `;` is the **literal EOB (End-Of-Block) code** in Mazak EIA file content — MUST be allowed, not forbidden.
- `[` `]` `#` `*` `=` `:` are valid ISO codes (TAP9TAP14) — allow.
- `( )` = comment Control-Out/In — allowed; only nested parens are risky.
- No 80-char block cap (input buffer 248×5). Drop the cap.
- Lowercase: not in EIA punch-code table → likely insignificant; UNCERTAIN for FTP text upload — verify on real control before hard-reject, do not assume.
- → Mazak validator must be **lenient** (optional 18 digit O-header, permissive char set), NOT added to `IsRestrictiveDialect()`.
- **Addressing (verified UNCERTAIN):** over FTP a program is a file on a Windows filesystem → addressed by **full filename incl. `.eia`**, not bare O-number (control maps file↔work-number internally). Verified: each program/subprogram = separate file named by number sans `O` (e.g. `O1234` content → file `1234.eia`); main calls subprograms at runtime via `M98 P1234`; control links files by name/number at execution, NOT bundled.
- **DECISION (user, 2026-06-16) — preserve filenames verbatim:** O#### naming is a Fanuc-only constraint (old-NC name reuse across same family). NOT relevant for Mazak. Therefore:
- `MazakMachine.ResolvePath` uses the operator-supplied filename **verbatim** — no O-number derivation, no rename, **no auto-`.eia` append**. Symmetric read/write on the same literal name. `MazakConnectionConfig` has NO `ProgramExtension` field.
- Validator for Mazak = **FULL PASSTHROUGH**`Validate(CncManufacturer.Mazak, …)` returns `ValidationResult.Success` unconditionally. No name check, no content check (FTP sends file as-is). Mazak NOT in `IsRestrictiveDialect()`, no forbidden-char/header/length rules. Verified Mazak EIA permissiveness (`;` EOB, `[ ]`, `#` valid) makes restrictive checks wrong anyway; passthrough is the deliberate choice.
- Multi-file jobs (main + subprograms) sent as **separate transfers**, each file keeping its own name.
- **`.eia` extension + port 21/23 + license-gating:** CONFIRMED online (Versicor Smooth Ai FTP article; Practical Machinist EIA-option threads). `.eia` vs `.EIA` case + exact NC dir path per Smooth firmware = still site-specific, settle on real machine.
- **Impact:** Medium — feature usable only where EIA option licensed; documented limitation, not fixable in code.
- **Effort:** Medium — mirrors Fagor phase (machine + FtpClient + config + **lenient** validator ruleset + factory + tests).
- **Suggested phase:** Future milestone v0.8 — Mazak Support.
---
### ISS-022: `DeleteProgramAsync` raw-path asymmetry (Mitsubishi — same class as ISS-020)
- **Discovered:** Mazak implementation review (2026-06-16) — Mazak copied the pattern, then fixed its own copy.
- **Type:** Bug (design) — path asymmetry.
- **Description:** `MitsubishiMachine.DeleteProgramAsync` calls `_ftp.DeleteFile(path)` with the RAW path, bypassing `ResolvePath`, while `ReadProgramAsync`/`WriteProgramAsync` resolve via `ResolvePath`. So `Delete("1")` targets `1` (relative) instead of `/PRG/1` — asymmetric with read/write, same defect family as ISS-020 (which fixed read/write but left Delete).
- **Files:** `Cnc/Mitsubishi/MitsubishiMachine.cs` `DeleteProgramAsync` (~line 122). Likely also Siemens/Fagor — verify.
- **Mazak status:** FIXED in new code (`MazakMachine.DeleteProgramAsync` resolves via `ResolvePath`). Mitsubishi/Siemens/Fagor still open.
- **Impact:** Medium — delete-by-identifier hits wrong FTP path; delete fails or no-ops silently. Masked until someone deletes by bare number.
- **Effort:** Low — one-line per maker + regression test.
- **Related:** ISS-020.
---
## Closed Issues ## Closed Issues
| ID | Summary | Phase | Commit | | ID | Summary | Phase | Commit |

View file

@ -32,7 +32,6 @@ namespace NcProgramManager
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; case "fagor": inputArgs.manufacturer = CncManufacturer.Fagor; break;
case "mazak": inputArgs.manufacturer = CncManufacturer.Mazak; break;
default: inputArgs.manufacturer = CncManufacturer.Fanuc; break; default: inputArgs.manufacturer = CncManufacturer.Fanuc; break;
} }
} }

View file

@ -2,7 +2,6 @@ using System;
using NcProgramManager.Cnc.Fagor; using NcProgramManager.Cnc.Fagor;
using NcProgramManager.Cnc.Fanuc; using NcProgramManager.Cnc.Fanuc;
using NcProgramManager.Cnc.Heidenhain; using NcProgramManager.Cnc.Heidenhain;
using NcProgramManager.Cnc.Mazak;
using NcProgramManager.Cnc.Mitsubishi; using NcProgramManager.Cnc.Mitsubishi;
using NcProgramManager.Cnc.Models; using NcProgramManager.Cnc.Models;
using NcProgramManager.Cnc.Siemens; using NcProgramManager.Cnc.Siemens;
@ -56,16 +55,6 @@ namespace NcProgramManager.Cnc
Password = args.password Password = args.password
}); });
case CncManufacturer.Mazak:
return new MazakMachine(new MazakConnectionConfig
{
Name = "Mazak",
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
{ {

View file

@ -1,10 +0,0 @@
namespace NcProgramManager.Cnc.Mazak
{
internal interface IMazakFtpClient
{
bool Ping();
string DownloadFile(string ncPath);
void UploadFile(string ncPath, string content);
void DeleteFile(string ncPath);
}
}

View file

@ -1,25 +0,0 @@
using System;
namespace NcProgramManager.Cnc.Mazak
{
public sealed class MazakConnectionConfig
{
public string Name { get; set; }
public string IpAddress { get; set; }
/// <summary>
/// FTP port. Default 21. Some Smooth Ai units run FTP on port 23
/// (port 21 collides with Mazatrol boot/shutdown backup) - operator
/// overrides via -porta.
/// </summary>
public int Port { get; set; } = 21;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
/// <summary>
/// Remote NC program directory. Site-specific: SD/D-drive
/// "Machine Programs" folder, varies per Smooth firmware. No safe
/// public default - "/" is a placeholder, operator must set it.
/// </summary>
public string ProgramDirectory { get; set; } = "/";
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(10);
}
}

View file

@ -1,71 +0,0 @@
using System;
using System.IO;
using System.Net;
using System.Text;
namespace NcProgramManager.Cnc.Mazak
{
/// <summary>Client FTP per trasferimento programmi Mazak CNC.</summary>
internal sealed class MazakFtpClient : IMazakFtpClient
{
private readonly MazakConnectionConfig _config;
public MazakFtpClient(MazakConnectionConfig config)
{
_config = config ?? throw new ArgumentNullException("config");
}
/// <summary>Verifica raggiungibilita 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 Mazak 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 Mazak.</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;
}
}
}

View file

@ -1,231 +0,0 @@
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.Mazak
{
/// <summary>
/// Mazak CNC machine - EIA/ISO program transfer over FTP.
/// Targets Mazak Smooth/Matrix controls exposing an FTP server for the
/// NC program directory. EIA/ISO files are dropped/read by their exact
/// filename: there is no active-program channel over FTP.
/// </summary>
public sealed class MazakMachine : ICncMachine
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private readonly MazakConnectionConfig _config;
private readonly IMazakFtpClient _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 MazakMachine(MazakConnectionConfig config)
{
_config = config ?? throw new ArgumentNullException("config");
_ftp = new MazakFtpClient(config);
_validator = new NcProgramValidator(CncManufacturer.Mazak);
}
internal MazakMachine(MazakConnectionConfig config, IMazakFtpClient ftpClient,
INcProgramValidator validator = null)
{
_config = config ?? throw new ArgumentNullException("config");
_ftp = ftpClient ?? throw new ArgumentNullException("ftpClient");
_validator = validator ?? new NcProgramValidator(CncManufacturer.Mazak);
}
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);
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;
}
// ISS-020: resolve the write path the same way ReadProgramAsync does.
// `path` is the EIA/ISO filename verbatim (e.g. "O1234" -> /PRG/O1234)
// or a full FTP path, NOT a directory to combine with program.Name.
// Mazak addresses files by their exact filename; program.Name is used
// for validation only, not for path construction.
string ncPath = ResolvePath(path);
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();
string ncPath = ResolvePath(path);
try { _ftp.DeleteFile(ncPath); 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, "MazakMachine", ex.Message));
return CncResult<T>.Fail(_errors.ToArray());
}
}, ct).ConfigureAwait(false);
}
finally { _gate.Release(); }
}
private void EnsureConnected()
{
if (_state != ConnectionState.Connected)
throw new InvalidOperationException("MazakMachine 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>
/// Build full FTP path. Mazak EIA/ISO files are addressed by their exact
/// filename verbatim - there is NO extension append (the operator supplies
/// the filename as it lives on the control). A bare identifier ("O1234") is
/// combined with <see cref="MazakConnectionConfig.ProgramDirectory"/>; an
/// explicit path containing '/' is used as-is. Read and write resolve
/// identically (ISS-020 symmetric path).
/// </summary>
private string ResolvePath(string path)
{
if (string.IsNullOrEmpty(path)) return _config.ProgramDirectory;
if (path.Contains("/")) return path;
string dir = _config.ProgramDirectory ?? "/";
if (!dir.EndsWith("/")) dir += "/";
return dir + path;
}
}
}

View file

@ -1,4 +1,4 @@
namespace NcProgramManager.Cnc.Models namespace NcProgramManager.Cnc.Models
{ {
public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi, Fagor, Mazak } public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi, Fagor }
} }

View file

@ -23,11 +23,6 @@ namespace NcProgramManager.Cnc
public ValidationResult Validate(string programName, string content) public ValidationResult Validate(string programName, string content)
{ {
// Mazak EIA: full passthrough. Not Fanuc-restrictive — filenames preserved
// verbatim, content sent as-is. No name/content/forbidden-char/length checks.
if (_manufacturer == CncManufacturer.Mazak)
return ValidationResult.Ok();
var errors = new List<ValidationError>(); var errors = new List<ValidationError>();
// Name-format check (uses just the bare filename without path) // Name-format check (uses just the bare filename without path)

View file

@ -1,132 +0,0 @@
using System;
using NUnit.Framework;
using NcProgramManager.Cnc.Models;
using NcProgramManager.Cnc.Mazak;
using NcProgramManager.Tests.Integration.Stubs;
namespace NcProgramManager.Tests.Integration
{
[TestFixture]
[Category("Integration")]
public class MazakMachineIntegrationTests
{
private FtpServerStub _stub;
private MazakMachine _machine;
private const string ProgramDir = "/PRG/";
[SetUp]
public void SetUp()
{
_stub = new FtpServerStub();
var config = new MazakConnectionConfig
{
Name = "MazakIntegration",
IpAddress = "127.0.0.1",
Port = _stub.Port,
Username = "user",
Password = "pass",
ProgramDirectory = ProgramDir,
RequestTimeout = TimeSpan.FromSeconds(5)
};
_machine = new MazakMachine(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_ProgramRoundTripsSamePathNoExtension()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
// Bare identifier "1234"; Mazak validator is passthrough but use clean EIA text.
const string identifier = "1234";
string content = "O1234\nG00 X10\nM30";
var prog = new CncProgram { Name = identifier, Content = content };
// Write target == ResolvePath(identifier) == ProgramDir + identifier (verbatim, no extension)
var writeResult = _machine.WriteProgramAsync(identifier, prog).GetAwaiter().GetResult();
Assert.That(writeResult.Success, Is.True);
string storedPath = ProgramDir + identifier;
Assert.That(_stub.Files.ContainsKey(storedPath), Is.True,
"File should be stored verbatim at " + storedPath);
Assert.That(_stub.Files.ContainsKey(storedPath + ".EIA"), Is.False,
"File must NOT be stored with an appended extension");
// Read SAME bare identifier -> ResolvePath resolves to the same target (ISS-020 symmetry)
var readResult = _machine.ReadProgramAsync(identifier).GetAwaiter().GetResult();
Assert.That(readResult.Success, Is.True);
Assert.That(readResult.Value, Is.EqualTo(content),
"Read content must match written content for the same identifier (ResolvePath symmetry)");
}
[Test]
public void WriteThenDelete_ProgramRemoved_SubsequentReadFails()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
const string identifier = "1234";
string content = "O1234\nG00 X10\nM30";
var prog = new CncProgram { Name = identifier, Content = content };
var writeResult = _machine.WriteProgramAsync(identifier, prog).GetAwaiter().GetResult();
Assert.That(writeResult.Success, Is.True);
string storedPath = ProgramDir + identifier;
Assert.That(_stub.Files.ContainsKey(storedPath), Is.True);
// ISS-020 symmetry: delete the SAME bare identifier used for write.
// ResolvePath(identifier) -> storedPath, so write/delete/read all align.
var deleteResult = _machine.DeleteProgramAsync(identifier).GetAwaiter().GetResult();
Assert.That(deleteResult.Success, Is.True);
Assert.That(_stub.Files.ContainsKey(storedPath), Is.False);
var readResult = _machine.ReadProgramAsync(identifier).GetAwaiter().GetResult();
Assert.That(readResult.Success, Is.False, "Read after delete must fail / not found");
}
[Test]
public void ReadProgramAsync_FileNotInStub_ResultFail()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.ReadProgramAsync("9999").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
[Test]
[Category("Hardware")]
public void Hardware_ConnectAsync_RealMachine()
{
string ip = HardwareTestHelper.GetIpOrSkip("MAZAK_TEST_IP");
var config = new MazakConnectionConfig
{
Name = "MazakHardware",
IpAddress = ip,
Port = 21,
Username = Environment.GetEnvironmentVariable("MAZAK_USER") ?? "user",
Password = Environment.GetEnvironmentVariable("MAZAK_PASS") ?? "pass",
ProgramDirectory = "/PRG/",
RequestTimeout = TimeSpan.FromSeconds(10)
};
using (var machine = new MazakMachine(config))
{
var result = machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True, "ConnectAsync failed: " + string.Join("; ", result.Errors));
}
}
}
}

View file

@ -62,7 +62,6 @@
<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="Unit\FagorMachineTests.cs" />
<Compile Include="Unit\MazakMachineTests.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" />
@ -70,7 +69,6 @@
<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="Integration\FagorMachineIntegrationTests.cs" />
<Compile Include="Integration\MazakMachineIntegrationTests.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" />

View file

@ -7,7 +7,6 @@ using NcProgramManager.Cnc.Mitsubishi;
using NcProgramManager.Cnc.Models; using NcProgramManager.Cnc.Models;
using NcProgramManager.Cnc.Siemens; using NcProgramManager.Cnc.Siemens;
using NcProgramManager.Cnc.Fagor; using NcProgramManager.Cnc.Fagor;
using NcProgramManager.Cnc.Mazak;
namespace NcProgramManager.Tests.Unit namespace NcProgramManager.Tests.Unit
{ {
@ -95,33 +94,6 @@ namespace NcProgramManager.Tests.Unit
} }
} }
[Test]
public void Create_Mazak_ReturnsMazakMachine()
{
var args = BaseArgs();
args.manufacturer = CncManufacturer.Mazak;
using (var m = CncMachineFactory.Create(args))
{
Assert.That(m, Is.InstanceOf<MazakMachine>());
}
}
[Test]
public void Create_Mazak_PortOverride_23()
{
var args = BaseArgs();
args.manufacturer = CncManufacturer.Mazak;
args.porta = "23";
using (var m = CncMachineFactory.Create(args) as MazakMachine)
{
var config = GetRequiredField<MazakConnectionConfig>(m, "_config");
Assert.That(config.Port, Is.EqualTo(23));
}
}
[Test] [Test]
public void Create_FanucHssbNode_TransportIsHssb() public void Create_FanucHssbNode_TransportIsHssb()
{ {

View file

@ -1,217 +0,0 @@
using System;
using NUnit.Framework;
using Moq;
using NcProgramManager.Cnc;
using NcProgramManager.Cnc.Models;
using NcProgramManager.Cnc.Mazak;
namespace NcProgramManager.Tests.Unit
{
[TestFixture]
public class MazakMachineTests
{
private MazakConnectionConfig _config;
private Mock<IMazakFtpClient> _ftpMock;
private Mock<INcProgramValidator> _validatorMock;
private MazakMachine _machine;
[SetUp]
public void SetUp()
{
_config = new MazakConnectionConfig
{
Name = "TestMazak",
IpAddress = "192.168.1.4",
Port = 21,
ProgramDirectory = "/PRG/"
};
_ftpMock = new Mock<IMazakFtpClient>(MockBehavior.Strict);
// Always-ok validator: Mazak addresses files verbatim (passthrough),
// so validation must not interfere with path/verbatim assertions.
_validatorMock = new Mock<INcProgramValidator>();
_validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
.Returns(ValidationResult.Ok());
_machine = new MazakMachine(_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 - verbatim filename, NO extension appended
[Test]
public void ReadProgramAsync_FullPath_CallsDownloadFileWithExactPath()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DownloadFile("/PRG/1234")).Returns("EIA_DATA");
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.ReadProgramAsync("/PRG/1234").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.EqualTo("EIA_DATA"));
_ftpMock.Verify(f => f.DownloadFile("/PRG/1234"), Times.Once);
}
[Test]
public void ReadProgramAsync_BareIdentifier_ResolvesWithDirectoryNoExtension()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
// Bare "1234" -> ProgramDirectory + "1234" verbatim, NO .eia / extension.
_ftpMock.Setup(f => f.DownloadFile("/PRG/1234")).Returns("DATA");
_machine.ConnectAsync().GetAwaiter().GetResult();
_machine.ReadProgramAsync("1234").GetAwaiter().GetResult();
_ftpMock.Verify(f => f.DownloadFile("/PRG/1234"), Times.Once);
// Must NOT append any extension (e.g. .eia / .EIA).
_ftpMock.Verify(f => f.DownloadFile(It.Is<string>(s => s.Contains("."))), Times.Never);
}
// WriteProgramAsync
[Test]
public void WriteProgramAsync_BareIdentifier_ResolvesWithDirectory_SymmetricWithRead()
{
// ISS-020 symmetry regression: write("1234") must upload to the SAME
// resolved path that read("1234") downloads from ("/PRG/1234"), so the
// roundtrip targets one location. Path comes from the identifier verbatim,
// NOT from a directory combined with program.Name.
const string readPath = "/PRG/1234"; // path used by ReadProgramAsync("1234")
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.UploadFile(readPath, "CONTENT"));
_machine.ConnectAsync().GetAwaiter().GetResult();
// program.Name deliberately differs from the identifier to prove the path
// is built from the identifier ("1234"), not from program.Name.
var prog = new CncProgram { Name = "O0001", Content = "CONTENT" };
var result = _machine.WriteProgramAsync("1234", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
// write-path == read-path for identical input -> symmetry guard.
_ftpMock.Verify(f => f.UploadFile(readPath, "CONTENT"), Times.Once);
}
[Test]
public void WriteProgramAsync_FullPath_CallsUploadFileWithExactPath()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.UploadFile("/PRG/1234", "CONTENT"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = "1234", Content = "CONTENT" };
var result = _machine.WriteProgramAsync("/PRG/1234", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.UploadFile("/PRG/1234", "CONTENT"), Times.Once);
}
[Test]
public void WriteProgramAsync_LetterName_VerbatimPassthroughNoRename()
{
// Passthrough proof: a letter name "MYPART.NC" is uploaded to
// ProgramDirectory + "MYPART.NC" UNCHANGED - no rename, no O#### enforcement,
// no extension mangling. Validator is always-ok (Mazak passthrough).
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.UploadFile("/PRG/MYPART.NC", "CONTENT"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = "MYPART.NC", Content = "CONTENT" };
var result = _machine.WriteProgramAsync("MYPART.NC", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.UploadFile("/PRG/MYPART.NC", "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 = "1234", Content = "" };
var result = _machine.WriteProgramAsync("/PRG/1234", 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("/PRG/1234", 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_BareIdentifier_ResolvesWithDirectory_SymmetricWithReadWrite()
{
// ISS-020 symmetry regression: delete("1234") must target the SAME
// resolved path read/write use for "1234" ("/PRG/1234"). Path comes from
// ResolvePath(identifier), NOT the raw identifier verbatim.
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DeleteFile("/PRG/1234"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.DeleteProgramAsync("1234").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.DeleteFile("/PRG/1234"), Times.Once);
}
// ReadActiveProgramAsync - not supported via FTP
[Test]
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
{
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
// SelectMainProgramAsync - not supported via FTP
[Test]
public void SelectMainProgramAsync_ReturnsFailNotSupported()
{
var result = _machine.SelectMainProgramAsync("/PRG/1234").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
}

View file

@ -199,85 +199,6 @@ namespace NcProgramManager.Tests.Unit
Assert.That(HasErrorContaining(r, "O####"), Is.True); Assert.That(HasErrorContaining(r, "O####"), Is.True);
} }
// ── Mazak rules (full passthrough) ────────────────────────────────────
[Test]
public void Validate_Mazak_ValidOProgram_Succeeds()
{
var v = new NcProgramValidator(CncManufacturer.Mazak);
var r = v.Validate("O1234", "O1234\nG00 X10");
Assert.That(r.Success, Is.True);
Assert.That(r.Errors, Is.Empty);
}
[Test]
public void Validate_Mazak_EightDigitOName_Succeeds()
{
// 8-digit O number would fail Fanuc 4-digit rule; Mazak passes through.
var v = new NcProgramValidator(CncManufacturer.Mazak);
var r = v.Validate("O12345678", "O12345678\nG00 X10");
Assert.That(r.Success, Is.True);
Assert.That(r.Errors, Is.Empty);
}
[Test]
public void Validate_Mazak_NoOHeaderLeadingPercent_Succeeds()
{
var v = new NcProgramValidator(CncManufacturer.Mazak);
var r = v.Validate("O1234", "%\nG00 X10\nM30");
Assert.That(r.Success, Is.True);
Assert.That(r.Errors, Is.Empty);
}
[Test]
public void Validate_Mazak_FanucForbiddenChars_Succeeds()
{
// ';', '[', ']', '#' would be rejected/warned for Fanuc; Mazak passes through.
var v = new NcProgramValidator(CncManufacturer.Mazak);
var r = v.Validate("O1234", "O1234\nG00 X10;\nG01 [X20] #100=5");
Assert.That(r.Success, Is.True);
Assert.That(r.Errors, Is.Empty);
}
[Test]
public void Validate_Mazak_LowercaseContent_Succeeds()
{
var v = new NcProgramValidator(CncManufacturer.Mazak);
var r = v.Validate("O1234", "g00 x10");
Assert.That(r.Success, Is.True);
Assert.That(r.Errors, Is.Empty);
}
[Test]
public void Validate_Mazak_BlockOver80Chars_Succeeds()
{
var v = new NcProgramValidator(CncManufacturer.Mazak);
string longLine = "O1234\n" + new string('X', 81);
var r = v.Validate("O1234", longLine);
Assert.That(r.Success, Is.True);
Assert.That(r.Errors, Is.Empty);
}
[Test]
public void Validate_Mazak_ArbitraryFilename_Succeeds()
{
// Non-O name; no name-pattern enforcement for Mazak.
var v = new NcProgramValidator(CncManufacturer.Mazak);
var r = v.Validate("MYPART.NC", "O1234\nG00 X10");
Assert.That(r.Success, Is.True);
Assert.That(r.Errors, Is.Empty);
}
[Test]
public void Validate_Mazak_EmptyContent_Succeeds()
{
// Passthrough: validator itself passes empty; machine layer guards empty separately.
var v = new NcProgramValidator(CncManufacturer.Mazak);
var r = v.Validate("O1234", "");
Assert.That(r.Success, Is.True);
Assert.That(r.Errors, Is.Empty);
}
// ── Siemens rules ───────────────────────────────────────────────────── // ── Siemens rules ─────────────────────────────────────────────────────
[Test] [Test]

View file

@ -122,10 +122,6 @@
<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\Mazak\MazakConnectionConfig.cs" />
<Compile Include="Cnc\Mazak\IMazakFtpClient.cs" />
<Compile Include="Cnc\Mazak\MazakFtpClient.cs" />
<Compile Include="Cnc\Mazak\MazakMachine.cs" />
<Compile Include="Cnc\Fagor\FagorConnectionConfig.cs" /> <Compile Include="Cnc\Fagor\FagorConnectionConfig.cs" />
<Compile Include="Cnc\Fagor\IFagorFtpClient.cs" /> <Compile Include="Cnc\Fagor\IFagorFtpClient.cs" />
<Compile Include="Cnc\Fagor\FagorFtpClient.cs" /> <Compile Include="Cnc\Fagor\FagorFtpClient.cs" />

View file

@ -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), Mitsubishi (FTP), Fagor (FTP) e Mazak (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` / `fagor` / `mazak` | 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, Fagor, Mazak | | `-username=` | nome utente | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor |
| `-password=` | password | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor, Mazak | | `-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 |
@ -78,7 +78,6 @@ NcProgramManager.exe -manufacturer=fagor -azione=Invia -ip=192.168.1.150 -porta=
| 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) | | Fagor | FTP | 21 | Directory programmi: `/Users/Prg/`. Modelli supportati: 8060, 8065, 8070 (FTP nativo); 8055 (richiede opzione Ethernet) |
| Mazak | FTP | 21 | G-code EIA/ISO (testo). Famiglia Smooth (G/X/Ai/EZ). Porta 21 o 23 (alcune unità Ai usano IIS su 23). Directory programmi site-specific (da configurare). **Richiede opzione EIA/ISO attiva sul controllo** |
### Controller Fagor ### Controller Fagor
@ -119,47 +118,6 @@ NcProgramManager.exe -manufacturer=fagor -azione=Invia -ip=192.168.1.150 -porta=
| `FAGOR_USER` | username FTP | `user` | | `FAGOR_USER` | username FTP | `user` |
| `FAGOR_PASS` | password FTP | `pass` | | `FAGOR_PASS` | password FTP | `pass` |
### Controller Mazak
#### Formato supportato
- Solo EIA/ISO (G-code in formato testo). Famiglia Smooth.
- **NON supportato: Mazatrol (formato binario proprietario CMT)** — nessuna API pubblica lato PC. Escluso.
- **Richiede opzione EIA/ISO attiva** sul controllo (opzione a pagamento, non sempre presente). Se assente l'invio è impossibile.
- Nessun equivalente FOCAS/SDK: il trasferimento avviene solo tramite file via FTP. MTConnect serve solo per il monitoraggio, non per l'invio.
#### Porta FTP
- Porta 21 di default; alcune unità Smooth Ai richiedono la 23 (la porta 21 va in conflitto con il backup Mazatrol all'avvio e allo spegnimento). Override tramite `-porta`.
#### Directory programmi e credenziali
- Directory programmi e credenziali sono site-specific, da configurare (nessun default sicuro).
#### Regole nome programma
- **Nomi file preservati così come sono** — nessun vincolo di rinomina `O####` (vincolo presente solo su Fanuc, non su Mazak).
#### Programmi multi-file
- Per i programmi composti da più file (main + sottoprogrammi): inviare ogni file separatamente. Il main richiama i sottoprogrammi a runtime via `M98 P<numero>`; il controllo li collega per nome/numero, quindi non vanno uniti in un unico file.
#### Funzioni non supportate via FTP
- `ReadActiveProgramAsync` — stato runtime non disponibile via FTP
- `SelectMainProgramAsync` — selezione del programma principale non disponibile via FTP
- `ReadAsync` — lettura runtime stato macchina non disponibile via FTP
Coerente con il comportamento di Fagor e Siemens.
#### Variabili d'ambiente per test integration con macchina reale
| Variabile | Valore | Default |
|-----------|--------|---------|
| `MAZAK_TEST_IP` | indirizzo IP macchina Mazak | (test skippato se assente) |
| `MAZAK_USER` | username FTP | `user` |
| `MAZAK_PASS` | password FTP | `pass` |
## Validazione del programma NC ## Validazione del programma NC
Prima di inviare un programma al controller, NcProgramManager esegue una validazione automatica del codice NC. Gli errori bloccano il trasferimento. Gli avvisi vengono registrati ma non interrompono l'operazione. Prima di inviare un programma al controller, NcProgramManager esegue una validazione automatica del codice NC. Gli errori bloccano il trasferimento. Gli avvisi vengono registrati ma non interrompono l'operazione.