feat(descriptions): Add human-readable descriptions of commonly used codes.

This commit is contained in:
James Harton 2021-01-05 15:45:02 +13:00
parent 875749e09b
commit dd763dc09a
21 changed files with 867 additions and 22 deletions

View file

@ -46,7 +46,7 @@ defmodule Gcode.Model.Block do
"""
@spec comment(t, Comment.t()) :: Result.t(t, block_error)
def comment(%Block{comment: none()} = block, %Comment{comment: some(_)} = comment),
def comment(%Block{comment: none()} = block, %Comment{} = comment),
do: {:ok, %Block{block | comment: some(comment)}}
def comment(%Block{comment: some(_)}, _comment),
@ -66,7 +66,7 @@ defmodule Gcode.Model.Block do
...> {:ok, block} = Block.push(block, word)
...> {:ok, word} = Word.init("N", 100)
...> Block.push(block, word)
{:ok, %Block{words: [%Word{word: some("N"), address: some(100)}, %Word{word: some("G"), address: some(0)}]}}
{:ok, %Block{words: [%Word{word: "N", address: 100}, %Word{word: "G", address: 0}]}}
"""
@spec push(t, block_contents) :: Result.t(t, block_error)
def push(%Block{words: words} = block, word)
@ -89,7 +89,7 @@ defmodule Gcode.Model.Block do
...> {:ok, word} = Word.init("N", 100)
...> {:ok, block} = Block.push(block, word)
...> Block.words(block)
{:ok, [%Word{word: some("G"), address: some(0)}, %Word{word: some("N"), address: some(100)}]}
{:ok, [%Word{word: "G", address: 0}, %Word{word: "N", address: 100}]}
"""
@spec words(t) :: Result.t([Word.t()], block_error)
def words(%Block{words: words}) when is_list(words), do: {:ok, Enum.reverse(words)}

View file

@ -0,0 +1,43 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Block do
alias Gcode.Model.{Block, Describe, Serialise}
use Gcode.Option
use Gcode.Result
@spec describe(Block.t(), options :: []) :: Option.t(String.t())
def describe(%Block{words: words, comment: some(comment)}, options) do
words = describe_words(words, options)
some("#{words} (#{comment.comment})")
end
def describe(%Block{words: words}, options) do
words = describe_words(words, options)
some(words)
end
defp describe_words(words, options) do
words
|> Enum.reverse()
|> Enum.map(&describe_or_serialise(&1, options))
|> Enum.reject(&Option.none?/1)
|> Enum.map(&Option.unwrap!/1)
|> Enum.join(", ")
end
defp describe_or_serialise(word, options) do
case Describe.describe(word, options) do
some(description) ->
some(description)
none() ->
case Serialise.serialise(word) do
ok(serialised) ->
serialised
|> Enum.join(", ")
|> some()
error(_) ->
none()
end
end
end
end

View file

@ -9,7 +9,7 @@ defmodule Gcode.Model.Comment do
"""
@type t :: %Comment{
comment: Option.t(String.t())
comment: String.t()
}
@type error :: {:comment_error, String.t()}
@ -21,12 +21,12 @@ defmodule Gcode.Model.Comment do
iex> "Doc, in the carpark, with plutonium"
...> |> Comment.init()
{:ok, %Comment{comment: some("Doc, in the carpark, with plutonium")}}
{:ok, %Comment{comment: "Doc, in the carpark, with plutonium"}}
"""
@spec init(String.t()) :: Result.t(t, error)
def init(comment) when is_binary(comment) do
if String.printable?(comment) do
ok(%Comment{comment: some(comment)})
ok(%Comment{comment: comment})
else
error(
{:comment_error,

View file

@ -0,0 +1,7 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Comment do
alias Gcode.Model.Comment
use Gcode.Option
@spec describe(Comment.t(), options :: []) :: Option.t(String.t())
def describe(_comment, _opts \\ []), do: none()
end

View file

@ -4,7 +4,7 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Comment do
use Gcode.Result
@spec serialise(Comment.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Comment{comment: some(comment)}) do
def serialise(%Comment{comment: comment}) when is_binary(comment) do
comment
|> String.split(~r/(\r\n|\r|\n)/)
|> Enum.reject(&(byte_size(&1) == 0))
@ -12,5 +12,5 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Comment do
|> ok()
end
def serialise(%Comment{comment: none()}), do: {:error, {:serialise_error, :empty_comment}}
def serialise(_comment), do: {:error, {:serialise_error, "Invalid comment"}}
end

View file

@ -0,0 +1,11 @@
defprotocol Gcode.Model.Describe do
alias Gcode.Model.Describe
use Gcode.Option
@moduledoc """
A protocol which is used to describe the model for human consumption.
"""
@spec describe(Describe.t(), options :: []) :: Option.t(String.t())
def describe(describable, opts \\ [])
end

View file

@ -0,0 +1,17 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Program do
alias Gcode.Model.{Describe, Program}
use Gcode.Option
@spec describe(Program.t(), options :: []) :: Option.t(String.t())
def describe(%Program{elements: elements}, options) do
lines =
elements
|> Enum.reverse()
|> Enum.map(&Describe.describe(&1, options))
|> Enum.reject(&(&1 == none()))
|> Enum.map(fn some(line) -> "#{line}\n" end)
|> Enum.join("")
some(lines)
end
end

View file

@ -1,4 +1,5 @@
defprotocol Gcode.Model.Serialise do
alias Gcode.Model.Serialise
alias Gcode.Result
@moduledoc """
@ -6,5 +7,5 @@ defprotocol Gcode.Model.Serialise do
"""
@spec serialise(Serialise.t()) :: Result.t([String.t()])
def serialise(value)
def serialise(serialisable)
end

View file

@ -0,0 +1,7 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Skip do
alias Gcode.Model.Skip
use Gcode.Option
@spec describe(Skip.t(), options :: []) :: Option.t(String.t())
def describe(_skip, _opts \\ []), do: none()
end

View file

@ -1,5 +1,5 @@
defmodule Gcode.Model.Tape do
defstruct leader: :none
defstruct leader: :error
alias Gcode.{Model.Tape}
use Gcode.Option
use Gcode.Result
@ -18,7 +18,7 @@ defmodule Gcode.Model.Tape do
## Example
iex> Tape.init()
{:ok, %Tape{leader: :none}}
{:ok, %Tape{leader: :error}}
"""
@spec init :: Result.t(t)
def init, do: ok(%Tape{leader: Option.none()})

