defmodule Modbux.Rtu.Framer do @moduledoc """ A framer for Modbus RTU frames. This framer doesn't do anything for the transmit direction, but for receives, it will collect bytes that follows the Modbus RTU protocol, it also execute the CRC validation and returns the frame or the error. """ @behaviour Circuits.UART.Framing require Logger alias Modbux.Helper defmodule State do @moduledoc false defstruct behavior: nil, max_len: nil, expected_length: nil, index: 0, processed: <<>>, in_process: <<>>, fc: nil, error: nil, error_message: nil, lines: [] end def init(args) do # modbus standard max len max_len = Keyword.get(args, :max_len, 255) behavior = Keyword.get(args, :behavior, :slave) state = %State{max_len: max_len, behavior: behavior} {:ok, state} end # do nothing, we assume this is already in the right form def add_framing(data, state) do {:ok, data, state} end def remove_framing(data, state) do # Logger.debug("new data #{inspect(state)}") new_state = process_data(state.in_process <> data, state.expected_length, state.processed, state) rc = if buffer_empty?(new_state), do: :ok, else: :in_frame # Logger.debug("new data processed #{inspect(new_state)}, #{inspect(rc)}") if has_error?(new_state), do: dispatch({:ok, new_state.error_message, new_state}), else: dispatch({rc, new_state.lines, new_state}) end def frame_timeout(state) do partial_line = {:partial, state.processed <> state.in_process} new_state = %State{max_len: state.max_len, behavior: state.behavior} {:ok, [partial_line], new_state} end def flush(direction, state) when direction == :receive or direction == :both do %State{max_len: state.max_len, behavior: state.behavior} end def flush(_direction, state) do state end # helper functions # handle empty byte defp process_data(<<>>, _len, _in_process, state) do # Logger.debug("End #{inspect(state)}") state end # get first byte (index 0) defp process_data(<>, nil, processed, %{index: 0} = state) do # Logger.debug("line 0") new_state = %{state | index: 1, processed: processed <> <>, in_process: <<>>} process_data(b_tail, nil, new_state.processed, new_state) end # get second byte (function code) (index 1) defp process_data(<>, nil, processed, %{index: 1} = state) do # Logger.debug("line 1") new_state = %{state | fc: fc, index: 2, processed: processed <> <>} process_data(b_tail, nil, new_state.processed, new_state) end # Clause for functions code => 5, 6 defp process_data(<>, nil, processed, %{index: 2, fc: fc} = state) when fc in [5, 6] do # Logger.debug("fc 5 or 6") new_state = %{state | expected_length: 8, index: 3, processed: processed <> <>} process_data(b_tail, new_state.expected_length, new_state.processed, new_state) end # Clause for functions code => 15 and 16 defp process_data(<>, nil, processed, %{index: 2, fc: fc} = state) when fc in [15, 16] do # Logger.debug("fc 15 or 16") new_state = if state.behavior == :slave do %{state | expected_length: 7, index: 3, processed: processed <> <>} else %{state | expected_length: 8, index: 3, processed: processed <> <>} end process_data(b_tail, new_state.expected_length, new_state.processed, new_state) end defp process_data(<>, _len, processed, %{index: 6, fc: fc} = state) when fc in [15, 16] do # Logger.debug("fc 15 or 16, len") new_state = if state.behavior == :slave do %{state | expected_length: len + 9, index: 7, processed: processed <> <>} else %{state | expected_length: 8, index: 7, processed: processed <> <>} end process_data(b_tail, new_state.expected_length, new_state.processed, new_state) end # Clause for functions code => 1, 2, 3 and 4 defp process_data(<>, nil, processed, %{index: 2, fc: fc} = state) when fc in 1..4 do # Logger.debug("fc 1, 2, 3 or 4") new_state = if state.behavior == :slave do %{state | expected_length: 8, index: 3, processed: processed <> <>} else %{state | expected_length: len + 5, index: 3, processed: processed <> <>} end process_data(b_tail, new_state.expected_length, new_state.processed, new_state) end # Clause for exceptions. defp process_data(<>, nil, processed, %{index: 2, fc: fc} = state) when fc in 129..144 do # Logger.debug("exceptions") new_state = %{state | expected_length: 5, index: 3, processed: processed <> <>} process_data(b_tail, new_state.expected_length, new_state.processed, new_state) end # Catch all fc (error) defp process_data(_data, nil, processed, %{index: 2, fc: _fc} = state) do %{state | error: true, error_message: [{:error, :einval, processed}]} end defp process_data(<>, len, processed, state) when is_binary(processed) do current_data = processed <> <> # Logger.info( # "(#{__MODULE__}) data_len: #{byte_size(current_data)}, len: #{inspect(len)}, state: #{inspect(state)}" # ) if len == byte_size(current_data) do new_state = %{ state | expected_length: nil, lines: [current_data], in_process: b_tail, index: 0, processed: <<>> } # we got the whole thing in 1 pass, so we're done check_crc(new_state) else new_state = %{state | index: state.index + 1, processed: current_data} # need to keep reading process_data(b_tail, len, current_data, new_state) end end def buffer_empty?(state) do state.processed == <<>> and state.in_process == <<>> end defp has_error?(state) do state.error != nil end defp dispatch({:in_frame, _lines, _state} = msg), do: msg defp dispatch({rc, msg, state}) do {rc, msg, %State{max_len: state.max_len, behavior: state.behavior}} end # once we have the full packet, verify it's CRC16 defp check_crc(state) do [packet] = state.lines packet_without_crc = Kernel.binary_part(packet, 0, byte_size(packet) - 2) expected_crc = Kernel.binary_part(packet, byte_size(packet), -2) <> = Helper.crc(packet_without_crc) real_crc = <> # Logger.info("(#{__MODULE__}) #{inspect(expected_crc)} == #{inspect(real_crc)}") if real_crc == expected_crc, do: state, else: %{state | error: true, error_message: [{:error, :ecrc, "CRC Error"}]} end end