############################################## package main; use strict; use warnings; use Time::HiRes qw(gettimeofday); use Device::SerialPort; sub FHZ_Write($$$); sub FHZ_Read($); sub FHZ_ReadAnswer($$); sub FhzCrc(@); sub CheckFhzCrc($); sub XmitLimitCheck($$); my $msgstart = pack('H*', "81");# Every msg starts wit this my %gets = ( "init1" => "c9 02011f64", "init2" => "c9 02011f60", "init3" => "c9 02011f0a", "serial" => "04 c90184570208", "fhtbuf" => "04 c90185", ); my %sets = ( "time" => "c9 020161", "initHMS" => "04 c90186", "initFS20" => "04 c90196", "FHTcode" => "04 c901839e0101", "activefor"=> "xx xx", "raw" => "xx xx", ); my %setnrparam = ( "time" => 0, "initHMS" => 0, "initFS20" => 0, "FHTcode" => 1, "activefor"=> 1, "raw" => 2, ); my %codes = ( "^8501..\$" => "fhtbuf", ); my $def; my %msghist; # Used when more than one FHZ is attached my $msgcount = 0; my $xmit_limit = 163; # Maximum nr of transmissions per hour (unconfirmed). ##################################### # Note: we are a data provider _and_ a consumer at the same time sub FHZ_Initialize($) { my ($hash) = @_; # Provider $hash->{ReadFn} = "FHZ_Read"; $hash->{WriteFn} = "FHZ_Write"; $hash->{Clients} = ":FHZ:FS20:FHT:HMS:KS300:"; # Consumer $hash->{Match} = "^81..C9..0102"; $hash->{DefFn} = "FHZ_Define"; $hash->{UndefFn} = "FHZ_Undef"; $hash->{GetFn} = "FHZ_Get"; $hash->{SetFn} = "FHZ_Set"; $hash->{StateFn} = "FHZ_SetState"; $hash->{ParseFn} = "FHZ_Parse"; $hash->{AttrList}= "do_not_notify:1,0 dummy:1,0 filtertimeout repeater:1,0 " . "showtime:1,0 model:fhz1000,fhz1300 loglevel:0,1,2,3,4,5,6 ". "fhtsoftbuffer:1,0"; } ##################################### sub FHZ_Set($@) { my ($hash, @a) = @_; return "Need one to three parameter" if(@a < 2); return "Unknown argument $a[1], choose one of " . join(" ", sort keys %sets) if(!defined($sets{$a[1]})); return "Need one to three parameter" if(@a > 4); return "Wrong number of parameters for $a[1], need " . ($setnrparam{$a[1]}+2) if(@a != ($setnrparam{$a[1]} + 2)); my ($fn, $arg) = split(" ", $sets{$a[1]}); my $v = join(" ", @a); my $name = $hash->{NAME}; Log GetLogLevel($name,2), "FHZ set $v"; if($a[1] eq "activefor") { my $dhash = $defs{$a[2]}; return "device $a[2] unknown" if(!defined($dhash)); return "Cannot handle $dhash->{TYPE} devices" if($modules{FHZ}->{Clients} !~ m/:$dhash->{TYPE}:/); $dhash->{IODev} = $hash; return undef; } elsif($a[1] eq "raw") { $fn = $a[2]; $arg = $a[3]; } elsif($a[1] eq "time") { my @t = localtime; $arg .= sprintf("%02x%02x%02x%02x%02x", $t[5]%100, $t[4]+1, $t[3], $t[2], $t[1]); } elsif($a[1] eq "FHTcode") { return "invalid argument, must be hex" if(!$a[2] || $a[2] !~ m/^[A-F0-9]{2}$/); $arg .= $a[2]; } FHZ_Write($hash, $fn, $arg) if(!IsDummy("FHZ")); return undef; } ##################################### sub FHZ_Get($@) { my ($hash, @a) = @_; return "\"get FHZ\" needs only one parameter" if(@a != 2); return "Unknown argument $a[1], choose one of " . join(",", sort keys %gets) if(!defined($gets{$a[1]})); my ($fn, $arg) = split(" ", $gets{$a[1]}); my $v = join(" ", @a); my $name = $hash->{NAME}; Log GetLogLevel($name,2), "FHZ get $v"; FHZ_Write($hash, $fn, $arg) if(!IsDummy("FHZ")); my $msg = FHZ_ReadAnswer($hash, $a[1]); return $msg if(!$msg || $msg !~ /^81..c9..0102/); if($a[1] eq "serial") { $v = substr($msg, 22, 8) } elsif($a[1] eq "fhtbuf") { $v = substr($msg, 16, 2); } else { $v = substr($msg, 12); } $hash->{READINGS}{$a[1]}{VAL} = $v; $hash->{READINGS}{$a[1]}{TIME} = TimeNow(); return "$a[0] $a[1] => $v"; } ##################################### sub FHZ_SetState($$$$) { my ($hash, $tim, $vt, $val) = @_; return "Undefined value $vt" if(!defined($gets{$vt})); return undef; } ##################################### sub DoInit($) { my $name = shift; my @init; push(@init, "get $name init2"); push(@init, "get $name serial"); push(@init, "set $name initHMS"); push(@init, "set $name initFS20"); push(@init, "set $name time"); # Workaround: Sending "set 0001 00 off" after initialization to enable # the fhz1000 receiver, else we won't get anything reported. push(@init, "set $name raw 04 01010100010000"); CommandChain(3, \@init); # Reset the counter my $hash = $defs{$name}; delete($hash->{XMIT_TIME}); delete($hash->{NR_CMD_LAST_H}); } ##################################### sub FHZ_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); $hash->{STATE} = "Initialized"; delete $hash->{PortObj}; delete $hash->{FD}; my $name = $a[0]; my $dev = $a[2]; $attr{$name}{savefirst} = 1; $attr{$name}{fhtsoftbuffer} = 0; if($dev eq "none") { Log 1, "FHZ device is none, commands will be echoed only"; $attr{$name}{dummy} = 1; return undef; } Log 3, "FHZ opening FHZ device $dev"; my $po = new Device::SerialPort ($dev); return "Can't open $dev: $!\n" if(!$po); Log 3, "FHZ opened FHZ device $dev"; $po->reset_error(); $po->baudrate(9600); $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); $hash->{PortObj} = $po; $hash->{FD} = $po->FILENO; $hash->{DeviceName} = $dev; $hash->{PARTIAL} = ""; DoInit($name); return undef; } ##################################### sub FHZ_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) { Log GetLogLevel($name,2), "deleting port for $d"; delete $defs{$d}{IODev}; } } $hash->{PortObj}->close() if($hash->{PortObj}); return undef; } ##################################### sub FHZ_Parse($$) { my ($hash,$msg) = @_; my $omsg = $msg; $msg = substr($msg, 12); # The first 12 bytes are not really interesting my $type = ""; my $name = $hash->{NAME}; foreach my $c (keys %codes) { if($msg =~ m/$c/) { $type = $codes{$c}; last; } } if(!$type) { Log 4, "FHZ $name unknown: $omsg"; $def->{CHANGED}[0] = "$msg"; return $hash->{NAME}; } if($type eq "fhtbuf") { $msg = substr($msg, 4, 2); } Log 4, "FHZ $name $type: $msg"; $def->{CHANGED}[0] = "$type: $msg"; return $hash->{NAME}; } ##################################### sub FhzCrc(@) { my $sum = 0; map { $sum += $_; } @_; return $sum & 0xFF; } ##################################### sub CheckFhzCrc($) { my $msg = shift; return 0 if(length($msg) < 8); my @data; for(my $i = 8; $i < length($msg); $i += 2) { push(@data, ord(pack('H*', substr($msg, $i, 2)))); } my $crc = hex(substr($msg, 6, 2)); # FS20 Repeater generate a CRC which is one or two greater then the computed # one. The FHZ1000 filters such pakets, so we do not see them return (($crc eq FhzCrc(@data)) ? 1 : 0); } ##################################### # This is a direct read for commands like get sub FHZ_ReadAnswer($$) { my ($hash,$arg) = @_; return undef if(!$hash || !defined($hash->{FD})); my ($mfhzdata, $rin) = ("", ''); for(;;) { vec($rin, $hash->{FD}, 1) = 1; my $nfound = select($rin, undef, undef, 3); # 3 seconds timeout if($nfound < 0) { next if ($! == EAGAIN() || $! == EINTR() || $! == 0); die("Select error $nfound / $!\n"); } return "Timeout reading answer for get $arg" if($nfound == 0); my $buf = $hash->{PortObj}->input(); Log 5, "FHZ/RAW: " . unpack('H*',$buf); $mfhzdata .= $buf; next if(length($mfhzdata) < 2); my $len = ord(substr($mfhzdata,1,1)) + 2; if($len>20) { Log 4, "Oversized message (" . unpack('H*',$mfhzdata) . "), dropping it ..."; return undef; } return unpack('H*', $mfhzdata) if(length($mfhzdata) == $len); } } ############## sub FHZ_CompleteMsg($$) { my ($fn,$msg) = @_; my $len = length($msg); my @data; for(my $i = 0; $i < $len; $i += 2) { push(@data, ord(pack('H*', substr($msg, $i, 2)))); } return pack('C*', 0x81, $len/2+2, ord(pack('H*',$fn)), FhzCrc(@data), @data); } ##################################### # Check if the 1% limit is reached and trigger notifies sub XmitLimitCheck($$) { my ($hash,$bstring) = @_; my $now = time(); $bstring = unpack('H*', $bstring); return if($bstring =~ m/c90185$/); # fhtbuf if(!$hash->{XMIT_TIME}) { $hash->{XMIT_TIME}[0] = $now; $hash->{NR_CMD_LAST_H} = 1; return; } my $nowM1h = $now-3600; my @b = grep { $_ > $nowM1h } @{$hash->{XMIT_TIME}}; if(@b > $xmit_limit) { my $me = $hash->{NAME}; Log GetLogLevel($me,2), "FHZ TRANSMIT LIMIT EXCEEDED"; DoTrigger($me, "TRANSMIT LIMIT EXCEEDED"); } else { push(@b, $now); } $hash->{XMIT_TIME} = \@b; $hash->{NR_CMD_LAST_H} = int(@b); } ##################################### sub FHZ_Write($$$) { my ($hash,$fn,$msg) = @_; if(!$hash || !defined($hash->{PortObj})) { Log 5, "FHZ device $hash->{NAME} is not active, cannot send"; return; } ############### # insert value into the msghist. At the moment this only makes sense for FS20 # devices. As the transmitted value differs from the received one, we have to # recompute. if($fn eq "04" && substr($msg,0,6) eq "010101") { my $nmsg = "0101a001" . substr($msg, 6, 6) . "00" . substr($msg, 12); $msghist{$msgcount}{TIME} = gettimeofday(); $msghist{$msgcount}{NAME} = $hash->{NAME}; $msghist{$msgcount}{MSG} = unpack('H*', FHZ_CompleteMsg($fn, $nmsg)); $msgcount++; } my $bstring = FHZ_CompleteMsg($fn, $msg); Log 5, "Sending " . unpack('H*', $bstring); if(!$hash->{QUEUECNT}) { XmitLimitCheck($hash,$bstring); $hash->{PortObj}->write($bstring); ############## # Write the next buffer not earlier than 0.22 seconds (= 65.6ms + 10ms + # 65.6ms + 10ms + 65.6ms), else it will be discarded by the FHZ1X00 PC InternalTimer(gettimeofday()+0.25, "FHZ_HandleWriteQueue", $hash, 1); } elsif($hash->{QUEUECNT} == 1) { $hash->{QUEUE} = [ $bstring ]; } else { push(@{$hash->{QUEUE}}, $bstring); } $hash->{QUEUECNT}++; } ##################################### sub FHZ_HandleWriteQueue($) { my $hash = shift; my $cnt = --$hash->{QUEUECNT}; if($cnt > 0) { my $bstring = shift(@{$hash->{QUEUE}}); XmitLimitCheck($hash,$bstring); $hash->{PortObj}->write($bstring); InternalTimer(gettimeofday()+0.25, "FHZ_HandleWriteQueue", $hash, 1); } } ##################################### sub FHZ_Read($) { my ($hash) = @_; my $buf = $hash->{PortObj}->input(); my $iohash = $modules{$hash->{TYPE}}; my $name = $hash->{NAME}; ########### # Lets' try again: Some drivers return len(0) on the first read... if(defined($buf) && length($buf) == 0) { $buf = $hash->{PortObj}->input(); } if(!defined($buf) || length($buf) == 0) { my $devname = $hash->{DeviceName}; Log 1, "USB device $devname disconnected, waiting to reappear"; $hash->{PortObj}->close(); for(;;) { sleep(5); $hash->{PortObj} = new Device::SerialPort($devname); if($hash->{PortObj}) { Log 1, "USB device $devname reappeared"; $hash->{FD} = $hash->{PortObj}->FILENO; DoInit($name); return; } } } my $fhzdata = $hash->{PARTIAL}; Log 5, "FHZ/RAW: " . unpack('H*',$buf) . " (Unparsed: " . unpack('H*', $fhzdata) . ")"; $fhzdata .= $buf; while(length($fhzdata) > 2) { ################################### # Skip trash. my $si = index($fhzdata, $msgstart); if($si) { if($si == -1) { Log(5, "Bogus message received, no start character found"); $fhzdata = ""; last; } else { Log(5, "Bogus message received, skipping to start character"); $fhzdata = substr($fhzdata, $si); } } my $len = ord(substr($fhzdata,1,1)) + 2; if($len>20) { Log 4, "Oversized message (" . unpack('H*',$fhzdata) . "), dropping it ..."; $fhzdata = ""; next; } last if(length($fhzdata) < $len); my $dmsg = unpack('H*', substr($fhzdata, 0, $len)); if(CheckFhzCrc($dmsg)) { if(substr($fhzdata,2,1) eq $msgstart) { # Skip function 0x81 $fhzdata = substr($fhzdata, 2); next; } ############### # check for duplicate msg from different FHZ's my $now = gettimeofday(); my $skip; my $meetoo = ($attr{$name}{repeater} ? 1 : 0); my $to = 3; if(defined($attr{$name}) && defined($attr{$name}{filtertimeout})) { $to = $attr{$name}{filtertimeout}; } foreach my $oidx (keys %msghist) { if($now-$msghist{$oidx}{TIME} > $to) { delete($msghist{$oidx}); next; } if($msghist{$oidx}{MSG} eq $dmsg && ($meetoo || $msghist{$oidx}{NAME} ne $name)) { Log 5, "Skipping $msghist{$oidx}{MSG}"; $skip = 1; } } goto NEXTMSG if($skip); $msghist{$msgcount}{TIME} = $now; $msghist{$msgcount}{NAME} = $name; $msghist{$msgcount}{MSG} = $dmsg; $msgcount++; my @found; my $last_module; foreach my $m (sort { $modules{$a}{ORDER} cmp $modules{$b}{ORDER} } keys %modules) { next if($iohash->{Clients} !~ m/:$m:/); next if($dmsg !~ m/$modules{$m}{Match}/i); no strict "refs"; @found = &{$modules{$m}{ParseFn}}($hash,$dmsg); use strict "refs"; $last_module = $m; last if(int(@found)); } if(!int(@found)) { Log 1, "Unknown code $dmsg, help me!"; goto NEXTMSG; } goto NEXTMSG if($found[0] eq ""); # Special return: Do not notify if($found[0] =~ m/^(UNDEFINED) ([^ ]*) (.*)$/) { my $d = $1; $defs{$d}{NAME} = $1; $defs{$d}{TYPE} = $last_module; DoTrigger($d, "$2 $3"); delete $defs{$d}; goto NEXTMSG; } foreach my $found (@found) { DoTrigger($found, undef); } NEXTMSG: $fhzdata = substr($fhzdata, $len); } else { Log 4, "Bad CRC message, skipping it (Bogus message follows)"; $fhzdata = substr($fhzdata, 2); } } $hash->{PARTIAL} = $fhzdata; } 1;