View file

@ -0,0 +1,7 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Tape do
alias Gcode.Model.Tape
use Gcode.Option
@spec describe(Tape.t(), options :: []) :: Option.t(String.t())
def describe(_tape, _opts \\ []), do: none()
end

View file

@ -10,8 +10,8 @@ defmodule Gcode.Model.Word do
"""
@type t :: %Word{
word: Option.some(String.t()),
address: Option.some(number)
word: String.t(),
address: number
}
@doc """
@ -20,12 +20,12 @@ defmodule Gcode.Model.Word do
## Example
iex> Word.init("G", 0)
{:ok, %Word{word: {:ok, "G"}, address: {:ok, 0}}}
{:ok, %Word{word: "G", address: 0}}
"""
@spec init(String.t(), number) :: Result.t(t)
def init(word, address) when is_binary(word) and is_number(address) do
if Regex.match?(~r/^[A-Z]$/, word) do
ok(%Word{word: some(word), address: some(address)})
ok(%Word{word: word, address: address})
else
error({:word_error, "Expected word to be a single character, received #{inspect(word)}"})
end

View file

@ -0,0 +1,331 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Word do
use Gcode.Option
use Gcode.Result
alias Gcode.Model.Word
@moduledoc """
Describes common/conventional words.
"""
@type options :: [option]
@type option :: operation | positioning | compensation | units
@type operation :: {:operation, :milling | :turning | :printing | :plotting}
@type positioning :: {:positioning, :absolute | :relative}
@type compensation :: {:compensation, :left | :right}
@type units :: {:units, :mm | :inches}
@doc "Refer `describe/2`"
@spec describe(Word.t()) :: Option.t(String.t())
def describe(word), do: do_describe(word, %{})
@doc """
Describe a word for human consumption.
*Note:* Many words have different meanings depending on the operation, machine
state or program state. Use can use the `options` argument to provide a hint,
otherwise a more-generic response will be shown.
## Examples
iex> {:ok, word} = Word.init("N", 100)
...> Word.Describe.describe(word)
{:ok, "Line/block number 100"}
iex> {:ok, word} = Word.init("G", 0)
...> Word.Describe.describe(word)
{:ok, "Rapid move"}
iex> {:ok, word} = Word.init("A", 15)
{:ok, "Rotate A axis counterclockwise by/to 15º"}
iex> {:ok, word} = Word.init("A", -15, positioning: :absolute)
{:ok, "Rotate A axis clockwise to 15º"}
iex> {:ok, word} = Word.init("G", 8)
:error
"""
@spec describe(Word.t(), options) :: Option.t(String.t())
def describe(%Word{} = word, options) when is_list(options),
do: do_describe(word, Enum.into(options, %{}))
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute})
when axis in ~w[A B C] and angle >= 0,
do: some("Rotate #{axis} axis counterclockwise to #{angle}º")
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :relative})
when axis in ~w[A B C] and angle >= 0,
do: some("Rotate #{axis} axis counterclockwise by #{angle}º")
defp do_describe(%Word{word: axis, address: angle}, _)
when axis in ~w[A B C] and angle >= 0,
do: some("Rotate #{axis} axis counterclockwise by/to #{angle}º")
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute})
when axis in ~w[A B C] and angle < 0,
do: some("Rotate #{axis} axis clockwise to #{abs(angle)}º")
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :relative})
when axis in ~w[A B C] and angle < 0,
do: some("Rotate #{axis} axis clockwise by #{abs(angle)}º")
defp do_describe(%Word{word: axis, address: angle}, _)
when axis in ~w[A B C] and angle < 0,
do: some("Rotate #{axis} axis clockwise by/to #{abs(angle)}º")
defp do_describe(%Word{word: "D", address: depth}, %{operation: :turning} = options),
do: some("Depth of cut #{distance_with_unit(depth, options)}")
defp do_describe(%Word{word: "D", address: aperture}, %{operation: :plotting}),
do: some("Aperture #{aperture}")
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :left} = options),
do: some("Left radial offset #{distance_with_unit(offset, options)}")
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :right} = options),
do: some("Right radial offset #{distance_with_unit(offset, options)}")
defp do_describe(%Word{word: "D", address: offset}, options),
do: some("Radial offset #{distance_with_unit(offset, options)}")
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :printing} = options),
do: some("Extruder feedrate #{feedrate(feedrate, options)}")
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :turning} = options),
do: some("Precision feedrate #{feedrate(feedrate, options)}")
defp do_describe(%Word{word: "F", address: feedrate}, options),
do: some("Feedrate #{feedrate(feedrate, options)}")
defp do_describe(%Word{word: "G", address: 0}, _), do: some("Rapid move")
defp do_describe(%Word{word: "G", address: 1}, _), do: some("Linear move")
defp do_describe(%Word{word: "G", address: 2}, _), do: some("Clockwise circular move")
defp do_describe(%Word{word: "G", address: 3}, _), do: some("Counterclockwise circular move")
defp do_describe(%Word{word: "G", address: 4}, _), do: some("Dwell")
defp do_describe(%Word{word: "G", address: 5}, _), do: some("High-precision contour control")
defp do_describe(%Word{word: "G", address: 5.1}, _), do: some("AI advanced preview control")
defp do_describe(%Word{word: "G", address: 6.1}, _), do: some("NURBS machining")
defp do_describe(%Word{word: "G", address: 7}, _), do: some("Imaginary axis designation")
defp do_describe(%Word{word: "G", address: 9}, _), do: some("Exact stop check - non-modal")
defp do_describe(%Word{word: "G", address: 10}, _), do: some("Programmable data input")
defp do_describe(%Word{word: "G", address: 11}, _), do: some("Data write cancel")
defp do_describe(%Word{word: "G", address: 17}, _), do: some("XY plane selection")
defp do_describe(%Word{word: "G", address: 18}, _), do: some("ZX plane selection")
defp do_describe(%Word{word: "G", address: 19}, _), do: some("YZ plane selection")
defp do_describe(%Word{word: "G", address: 20}, _), do: some("Unit is inches")
defp do_describe(%Word{word: "G", address: 21}, _), do: some("Unit is mm")
defp do_describe(%Word{word: "G", address: 28}, _), do: some("Return to home position")
defp do_describe(%Word{word: "G", address: 30}, _),
do: some("Return to secondary home position")
defp do_describe(%Word{word: "G", address: 31}, _), do: some("Feed until skip function")
defp do_describe(%Word{word: "G", address: 32}, _), do: some("Single-point threading")
defp do_describe(%Word{word: "G", address: 33}, _), do: some("Variable pitch threading")
defp do_describe(%Word{word: "G", address: 40}, _), do: some("Tool radius compensation off")
defp do_describe(%Word{word: "G", address: 41}, _), do: some("Tool radius compensation left")
defp do_describe(%Word{word: "G", address: 42}, _), do: some("Tool radius compensation right")
defp do_describe(%Word{word: "G", address: 43}, _),
do: some("Tool height offset compensation negative")
defp do_describe(%Word{word: "G", address: 44}, _),
do: some("Tool height offset compensation positive")
defp do_describe(%Word{word: "G", address: 45}, _), do: some("Axis offset single increase")
defp do_describe(%Word{word: "G", address: 46}, _), do: some("Axis offset single decrease")
defp do_describe(%Word{word: "G", address: 47}, _), do: some("Axis offset double increase")
defp do_describe(%Word{word: "G", address: 48}, _), do: some("Axis offset double decrease")
defp do_describe(%Word{word: "G", address: 49}, _),
do: some("Tool length offset compensation cancel")
defp do_describe(%Word{word: "G", address: 50}, %{operation: :turning}),
do: some("Position register")
defp do_describe(%Word{word: "G", address: 50}, _), do: some("Scaling function cancel")
defp do_describe(%Word{word: "G", address: 52}, _), do: some("Local coordinate system")
defp do_describe(%Word{word: "G", address: 53}, _), do: some("Machine coordinate system")
defp do_describe(%Word{word: "G", address: address}, _)
when address in [54, 55, 56, 57, 58, 59, 54.1],
do: some("Work coordinate system")
defp do_describe(%Word{word: "G", address: 61}, _), do: some("Exact stop check - modal")
defp do_describe(%Word{word: "G", address: 62}, _), do: some("Automatic corner override")
defp do_describe(%Word{word: "G", address: 64}, _), do: some("Default cutting mode")
defp do_describe(%Word{word: "G", address: 68}, _), do: some("Rotate coordinate system")
defp do_describe(%Word{word: "G", address: 69}, _),
do: some("Turn off coordinate system rotation")
defp do_describe(%Word{word: "G", address: 70}, %{operation: :turning}),
do: some("Fixed cycle, multiple repetitive cycle - for finishing")
defp do_describe(%Word{word: "G", address: 71}, %{operation: :turning}),
do: some("Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis")
defp do_describe(%Word{word: "G", address: 72}, %{operation: :turning}),
do: some("Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis")
defp do_describe(%Word{word: "G", address: 73}, %{operation: :turning}),
do: some("Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition")
defp do_describe(%Word{word: "G", address: 73}, _), do: some("Peck drilling cycle")
defp do_describe(%Word{word: "G", address: 74}, %{operation: :turning}),
do: some("Peck drilling cycle")
defp do_describe(%Word{word: "G", address: 74}, _), do: some("Tapping cycle")
defp do_describe(%Word{word: "G", address: 75}, %{operation: :turning}),
do: some("Peck grooving cycle")
defp do_describe(%Word{word: "G", address: 76}, %{operation: :turning}),
do: some("Threading cycle")
defp do_describe(%Word{word: "G", address: 76}, _), do: some("Fine boring cycle")
defp do_describe(%Word{word: "G", address: 80}, _), do: some("Cancel cycle")
defp do_describe(%Word{word: "G", address: 81}, _), do: some("Simple drilling cycle")
defp do_describe(%Word{word: "G", address: 82}, _), do: some("Drilling cycle with dwell")
defp do_describe(%Word{word: "G", address: 83}, _), do: some("Peck drilling cycle")
defp do_describe(%Word{word: "G", address: 84}, _),
do: some("Tapping cycle, righthand thread, M03 spindle direction")
defp do_describe(%Word{word: "G", address: 84.2}, _),
do: some("Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder")
defp do_describe(%Word{word: "G", address: 84.3}, _),
do: some("Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder")
defp do_describe(%Word{word: "G", address: 85}, _), do: some("Boring cycle, feed in/feed out")
defp do_describe(%Word{word: "G", address: 86}, _),
do: some("Boring cycle, feed in/spindle stop/rapid out")
defp do_describe(%Word{word: "G", address: 87}, _), do: some("Boring cycle, backboring")
defp do_describe(%Word{word: "G", address: 88}, _),
do: some("Boring cycle, feed in/spindle stop/manual operation")
defp do_describe(%Word{word: "G", address: 89}, _),
do: some("Boring cycle, feed in/dwell/feed out")
defp do_describe(%Word{word: "G", address: 90}, _), do: some("Absolute positioning")
defp do_describe(%Word{word: "G", address: 91}, _), do: some("Relative positioning")
defp do_describe(%Word{word: "G", address: 92}, _), do: some("Position register")
defp do_describe(%Word{word: "G", address: 94}, _), do: some("Feedrate per minute")
defp do_describe(%Word{word: "G", address: 95}, _), do: some("Feedrate per revolution")
defp do_describe(%Word{word: "G", address: 96}, _), do: some("Constant surface speed")
defp do_describe(%Word{word: "G", address: 97}, _), do: some("Constant spindle speed")
defp do_describe(%Word{word: "G", address: 98}, %{operation: :turning}),
do: some("Feedrate per minute")
defp do_describe(%Word{word: "G", address: 98}, _),
do: some("Return to initial Z level in canned cycle")
defp do_describe(%Word{word: "G", address: 99}, %{operation: :turning}),
do: some("Feedrate per revolution")
defp do_describe(%Word{word: "G", address: 99}, _),
do: some("Return to R level in canned cycle")
defp do_describe(%Word{word: "G", address: 100}, _), do: some("Tool length measurement")
defp do_describe(%Word{word: "H", address: length}, options),
do: some("Tool length offset #{distance_with_unit(length, options)}")
defp do_describe(%Word{word: "I", address: offset}, options),
do: some("X arc center offset #{distance_with_unit(offset, options)}")
defp do_describe(%Word{word: "J", address: offset}, options),
do: some("Y arc center offset #{distance_with_unit(offset, options)}")
defp do_describe(%Word{word: "K", address: offset}, options),
do: some("Z arc center offset #{distance_with_unit(offset, options)}")
defp do_describe(%Word{word: "L", address: count}, _), do: some("Loop count #{count}")
defp do_describe(%Word{word: "M", address: 0}, _), do: some("Compulsory stop")
defp do_describe(%Word{word: "M", address: 1}, _), do: some("Optional stop")
defp do_describe(%Word{word: "M", address: 2}, _), do: some("End of program")
defp do_describe(%Word{word: "M", address: 3}, _), do: some("Spindle on clockwise")
defp do_describe(%Word{word: "M", address: 4}, _), do: some("Spindle on counterclockwise")
defp do_describe(%Word{word: "M", address: 5}, _), do: some("Spindle stop")
defp do_describe(%Word{word: "M", address: 6}, _), do: some("Automatic tool change")
defp do_describe(%Word{word: "M", address: 7}, _), do: some("Coolant mist")
defp do_describe(%Word{word: "M", address: 8}, _), do: some("Coolant flood")
defp do_describe(%Word{word: "M", address: 9}, _), do: some("Coolant off")
defp do_describe(%Word{word: "M", address: 10}, _), do: some("Pallet clamp on")
defp do_describe(%Word{word: "M", address: 11}, _), do: some("Pallet clamp off")
defp do_describe(%Word{word: "M", address: 13}, _),
do: some("Spindle on clockwise and coolant flood")
defp do_describe(%Word{word: "M", address: 19}, _), do: some("Spindle orientation")
defp do_describe(%Word{word: "M", address: 21}, %{operation: :turning}),
do: some("Tailstock forward")
defp do_describe(%Word{word: "M", address: 21}, _), do: some("Mirror X axis")
defp do_describe(%Word{word: "M", address: 22}, %{operation: :turning}),
do: some("Tailstock backward")
defp do_describe(%Word{word: "M", address: 22}, _), do: some("Mirror Y axis")
defp do_describe(%Word{word: "M", address: 23}, %{operation: :turning}),
do: some("Thread gradual pullout on")
defp do_describe(%Word{word: "M", address: 23}, _), do: some("Mirror off")
defp do_describe(%Word{word: "M", address: 24}, %{operation: :turning}),
do: some("Thread gradual pullout off")
defp do_describe(%Word{word: "M", address: 30}, _), do: some("End of program")
defp do_describe(%Word{word: "M", address: gear}, %{operation: :turning})
when is_integer(gear) and gear > 40 and gear < 45,
do: some("Gear select #{gear - 40}")
defp do_describe(%Word{word: "M", address: 48}, _), do: some("Feedrate override allowed")
defp do_describe(%Word{word: "M", address: 49}, _), do: some("Feedrate override not allowed")
defp do_describe(%Word{word: "M", address: 52}, _), do: some("Unload tool")
defp do_describe(%Word{word: "M", address: 60}, _), do: some("Automatic pallet change")
defp do_describe(%Word{word: "M", address: 98}, _), do: some("Subprogram call")
defp do_describe(%Word{word: "M", address: 99}, _), do: some("Subprogram end")
defp do_describe(%Word{word: "M", address: 100}, _), do: some("Clean nozzle")
defp do_describe(%Word{word: "N", address: line}, _), do: some("Line #{line}")
defp do_describe(%Word{word: "O", address: name}, _), do: some("Program #{name}")
defp do_describe(%Word{word: "P", address: param}, _), do: some("Parameter #{param}")
defp do_describe(%Word{word: "Q", address: distance}, options),
do: some("Peck increment #{distance_with_unit(distance, options)}")
defp do_describe(%Word{word: "R", address: distance}, options),
do: some("Radius #{distance_with_unit(distance, options)}")
defp do_describe(%Word{word: "S", address: speed}, _), do: some("Speed #{speed}")
defp do_describe(%Word{word: "T", address: tool}, _), do: some("Tool #{tool}")
defp do_describe(%Word{word: "X", address: position}, options),
do: some("X #{distance_with_unit(position, options)}")
defp do_describe(%Word{word: "Y", address: position}, options),
do: some("Y #{distance_with_unit(position, options)}")
defp do_describe(%Word{word: "Z", address: position}, options),
do: some("Z #{distance_with_unit(position, options)}")
defp do_describe(%Word{}, _options), do: none()
defp distance_with_unit(distance, %{units: :mm}), do: "#{distance}mm"
defp distance_with_unit(distance, %{units: :inches}), do: "#{distance}\""
defp distance_with_unit(distance, _), do: distance
defp feedrate(distance, %{operation: :turning} = options),
do: "#{distance_with_unit(distance, options)}/rev"
defp feedrate(distance, options), do: "#{distance_with_unit(distance, options)}/min"
end

View file

@ -0,0 +1,20 @@
defimpl Inspect, for: Gcode.Model.Word do
alias Gcode.Model.{Describe, Word}
use Gcode.Option
import Inspect.Algebra
@moduledoc false
def inspect(%Word{word: letter, address: address} = word, opts) do
case Describe.describe(word) do
some(description) ->
concat([
"#Gcode.Word<",
to_doc([word: letter, address: address], opts),
" (#{description})>"
])
none() ->
concat(["#Gcode.Word<", to_doc([word: letter, address: address], opts), ">"])
end
end
end

View file

@ -4,11 +4,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Word do
use Gcode.Result
@spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Word{word: some(word), address: some(address)}) when is_integer(address) do
def serialise(%Word{word: word, address: address}) when is_integer(address) do
ok(["#{word}#{address}"])
end
def serialise(%Word{word: some(word), address: some(address)}) when is_float(address) do
def serialise(%Word{word: word, address: address}) when is_float(address) do
ok(["#{word}#{address}"])
end

View file

@ -11,22 +11,23 @@ defmodule Gcode.Option do
end
end
@type t :: t(any)
@type t(value) :: some(value) | opt_none
@type some(t) :: {:ok, t}
@type opt_none :: :none
@type opt_none :: :error
@spec none?(t(any)) :: boolean
def none?(:none), do: true
def none?(:error), do: true
def none?({:ok, _}), do: false
@spec some?(t(any)) :: boolean
def some?(:none), do: false
def some?(:error), do: false
def some?({:ok, _}), do: true
@spec none :: Macro.t()
defmacro none do
quote do
:none
:error
end
end
@ -36,4 +37,8 @@ defmodule Gcode.Option do
{:ok, unquote(pattern)}
end
end
@spec unwrap!(t) :: any | no_return
def unwrap!({:ok, result}), do: result
def unwrap!(:error), do: raise("Attempt to unwrap a none")
end

View file

@ -1,5 +1,6 @@
defmodule Gcode.MixProject do
use Mix.Project
@moduledoc false
@version "0.2.1"
@description """
@ -15,7 +16,8 @@ defmodule Gcode.MixProject do
package: package(),
description: @description,
deps: deps(),
consolidate_protocols: Mix.env() != :test
consolidate_protocols: Mix.env() != :test,
elixirc_paths: elixirc_paths(Mix.env())
]
end
@ -45,4 +47,7 @@ defmodule Gcode.MixProject do
{:git_ops, "~> 2.3", only: ~w[dev test]a, runtime: false}
]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
end

