package Device::Firmata::Protocol;

=head1 NAME

Device::Firmata::Protocol - Firmata protocol implementation

=cut

use strict;
use warnings;
use vars qw/ $MIDI_DATA_SIZES /;

use constant {
  MIDI_COMMAND                => 0x80,
  MIDI_PARSE_NORMAL           => 0,
  MIDI_PARSE_SYSEX            => 1,
  MIDI_START_SYSEX            => 0xf0,
  MIDI_END_SYSEX              => 0xf7,
  MAX_PROTOCOL_VERSION        => 'V_2_05',  # highest Firmata protocol version currently implemented
};

use Device::Firmata::Constants qw/ :all /;
use Device::Firmata::Base
  ISA                         => 'Device::Firmata::Base',
  FIRMATA_ATTRIBS             => {
  buffer                      => [],
  parse_status                => MIDI_PARSE_NORMAL,
  protocol_version            => MAX_PROTOCOL_VERSION, # We are starting with the highest protocol
  };

$MIDI_DATA_SIZES = {
  0x80                        => 2,
  0x90                        => 2,
  0xA0                        => 2,
  0xB0                        => 2,
  0xC0                        => 1,
  0xD0                        => 1,
  0xE0                        => 2,
  0xF0                        => 0,    # note that this requires special handling

  # Special for version queries
  0xF4                        => 2,
  0xF9                        => 2,
  0x71                        => 0,
  0xFF                        => 0,
};

our $ONE_WIRE_COMMANDS = {
  SEARCH_REQUEST              => 0x40,
  CONFIG_REQUEST              => 0x41,
  SEARCH_REPLY                => 0x42,
  READ_REPLY                  => 0x43,
  SEARCH_ALARMS_REQUEST       => 0x44,
  SEARCH_ALARMS_REPLY         => 0x45,
  RESET_REQUEST_BIT           => 0x01,
  SKIP_REQUEST_BIT            => 0x02,
  SELECT_REQUEST_BIT          => 0x04,
  READ_REQUEST_BIT            => 0x08,
  DELAY_REQUEST_BIT           => 0x10,
  WRITE_REQUEST_BIT           => 0x20,
};

our $SCHEDULER_COMMANDS = {
  CREATE_FIRMATA_TASK         => 0,
  DELETE_FIRMATA_TASK         => 1,
  ADD_TO_FIRMATA_TASK         => 2,
  DELAY_FIRMATA_TASK          => 3,
  SCHEDULE_FIRMATA_TASK       => 4,
  QUERY_ALL_FIRMATA_TASKS     => 5,
  QUERY_FIRMATA_TASK          => 6,
  RESET_FIRMATA_TASKS         => 7,
  ERROR_TASK_REPLY            => 8,
  QUERY_ALL_TASKS_REPLY       => 9,
  QUERY_TASK_REPLY            => 10,
};

our $STEPPER_COMMANDS = {
  STEPPER_CONFIG              => 0,
  STEPPER_STEP                => 1,
};

our $STEPPER_INTERFACES = {
  DRIVER                      => 1,
  TWO_WIRE                    => 2,
  FOUR_WIRE                   => 4,
};

our $ENCODER_COMMANDS = {
  ENCODER_ATTACH              => 0,
  ENCODER_REPORT_POSITION     => 1,
  ENCODER_REPORT_POSITIONS    => 2,
  ENCODER_RESET_POSITION      => 3,
  ENCODER_REPORT_AUTO         => 4,
  ENCODER_DETACH              => 5,
};

our $SERIAL_COMMANDS = {
  SERIAL_CONFIG            => 0x10, # config serial port stetting such as baud rate and pins
  SERIAL_WRITE             => 0x20, # write to serial port
  SERIAL_READ              => 0x30, # read request to serial port
  SERIAL_REPLY             => 0x40, # read reply from serial port
  SERIAL_LISTEN            => 0x70, # start listening on software serial port
};

our $MODENAMES = {
  0                           => 'INPUT',
  1                           => 'OUTPUT',
  2                           => 'ANALOG',
  3                           => 'PWM',
  4                           => 'SERVO',
  5                           => 'SHIFT',
  6                           => 'I2C',
  7                           => 'ONEWIRE',
  8                           => 'STEPPER',
  9                           => 'ENCODER',
 10                           => 'SERIAL',
 11                           => 'PULLUP',
};

=head1 DESCRIPTION

Implementation of the Firmata 2.5 protocol specification.

Because we're dealing with a permutation of the
MIDI protocol, certain commands are one byte,
others 2 or even 3. We do this part to figure out
how many bytes we're actually looking at

One of the first things to know is that while
MIDI is packet based, the bytes have specialized
construction (where the top-most bit has been
reserved to differentiate if it's a command or a
data bit)

So any byte being transferred in a MIDI stream
will look like the following

 BIT# | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
 DATA | X | ? | ? | ? | ? | ? | ? | ? |

If X is a "1" this byte is considered a command byte
If X is a "0" this byte is considered a data bte

We figure out how many bytes a packet is by looking at the
command byte and of that byte, only the high nibble.
This nibble tells us the requisite information via a lookup
table...

See: http://www.midi.org/techspecs/midimessages.php
And
http://www.ccarh.org/courses/253/handout/midiprotocol/
For more information

Basically, however:

command
nibble  bytes
8       2
9       2
A       2
B       2
C       1
D       1
E       2
F       0 or variable

=head1 METHODS

=head2 message_data_receive

Receive a string of data. Normally, only one byte
is passed due to the code, but you can also pass as
many bytes in a string as you'd like.

=cut

