215 lines
8.7 KiB
C#
215 lines
8.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text.RegularExpressions;
|
|
using FanucProgramManager.Cnc.Models;
|
|
|
|
namespace FanucProgramManager.Cnc
|
|
{
|
|
/// <summary>Validates NC program content per-manufacturer before upload.</summary>
|
|
public sealed class NcProgramValidator : INcProgramValidator
|
|
{
|
|
private const int MaxBlockLength = 80;
|
|
private const int MaxCommentLength = 32;
|
|
|
|
private static readonly Regex FanucNamePattern = new Regex(@"^O\d{4}$", RegexOptions.Compiled);
|
|
private static readonly Regex FanucFirstLinePattern = new Regex(@"^O\d{4}", RegexOptions.Compiled);
|
|
|
|
private readonly CncManufacturer _manufacturer;
|
|
|
|
public NcProgramValidator(CncManufacturer manufacturer)
|
|
{
|
|
_manufacturer = manufacturer;
|
|
}
|
|
|
|
public ValidationResult Validate(string programName, string content)
|
|
{
|
|
var errors = new List<ValidationError>();
|
|
|
|
// Name-format check (uses just the bare filename without path)
|
|
string name = ExtractName(programName);
|
|
ValidateName(name, errors);
|
|
|
|
if (string.IsNullOrEmpty(content))
|
|
return errors.Count == 0 ? ValidationResult.Ok() : ValidationResult.Fail(errors);
|
|
|
|
// Normalise: strip CR, split on LF
|
|
string normalized = content.Replace("\r", "");
|
|
string[] lines = normalized.Split('\n');
|
|
|
|
// First-line check (Fanuc/Mitsubishi)
|
|
if ((_manufacturer == CncManufacturer.Fanuc || _manufacturer == CncManufacturer.Mitsubishi)
|
|
&& lines.Length > 0)
|
|
{
|
|
string first = lines[0].Trim();
|
|
if (!FanucFirstLinePattern.IsMatch(first))
|
|
errors.Add(new ValidationError(1, "First line must be O#### program number", ValidationSeverity.Error));
|
|
}
|
|
|
|
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 (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);
|
|
|
|
// Forbidden chars per manufacturer
|
|
ValidateForbiddenChars(line, lineNum, errors);
|
|
}
|
|
|
|
bool hasErrors = false;
|
|
for (int i = 0; i < errors.Count; i++)
|
|
if (errors[i].Severity == ValidationSeverity.Error) { hasErrors = true; break; }
|
|
|
|
if (hasErrors)
|
|
return ValidationResult.Fail(errors);
|
|
// Preserve warnings even when overall result is Success
|
|
return errors.Count == 0 ? ValidationResult.Ok() : ValidationResult.OkWithWarnings(errors);
|
|
}
|
|
|
|
// ── private helpers ───────────────────────────────────────────────────
|
|
|
|
private void ValidateName(string name, List<ValidationError> errors)
|
|
{
|
|
if (string.IsNullOrEmpty(name)) return;
|
|
|
|
switch (_manufacturer)
|
|
{
|
|
case CncManufacturer.Fanuc:
|
|
case CncManufacturer.Mitsubishi:
|
|
if (!FanucNamePattern.IsMatch(name))
|
|
errors.Add(new ValidationError(0,
|
|
string.Format("Program name '{0}' must match O####", name),
|
|
ValidationSeverity.Error));
|
|
break;
|
|
|
|
case CncManufacturer.Siemens:
|
|
if (!name.EndsWith(".MPF"))
|
|
errors.Add(new ValidationError(0,
|
|
string.Format("Program name '{0}' must end with .MPF", name),
|
|
ValidationSeverity.Error));
|
|
break;
|
|
|
|
case CncManufacturer.Heidenhain:
|
|
if (!name.EndsWith(".H"))
|
|
errors.Add(new ValidationError(0,
|
|
string.Format("Program name '{0}' must end with .H", name),
|
|
ValidationSeverity.Error));
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ValidateParensAndComments(string line, int lineNum, List<ValidationError> errors)
|
|
{
|
|
int depth = 0;
|
|
int commentStart = -1;
|
|
|
|
for (int i = 0; i < line.Length; i++)
|
|
{
|
|
char c = line[i];
|
|
if (c == '(')
|
|
{
|
|
depth++;
|
|
if (depth > 1)
|
|
{
|
|
errors.Add(new ValidationError(lineNum, "Nested parentheses not allowed", ValidationSeverity.Error));
|
|
// Don't keep adding the same error for this line
|
|
return;
|
|
}
|
|
commentStart = i;
|
|
}
|
|
else if (c == ')')
|
|
{
|
|
if (depth == 1 && commentStart >= 0)
|
|
{
|
|
// Extract comment content between the parens
|
|
int len = i - commentStart - 1;
|
|
if (len > MaxCommentLength)
|
|
errors.Add(new ValidationError(lineNum,
|
|
string.Format("Comment length {0} exceeds 32 chars", len),
|
|
ValidationSeverity.Error));
|
|
}
|
|
if (depth > 0) depth--;
|
|
commentStart = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ValidateForbiddenChars(string line, int lineNum, List<ValidationError> errors)
|
|
{
|
|
switch (_manufacturer)
|
|
{
|
|
case CncManufacturer.Fanuc:
|
|
case CncManufacturer.Mitsubishi:
|
|
for (int i = 0; i < line.Length; i++)
|
|
{
|
|
char c = line[i];
|
|
if (c == ';' || c == '[' || c == ']')
|
|
{
|
|
errors.Add(new ValidationError(lineNum,
|
|
string.Format("Forbidden char '{0}' for Fanuc/Mitsubishi", c),
|
|
ValidationSeverity.Error));
|
|
}
|
|
else if (c == '#')
|
|
{
|
|
errors.Add(new ValidationError(lineNum,
|
|
"Macro variable '#' detected — verify macro support on target CNC",
|
|
ValidationSeverity.Warning));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CncManufacturer.Siemens:
|
|
for (int i = 0; i < line.Length; i++)
|
|
{
|
|
if (line[i] == ';')
|
|
errors.Add(new ValidationError(lineNum,
|
|
"Forbidden char ';' for Siemens (use () for comments)",
|
|
ValidationSeverity.Error));
|
|
}
|
|
break;
|
|
|
|
case CncManufacturer.Heidenhain:
|
|
for (int i = 0; i < line.Length; i++)
|
|
{
|
|
char c = line[i];
|
|
if (c == '%' || c == ';')
|
|
errors.Add(new ValidationError(lineNum,
|
|
string.Format("Forbidden char '{0}' for Heidenhain", c),
|
|
ValidationSeverity.Error));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>Extracts bare filename from a path (strips directory prefix).</summary>
|
|
private static string ExtractName(string programName)
|
|
{
|
|
if (string.IsNullOrEmpty(programName)) return programName;
|
|
int slash = programName.LastIndexOf('/');
|
|
int backslash = programName.LastIndexOf('\\');
|
|
int sep = slash > backslash ? slash : backslash;
|
|
return sep >= 0 ? programName.Substring(sep + 1) : programName;
|
|
}
|
|
}
|
|
}
|