View file

@ -0,0 +1,11 @@
defmodule Gcode.Model.Block.DescribeTest do
use DescribeCase, async: true
@moduledoc false
describes_block("A13 B-15.2",
with: [positioning: :absolute],
as: "Rotate A axis counterclockwise to 13º, Rotate B axis clockwise to 15.2º"
)
describes_block("G90 M213", as: "Absolute positioning, M213")
end

View file

@ -0,0 +1,46 @@
defmodule Gcode.Model.Program.DescribeTest do
use DescribeCase, async: true
@moduledoc false
@program """
O4968
N01 M216
N02 G20 G90 G54 D200 G40
N03 G50 S2000
N04 T0300
N05 G96 S854 M03
N06 G41 G00 X1.1 Z1.1 T0303 M08
N07 G01 Z1.0 F.05
N08 X-0.016
N09 G00 Z1.1
N10 X1.0
N11 G01 Z0.0 F.05
N12 G00 X1.1 M05 M09
N13 G91 G28 X0
N14 G91 G28 Z0
N15 G90
N16 M30
"""
@expected """
Program 4968
Line 1, M216
Line 2, Unit is inches, Absolute positioning, Work coordinate system, Radial offset 200, Tool radius compensation off
Line 3, Scaling function cancel, Speed 2000
Line 4, Tool 300
Line 5, Constant surface speed, Speed 854, Spindle on clockwise
Line 6, Tool radius compensation left, Rapid move, X 1.1, Z 1.1, Tool 303, Coolant flood
Line 7, Linear move, Z 1.0
Line 8, X -0.016
Line 9, Rapid move, Z 1.1
Line 10, X 1.0
Line 11, Linear move, Z 0.0
Line 12, Rapid move, X 1.1, Spindle stop, Coolant off
Line 13, Relative positioning, Return to home position, X 0
Line 14, Relative positioning, Return to home position, Z 0
Line 15, Absolute positioning
Line 16, End of program
"""
describes_program(@program, as: @expected)
end

