nc_program_manager/Cnc/NcProgramValidator.cs

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;
}
}
}