diff --git a/fhem/FHEM/10_FRM.pm b/fhem/FHEM/10_FRM.pm index 5d53bc181..5a3a62b1b 100755 --- a/fhem/FHEM/10_FRM.pm +++ b/fhem/FHEM/10_FRM.pm @@ -323,6 +323,8 @@ sub FRM_DoInit($) { $main::defs{$name}{i2c_pins} = join(",", sort{$a<=>$b}(@$i2cpins)) if (defined $i2cpins and scalar @$i2cpins); my $onewirepins = $device->{metadata}{onewire_pins}; $main::defs{$name}{onewire_pins} = join(",", sort{$a<=>$b}(@$onewirepins)) if (defined $onewirepins and scalar @$onewirepins); + my $encoderpins = $device->{metadata}{encoder_pins}; + $main::defs{$name}{encoder_pins} = join(",", sort{$a<=>$b}(@$encoderpins)) if (defined $encoderpins and scalar @$encoderpins); if (defined $device->{metadata}{analog_resolutions}) { my @analog_resolutions; foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{analog_resolutions}})) { @@ -344,6 +346,13 @@ sub FRM_DoInit($) { } $main::defs{$name}{servo_resolutions} = join(",",@servo_resolutions) if (scalar @servo_resolutions); } + if (defined $device->{metadata}{encoder_resolutions}) { + my @encoder_resolutions; + foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{encoder_resolutions}})) { + push @encoder_resolutions,$pin.":".$device->{metadata}{encoder_resolutions}{$pin}; + } + $main::defs{$name}{encoder_resolutions} = join(",",@encoder_resolutions) if (scalar @encoder_resolutions); + } $found = 1; } else { select (undef,undef,undef,0.01); diff --git a/fhem/FHEM/20_FRM_ROTENC.pm b/fhem/FHEM/20_FRM_ROTENC.pm new file mode 100755 index 000000000..a0728cb78 --- /dev/null +++ b/fhem/FHEM/20_FRM_ROTENC.pm @@ -0,0 +1,224 @@ +############################################# +package main; + +use strict; +use warnings; + +#add FHEM/lib to @INC if it's not allready included. Should rather be in fhem.pl than here though... +BEGIN { + if (!grep(/FHEM\/lib$/,@INC)) { + foreach my $inc (grep(/FHEM$/,@INC)) { + push @INC,$inc."/lib"; + }; + }; +}; + +use Device::Firmata::Constants qw/ :all /; + +##################################### + +my %sets = ( + "reset" => "noArg", +); + +my %gets = ( + "position" => "noArg", +); + +sub +FRM_ROTENC_Initialize($) +{ + my ($hash) = @_; + + $hash->{SetFn} = "FRM_ROTENC_Set"; + $hash->{GetFn} = "FRM_ROTENC_Get"; + $hash->{AttrFn} = "FRM_ROTENC_Attr"; + $hash->{DefFn} = "FRM_Client_Define"; + $hash->{InitFn} = "FRM_ROTENC_Init"; + $hash->{UndefFn} = "FRM_ROTENC_Undef"; + + $hash->{AttrList} = "IODev $main::readingFnAttributes"; + main::LoadModule("FRM"); +} + +sub +FRM_ROTENC_Init($$) +{ + my ($hash,$args) = @_; + + my $u = "wrong syntax: define FRM_ROTENC pinA pinB [id]"; + return $u unless defined $args and int(@$args) > 1; + my $pinA = @$args[0]; + my $pinB = @$args[1]; + my $encoder = defined @$args[2] ? @$args[2] : 0; + + $hash->{PINA} = $pinA; + $hash->{PINB} = $pinB; + + $hash->{ENCODERNUM} = $encoder; + + eval { + FRM_Client_AssignIOPort($hash); + my $firmata = FRM_Client_FirmataDevice($hash); + $firmata->encoder_attach($encoder,$pinA,$pinB); + $firmata->observe_encoder($encoder, \&FRM_ROTENC_observer, $hash ); + }; + if ($@) { + $@ =~ /^(.*)( at.*FHEM.*)$/; + $hash->{STATE} = "error initializing: ".$1; + return "error initializing '".$hash->{NAME}."': ".$1; + } + + if (! (defined AttrVal($hash->{NAME},"stateFormat",undef))) { + $main::attr{$hash->{NAME}}{"stateFormat"} = "position"; + } + main::readingsSingleUpdate($hash,"state","Initialized",1); + return undef; +} + +sub +FRM_ROTENC_observer +{ + my ( $data, $hash ) = @_; + my $name = $hash->{NAME}; + Log3 $name,5,"onEncoderMessage for pins ".$hash->{PINA}.",".$hash->{PINB}." encoder: ".$data->{encoderNum}." position: ".$data->{value}."\n"; + main::readingsBeginUpdate($hash); + main::readingsBulkUpdate($hash,"position",$data->{value}, 1); + main::readingsEndUpdate($hash,1); +} + +sub +FRM_ROTENC_Set +{ + my ($hash, @a) = @_; + return "Need at least one parameters" if(@a < 2); + my $command = $a[1]; + my $value = $a[2]; + if(!defined($sets{$command})) { + my @commands = (); + foreach my $key (sort keys %sets) { + push @commands, $sets{$key} ? $key.":".join(",",$sets{$key}) : $key; + } + return "Unknown argument $a[1], choose one of " . join(" ", @commands); + } + COMMAND_HANDLER: { + $command eq "reset" and do { + eval { + FRM_Client_FirmataDevice($hash)->encoder_reset_position($hash->{ENCODERNUM}); + }; + last; + }; + } +} + +sub +FRM_ROTENC_Get($) +{ + my ($hash, @a) = @_; + return "Need at least one parameters" if(@a < 2); + my $command = $a[1]; + my $value = $a[2]; + if(!defined($gets{$command})) { + my @commands = (); + foreach my $key (sort keys %gets) { + push @commands, $gets{$key} ? $key.":".join(",",$gets{$key}) : $key; + } + return "Unknown argument $a[1], choose one of " . join(" ", @commands); + } + my $name = shift @a; + my $cmd = shift @a; + ARGUMENT_HANDLER: { + $cmd eq "position" and do { + return ReadingsVal($hash->{NAME},"position","0"); + }; + } + return undef; +} + +sub +FRM_ROTENC_Attr($$$$) { + my ($command,$name,$attribute,$value) = @_; + my $hash = $main::defs{$name}; + my $pin = $hash->{PIN}; + if ($command eq "set") { + ARGUMENT_HANDLER: { + $attribute eq "IODev" and do { + if (!defined ($hash->{IODev}) or $hash->{IODev}->{NAME} ne $value) { + $hash->{IODev} = $defs{$value}; + FRM_Init_Client($hash) if (defined ($hash->{IODev})); + } + last; + }; + } + } +} + +sub +FRM_ROTENC_Undef($$) +{ + my ($hash, $name) = @_; + my $pinA = $hash->{PINA}; + my $pinB = $hash->{PINB}; + eval { + my $firmata = FRM_Client_FirmataDevice($hash); + $firmata->encoder_detach($hash->{ENCODERNUM}); + $firmata->pin_mode($pinA,PIN_ANALOG); + $firmata->pin_mode($pinB,PIN_ANALOG); + }; + if ($@) { + eval { + my $firmata = FRM_Client_FirmataDevice($hash); + $firmata->pin_mode($pinA,PIN_INPUT); + $firmata->digital_write($pinA,0); + $firmata->pin_mode($pinB,PIN_INPUT); + $firmata->digital_write($pinB,0); + }; + } + return undef; +} + +1; + +=pod +=begin html + + +

FRM_ROTENC

+ +
+ +=end html +=cut diff --git a/fhem/FHEM/lib/Device/Firmata.pm b/fhem/FHEM/lib/Device/Firmata.pm index 7e1c2a156..30fe847f7 100644 --- a/fhem/FHEM/lib/Device/Firmata.pm +++ b/fhem/FHEM/lib/Device/Firmata.pm @@ -19,7 +19,7 @@ Version 0.50 =cut -our $VERSION = '0.52'; +our $VERSION = '0.53'; our $DEBUG = 0; diff --git a/fhem/FHEM/lib/Device/Firmata/Constants.pm b/fhem/FHEM/lib/Device/Firmata/Constants.pm index d9e449b09..c7ade028c 100644 --- a/fhem/FHEM/lib/Device/Firmata/Constants.pm +++ b/fhem/FHEM/lib/Device/Firmata/Constants.pm @@ -28,6 +28,7 @@ use constant ( PIN_I2C => 6, PIN_ONEWIRE => 7, PIN_STEPPER => 8, + PIN_ENCODER => 9, PIN_LOW => 0, PIN_HIGH => 1, } @@ -293,6 +294,65 @@ use constant ( ], }, # /Constants for Version 2.5 + + V_2_06 => { + + MAX_DATA_BYTES => 64, # max number of data bytes in non-Sysex messages + + # message command bytes (128-255/0x80-0xFF) + DIGITAL_MESSAGE => 0x90, # send data for a digital pin + ANALOG_MESSAGE => 0xE0, # send data for an analog pin (or PWM) + REPORT_ANALOG => 0xC0, # enable analog input by pin # + REPORT_DIGITAL => 0xD0, # enable digital input by port pair + SET_PIN_MODE => 0xF4, # set a pin to INPUT/OUTPUT/PWM/etc + REPORT_VERSION => 0xF9, # report protocol version + SYSTEM_RESET => 0xFF, # reset from MIDI + START_SYSEX => 0xF0, # start a MIDI Sysex message + END_SYSEX => 0xF7, # end a MIDI Sysex message + + # extended command set using sysex (0-127/0x00-0x7F) + RESERVED_COMMAND => 0x00, # 2nd SysEx data byte is a chip-specific command (AVR, PIC, TI, etc). + ENCODER_DATA => 0x61, # receive rotary-encoders current positions + ANALOG_MAPPING_QUERY => 0x69, # ask for mapping of analog to pin numbers + ANALOG_MAPPING_RESPONSE => 0x6A, # reply with mapping info + CAPABILITY_QUERY => 0x6B, # ask for supported modes and resolution of all pins + CAPABILITY_RESPONSE => 0x6C, # reply with supported modes and resolution + PIN_STATE_QUERY => 0x6D, # ask for a pin's current mode and value + PIN_STATE_RESPONSE => 0x6E, # reply with a pin's current mode and value + EXTENDED_ANALOG => 0x6F, # analog write (PWM, Servo, etc) to any pin + SERVO_CONFIG => 0x70, # set max angle, minPulse, maxPulse, freq + STRING_DATA => 0x71, # a string message with 14-bits per char + STEPPER_DATA => 0x72, # control a stepper motor + ONEWIRE_DATA => 0x73, # OneWire read/write/reset/select/skip/search request + read/search reply + SHIFT_DATA => 0x75, # shiftOut config/data message (34 bits) + I2C_REQUEST => 0x76, # send an I2C read/write request + I2C_REPLY => 0x77, # a reply to an I2C read request + I2C_CONFIG => 0x78, # config I2C settings such as delay times and power pins + REPORT_FIRMWARE => 0x79, # report name and version of the firmware + SAMPLING_INTERVAL => 0x7A, # set the poll rate of the main loop + SCHEDULER_DATA => 0x7B, # createtask/deletetask/addtotask/schedule/querytasks/querytask request and querytasks/querytask reply + SYSEX_NON_REALTIME => 0x7E, # MIDI Reserved for non-realtime messages + SYSEX_REALTIME => 0x7F, # MIDI Reserved for realtime messages + + # pin modes + INPUT => 0x00, # digital pin in digitalOut mode + OUTPUT => 0x01, # digital pin in digitalInput mode + ANALOG => 0x02, # analog pin in analogInput mode + PWM => 0x03, # digital pin in PWM output mode + SERVO => 0x04, # digital pin in Servo output mode + SHIFT => 0x05, # shiftIn/shiftOut mode + I2C => 0x06, # pin included in I2C setup + ONEWIRE => 0x07, # pin configured for 1-Wire commuication + STEPPER => 0x08, # pin configured for stepper motor + ENCODER => 0x09, # pin configured for rotary-encoders + + + # Deprecated entries + deprecated => [ + qw( FIRMATA_STRING SYSEX_I2C_REQUEST SYSEX_I2C_REPLY SYSEX_SAMPLING_INTERVAL ) + ], + + }, # /Constants for Version 2.6 } ); diff --git a/fhem/FHEM/lib/Device/Firmata/Platform.pm b/fhem/FHEM/lib/Device/Firmata/Platform.pm index 1837645ce..a8a128630 100644 --- a/fhem/FHEM/lib/Device/Firmata/Platform.pm +++ b/fhem/FHEM/lib/Device/Firmata/Platform.pm @@ -31,6 +31,7 @@ use Device::Firmata::Base ports => [], pins => {}, pin_modes => {}, + encoders => [], # To notify on events digital_observer => [], @@ -38,6 +39,8 @@ use Device::Firmata::Base sysex_observer => undef, i2c_observer => undef, onewire_observer => [], + stepper_observer => [], + encoder_observer => [], scheduler_observer => undef, string_observer => undef, @@ -242,6 +245,9 @@ sub sysex_handle { my @shiftpins; my @i2cpins; my @onewirepins; + my @stepperpins; + my @encoderpins; + foreach my $pin (keys %$capabilities) { if (defined $capabilities->{$pin}) { if ($capabilities->{$pin}->{PIN_INPUT+0}) { @@ -271,6 +277,12 @@ sub sysex_handle { if ($capabilities->{$pin}->{PIN_ONEWIRE+0}) { push @onewirepins, $pin; } + if ($capabilities->{$pin}->{PIN_STEPPER+0}) { + push @stepperpins, $pin; + } + if ($capabilities->{$pin}->{PIN_ENCODER+0}) { + push @encoderpins, $pin; + } } } $self->{metadata}{input_pins} = \@inputpins; @@ -281,6 +293,8 @@ sub sysex_handle { $self->{metadata}{shift_pins} = \@shiftpins; $self->{metadata}{i2c_pins} = \@i2cpins; $self->{metadata}{onewire_pins} = \@onewirepins; + $self->{metadata}{stepper_pins} = \@stepperpins; + $self->{metadata}{encoder_pins} = \@encoderpins; last; }; @@ -332,7 +346,23 @@ sub sysex_handle { $observer->{method}( $data->{string}, $observer->{context} ); } last; - } + }; + + $sysex_message->{command_str} eq 'STEPPER_DATA' and do { + #TODO implement handling of STEPPER_DATA and call observer. + last; + }; + + $sysex_message->{command_str} eq 'ENCODER_DATA' and do { + foreach my $encoder_data ( @$data ) { + my $encoderNum = $encoder_data->{encoderNum}; + my $observer = $self->{encoder_observer}[$encoderNum]; + if (defined $observer) { + $observer->{method}( $encoder_data, $observer->{context} ); + } + }; + last; + }; } } @@ -739,6 +769,38 @@ sub onewire_command_series { return $self->{io}->data_write($self->{protocol}->packet_onewire_request( $pin, $args )); } +sub encoder_attach { + my ( $self, $encoderNum, $pinA, $pinB ) = @_; + die "unsupported mode 'ENCODER' for pin '".$pinA."'" unless $self->is_supported_mode($pinA,PIN_ENCODER); + die "unsupported mode 'ENCODER' for pin '".$pinB."'" unless $self->is_supported_mode($pinB,PIN_ENCODER); + return $self->{io}->data_write($self->{protocol}->packet_encoder_attach( $encoderNum, $pinA, $pinB )); +} + +sub encoder_report_position { + my ( $self, $encoderNum ) = @_; + return $self->{io}->data_write($self->{protocol}->packet_encoder_report_position( $encoderNum )); +} + +sub encoder_report_positions { + my ( $self ) = @_; + return $self->{io}->data_write($self->{protocol}->packet_encoder_report_positions()); +} + +sub encoder_reset_position { + my ( $self, $encoderNum ) = @_; + return $self->{io}->data_write($self->{protocol}->packet_encoder_reset_position( $encoderNum )); +} + +sub encoder_report_auto { + my ( $self, $enable ) = @_; + return $self->{io}->data_write($self->{protocol}->packet_encoder_report_auto( $enable )); +} + +sub encoder_detach { + my ( $self, $encoderNum ) = @_; + return $self->{io}->data_write($self->{protocol}->packet_encoder_detach( $encoderNum )); +} + =head2 poll Call this function every once in a while to @@ -809,6 +871,21 @@ sub observe_onewire { return 1; } +sub observe_stepper { + #TODO implement observe_stepper + return 1; +} + +sub observe_encoder { + my ( $self, $encoderNum, $observer, $context ) = @_; +#TODO validation? die "unsupported mode 'ENCODER' for pin '".$pin."'" unless ($self->is_supported_mode($pin,PIN_ENCODER)); + $self->{encoder_observer}[$encoderNum] = { + method => $observer, + context => $context, + }; + return 1; +} + sub observe_scheduler { my ( $self, $observer, $context ) = @_; $self->{scheduler_observer} = { diff --git a/fhem/FHEM/lib/Device/Firmata/Protocol.pm b/fhem/FHEM/lib/Device/Firmata/Protocol.pm index 47af6d94c..4437da9e8 100644 --- a/fhem/FHEM/lib/Device/Firmata/Protocol.pm +++ b/fhem/FHEM/lib/Device/Firmata/Protocol.pm @@ -73,6 +73,20 @@ our $SCHEDULER_COMMANDS = { QUERY_TASK_REPLY => 10, }; +our $STEPPER_COMMANDS = { + STEPPER_CONFIG => 0, + STEPPER_STEP => 1, +}; + +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 $MODENAMES = { 0 => 'INPUT', 1 => 'OUTPUT', @@ -82,6 +96,8 @@ our $MODENAMES = { 5 => 'SHIFT', 6 => 'I2C', 7 => 'ONEWIRE', + 8 => 'STEPPER', + 9 => 'ENCODER', }; =head1 DESCRIPTION @@ -289,6 +305,16 @@ sub sysex_parse { $return_data = $self->handle_scheduler_response($sysex_data); last; }; + + $command == $protocol_commands->{STEPPER_DATA} and do { + #TODO implement and call handle_stepper_response + last; + }; + + $command == $protocol_commands->{ENCODER_DATA} and do { + $return_data = $self->handle_encoder_response($sysex_data); + last; + }; $command == $protocol_commands->{RESERVED_COMMAND} and do { $return_data = $sysex_data; @@ -841,6 +867,75 @@ sub handle_scheduler_response { } } +#TODO packet_stepper_config +sub packet_stepper_config { + my ( $self ) = @_; + my $packet = $self->packet_sysex_command('STEPPER_DATA', $STEPPER_COMMANDS->{STEPPER_CONFIG}); +} + +#TODO packet_stepper_step +sub packet_stepper_step { + my ( $self ) = @_; + my $packet = $self->packet_sysex_command('STEPPER_DATA', $STEPPER_COMMANDS->{STEPPER_STEP}); +} + +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 ) = @_; #TODO clarify encoder_report_auto $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; +} + sub shift14bit { my $data = shift;