@rebar update-deps
dialyze:
- @dialyzer ebin deps/hope/ebin
+ @dialyzer ebin
test:
@rebar ct skip_deps=true --verbose=0
-------
```erlang
--include_lib("include/x_plane_data.hrl").
-
main(Port) ->
{ok, Socket} = gen_udp:open(Port, [binary, {active, false}]),
{ok, {_, _, <<XPlaneDataPacket/binary>>}} = gen_udp:recv(Socket, 0),
- {ok, XPlaneData} = x_plane_data:of_bin(XPlaneDataPacket),
-
- % Currently there're 133 possible data types sent by X-Plane 10, of which
- % I've identified and labeled only some of. See x_plane_datum:t() type for
- % what is currently labeled.
- % The types I've not yet labeled are in the format specified by
- % x_plane_datum:anonymous() and can be looked-up by their index number.
-
- % Find labeled data types
- {some, #x_plane_datum_speeds
- { vind_kias = VindKias
- , vind_keas = VindKeas
- , vtrue_ktas = VtrueKtas
- , vtrue_ktgs = VtrueKtgs
- , vind_mph = VindMph
- , vtrue_mphas = VtrueMphas
- , vtrue_mphgs = VtrueMphgs
- },
- } = hope_kv_list:get(XPlaneData, speeds),
-
- {some, #x_plane_datum_pitch_roll_heading
- { pitch_deg = PitchDeg
- , roll_deg = RollDeg
- , hding_true = HdingTrue
- , hding_mag = HdingMag
- },
- } = hope_kv_list:get(XPlaneData, pitch_roll_heading),
-
- {some, #x_plane_datum_lat_lon_alt
- { lat_deg = LatDeg
- , lon_deg = LonDeg
- , alt_ftmsl = AltFtmsl
- , alt_ftagl = AltFtagl
- , on_runwy = OnRunwy
- , alt_ind = AltInd
- , lat_south = LatSouth
- , lon_west = LatWest
- },
- } = hope_kv_list:get(XPlaneData, lat_lon_alt),
-
- % Find an unlabled data type
- {some, {10, V1, V2, V3, V4, V5, V6, V7, V8}} = hope_kv_list:get(XPlaneData, 10),
-
- % Attempt to find a data type that was not included in current packet
- none = hope_kv_list:get(XPlaneData, 130),
- none = hope_kv_list:get(XPlaneData, 67),
+ {ok, {Index, Groups}} = x_plane_data_raw:of_bin(XPlaneDataPacket),
+
+ % Speeds are in group 3
+ {3, Speeds} = lists:keyfind(3, 1, Groups),
+ { VindKias
+ , VindKeas
+ , VtrueKtas
+ , VtrueKtgs
+ , _
+ , VindMph
+ , VtrueMphas
+ , VtrueMphgs
+ } = Speeds,
+
+ % Pitch roll and headings values are in group 17
+ {17, PitchRollHeadings} = lists:keyfind(17, 1, Groups),
+ { PitchDeg
+ , RollDeg
+ , HdingTrue
+ , HdingMag
+ , _
+ , _
+ , _
+ , _
+ } = PitchRollHeadings,
...
```
-Note: you can, of course, use any other method to search a `[{K, V}]` list
-(which is how `x_plane_data:t()` is structured), such as:
-`proplists:get_value/2`, `lists:keyfind/3`, etc., but I prefer the API of
-`hope_kv_list`, so I used that.
-
-
Data format references
----------------------
+++ /dev/null
--record(x_plane_datum_speeds,
- { vind_kias :: float() % 1
- , vind_keas :: float() % 2
- , vtrue_ktas :: float() % 3
- , vtrue_ktgs :: float() % 4
- % 5
- , vind_mph :: float() % 6
- , vtrue_mphas :: float() % 7
- , vtrue_mphgs :: float() % 8
- }).
-
--record(x_plane_datum_pitch_roll_heading,
- { pitch_deg :: float() % 1
- , roll_deg :: float() % 2
- , hding_true :: float() % 3
- , hding_mag :: float() % 4
- % 5
- % 6
- % 7
- % 8
- }).
-
--record(x_plane_datum_lat_lon_alt,
- { lat_deg :: float() % 1
- , lon_deg :: float() % 2
- , alt_ftmsl :: float() % 3
- , alt_ftagl :: float() % 4
- , on_runwy :: float() % 5
- , alt_ind :: float() % 6
- , lat_south :: float() % 7
- , lon_west :: float() % 8
- }).
%%% vim: set filetype=erlang:
{ deps
-, [ {hope , ".*" , {git , "https://github.com/ibnfirnas/hope.git" , {tag , "3.8.1"}}}
+, [
]
}.
{application, x_plane_data,
[
{description, "X-Plane UDP data packet parser."},
- {vsn, "0.0.1"},
+ {vsn, "0.1.0"},
{registered, []},
{applications, [
kernel,
+++ /dev/null
--module(x_plane_data).
-
--include("x_plane_datum_defaults.hrl").
-
--export_type(
- [ t/0
- ]).
-
--export(
- [ of_bin/1
- ]).
-
--type parsing_error() ::
- packet_unrecognized
- | packet_length_invalid
- | x_plane_datum:parsing_error()
- .
-
--type t() ::
- [x_plane_datum:t()].
-
--define(BYTE_SIZE_OF_EACH_BLOCK, 36).
-
--spec of_bin(binary()) ->
- hope_result:t(t(), parsing_error()).
-of_bin(<<Packet/binary>>) ->
- of_bin(Packet, ?DEFAULT_MAX_INDEX).
-
--spec of_bin(binary(), non_neg_integer()) ->
- hope_result:t(t(), parsing_error()).
-of_bin(<<"DATA", _PacketIndexByte:1/bytes, ContiguousBlocks/binary>>, MaxIndex) ->
- % Packet index byte seems to be changing from X-Plane version to version.
- % What is it's meaning?
- if byte_size(ContiguousBlocks) rem ?BYTE_SIZE_OF_EACH_BLOCK =:= 0 ->
- Blocks = blocks_split(ContiguousBlocks),
- ParseBlock = fun (B) -> x_plane_datum:of_bin(B, MaxIndex) end,
- hope_list:map_result(Blocks, ParseBlock)
- ; true ->
- {error, packet_length_invalid}
- end;
-of_bin(<<_/binary>>, _) ->
- {error, packet_unrecognized}.
-
--spec blocks_split(binary()) ->
- [binary()].
-blocks_split(<<>>) ->
- [];
-blocks_split(<<Block:?BYTE_SIZE_OF_EACH_BLOCK/bytes, Blocks/binary>>) ->
- [Block | blocks_split(Blocks)].
--- /dev/null
+-module(x_plane_data_raw).
+
+-export_type(
+ [ t/0
+ , index/0
+ , group_index/0
+ , group_values/0
+ , group/0
+ , groups/0
+ ]).
+
+-export(
+ [ of_bin/1
+ ]).
+
+-type parsing_error() ::
+ packet_bad_header
+ | packet_bad_length
+ .
+
+-type group_index() ::
+ non_neg_integer().
+
+-type group_values() ::
+ { float()
+ , float()
+ , float()
+ , float()
+ , float()
+ , float()
+ , float()
+ , float()
+ }.
+
+-type group() ::
+ {group_index(), group_values()}.
+
+% Packet index byte. Essentially a schema version.
+-type index() ::
+ integer().
+
+-type groups() ::
+ [group()].
+
+-type t() ::
+ {index(), groups()}.
+
+-define(BYTE_SIZE_OF_EACH_BLOCK, 36).
+-define(PACKET_HEADER, "DATA").
+
+-spec of_bin(binary()) ->
+ {ok, t()}
+ | {error, parsing_error()}
+ .
+of_bin(<<?PACKET_HEADER, _:8/integer, ContiguousBlocks/binary>>)
+ when byte_size(ContiguousBlocks) rem ?BYTE_SIZE_OF_EACH_BLOCK =/= 0 ->
+ {error, packet_bad_length};
+of_bin(<<?PACKET_HEADER, Index:8/integer, ContiguousBlocks/binary>>) ->
+ Groups = [group_of_bin(B) || B <- blocks_split(ContiguousBlocks)],
+ {ok, {Index, Groups}};
+of_bin(<<_/binary>>) ->
+ {error, packet_bad_header}.
+
+-spec blocks_split(binary()) ->
+ [binary()].
+blocks_split(<<>>) ->
+ [];
+blocks_split(<<Block:?BYTE_SIZE_OF_EACH_BLOCK/bytes, Blocks/binary>>) ->
+ [Block | blocks_split(Blocks)].
+
+-spec group_of_bin(binary()) ->
+ group().
+group_of_bin(
+ << Index:32/little-integer
+ , V1:32/little-float
+ , V2:32/little-float
+ , V3:32/little-float
+ , V4:32/little-float
+ , V5:32/little-float
+ , V6:32/little-float
+ , V7:32/little-float
+ , V8:32/little-float
+ >>
+) ->
+ Values = {V1, V2, V3, V4, V5, V6, V7, V8},
+ {Index, Values}.
+++ /dev/null
--module(x_plane_datum).
-
--include("x_plane_datum_defaults.hrl").
--include("include/x_plane_data.hrl").
-
--export_type(
- [ t/0
- , label/0
- , anonymous/0
- , identified/0
- , parsing_error/0
- ]).
-
--export(
- [ of_bin/1 % Use default max index
- , of_bin/2 % Specify max index
- ]).
-
--type parsing_error() ::
- {block_structure_invalid, binary()}
- | {block_index_byte_out_of_range, anonymous()}
- .
-
--type anonymous() ::
- { non_neg_integer()
- , float()
- , float()
- , float()
- , float()
- , float()
- , float()
- , float()
- , float()
- }.
-
--type label() ::
- speeds
- | pitch_roll_heading
- | lat_lon_alt
- .
-
--type identified() ::
- #x_plane_datum_speeds{}
- | #x_plane_datum_pitch_roll_heading{}
- | #x_plane_datum_lat_lon_alt{}
- .
-
--type t() ::
- {non_neg_integer() , anonymous()}
- | {label() , identified()}
- .
-
--spec of_bin(binary()) ->
- hope_result:t(t(), parsing_error()).
-of_bin(<<Block/binary>>) ->
- of_bin(Block, ?DEFAULT_MAX_INDEX).
-
--spec of_bin(binary(), non_neg_integer()) ->
- hope_result:t(t(), parsing_error()).
-of_bin(<<Block/binary>>, MaxIndex) ->
- case anonymous_of_bin(Block, MaxIndex)
- of {ok, Anonymous} ->
- IdentifiedOrIndexed = identify_or_index(Anonymous),
- {ok, IdentifiedOrIndexed}
- ; {error, _}=Error ->
- Error
- end.
-
--spec anonymous_of_bin(binary(), non_neg_integer()) ->
- hope_result:t(anonymous(), parsing_error()).
-anonymous_of_bin(
- << Index:32/little-integer
- , V1:32/little-float
- , V2:32/little-float
- , V3:32/little-float
- , V4:32/little-float
- , V5:32/little-float
- , V6:32/little-float
- , V7:32/little-float
- , V8:32/little-float
- >>,
- MaxIndex
-) ->
- Anonymous = {Index, V1, V2, V3, V4, V5, V6, V7, V8},
- if Index > 0 andalso Index =< MaxIndex ->
- {ok, Anonymous}
- ; true ->
- {error, {block_index_byte_out_of_range, Anonymous}}
- end;
-anonymous_of_bin(<<Block/binary>>, _) ->
- % This case shouldn't be possible with a correct packet length, but we want
- % to allow for possibility of using this module independently of it's
- % parent, data module.
- {error, {block_structure_invalid, Block}}.
-
--spec identify_or_index(anonymous()) ->
- t().
-identify_or_index({3, V1, V2, V3, V4, _, V6, V7, V8}) ->
- Datum =
- #x_plane_datum_speeds
- { vind_kias = V1
- , vind_keas = V2
- , vtrue_ktas = V3
- , vtrue_ktgs = V4
-
- , vind_mph = V6
- , vtrue_mphas = V7
- , vtrue_mphgs = V8
- },
- {speeds, Datum};
-identify_or_index({17, V1, V2, V3, V4, _, _, _, _}) ->
- Datum =
- #x_plane_datum_pitch_roll_heading
- { pitch_deg = V1
- , roll_deg = V2
- , hding_true = V3
- , hding_mag = V4
- },
- {pitch_roll_heading, Datum};
-identify_or_index({20, V1, V2, V3, V4, V5, V6, V7, V8}) ->
- Datum =
- #x_plane_datum_lat_lon_alt
- { lat_deg = V1
- , lon_deg = V2
- , alt_ftmsl = V3
- , alt_ftagl = V4
- , on_runwy = V5
- , alt_ind = V6
- , lat_south = V7
- , lon_west = V8
- },
- {lat_lon_alt, Datum};
-identify_or_index({Index, _, _, _, _, _, _, _, _}=Anonymous) ->
- {Index, Anonymous}.
+++ /dev/null
--define(DEFAULT_MAX_INDEX, 133). % As of X-Plane 10.36r1
-module(x_plane_data_SUITE).
--include_lib("x_plane_data.hrl").
-
%% CT callbacks
-export(
[ all/0
Test =
fun (PacketBase64) ->
Packet = base64:decode(PacketBase64),
- MaxIndex = 133,
- BadIndex = MaxIndex + 1,
- FakeBlockData = list_to_binary(lists:seq(1, 32)),
- FakeBlockOk = <<MaxIndex:32/little-integer, FakeBlockData/binary>>,
- FakeBlockBadIndex = <<BadIndex:32/little-integer, FakeBlockData/binary>>,
- {error, {block_index_byte_out_of_range, {BadIndex,_,_,_,_,_,_,_,_}}} =
- x_plane_data:of_bin(<<Packet/binary, FakeBlockBadIndex/binary>>),
- {error, packet_unrecognized} =
- x_plane_data:of_bin(<<"bad-header", Packet/binary>>),
- {error, packet_length_invalid} =
- x_plane_data:of_bin(<<Packet/binary, "extra-stuff">>),
- {ok, Data} =
- x_plane_data:of_bin(<<Packet/binary, FakeBlockOk/binary>>),
- {some, #x_plane_datum_speeds{}} =
- hope_kv_list:get(Data, speeds),
- {some, #x_plane_datum_pitch_roll_heading{}} =
- hope_kv_list:get(Data, pitch_roll_heading),
- {some, #x_plane_datum_lat_lon_alt{}} =
- hope_kv_list:get(Data, lat_lon_alt),
- {some, {MaxIndex,_,_,_,_,_,_,_,_}} =
- hope_kv_list:get(Data, MaxIndex),
+ {error, packet_bad_header} =
+ x_plane_data_raw:of_bin(<<"bad-header", Packet/binary>>),
+ {error, packet_bad_length} =
+ x_plane_data_raw:of_bin(<<Packet/binary, "extra-stuff">>),
+ {ok, {_, Groups}} =
+ x_plane_data_raw:of_bin(<<Packet/binary>>),
+ {some, {_, _, _, _, _, _, _, _}} = kv_list_find(Groups, 3),
+ {some, {_, _, _, _, _, _, _, _}} = kv_list_find(Groups, 17),
+ {some, {_, _, _, _, _, _, _, _}} = kv_list_find(Groups, 20),
ok
end,
lists:foreach(Test, sample_packets_base64_encoded()).
-
-
%% =============================================================================
%% Sample data
%% =============================================================================
sample_packets_base64_encoded() ->
[ <<"REFUQUADAAAAbcpGQLt81EBfZNlATnUoNwDAecSow2RAnCv6QLrbQTcRAAAA3i8VQFL3ZT6dPfFCx4IFQwDAecQAwHnEAMB5xADAecQUAAAA1ZciQg6ik8JGBv9AdDxoPgAAgD9G/o3CAAAgQgAAlsI=">>
].
+
+
+%% =============================================================================
+%% Helpers
+%% =============================================================================
+
+kv_list_find(KVL, K) ->
+ case lists:keyfind(K, 1, KVL)
+ of false -> none
+ ; {K, V} -> {some, V}
+ end.