Add VCR system for FarmbotFirmware
parent
469ac1b8ed
commit
849f7af1c3
|
@ -22,3 +22,5 @@ erl_crash.dump
|
|||
# Ignore package tarball (built via "mix hex.build").
|
||||
farmbot_firmware-*.tar
|
||||
|
||||
# Ignore vcr files
|
||||
*.txt
|
|
@ -77,6 +77,29 @@ defmodule FarmbotFirmware do
|
|||
def handle_call({"166", {:parameter_write, [some_param: 100.00]}}, _from, state)
|
||||
|
||||
and reply with `:ok | {:error, term()}`
|
||||
|
||||
# VCR
|
||||
|
||||
This server can save all the input and output gcodes to a text file for
|
||||
further external analysis or playback later.
|
||||
|
||||
## Using VCR mode
|
||||
|
||||
The server can be started in VCR mode by doing:
|
||||
|
||||
FarmbotFirmware.start_link([transport: FarmbotFirmware.StubTransport, vcr_path: "/tmp/vcr.txt"], [])
|
||||
|
||||
or can be started at runtime:
|
||||
|
||||
FarmbotFirmware.enter_vcr_mode(firmware_server, "/tmp/vcr.txt")
|
||||
|
||||
in either case the VCR recording needs to be stopped:
|
||||
|
||||
FarmbotFirmware.exit_vcr_mode(firmware_server)
|
||||
|
||||
VCRs can later be played back:
|
||||
|
||||
FarmbotFirmware.VCR.playback!("/tmp/vcr.txt")
|
||||
"""
|
||||
use GenServer
|
||||
require Logger
|
||||
|
@ -105,7 +128,8 @@ defmodule FarmbotFirmware do
|
|||
:configuration_queue,
|
||||
:command_queue,
|
||||
:caller_pid,
|
||||
:current
|
||||
:current,
|
||||
:vcr_fd
|
||||
]
|
||||
|
||||
@type state :: %State{
|
||||
|
@ -119,7 +143,8 @@ defmodule FarmbotFirmware do
|
|||
configuration_queue: [{GCODE.kind(), GCODE.args()}],
|
||||
command_queue: [{pid(), GCODE.t()}],
|
||||
caller_pid: nil | pid,
|
||||
current: nil | GCODE.t()
|
||||
current: nil | GCODE.t(),
|
||||
vcr_fd: nil | File.io_device()
|
||||
}
|
||||
|
||||
@doc """
|
||||
|
@ -175,6 +200,22 @@ defmodule FarmbotFirmware do
|
|||
GenServer.call(server, {:open_transport, module, args})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the Firmware server to record input and output GCODES
|
||||
to a pair of text files.
|
||||
"""
|
||||
def enter_vcr_mode(server \\ __MODULE__, tape_path) do
|
||||
GenServer.call(server, {:enter_vcr_mode, tape_path})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the Firmware server to stop recording input and output
|
||||
GCODES.
|
||||
"""
|
||||
def exit_vcr_mode(server \\ __MODULE__) do
|
||||
GenServer.cast(server, :exit_vcr_mode)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starting the Firmware server requires at least:
|
||||
* `:transport` - a module implementing the Transport GenServer behaviour.
|
||||
|
@ -193,6 +234,16 @@ defmodule FarmbotFirmware do
|
|||
transport = Keyword.fetch!(args, :transport)
|
||||
side_effects = Keyword.get(args, :side_effects)
|
||||
|
||||
vcr_fd =
|
||||
case Keyword.get(args, :vcr_path) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
tape_path ->
|
||||
{:ok, vcr_fd} = File.open(tape_path, [:binary, :append, :exclusive, :write])
|
||||
vcr_fd
|
||||
end
|
||||
|
||||
# Add an anon function that transport implementations should call.
|
||||
fw = self()
|
||||
fun = fn {_, _} = code -> GenServer.cast(fw, code) end
|
||||
|
@ -206,7 +257,8 @@ defmodule FarmbotFirmware do
|
|||
side_effects: side_effects,
|
||||
status: :transport_boot,
|
||||
command_queue: [],
|
||||
configuration_queue: []
|
||||
configuration_queue: [],
|
||||
vcr_fd: vcr_fd
|
||||
}
|
||||
|
||||
send(self(), :timeout)
|
||||
|
@ -251,7 +303,8 @@ defmodule FarmbotFirmware do
|
|||
case GenServer.call(state.transport_pid, {state.tag, code}) do
|
||||
:ok ->
|
||||
new_state = %{state | current: code, configuration_queue: rest}
|
||||
side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
|
||||
_ = side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
|
||||
_ = vcr_write(state.vcr_fd, :out, {state.tag, code})
|
||||
{:noreply, new_state}
|
||||
|
||||
error ->
|
||||
|
@ -263,7 +316,8 @@ defmodule FarmbotFirmware do
|
|||
case GenServer.call(state.transport_pid, {tag, code}) do
|
||||
:ok ->
|
||||
new_state = %{state | tag: tag, current: code, command_queue: rest, caller_pid: pid}
|
||||
side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
|
||||
_ = side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
|
||||
_ = vcr_write(state.vcr_fd, :out, {state.tag, code})
|
||||
{:noreply, new_state}
|
||||
|
||||
error ->
|
||||
|
@ -317,6 +371,15 @@ defmodule FarmbotFirmware do
|
|||
{:reply, {:error, s}, state}
|
||||
end
|
||||
|
||||
def handle_call({:enter_vcr_mode, tape_path}, _from, state) do
|
||||
with {:ok, vcr_fd} <- File.open(tape_path, [:binary, :append, :exclusive, :write]) do
|
||||
{:reply, :ok, %{state | vcr_fd: vcr_fd}}
|
||||
else
|
||||
error ->
|
||||
{:reply, error, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({_tag, _code} = gcode, from, state) do
|
||||
handle_command(gcode, from, state)
|
||||
end
|
||||
|
@ -358,9 +421,15 @@ defmodule FarmbotFirmware do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_cast(:exit_vcr_mode, state) do
|
||||
state.vcr_fd && File.close(state.vcr_fd)
|
||||
{:noreply, %{state | vcr_fd: nil}}
|
||||
end
|
||||
|
||||
# Extracts tag
|
||||
def handle_cast({tag, {_, _} = code}, state) do
|
||||
side_effects(state, :handle_input_gcode, [{tag, code}])
|
||||
_ = side_effects(state, :handle_input_gcode, [{tag, code}])
|
||||
_ = vcr_write(state.vcr_fd, :in, {tag, code})
|
||||
handle_report(code, %{state | tag: tag})
|
||||
end
|
||||
|
||||
|
@ -616,4 +685,17 @@ defmodule FarmbotFirmware do
|
|||
@spec side_effects(state, atom, GCODE.args()) :: any()
|
||||
defp side_effects(%{side_effects: nil}, _function, _args), do: nil
|
||||
defp side_effects(%{side_effects: m}, function, args), do: apply(m, function, args)
|
||||
|
||||
@spec vcr_write(nil | File.io_device(), :in | :out, GCODE.t()) :: :ok
|
||||
defp vcr_write(nil, _direction, _code), do: :ok
|
||||
|
||||
defp vcr_write(fd, :in, code), do: vcr_write(fd, "<", code)
|
||||
|
||||
defp vcr_write(fd, :out, code), do: vcr_write(fd, ">", code)
|
||||
|
||||
defp vcr_write(fd, direction, code) do
|
||||
data = GCODE.encode(code)
|
||||
time = :os.system_time()
|
||||
IO.write(fd, direction <> " #{time} " <> data <> "\n")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,7 @@ defmodule FarmbotFirmware.GCODE do
|
|||
| :report_invalid
|
||||
| :report_home_complete
|
||||
| :report_position
|
||||
| :report_position_change
|
||||
| :report_parameters_complete
|
||||
| :report_parameter_value
|
||||
| :report_calibration_parameter_value
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
defmodule FarmbotFirmware.VCR do
|
||||
@moduledoc """
|
||||
Helpers for working with Firmware tapes
|
||||
"""
|
||||
alias FarmbotFirmware.GCODE
|
||||
|
||||
@doc "Convert a .txt file to Elixir terms"
|
||||
def to_elixir!(path) do
|
||||
File.stream!(path)
|
||||
|> Stream.map(&split_decode/1)
|
||||
|> Enum.to_list()
|
||||
end
|
||||
|
||||
@doc "Play a tape back on a server"
|
||||
def playback!(path, firmware_server \\ FarmbotFirmware) do
|
||||
path
|
||||
|> to_elixir!()
|
||||
|> Enum.reject(fn
|
||||
{:in, _timestamp, _type, _code} -> true
|
||||
{:out, _timestamp, _type, _code} -> false
|
||||
end)
|
||||
|> Enum.each(fn {:out, _timestamp, type, code} ->
|
||||
apply(FarmbotFirmware, type, [firmware_server, code])
|
||||
end)
|
||||
end
|
||||
|
||||
defp split_decode(data) do
|
||||
data
|
||||
|> do_split()
|
||||
|> do_decode()
|
||||
end
|
||||
|
||||
defp do_split(data) do
|
||||
data
|
||||
|> String.trim()
|
||||
|> String.split(" ")
|
||||
end
|
||||
|
||||
defp do_decode([direction, timestamp | rest]) do
|
||||
direction = decode_direction(direction)
|
||||
timestamp = decode_timestamp(timestamp)
|
||||
|
||||
case GCODE.decode(Enum.join(rest, " ")) do
|
||||
{_, {kind, _args}} = code
|
||||
when kind not in [
|
||||
:parameter_read,
|
||||
:status_read,
|
||||
:pin_read,
|
||||
:end_stops_read,
|
||||
:position_read,
|
||||
:software_version_read
|
||||
] ->
|
||||
{direction, timestamp, :command, code}
|
||||
|
||||
code ->
|
||||
{direction, timestamp, :request, code}
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_direction("<"), do: :in
|
||||
defp decode_direction(">"), do: :out
|
||||
defp decode_timestamp(timestamp), do: String.to_integer(timestamp)
|
||||
end
|
Loading…
Reference in New Issue