sub message_data_receive {

  # --------------------------------------------------
  my ( $self, $data ) = @_;

  defined $data and length $data or return;

  my $protocol_version  = $self->{protocol_version};
  my $protocol_commands = $COMMANDS->{$protocol_version};
  my $protocol_lookup   = $COMMAND_LOOKUP->{$protocol_version};

  # Add the new data to the buffer
  my $buffer = $self->{buffer} ||= [];
  push @$buffer, unpack "C*", $data;

  my @packets;

  # Loop until we're finished parsing all available packets
  while (@$buffer) {
    # Not in SYSEX mode, we can proceed normally
    if (    $self->{parse_status} == MIDI_PARSE_NORMAL and $buffer->[0] == MIDI_START_SYSEX ) {
      my $command = shift @$buffer;
      push @packets, {
          command     => $command,
          command_str => $protocol_lookup->{$command} || 'START_SYSEX',
        };
      $self->{parse_status} = MIDI_PARSE_SYSEX;
      next;
    }
    # If in sysex mode, we will check for the end of the sysex message here
    elsif ( $self->{parse_status} == MIDI_PARSE_SYSEX and $buffer->[0] == MIDI_END_SYSEX ) {
      $self->{parse_status} = MIDI_PARSE_NORMAL;
      my $command = shift @$buffer;
      push @packets, {
          command     => $command,
          command_str => $protocol_lookup->{$command} || 'END_SYSEX',
        };
    }

# Regardless of the SYSEX mode we are in, we will allow commands to interrupt the flowthrough
    elsif ( $buffer->[0] & MIDI_COMMAND ) {
      my $command = $buffer->[0] & 0xf0;
      my $bytes = ( $MIDI_DATA_SIZES->{$command} || $MIDI_DATA_SIZES->{ $buffer->[0] } ) + 1;
      last if ( @$buffer < $bytes );
      my @data = splice @$buffer, 0, $bytes;
      $command = shift @data;
      push @packets,
        {
          command     => $command,
          command_str => $protocol_lookup->{$command}
            || $protocol_lookup->{ $command & 0xf0 }
            || 'UNKNOWN',
            data => \@data
        };
    }

# We have a data byte, if we're in SYSEX mode, we'll just add that to the data stream
# packet
    elsif ( $self->{parse_status} == MIDI_PARSE_SYSEX ) {
      my $data = shift @$buffer;
      if ( @packets and $packets[-1]{command_str} eq 'DATA_SYSEX' ) {
        push @{ $packets[-1]{data} }, $data;
      }
      else {
        push @packets,
          {
            command     => 0x0,
            command_str => 'DATA_SYSEX',
            data        => [$data]
          };
      }

    }

    # No idea what to do with this one, eject it and skip to the next
    else {
      shift @$buffer;
      last if ( not @$buffer );
    }
  }

  return if not @packets;
  return \@packets;
}

=head2 sysex_parse

Takes the sysex data buffer and parses it into
something useful

=cut

sub sysex_parse {

  # --------------------------------------------------
  my ( $self, $sysex_data ) = @_;

  my $protocol_version  = $self->{protocol_version};
  my $protocol_commands = $COMMANDS->{$protocol_version};
  my $protocol_lookup   = $COMMAND_LOOKUP->{$protocol_version};

  my $command = shift @$sysex_data;
  if ( defined $command ) {
    my $command_str = $protocol_lookup->{$command};

    if ($command_str) {
      my $return_data;

      COMMAND_HANDLER: {

        $command == $protocol_commands->{STRING_DATA} and do {
          $return_data = $self->handle_string_data($sysex_data);
          last;
        };

        $command == $protocol_commands->{REPORT_FIRMWARE} and do {
          $return_data = $self->handle_report_firmware($sysex_data);
          last;
        };

        $command == $protocol_commands->{CAPABILITY_RESPONSE} and do {
          $return_data = $self->handle_capability_response($sysex_data);
          last;
        };

        $command == $protocol_commands->{ANALOG_MAPPING_RESPONSE} and do {
          $return_data =
            $self->handle_analog_mapping_response($sysex_data);
          last;
        };

        $command == $protocol_commands->{PIN_STATE_RESPONSE} and do {
          $return_data = $self->handle_pin_state_response($sysex_data);
          last;
        };

        $command == $protocol_commands->{I2C_REPLY} and do {
          $return_data = $self->handle_i2c_reply($sysex_data);
          last;
        };

        $command == $protocol_commands->{ONEWIRE_DATA} and do {
          $return_data = $self->handle_onewire_reply($sysex_data);
          last;
        };

        $command == $protocol_commands->{SCHEDULER_DATA} and do {
          $return_data = $self->handle_scheduler_response($sysex_data);
          last;
        };

        $command == $protocol_commands->{STEPPER_DATA} and do {
          $return_data = $self->handle_stepper_response($sysex_data);
          last;
        };

        $command == $protocol_commands->{ENCODER_DATA} and do {
          $return_data = $self->handle_encoder_response($sysex_data);
          last;
        };

        $command == $protocol_commands->{SERIAL_DATA} and do {
          $return_data = $self->handle_serial_reply($sysex_data);
          last;
        };

        $command == $protocol_commands->{RESERVED_COMMAND} and do {
          $return_data = $sysex_data;
          last;
        };
      }

      return {
        command     => $command,
        command_str => $command_str,
        data        => $return_data
      };
    } else {
      return {
        command     => $command,
        data        => $sysex_data
      }
    }
  }
  return undef;
}

=head2 message_prepare

Using the midi protocol, create a binary packet
that can be transmitted to the serial output

=cut

