The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package UAV::Pilot::ARDrone::NavPacket;
use v5.14;
use Moose;
use namespace::autoclean;

use UAV::Pilot::Exceptions;

use constant { # Values used as Option IDs
    NAVDATA_DEMO           => 0,
    NAVDATA_TIME           => 1,
    NAVDATA_RAW_MEASURES   => 2,
    NAVDATA_PHYS_MEASURES  => 3,
    NAVDATA_GYROS_OFFSETS  => 4,
    NAVDATA_EULER_ANGLES   => 5,
    NAVDATA_REFERNCES      => 6,
    NAVDATA_TRIMS          => 7,
    NAVDATA_RC_REFERENCES  => 8,
    NAVDATA_PWM            => 9,
    NAVDATA_ALTITUDE       => 10,
    NAVDATA_VISION_RAW     => 11,
    NAVDATA_VISION_OF      => 12,
    NAVDATA_VISION         => 13,
    NAVDATA_VISION_PERF    => 14,
    NAVDATA_TRACKERS_SEND  => 15,
    NAVDATA_VISION_DETECT  => 16,
    NAVDATA_WATCHDOG       => 17,
    NAVDATA_ADC_DATA_FRAME => 18,
    NAVDATA_VIDEO_STREAM   => 19,
    NAVDATA_CKS            => 0xffff,
};
use constant { # Bits for the drone state field
    NAVDATA_STATE_FLYING                        => 0,
    NAVDATA_STATE_VIDEO_ENABLED                 => 1,
    NAVDATA_STATE_VISION_ENABLED                => 2,
    NAVDATA_STATE_CONTROL_ALGORITHM             => 3,
    NAVDATA_STATE_ALTITUDE_CONTROL_ACTIVE       => 4,
    NAVDATA_STATE_USER_FEEDBACK_ON              => 5,
    NAVDATA_STATE_CONTROL_RECEIVED              => 6,
    NAVDATA_STATE_TRIM_RECEIVED                 => 7,
    NAVDATA_STATE_TRIM_RUNNING                  => 8,
    NAVDATA_STATE_TRIM_SUCCEEDED                => 9,
    NAVDATA_STATE_NAV_DATA_DEMO_ONLY            => 10,
    NAVDATA_STATE_NAV_DATA_BOOTSTRAP            => 11,
    NAVDATA_STATE_MOTORS_DOWN                   => 12,
    # 13 unknown (reserved for future use?)
    NAVDATA_STATE_GYROMETERS_DOWN               => 14,
    NAVDATA_STATE_BATTERY_TOO_LOW               => 15,
    NAVDATA_STATE_BATTERY_TOO_HIGH              => 16,
    NAVDATA_STATE_TIMER_ELAPSED                 => 17,
    NAVDATA_STATE_NOT_ENOUGH_POWER              => 18,
    NAVDATA_STATE_ANGELS_OUT_OF_RANGE           => 19,
    NAVDATA_STATE_TOO_MUCH_WIND                 => 20,
    NAVDATA_STATE_ULTRASONIC_SENSOR_DEAF        => 21,
    NAVDATA_STATE_CUTOUT_SYSTEM_DETECTED        => 22,
    NAVDATA_STATE_PIC_VERSION_OK                => 23,
    NAVDATA_STATE_AT_CODED_THREAD_ON            => 24,
    NAVDATA_STATE_NAV_DATA_THREAD_ON            => 25,
    NAVDATA_STATE_VIDEO_THREAD_ON               => 26,
    NAVDATA_STATE_ACQUISITION_THREAD_ON         => 27,
    NAVDATA_STATE_CONTROL_WATCHDOG_DELAYED      => 28,
    NAVDATA_STATE_ADC_WATCHDOG_DELAYED          => 29,
    NAVDATA_STATE_COMMUNICATION_PROBLEM_OCURRED => 30,
    NAVDATA_STATE_EMERGENCY                     => 31,
};
use constant EXPECT_HEADER_MAGIC_NUM => 0x55667788;

with 'UAV::Pilot::Logger';

