From 76b280ec3ba9bafd2c8fe431efacd164c1b38813 Mon Sep 17 00:00:00 2001 From: jensb Date: Wed, 3 Jan 2018 18:59:05 +0000 Subject: [PATCH] 10_FRM.pm: Firmata 2.7+ update (Forum #81815), OWX fix (Forum #80409) git-svn-id: https://svn.fhem.de/fhem/trunk@15768 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/10_FRM.pm | 804 ++++++++++++++++++++++++++++++++++---------- fhem/MAINTAINER.txt | 2 +- 2 files changed, 627 insertions(+), 179 deletions(-) diff --git a/fhem/FHEM/10_FRM.pm b/fhem/FHEM/10_FRM.pm index 272610aac..a03503e91 100755 --- a/fhem/FHEM/10_FRM.pm +++ b/fhem/FHEM/10_FRM.pm @@ -1,6 +1,37 @@ -############################################## +######################################################################################## +# # $Id$ -############################################## +# +# FHEM module to commmunicate with Firmata devices +# +######################################################################################## +# +# LICENSE AND COPYRIGHT +# +# Copyright (C) 2013 ntruchess +# Copyright (C) 2015 jensb +# +# All rights reserved +# +# This script is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# This copyright notice MUST APPEAR in all copies of the script! +# +######################################################################################## + package main; use vars qw{%attr %defs}; @@ -8,7 +39,7 @@ use strict; use warnings; use GPUtils qw(:all); -#add FHEM/lib to @INC if it's not allready included. Should rather be in fhem.pl than here though... +#add FHEM/lib to @INC if it's not already included. Should rather be in fhem.pl than here though... BEGIN { if (!grep(/FHEM\/lib$/,@INC)) { foreach my $inc (grep(/FHEM$/,@INC)) { @@ -73,6 +104,10 @@ sub FRM_Initialize($) { $hash->{I2CWrtFn} = "FRM_I2C_Write"; + $hash->{IOOpenFn} = "FRM_Serial_Open"; + $hash->{IOWriteFn} = "FRM_Serial_Write"; + $hash->{IOCloseFn} = "FRM_Serial_Close"; + # Consumer $hash->{DefFn} = "FRM_Define"; $hash->{UndefFn} = "FRM_Undef"; @@ -81,7 +116,7 @@ sub FRM_Initialize($) { $hash->{AttrFn} = "FRM_Attr"; $hash->{NotifyFn} = "FRM_Notify"; - $hash->{AttrList} = "model:nano dummy:1,0 sampling-interval i2c-config $main::readingFnAttributes"; + $hash->{AttrList} = "model:nano dummy:1,0 sampling-interval i2c-config resetDeviceOnConnect:0,1 software-serial-config $main::readingFnAttributes"; } ##################################### @@ -128,6 +163,7 @@ sub FRM_Undef($) { sub FRM_Start { my ($hash) = @_; + my $name = $hash->{NAME}; my ($dev, $global) = split("[ \t]+", $hash->{DEF}); $hash->{DeviceName} = $dev; @@ -140,11 +176,14 @@ sub FRM_Start { # ($isClient && $global) || # ($global && $global ne "global")); + # clear old device ids to force full init + FRM_ClearConfiguration($hash); + # Make sure that fhem only runs once if($isServer) { my $ret = TcpServer_Open($hash, $dev, $global); if (!$ret) { - $hash->{STATE}="listening"; + readingsSingleUpdate($hash, 'state', "listening", 1); } return $ret; } @@ -185,6 +224,7 @@ sub FRM_Set($@) { $command eq "reset" and do { return $hash->{NAME}." is not connected" unless (FRM_is_firmata_connected($hash) && (defined $hash->{FD} or ($^O=~/Win/ and defined $hash->{USBDev}))); $hash->{FirmataDevice}->system_reset(); + FRM_ClearConfiguration($hash); if (defined $hash->{SERVERSOCKET}) { # dispose preexisting connections foreach my $e ( sort keys %main::defs ) { @@ -262,6 +302,7 @@ sub FRM_Read($) { } my $device = $hash->{FirmataDevice} or return; $device->poll(); + FRM_SetupDevice($hash); } sub FRM_Ready($) { @@ -271,6 +312,7 @@ sub FRM_Ready($) { if ($name=~/^^FRM:.+:\d+$/) { # this is a closed tcp-connection, remove it FRM_Tcp_Connection_Close($hash); FRM_FirmataDevice_Close($hash); + return; } return DevIo_OpenDev($hash, 1, "FRM_DoInit") if($hash->{STATE} eq "disconnected"); @@ -289,8 +331,8 @@ sub FRM_Tcp_Connection_Close($) { if ($hash->{SNAME}) { my $shash = $main::defs{$hash->{SNAME}}; if (defined $shash) { - $shash->{STATE}="listening"; delete $shash->{SocketDevice} if (defined $shash->{SocketDevice}); + readingsSingleUpdate($shash, 'state', "listening", 1); } } my $dev = $hash->{DeviceName}; @@ -305,6 +347,7 @@ sub FRM_Tcp_Connection_Close($) { sub FRM_FirmataDevice_Close($) { my $hash = shift; + my $name = $hash->{NAME}; my $device = $hash->{FirmataDevice}; if (defined $device) { if (defined $device->{io}) { @@ -359,121 +402,289 @@ sub FRM_apply_attribute { } sub FRM_DoInit($) { - - my ($hash) = @_; - - my $sname = $hash->{SNAME}; #is this a serversocket-connection? - my $shash = defined $sname ? $main::defs{$sname} : $hash; - - my $name = $shash->{NAME}; - - my $firmata_io = Firmata_IO->new($hash,$name); - my $device = Device::Firmata::Platform->attach($firmata_io) or return 1; + my ($hash) = @_; - $shash->{FirmataDevice} = $device; - if (defined $sname) { - $shash->{SocketDevice} = $hash; - #as FRM_Read gets the connected socket hash, but calls firmatadevice->poll(): - $hash->{FirmataDevice} = $device; - } - $device->observe_string(\&FRM_string_observer,$shash); - - my $found; # we cannot call $device->probe() here, as it doesn't select bevore read, so it would likely cause IODev to close the connection on the first attempt to read from empty stream - my $endTicks = time+5; - my $queryTicks = time+2; - $device->system_reset(); - do { - FRM_poll($shash); - if ($device->{metadata}{firmware} && $device->{metadata}{firmware_version}) { - $device->{protocol}->{protocol_version} = $device->{metadata}{firmware_version}; - $main::defs{$name}{firmware} = $device->{metadata}{firmware}; - $main::defs{$name}{firmware_version} = $device->{metadata}{firmware_version}; - Log3 $name,3,"Firmata Firmware Version: ".$device->{metadata}{firmware}." ".$device->{metadata}{firmware_version}; - $device->analog_mapping_query(); - $device->capability_query(); - do { - FRM_poll($shash); - if ($device->{metadata}{analog_mappings} and $device->{metadata}{capabilities}) { - my $inputpins = $device->{metadata}{input_pins}; - $main::defs{$name}{input_pins} = join(",", sort{$a<=>$b}(@$inputpins)) if (defined $inputpins and scalar @$inputpins); - my $outputpins = $device->{metadata}{output_pins}; - $main::defs{$name}{output_pins} = join(",", sort{$a<=>$b}(@$outputpins)) if (defined $outputpins and scalar @$outputpins); - my $analogpins = $device->{metadata}{analog_pins}; - $main::defs{$name}{analog_pins} = join(",", sort{$a<=>$b}(@$analogpins)) if (defined $analogpins and scalar @$analogpins); - my $pwmpins = $device->{metadata}{pwm_pins}; - $main::defs{$name}{pwm_pins} = join(",", sort{$a<=>$b}(@$pwmpins)) if (defined $pwmpins and scalar @$pwmpins); - my $servopins = $device->{metadata}{servo_pins}; - $main::defs{$name}{servo_pins} = join(",", sort{$a<=>$b}(@$servopins)) if (defined $servopins and scalar @$servopins); - my $i2cpins = $device->{metadata}{i2c_pins}; - $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); - my $stepperpins = $device->{metadata}{stepper_pins}; - $main::defs{$name}{stepper_pins} = join(",", sort{$a<=>$b}(@$stepperpins)) if (defined $stepperpins and scalar @$stepperpins); - if (defined $device->{metadata}{analog_resolutions}) { - my @analog_resolutions; - foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{analog_resolutions}})) { - push @analog_resolutions,$pin.":".$device->{metadata}{analog_resolutions}{$pin}; - } - $main::defs{$name}{analog_resolutions} = join(",",@analog_resolutions) if (scalar @analog_resolutions); - } - if (defined $device->{metadata}{pwm_resolutions}) { - my @pwm_resolutions; - foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{pwm_resolutions}})) { - push @pwm_resolutions,$pin.":".$device->{metadata}{pwm_resolutions}{$pin}; - } - $main::defs{$name}{pwm_resolutions} = join(",",@pwm_resolutions) if (scalar @pwm_resolutions); - } - if (defined $device->{metadata}{servo_resolutions}) { - my @servo_resolutions; - foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{servo_resolutions}})) { - push @servo_resolutions,$pin.":".$device->{metadata}{servo_resolutions}{$pin}; - } - $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); - } - if (defined $device->{metadata}{stepper_resolutions}) { - my @stepper_resolutions; - foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{stepper_resolutions}})) { - push @stepper_resolutions,$pin.":".$device->{metadata}{stepper_resolutions}{$pin}; - } - $main::defs{$name}{stepper_resolutions} = join(",",@stepper_resolutions) if (scalar @stepper_resolutions); - } - $found = 1; - } else { - select (undef,undef,undef,0.01); - } - } while (time < $endTicks and !$found); - $found = 1; - } else { - select (undef,undef,undef,0.01); - if (time > $queryTicks) { - Log3 $name,3,"querying Firmata Firmware Version"; - $device->firmware_version_query(); - $queryTicks++; - } - } - } while (time < $endTicks and !$found); - if ($found) { - FRM_apply_attribute($shash,"sampling-interval"); - FRM_apply_attribute($shash,"i2c-config"); - FRM_forall_clients($shash,\&FRM_Init_Client,undef); - $shash->{STATE}="Initialized"; - return undef; - } - Log3 $name,3,"no response from Firmata, closing DevIO"; - DevIo_Disconnected($shash); - delete $shash->{FirmataDevice}; - delete $shash->{SocketDevice}; - return "FirmataDevice not responding"; + my $sname = $hash->{SNAME}; #is this a serversocket-connection? + my $shash = defined $sname ? $main::defs{$sname} : $hash; + my $name = $shash->{NAME}; + + Log3 $name, 5, "$name FRM_DoInit"; + + my $firmata_io = Firmata_IO->new($hash, $name); + my $device = Device::Firmata::Platform->attach($firmata_io) or return 1; + + $shash->{FirmataDevice} = $device; + if (defined $sname) { + $shash->{SocketDevice} = $hash; + #as FRM_Read gets the connected socket hash, but calls firmatadevice->poll(): + $hash->{FirmataDevice} = $device; + } + $device->observe_string(\&FRM_string_observer, $shash); + + if (AttrVal($name, 'resetDeviceOnConnect', 1)) { + $device->system_reset(); + FRM_ClearConfiguration($hash); + } + + $shash->{SETUP_START} = gettimeofday(); + $shash->{SETUP_STAGE} = 1; # detect connect mode (fhem startup, device startup or device reconnect) and query versions + $shash->{SETUP_TRIES} = 1; + + return FRM_SetupDevice($shash); +} + +=item FRM_ClearConfiguration() + + Parameters: + @args: hash reference of FRM device instance + + Returns: nothing + + Desciption: + delete all version and capability readings + +=cut + +sub FRM_ClearConfiguration($) { + my $hash = shift; + my $name = $hash->{NAME}; + + delete $main::defs{$name}{protocol_version}; + delete $main::defs{$name}{firmware}; + delete $main::defs{$name}{firmware_version}; + delete $main::defs{$name}{input_pins}; + delete $main::defs{$name}{input_pins}; + delete $main::defs{$name}{output_pins}; + delete $main::defs{$name}{analog_pins}; + delete $main::defs{$name}{pwm_pins}; + delete $main::defs{$name}{servo_pins}; + delete $main::defs{$name}{i2c_pins}; + delete $main::defs{$name}{onewire_pins}; + delete $main::defs{$name}{encoder_pins}; + delete $main::defs{$name}{stepper_pins}; + delete $main::defs{$name}{serial_pins}; + delete $main::defs{$name}{analog_resolutions}; + delete $main::defs{$name}{pwm_resolutions}; + delete $main::defs{$name}{servo_resolutions}; + delete $main::defs{$name}{encoder_resolutions}; + delete $main::defs{$name}{stepper_resolutions}; + delete $main::defs{$name}{serial_resolutions}; +} + +=item FRM_SetupDevice() + + Parameters: + @args: hash reference of FRM device instance or TCP session device instance + + Returns: nothing + + Desciption: + Monitor data received from Firmata device immediately after connect, perform + protocol, firmware and capability queries and configure device according to + the FRM device attributes. + +=cut + +sub FRM_SetupDevice($); + +sub FRM_SetupDevice($) { + my ($hash) = @_; + + $hash = $main::defs{$hash->{SNAME}} if (defined($hash->{SNAME})); + return undef if (!defined($hash->{SETUP_START})); + + my $name = $hash->{NAME}; + + Log3 $name, 5, "$name setup stage $hash->{SETUP_STAGE}"; + + my $now = gettimeofday(); + my $elapsed = $now - $hash->{SETUP_START}; + my $device = $hash->{FirmataDevice}; + + if ($hash->{SETUP_STAGE} == 1) { # protocol and firmware version + RemoveInternalTimer($hash); + InternalTimer($now + (($elapsed < 1)? 0.1 : 1), 'FRM_SetupDevice', $hash, 0); + # wait for protocol and firmware version + my $fhemRestart = !defined($main::defs{$name}{protocol_version}); + my $versionsReceived = $device->{metadata}{firmware} && $device->{metadata}{firmware_version} && $device->{metadata}{protocol_version}; + if ($versionsReceived) { + # clear old version and capability readings if not already done + if (!$fhemRestart) { + FRM_ClearConfiguration($hash); + } + # protocol and firmware versions have been received + $main::defs{$name}{firmware} = $device->{metadata}{firmware}; + $main::defs{$name}{firmware_version} = $device->{metadata}{firmware_version}; + $main::defs{$name}{protocol_version} = $device->{protocol}->get_max_supported_protocol_version($device->{metadata}{protocol_version}); + Log3 $name, 3, $name." Firmata Firmware Version: ".$device->{metadata}{firmware}." ".$device->{metadata}{firmware_version}." (using Protocol Version: ".$main::defs{$name}{protocol_version}.")"; + # query capabilities + $device->analog_mapping_query(); + $device->capability_query(); + # wait for capabilities + $hash->{SETUP_STAGE} = 2; + } elsif ($elapsed >= 3) { + # protocol and firmware version still missing + if ($hash->{SETUP_TRIES} < 3) { + # requery versions + Log3 $name, 3, "$name querying Firmata versions"; + $device->protocol_version_query(); + $device->firmware_version_query(); + # restart setup + $hash->{SETUP_START} = gettimeofday(); + $hash->{SETUP_STAGE} = 1; + $hash->{SETUP_TRIES}++; + } else { + # retry limit exceeded, abort + $hash->{SETUP_STAGE} = 5; + FRM_SetupDevice($hash); + } + } elsif ($elapsed >= 0.2 && defined($hash->{PORT})) { + # if we don't receive the protocol version within 200 millis, the device has reconnected (or has an old firmware) + my $deviceRestart = $device->{metadata}{protocol_version}; + if (!$deviceRestart && !$fhemRestart) { + # probably a reconnect + Log3 $name, 3, "$name Firmata device has reconnected"; + #if ($skipSetupOnReconnect) { + # # skip capability queries and device setup, just reinit client modules + # $hash->{SETUP_STAGE} = 3; + # FRM_SetupDevice($hash); + # return undef; + #} + # clear old version and capability readings + FRM_ClearConfiguration($hash); + # query versions + Log3 $name, 3, "$name querying Firmata versions"; + $device->protocol_version_query(); + $device->firmware_version_query(); + } + } + + } elsif ($hash->{SETUP_STAGE} == 2) { # device capabilities + RemoveInternalTimer($hash); + InternalTimer(gettimeofday() + 1, 'FRM_SetupDevice', $hash, 0); + my $capabilitiesReceived = $device->{metadata}{analog_mappings} && $device->{metadata}{capabilities}; + if ($capabilitiesReceived) { + # device capabilities have been received, convert to readings + my $inputpins = $device->{metadata}{input_pins}; + $main::defs{$name}{input_pins} = join(",", sort{$a<=>$b}(@$inputpins)) if (defined $inputpins and scalar @$inputpins); + my $outputpins = $device->{metadata}{output_pins}; + $main::defs{$name}{output_pins} = join(",", sort{$a<=>$b}(@$outputpins)) if (defined $outputpins and scalar @$outputpins); + my $analogpins = $device->{metadata}{analog_pins}; + $main::defs{$name}{analog_pins} = join(",", sort{$a<=>$b}(@$analogpins)) if (defined $analogpins and scalar @$analogpins); + my $pwmpins = $device->{metadata}{pwm_pins}; + $main::defs{$name}{pwm_pins} = join(",", sort{$a<=>$b}(@$pwmpins)) if (defined $pwmpins and scalar @$pwmpins); + my $servopins = $device->{metadata}{servo_pins}; + $main::defs{$name}{servo_pins} = join(",", sort{$a<=>$b}(@$servopins)) if (defined $servopins and scalar @$servopins); + my $i2cpins = $device->{metadata}{i2c_pins}; + $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); + my $stepperpins = $device->{metadata}{stepper_pins}; + $main::defs{$name}{stepper_pins} = join(",", sort{$a<=>$b}(@$stepperpins)) if (defined $stepperpins and scalar @$stepperpins); + my $serialpins = $device->{metadata}{serial_pins}; + $main::defs{$name}{serial_pins} = join(",", sort{$a<=>$b}(@$serialpins)) if (defined $serialpins and scalar @$serialpins); + if (defined $device->{metadata}{analog_resolutions}) { + my @analog_resolutions; + foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{analog_resolutions}})) { + push @analog_resolutions,$pin.":".$device->{metadata}{analog_resolutions}{$pin}; + } + $main::defs{$name}{analog_resolutions} = join(",",@analog_resolutions) if (scalar @analog_resolutions); + } + if (defined $device->{metadata}{pwm_resolutions}) { + my @pwm_resolutions; + foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{pwm_resolutions}})) { + push @pwm_resolutions,$pin.":".$device->{metadata}{pwm_resolutions}{$pin}; + } + $main::defs{$name}{pwm_resolutions} = join(",",@pwm_resolutions) if (scalar @pwm_resolutions); + } + if (defined $device->{metadata}{servo_resolutions}) { + my @servo_resolutions; + foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{servo_resolutions}})) { + push @servo_resolutions,$pin.":".$device->{metadata}{servo_resolutions}{$pin}; + } + $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); + } + if (defined $device->{metadata}{stepper_resolutions}) { + my @stepper_resolutions; + foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{stepper_resolutions}})) { + push @stepper_resolutions,$pin.":".$device->{metadata}{stepper_resolutions}{$pin}; + } + $main::defs{$name}{stepper_resolutions} = join(",",@stepper_resolutions) if (scalar @stepper_resolutions); + } + if (defined $device->{metadata}{serial_resolutions}) { + my @serial_resolutions; + foreach my $pin (sort{$a<=>$b}(keys %{$device->{metadata}{serial_resolutions}})) { + push @serial_resolutions,$pin.":".$device->{metadata}{serial_resolutions}{$pin}; + } + $main::defs{$name}{serial_resolutions} = join(",",@serial_resolutions) if (scalar @serial_resolutions); + } + # setup device + FRM_apply_attribute($hash, "sampling-interval"); + FRM_apply_attribute($hash, "i2c-config"); + FRM_serial_setup($hash); + # ready, init client modules + $hash->{SETUP_STAGE} = 3; + FRM_SetupDevice($hash); + + } elsif ($elapsed >= 5) { + # capabilities receive timeout, abort + $hash->{SETUP_STAGE} = 5; + FRM_SetupDevice($hash); + } + + } elsif ($hash->{SETUP_STAGE} == 3) { # client modules + # client module initialization + FRM_forall_clients($hash, \&FRM_Init_Client, undef); + readingsSingleUpdate($hash, 'state', "Initialized", 1); + # done, terminate setup sequence + $hash->{SETUP_STAGE} = 4; + FRM_SetupDevice($hash); + + } elsif ($hash->{SETUP_STAGE} == 4) { # finish setup + # terminate setup sequence + RemoveInternalTimer($hash); + delete $hash->{SETUP_START}; + delete $hash->{SETUP_STAGE}; + delete $hash->{SETUP_TRIES}; + + } elsif ($hash->{SETUP_STAGE} == 5) { # abort setup + # device setup has failed, cleanup connection + if (defined $hash->{SERVERSOCKET}) { + Log3 $name, 3, "$name no response from Firmata, closing connection"; + foreach my $e (sort keys %main::defs) { + if (defined(my $dev = $main::defs{$e})) { + if (defined($dev->{SNAME}) && ($dev->{SNAME} eq $hash->{NAME})) { + FRM_Tcp_Connection_Close($dev); + } + } + } + FRM_FirmataDevice_Close($hash); + } else { + Log3 $name, 3, "$name no response from Firmata, closing DevIo"; + DevIo_Disconnected($hash); + delete $hash->{FirmataDevice}; + delete $hash->{SocketDevice}; + } + # cleanup setup + $hash->{SETUP_STAGE} = 4; + FRM_SetupDevice($hash); + + } else { + # invalid state, abort + $hash->{SETUP_STAGE} = 5; + FRM_SetupDevice($hash); + } + + return undef; } sub @@ -517,8 +728,8 @@ FRM_Init_Pin_Client($$$) { FRM_Client_FirmataDevice($hash)->pin_mode($pin,$mode); }; if ($@) { + readingsSingleUpdate($hash, 'state', "error initializing: pin $pin", 1); $@ =~ /^(.*)( at.*FHEM.*)$/; - $hash->{STATE} = "error initializing: ".$1; return $1; } return undef; @@ -530,7 +741,7 @@ FRM_Client_Define($$) my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); - $hash->{STATE}="defined"; + readingsSingleUpdate($hash, 'state', "defined", 1); if ($main::init_done) { eval { @@ -568,7 +779,7 @@ FRM_Client_Unassign($) { my ($dev) = @_; delete $dev->{IODev} if defined $dev->{IODev}; - $dev->{STATE}="defined"; + readingsSingleUpdate($dev, 'state', "defined", 1); } sub @@ -576,9 +787,14 @@ FRM_Client_AssignIOPort($@) { my ($hash,$iodev) = @_; my $name = $hash->{NAME}; - AssignIoPort($hash,defined $iodev ? $iodev : AttrVal($hash->{NAME},"IODev",undef)); + + # use proposed $iodev or assigned {IODev} (FHEM will additionally check IODev attribute if not defined) + $iodev = defined($iodev)? $iodev : (defined($hash->{IODev})? $hash->{IODev}{NAME} : undef); + Log3 $name, 5, "$name FRM_Client_AssignIOPort before IODev " . (defined($hash->{IODev})? $hash->{IODev}{NAME} : "-" ) . " -> " . (defined($iodev)? $iodev : "?"); + AssignIoPort($hash, $iodev); die "unable to assign IODev to '$name'" unless defined ($hash->{IODev}); - + Log3 $name, 5, "$name FRM_Client_AssignIOPort after IODev $hash->{IODev}{NAME}"; + if (defined($hash->{IODev}->{SNAME})) { $hash->{IODev} = $main::defs{$hash->{IODev}->{SNAME}}; $attr{$name}{IODev} = $hash->{IODev}{NAME}; @@ -594,7 +810,7 @@ FRM_Client_AssignIOPort($@) && grep {$_ == $hash->{PIN}} split(" ",$dev->{PIN}) ) { delete $hash->{IODev}; delete $attr{$name}{IODev}; - die "Device '$main::defs{$d}{NAME}' allready defined for pin $hash->{PIN}"; + die "Device '$main::defs{$d}{NAME}' already defined for pin $hash->{PIN}"; } } } @@ -630,7 +846,7 @@ sub new($$) { sub data_write { my ( $self, $buf ) = @_; my $hash = $self->{hash}; - main::Log3 $self->{name},5,"FRM:>".unpack "H*",$buf; + main::Log3 $self->{name},5,"$self->{name} FRM:>".unpack "H*",$buf; main::DevIo_SimpleWrite($hash,$buf,undef); } @@ -639,7 +855,7 @@ sub data_read { my $hash = $self->{hash}; my $string = main::DevIo_SimpleRead($hash); if (defined $string ) { - main::Log3 $self->{name},5,"FRM:<".unpack "H*",$string; + main::Log3 $self->{name},5,"$self->{name} FRM:<".unpack "H*",$string; } return $string; } @@ -680,7 +896,7 @@ sub FRM_I2C_Write { my ($hash,$package) = @_; - if (FRM_is_firmata_connected($hash)) { + if (FRM_is_firmata_connected($hash) && defined($package) && defined($package->{i2caddress})) { my $firmata = $hash->{FirmataDevice}; COMMANDHANDLER: { $package->{direction} eq "i2cwrite" and do { @@ -692,6 +908,7 @@ sub FRM_I2C_Write last; }; $package->{direction} eq "i2cread" and do { + delete $hash->{I2C_ERROR}; if (defined $package->{reg}) { $firmata->i2c_readonce($package->{i2caddress},$package->{reg},defined $package->{nbyte} ? $package->{nbyte} : 1); } else { @@ -716,13 +933,17 @@ sub FRM_i2c_update_device my ($hash,$data) = @_; if (defined $hash->{I2C_Address} and $hash->{I2C_Address} eq $data->{address}) { + my $sendStat = "Ok"; + if (defined($hash->{IODev}->{I2C_ERROR})) { + $sendStat = $hash->{IODev}->{I2C_ERROR}; + } CallFn($hash->{NAME}, "I2CRecFn", $hash, { i2caddress => $data->{address}, direction => "i2cread", reg => $data->{register}, nbyte => scalar(@{$data->{data}}), received => join (' ',@{$data->{data}}), - $hash->{IODev}->{NAME}."_SENDSTAT" => "Ok", + $hash->{IODev}->{NAME}."_SENDSTAT" => $sendStat, }); } elsif (defined $hash->{"i2c-address"} && $hash->{"i2c-address"}==$data->{address}) { my $replydata = $data->{data}; @@ -740,40 +961,75 @@ sub FRM_string_observer my ($string,$hash) = @_; Log3 $hash->{NAME},3,"received String_data: ".$string; readingsSingleUpdate($hash,"error",$string,1); + if ($string =~ "I2C.*") { + $hash->{I2C_ERROR} = substr($string, 5); + } +} + +sub FRM_serial_observer +{ + my ($data,$hash) = @_; + #Log3 $hash->{NAME},5,"onSerialMessage port: '".$data->{port}."' data: [".(join(',',@{$data->{data}}))."]"; + FRM_forall_clients($hash,\&FRM_serial_update_device,$data); +} + +sub FRM_serial_update_device +{ + my ($hash,$data) = @_; + + if (defined $hash->{IODevPort} and $hash->{IODevPort} eq $data->{port}) { + my $buf = pack("C*", @{$data->{data}}); + #Log3 $hash->{NAME},5,"FRM_serial_update_device port: " . length($buf) . " bytes on serial port " . $data->{port} . " for " . $hash->{NAME}; + $hash->{IODevRxBuffer} = "" if (!defined($hash->{IODevRxBuffer})); + $hash->{IODevRxBuffer} = $hash->{IODevRxBuffer} . $buf; + CallFn($hash->{NAME}, "ReadFn", $hash); + } +} + +sub FRM_serial_setup +{ + my ($hash) = @_; + + foreach my $port ( keys %{$hash->{SERIAL}} ) { + my $chash = $defs{$hash->{SERIAL}{$port}}; + if (defined($chash)) { + FRM_Serial_Setup($chash); + } + } } sub FRM_poll { - my ($hash) = @_; - if (defined $hash->{SocketDevice} and defined $hash->{SocketDevice}->{FD}) { - my ($rout, $rin) = ('', ''); - vec($rin, $hash->{SocketDevice}->{FD}, 1) = 1; - my $nfound = select($rout=$rin, undef, undef, 0.1); - my $mfound = vec($rout, $hash->{SocketDevice}->{FD}, 1); - if($mfound && FRM_is_firmata_connected($hash)) { - $hash->{FirmataDevice}->poll(); - } - return $mfound; - } elsif (defined $hash->{FD}) { - my ($rout, $rin) = ('', ''); - vec($rin, $hash->{FD}, 1) = 1; - my $nfound = select($rout=$rin, undef, undef, 0.1); - my $mfound = vec($rout, $hash->{FD}, 1); - if($mfound && FRM_is_firmata_connected($hash)) { - $hash->{FirmataDevice}->poll(); - } - return $mfound; - } else { - # This is relevant for windows/USB only - my $po = $hash->{USBDev}; - my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags); - if($po) { - ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; - } - if ($InBytes && $InBytes>0 && FRM_is_firmata_connected($hash)) { - $hash->{FirmataDevice}->poll(); - } - } + my ($hash) = @_; + if (defined $hash->{SocketDevice} and defined $hash->{SocketDevice}->{FD}) { + my ($rout, $rin) = ('', ''); + vec($rin, $hash->{SocketDevice}->{FD}, 1) = 1; + my $nfound = select($rout=$rin, undef, undef, 0.1); + my $mfound = vec($rout, $hash->{SocketDevice}->{FD}, 1); + if($mfound && FRM_is_firmata_connected($hash)) { + $hash->{FirmataDevice}->poll(); + } + return $mfound; + } elsif (defined $hash->{FD}) { + my ($rout, $rin) = ('', ''); + vec($rin, $hash->{FD}, 1) = 1; + my $nfound = select($rout=$rin, undef, undef, 0.1); + my $mfound = vec($rout, $hash->{FD}, 1); + if($mfound && FRM_is_firmata_connected($hash)) { + $hash->{FirmataDevice}->poll(); + } + return $mfound; + } else { + # This is relevant for windows/USB only + my $po = $hash->{USBDev}; + my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags); + if($po) { + ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; + } + if ($InBytes && $InBytes>0 && FRM_is_firmata_connected($hash)) { + $hash->{FirmataDevice}->poll(); + } + } } ######### following is code to be called from OWX: ########## @@ -796,7 +1052,7 @@ FRM_OWX_Init($$) } }; return GP_Catch($@) if ($@); - $hash->{STATE}="Initialized"; + ReadingsSingleUpdate($hash, 'state', "Initialized", 1); InternalTimer(gettimeofday()+10, "OWX_Discover", $hash,0); return undef; } @@ -1031,13 +1287,136 @@ sub FRM_OWX_Discover ($) { return $ret; } +######### following is code to be called from DevIo: ########## + +sub FRM_Serial_Open { + my ($hash) = @_; + + if (!defined $hash->{IODevPort} || !defined $hash->{IODevParameters}) { + Log3 $hash->{NAME},3,"$hash->{IODev}{NAME} Serial_Open: serial port or baudrate not defined by $hash->{NAME}"; + return 0; + } + + $hash->{IODev}{SERIAL}{$hash->{IODevPort}} = $hash->{NAME}; + + Log3 $hash->{NAME},5,"$hash->{IODev}{NAME} Serial_Open: serial port $hash->{IODevPort} registered for $hash->{NAME}"; + + return FRM_Serial_Setup($hash); +} + +sub FRM_Serial_Setup { + my ($hash) = @_; + + if (FRM_is_firmata_connected($hash->{IODev})) { + my $firmata = FRM_Client_FirmataDevice($hash); + if (!defined $firmata ) { + Log3 $hash->{NAME},3,"$hash->{IODev}{NAME} Serial_Setup: no Firmata device available"; + return 0; + } + + # configure port by claiming pins, setting baud rate and start reading + my $port = $hash->{IODevPort}; + if ($hash->{IODevParameters} =~ m/(\d+)(,([78])(,([NEO])(,([012]))?)?)?/) { + my $baudrate = $1; + if ($port > 7) { + # software serial port, get serial pins from attribute + my $err; + my $serialattr = AttrVal($hash->{IODev}{NAME}, "software-serial-config", undef); + if (defined $serialattr) { + my @a = split(":", $serialattr); + if (scalar @a == 3 && $a[0] == $port) { + $hash->{PIN_RX} = $a[1]; + $hash->{PIN_TX} = $a[2]; + + # activate port + $firmata->serial_config($port, $baudrate, $a[1], $a[2]); + } else { + $err = "Error, invalid software-serial-config, must be ::"; + } + } else { + $err = "Error, attribute software-serial-config required for using software serial port $port"; + } + if ($err) { + Log3 $hash->{NAME},2,"$hash->{IODev}{NAME}: $err"; + return 0; + } + } else { + # hardware serial port, get serial pins by port number from capability metadata + my $rxPinType = 2*$port; + my $txPinType = $rxPinType + 1; + my $rxPin = undef; + my $txPin = undef; + foreach my $pin ( keys %{$firmata->{metadata}{serial_resolutions}} ) { + if ($firmata->{metadata}{serial_resolutions}{$pin} == $rxPinType) { + $rxPin = $pin; + } + if ($firmata->{metadata}{serial_resolutions}{$pin} == $txPinType) { + $txPin = $pin; + } + } + if (!defined $rxPin || !defined $txPin) { + Log3 $hash->{NAME},3,"$hash->{IODev}{NAME} Serial_Setup: serial pins of port $port not available on Arduino"; + return 0; + } + $hash->{PIN_RX} = $rxPin; + $hash->{PIN_TX} = $txPin; + + # activate port + $firmata->serial_config($port, $baudrate); + } + $firmata->observe_serial($port, \&FRM_serial_observer, $hash->{IODev}); + $firmata->serial_read($port, 0); # continuously read and send all available bytes + Log3 $hash->{NAME},5,"$hash->{IODev}{NAME} Serial_Setup: serial port $hash->{IODevPort} opened with $baudrate baud for $hash->{NAME}"; + } else { + Log3 $hash->{NAME},3,"$hash->{IODev}{NAME} Serial_Setup: invalid baudrate definition $hash->{IODevParameters} for port $port by $hash->{NAME}"; + return 0; + } + } + + return 1; +} + +sub FRM_Serial_Write { + my ($hash, $msg) = @_; + + my $firmata = FRM_Client_FirmataDevice($hash); + my $port = $hash->{IODevPort}; + return 0 unless ( defined $firmata and defined $port ); + + if (FRM_is_firmata_connected($hash->{IODev}) && defined($msg)) { + my @data = unpack("C*", $msg); + #my $size = scalar(@data); + #Log3 $hash->{NAME},3,"$hash->{IODev}{NAME} Serial_Write: $size bytes on serial port $hash->{IODevPort} $msg by $hash->{NAME}"; + $firmata->serial_write($port, @data); + return length($msg); + } else { + return 0; + } +} + +sub FRM_Serial_Close { + my ($hash) = @_; + + my $port = $hash->{IODevPort}; + return 0 unless ( defined $port ); + + if (FRM_is_firmata_connected($hash->{IODev})) { + my $firmata = FRM_Client_FirmataDevice($hash); + $firmata->serial_stopreading($port); + } + + delete $hash->{PIN_RX}; + delete $hash->{PIN_TX}; + delete $hash->{IODev}{SERIAL}{$hash->{IODevPort}}; + + #Log3 $hash->{NAME},5,"$hash->{IODev}{NAME} Serial_Close: serial port $hash->{IODevPort} unregistered for $hash->{NAME}"; + + return 1; +} + 1; =pod -=item device -=item summary accesses FRM devices -=item summary_DE Zugriff auf FRM Geräte -=begin html CHANGES @@ -1054,9 +1433,43 @@ sub FRM_OWX_Discover ($) { - set STATE to listening and delete SocketDevice (to present same idle state as FRM_Start) o help updated + + 22.12.2015 jensb + o modified sub FRM_DoInit: + - clear internal readings (device may have changed) + o added serial pin support + + 05.01.2016 jensb + o modified FRM_DoInit: + - do not disconnect DevIo in TCP mode to stay reconnectable + o use readingsSingleUpdate on state instead of directly changing STATE + + 26.03.2016 jensb + o asynchronous device setup (FRM_DoInit, FRM_SetupDevice) + o experimental reconnect detection + o new attribute to skip device reset on connect + o help updated + + 31.12.2016 jensb + o I2C read error detection + - modified FRM_I2C_Write: delete internal I2C_ERROR reading before performing I2C read operation to detect read errors + - modified FRM_string_observer: assign Firmata message to internal I2C_ERROR reading when string starts with "I2C" + - modified FRM_i2c_update_device: assign internal I2C_ERROR to XXX_SENDSTAT of IODev if defined after performing a I2C read + + 27.12.2017 JB + o I2C write parameter validation + - modified FRM_I2C_Write: prevent processing if parameters are undefined + + 01.01.2018 JB + o OWX support + - modified FRM_Client_AssignIOPort: use already assigned IODev + =cut =pod +=item device +=item summary Firmata device gateway +=item summary_DE Firmata Gateway =begin html @@ -1081,14 +1494,14 @@ sub FRM_OWX_Discover ($) { Each client stands for a Pin of the Arduino configured for a specific use (digital/analog in/out) or an integrated circuit connected to Arduino by i2c.

- + Note: this module is based on Device::Firmata module (perl-firmata). perl-firmata is included in FHEM-distributions lib-directory. You can download the latest version as a single zip file from github.

Note: this module may require the Device::SerialPort or Win32::SerialPort module if you attach the device via USB and the OS sets strange default parameters for serial devices.

- + Define

    @@ -1113,11 +1526,11 @@ sub FRM_OWX_Discover ($) { with simple file io. This might work if the operating system uses sane defaults for the serial parameters, e.g. some Linux distributions and OSX.

    - + The Arduino has to run either 'StandardFirmata' or 'ConfigurableFirmata'. StandardFirmata supports Digital and Analog-I/O, Servo and I2C. In addition to that ConfigurableFirmata supports 1-Wire and Stepper-motors.

    - + You can find StandardFirmata in the Arduino-IDE under 'Examples->Firmata->StandardFirmata

    ConfigurableFirmata has to be installed manualy. See ConfigurableFirmata on GitHub or FHEM-Wiki
    @@ -1153,7 +1566,7 @@ sub FRM_OWX_Discover ($) { Set
    • - set <name> init
      + set <name> reinit
      reinitializes the FRM-Client-devices configured for this Arduino

    • @@ -1166,22 +1579,57 @@ sub FRM_OWX_Discover ($) { Attributes
        -
      • i2c-config
        +
      • resetDeviceOnConnect
        + Reset the Firmata device immediately after connect to force default Firmata startup state (default: enabled): + all pins with analog capability are configured as input, all other (digital) pins are configured as output + and the input pin reporting, the i2c configuration and the serial port configuration are cancelled. +

      • +
      • i2c-config <write-read-delay>
        Configure the Arduino for ic2 communication. This will enable i2c on the i2c_pins received by the capability-query issued during initialization of FRM.
        - As of Firmata 2.3 you can set a delay-time (in microseconds, max. 65535, default 0) that will be + As of Firmata 2.3 you can set a delay-time (in microseconds, max. 32767, default: 0) that will be inserted into i2c protocol when switching from write to read. This may be necessary because Firmata i2c write does not block on the fhem side so consecutive i2c write/read operations get queued and will be executed on the Firmata device in a different time sequence. Use the maximum operation time required by the connected i2c devices (e.g. 30000 for the BMP180 with triple oversampling, - see i2c device manufacturer documentation for details).
        - See: Firmata Protocol details about I2C
        + see i2c device manufacturer documentation for details).
        + See: Firmata Protocol details about I2C

      • -
      • sampling-interval
        - Configure the interval Firmata reports analog data to FRM (in milliseconds, max. 65535).
        - See: Firmata Protocol details about Sampling Interval
        +
      • sampling-interval <interval>
        + Configure the interval Firmata reports analog data to FRM (in milliseconds, max. 32767, default: 19 ms).
        + See: Firmata Protocol details about Sampling Interval +

      • +
      • software-serial-config <port>:<rx pin>:<tx pin>
        + For using a software serial port (port number 8, 9, 10 or 11) two io pins must be specified. + The RX pin must have interrupt capability and the TX pin must have digital output capability.
        + See: Arduino SoftwareSerial Library
      • -
      +
    +

    + + + Notes
    +
      +
    • ConfigurableFirmata
      + AnalogInputFirmata must always be enabled, even if not used. Otherwise the device setup will fail with the error Unhandled sysex command when connecting because the pin capability query cannot be completed. +
    • +
    • Serial Ports
      + Some serial devices can be connected to a serial port of a Firmata device acting as a serial over LAN adapter + if their FHEM modules are using basic DevIo (e.g. HEATRONIC in read only mode) by changing their serial device descriptor to +

      + FHEM:DEVIO:<FRM device name>:<serial port>@<baud rate>
      + The <serial port> of a pin is its <serial resolution> integer divided by two (e.g. resolution=0/1 -> serial port 0). +

      + To use a serial port both the RX and TX pin of this port must be available via Firmata, even if one of the pins will not be used. + Depending on the Firmata version the first hardware serial port (port 0) cannot be used even with network connected + devices because port 0 is always reserved for the Arduino host communication. + On some Arduinos you can use software serial ports (ports 8 to 11). FRM supports a maximum of one software serial port that can + be activated using the software-serial-config attribute. +

      + In current Firmata versions the serial options (data bits, parity, stop bits) cannot be configured but may be compiled into the + Firmata Firmware (see SerialFirmata.cpp ((HardwareSerial*)serialPort)->begin(baud, options)). +
    • +

diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 0c20ee71d..8c7ded909 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -56,7 +56,7 @@ FHEM/10_KNX.pm andi291 KNX/EIB FHEM/10_EnOcean.pm klaus-schauer EnOcean FHEM/10_EQ3BT.pm dominikkarall Sonstige Systeme FHEM/10_FBDECT.pm rudolfkoenig FRITZ!Box -FHEM/10_FRM.pm ntruchsess Sonstige Systeme +FHEM/10_FRM.pm jensb Sonstige Systeme FHEM/10_FS20.pm rudolfkoenig SlowRF FHEM/00_HXBDevice.pm neubert Sonstige Systeme FHEM/10_IT.pm dancer0705/bjoernh InterTechno