sub message_prepare {

  # --------------------------------------------------
  my ( $self, $command_name, $channel, @data ) = @_;

  my $protocol_version  = $self->{protocol_version};
  my $protocol_commands = $COMMANDS->{$protocol_version};
  my $command           = $protocol_commands->{$command_name} or return;

  my $bytes = 1 +
    ( $MIDI_DATA_SIZES->{ $command & 0xf0 } || $MIDI_DATA_SIZES->{$command} );
  my $packet = pack "C" x $bytes, $command | $channel, @data;
  return $packet;
}

=head2 packet_sysex

create a binary packet containing a sysex-message

=cut

sub packet_sysex {

  my ( $self, @sysex_data ) = @_;

  my $protocol_version  = $self->{protocol_version};
  my $protocol_commands = $COMMANDS->{$protocol_version};
  
  my $bytes = @sysex_data + 2;
  my $packet = pack "C" x $bytes, $protocol_commands->{START_SYSEX},
    @sysex_data,
    $protocol_commands->{END_SYSEX};
  return $packet;
}

=head2 packet_sysex_command

create a binary packet containing a sysex-command

=cut

sub packet_sysex_command {

  my ( $self, $command_name, @data ) = @_;

  my $protocol_version  = $self->{protocol_version};
  my $protocol_commands = $COMMANDS->{$protocol_version};
  my $command           = $protocol_commands->{$command_name} or return;

#    my $bytes = 3+($MIDI_DATA_SIZES->{$command & 0xf0}||$MIDI_DATA_SIZES->{$command});
  my $bytes = @data + 3;
  my $packet = pack "C" x $bytes, $protocol_commands->{START_SYSEX},
    $command,
    @data,
    $protocol_commands->{END_SYSEX};
  return $packet;
}

=head2 packet_query_version

Craft a firmware version query packet to be sent

=cut

sub packet_query_version {
  my $self = shift;
  return $self->message_prepare( REPORT_VERSION => 0 );

}

sub handle_query_version_response {
  my ( $self, $data ) = @_;
  return {
      major_version => shift @$data,
      minor_version => shift @$data,
    };
}

sub handle_string_data {
  my ( $self, $sysex_data ) = @_;
  return { string => double_7bit_to_string($sysex_data) };
}

=head2 packet_query_firmware

Craft a firmware variant query packet to be sent

=cut

sub packet_query_firmware {
  my $self = shift;
  return $self->packet_sysex_command(REPORT_FIRMWARE);
}

sub handle_report_firmware {
  my ( $self, $sysex_data ) = @_;
  return {
      major_version => shift @$sysex_data,
      minor_version => shift @$sysex_data,
      firmware      => double_7bit_to_string($sysex_data)
    };
}

sub packet_query_capability {
  my $self = shift;
  return $self->packet_sysex_command(CAPABILITY_QUERY);
}

#/* capabilities response
# * -------------------------------
# * 0  START_SYSEX (0xF0) (MIDI System Exclusive)
# * 1  capabilities response (0x6C)
# * 2  1st mode supported of pin 0
# * 3  1st mode's resolution of pin 0
# * 4  2nd mode supported of pin 0
# * 5  2nd mode's resolution of pin 0
# ...   additional modes/resolutions, followed by a single 127 to mark the
#       end of the first pin's modes.  Each pin follows with its mode and
#       127, until all pins implemented.
# * N  END_SYSEX (0xF7)
# */

sub handle_capability_response {
  my ( $self, $sysex_data ) = @_;
  my %capabilities;
  my $byte = shift @$sysex_data;
  my $i=0;
  while ( defined $byte ) {
    my %pinmodes;
    while ( defined $byte && $byte != 127 ) {
      $pinmodes{$byte} = {
        mode_str   => $MODENAMES->{$byte},
        resolution => shift @$sysex_data    # /secondbyte
      };
      $byte = shift @$sysex_data;
    }
    $capabilities{$i}=\%pinmodes;
    $i++;
    $byte = shift @$sysex_data;
  }
  return { capabilities => \%capabilities };
}

sub packet_query_analog_mapping {
  my $self = shift;
  return $self->packet_sysex_command(ANALOG_MAPPING_QUERY);
}

#/* analog mapping response
# * -------------------------------
# * 0  START_SYSEX (0xF0) (MIDI System Exclusive)
# * 1  analog mapping response (0x6A)
# * 2  analog channel corresponding to pin 0, or 127 if pin 0 does not support analog
# * 3  analog channel corresponding to pin 1, or 127 if pin 1 does not support analog
# * 4  analog channel corresponding to pin 2, or 127 if pin 2 does not support analog
# ...   etc, one byte for each pin
# * N  END_SYSEX (0xF7)
# */

sub handle_analog_mapping_response {
  my ( $self, $sysex_data ) = @_;
  my %pins;
  my $pin_mapping = shift @$sysex_data;
  my $i=0;

  while ( defined $pin_mapping ) {
    $pins{$pin_mapping}=$i if ($pin_mapping!=127);
    $pin_mapping = shift @$sysex_data;
    $i++;
  }
  return { mappings => \%pins };
}

#/* pin state query
# * -------------------------------
# * 0  START_SYSEX (0xF0) (MIDI System Exclusive)
# * 1  pin state query (0x6D)
# * 2  pin (0 to 127)
# * 3  END_SYSEX (0xF7) (MIDI End of SysEx - EOX)
# */

sub packet_query_pin_state {
  my ( $self, $pin ) = @_;
  return $self->packet_sysex_command( PIN_STATE_QUERY, $pin );
}

