Gary Thom
Gary Thom

Reputation: 21

In Elixir, how do I pattern match against a binary stream which arrives via TCP in broken up packets

Background:

I have a serial to ethernet adaptor which is receiving status updates (Alarm Panel) and making those available via an IP/PORT on my home network. Each packet is part of the overall message and the protocol is binary.

Question:

How do I assemble the individual packets and binary data into a buffer that I can pattern match against. The data I'm interested in is mixed in with other data that I'm not. There are no real delimiters but the message I'm interested in will always start with 0x01 0x06 then the message is a fixed length (other messages are variable length)

I know how to connect to the IP/Port and receive the data, one byte at a time, but I'm stuck with how to gather them and do a sliding window match on the data till I find the packet of information I'm interested in.

Example Packet that I wish to extract from the data stream:

The packet will contain a checksum computed as the sum of all previous bytes.

Upvotes: 2

Views: 878

Answers (1)

tkowal
tkowal

Reputation: 9299

The pattern match for your example packet could be something like this:

<<real_time_status_message :: bytes-size(1),
  total_bytes :: bytes-size(1),
  status_descriptor_area :: bytes-size(1),
  main_status_descriptor :: bytes-size(1),
  status_descriptor_zone :: bytes-size(1),
  checksum :: bytes-size(1)>> = binary

However before you can apply the pattern match you need to find the beginning of message. You don't have to actually receive input byte after byte. You can get chunks of variable length in a loop.

#parser reads tcp data and maintains state of unparsed input (leftovers)
def parse(), do: parse("")
def parse(leftovers) do
  chunk = #receive binary from tcp
  binary_to_scan = leftovers <> chunk
  {:messages, msgs, rest} = scan_all(binary_to_scan)
  do_something_with_msgs(msgs)
  parse(rest)
end

# there might be more than one msg in binary,
# so we need recursive function with accumulator to collect them
def scan_all(binary), do: scan_all(binary, [])
def scan_all(binary, acc) do
  case scan(binary) do
    {:found, _previous, msg, rest} ->
      scan_all(rest, [msg | acc])
    {:not_found, binary} ->
      {:messages, Enum.reverse(acc), binary}
  end
end

# actual sliding window uses pattern matching with bytes-size
def scan(binary), do: scan(binary, byte_size(binary), 0)
def scan(binary, size, n) when size == (n - 2) do
  {:not_found, binary}
end
def scan(binary, size, n) do
  case binary do
    << part1 :: bytes-size(n), 1, 6, msg :: bytes-size(4), rest :: binary >> ->
      {:found, <<part1>>, <<1, 6, msg :: binary>>, rest}
    binary ->
      scan(binary, size, n+1)
  end
end

I typed this code by hand so it might have bugs or syntactic problems, but you can treat it as a pseudo code implementing the scanning algorithm.

Upvotes: 2

Related Questions