# # # 66_ECMD.pm # written by Dr. Boris Neubert 2011-01-15 # e-mail: omega at online dot de # ############################################## # $Id$ package main; use strict; use warnings; use Time::HiRes qw(gettimeofday); #sub ECMD_Attr(@); sub ECMD_Clear($); #sub ECMD_Parse($$$$$); #sub ECMD_Read($); sub ECMD_ReadAnswer($$); #sub ECMD_Ready($); sub ECMD_Write($$); sub ECMD_OpenDev($$); sub ECMD_CloseDev($); sub ECMD_SimpleWrite(@); sub ECMD_SimpleRead($); sub ECMD_Disconnected($); use vars qw {%attr %defs}; ##################################### sub ECMD_Initialize($) { my ($hash) = @_; # Provider $hash->{WriteFn} = "ECMD_Write"; #$hash->{ReadFn} = "ECMD_Read"; $hash->{Clients}= ":ECMDDevice:"; # Consumer $hash->{DefFn} = "ECMD_Define"; $hash->{UndefFn} = "ECMD_Undef"; $hash->{GetFn} = "ECMD_Get"; $hash->{SetFn} = "ECMD_Set"; $hash->{AttrFn} = "ECMD_Attr"; $hash->{AttrList}= "classdefs nonl loglevel:0,1,2,3,4,5"; } ##################################### sub ECMD_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t]+", $def); my $name = $a[0]; my $protocol = $a[2]; if(@a < 4 || @a > 4 || (($protocol ne "telnet") && ($protocol ne "serial"))) { my $msg = "wrong syntax: define ECMD telnet or define ECMD serial "; Log 2, $msg; return $msg; } ECMD_CloseDev($hash); $hash->{Protocol}= $protocol; my $devicename= $a[3]; $hash->{DeviceName} = $devicename; my $ret = ECMD_OpenDev($hash, 0); return $ret; } ##################################### sub ECMD_Undef($$) { my ($hash, $arg) = @_; my $name = $hash->{NAME}; foreach my $d (sort keys %defs) { if(defined($defs{$d}) && defined($defs{$d}{IODev}) && $defs{$d}{IODev} == $hash) { my $lev = ($reread_active ? 4 : 2); Log3 $hash, $lev, "deleting port for $d"; delete $defs{$d}{IODev}; } } ECMD_CloseDev($hash); return undef; } ##################################### sub ECMD_CloseDev($) { my ($hash) = @_; my $name = $hash->{NAME}; my $dev = $hash->{DeviceName}; return if(!$dev); if($hash->{TCPDev}) { $hash->{TCPDev}->close(); delete($hash->{TCPDev}); } elsif($hash->{USBDev}) { $hash->{USBDev}->close() ; delete($hash->{USBDev}); } ($dev, undef) = split("@", $dev); # Remove the baudrate delete($selectlist{"$name.$dev"}); delete($readyfnlist{"$name.$dev"}); delete($hash->{FD}); } ######################## sub ECMD_OpenDev($$) { my ($hash, $reopen) = @_; my $protocol = $hash->{Protocol}; my $name = $hash->{NAME}; my $devicename = $hash->{DeviceName}; $hash->{PARTIAL} = ""; Log3 $hash, 3, "ECMD opening $name (protocol $protocol, device $devicename)" if(!$reopen); if($hash->{Protocol} eq "telnet") { # This part is called every time the timeout (5sec) is expired _OR_ # somebody is communicating over another TCP connection. As the connect # for non-existent devices has a delay of 3 sec, we are sitting all the # time in this connect. NEXT_OPEN tries to avoid this problem. if($hash->{NEXT_OPEN} && time() < $hash->{NEXT_OPEN}) { return; } my $conn; eval { local $SIG{ALRM} = sub { die 'Timed Out'; }; alarm 10; $conn = IO::Socket::INET->new(PeerAddr => $devicename, timeout => 5); }; alarm 0; $conn= undef if $@; # return "Error: timeout." if ( $@ && $@ =~ /Timed Out/ ); # return "Error: Eval corrupted: $@" if $@; if($conn) { delete($hash->{NEXT_OPEN}) } else { Log3 $hash, 3, "Can't connect to $devicename: $!" if(!$reopen); $readyfnlist{"$name.$devicename"} = $hash; $hash->{STATE} = "disconnected"; $hash->{NEXT_OPEN} = time()+60; return ""; } $hash->{TCPDev} = $conn; $hash->{FD} = $conn->fileno(); delete($readyfnlist{"$name.$devicename"}); $selectlist{"$name.$devicename"} = $hash; } else { my $baudrate; ($devicename, $baudrate) = split("@", $devicename); my $po; if ($^O=~/Win/) { require Win32::SerialPort; $po = new Win32::SerialPort ($devicename); } else { require Device::SerialPort; $po = new Device::SerialPort ($devicename); } if(!$po) { return undef if($reopen); Log3 $hash, 3, "Can't open $devicename: $!"; $readyfnlist{"$name.$devicename"} = $hash; $hash->{STATE} = "disconnected"; return ""; } $hash->{USBDev} = $po; if( $^O =~ /Win/ ) { $readyfnlist{"$name.$devicename"} = $hash; } else { $hash->{FD} = $po->FILENO; delete($readyfnlist{"$name.$devicename"}); $selectlist{"$name.$devicename"} = $hash; } if($baudrate) { $po->reset_error(); Log3 $hash, 3, "ECMD setting $name baudrate to $baudrate"; $po->baudrate($baudrate); $po->databits(8); $po->parity('none'); $po->stopbits(1); $po->handshake('none'); # This part is for some Linux kernel versions whih has strange default # settings. Device::SerialPort is nice: if the flag is not defined for your # OS then it will be ignored. $po->stty_icanon(0); #$po->stty_parmrk(0); # The debian standard install does not have it $po->stty_icrnl(0); $po->stty_echoe(0); $po->stty_echok(0); $po->stty_echoctl(0); # Needed for some strange distros $po->stty_echo(0); $po->stty_icanon(0); $po->stty_isig(0); $po->stty_opost(0); $po->stty_icrnl(0); } $po->write_settings; } if($reopen) { Log3 $hash, 1, "ECMD $name ($devicename) reappeared"; } else { Log3 $hash, 3, "ECMD device opened"; } $hash->{STATE}= ""; # Allow InitDev to set the state my $ret = ECMD_DoInit($hash); if($ret) { Log3 $hash, 1, "$ret"; ECMD_CloseDev($hash); Log3 $hash, 1, "Cannot init $name ($devicename), ignoring it"; } DoTrigger($name, "CONNECTED") if($reopen); return $ret; } ##################################### sub ECMD_DoInit($) { my $hash = shift; my $name = $hash->{NAME}; my $msg = undef; ECMD_Clear($hash); #ECMD_SimpleWrite($hash, "version"); #my ($err,$version)= ECMD_ReadAnswer($hash, "version"); #return "$name: $err" if($err); #Log 2, "ECMD version: $version"; #$hash->{VERSION} = $version; $hash->{STATE} = "Initialized" if(!$hash->{STATE}); return undef; } ######################## sub ECMD_SimpleWrite(@) { my ($hash, $msg, $nonl) = @_; return if(!$hash); $msg .= "\n" unless($nonl); $hash->{USBDev}->write($msg) if($hash->{USBDev}); syswrite($hash->{TCPDev}, $msg) if($hash->{TCPDev}); select(undef, undef, undef, 0.001); } ######################## sub ECMD_SimpleRead($) { my ($hash) = @_; if($hash->{USBDev}) { return $hash->{USBDev}->input(); } if($hash->{TCPDev}) { my $buf; if(!defined(sysread($hash->{TCPDev}, $buf, 1024))) { ECMD_Disconnected($hash); return undef; } return $buf; } return undef; } ##################################### # This is a direct read for commands like get sub ECMD_ReadAnswer($$) { my ($hash, $arg) = @_; #Log 5, "ECMD reading answer for get $arg..."; return ("No FD", undef) if(!$hash || ($^O !~ /Win/ && !defined($hash->{FD}))); my ($data, $rin) = ("", ''); my $buf; my $to = 3; # 3 seconds timeout $to = $hash->{RA_Timeout} if($hash->{RA_Timeout}); # ...or less #Log 5, "Timeout is $to seconds"; for(;;) { return ("Error: device lost when reading answer for get $arg", undef) if(!$hash->{FD}); vec($rin, $hash->{FD}, 1) = 1; my $nfound = select($rin, undef, undef, $to); if($nfound < 0) { next if ($! == EAGAIN() || $! == EINTR() || $! == 0); my $err = $!; ECMD_Disconnected($hash); return("Error reading answer for get $arg: $err", undef); } return ("Error: timeout reading answer for get $arg", undef) if($nfound == 0); $buf = ECMD_SimpleRead($hash); return ("No data", undef) if(!defined($buf)); if($buf) { chomp $buf; # remove line break Log3 $hash, 5, "ECMD (ReadAnswer): $buf"; $data .= $buf; } return (undef, $data) } } ##################################### sub ECMD_SetState($$$$) { my ($hash, $tim, $vt, $val) = @_; return undef; } ##################################### sub ECMD_Clear($) { my $hash = shift; # Clear the pipe $hash->{RA_Timeout} = 0.1; for(;;) { my ($err, undef) = ECMD_ReadAnswer($hash, "clear"); last if($err && $err =~ m/^Error/); } delete($hash->{RA_Timeout}); } ##################################### sub ECMD_Disconnected($) { my $hash = shift; my $dev = $hash->{DeviceName}; my $name = $hash->{NAME}; return if(!defined($hash->{FD})); # Already deleted o Log3 $hash, 1, "$dev disconnected, waiting to reappear"; ECMD_CloseDev($hash); $readyfnlist{"$name.$dev"} = $hash; # Start polling $hash->{STATE} = "disconnected"; # Without the following sleep the open of the device causes a SIGSEGV, # and following opens block infinitely. Only a reboot helps. sleep(5); DoTrigger($name, "DISCONNECTED"); } ##################################### sub ECMD_Get($@) { my ($hash, @a) = @_; return "get needs at least one parameter" if(@a < 2); my $name = $a[0]; my $cmd= $a[1]; my $arg = ($a[2] ? $a[2] : ""); my @args= @a; shift @args; shift @args; my ($msg, $err); return "No get $cmd for dummies" if(IsDummy($name)); if($cmd eq "raw") { return "get raw needs an argument" if(@a< 3); my $nonl= AttrVal($name, "nonl", 0); my $ecmd= join " ", @args; Log3 $hash, 5, $ecmd; ECMD_SimpleWrite($hash, $ecmd, $nonl); ($err, $msg) = ECMD_ReadAnswer($hash, "raw"); return $err if($err); } else { return "get $cmd: unknown command "; } $hash->{READINGS}{$cmd}{VAL} = $msg; $hash->{READINGS}{$cmd}{TIME} = TimeNow(); return "$name $cmd => $msg"; } ##################################### sub ECMD_EvalClassDef($$$) { my ($hash, $classname, $filename)=@_; my $name= $hash->{NAME}; # refuse overwriting existing definitions if(defined($hash->{fhem}{classDefs}{$classname})) { my $err= "$name: class $classname is already defined."; Log3 $hash, 1, $err; return $err; } # try and open the class definition file if(!open(CLASSDEF, $filename)) { my $err= "$name: cannot open file $filename for class $classname."; Log3 $hash, 1, $err; return $err; } my @classdef= ; close(CLASSDEF); # add the class definition Log3 $hash, 5, "$name: adding new class $classname from file $filename"; $hash->{fhem}{classDefs}{$classname}{filename}= $filename; # format of the class definition: # params parameters for device definition # get cmd {} defines a get command # get params parameters for get command # set cmd {} defines a set command # set params parameters for get command # all lines are optional # # eaxmple class definition 1: # get adc cmd {"adc get %channel"} # get adc params channel # # eaxmple class definition 1: # params btnup btnstop btndown # set up cmd {"io set ddr 2 ff\nio set port 2 1%btnup\nwait 1000\nio set port 2 00"} # set stop cmd {"io set ddr 2 ff\nio set port 2 1%btnstop\nwait 1000\nio set port 2 00"} # set down cmd {"io set ddr 2 ff\nio set port 2 1%btndown\nwait 1000\nio set port 2 00"} my $cont= ""; foreach my $line (@classdef) { # kill trailing newline chomp $line; # kill comments and blank lines $line=~ s/\#.*$//; $line=~ s/\s+$//; $line= $cont . $line; if($line=~ s/\\$//) { $cont= $line; undef $line; } next unless($line); $cont= ""; Log3 $hash, 5, "$name: evaluating >$line<"; # split line into command and definition my ($cmd, $def)= split("[ \t]+", $line, 2); if($cmd eq "nonl") { Log3 $hash, 5, "$name: no newline"; $hash->{fhem}{classDefs}{$classname}{nonl}= 1; } elsif($cmd eq "params") { Log3 $hash, 5, "$name: parameters are $def"; $hash->{fhem}{classDefs}{$classname}{params}= $def; } elsif($cmd eq "set" || $cmd eq "get") { my ($cmdname, $spec, $arg)= split("[ \t]+", $def, 3); if($spec eq "params") { if($cmd eq "set") { Log3 $hash, 5, "$name: set $cmdname has parameters $arg"; $hash->{fhem}{classDefs}{$classname}{sets}{$cmdname}{params}= $arg; } elsif($cmd eq "get") { Log3 $hash, 5, "$name: get $cmdname has parameters $arg"; $hash->{fhem}{classDefs}{$classname}{gets}{$cmdname}{params}= $arg; } } elsif($spec eq "cmd") { if($arg !~ m/^{.*}$/s) { Log3 $hash, 1, "$name: command for $cmd $cmdname is not a perl command."; next; } $arg =~ s/^(\\\n|[ \t])*//; # Strip space or \\n at the beginning $arg =~ s/[ \t]*$//; if($cmd eq "set") { Log3 $hash, 5, "$name: set $cmdname command defined as $arg"; $hash->{fhem}{classDefs}{$classname}{sets}{$cmdname}{cmd}= $arg; } elsif($cmd eq "get") { Log3 $hash, 5, "$name: get $cmdname command defined as $arg"; $hash->{fhem}{classDefs}{$classname}{gets}{$cmdname}{cmd}= $arg; } } elsif($spec eq "postproc") { if($arg !~ m/^{.*}$/s) { Log3 $hash, 1, "$name: postproc command for $cmd $cmdname is not a perl command."; next; } $arg =~ s/^(\\\n|[ \t])*//; # Strip space or \\n at the beginning $arg =~ s/[ \t]*$//; if($cmd eq "set") { Log3 $hash, 5, "$name: set $cmdname postprocessor defined as $arg"; $hash->{fhem}{classDefs}{$classname}{sets}{$cmdname}{postproc}= $arg; } elsif($cmd eq "get") { Log3 $hash, 5, "$name: get $cmdname postprocessor defined as $arg"; $hash->{fhem}{classDefs}{$classname}{gets}{$cmdname}{postproc}= $arg; } } } else { Log3 $hash, 1, "$name: illegal tag $cmd for class $classname in file $filename."; } } # store class definitions in attribute $attr{$name}{classdefs}= ""; my @a; foreach my $c (keys %{$hash->{fhem}{classDefs}}) { push @a, "$c=$hash->{fhem}{classDefs}{$c}{filename}"; } $attr{$name}{"classdefs"}= join(":", @a); return undef; } ##################################### sub ECMD_Attr($@) { my @a = @_; my $hash= $defs{$a[1]}; if($a[0] eq "set" && $a[2] eq "classdefs") { my @classdefs= split(/:/,$a[3]); delete $hash->{fhem}{classDefs}; foreach my $classdef (@classdefs) { my ($classname,$filename)= split(/=/,$classdef,2); ECMD_EvalClassDef($hash, $classname, $filename); } } return undef; } ##################################### sub ECMD_Reopen($) { my ($hash) = @_; ECMD_CloseDev($hash); ECMD_OpenDev($hash, 1); return undef; } ##################################### sub ECMD_Set($@) { my ($hash, @a) = @_; my $name = $a[0]; # usage check #my $usage= "Usage: set $name classdef OR set $name reopen"; my $usage= "Unknown argument $a[1], choose one of reopen classdef"; if((@a == 2) && ($a[1] eq "reopen")) { return ECMD_Reopen($hash); } return $usage if(@a != 4); return $usage if($a[1] ne "classdef"); # from the definition my $classname= $a[2]; my $filename= $a[3]; return ECMD_EvalClassDef($hash, $classname, $filename); } ##################################### sub ECMD_Write($$) { my ($hash,$msg) = @_; my $answer; my @r; my @ecmds= split "\n", $msg; my $nonl= AttrVal($hash->{NAME}, "nonl", 0); foreach my $ecmd (@ecmds) { Log3 $hash, 5, "$hash->{NAME} sending $ecmd"; ECMD_SimpleWrite($hash, $ecmd, $nonl); $answer= ECMD_ReadAnswer($hash, "$ecmd"); if(defined($answer)) { push @r, $answer; Log3 $hash, 5, $answer; } else { Log3 $hash, 5, "no answer received"; } } return join("\n", @r) unless($#r<0); return undef; } ##################################### 1; =pod =begin html