#/* pin state response
# * -------------------------------
# * 0  START_SYSEX (0xF0) (MIDI System Exclusive)
# * 1  pin state response (0x6E)
# * 2  pin (0 to 127)
# * 3  pin mode (the currently configured mode)
# * 4  pin state, bits 0-6
# * 5  (optional) pin state, bits 7-13
# * 6  (optional) pin state, bits 14-20
# ...  additional optional bytes, as many as needed
# * N  END_SYSEX (0xF7)
# */

sub handle_pin_state_response {
  my ( $self, $sysex_data ) = @_;
  my $pin    = shift @$sysex_data;
  my $mode   = shift @$sysex_data;
  my $state  = shift @$sysex_data & 0x7f;
  my $nibble = shift @$sysex_data;
  for ( my $i = 1 ; defined $nibble ; $nibble = shift @$sysex_data ) {
    $state += ( $nibble & 0x7f ) << ( 7 * $i );
  }

  return {
    pin       => $pin,
    mode      => $mode,
    moden_str => $MODENAMES->{$mode},
    state     => $state
  };

}

sub packet_sampling_interval {
  my ( $self, $interval ) = @_;
  return $self->packet_sysex_command( SAMPLING_INTERVAL,
    $interval & 0x7f,
    $interval >> 7
  );
}

#/* I2C read/write request
# * -------------------------------
# * 0  START_SYSEX (0xF0) (MIDI System Exclusive)
# * 1  I2C_REQUEST (0x76)
# * 2  slave address (LSB)
# * 3  slave address (MSB) + read/write and address mode bits
#      {7: always 0} + {6: reserved} + {5: address mode, 1 means 10-bit mode} +
#      {4-3: read/write, 00 => write, 01 => read once, 10 => read continuously, 11 => stop reading} +
#      {2-0: slave address MSB in 10-bit mode, not used in 7-bit mode}
# * 4  data 0 (LSB)
# * 5  data 0 (MSB)
# * 6  data 1 (LSB)
# * 7  data 1 (MSB)
# * ...
# * n  END_SYSEX (0xF7)
# */

sub packet_i2c_request {
  my ( $self, $address, $command, @i2cdata ) = @_;
  if (($address & 0x380) > 0) {
    $command |= (0x20 | (($address >> 7) & 0x7));
  }

  if (scalar @i2cdata) {
    my @data;
    push_array_as_two_7bit(\@i2cdata,\@data);
    return $self->packet_sysex_command( I2C_REQUEST,
      $address & 0x7f,
      $command,
      @data,
    );
  } else {
    return $self->packet_sysex_command( I2C_REQUEST,
      $address & 0x7f,
      $command,
    );
  }
}

#/* I2C reply
# * -------------------------------
# * 0  START_SYSEX (0xF0) (MIDI System Exclusive)
# * 1  I2C_REPLY (0x77)
# * 2  slave address (LSB)
# * 3  slave address (MSB)
# * 4  register (LSB)
# * 5  register (MSB)
# * 6  data 0 LSB
# * 7  data 0 MSB
# * ...
# * n  END_SYSEX (0xF7)
# */

sub handle_i2c_reply {
  my ( $self, $sysex_data ) = @_;
  my $address = shift14bit($sysex_data);
  my $register = shift14bit($sysex_data);
  my @data = double_7bit_to_array($sysex_data);
  return {
    address       => $address,
    register      => $register,
    data          => \@data,
  };
}

#/* I2C config
# * -------------------------------
# * 0  START_SYSEX (0xF0) (MIDI System Exclusive)
# * 1  I2C_CONFIG (0x78)
# * 2  Delay in microseconds (LSB)
# * 3  Delay in microseconds (MSB)
# * ... user defined for special cases, etc
# * n  END_SYSEX (0xF7)
# */

sub packet_i2c_config {
  my ( $self, $delay, @data ) = @_;
  return $self->packet_sysex_command( I2C_CONFIG,
    $delay & 0x7f,
    $delay >> 7, @data
  );
}

#/* servo config
# * --------------------
# * 0  START_SYSEX (0xF0)
# * 1  SERVO_CONFIG (0x70)
# * 2  pin number (0-127)
# * 3  minPulse LSB (0-6)
# * 4  minPulse MSB (7-13)
# * 5  maxPulse LSB (0-6)
# * 6  maxPulse MSB (7-13)
# * 7  END_SYSEX (0xF7)
# */

sub packet_servo_config_request {
  my ( $self, $pin, $data ) = @_;
  my $min_pulse = $data->{min_pulse};
  my $max_pulse = $data->{max_pulse};

  return $self->packet_sysex_command( SERVO_CONFIG,
    $pin & 0x7f,
    $min_pulse & 0x7f,
    $min_pulse >> 7,
    $max_pulse & 0x7f,
    $max_pulse >> 7
  );
}

#This is just the standard SET_PIN_MODE message:

#/* set digital pin mode
# * --------------------
# * 1  set digital pin mode (0xF4) (MIDI Undefined)
# * 2  pin number (0-127)
# * 3  state (INPUT/OUTPUT/ANALOG/PWM/SERVO, 0/1/2/3/4)
# */

#Then the normal ANALOG_MESSAGE data format is used to send data.

#/* write to servo, servo write is performed if the pins mode is SERVO
# * ------------------------------
# * 0  ANALOG_MESSAGE (0xE0-0xEF)
# * 1  value lsb
# * 2  value msb
# */

sub packet_onewire_search_request {
  my ( $self, $pin ) = @_;
  return $self->packet_sysex_command( ONEWIRE_DATA,$ONE_WIRE_COMMANDS->{SEARCH_REQUEST},$pin);
};

