mauroalderete/gcode-core

View on GitHub
block/gcodeblock/gcodeblock.go

Summary

Maintainability
A
0 mins
Test Coverage
// gcodeblock is an implementation of block package.
//
// This package define GcodeBlock struct as a implemention of block.Blocker interface.
//
// Furemore, it defines two package functions that allows create new instances of GcodeBlock.
// These functions can be used to a any instances that implement block.BlockerFactory
package gcodeblock

import (
    "fmt"
    "hash"
    "regexp"
    "strconv"
    "strings"

    "github.com/mauroalderete/gcode-core/block"
    "github.com/mauroalderete/gcode-core/block/internal/gcodefactory"
    "github.com/mauroalderete/gcode-core/checksum"
    "github.com/mauroalderete/gcode-core/gcode"
)

const (
    // BLOC_SEPARATOR defines a string used to separate the sections of the block when is exported as line string format
    BLOCK_SEPARATOR = " "
)

//#region block struct

// GcodeBlock struct represents a single gcode block.
//
// Stores data and gcode expressions of each section of the block.
//
// His methods allow export of the block as a line string format or check of the integrity.
//
// To be constructed using the Parse function from a line of the gcode file.
type GcodeBlock struct {

    // special gcode that store the value of the verification of the integrity of the block
    checksum gcode.AddressableGcoder[uint32]

    // first gcode expression and main significance of the block. Always is present.
    command gcode.Gcoder

    // expression attached at the block with some comment. Can be empty
    comment string

    // gcode factory
    gcodeFactory gcode.GcoderFactory

    // instance of the hash algorithm to handle the checksum
    hash hash.Hash

    // line number of the block. It can be null. Always has an int32 type address.
    lineNumber gcode.AddressableGcoder[uint32]

    // list of the rest of the gcode expression that adds information to the command. Can be empty.
    parameters []gcode.Gcoder
}

// String returns the block exported as single-line string format including check and comments section.
//
// It is the same invoke ToLine method
func (b *GcodeBlock) String() string {
    return b.ToLine("%l %c %p")
}

// LineNumber returns a gcode addressable of the int32 type.
//
// Represent the line number of the block. It can be null. Always has an int32 type address.
func (b *GcodeBlock) LineNumber() gcode.AddressableGcoder[uint32] {
    return b.lineNumber
}

// Command returns a gcoder struct. Can be addressable or not.
//
// Represent the first gcode expression and main significance of the block. Always is present.
func (b *GcodeBlock) Command() gcode.Gcoder {
    return b.command
}

// Parameters return a list of gcoder structs. Each gcoder can be addressable or not.
//
// Parameters is a list of the rest of the gcode expression that adds information to the command. Can be empty.
func (b *GcodeBlock) Parameters() []gcode.Gcoder {
    return b.parameters
}

// Checksum returns a GcodeAddressable[uint32] if the line of the block contained, else returns nil.
//
// The value of the address is calculated using the hash instances stored.
func (b *GcodeBlock) Checksum() gcode.AddressableGcoder[uint32] {
    return b.checksum
}

// CalculateChecksum calculates a checksum from the block and returns a new GcodeAddressable[uint32] with the value computed.
func (b *GcodeBlock) CalculateChecksum() (gcode.AddressableGcoder[uint32], error) {

    b.hash.Reset()
    _, err := b.hash.Write([]byte(b.String()))
    if err != nil {
        return nil, fmt.Errorf("failed to calculate hash to block %s: %w", b, err)
    }

    gc, err := b.gcodeFactory.NewAddressableGcodeUint32('*', uint32(b.hash.Sum(nil)[0]))
    if err != nil {
        return nil, fmt.Errorf("failed to create checksum gcode instance with hash %v: %w", uint32(b.hash.Sum(nil)[0]), err)
    }

    return gc, nil
}

// UpdateChecksum calculates a checksum from the block and stores him in as a new checksum gcode.
func (b *GcodeBlock) UpdateChecksum() error {

    gc, err := b.CalculateChecksum()
    if err != nil {
        return fmt.Errorf("failed update checksum of the block %s: %w", b, err)
    }

    b.checksum = gc

    return nil
}

// VerifyChecksum calculates a checksum and compare him with the checksum stored in the block, it returns true if both matches.
func (b *GcodeBlock) VerifyChecksum() (bool, error) {

    if b.checksum == nil {
        return false, fmt.Errorf("the block '%s' hasn't check section", b)
    }

    gc, err := b.CalculateChecksum()
    if err != nil {
        return false, fmt.Errorf("failed to calculate hash to the control of the checksum of the block %s: %w", b, err)
    }

    return b.checksum.Compare(gc), nil
}