has 'header' => (
    is  => 'ro',
    isa => 'Int',
);
has 'drone_state' => (
    is  => 'ro',
    isa => 'Int',
);
has 'sequence_num' => (
    is  => 'ro',
    isa => 'Int',
);
has 'vision_flag' => (
    is  => 'ro',
    isa => 'Int',
);
has 'state_flying' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_video_enabled' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_vision_enabled' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_control_algorithm' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_altitude_control_active' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_user_feedback_on' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_control_received' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_trim_received' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_trim_running' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_trim_succeeded' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_nav_data_demo_only' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_nav_data_bootstrap' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_motors_down' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_gyrometers_down' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_battery_too_low' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_battery_too_high' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_timer_elapsed' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_not_enough_power' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_angles_out_of_range' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_too_much_wind' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_ultrasonic_sensor_deaf' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_cutout_system_detected' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_pic_version_ok' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_at_codec_thread_on' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_nav_data_thread_on' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_video_thread_on' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_acquisition_thread_on' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_control_watchdog_delayed' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_adc_watchdog_delayed' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_communication_problem_occured' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'state_emergency' => (
    is  => 'ro',
    isa => 'Bool',
);
has 'checksum' => (
    is  => 'ro',
    isa => 'Int',
);
has 'control_state' => (
    is  => 'ro',
    isa => 'Maybe[Int]',
);
has 'battery_voltage_percentage' => (
    is  => 'ro',
    isa => 'Maybe[Int]',
);
has 'pitch' => (
    is  => 'ro',
    isa => 'Maybe[Num]',
);
has 'roll' => (
    is  => 'ro',
    isa => 'Maybe[Num]',
);
has 'yaw' => (
    is  => 'ro',
    isa => 'Maybe[Num]',
);
has 'altitude' => (
    is  => 'ro',
    isa => 'Maybe[Int]',
);
has 'velocity_x' => (
    is  => 'ro',
    isa => 'Maybe[Num]',
);
has 'velocity_y' => (
    is  => 'ro',
    isa => 'Maybe[Num]',
);
has 'velocity_z' => (
    is  => 'ro',
    isa => 'Maybe[Num]',
);
has 'video_frame_index' => (
    is  => 'ro',
    isa => 'Maybe[Int]',
);
has 'camera_detection_type' => (
    is  => 'ro',
    isa => 'Int',
);
has 'packet_bytes' => (
    is  => 'ro',
    isa => 'ArrayRef[Int]',
);