View file

@ -0,0 +1,192 @@
defmodule Gcode.Model.Word.DescribeTest do
use DescribeCase, async: true
@moduledoc false
describes_word("A13", as: "Rotate A axis counterclockwise by/to 13º")
describes_word("B-15.2", with: [positioning: :absolute], as: "Rotate B axis clockwise to 15.2º")
describes_word("C99",
with: [positioning: :relative],
as: "Rotate C axis counterclockwise by 99º"
)
describes_word("D22", as: "Radial offset 22")
describes_word("D22", with: [operation: :turning], as: "Depth of cut 22")
describes_word("D22", with: [operation: :plotting], as: "Aperture 22")
describes_word("D22", with: [compensation: :left, units: :mm], as: "Left radial offset 22mm")
describes_word("D22",
with: [compensation: :right, units: :inches],
as: "Right radial offset 22\""
)
describes_word("E123",
with: [operation: :turning, units: :mm],
as: "Precision feedrate 123mm/rev"
)
describes_word("E123",
with: [operation: :printing, units: :inches],
as: "Extruder feedrate 123\"/min"
)
describes_word("E123", with: [operation: :turning], as: "Precision feedrate 123/rev")
describes_word("F100", with: [operation: :turning, units: :mm], as: "Feedrate 100mm/rev")
describes_word("F100", with: [units: :inches], as: "Feedrate 100\"/min")
describes_word("F100", as: "Feedrate 100/min")
describes_word("G0", as: "Rapid move")
describes_word("G1", as: "Linear move")
describes_word("G2", as: "Clockwise circular move")
describes_word("G3", as: "Counterclockwise circular move")
describes_word("G4", as: "Dwell")
describes_word("G5", as: "High-precision contour control")
describes_word("G5.1", as: "AI advanced preview control")
describes_word("G6.1", as: "NURBS machining")
describes_word("G7", as: "Imaginary axis designation")
describes_word("G9", as: "Exact stop check - non-modal")
describes_word("G10", as: "Programmable data input")
describes_word("G11", as: "Data write cancel")
describes_word("G17", as: "XY plane selection")
describes_word("G18", as: "ZX plane selection")
describes_word("G19", as: "YZ plane selection")
describes_word("G20", as: "Unit is inches")
describes_word("G21", as: "Unit is mm")
describes_word("G28", as: "Return to home position")
describes_word("G30", as: "Return to secondary home position")
describes_word("G31", as: "Feed until skip function")
describes_word("G32", as: "Single-point threading")
describes_word("G33", as: "Variable pitch threading")
describes_word("G40", as: "Tool radius compensation off")
describes_word("G41", as: "Tool radius compensation left")
describes_word("G42", as: "Tool radius compensation right")
describes_word("G43", as: "Tool height offset compensation negative")
describes_word("G44", as: "Tool height offset compensation positive")
describes_word("G45", as: "Axis offset single increase")
describes_word("G46", as: "Axis offset single decrease")
describes_word("G47", as: "Axis offset double increase")
describes_word("G48", as: "Axis offset double decrease")
describes_word("G49", as: "Tool length offset compensation cancel")
describes_word("G50", as: "Scaling function cancel")
describes_word("G50", with: [operation: :turning], as: "Position register")
describes_word("G52", as: "Local coordinate system")
describes_word("G53", as: "Machine coordinate system")
describes_word("G54", as: "Work coordinate system")
describes_word("G55", as: "Work coordinate system")
describes_word("G56", as: "Work coordinate system")
describes_word("G57", as: "Work coordinate system")
describes_word("G58", as: "Work coordinate system")
describes_word("G59", as: "Work coordinate system")
describes_word("G54.1", as: "Work coordinate system")
describes_word("G61", as: "Exact stop check - modal")
describes_word("G62", as: "Automatic corner override")
describes_word("G64", as: "Default cutting mode")
describes_word("G68", as: "Rotate coordinate system")
describes_word("G69", as: "Turn off coordinate system rotation")
describes_word("G70",
with: [operation: :turning],
as: "Fixed cycle, multiple repetitive cycle - for finishing"
)
describes_word("G71",
with: [operation: :turning],
as: "Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis"
)
describes_word("G72",
with: [operation: :turning],
as: "Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis"
)
describes_word("G73",
with: [operation: :turning],
as: "Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition"
)
describes_word("G73", as: "Peck drilling cycle")
describes_word("G74", with: [operation: :turning], as: "Peck drilling cycle")
describes_word("G74", as: "Tapping cycle")
describes_word("G75", with: [operation: :turning], as: "Peck grooving cycle")
describes_word("G76", as: "Fine boring cycle")
describes_word("G76", with: [operation: :turning], as: "Threading cycle")
describes_word("G80", as: "Cancel cycle")
describes_word("G81", as: "Simple drilling cycle")
describes_word("G82", as: "Drilling cycle with dwell")
describes_word("G83", as: "Peck drilling cycle")
describes_word("G84", as: "Tapping cycle, righthand thread, M03 spindle direction")
describes_word("G84.2",
as: "Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder"
)
describes_word("G84.3",
as: "Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder"
)
describes_word("G85", as: "Boring cycle, feed in/feed out")
describes_word("G86", as: "Boring cycle, feed in/spindle stop/rapid out")
describes_word("G87", as: "Boring cycle, backboring")
describes_word("G88", as: "Boring cycle, feed in/spindle stop/manual operation")
describes_word("G89", as: "Boring cycle, feed in/dwell/feed out")
describes_word("G90", as: "Absolute positioning")
describes_word("G91", as: "Relative positioning")
describes_word("G92", as: "Position register")
describes_word("G94", as: "Feedrate per minute")
describes_word("G95", as: "Feedrate per revolution")
describes_word("G96", as: "Constant surface speed")
describes_word("G97", as: "Constant spindle speed")
describes_word("G98", as: "Return to initial Z level in canned cycle")
describes_word("G98", with: [operation: :turning], as: "Feedrate per minute")
describes_word("G99", as: "Return to R level in canned cycle")
describes_word("G99", with: [operation: :turning], as: "Feedrate per revolution")
describes_word("G100", as: "Tool length measurement")
describes_word("H76", with: [units: :mm], as: "Tool length offset 76mm")
describes_word("I1.21", with: [units: :mm], as: "X arc center offset 1.21mm")
describes_word("J1.21", with: [units: :inches], as: "Y arc center offset 1.21\"")
describes_word("K1.21", as: "Z arc center offset 1.21")
describes_word("L87", as: "Loop count 87")
describes_word("M0", as: "Compulsory stop")
describes_word("M1", as: "Optional stop")
describes_word("M2", as: "End of program")
describes_word("M3", as: "Spindle on clockwise")
describes_word("M4", as: "Spindle on counterclockwise")
describes_word("M5", as: "Spindle stop")
describes_word("M6", as: "Automatic tool change")
describes_word("M7", as: "Coolant mist")
describes_word("M8", as: "Coolant flood")
describes_word("M9", as: "Coolant off")
describes_word("M10", as: "Pallet clamp on")
describes_word("M11", as: "Pallet clamp off")
describes_word("M13", as: "Spindle on clockwise and coolant flood")
describes_word("M19", as: "Spindle orientation")
describes_word("M21", as: "Mirror X axis")
describes_word("M21", with: [operation: :turning], as: "Tailstock forward")
describes_word("M22", as: "Mirror Y axis")
describes_word("M22", with: [operation: :turning], as: "Tailstock backward")
describes_word("M23", as: "Mirror off")
describes_word("M23", with: [operation: :turning], as: "Thread gradual pullout on")
describes_word("M24", with: [operation: :turning], as: "Thread gradual pullout off")
describes_word("M30", as: "End of program")
describes_word("M41", with: [operation: :turning], as: "Gear select 1")
describes_word("M42", with: [operation: :turning], as: "Gear select 2")
describes_word("M43", with: [operation: :turning], as: "Gear select 3")
describes_word("M44", with: [operation: :turning], as: "Gear select 4")
describes_word("M48", as: "Feedrate override allowed")
describes_word("M49", as: "Feedrate override not allowed")
describes_word("M52", as: "Unload tool")
describes_word("M60", as: "Automatic pallet change")
describes_word("M98", as: "Subprogram call")
describes_word("M99", as: "Subprogram end")
describes_word("M100", as: "Clean nozzle")
describes_word("N100", as: "Line 100")
describes_word("O200", as: "Program 200")
describes_word("P12", as: "Parameter 12")
describes_word("Q34", as: "Peck increment 34")
describes_word("R37", with: [units: :mm], as: "Radius 37mm")
describes_word("S12", as: "Speed 12")
describes_word("T4", as: "Tool 4")
describes_word("X2.34", as: "X 2.34")
describes_word("Y3.45", as: "Y 3.45")
describes_word("Z4.56", as: "Z 4.56")
end