sub packet_onewire_search_alarms_request {
  my ( $self, $pin ) = @_;
  return $self->packet_sysex_command( ONEWIRE_DATA,$ONE_WIRE_COMMANDS->{SEARCH_ALARMS_REQUEST},$pin);
};

sub packet_onewire_config_request {
  my ( $self, $pin, $power ) = @_;
  return $self->packet_sysex_command( ONEWIRE_DATA, $ONE_WIRE_COMMANDS->{CONFIG_REQUEST},$pin,
    ( defined $power ) ? $power : 1
  );
};

#$args = {
# reset => undef | 1,
# skip => undef | 1,
# select => undef | device,
# read => undef | short int,
# delay => undef | long int,
# write => undef | bytes[],
#}

sub packet_onewire_request {
  my ( $self, $pin, $args ) = @_;
  my $subcommand = 0;
  my @data;
  if (defined $args->{reset}) {
    $subcommand |= $ONE_WIRE_COMMANDS->{RESET_REQUEST_BIT};
  }
  if (defined $args->{skip}) {
    $subcommand |= $ONE_WIRE_COMMANDS->{SKIP_REQUEST_BIT};
  }
  if (defined $args->{select}) {
    $subcommand |= $ONE_WIRE_COMMANDS->{SELECT_REQUEST_BIT};
    push_onewire_device_to_byte_array($args->{select},\@data);
  }
  if (defined $args->{read}) {
    $subcommand |= $ONE_WIRE_COMMANDS->{READ_REQUEST_BIT};
    push @data,$args->{read} & 0xFF;
    push @data,($args->{read}>>8) & 0xFF;
    if ($self->{protocol_version} ne 'V_2_04') {
      my $id = (defined $args->{id}) ? $args->{id} : 0;
      push @data,$id &0xFF;
      push @data,($id>>8) & 0xFF;
    }
  }
  if (defined $args->{delay}) {
    $subcommand |= $ONE_WIRE_COMMANDS->{DELAY_REQUEST_BIT};
    push @data,$args->{delay} & 0xFF;
    push @data,($args->{delay}>>8) & 0xFF;
    push @data,($args->{delay}>>16) & 0xFF;
    push @data,($args->{delay}>>24) & 0xFF;
  }
  if (defined $args->{write}) {
    $subcommand |= $ONE_WIRE_COMMANDS->{WRITE_REQUEST_BIT};
    my $writeBytes=$args->{write};
    push @data,@$writeBytes;
  }
  return $self->packet_sysex_command( ONEWIRE_DATA, $subcommand, $pin, pack_as_7bit(@data));
};

sub handle_onewire_reply {
  my ( $self, $sysex_data ) = @_;
  my $command = shift @$sysex_data;
  my $pin     = shift @$sysex_data;

  if ( defined $command ) {
    COMMAND_HANDLER: {
      $command == $ONE_WIRE_COMMANDS->{READ_REPLY} and do {    #PIN,COMMAND,ADDRESS,DATA
        my @data = unpack_from_7bit(@$sysex_data);
        if ($self->{protocol_version} eq 'V_2_04') {
          my $device = shift_onewire_device_from_byte_array(\@data);
          return {
            pin     => $pin,
            command => 'READ_REPLY',
            device  => $device,
            data    => \@data
          };
        } else {
          my $id = shift @data;
          $id += (shift @data)<<8;
          return {
            pin     => $pin,
            command => 'READ_REPLY',
            id      => $id,
            data    => \@data
          };
        };
      };

      ($command == $ONE_WIRE_COMMANDS->{SEARCH_REPLY} or $command == $ONE_WIRE_COMMANDS->{SEARCH_ALARMS_REPLY}) and do {    #PIN,COMMAND,ADDRESS...
        my @devices;
        my @data = unpack_from_7bit(@$sysex_data);
        my $device = shift_onewire_device_from_byte_array(\@data);
        while ( defined $device ) {
          push @devices, $device;
          $device = shift_onewire_device_from_byte_array(\@data);
        }
        return {
          pin     => $pin,
          command => $command == $ONE_WIRE_COMMANDS->{SEARCH_REPLY} ? 'SEARCH_REPLY' : 'SEARCH_ALARMS_REPLY',
          devices => \@devices,
        };
      };
    }
  }
}

sub packet_create_task {
  my ($self,$id,$len) = @_;
  my $packet = $self->packet_sysex_command('SCHEDULER_DATA', $SCHEDULER_COMMANDS->{CREATE_FIRMATA_TASK}, $id, $len & 0x7F, $len>>7);
  return $packet;
}

sub packet_delete_task {
  my ($self,$id) = @_;
  return $self->packet_sysex_command('SCHEDULER_DATA', $SCHEDULER_COMMANDS->{DELETE_FIRMATA_TASK}, $id);
}

sub packet_add_to_task {
  my ($self,$id,@data) = @_;
  my $packet = $self->packet_sysex_command('SCHEDULER_DATA', $SCHEDULER_COMMANDS->{ADD_TO_FIRMATA_TASK}, $id, pack_as_7bit(@data));
  return $packet;
}

sub packet_delay_task {
  my ($self,$time_ms) = @_;
  my $packet = $self->packet_sysex_command('SCHEDULER_DATA', $SCHEDULER_COMMANDS->{DELAY_FIRMATA_TASK}, pack_as_7bit($time_ms & 0xFF, ($time_ms & 0xFF00)>>8, ($time_ms & 0xFF0000)>>16,($time_ms & 0xFF000000)>>24));
  return $packet;
}