ECMD

    Any physical device with request/response-like communication capabilities over a TCP connection can be defined as ECMD device. A practical example of such a device is the AVR microcontroller board AVR-NET-IO from Pollin with ECMD-enabled Ethersex firmware.

    A physical ECMD device can host any number of logical ECMD devices. Logical devices are defined as ECMDDevices in fhem. ADC 0 to 3 and I/O port 0 to 3 of the above mentioned board are examples of such logical devices. ADC 0 to 3 all belong to the same device class ADC (analog/digital converter). I/O port 0 to 3 belong to the device class I/O port. By means of extension boards you can make your physical device drive as many logical devices as you can imagine, e.g. IR receivers, LC displays, RF receivers/transmitters, 1-wire devices, etc.

    Defining one fhem module for any device class would create an unmanageable number of modules. Thus, an abstraction layer is used. You create a device class on the fly and assign it to a logical ECMD device. The class definition names the parameters of the logical device, e.g. a placeholder for the number of the ADC or port, as well as the get and set capabilities. Worked examples are to be found in the documentation of the ECMDDevice device.

    Note: this module requires the Device::SerialPort or Win32::SerialPort module if the module is connected via serial Port or USB.

    Define

      define <name> ECMD telnet <IPAddress:Port>

      or

      define <name> ECMD serial <SerialDevice>[<@BaudRate>]

      Defines a physical ECMD device. The keywords telnet or serial are fixed.

      Examples:
        define AVRNETIO ECMD telnet 192.168.0.91:2701
        define AVRNETIO ECMD serial /dev/ttyS0
        define AVRNETIO ECMD serial /sev/ttyUSB0@38400

    Set
      set <name> classdef <classname> <filename>

      Creates a new device class <classname> for logical devices. The class definition is in the file <filename>. You must create the device class before you create a logical device that adheres to that definition.

      Example:
        define AVRNETIO classdef /etc/fhem/ADC.classdef

      set <name> reopen

      Closes and reopens the device. Could be handy if connection is lost and cannot be reestablished automatically.

    Get
      get <name> raw <command>

      Sends the command <command> to the physical ECMD device <name> and reads the response.


    Attributes

    • classdefs
      A colon-separated list of <classname>=<filename>. The list is automatically updated if a class definition is added. You can directly set the attribute.
    • nonl
      A newline (\n) is automatically appended to every command string sent to the device unless this attribute is set. Please note that newlines (\n) in a command string are interpreted as separators to split the command string into several commands and are never literally sent.


    Class definition

      The class definition for a logical ECMD device class is contained in a text file. The text file is made up of single lines. Empty lines and text beginning with # (hash) are ignored. Therefore make sure not to use hashes in commands.
      The following commands are recognized in the device class definition:

      • params <parameter1> [<parameter2> [<parameter3> ... ]]

        Declares the names of the named parameters that must be present in the definition of the logical ECMD device.

      • set <commandname> cmd { <perl special> }

        Declares a new set command <commandname>.

      • get <commandname> cmd { <perl special> }

        Declares a new get command <commandname>.

      • set <commandname> postproc { <perl command> }
        get <commandname> postproc { <perl command> }

        Declares a postprocessor for the command <commandname>.

      • set <commandname> params <parameter1> [<parameter2> [<parameter3> ... ]]
        get <commandname> params <parameter1> [<parameter2> [<parameter3> ... ]]

        Declares the names of the named parameters that must be present in the set or get command <commandname>. Be careful not to use a parameter name that is already used in the device definition (see params above).

      The perl specials in the definitions of the set and get commands can contain macros. Apart from the rules outlined in the documentation of perl specials in fhem, the following rules apply:

      • The character @ will be replaced with the device name. To use @ in the text itself, use the double mode (@@).
      • The macro %NAME will expand to the device name (same as @).
      • The macro %<parameter> will expand to the current value of the named parameter. This can be either a parameter from the device definition or a parameter from the set or get command.
      • The macro substitution occurs before perl evaluates the expression. It is a plain text substitution.
      • If in doubt what happens, run the commands with loglevel 5 and observe the log file.


      The rules outlined in the documentation of perl specials for the <perl command> in the postprocessor definitions apply. Note: Beware of undesired side effects from e.g. doubling of semicolons! The perl command acts on $_. The result of the perl command is the final result of the get or set command.
=end html =cut