View file

@ -0,0 +1,142 @@
defmodule DescribeCase do
use ExUnit.CaseTemplate
alias Gcode.Model.{Block, Describe, Program, Word}
use Gcode.Option
use Gcode.Result
@moduledoc false
using do
quote do
use Gcode.Option
use Gcode.Result
import DescribeCase
end
end
@regex ~r/^(?<word>[A-Z])(?<address>[+-]?(?<float>[0-9]+\.)?[0-9]+)$/
def make_word(input) do
case Regex.named_captures(@regex, input) do
%{"word" => word, "address" => address, "float" => ""} ->
Word.init(word, String.to_integer(address))
%{"word" => word, "address" => address, "float" => _} ->
Word.init(word, String.to_float(address))
_ ->
none()
end
end
def make_block(input) do
{:ok, block} = Block.init()
input
|> String.trim()
|> String.split(~r/\s+/)
|> Enum.map(&make_word/1)
|> Enum.reject(&Option.none?/1)
|> Enum.map(&Result.unwrap!/1)
|> Result.Enum.reduce_while_ok(block, &Block.push(&2, &1))
end
@spec make_program(binary) :: {:error, any} | {:ok, any}
def make_program(input) do
{:ok, program} = Program.init()
input
|> String.trim()
|> String.split("\n")
|> Enum.map(&make_block/1)
|> Enum.reject(&Option.none?/1)
|> Enum.map(&Result.unwrap!/1)
|> Result.Enum.reduce_while_ok(program, &Program.push(&2, &1))
end
defmacro describes_word(input, opts) when is_list(opts) do
output = Keyword.fetch!(opts, :as)
options = Keyword.get(opts, :with, [])
description =
if Enum.any?(options) do
plural = if Enum.count(options) == 1, do: "option", else: "options"
description =
options
|> Enum.map(fn {k, v} -> "#{k}: #{v}" end)
|> Enum.join(", ")
"#{input} with #{plural} #{description}"
else
input
end
quote do
describe unquote(description) do
test "is described as #{inspect(unquote(output))}" do
assert ok(word) = make_word(unquote(input))
assert ok(description) = Describe.describe(word, unquote(options))
assert description == unquote(output)
end
end
end
end
defmacro describes_block(input, opts) when is_list(opts) do
output = Keyword.fetch!(opts, :as)
options = Keyword.get(opts, :with, [])
description =
if Enum.any?(options) do
plural = if Enum.count(options) == 1, do: "option", else: "options"
description =
options
|> Enum.map(fn {k, v} -> "#{k}: #{v}" end)
|> Enum.join(", ")
"#{input} with #{plural} #{description}"
else
input
end
quote do
describe unquote(description) do
test "is described as #{inspect(unquote(output))}" do
assert ok(block) = make_block(unquote(input))
assert ok(description) = Describe.describe(block, unquote(options))
assert description == unquote(output)
end
end
end
end
defmacro describes_program(input, opts) do
output = Keyword.fetch!(opts, :as)
options = Keyword.get(opts, :with, [])
description =
if Enum.any?(options) do
plural = if Enum.count(options) == 1, do: "option", else: "options"
description =
options
|> Enum.map(fn {k, v} -> "#{k}: #{v}" end)
|> Enum.join(", ")
"program with #{plural} #{description}"
else
"program"
end
quote do
describe unquote(description) do
test "is described correctly" do
assert ok(program) = make_program(unquote(input))
assert ok(description) = Describe.describe(program, unquote(options))
assert description == unquote(output)
end
end
end
end
end