192 lines
4.9 KiB
Elixir
192 lines
4.9 KiB
Elixir
defmodule Farmbot.Firmware.UartHandler.Framing do
|
|
@behaviour Circuits.UART.Framing
|
|
import Farmbot.Firmware.Gcode.Parser
|
|
use Farmbot.Logger
|
|
|
|
# credo:disable-for-this-file Credo.Check.Refactor.FunctionArity
|
|
|
|
@moduledoc """
|
|
Each message is one line. This framer appends and removes newline sequences
|
|
as part of the framing. Buffering is performed internally, so users can get
|
|
the complete messages under normal circumstances. Attention should be paid
|
|
to the following:
|
|
|
|
1. Lines must have a fixed max length so that a misbehaving sender can't
|
|
cause unbounded buffer expansion. When the max length is passed, a
|
|
`{:partial, data}` is reported. The application can decide what to do with
|
|
this.
|
|
2. The separation character varies depending on the target device. Some
|
|
devices require "\\r\\n" sequences, so be sure to specify this. Currently
|
|
only one or two character separators are supported.
|
|
3. It may be desirable to set a `:rx_framer_timeout` to prevent
|
|
characters received in error from collecting during idle times. When the
|
|
receive timer expires, `{:partial, data}` is reported.
|
|
4. Line separators must be ASCII characters (0-127) or be valid UTF-8
|
|
sequences. If the device only sends ASCII, high characters (128-255)
|
|
should work as well. [Note: please report if using extended
|
|
characters.]
|
|
"""
|
|
|
|
defmodule State do
|
|
@moduledoc false
|
|
defstruct max_length: nil,
|
|
separator: nil,
|
|
processed: <<>>,
|
|
in_process: <<>>,
|
|
log_input: false,
|
|
log_output: false
|
|
end
|
|
|
|
def init(args) do
|
|
max_length = Keyword.get(args, :max_length, 4096)
|
|
separator = Keyword.get(args, :separator, "\n")
|
|
|
|
log_input =
|
|
Farmbot.System.ConfigStorage.get_config_value(
|
|
:bool,
|
|
"settings",
|
|
"firmware_input_log"
|
|
)
|
|
|
|
log_output =
|
|
Farmbot.System.ConfigStorage.get_config_value(
|
|
:bool,
|
|
"settings",
|
|
"firmware_output_log"
|
|
)
|
|
|
|
state = %State{
|
|
max_length: max_length,
|
|
separator: separator,
|
|
log_input: log_input,
|
|
log_output: log_output
|
|
}
|
|
|
|
{:ok, state}
|
|
end
|
|
|
|
def add_framing(data, state) do
|
|
# maybe log output here
|
|
if state.log_output do
|
|
Logger.debug(3, data)
|
|
end
|
|
|
|
{:ok, data <> state.separator, state}
|
|
end
|
|
|
|
def remove_framing(data, state) do
|
|
{new_processed, new_in_process, lines} =
|
|
process_data(
|
|
state.separator,
|
|
byte_size(state.separator),
|
|
state.max_length,
|
|
state.processed,
|
|
state.in_process <> data,
|
|
[],
|
|
state.log_input
|
|
)
|
|
|
|
new_state = %{state | processed: new_processed, in_process: new_in_process}
|
|
rc = if buffer_empty?(new_state), do: :ok, else: :in_frame
|
|
{rc, lines, new_state}
|
|
end
|
|
|
|
def frame_timeout(state) do
|
|
partial_line = {:partial, state.processed <> state.in_process}
|
|
new_state = %{state | processed: <<>>, in_process: <<>>}
|
|
{:ok, [partial_line], new_state}
|
|
end
|
|
|
|
def flush(direction, state)
|
|
when direction == :receive or direction == :both do
|
|
%{state | processed: <<>>, in_process: <<>>}
|
|
end
|
|
|
|
def flush(_direction, state) do
|
|
state
|
|
end
|
|
|
|
def buffer_empty?(state) do
|
|
state.processed == <<>> and state.in_process == <<>>
|
|
end
|
|
|
|
# Handle not enough data case
|
|
defp process_data(
|
|
_separator,
|
|
sep_length,
|
|
_max_length,
|
|
processed,
|
|
to_process,
|
|
lines,
|
|
_log_input
|
|
)
|
|
when byte_size(to_process) < sep_length do
|
|
{processed, to_process, lines}
|
|
end
|
|
|
|
# Process data until separator or next char
|
|
defp process_data(
|
|
separator,
|
|
sep_length,
|
|
max_length,
|
|
processed,
|
|
to_process,
|
|
lines,
|
|
log_input
|
|
) do
|
|
case to_process do
|
|
# Handle separater
|
|
<<^separator::binary-size(sep_length), rest::binary>> ->
|
|
new_lines = lines ++ [do_parse_code(processed, log_input)]
|
|
|
|
process_data(
|
|
separator,
|
|
sep_length,
|
|
max_length,
|
|
<<>>,
|
|
rest,
|
|
new_lines,
|
|
log_input
|
|
)
|
|
|
|
# Handle line too long case
|
|
to_process when byte_size(processed) == max_length and to_process != <<>> ->
|
|
new_lines = lines ++ [{:partial, processed}]
|
|
|
|
process_data(
|
|
separator,
|
|
sep_length,
|
|
max_length,
|
|
<<>>,
|
|
to_process,
|
|
new_lines,
|
|
log_input
|
|
)
|
|
|
|
# Handle next char
|
|
<<next_char::binary-size(1), rest::binary>> ->
|
|
process_data(
|
|
separator,
|
|
sep_length,
|
|
max_length,
|
|
processed <> next_char,
|
|
rest,
|
|
lines,
|
|
log_input
|
|
)
|
|
end
|
|
end
|
|
|
|
defp do_parse_code(processed, log_input) do
|
|
if log_input do
|
|
Logger.debug(3, processed)
|
|
end
|
|
|
|
parse_code(processed)
|
|
rescue
|
|
er ->
|
|
Logger.error(1, "Firmware parser error: #{Exception.message(er)}")
|
|
{nil, :noop}
|
|
end
|
|
end
|