Implement UDP data parsing.
authorSiraaj Khandkar <siraaj@khandkar.net>
Thu, 30 Jul 2015 05:13:40 +0000 (01:13 -0400)
committerSiraaj Khandkar <siraaj@khandkar.net>
Thu, 30 Jul 2015 05:56:01 +0000 (01:56 -0400)
.travis.yml [new file with mode: 0644]
Makefile [new file with mode: 0644]
include/x_plane_data.hrl [new file with mode: 0644]
rebar.config [new file with mode: 0644]
src/x_plane_data.app.src [new file with mode: 0644]
src/x_plane_data.erl [new file with mode: 0644]
src/x_plane_datum.erl [new file with mode: 0644]
src/x_plane_datum_defaults.hrl [new file with mode: 0644]
test/x_plane_data_SUITE.erl [new file with mode: 0644]

diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..c38a8fb
--- /dev/null
@@ -0,0 +1,13 @@
+notifications:
+    recipients:
+    - siraaj@khandkar.net
+
+language:
+    erlang
+
+otp_release:
+    - 17.0
+    - R16B02
+
+script:
+    "make travis_ci"
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..3be17b7
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,48 @@
+.PHONY: \
+       travis_ci \
+       fresh-build \
+       compile \
+       clean \
+       deps \
+       deps-get \
+       deps-update \
+       dialyze \
+       test
+
+all: \
+       clean \
+       deps \
+       compile \
+       test \
+       dialyze
+
+travis_ci: \
+       deps \
+       compile \
+       test
+
+fresh-build: \
+       clean \
+       compile
+
+compile:
+       @rebar compile
+
+clean:
+       @rebar clean
+
+deps: \
+       deps-get \
+       deps-update
+
+deps-get:
+       @rebar get-deps
+
+deps-update:
+       @rebar update-deps
+
+dialyze:
+       @dialyzer ebin deps/hope/ebin
+
+test:
+       @rebar ct skip_deps=true --verbose=0
diff --git a/include/x_plane_data.hrl b/include/x_plane_data.hrl
new file mode 100644 (file)
index 0000000..e922992
--- /dev/null
@@ -0,0 +1,32 @@
+-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
+    , lat_west  :: float() % 8
+    }).
diff --git a/rebar.config b/rebar.config
new file mode 100644 (file)
index 0000000..59dea81
--- /dev/null
@@ -0,0 +1,9 @@
+%%% vim: set filetype=erlang:
+{ deps
+, [ {hope , ".*" , {git , "https://github.com/ibnfirnas/hope.git" , {tag , "3.7.0"}}}
+  ]
+}.
+
+{cover_enabled, true}.
+
+{clean_files, ["test/*.beam"]}.
diff --git a/src/x_plane_data.app.src b/src/x_plane_data.app.src
new file mode 100644 (file)
index 0000000..c4e86a9
--- /dev/null
@@ -0,0 +1,12 @@
+{application, x_plane_data,
+ [
+  {description, "X-Plane UDP data packet parser."},
+  {vsn, "0.0.0"},
+  {registered, []},
+  {applications, [
+                  kernel,
+                  stdlib,
+                  hope
+                 ]},
+  {env, []}
+ ]}.
diff --git a/src/x_plane_data.erl b/src/x_plane_data.erl
new file mode 100644 (file)
index 0000000..207673d
--- /dev/null
@@ -0,0 +1,49 @@
+-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 verion.
+    % 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)].
diff --git a/src/x_plane_datum.erl b/src/x_plane_datum.erl
new file mode 100644 (file)
index 0000000..cd55c2f
--- /dev/null
@@ -0,0 +1,134 @@
+-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 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
+        , lat_west  = V8
+        },
+    {lat_lon_alt, Datum};
+identify_or_index({Index, _, _, _, _, _, _, _, _}=Anonymous) ->
+    {Index, Anonymous}.
diff --git a/src/x_plane_datum_defaults.hrl b/src/x_plane_datum_defaults.hrl
new file mode 100644 (file)
index 0000000..9168e48
--- /dev/null
@@ -0,0 +1 @@
+-define(DEFAULT_MAX_INDEX, 133). % As of X-Plane 10.36r1
diff --git a/test/x_plane_data_SUITE.erl b/test/x_plane_data_SUITE.erl
new file mode 100644 (file)
index 0000000..3c69676
--- /dev/null
@@ -0,0 +1,76 @@
+-module(x_plane_data_SUITE).
+
+-include_lib("x_plane_data.hrl").
+
+%% CT callbacks
+-export(
+    [ all/0
+    , groups/0
+    ]).
+
+%% Test cases
+-export(
+    [ t_basic_sanity_check/1
+    ]).
+
+-define(GROUP, x_plane_data).
+
+%% ============================================================================
+%% CT callbacks
+%% ============================================================================
+
+all() ->
+    [ {group, ?GROUP}
+    ].
+
+groups() ->
+    Tests =
+        [ t_basic_sanity_check
+        ],
+    Properties = [parallel],
+    [ {?GROUP, Properties, Tests}
+    ].
+
+
+%% =============================================================================
+%%  Test cases
+%% =============================================================================
+
+t_basic_sanity_check(_Cfg) ->
+    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),
+            ok
+        end,
+    lists:foreach(Test, sample_packets_base64_encoded()).
+
+
+
+%% =============================================================================
+%% Sample data
+%% =============================================================================
+
+sample_packets_base64_encoded() ->
+        [ <<"REFUQUADAAAAbcpGQLt81EBfZNlATnUoNwDAecSow2RAnCv6QLrbQTcRAAAA3i8VQFL3ZT6dPfFCx4IFQwDAecQAwHnEAMB5xADAecQUAAAA1ZciQg6ik8JGBv9AdDxoPgAAgD9G/o3CAAAgQgAAlsI=">>
+        ].
This page took 0.046183 seconds and 4 git commands to generate.