Packages

Parse SBus packets from an Frsky SBus receiver

Current section

Files

Jump to
frsky_parser lib frsky_parser.ex
Raw

lib/frsky_parser.ex

defmodule FrskyParser do
require Logger
use Bitwise
@moduledoc """
Parse and store SBus packets coming from FrSky receiver.
"""
@pw_mid 991
@pw_half_range 819
@start_byte 0x0F
@end_byte 0x00
@payload_length 24
@doc false
defstruct parser_state: 0,
prev_byte: 0,
payload_rev: [],
payload_ready: false,
channels: [],
remaining_data: []
# Public API
@doc """
Create a new FrskyParser struct that can parse new serial data and
store the most recently received RX output.
"""
@spec new() :: struct()
def new() do
%FrskyParser{}
end
@doc """
Appends the latest data to any leftover data from the previous `check_for_new_messages` operation.
Arguments are the `%FrskyParser{}` struct and the newest serial data from the receiver (must already by converted from binary to list)
Returns `{%FrskyParser{}, [list of channels]}`. If no valid SBus messages was found, the list of channels will be empty.
NOTE: After a valid message has been received, the `clear` function must be called if you do not want the channel values to persist.
Otherwise this function will continue to return a populated channel list even if a new valid message has not been received.
Example:
```
{frsky_parser, channel_values} = FrskyParser.check_for_new_messages(frsky_parser, new_data_list)
frsky_parser = FrskyParser.clear(frsky_parser)
```
"""
@spec check_for_new_messages(struct(), list()) :: tuple()
def check_for_new_messages(frsky, data) do
frsky = parse_data(frsky, data)
if frsky.payload_ready do
{frsky, frsky.channels}
else
{frsky, []}
end
end
@doc false
@spec parse_data(struct(), list()) :: struct()
def parse_data(frsky, data) do
data = frsky.remaining_data ++ data
if Enum.empty?(data) do
frsky
else
{[byte], remaining_data} = Enum.split(data, 1)
frsky = parse_byte(frsky, byte)
cond do
frsky.payload_ready -> %{frsky | remaining_data: remaining_data}
Enum.empty?(remaining_data) -> %{frsky | remaining_data: []}
true -> parse_data(%{frsky | remaining_data: []}, remaining_data)
end
end
end
@doc false
@spec parse_byte(struct(), integer()) :: struct()
def parse_byte(frsky, byte) do
parser_state = frsky.parser_state
frsky =
if parser_state == 0 do
if byte == @start_byte and frsky.prev_byte == @end_byte do
%{frsky | parser_state: parser_state + 1, payload_rev: []}
else
frsky
end
else
{parser_state, payload_rev} =
if parser_state - 1 < @payload_length do
payload_rev = [byte] ++ frsky.payload_rev
{parser_state + 1, payload_rev}
else
{parser_state, frsky.payload_rev}
end
if parser_state - 1 == @payload_length do
if byte == @end_byte do
parse_payload(frsky, payload_rev)
|> Map.put(:parser_state, 0)
else
%{frsky | parser_state: 0}
end
else
%{frsky | parser_state: parser_state, payload_rev: payload_rev}
end
end
%{frsky | prev_byte: byte}
end
@doc false
@spec parse_payload(struct(), list()) :: struct()
def parse_payload(frsky, payload_rev) do
payload = Enum.reverse(payload_rev)
channels = [
Enum.at(payload, 0) + (Enum.at(payload, 1) <<< 8),
(Enum.at(payload, 1) >>> 3) + (Enum.at(payload, 2) <<< 5),
(Enum.at(payload, 2) >>> 6) + (Enum.at(payload, 3) <<< 2) + (Enum.at(payload, 4) <<< 10),
(Enum.at(payload, 4) >>> 1) + (Enum.at(payload, 5) <<< 7),
(Enum.at(payload, 5) >>> 4) + (Enum.at(payload, 6) <<< 4),
(Enum.at(payload, 6) >>> 7) + (Enum.at(payload, 7) <<< 1) + (Enum.at(payload, 8) <<< 9),
(Enum.at(payload, 8) >>> 2) + (Enum.at(payload, 9) <<< 6),
(Enum.at(payload, 9) >>> 5) + (Enum.at(payload, 10) <<< 3),
Enum.at(payload, 11) + (Enum.at(payload, 12) <<< 8),
(Enum.at(payload, 12) >>> 3) + (Enum.at(payload, 13) <<< 5),
(Enum.at(payload, 13) >>> 6) + (Enum.at(payload, 14) <<< 2) + (Enum.at(payload, 15) <<< 10),
(Enum.at(payload, 15) >>> 1) + (Enum.at(payload, 16) <<< 7),
(Enum.at(payload, 16) >>> 4) + (Enum.at(payload, 17) <<< 4),
(Enum.at(payload, 17) >>> 7) + (Enum.at(payload, 18) <<< 1) + (Enum.at(payload, 19) <<< 9),
(Enum.at(payload, 19) >>> 2) + (Enum.at(payload, 20) <<< 6),
(Enum.at(payload, 20) >>> 5) + (Enum.at(payload, 21) <<< 3)
]
channels =
Enum.reduce(Enum.reverse(channels), [], fn value, acc ->
# [(value &&& 0x07FF)] ++ acc
value_float = constrain(((value &&& 0x07FF) - @pw_mid) / @pw_half_range, -1.0, 1.0)
[value_float] ++ acc
end)
flag_byte = Enum.at(payload, 22)
# failsafe_active = ((flag_byte &&& 0x08) > 0)
frame_lost = (flag_byte &&& 0x04) > 0
%{frsky | payload_ready: !frame_lost, channels: channels}
end
@doc """
Returns the stored channel values.
"""
@spec channels(struct()) :: list()
def channels(frsky) do
frsky.channels
end
@doc """
Empties the stored channel value list.
"""
@spec clear(struct()) :: struct()
def clear(frsky) do
%{frsky | payload_ready: false}
end
@doc false
@spec constrain(number(), number(), number()) :: number()
def constrain(x, min_value, max_value) do
case x do
_ when x > max_value -> max_value
_ when x < min_value -> min_value
x -> x
end
end
end