feat(mazak): add Mazak CNC support via FTP (EIA/ISO)
New Cnc/Mazak/ machine integration mirroring the Fagor/Mitsubishi FTP pattern: MazakConnectionConfig, IMazakFtpClient, MazakFtpClient, MazakMachine (ICncMachine). Transfers EIA/ISO G-code text over FTP to Smooth-family controllers. Mazatrol CMT (proprietary binary) out of scope. Design (verified against Mazak Matrix EIA manual + field reports): - Filenames preserved verbatim; no O#### rename, no extension append. - Symmetric ResolvePath for Read/Write/Delete (avoids ISS-020 class; Delete resolved too, unlike copied Mitsubishi source). - Validator = full passthrough for Mazak (EIA permits ';' EOB, '[ ]', '#'; restrictive Fanuc checks would false-reject valid programs). - Port 21 default, -porta=23 for Smooth Ai IIS variant. - ProgramDirectory site-specific, no default. Wiring: CncManufacturer.Mazak, ArgParser 'mazak' token, CncMachineFactory. Tests: MazakMachineTests (unit), MazakMachineIntegrationTests (FTP-stub roundtrip + symmetry guard), Mazak cases in validator/factory tests. Docs: README Controller Mazak section + table row; ISS-021 (constraints, EIA-option requirement), ISS-022 (Mitsubishi delete asymmetry logged). NOT build-verified (no .NET toolchain in build env) — verify on Windows. Projects: NcProgramManager, NcProgramManager.Tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f1aa002297
commit
19da0e598e
16 changed files with 909 additions and 5 deletions
|
|
@ -228,6 +228,52 @@ 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 (TAP9–TAP14) — 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 1–8 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
|
||||
|
||||
| ID | Summary | Phase | Commit |
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ namespace NcProgramManager
|
|||
case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break;
|
||||
case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break;
|
||||
case "fagor": inputArgs.manufacturer = CncManufacturer.Fagor; break;
|
||||
case "mazak": inputArgs.manufacturer = CncManufacturer.Mazak; break;
|
||||
default: inputArgs.manufacturer = CncManufacturer.Fanuc; break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using NcProgramManager.Cnc.Fagor;
|
||||
using NcProgramManager.Cnc.Fanuc;
|
||||
using NcProgramManager.Cnc.Heidenhain;
|
||||
using NcProgramManager.Cnc.Mazak;
|
||||
using NcProgramManager.Cnc.Mitsubishi;
|
||||
using NcProgramManager.Cnc.Models;
|
||||
using NcProgramManager.Cnc.Siemens;
|
||||
|
|
@ -55,6 +56,16 @@ namespace NcProgramManager.Cnc
|
|||
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
|
||||
return new FanucMachine(new FanucConnectionConfig
|
||||
{
|
||||
|
|
|
|||
10
Cnc/Mazak/IMazakFtpClient.cs
Normal file
10
Cnc/Mazak/IMazakFtpClient.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace NcProgramManager.Cnc.Mazak
|
||||
{
|
||||
internal interface IMazakFtpClient
|
||||
{
|
||||
bool Ping();
|
||||
string DownloadFile(string ncPath);
|
||||
void UploadFile(string ncPath, string content);
|
||||
void DeleteFile(string ncPath);
|
||||
}
|
||||
}
|
||||
25
Cnc/Mazak/MazakConnectionConfig.cs
Normal file
25
Cnc/Mazak/MazakConnectionConfig.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
71
Cnc/Mazak/MazakFtpClient.cs
Normal file
71
Cnc/Mazak/MazakFtpClient.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
231
Cnc/Mazak/MazakMachine.cs
Normal file
231
Cnc/Mazak/MazakMachine.cs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace NcProgramManager.Cnc.Models
|
||||
{
|
||||
public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi, Fagor }
|
||||
public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi, Fagor, Mazak }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ namespace NcProgramManager.Cnc
|
|||
|
||||
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>();
|
||||
|
||||
// Name-format check (uses just the bare filename without path)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +62,7 @@
|
|||
<Compile Include="Unit\MitsubishiMachineTests.cs" />
|
||||
<Compile Include="Unit\HeidenhainMachineTests.cs" />
|
||||
<Compile Include="Unit\FagorMachineTests.cs" />
|
||||
<Compile Include="Unit\MazakMachineTests.cs" />
|
||||
<Compile Include="Integration\Stubs\FtpServerStub.cs" />
|
||||
<Compile Include="Integration\Stubs\Lsv2ServerStub.cs" />
|
||||
<Compile Include="Integration\HardwareTestHelper.cs" />
|
||||
|
|
@ -69,6 +70,7 @@
|
|||
<Compile Include="Integration\MitsubishiMachineIntegrationTests.cs" />
|
||||
<Compile Include="Integration\HeidenhainMachineIntegrationTests.cs" />
|
||||
<Compile Include="Integration\FagorMachineIntegrationTests.cs" />
|
||||
<Compile Include="Integration\MazakMachineIntegrationTests.cs" />
|
||||
<Compile Include="Unit\LicenseFileTests.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using NcProgramManager.Cnc.Mitsubishi;
|
|||
using NcProgramManager.Cnc.Models;
|
||||
using NcProgramManager.Cnc.Siemens;
|
||||
using NcProgramManager.Cnc.Fagor;
|
||||
using NcProgramManager.Cnc.Mazak;
|
||||
|
||||
namespace NcProgramManager.Tests.Unit
|
||||
{
|
||||
|
|
@ -94,6 +95,33 @@ 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]
|
||||
public void Create_FanucHssbNode_TransportIsHssb()
|
||||
{
|
||||
|
|
|
|||
217
NcProgramManager.Tests/Unit/MazakMachineTests.cs
Normal file
217
NcProgramManager.Tests/Unit/MazakMachineTests.cs
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -199,6 +199,85 @@ namespace NcProgramManager.Tests.Unit
|
|||
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 ─────────────────────────────────────────────────────
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@
|
|||
<Compile Include="Cnc\Mitsubishi\IMitsubishiFtpClient.cs" />
|
||||
<Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.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\IFagorFtpClient.cs" />
|
||||
<Compile Include="Cnc\Fagor\FagorFtpClient.cs" />
|
||||
|
|
|
|||
50
README.md
50
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), Mitsubishi (FTP) e Fagor (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), Fagor (FTP) e Mazak (FTP). Funziona completamente offline come applicazione console per Windows.
|
||||
|
||||
## Requisiti
|
||||
|
||||
|
|
@ -25,13 +25,13 @@ NcProgramManager.exe -manufacturer=<tipo> -azione=<azione> -ip=<ip> -porta=<port
|
|||
| Parametro | Valori | Descrizione |
|
||||
|-----------|--------|-------------|
|
||||
| `-azione=` | `Scarica` / `Invia` | Azione da eseguire |
|
||||
| `-manufacturer=` | `fanuc` / `heidenhain` / `siemens` / `mitsubishi` / `fagor` | Tipo di controller CNC (predefinito: `fanuc`) |
|
||||
| `-manufacturer=` | `fanuc` / `heidenhain` / `siemens` / `mitsubishi` / `fagor` / `mazak` | Tipo di controller CNC (predefinito: `fanuc`) |
|
||||
| `-ip=` | indirizzo IP | Indirizzo IP del controller — connessione Ethernet |
|
||||
| `-porta=` | numero porta | Porta di connessione Ethernet |
|
||||
| `-tipo=` | `3` / `2` | Tipo di connessione: `3` = Ethernet, `2` = HSSB (solo Fanuc, legacy) |
|
||||
| `-nodo=` | numero nodo | Nodo HSSB (solo Fanuc, connessione HSSB) |
|
||||
| `-username=` | nome utente | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor |
|
||||
| `-password=` | password | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor |
|
||||
| `-username=` | nome utente | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor, Mazak |
|
||||
| `-password=` | password | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor, Mazak |
|
||||
| `-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/` |
|
||||
| `-workzero=` | qualsiasi valore | Se presente: include dati work zero offset nell'operazione |
|
||||
|
|
@ -78,6 +78,7 @@ 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 |
|
||||
| Mitsubishi | FTP | 21 | Directory programmi: `/PRG/` |
|
||||
| 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
|
||||
|
||||
|
|
@ -118,6 +119,47 @@ NcProgramManager.exe -manufacturer=fagor -azione=Invia -ip=192.168.1.150 -porta=
|
|||
| `FAGOR_USER` | username FTP | `user` |
|
||||
| `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
|
||||
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue