Add VCR system for FarmbotFirmware

pull/974/head
Connor Rigby 2019-05-29 11:25:31 -07:00
parent 469ac1b8ed
commit 849f7af1c3
No known key found for this signature in database
GPG Key ID: 29A88B24B70456E0
4 changed files with 154 additions and 6 deletions

View File

@ -22,3 +22,5 @@ erl_crash.dump
# Ignore package tarball (built via "mix hex.build").
farmbot_firmware-*.tar
# Ignore vcr files
*.txt

View File

@ -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

View File

@ -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

View File

@ -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