sub BUILDARGS
{
    my ($class, $args) = @_;
    my $packet = $args->{packet};

    my @packet_bytes = unpack "C*", $packet;
    my $header      = $class->_convert_endian_32bit( @packet_bytes[0..3]  );
    my $state       = $class->_convert_endian_32bit( @packet_bytes[4..7]  );
    my $seq         = $class->_convert_endian_32bit( @packet_bytes[8..11]  );
    my $vision_flag = $class->_convert_endian_32bit( @packet_bytes[12..15] );
    UAV::Pilot::NavPacketException::BadHeader->throw(
        error      => "Header '$header' did not match " . $class->EXPECT_HEADER_MAGIC_NUM,
        got_header => $header,
    ) if $class->EXPECT_HEADER_MAGIC_NUM != $header;

    my @option_bytes = @packet_bytes[16..$#packet_bytes];

    my %new_args = (
        header       => $header,
        drone_state  => $state,
        sequence_num => $seq,
        vision_flag  => $vision_flag,
        packet_bytes => \@packet_bytes,
        %{ $class->_parse_state( $state ) },
        %{ $class->_parse_options( @option_bytes ) },
    );
    return \%new_args;
}


sub to_string
{
    my ($self) = @_;
    my @strs = ();

    push @strs => "Battery: "  . $self->battery_voltage_percentage . '%';
    push @strs => "Pitch: "    . $self->pitch;
    push @strs => "Roll: "     . $self->roll;
    push @strs => "Yaw: "      . $self->yaw;
    push @strs => "Altitude: " . $self->altitude;

    return join ', ', @strs;
}

sub to_hex_string
{
    my ($self) = @_;
    my @packet_bytes = @{ $self->packet_bytes };
    return sprintf( ('%02x' x @packet_bytes), @packet_bytes );
}


sub _parse_state
{
    my ($class, $state) = @_;
    return {
        state_flying                        => ($state & 1),
        state_video_enabled                 => (($state >> 1) & 1),
        state_vision_enabled                => (($state >> 2) & 1),
        state_control_algorithm             => (($state >> 3) & 1),
        state_altitude_control_active       => (($state >> 4) & 1),
        state_user_feedback_on              => (($state >> 5) & 1),
        state_control_received              => (($state >> 6) & 1),
        state_trim_received                 => (($state >> 7) & 1),
        state_trim_running                  => (($state >> 8) & 1),
        state_trim_succeeded                => (($state >> 9) & 1),
        state_nav_data_demo_only            => (($state >> 10) & 1),
        state_nav_data_bootstrap            => (($state >> 11) & 1),
        state_motors_down                   => (($state >> 12) & 1),
        state_gyrometers_down               => (($state >> 13) & 1),
        state_battery_too_low               => (($state >> 14) & 1),
        state_battery_too_high              => (($state >> 15) & 1),
        state_timer_elapsed                 => (($state >> 16) & 1),
        state_not_enough_power              => (($state >> 17) & 1),
        # 18 unknown (reserved for future use?)
        state_angles_out_of_range           => (($state >> 19) & 1),
        state_too_much_wind                 => (($state >> 20) & 1),
        state_ultrasonic_sensor_deaf        => (($state >> 21) & 1),
        state_cutout_system_detected        => (($state >> 22) & 1),
        state_pic_version_ok                => (($state >> 23) & 1),
        state_at_codec_thread_on            => (($state >> 24) & 1),
        state_nav_data_thread_on            => (($state >> 25) & 1),
        state_video_thread_on               => (($state >> 26) & 1),
        state_acquisition_thread_on         => (($state >> 27) & 1),
        state_control_watchdog_delayed      => (($state >> 28) & 1),
        state_adc_watchdog_delayed          => (($state >> 29) & 1),
        state_communication_problem_occured => (($state >> 30) & 1),
        state_emergency                     => (($state >> 31) & 1),
    };
}

sub _parse_options
{
    my ($class, @bytes) = @_;
    my %args;

    while( @bytes ) {
        my $id = $class->_convert_endian_16bit( shift(@bytes), shift(@bytes) );
        my $size = $class->_convert_endian_16bit( shift(@bytes), shift(@bytes) );

        my @data = splice @bytes, 0, $size;

        %args = (
            %args,
            %{ $class->_parse_option( $id, @data ) },
        );
    }

    return \%args;
}

sub _parse_option
{
    my ($self, $id, @bytes) = @_;
    my $ret = 
        $self->NAVDATA_CKS  == $id ? $self->_parse_option_checksum( @bytes ) :
        $self->NAVDATA_DEMO == $id ? $self->_parse_option_demo( @bytes )     :
        {};
    return $ret;
}

sub _parse_option_checksum
{
    my ($self, @data) = @_;
    @data = reverse @data;

    my $data = 0;
    foreach (@data) {
        $data <<= 8;
        $data |= $_;
    }

    return { checksum => $data };
}

sub _parse_option_demo
{
    my ($self, @data) = @_;
    return {} unless scalar(@data) >= 64;
    my %args;

    $args{control_state}              = $self->_convert_endian_32bit( @data[0..3]   );
    $args{battery_voltage_percentage} = $self->_convert_endian_32bit( @data[4..7]   );
    $args{pitch}                      = $self->_to_float_32bit( @data[8..11]  );
    $args{roll}                       = $self->_to_float_32bit( @data[12..15] );
    $args{yaw}                        = $self->_to_float_32bit( @data[16..19] );
    $args{altitude}                   = $self->_convert_endian_32bit( @data[20..23] );
    $args{velocity_x}                 = $self->_to_float_32bit( @data[24..27] );
    $args{velocity_y}                 = $self->_to_float_32bit( @data[28..31] );
    $args{velocity_z}                 = $self->_to_float_32bit( @data[32..35] );
    $args{video_frame_index}          = $self->_convert_endian_32bit( @data[36..39] );
    # Bytes 40 - 47 are for deprecated parameters
    $args{camera_detection_type}      = $self->_convert_endian_32bit( @data[48..51] );
    # Bytes 52 - 63 are for deprecated parameters

    # Check for NaN on floats, converting to zero and issuing a warning
    foreach my $field (qw{ pitch roll yaw velocity_x velocity_y velocity_z }) {
        if( $args{$field} =~ /\A -? nan \z/ix ) {
            $self->_logger->warn( "Field '$field' was set to NaN"
                . ', converting to zero' );
            $args{$field} = 0;
        }
    }

    return \%args;
}

sub _convert_endian_32bit
{
    my ($class, @bytes) = @_;
    my $val = $bytes[0]
        | ($bytes[1] << 8)
        | ($bytes[2] << 16)
        | ($bytes[3] << 24);
    return $val;
}

sub _convert_endian_16bit
{
    my ($class, @bytes) = @_;
    my $val = $bytes[0] | ($bytes[1] << 8);
    return $val;
}

sub _to_float_32bit
{
    my ($self, @bytes) = @_;
    my $val = $bytes[3]
        | ($bytes[2] << 8)
        | ($bytes[1] << 16)
        | ($bytes[0] << 24);
    my $float = unpack( "f", pack( "l", $val ) );
    return $float;
}


no Moose;
__PACKAGE__->meta->make_immutable;
1;
__END__


=head1 NAME

  UAV::Pilot::ARDrone::NavPacket

=head1 SYNOPSIS

  my $nav = UAV::Pilot::ARDrone::NavPacket->new({
      packet => '...',
  });
  say "Roll: " . $nav->roll;
  say "Pitch: " . $nav->pitch;
  say "Yaw: " . $nav->yaw;

=head1 DESCRIPTION

Parses nav packets from the Parrot AR.Drone.  Specifically handles packets with the 
option for demo data, which provides basic roll/pitch/yaw/altitude, among a few other 
things.

Other nav packet types will be parsed in the future.

=cut