// Comment returns the string with the comment of the block. Or nil if there isn't one.
//
// Is an expression attached at the block with some comment. Can be empty.
func (b *GcodeBlock) Comment() string {
    return b.comment
}

// ToLine export the block as a single-line string format
//
// format is a string that contain verbs to define the place of each element of the block.
// The verbs available are:
//
// %l: linenumber gcode of the block
//
// %c: command gcode of the block
//
// %p: series of parameters gcode of the command gcode
//
// %k: checksum gcode of the useful part of the block
//
// %m: comments of the block
//
// We can used the format string to determine how each element is showing. For example:
//
// The line generated depends on the available of elements contained in the block.
// If any element isn't available then is ignored.
func (b *GcodeBlock) ToLine(format string) string {
    var values []string

    result := strings.ReplaceAll(format, "%c", b.Command().String())

    if b.lineNumber != nil {
        result = strings.ReplaceAll(result, "%l", b.LineNumber().String())
    } else {
        result = strings.ReplaceAll(result, "%l", "")
    }

    if b.parameters != nil {
        for _, g := range b.parameters {
            values = append(values, g.String())
        }
        if len(values) == 0 {
            values = append(values, "")
        }
        result = strings.ReplaceAll(result, "%p", strings.Join(values, BLOCK_SEPARATOR))
    } else {
        result = strings.ReplaceAll(result, "%p", "")
    }

    if b.checksum != nil {
        result = strings.ReplaceAll(result, "%k", b.Checksum().String())
    } else {
        result = strings.ReplaceAll(result, "%k", "")
    }

    result = strings.ReplaceAll(result, "%m", b.comment)

    return strings.TrimSpace(result)
}

//#endregion
//#region constructor

// New return a new block instance with the configurations wishes.
//
// command is a gcode with address or not that define the block command.
// options are a series of configuration callbacks to allow set different aspects of the block.
// each option provides a config object that can be used to load the values that define the block.
func New(command gcode.Gcoder, options ...block.BlockConstructorConfigurationCallbackable) (*GcodeBlock, error) {

    // command is required
    if command == nil {
        return nil, fmt.Errorf("command parameter is required")
    }

    // prepare a new GcodeBlock instance with some values by default
    gcodeBlock := &GcodeBlock{
        command:      command,
        gcodeFactory: &gcodefactory.GcodeFactory{},
        hash:         checksum.New(),
    }

    // prepare an instance of the BlockConfigurer interface to store each configuration callback received
    configurator := &blockConfigurator{}

    // call each options to load configurations callback at configurator instance
    for _, option := range options {
        err := option(configurator)
        if err != nil {
            return nil, fmt.Errorf("failed to load configuration: %w", err)
        }

        // apply each configuration callback that modify the new gcodeBlock instance
        for _, action := range configurator.configurationCallbacks {
            err := action(gcodeBlock)
            if err != nil {
                return nil, fmt.Errorf("failed to apply configuration: %w", err)
            }
        }
    }

    // if is necessary, can validate that gcodeBlock is in valid state
    if gcodeBlock.checksum != nil {
        if ok, err := gcodeBlock.VerifyChecksum(); !ok || err != nil {
            return gcodeBlock, fmt.Errorf("gcode block %s is invalid, checksum result is %v, error: %w ", gcodeBlock, ok, err)
        }
    }

    return gcodeBlock, nil
}

//#endregion

//#region package functions