sub packet_schedule_task {
  my ($self,$id,$time_ms) = @_;
  my $packet = $self->packet_sysex_command('SCHEDULER_DATA', $SCHEDULER_COMMANDS->{SCHEDULE_FIRMATA_TASK}, $id, pack_as_7bit($time_ms & 0xFF, ($time_ms & 0xFF00)>>8, ($time_ms & 0xFF0000)>>16,($time_ms & 0xFF000000)>>24));
  return $packet;
}

sub packet_query_all_tasks {
  my $self = shift;
  return $self->packet_sysex_command('SCHEDULER_DATA', $SCHEDULER_COMMANDS->{QUERY_ALL_FIRMATA_TASKS});
}

sub packet_query_task {
  my ($self,$id) = @_;
  return $self->packet_sysex_command('SCHEDULER_DATA', $SCHEDULER_COMMANDS->{QUERY_FIRMATA_TASK},$id);
}

sub packet_reset_scheduler {
  my $self = shift;
  return $self->packet_sysex_command('SCHEDULER_DATA', $SCHEDULER_COMMANDS->{RESET_FIRMATA_TASKS});
}

sub handle_scheduler_response {
  my ( $self, $sysex_data ) = @_;
  my $command = shift @$sysex_data;

  if ( defined $command ) {
    COMMAND_HANDLER: {
      $command == $SCHEDULER_COMMANDS->{QUERY_ALL_TASKS_REPLY} and do {
        return {
          command => 'QUERY_ALL_TASKS_REPLY',
          ids => $sysex_data,
        }
      };

      ($command == $SCHEDULER_COMMANDS->{QUERY_TASK_REPLY} or $command == $SCHEDULER_COMMANDS->{ERROR_TASK_REPLY}) and do {
        my $error = ($command == $SCHEDULER_COMMANDS->{ERROR_TASK_REPLY});
        if (scalar @$sysex_data == 1) {
          return {
            command => ($error ? 'ERROR_TASK_REPLY' : 'QUERY_TASK_REPLY'),
            id => shift @$sysex_data,
          }
        }
        if (scalar @$sysex_data >= 11) {
          my $id = shift @$sysex_data;
          my @data = unpack_from_7bit(@$sysex_data);
          return {
            command => ($error ? 'ERROR_TASK_REPLY' : 'QUERY_TASK_REPLY'),
            id => $id,
            time_ms => shift @data | (shift @data)<<8 | (shift @data)<<16 | (shift @data)<<24,
            len => shift @data  | (shift @data)<<8,
            position => shift @data  | (shift @data)<<8,
            messages => \@data,
          }
        }
      };
    }
  }
}

#	stepper_data 0
#	stepper_config 1
#	devicenum 2 (0 < devicenum < 6)
#	interface (DRIVER | TWO_WIRE | FOUR_WIRE) 3
#	stepsPerRev 4+5 (14bit)
#	directionPin 6
#	stepPin 7
#	motorPin3 8 (interface FOUR_WIRE only)
#	motorPin4 9 (interface FOUR_WIRE only)

sub packet_stepper_config {
  my ( $self, $stepperNum, $interface, $stepsPerRev, $directionPin, $stepPin, $motorPin3, $motorPin4 ) = @_;
  
  die "invalid stepper interface ".$interface unless defined ($STEPPER_INTERFACES->{$interface});
  my @configdata = ($stepperNum,$STEPPER_INTERFACES->{$interface});
  
  push_value_as_two_7bit($stepsPerRev, \@configdata);
  push @configdata, $directionPin;
  push @configdata, $stepPin;
  
  if ($interface eq 'FOUR_WIRE') {
    push @configdata, $motorPin3;
    push @configdata, $motorPin4;
  }
  my $packet = $self->packet_sysex_command('STEPPER_DATA',$STEPPER_COMMANDS->{STEPPER_CONFIG},@configdata);
  return $packet;
}

#	stepper_data 0
#	stepper_step 1
#	devicenum 2
#	stepDirection 3 0/>0
#	numSteps 4,5,6 (21bit)
#	stepSpeed 7,8 (14bit)
#	accel 9,10 (14bit, optional, aber nur zusammen mit decel)
#	decel 11,12 (14bit, optional, aber nur zusammen mit accel)

sub packet_stepper_step {
  my ( $self, $stepperNum, $direction, $numSteps, $stepSpeed, $accel, $decel ) = @_;
  my @stepdata = ($stepperNum, $direction);
  push @stepdata, $numSteps & 0x7f;
  push @stepdata, ($numSteps >> 7) & 0x7f;
  push @stepdata, ($numSteps >> 14) & 0x7f;
  push_value_as_two_7bit($stepSpeed, \@stepdata);
  if (defined $accel and defined $decel) {
    push_value_as_two_7bit($accel, \@stepdata);
    push_value_as_two_7bit($decel, \@stepdata);
  }
  my $packet = $self->packet_sysex_command('STEPPER_DATA', $STEPPER_COMMANDS->{STEPPER_STEP},@stepdata);
  return $packet;
}

sub handle_stepper_response {
  my ( $self, $sysex_data ) = @_;

  my $stepperNum = shift @$sysex_data;
  return {
    stepperNum => $stepperNum,
  };
}


sub packet_encoder_attach {
  my ( $self,$encoderNum, $pinA, $pinB ) = @_;
  my $packet = $self->packet_sysex_command('ENCODER_DATA', $ENCODER_COMMANDS->{ENCODER_ATTACH}, $encoderNum, $pinA, $pinB);
  return $packet;
}

sub packet_encoder_report_position {
  my ( $self,$encoderNum ) = @_;
  my $packet = $self->packet_sysex_command('ENCODER_DATA', $ENCODER_COMMANDS->{ENCODER_REPORT_POSITION}, $encoderNum);
  return $packet;
}

