///|
pub(all) enum ControllerFamily {
GenericCnc
GrblCompatible
MarlinCompatible
} derive(Eq, Debug)
///|
pub struct ValidationProfile {
name_value : String
family_value : ControllerFamily
allowed_g_codes_value : Array[Double]
allowed_m_codes_value : Array[Int]
allowed_letters_value : Array[Int]
max_x_value : Double
max_y_value : Double
max_z_value : Double
max_feed_value : Double
max_spindle_value : Double
max_tool_value : Int
require_sequence_value : Bool
require_checksum_value : Bool
require_monotonic_sequence_value : Bool
allow_extrusion_value : Bool
allow_arcs_value : Bool
} derive(Eq, Debug)
///|
pub fn ValidationProfile::generic_cnc() -> ValidationProfile {
{
name_value: "generic-cnc",
family_value: GenericCnc,
allowed_g_codes_value: [
0.0, 1.0, 2.0, 3.0, 17.0, 18.0, 19.0, 20.0, 21.0, 90.0, 91.0, 92.0, 92.1,
],
allowed_m_codes_value: [3, 4, 5, 6, 7, 8, 9],
allowed_letters_value: [
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 90,
],
max_x_value: 1000.0,
max_y_value: 1000.0,
max_z_value: 500.0,
max_feed_value: 50000.0,
max_spindle_value: 100000.0,
max_tool_value: 999,
require_sequence_value: false,
require_checksum_value: false,
require_monotonic_sequence_value: false,
allow_extrusion_value: false,
allow_arcs_value: true,
}
}
///|
pub fn ValidationProfile::grbl() -> ValidationProfile {
{
..ValidationProfile::generic_cnc(),
name_value: "grbl-common-subset",
family_value: GrblCompatible,
allowed_m_codes_value: [3, 4, 5, 7, 8, 9],
allowed_letters_value: [
65, 66, 67, 70, 71, 73, 74, 75, 76, 77, 78, 80, 82, 83, 84, 88, 89, 90,
],
max_x_value: 500.0,
max_y_value: 500.0,
max_z_value: 200.0,
max_feed_value: 30000.0,
max_spindle_value: 30000.0,
max_tool_value: 0,
}
}
///|
pub fn ValidationProfile::marlin() -> ValidationProfile {
{
..ValidationProfile::generic_cnc(),
name_value: "marlin-common-subset",
family_value: MarlinCompatible,
allowed_m_codes_value: [3, 4, 5, 6, 7, 8, 9, 82, 83],
max_x_value: 500.0,
max_y_value: 500.0,
max_z_value: 500.0,
max_feed_value: 60000.0,
max_spindle_value: 100000.0,
max_tool_value: 31,
allow_extrusion_value: true,
}
}
///|
pub fn ValidationProfile::name(self : ValidationProfile) -> String {
self.name_value
}
///|
pub fn ValidationProfile::family(self : ValidationProfile) -> ControllerFamily {
self.family_value
}
///|
pub fn ValidationProfile::with_workspace(
self : ValidationProfile,
max_x : Double,
max_y : Double,
max_z : Double,
) -> ValidationProfile {
{ ..self, max_x_value: max_x, max_y_value: max_y, max_z_value: max_z }
}
///|
pub fn ValidationProfile::with_max_feed(
self : ValidationProfile,
max_feed : Double,
) -> ValidationProfile {
{ ..self, max_feed_value: max_feed }
}
///|
pub fn ValidationProfile::with_max_spindle(
self : ValidationProfile,
max_spindle : Double,
) -> ValidationProfile {
{ ..self, max_spindle_value: max_spindle }
}
///|
pub fn ValidationProfile::with_sequence_policy(
self : ValidationProfile,
required : Bool,
monotonic : Bool,
) -> ValidationProfile {
{
..self,
require_sequence_value: required,
require_monotonic_sequence_value: monotonic,
}
}
///|
pub fn ValidationProfile::with_checksum_required(
self : ValidationProfile,
required : Bool,
) -> ValidationProfile {
{ ..self, require_checksum_value: required }
}
///|
pub fn ValidationProfile::with_arcs_allowed(
self : ValidationProfile,
allowed : Bool,
) -> ValidationProfile {
{ ..self, allow_arcs_value: allowed }
}
///|
pub struct ValidationSummary {
physical_lines_value : Int
executable_blocks_value : Int
checked_words_value : Int
error_count_value : Int
warning_count_value : Int
first_sequence_value : Int?
last_sequence_value : Int?
} derive(Eq, Debug)
///|
pub fn ValidationSummary::physical_lines(self : ValidationSummary) -> Int {
self.physical_lines_value
}
///|
pub fn ValidationSummary::executable_blocks(self : ValidationSummary) -> Int {
self.executable_blocks_value
}
///|
pub fn ValidationSummary::checked_words(self : ValidationSummary) -> Int {
self.checked_words_value
}
///|
pub fn ValidationSummary::error_count(self : ValidationSummary) -> Int {
self.error_count_value
}
///|
pub fn ValidationSummary::warning_count(self : ValidationSummary) -> Int {
self.warning_count_value
}
///|
pub fn ValidationSummary::first_sequence(self : ValidationSummary) -> Int? {
self.first_sequence_value
}
///|
pub fn ValidationSummary::last_sequence(self : ValidationSummary) -> Int? {
self.last_sequence_value
}
///|
pub struct DocumentValidation {
profile_value : ValidationProfile
diagnostics_value : Array[Diagnostic]
summary_value : ValidationSummary
state_trace_value : StateTraceReport
} derive(Eq, Debug)
///|
pub fn DocumentValidation::profile(
self : DocumentValidation,
) -> ValidationProfile {
self.profile_value
}
///|
pub fn DocumentValidation::diagnostics(
self : DocumentValidation,
) -> Array[Diagnostic] {
self.diagnostics_value.copy()
}
///|
pub fn DocumentValidation::summary(
self : DocumentValidation,
) -> ValidationSummary {
self.summary_value
}
///|
pub fn DocumentValidation::state_trace(
self : DocumentValidation,
) -> StateTraceReport {
self.state_trace_value
}
///|
pub fn DocumentValidation::accepted(self : DocumentValidation) -> Bool {
self.summary_value.error_count() == 0
}
///|
fn contains_int(values : Array[Int], expected : Int) -> Bool {
for value in values {
if value == expected {
return true
}
}
false
}
///|
fn contains_double(values : Array[Double], expected : Double) -> Bool {
for value in values {
if value == expected {
return true
}
}
false
}
///|
fn profile_issue(
diagnostics : Array[Diagnostic],
severity : DiagnosticSeverity,
code : String,
message : String,
line : Int,
) -> Unit {
diagnostics.push({
severity_value: severity,
code_value: code,
message_value: message,
line_value: line,
})
}
///|
fn validate_word_letter(
word : BlockWord,
block : GCodeBlock,
profile : ValidationProfile,
diagnostics : Array[Diagnostic],
) -> Unit {
if !contains_int(profile.allowed_letters_value, word.letter()) {
profile_issue(
diagnostics,
Error,
"word.not_allowed",
"letter " + word.letter_text() + " is not allowed by profile",
block.source_line(),
)
}
}
///|
fn validate_g_code(
value : Double,
block : GCodeBlock,
profile : ValidationProfile,
diagnostics : Array[Diagnostic],
) -> Unit {
if !contains_double(profile.allowed_g_codes_value, value) {
profile_issue(
diagnostics,
Error,
"command.g_not_allowed",
"G" + value.to_string() + " is outside the declared profile subset",
block.source_line(),
)
}
if !profile.allow_arcs_value && (value == 2.0 || value == 3.0) {
profile_issue(
diagnostics,
Error,
"command.arc_disabled",
"arc commands are disabled by profile",
block.source_line(),
)
}
}
///|
fn validate_m_code(
value : Double,
block : GCodeBlock,
profile : ValidationProfile,
diagnostics : Array[Diagnostic],
) -> Unit {
match integer_code(value) {
Some(code) =>
if !contains_int(profile.allowed_m_codes_value, code) {
profile_issue(
diagnostics,
Error,
"command.m_not_allowed",
"M" + code.to_string() + " is outside the declared profile subset",
block.source_line(),
)
}
None =>
profile_issue(
diagnostics,
Error,
"command.m_fractional",
"M code must be an integer",
block.source_line(),
)
}
}
///|
fn validate_arc_words(
block : GCodeBlock,
diagnostics : Array[Diagnostic],
) -> Unit {
let mut arc = false
for value in block.values_for(71) {
if value == 2.0 || value == 3.0 {
arc = true
}
}
if !arc {
return
}
let has_offsets = block.has_letter(73) ||
block.has_letter(74) ||
block.has_letter(75)
let has_radius = block.has_letter(82)
if !has_offsets && !has_radius {
profile_issue(
diagnostics,
Error,
"arc.geometry_missing",
"arc requires I/J/K offsets or R radius",
block.source_line(),
)
}
if has_offsets && has_radius {
profile_issue(
diagnostics,
Error,
"arc.geometry_ambiguous",
"arc cannot mix center offsets and radius",
block.source_line(),
)
}
}
///|
fn validate_scalar_ranges(
block : GCodeBlock,
profile : ValidationProfile,
diagnostics : Array[Diagnostic],
) -> Unit {
match block.first_value(70) {
Some(value) =>
if value < 0.0 {
profile_issue(
diagnostics,
Error,
"feed.negative",
"feed must not be negative",
block.source_line(),
)
} else if value > profile.max_feed_value {
profile_issue(
diagnostics,
Error,
"feed.above_profile",
"feed exceeds profile maximum",
block.source_line(),
)
}
None => ()
}
match block.first_value(83) {
Some(value) =>
if value < 0.0 {
profile_issue(
diagnostics,
Error,
"spindle.negative",
"spindle speed must not be negative",
block.source_line(),
)
} else if value > profile.max_spindle_value {
profile_issue(
diagnostics,
Error,
"spindle.above_profile",
"spindle speed exceeds profile maximum",
block.source_line(),
)
}
None => ()
}
match block.first_value(84) {
Some(value) =>
match integer_code(value) {
Some(tool) if tool >= 0 && tool <= profile.max_tool_value => ()
_ =>
profile_issue(
diagnostics,
Error,
"tool.out_of_range",
"tool number exceeds profile range",
block.source_line(),
)
}
None => ()
}
if !profile.allow_extrusion_value && block.has_letter(69) {
profile_issue(
diagnostics,
Error,
"word.extrusion_disabled",
"E word is not allowed by this profile",
block.source_line(),
)
}
}
///|
fn validate_sequence(
block : GCodeBlock,
profile : ValidationProfile,
previous : Int?,
diagnostics : Array[Diagnostic],
) -> Int? {
if block.word_count() == 0 {
return previous
}
let sequence = block.sequence()
if profile.require_sequence_value && sequence is None {
profile_issue(
diagnostics,
Error,
"sequence.required",
"executable block requires N sequence",
block.source_line(),
)
}
if profile.require_checksum_value && block.checksum() is None {
profile_issue(
diagnostics,
Error,
"checksum.required",
"executable block requires checksum",
block.source_line(),
)
}
if !block.checksum_valid() {
profile_issue(
diagnostics,
Error,
"checksum.invalid",
"checksum does not match block source",
block.source_line(),
)
}
match (previous, sequence) {
(Some(old), Some(current)) if profile.require_monotonic_sequence_value &&
current <= old =>
profile_issue(
diagnostics,
Error,
"sequence.not_monotonic",
"sequence number must increase",
block.source_line(),
)
_ => ()
}
match sequence {
Some(value) => Some(value)
None => previous
}
}
///|
fn validate_state_workspace(
trace : StateTraceReport,
profile : ValidationProfile,
diagnostics : Array[Diagnostic],
) -> Unit {
for snapshot in trace.snapshots() {
let position = snapshot.after().position()
if position.x() < 0.0 || position.x() > profile.max_x_value {
profile_issue(
diagnostics,
Error,
"axis.x_out_of_bounds",
"X position exceeds profile workspace",
snapshot.line(),
)
}
if position.y() < 0.0 || position.y() > profile.max_y_value {
profile_issue(
diagnostics,
Error,
"axis.y_out_of_bounds",
"Y position exceeds profile workspace",
snapshot.line(),
)
}
if position.z() < 0.0 || position.z() > profile.max_z_value {
profile_issue(
diagnostics,
Error,
"axis.z_out_of_bounds",
"Z position exceeds profile workspace",
snapshot.line(),
)
}
}
}
///|
pub fn validate_document(
document : GCodeDocument,
profile? : ValidationProfile = ValidationProfile::generic_cnc(),
) -> DocumentValidation {
let diagnostics : Array[Diagnostic] = []
let state_trace = interpret_modal_state(document)
for issue in state_trace.diagnostics() {
diagnostics.push(issue)
}
let mut checked_words = 0
let mut previous_sequence : Int? = None
let mut first_sequence : Int? = None
let mut last_sequence : Int? = None
for block in document.blocks() {
previous_sequence = validate_sequence(
block, profile, previous_sequence, diagnostics,
)
match block.sequence() {
Some(value) => {
if first_sequence is None {
first_sequence = Some(value)
}
last_sequence = Some(value)
}
None => ()
}
for word in block.words() {
checked_words += 1
validate_word_letter(word, block, profile, diagnostics)
if word.letter() == 71 {
validate_g_code(word.value(), block, profile, diagnostics)
} else if word.letter() == 77 {
validate_m_code(word.value(), block, profile, diagnostics)
}
}
validate_arc_words(block, diagnostics)
validate_scalar_ranges(block, profile, diagnostics)
}
validate_state_workspace(state_trace, profile, diagnostics)
let mut errors = 0
let mut warnings = 0
for issue in diagnostics {
match issue.severity() {
Error => errors += 1
Warning => warnings += 1
}
}
{
profile_value: profile,
diagnostics_value: diagnostics,
summary_value: {
physical_lines_value: document.physical_lines(),
executable_blocks_value: document.executable_block_count(),
checked_words_value: checked_words,
error_count_value: errors,
warning_count_value: warnings,
first_sequence_value: first_sequence,
last_sequence_value: last_sequence,
},
state_trace_value: state_trace,
}
}
///|
pub fn render_validation(validation : DocumentValidation) -> String {
let summary = validation.summary()
let mut output = "profile=" + validation.profile().name()
output = output + ", accepted=" + validation.accepted().to_string()
output = output + ", lines=" + summary.physical_lines().to_string()
output = output + ", blocks=" + summary.executable_blocks().to_string()
output = output + ", words=" + summary.checked_words().to_string()
output = output + ", errors=" + summary.error_count().to_string()
output = output + ", warnings=" + summary.warning_count().to_string() + "\n"
output + render_diagnostics(validation.diagnostics())
}