diff --git a/fhem/FHEM/20_FRM_I2C.pm b/fhem/FHEM/20_FRM_I2C.pm index 1c2cd3b01..0566ff59f 100755 --- a/fhem/FHEM/20_FRM_I2C.pm +++ b/fhem/FHEM/20_FRM_I2C.pm @@ -1,137 +1,438 @@ -############################################## +################################################################################ # $Id$ -############################################## +################################################################################ + +=encoding UTF-8 + +=head1 NAME + +FHEM module to read continuously from and to write to a I2C device connected to +a Firmata device + +=head1 LICENSE AND COPYRIGHT + +Copyright (C) 2013 ntruchess +Copyright (C) 2018 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. + +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. + +You should have received a copy of the GNU General Public License +along with this script; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +A copy of the GNU General Public License, Version 2 can also be found at + +http://www.gnu.org/licenses/old-licenses/gpl-2.0. + +This copyright notice MUST APPEAR in all copies of the script! + +=cut + 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... +use Scalar::Util qw(looks_like_number); + +#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)) { - push @INC,$inc."/lib"; - }; - }; + if (!grep(/FHEM\/lib$/,@INC)) { + foreach my $inc (grep(/FHEM$/,@INC)) { + push @INC,$inc."/lib"; + }; + }; }; -use Device::Firmata::Constants qw/ :all /; - ##################################### -sub -FRM_I2C_Initialize($) + +# number of arguments +my %sets = ( + "register" => 2, +); + +# number of arguments +my %gets = ( + "register" => 1, +); + +sub FRM_I2C_Initialize { my ($hash) = @_; - $hash->{DefFn} = "FRM_Client_Define"; - $hash->{InitFn} = "FRM_I2C_Init"; - $hash->{UndefFn} = "FRM_Client_Undef"; - $hash->{AttrFn} = "FRM_I2C_Attr"; - - $hash->{AttrList} = "IODev $main::readingFnAttributes"; + $hash->{DefFn} = "FRM_I2C_Define"; + $hash->{InitFn} = "FRM_I2C_Init"; + $hash->{UndefFn} = "FRM_I2C_Undef"; + $hash->{AttrFn} = "FRM_I2C_Attr"; + $hash->{GetFn} = "FRM_I2C_Get"; + $hash->{SetFn} = "FRM_I2C_Set"; + $hash->{I2CRecFn} = "FRM_I2C_Receive"; + + $hash->{AttrList} = "IODev $main::readingFnAttributes"; + main::LoadModule("FRM"); } -sub -FRM_I2C_Init($) +sub FRM_I2C_Define { - my ($hash,$args) = @_; - my $u = "wrong syntax: define FRM_I2C address register numbytes"; + my ($hash, $def) = @_; - return $u if(int(@$args) < 3); - - $hash->{"i2c-address"} = @$args[0]; - $hash->{"i2c-register"} = @$args[1]; - $hash->{"i2c-bytestoread"} = @$args[2]; + # verify define arguments + my $usage = "usage: define FRM_I2C address register numbytes"; - eval { - FRM_Client_AssignIOPort($hash); - FRM_Client_FirmataDevice($hash)->i2c_read(@$args[0],@$args[1],@$args[2]); - }; - if ($@) { - $@ =~ /^(.*)( at.*FHEM.*)$/; - $hash->{STATE} = "error initializing: ".$1; - return "error initializing '".$hash->{NAME}."': ".$1; - } + my @a = split("[ \t]+", $def); + return $usage if (scalar(@a) < 5); + my $args = [@a[2..scalar(@a)-1]]; - return "error calling i2c_read: ".$@ if ($@); - if (! (defined AttrVal($hash->{NAME},"event-min-interval",undef))) { - $main::attr{$hash->{NAME}}{"event-min-interval"} = 5; - } - return undef; + $hash->{I2C_Address} = @$args[0]; + $hash->{I2C_READ_REGISTER} = @$args[1]; + $hash->{I2C_READ_BYTES} = @$args[2]; + + my $ret = FRM_Client_Define($hash, $def); + if ($ret) { + return $ret; + } + return undef; } -sub -FRM_I2C_Attr($$$$) { - my ($command,$name,$attribute,$value) = @_; - my $hash = $main::defs{$name}; +sub FRM_I2C_Init +{ + my ($hash, $args) = @_; + my $name = $hash->{NAME}; + + if (defined($main::defs{$name}{IODev_ERROR})) { + return 'Perl module Device::Firmata not properly installed'; + } + + # stop reading + if ($main::init_done && defined($hash->{IODev})) { + eval { + FRM_Client_FirmataDevice($hash)->i2c_stopreading($hash->{I2C_Address}); + }; + } + + # assign IODev eval { - if ($command eq "set") { - ARGUMENT_HANDLER: { - $attribute eq "IODev" and do { - if ($main::init_done and (!defined ($hash->{IODev}) or $hash->{IODev}->{NAME} ne $value)) { - FRM_Client_AssignIOPort($hash,$value); - FRM_Init_Client($hash) if (defined ($hash->{IODev})); - } - last; - }; - } - } + FRM_Client_AssignIOPort($hash); }; if ($@) { - $@ =~ /^(.*)( at.*FHEM.*)$/; - $hash->{STATE} = "error setting $attribute to $value: ".$1; - return "cannot $command attribute $attribute to $value for $name: ".$1; + my $ret = FRM_Catch($@); + readingsSingleUpdate($hash, 'state', "error assigning IODev: $ret", 1); + return $ret; } + + # start reading + if ($hash->{I2C_READ_BYTES} > 0) { + eval { + FRM_Client_FirmataDevice($hash)->i2c_read(@$args[0], @$args[1], @$args[2]); + }; + if ($@) { + my $ret = FRM_Catch($@); + readingsSingleUpdate($hash, 'state', "error initializing periodic I2C read: $ret", 1); + return $ret; + } + } + + readingsSingleUpdate($hash, 'state', 'Initialized', 1); + + return undef; +} + +sub FRM_I2C_Undef +{ + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + # try to stop reading + eval { + FRM_Client_FirmataDevice($hash)->i2c_stopreading($hash->{I2C_Address}); + }; + + return FRM_Client_Undef($hash, $arg); +} + +sub FRM_I2C_Attr +{ + my ($command, $name, $attribute, $value) = @_; + my $hash = $main::defs{$name}; + + if (defined ($command)) { + eval { + if ($command eq "set") { + ARGUMENT_HANDLER: { + $attribute eq "IODev" and do { + if ($main::init_done) { + # stop reading on old IODev + if (defined($hash->{IODev}) && $hash->{IODev}->{NAME} ne $value) { + if (defined($main::defs{$name}{IODev_ERROR})) { + die 'Perl module Device::Firmata not properly installed'; + } + eval { + FRM_Client_FirmataDevice($hash)->i2c_stopreading($hash->{I2C_Address}); + }; + } + # assign new IODev and init FRM client + if (!defined($hash->{IODev}) || $hash->{IODev}->{NAME} ne $value) { + FRM_Client_AssignIOPort($hash, $value); + if (!defined($hash->{IODev}) || $hash->{IODev}->{NAME} ne $value) { + die "$value not valid"; + } + FRM_Init_Client($hash) if (defined ($hash->{IODev})); + } + } + last; + }; + } + } + }; + if ($@) { + my $ret = FRM_Catch($@); + $hash->{STATE} = "$command $attribute error: " . $ret; + return $hash->{STATE}; + } + } else { + return "no command specified"; + } +} + +sub FRM_I2C_Get +{ + my ($hash, $name, $cmd, @a) = @_; + + return "get command missing" if(!defined($cmd)); + return "unknown get command '$cmd', choose one of " . join(" ", sort keys %gets) if(!defined($gets{$cmd})); + my @match = grep( $_ =~ /^$cmd($|:)/, keys %gets ); + return "$cmd requires at least $gets{$match[0]} number as argument" unless (@a >= $gets{$match[0]}); + + if ($cmd eq 'register') { + my $usage = "usage: get $name register <register> [<bytes-to-read>]"; + if (scalar(@a) == 1 || scalar(@a) == 2) { + my $register = shift @a; + my $numberOfBytes = scalar(@a) == 2? shift @a : 1; + if (looks_like_number($register) && $register >= 0 && looks_like_number($numberOfBytes) && $numberOfBytes > 0) { + my $iodev = $hash->{IODev}; + my %package = (direction => 'i2cread', + i2caddress => $hash->{I2C_Address}, + reg => $register, + nbyte => $numberOfBytes + ); + eval { + CallFn($iodev->{NAME}, 'I2CWrtFn', $iodev, \%package); + }; + if ($@) { + my $ret = FRM_Catch($@); + $hash->{STATE} = "get $cmd error: " . $ret; + return $hash->{STATE}; + } + my $sendStat = $package{$iodev->{NAME} . '_SENDSTAT'}; + if (defined($sendStat) && $sendStat ne 'Ok') { + return "get $cmd $register failed: $sendStat"; + } + } else { + return $usage; + } + } else { + return $usage; + } + } + + return undef; +} + +sub FRM_I2C_Set +{ + my ($hash, $name, $cmd, @a) = @_; + + return "set command missing" if(!defined($cmd)); + return "unknown set command '$cmd', choose one of " . join(" ", sort keys %sets) if(!defined($sets{$cmd})); + my @match = grep( $_ =~ /^$cmd($|:)/, keys %sets ); + return "$cmd requires at least $sets{$match[0]} numbers as arguments" unless (@a >= $sets{$match[0]}); + + if (defined($main::defs{$name}{IODev_ERROR})) { + return 'Perl module Device::Firmata not properly installed'; + } + + if ($cmd eq 'register') { + my $usage = "usage: set $name register [ ... ]"; + my $register = shift @a; + if (looks_like_number($register) && $register >= 0 && looks_like_number($a[0]) && $a[0] >= 0) { + my $iodev = $hash->{IODev}; + my %package = (direction => 'i2cwrite', + i2caddress => $hash->{I2C_Address}, + reg => $register, + data => join(' ', @a) + ); + eval { + CallFn($iodev->{NAME}, 'I2CWrtFn', $iodev, \%package); + }; + if ($@) { + my $ret = FRM_Catch($@); + $hash->{STATE} = "set $cmd error: " . $ret; + return $hash->{STATE}; + } + my $sendStat = $package{$iodev->{NAME} . '_SENDSTAT'}; + if (defined($sendStat) && $sendStat ne 'Ok') { + return "set $cmd $register failed: $sendStat"; + } + } else { + return $usage; + } + } + + return undef; +} + +sub FRM_I2C_Receive +{ + my ($hash, $clientmsg) = @_; + my $name = $hash->{NAME}; + + my $iodevName = $hash->{IODev}->{NAME}; + my $sendStat = defined($iodevName) && defined($clientmsg->{$iodevName . '_SENDSTAT'})? $clientmsg->{$iodevName . '_SENDSTAT'} : '?'; + + if ($sendStat ne 'Ok') { + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash, 'state', "error: $sendStat", 1); + readingsEndUpdate($hash, 1); + } elsif (defined($clientmsg->{direction}) && $clientmsg->{direction} eq 'i2cread' && + defined($clientmsg->{reg}) && defined($clientmsg->{received})) { + my @raw = split(' ', $clientmsg->{received}); + my @values = split(' ', ReadingsVal($name, 'values', '')); + while (scalar(@values) < 256) { + push(@values, 0); + } + splice(@values, $clientmsg->{reg}, scalar(@raw), @raw); + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, 'values', join (' ', @values), 1); + readingsBulkUpdateIfChanged($hash, 'state', 'active', 1); + readingsEndUpdate($hash, 1); + } + + return undef; } 1; =pod + +=head1 CHANGES + + 28.12.2018 jensb + o moved I2C receive processing from FRM module to FRM_I2C module + o added I2C read function "get register" + o added I2C write function "set register" + o improve live modification of IODev + + 23.12.2018 jensb + o issue I2C stop reading command if device is initialized with zero byte count or is deleted + o updated module help + + 24.08.2020 jensb + o check for IODev install error in Init, Set, Attr and Undef + o prototypes removed + o moved define argument verification and decoding from Init to Define + + 19.10.2020 jensb + o annotaded module help of attributes for FHEMWEB + +=cut + + +=pod + +=head1 FHEM COMMANDREF METADATA + +=over + +=item device + +=item summary Firmata: read/write I2C register + +=item summary_DE Firmata: I2C Register lesen/schreiben + +=back + +=head1 INSTALLATION AND CONFIGURATION + =begin html - +

FRM_I2C


+ + Get
    - N/A
    +
  • register <register> [<bytes-to-read>]
    + request single asynchronous read of the specified number of bytes from the I2C register
    + bytes-to-read defaults to 1 if not specified, the maximum number of bytes that can be read at the same time is limited by the Firmata firmware and the I2C device capabilities +

-
+ + + Set
+
    +
  • register <register> <byte> [<byte> ... <byte>]
    + write the space separated list of byte values to the specified I2C register +
  • +

+ +
Attributes
+ +
  • IODev
    + Specify which FRM to use. Only required if there is more than one FRM-device defined. +
  • + +
  • global attributes
  • + +
  • readingFnAttributes
  • +
    + + + Readings
    +
      +
    • values
      + space separated list of 256 byte register image using decimal values - may be preset to any value, only the requested bytes will be updated +
    -
    +
    =end html + +=begin html_DE + +
    +

    FRM_I2C

    +

    + +=end html_DE + =cut