sub packet_encoder_report_positions {
  my ( $self ) = @_;
  my $packet = $self->packet_sysex_command('ENCODER_DATA', $ENCODER_COMMANDS->{ENCODER_REPORT_POSITIONS});
  return $packet;
}

sub packet_encoder_reset_position {
  my ( $self,$encoderNum ) = @_;
  my $packet = $self->packet_sysex_command('ENCODER_DATA', $ENCODER_COMMANDS->{ENCODER_RESET_POSITION}, $encoderNum);
  return $packet;
}

sub packet_encoder_report_auto {
  my ( $self,$arg ) = @_;
  my $packet = $self->packet_sysex_command('ENCODER_DATA', $ENCODER_COMMANDS->{ENCODER_REPORT_AUTO}, $arg);
  return $packet;
}

sub packet_encoder_detach {
  my ( $self,$encoderNum ) = @_;
  my $packet = $self->packet_sysex_command('ENCODER_DATA', $ENCODER_COMMANDS->{ENCODER_DETACH}, $encoderNum);
  return $packet;
}

sub handle_encoder_response {
  my ( $self, $sysex_data ) = @_;
  
  my @retval = ();
  
  while (@$sysex_data) {
    
    my $command = shift @$sysex_data;
    my $direction = ($command & 0x40) >> 6;
    my $encoderNum = $command & 0x3f;
    my $value = shift14bit($sysex_data) + (shift14bit($sysex_data) << 14);
    
    push @retval,{
      encoderNum => $encoderNum,
      value => $direction ? -1 * $value : $value,
    };
  };
  
  return \@retval;
}

#/* serial config
# * -------------------------------
# * 0  START_SYSEX      (0xF0)
# * 1  SERIAL_DATA      (0x60)  // command byte
# * 2  SERIAL_CONFIG    (0x10)  // OR with port (0x11 = SERIAL_CONFIG | HW_SERIAL1)
# * 3  baud             (bits 0 - 6)
# * 4  baud             (bits 7 - 13)
# * 5  baud             (bits 14 - 20) // need to send 3 bytes for baud even if value is < 14 bits
# * 6  rxPin            (0-127) [optional] // only set if platform requires RX pin number
# * 7  txPin            (0-127) [optional] // only set if platform requires TX pin number
# * 6|8 END_SYSEX       (0xF7)
# */

sub packet_serial_config {
  my ( $self, $port, $baud, $rxPin, $txPin ) = @_;
  if (defined($rxPin) && defined($txPin)) {
    return $self->packet_sysex_command( SERIAL_DATA,
      $SERIAL_COMMANDS->{SERIAL_CONFIG} | $port,
      $baud & 0x7f,
      ($baud >> 7) & 0x7f,
      ($baud >> 14) & 0x7f,
      $rxPin & 0x7f,
      $txPin & 0x7f
    );
  } else {  
    return $self->packet_sysex_command( SERIAL_DATA,
      $SERIAL_COMMANDS->{SERIAL_CONFIG} | $port,
      $baud & 0x7f,
      ($baud >> 7) & 0x7f,
      ($baud >> 14) & 0x7f
    );
  }
}

#/* serial listen
# * -------------------------------
# * 0  START_SYSEX      (0xF0)
# * 1  SERIAL_DATA      (0x60)  // command byte
# * 2  SERIAL_LISTEN    (0x70)  // OR with port to switch to (0x79 = switch to SW_SERIAL1)
# * 3  END_SYSEX        (0xF7)
# */

sub packet_serial_listen {
  my ( $self, $port ) = @_;
  return $self->packet_sysex_command( SERIAL_DATA,
    $SERIAL_COMMANDS->{SERIAL_LISTEN} | $port
  );
}

#/* serial write
# * -------------------------------
# * 0  START_SYSEX      (0xF0)
# * 1  SERIAL_DATA      (0x60)
# * 2  SERIAL_WRITE     (0x20) // OR with port (0x21 = SERIAL_WRITE | HW_SERIAL1)
# * 3  data 0           (LSB)
# * 4  data 0           (MSB)
# * 5  data 1           (LSB)
# * 6  data 1           (MSB)
# * ...                 // up to max buffer - 5
# * n  END_SYSEX        (0xF7)
# */

sub packet_serial_write {
  my ( $self, $port, @serialdata ) = @_;
  
  if (scalar @serialdata) {
    my @data;
    push_array_as_two_7bit(\@serialdata,\@data);
    return $self->packet_sysex_command( SERIAL_DATA,
      $SERIAL_COMMANDS->{SERIAL_WRITE} | $port,
      @data
    );
  } else {
    return $self->packet_sysex_command( SERIAL_DATA,
      $SERIAL_COMMANDS->{SERIAL_WRITE} | $port
    );
  }
}

#/* serial read
# * -------------------------------
# * 0  START_SYSEX        (0xF0)
# * 1  SERIAL_DATA        (0x60)
# * 2  SERIAL_READ        (0x30) // OR with port (0x31 = SERIAL_READ | HW_SERIAL1)
# * 3  SERIAL_READ_MODE   (0x00) // 0x00 => read continuously, 0x01 => stop reading
# * 4  maxBytesToRead     (lsb)  // 0x00 for all bytes available [optional]
# * 5  maxBytesToRead     (msb)  // 0x00 for all bytes available [optional]
# * 4|6 END_SYSEX         (0xF7)
# */