// Parse returns a new block instance with the configurations we wish, from a single block line.
// The block line must contain the correct format, on the contrary, the parsing process will end with an error.
//
// source is the string line to parse.
// options are a series of configuration callbacks to allow set different aspects of the block.
// each option provides a config object that can be used to load the values that define the block.
func Parse(source string, options ...block.BlockParserConfigurationCallbackable) (*GcodeBlock, error) {

    gcodeFactory := &gcodefactory.GcodeFactory{}
    hashGenerator := checksum.New()

    gcodeBlock := &GcodeBlock{
        gcodeFactory: gcodeFactory,
        hash:         hashGenerator,
    }

    // prepare an instance of the BlockConfigurer interface to store each configuration callback received
    configurator := &blockConfigurator{}

    // call each options to load configurations callback at configurator instance
    for _, option := range options {
        err := option(configurator)
        if err != nil {
            return nil, fmt.Errorf("failed to load configuration: %w", err)
        }

        // apply each configuration callback that modify the new gcodeBlock instance
        for _, action := range configurator.configurationCallbacks {
            err := action(gcodeBlock)
            if err != nil {
                return nil, fmt.Errorf("failed to apply configuration: %w", err)
            }
        }
    }

    parse := prepareSourceToParse(source)

    // recover comments value if is exist
    element := take(parse, `\s*;.*$`)
    if element.taken != "" {
        gcodeBlock.comment = element.taken
        parse = strings.TrimSpace(element.remainder)
    }

    // recover linenumber value if is exist
    element = take(parse, `^N\d+`)
    if element.taken != "" {
        address, err := strconv.ParseInt(element.taken[1:], 10, 32)
        if err != nil {
            return nil, fmt.Errorf("try parse Linenumber %v: %w", element.taken, err)
        }
        gcode, err := gcodeBlock.gcodeFactory.NewAddressableGcodeUint32('N', uint32(address))
        if err != nil {
            return nil, fmt.Errorf("try generate Linenumber gcode: %w", err)
        }
        gcodeBlock.lineNumber = gcode
        parse = strings.TrimSpace(element.remainder)
    }

    // recover checksum value if is exist
    element = take(parse, `\b\*\d+$`)
    if element.taken != "" {
        address, err := strconv.ParseInt(element.taken[1:], 10, 32)
        if err != nil {
            return nil, fmt.Errorf("try parse checksum %v: %w", element.taken, err)
        }
        gcode, err := gcodeBlock.gcodeFactory.NewAddressableGcodeUint32('*', uint32(address))
        if err != nil {
            return nil, fmt.Errorf("try generate checksum gcode: %w", err)
        }
        gcodeBlock.checksum = gcode
        parse = strings.TrimSpace(element.remainder)
    }

    // apply mask to simplify quotes handle
    quoteRegex := regexp.MustCompile(`(?U)""`)
    parseQuotesSimplify := string(quoteRegex.ReplaceAll([]byte(parse), []byte{'#', '#'}))

    // get gcodes index from parseQuotesSimplify
    gcodesRegex := regexp.MustCompile(`(?U)(\w-?\d+(\.\d+)?\s)|(\w\.\d+\s)|(((^\w")|(\s*\w(##)*")).*")|(\w-?\d+(\.\d+)?$)|(\w\.\d+$)`)
    gcodesMatchIndex := gcodesRegex.FindAllStringIndex(parseQuotesSimplify, -1)
    if gcodesMatchIndex == nil {
        return nil, fmt.Errorf("failed to try get command gcode: There isn't match to (%d):%s", len(parse), parse)
    }

    // apply gcodes index getting, using parseQuotesSimplify, on parse
    // search characters remainders
    // if there are some characters remained then is error
    parseCheckEmpty := parse
    for _, loc := range gcodesMatchIndex {
        for i := loc[0]; i < loc[1]; i++ {
            out := []rune(parseCheckEmpty)
            out[i] = ' '
            parseCheckEmpty = string(out)
        }
    }
    parseCheckEmpty = strings.TrimSpace(parseCheckEmpty)
    if len(parseCheckEmpty) > 0 {
        return nil, fmt.Errorf("found undefined symbols %s", parseCheckEmpty)
    }

    // parsing gcodes
    for _, loc := range gcodesMatchIndex {
        m := parse[loc[0]:loc[1]]
        m = strings.TrimSpace(m)

        gcode, err := gcodeFactory.Parse(m)
        if err != nil {
            return nil, err
        }

        if gcodeBlock.command == nil {
            gcodeBlock.command = gcode
        } else {
            gcodeBlock.parameters = append(gcodeBlock.parameters, gcode)
        }
    }

    return gcodeBlock, nil
}

//#endregion
//#region private functions

// removeDuplicateSpaces remove all space char consecutive two or more times
func removeDuplicateSpaces(s string) string {
    rx := regexp.MustCompile(`\s{2,}`)
    return rx.ReplaceAllString(s, " ")
}

// removeSpecialChars remove all escape characters
func removeSpecialChars(s string) string {
    rx := regexp.MustCompile(`[\n\t\r]`)
    return rx.ReplaceAllString(s, " ")
}

// prepareSourceToParse modify a string to can be parsed for the Parse function
//
// It doesn't verify if s strings is a gcode line valid
func prepareSourceToParse(s string) string {
    s = strings.TrimSpace(s)
    s = removeDuplicateSpaces(s)
    s = removeSpecialChars(s)

    return s
}

type elementTaken struct {
    taken     string
    remainder string
}

func take(source string, regex string) elementTaken {

    r := regexp.MustCompile(regex)

    match := r.FindIndex([]byte(source))
    if match == nil {
        return elementTaken{remainder: source}
    }

    return elementTaken{taken: source[match[0]:match[1]], remainder: source[:match[0]] + source[match[1]:]}
}

//#endregion