sub packet_serial_read {
  my ( $self, $port, $command, $maxBytes ) = @_;
  
  if ($maxBytes > 0) { 
    return $self->packet_sysex_command( SERIAL_DATA,
      $SERIAL_COMMANDS->{SERIAL_READ} | $port,
      $command,
      $maxBytes & 0x7f,
      ($maxBytes >> 7) & 0x7f
    );
  } else {
    return $self->packet_sysex_command( SERIAL_DATA,
      $SERIAL_COMMANDS->{SERIAL_READ} | $port,
      $command
    );
  }
}

#/* serial reply
# * -------------------------------
# * 0  START_SYSEX        (0xF0)
# * 1  SERIAL_DATA        (0x60)
# * 2  SERIAL_REPLY       (0x40) // OR with port (0x41 = SERIAL_REPLY | HW_SERIAL1)
# * 3  data 0             (LSB)
# * 4  data 0             (MSB)
# * 3  data 1             (LSB)
# * 4  data 1             (MSB)
# * ...                   // up to max buffer - 5
# * n  END_SYSEX          (0xF7)
# */

sub handle_serial_reply {
  my ( $self, $sysex_data ) = @_;
  
  my $command = shift @$sysex_data;
  my $port = $command & 0xF;
  my @data = double_7bit_to_array($sysex_data);
  return {
    port => $port,
    data => \@data,
  };
}

sub shift14bit {
  my $data = shift;
  my $lsb  = shift @$data;
  my $msb  = shift @$data;
  return
      defined $lsb
    ? defined $msb
      ? ( $msb << 7 ) + ( $lsb & 0x7f )
      : $lsb
    : undef;
}

sub double_7bit_to_string {
  my ( $data, $numbytes ) = @_;
  my $ret;
  if ( defined $numbytes ) {
    for ( my $i = 0 ; $i < $numbytes ; $i++ ) {
      my $value = shift14bit($data);
      $ret .= chr($value);
    }
  }
  else {
    while (@$data) {
      my $value = shift14bit($data);
      $ret .= chr($value);
    }
  }
  return $ret;
}

sub double_7bit_to_array {
  my ( $data, $numbytes ) = @_;
  my @ret;
  if ( defined $numbytes ) {
    for ( my $i = 0 ; $i < $numbytes ; $i++ ) {
      push @ret, shift14bit($data);
    }
  }
  else {
    while (@$data) {
      my $value = shift14bit($data);
      push @ret, $value;
    }
  }
  return @ret;
}

sub shift_onewire_device_from_byte_array {
  my $buffer = shift;
  my $family = shift @$buffer;
  if ( defined $family ) {
    my @address;
    for (my $i=0;$i<6;$i++) { push @address,shift @$buffer; }
    my $crc = shift @$buffer;
    return {
      family   => $family,
      identity => \@address,
      crc      => $crc
    };
  }
  else {
    return undef;
  }
}

sub push_value_as_two_7bit {
  my ( $value, $buffer ) = @_;
  push @$buffer, $value & 0x7f;    #LSB
  push @$buffer, ( $value >> 7 ) & 0x7f;    #MSB
}

sub push_onewire_device_to_byte_array {
  my ( $device, $buffer ) = @_;
  push @$buffer, $device->{family};
  for ( my $i = 0 ; $i < 6 ; $i++ ) { push @$buffer, $device->{identity}[$i]; }
  push @$buffer, $device->{crc};
}

sub push_array_as_two_7bit {
  my ( $data, $buffer ) = @_;
  my $byte = shift @$data;
  while ( defined $byte ) {
    push_value_as_two_7bit( $byte, $buffer );
    $byte = shift @$data;
  }
}

sub pack_as_7bit {
  my @data = @_;
  my @outdata;
  my $numBytes    = @data;
  my $messageSize = ( $numBytes << 3 ) / 7;
  for ( my $i = 0 ; $i < $messageSize ; $i++ ) {
    my $j     = $i * 7;
    my $pos   = $j >> 3;
    my $shift = $j & 7;
    my $out   = $data[$pos] >> $shift & 0x7F;
    printf "%b, %b, %d\n",$data[$pos],$out,$shift if ($out >> 7 > 0);
    $out |= ( $data[ $pos + 1 ] << ( 8 - $shift ) ) & 0x7F if ( $shift > 1 && $pos < $numBytes-1 );
    push( @outdata, $out );
  }
  return @outdata;
}

sub unpack_from_7bit {
  my @data = @_;
  my @outdata;
  my $numBytes = @data;
  my $outBytes = ( $numBytes * 7 ) >> 3;
  for ( my $i = 0 ; $i < $outBytes ; $i++ ) {
    my $j     = $i << 3;
    my $pos   = $j / 7;
    my $shift = $j % 7;
    push( @outdata,
      ( $data[$pos] >> $shift ) |
        ( ( $data[ $pos + 1 ] << ( 7 - $shift ) ) & 0xFF ) );
  }
  return @outdata;
}

=head2 get_max_compatible_protocol_version

Search list of implemented protocols for identical or next lower version.

=cut

sub get_max_supported_protocol_version {
  my ( $self, $deviceProtcolVersion ) = @_;
  return "V_2_01" unless (defined($deviceProtcolVersion));                       # min. supported protocol version if undefined
  return $deviceProtcolVersion if (defined($COMMANDS->{$deviceProtcolVersion})); # requested version if known
  
  my $maxSupportedProtocolVersion = undef;
  foreach my $protocolVersion (sort keys %{$COMMANDS}) {
    if ($protocolVersion lt $deviceProtcolVersion) {
      $maxSupportedProtocolVersion = $protocolVersion;                           # nearest lower version if not known
    }
  }
  
  return $maxSupportedProtocolVersion;
}

1;
