2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-02-25 16:05:19 +00:00

98_Modbus.pm: fix memory leak as relay or slave

git-svn-id: https://svn.fhem.de/fhem/trunk@26497 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
StefanStrobel 2022-10-07 17:27:36 +00:00
parent baa770972d
commit 101165b526

View File

@ -112,8 +112,7 @@
# more explanations
####################
#
# if a logical device uses a serial physical device as io device, then $hash->{MODE}
# if a logical device uses a physical device as io device, then $hash->{MODE}
# is copied to the physical device and locks this device into this mode.
#
# $hash->{PROTOCOL} can be RTU, ASCII or TCP
@ -124,19 +123,18 @@
# phys connection proto mode on physical device
#
# serial rtu / ascii master and slave at same time not working,
# serial/tcp rtu / ascii master and slave at same time not working,
# slave can not hear master / only one master per line
# also master and passive at sime time does not make sense
# also slave and passive is useless
# so if one logical device is passive, physical device can be locked passive
#
# if one is master or slave, physical can be set to same
#
# serial rtu / ascii passive possible, physical then can also be locked.
#
# serial tcp nonsense
#
# tcp rtu / ascii passive not possible, only master / slave. phys = logocal
# tcp rtu / ascii passive not possible, only master / slave. phys = logical
# tcp same.
# so when definig / assigning iodev, mode can be locked on physical side.
@ -150,14 +148,12 @@
# NotifyFn is triggered at INITIALIZED, REREADCFG and MODIFIED.
# here ModbusLD_GetIOHash($hash) is called where everything should happen (call register etc.)
#
# for enable after disable on physical side everything is done. On logical side GetIOHash is called again.
# for attr IODev SetIODev is called
# ReadyFn also doesnt change anything regarding IODev / Registration
# So mainly things are handled after a define / initialized which triggers NotifyFn for every device
# Notify calls GetIoHash which calls SetIODev
#
#
# Exprs und Maps
@ -191,6 +187,7 @@ use Scalar::Util qw(looks_like_number);
use TcpServerUtils qw(:all);
use DevIo;
use FHEM::HTTPMOD::Utils qw(:all);
use Scalar::Util qw(weaken); # needed for backlinks in queue structures (see chapter 11 in PBP / memory leak reported with relays)
use Exporter ('import');
our @EXPORT_OK = qw();
@ -267,7 +264,7 @@ BEGIN { # functions / variables needed from package main
};
my $Module_Version = '4.4.04 - 17.7.2021';
my $Module_Version = '4.4.11 - 5.10.2022';
my $PhysAttrs = join (' ',
'queueDelay',
@ -283,14 +280,15 @@ my $PhysAttrs = join (' ',
'openTimeout',
'nextOpenDelay',
'nextOpenDelay2',
'maxTimeoutsToReconnect', # for Modbus over TCP/IP only
'maxTimeoutsToReconnect', # for Modbus over TCP/IP only
'skipGarbage:0,1',
'requestDelay', # for debugging / testing / simulations
'timeoutLogLevel:3,4',
'closeAfterResponse:0,1', # for Modbus over TCP/IP only
'closeAfterResponse:0,1', # for Modbus over TCP/IP only
'silentReconnect:0,1');
my $LogAttrs = join (' ',
'IODev', # fhem.pl macht dann $hash->{IODev} = $defs{$ioname}
'IODev', # fhem.pl macht dann $hash->{IODev} = $defs{$ioname}
'queueMax',
'alignTime',
'enableControlSet:0,1',
@ -329,7 +327,7 @@ my $ObjAttrs = join (' ',
'obj-[cdih][0-9]+-expr',
'obj-[cdih][0-9]+-ignoreExpr',
'obj-[cdih][0-9]+-format',
'obj-[ih][0-9]+-type',
'obj-[cdih][0-9]+-type',
'obj-[cdih][0-9]+-showGet',
'obj-[cdih][0-9]+-allowWrite',
'obj-[cdih][0-9]+-group',
@ -382,7 +380,8 @@ my $DevAttrs = join (' ',
'dev-type-[A-Za-z0-9_]+-set',
'dev-timing-timeout',
'dev-timing-serverTimeout',
'dev-timing-serverTimeout',
'dev-timing-serverTimeoutAbs', # just for testing
'dev-timing-sendDelay',
'dev-timing-commDelay');
@ -557,8 +556,8 @@ sub DefineFn {
$ioHash->{DeviceName} = $dev; # needed by DevIo to get Device, Port, Speed etc.
$ioHash->{IODev} = $ioHash; # point back to self to make getIOHash easier
$ioHash->{NOTIFYDEV} = 'global'; # NotifyFn nur aufrufen wenn global events (INITIALIZED)
DoClose($ioHash);
DoClose($ioHash, 1); # close, set Expect, clear Buffer, but don't set state to disconnected
Log3 $name, 3, "$name: defined as $dev";
return; # open is done later from NOTIFY
}
@ -638,8 +637,8 @@ sub DefineLDFn {
$hash->{PROTOCOL} = $proto // 'RTU';
Log3 $name, 3, "$name: defined $mode with id $id, protocol $hash->{PROTOCOL}" . ($logInfo // '');
# for Modbus TCP physical hash = logical has so MODE is set for physical device as well.
# for Modbus over serial lines this is set when IODev Attr and GetIOHash is called
# for Modbus TCP physical hash = logical hash so MODE is set for physical device as well.
# for Modbus over IODev this is set when IODev Attr and GetIOHash is called
# or later when it is needed and GetIOHash is called
# for TCP $id is an optional Unit ID that is ignored by most devices
@ -694,16 +693,18 @@ sub UndefFn {
my $name = $ioHash->{NAME};
# device is already in the process of being deleted so we should not issue commandDelete inside _Close again
DoClose($ioHash,1 ,1) if (IsOpen($ioHash)); # close, set Expect, clear Buffer, don't set state, don't delete yet
# dont check isOpen, call close even if closed and potentially on readyfn list
DoClose($ioHash, {NODELETE => 1});
# lösche auch die Verweise aus logischen Modulen auf dieses physische.
foreach my $d (keys %{$ioHash->{defptr}}) {
foreach my $d (keys %{$ioHash->{defptr}}) { # go through all logical devices using this physical
Log3 $name, 3, "$name: Undef is removing IO device for $d";
my $lHash = $defs{$d};
delete $lHash->{IODev} if ($lHash);
UpdateTimer($lHash, \&Modbus::GetUpdate, 'stop');
delete $lHash->{IODev} if ($lHash); # remove IODev entry at logical device
UpdateTimer($lHash, \&Modbus::GetUpdate, 'stop'); # stop update timer of logical device
}
Profiler($ioHash, 'Idle'); # set category to book following time, can be Delay, Fhem, Idle, Read, Send or Wait
Profiler($ioHash, 'Idle'); # category to book time, Delay, Fhem, Idle, Read, Send or Wait
#Log3 $name, 3, "$name: _UnDef done";
return;
}
@ -716,15 +717,20 @@ sub UndefLDFn {
my $hash = shift;
my $arg = shift;
my $name = $hash->{NAME};
Log3 $name, 3, "$name: _UnDef is closing $name";
Log3 $name, 3, "$name: _UnDef is preparing $name for deletion";
UnregAtIODev($hash);
# device is already in the process of being deleted so we should not issue commandDelete inside _Close again
DoClose($hash,1 ,1) if (IsOpen($hash)); # close, set Expect, clear Buffer, don't set state, don't delete yet
UpdateTimer($hash, \&Modbus::GetUpdate, 'stop');
delete $hash->{PROTOCOL}; # just in case somebody keeps a pointer to our hash ...
# dont check isOpen, close even if closed and potentially on readyfn list
DoClose($hash, {NODELETE => 1});
UpdateTimer($hash, \&Modbus::GetUpdate, 'stop');
RemoveInternalTimer ("scan:$name"); # scan timer for logical devices
# other timers are stopped in DoClose
delete $hash->{PROTOCOL}; # fix memory leak
delete $hash->{MODE};
delete $hash->{IODev};
delete $hash->{CHILDOF};
return;
}
@ -741,12 +747,14 @@ sub AttrFn {
Log3 $name, 5, "$name: attr $cmd $aName" . (defined($aVal) ? ", $aVal" : "");
if ($aName eq 'disable' && $init_done) { # only after init_done, otherwise see NotifyFN
# disable on a physical serial device
# disable on a physical device
if ($cmd eq "set" && $aVal) {
Log3 $name, 3, "$name: attr disable set" . (IsOpen($hash) ? ", closing connection" : "");
# todo: call setstates here? would call DoClose, stop timers
DoClose($hash); # close, set Expect, clear Buffer, set state to disconnected
UpdateTimer($hash, \&Modbus::GetUpdate, 'stop');
}
UpdateTimer($hash, \&Modbus::GetUpdate, 'stop'); # even here for physical device?
# other timers are stopped in DoClose
}
elsif ($cmd eq 'del' || ($cmd eq 'set' && !$aVal)) {
Log3 $name, 3, "$name: attr disable removed";
DoOpen($hash) if (!AttrVal($name, 'closeAfterResponse', 0));
@ -878,61 +886,17 @@ sub AttrLDFn {
if ($aName eq 'disable' && $init_done) { # if not init_done, nothing to be done here (see NotifyFN)
if ($cmd eq "set" && $aVal) { # disable set on a logical device (not physical serial here!)
SetLDInactive($hash);
SetStates($hash, 'disabled');
SetStates($hash, 'disabled'); # set state, close / stop timers
}
elsif ($cmd eq 'del' || ($cmd eq 'set' && !$aVal)) { # disable removed / cleared
Log3 $name, 3, "$name: attr disable removed";
SetLDActive($hash);
SetStates($hash, 'enabled'); # don't check attr disable (not cleared yet) and set to active temporarily
SetStates($hash, 'enabled'); # set state, open / start update timer
}
}
return;
}
######################################################
# set the logical device to inactive, close IO
# and stop timer
sub SetLDInactive {
my $hash = shift;
my $name = $hash->{NAME};
if ($hash->{TCPConn}) { # Modbus over TCP connection
Log3 $name, 3, "$name: device is beeing set to inactive / disabled" . (IsOpen($hash) ? ", closing TCP connection" : "");
DoClose($hash); # close, set Expect, clear Buffer, set state to disconnected
UpdateTimer($hash, \&Modbus::GetUpdate, 'stop');
}
else { # connection via serial io device
UnregAtIODev($hash); # unregister at physical device because logical device is disabled
}
UpdateTimer($hash, \&Modbus::GetUpdate, 'stop');
return;
}
######################################################
# activate the logical device, reopen, set timer
sub SetLDActive {
my $hash = shift;
my $name = $hash->{NAME};
if ($hash->{TCPConn}) { # Modbus over TCP connection
if (!IsOpen($hash)) {
DoOpen($hash) if !AttrVal($name, "closeAfterResponse", 0);
}
}
else {
my $ioHash = GetIOHash($hash); # get ioHash / check compatibility and set / register if necessary
Log3 $name, 3, "$name: " . ($ioHash ? "using $ioHash->{NAME}" : "no IODev") . " for communication";
}
if ($hash->{MODE} && $hash->{MODE} eq 'master') {
UpdateTimer($hash, \&Modbus::GetUpdate, 'start'); # set / change timer
}
return;
}
###########################################################################
# called from get / set if $hash->{'.updateSetGet'} is set
# which is done in define and attr
@ -1121,7 +1085,9 @@ sub SetLDFn {
my $fCode = GetFC($hash, {TYPE => $type, ADR => $adr, LEN => $len, OPERATION => 'write'});
my $ioHash = GetIOHash($hash); # ioHash has been checked in GetSetChecks above already
DoRequest($hash, {TYPE => $type, ADR => $adr, LEN => $len, OPERATION => 'write', VALUES => $packedVal, FORCE => !$async, DBGINFO => "set $setName"});
StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0}); # call processRequestQueue at next possibility (others waiting?)
# StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0}); # call processRequestQueue at next possibility (others waiting?)
# DoRequest should call QueueRequest which calls StartQueueTimer without delay ...
if (!$async) {
my $err = ReadAnswer($ioHash);
return $err if ($err);
@ -1139,7 +1105,7 @@ sub SetLDFn {
########################################################################
# SET command - handle predefined control sets fpr logical device
# SET command - handle predefined control sets for logical device
sub ControlSet {
my $hash = shift;
my $setName = shift;
@ -1148,7 +1114,7 @@ sub ControlSet {
if ($setName eq 'interval') {
return 'set interval is only allowed when Fhem is Modbus master' if ($hash->{MODE} ne 'master');
if (!$setVal || $setVal !~ m{ \A [0-9.]+ (\.[0-9]+)? \z}xms ) {
if (!defined($setVal) || $setVal !~ m{ \A [0-9.]+ (\.[0-9]+)? \z}xms ) {
Log3 $name, 3, "$name: set interval $setVal not valid";
Log3 $name, 3, "$name: continuing with $hash->{Interval} (sec)" if ($hash->{Interval});
return 'No valid Interval specified';
@ -1186,14 +1152,12 @@ sub ControlSet {
}
if ($setName eq 'active' && AttrVal($name, 'enableSetInactive', 1) ) {
return 'device is disabled' if (AttrVal($name, 'disable', 0));
SetStates($hash, 'active');
SetLDActive($hash);
SetStates($hash, 'active'); # set state, open / start update timer
return '0';
}
if ($setName eq 'inactive' && AttrVal($name, 'enableSetInactive', 1)) {
return 'device is disabled' if (AttrVal($name, 'disable', 0));
SetStates($hash, 'inactive');
SetLDInactive($hash);
SetStates($hash, 'inactive'); # set state, close / stop timers
return '0';
}
if ($setName eq 'stop') {
@ -1583,7 +1547,7 @@ sub DoOpen {
my $caller = FhemCaller();
if ($hash->{DeviceName} eq 'none') {
Log3 $name, 5, "$name: open called from $caller, device is defined with none" if ($caller ne 'Ready');
Log3 $name, 5, "$name: open called from $caller, device is defined with none" if ($caller ne 'ReadyFn');
SetStates($hash, 'opened');
}
elsif (!$hash->{TCPConn} && $hash->{TYPE} ne 'Modbus') { # only open physical devices or TCP
@ -1597,8 +1561,8 @@ sub DoOpen {
elsif ($hash->{TCPServer}) {
# Modbus slave or relay over TCP connection -> open listening port
Log3 $name, 5, "$name: Open called for listening to a TCP connection";
if ($arg_ref->{CLOSEFIRST} && IsOpen($hash)) {
DoClose($hash, 1); # close, set Expect, clear Buffer, don't set state
if ($arg_ref->{CLOSEFIRST}) {
DoClose($hash); # close and set state in case open fails
}
my ($dest, $port) = split(/[:\s]+/, $hash->{DeviceName});
my $ret = TcpServer_Open($hash, $port, $dest);
@ -1608,7 +1572,7 @@ sub DoOpen {
SetStates($hash, 'opened');
}
}
else { # normal case, physical device or TCP
else { # normal case, physical device or TCP
my $timeOt = AttrVal($name, 'openTimeout', 3);
my $delay2 = AttrVal($name, 'nextOpenDelay2', 1);
my $nextOp = $hash->{NEXT_OPEN} // 0;
@ -1622,11 +1586,12 @@ sub DoOpen {
Log3 $name, 3, "$name: open - stop waiting for callback and reset the BUSY flag.";
$hash->{BUSY_OPENDEV} = 0;
}
if ($arg_ref->{CLOSEFIRST} && IsOpen($hash)) { # close first and already open
if ($arg_ref->{CLOSEFIRST}) { # close first and already open
Log3 $name, 5, "$name: Open called for DevIo connection - closing first";
DoClose($hash, 1); # close, set Expect, clear Buffer, don't set state to disconnected
DoClose($hash); # close and set state in case open fails
delete $hash->{DevIoJustClosed}; # allow direct opening without further delay
} elsif ($nextOp && ($nextOp > $now)) {
}
elsif ($nextOp && ($nextOp > $now)) {
Log3 $name, 5, "$name: open ignored because DevIo has set NEXT_OPEN to $nextOp / " . FmtTimeMs($nextOp) .
" and now is $now / " . FmtTimeMs($now);
return;
@ -1655,7 +1620,10 @@ sub DoOpen {
}
Profiler($hash, 'Idle'); # set category to book following time, can be Delay, Fhem, Idle, Read, Send or Wait
ResetExpect($hash);
StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0.5, silent => 0}); # process queue in case something is waiting but delay so open can call back
# don't start queue timer until connection is actually open -> OpenCB
# StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0.5, silent => 0}); # process queue in case something is waiting but delay so open can call back
DropBuffer($hash);
delete $hash->{TIMEOUT};
return;
@ -1674,7 +1642,10 @@ sub OpenCB {
if (IsOpen($hash)) {
delete $hash->{TIMEOUTS} ;
UpdateTimer($hash, \&Modbus::GetUpdate, 'start'); # set / change timer
}
}
# stop queue-Timer while disconnected and start it again with delay 0 here
StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0, silent => 0});
return;
}
@ -1683,8 +1654,11 @@ sub OpenCB {
# close connection
# $hash is physical or both (connection over TCP)
sub DoClose {
my ($hash, $noState, $noDelete) = @_;
my $name = $hash->{NAME};
my $hash = shift;
my $arg_ref = shift // {};
my $noDelete = $arg_ref->{NODELETE} // 0;
my $keepQueue = $arg_ref->{KEEPQUEUE} // 0;
my $name = $hash->{NAME};
if (!$hash->{TCPConn} && $hash->{TYPE} ne 'Modbus') {
Log3 $name, 3, "$name: close called from " . FhemCaller() .
@ -1693,19 +1667,31 @@ sub DoClose {
}
Log3 $name, 5, "$name: Close called from " . FhemCaller() .
($noState || $noDelete ? ' with ' : '') . ($noState ? 'noState' : '') . # set state?
($noState && $noDelete ? ' and ' : '') . ($noDelete ? 'noDelete' : ''); # command delete on connection device?
($keepQueue || $noDelete ? ' with ' : '') . ($keepQueue ? 'keepQueue' : '') .
($keepQueue && $noDelete ? ' and ' : '') . ($noDelete ? 'noDelete' : '');
delete $hash->{LASTOPEN}; # reset so next open will actually call OpenDev
if ($hash->{TCPChild} && IsOpen($hash)) { # this is a slave or relay connection hash
if ($hash->{TCPChild}) { # this is a slave or relay connection hash
Log3 $name, 4, "$name: Close TCP server listening connection and delete hash";
TcpServer_Close($hash);
RemoveInternalTimer ("stimeout:$name");
CommandDelete(undef, $name) if (!$noDelete);
if ($hash->{CHILDOF} && $hash->{CHILDOF}{LASTCONN} && $hash->{CHILDOF}{LASTCONN} eq $hash->{NAME}) {
Log3 $name, 5, "$name: Close is removing lastconn from parent device $hash->{CHILDOF}{NAME}";
delete $hash->{CHILDOF}{LASTCONN}
if ($hash->{CHILDOF}){
my $parent = $hash->{CHILDOF};
delete $parent->{CONNECTHASH}{$name};
if ($parent->{LASTCONN} && $parent->{LASTCONN} eq $hash->{NAME}) {
Log3 $name, 5, "$name: Close is removing lastconn from parent device $parent->{NAME}";
delete $parent->{LASTCONN};
}
}
delete $hash->{CHILDOF}; # fix memory leak
delete $hash->{IODEV};
delete $hash->{REQUEST};
delete $hash->{RESPONSE};
delete $hash->{FRAME};
delete $hash->{READ};
delete $hash->{REMEMBER};
CommandDelete(undef, $name) if (!$noDelete); # delete tcp connection device unless called from undefFn which wants to delete later
return; # this hash is removed from %defs, attr ...
}
elsif ($hash->{TCPServer} && IsOpen($hash)) { # this is a slave or relay listening device
Log3 $name, 4, "$name: Close TCP server socket, now look for active connections";
@ -1713,7 +1699,7 @@ sub DoClose {
foreach my $conn (keys %{$hash->{CONNECTHASH}}) {
my $chash = $hash->{CONNECTHASH}{$conn};
TcpServer_Close($chash);
Log3 $chash->{NAME}, 4, "$chash->{NAME}: Close TCP server connection of parent $name and delete hash";
Log3 $chash->{NAME}, 4, "$chash->{NAME}: Close TCP server connection of parent $name";
RemoveInternalTimer ("stimeout:$chash->{NAME}");
CommandDelete(undef, $chash->{NAME}) if (!$noDelete);
}
@ -1728,14 +1714,21 @@ sub DoClose {
# close even if it was not open yet but on ready list (need to remove entry from readylist)
DevIo_CloseDev($hash);
}
SetStates($hash, 'disconnected') if (!$noState);
SetStates($hash, 'disconnected');
ResetExpect($hash);
DropBuffer($hash);
Profiler($hash, 'Idle'); # set category to book following time, can be Delay, Fhem, Idle, Read, Send or Wait
StopQueueTimer($hash, {silent => 1});
delete $hash->{IODev};
if($keepQueue) {
my $qlen = ($hash->{QUEUE} ? scalar(@{$hash->{QUEUE}}) : 0);
Log3 $name, 4, "$name: DoClose keeps queue with len $qlen";
}
else {
StopQueueTimer($hash, {silent => 1});
delete $hash->{QUEUE};
}
RemoveInternalTimer ("timeout:$name"); # remove ResponseTimeout timer when connection is closed
delete $hash->{nextTimeout};
delete $hash->{QUEUE};
return;
}
@ -1750,8 +1743,7 @@ sub ReadyFn {
if($hash->{STATE} eq 'disconnected') {
if (IsDisabled($name)) {
Log3 $name, 3, "$name: ready called but $name is disabled - don't try to reconnect - call DoClose";
DoClose($hash, 1); # close, set Expect, clear Buffer, don't set state to disconnected (must have already been done)
UpdateTimer($hash, \&Modbus::GetUpdate, 'stop');
DoClose($hash); # state sollte schon disabled sein, macht aber nichts.
return;
}
DoOpen($hash, {READY => 1}); # reopen, dont call DevIoClose before reopening
@ -1781,7 +1773,7 @@ sub HandleServerConnection {
$chash->{MODBUSID} = $hash->{MODBUSID};
$chash->{PROTOCOL} = $hash->{PROTOCOL};
$chash->{MODE} = $hash->{MODE};
$chash->{RELAY} = $hash->{RELAY};
$chash->{RELAY} = $hash->{RELAY}; # name of relay master device if this is a relay
$chash->{CHILDOF} = $hash; # point to parent device to get object definitions from there
$chash->{IODev} = $chash;
$chash->{TCPConn} = 1;
@ -1794,14 +1786,13 @@ sub HandleServerConnection {
$attr{$chash->{NAME}}{verbose} = $attr{$name}{verbose}; # copy verbose attr from parent
$hash->{LASTCONN} = $chash->{NAME}; # point from parent device to last connection device
$hash->{CONNECTHASH}{$chash->{NAME}} = $chash;
weaken $hash->{CONNECTHASH}{$chash->{NAME}};
my $room = AttrVal($name, 'connectionsRoom', 'Connections');
if ($room !~ '[Nn]one') {
CommandAttr(undef, "$chash->{NAME} room $room"); # set room
}
my $to = gettimeofday() + DevInfo($hash, 'timing', 'serverTimeout', 120);
InternalTimer($to, \&Modbus::ServerTimeout, "stimeout:$chash->{NAME}", 0);
return;
}
@ -1860,16 +1851,19 @@ sub ReadFn {
return;
}
# TCP client device connection device hash
Profiler($hash, 'Read'); # read from TCP socket
Profiler($hash, 'Read'); # read from TCP socket
$ret = sysread($hash->{CD}, $buf, 256) if ($hash->{CD});
if(!defined($ret) || $ret <= 0) { # connection closed
if(!defined($ret) || $ret <= 0) { # connection closed
Log3 $name, 3, "$name: read from TCP server connection got null -> closing";
CommandDelete(undef, $name);
return;
}
RemoveInternalTimer ("stimeout:$name");
my $to = $now + DevInfo($hash, 'timing', 'serverTimeout', 120);
InternalTimer($to, \&Modbus::ServerTimeout, "stimeout:$name", 0);
my $tA = DevInfo($hash, 'timing', 'serverTimeoutAbs', 0);
if (!$tA) {
RemoveInternalTimer ("stimeout:$name");
InternalTimer($to, \&Modbus::ServerTimeout, "stimeout:$name", 0);
}
}
else {
Profiler($hash, 'Read');
@ -1897,6 +1891,26 @@ sub ReadFn {
}
my $frame = $hash->{FRAME}; # is set after calling ParseFrameStart
# only for testing - remove later # todo: remove
my $attrsName = $name;
if ($hash->{CHILDOF}) {
$attrsName = $hash->{CHILDOF}{NAME};
}
my $requestDelay = AttrVal ($attrsName, 'requestDelay', 0);
#Log3 $name, 3, "$name: requestDelay is $requestDelay";
if ($requestDelay) {
if ($hash->{LastRequest}) {
if ($hash->{LastRequest} + $requestDelay > $now) {
Log3 $name, 3, "$name: requestDelay causes request to be ignored";
DropFrame($hash);
return;
}
}
$hash->{LastRequest} = $now;
}
# EXPECT exists on io dev. Special case for relays:
# there are two io devs. receiving side and forwarding side.
# read can be called when a new request comes in on receiving side (mode relay)
@ -1948,7 +1962,7 @@ sub ReadFn {
sub ReadAnswer {
my $hash = shift; # called with physical io device hash
my $name = $hash->{NAME};
my $logHash = $hash->{REQUEST}{MASTERHASH}; # logical device that sent last request, stored by ProcessRequestQueue
my $logHash = $hash->{REQUEST}{MASTERHASH}; # logical device that sent last request, stored by ProcessRequestQueue, checked in loop ...
my $timeout = DevInfo($logHash, 'timing', 'timeout', 2);
my $now = gettimeofday();
my $timeRest;
@ -1976,6 +1990,11 @@ sub ReadAnswer {
if ($timeout <= 0 || ($hash->{DeviceName} eq 'none' && !$hash->{TestInput})) {
last READLOOP; # Timeout - will be logged after the loop
}
if (!$hash->{REQUEST}{MASTERHASH}) {
Log3 $name, 5, "$name: ReadAnswer misses MASTERHASH and returns";
$msg = 'ReadAnswer called but MASTERHASH disappeared';
last READLOOP;
}
if ($hash->{DeviceName} eq 'none') { # simulate receiving
$buf = $hash->{TestInput};
delete $hash->{TestInput};
@ -1996,6 +2015,7 @@ sub ReadAnswer {
if ($nfound < 0) {
next if ($! == EAGAIN() || $! == EINTR() || $! == 0);
$msg = 'Error in ReadAnswer: $!';
Log3 $name, 3, "$name: ReadAnswer sets device to disconnected";
DevIo_Disconnected($hash); # close, set state but put back on readyfnlist for reopening
last READLOOP;
}
@ -2045,6 +2065,7 @@ sub ReadAnswer {
DropFrame($hash); # drop $hash->{FRAME} and the relevant part of $hash->{READ}{BUFFER}
delete $hash->{nextTimeout};
delete $hash->{REQUEST};
# todo: normal queue delay when closeAfterResponse 2?
StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0}); # call processRequestQueue at next possibility if appropriate
return $msg;
}
@ -2210,8 +2231,15 @@ sub HandleResponse {
$response->{ADR} = $request->{ADR}; # prefill so we don't need $request in ParseResponse and it gets shorter
$response->{LEN} = $request->{LEN};
$response->{OPERATION} = $request->{OPERATION}; # for later call to ParseDataString
$response->{MASTERHASH} = $masterHash if ($masterHash);
$response->{RELAYHASH} = $request->{RELAYHASH} if ($request->{RELAYHASH}); # not $relayHash!
if ($masterHash) {
$response->{MASTERHASH} = $masterHash;
weaken $response->{MASTERHASH};
}
if ($request->{RELAYHASH}) { # not $relayHash!
$response->{RELAYHASH} = $request->{RELAYHASH};
weaken $response->{RELAYHASH};
}
} # if no request known, we will skip most of the part below
# parse response and fill response hash
@ -2244,10 +2272,6 @@ sub HandleResponse {
RelayResponse($hash, $request, $response) if ($relayHash && $request); # add to {ERROR} if relay device is unavailable
}
if ($hash->{MODE} eq 'master' && AttrVal($name, 'closeAfterResponse', 0) && ($hash->{QUEUE} ? scalar(@{$hash->{QUEUE}}) : 0) == 0) {
Log3 $name, 4, "$name: HandleResponse will close because closeAfterResponse is set and queue is empty";
DoClose($hash)
}
LogFrame($hash, ($hash->{FRAME}{ERROR} ? "HandleResponse error" : 'HandleResponse done'), 4);
Statistics($hash, 'Timeouts', 0); # damit bei Bedarf das Reading gesetzt wird
ResetExpect($hash); # for master back to 'idle', otherwise back to 'request'
@ -2255,6 +2279,22 @@ sub HandleResponse {
delete $hash->{REQUEST};
delete $hash->{RESPONSE};
RemoveInternalTimer ("timeout:$name"); # remove ResponseTimeout timer now that Response has arrived
if ($hash->{MODE} eq 'master') {
if (AttrVal($name, 'closeAfterResponse', 0) && ($hash->{QUEUE} ? scalar(@{$hash->{QUEUE}}) : 0) == 0) {
Log3 $name, 4, "$name: HandleResponse will close because closeAfterResponse is set and queue is empty";
DoClose($hash);
return 1;
}
elsif (AttrVal($name, 'closeAfterResponse', 0) == 2) {
Log3 $name, 4, "$name: HandleResponse will close because closeAfterResponse is 2";
DoClose($hash, {KEEPQUEUE => 1});
StopQueueTimer($hash); # restart with full queue delay
StartQueueTimer($hash, \&Modbus::ProcessRequestQueue);
return 1;
}
}
StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0}); # set timer to call processRequestQueue asap
return 1; # error or not, parsing is done.
}
@ -2509,7 +2549,7 @@ sub WriteObject {
next OBJLOOP;
}
if (!TryCall($hash, 'ModbusReadingsFn', $reading, $val)) {
Log3 $name, 4, "$name: ParseDataString assigns value $val to reading $rname of device $device";
Log3 $name, 4, "$name: WriteObject assigns value $val to reading $rname of device $device";
if ($dev eq $hash) {
readingsBulkUpdate($dev, $rname, $val); # assign value to one of this devices readings
} else {
@ -2524,6 +2564,7 @@ sub WriteObject {
#####################################################
# split data part in a response or write request
# into objects that later can be assigned to readings
# only called from ParseDataString
sub SplitDataString {
my $hash = shift;
my $transPtr = shift; # $transPtr can be response (mode master) or request (mode slave and write request)
@ -2618,6 +2659,7 @@ sub CreateParseInfoCache {
'rmapDefault' => ObjInfo($hash, $objCombi, 'rmapDefault'),
'format' => ObjInfo($hash, $objCombi, 'format'),
};
return;
}
@ -2667,7 +2709,7 @@ sub CreateDataObjects {
my $val = $val[0];
next OBJLOOP if ($pi->{'ignoreExpr'} && EvalExpr($hash, # ignore exp results true -> skip to next object
{expr => $pi->{'ignoreExpr'}, val => $val,, '@val' => \@val,
{expr => $pi->{'ignoreExpr'}, val => $val, '@val' => \@val,
nullIfNoExp => 1, action => "ignoreExpr for $obj->{reading}"}));
if ($transPtr->{OPERATION} && $transPtr->{OPERATION} =~ /^scan/) {
@ -2698,10 +2740,9 @@ sub CreateDataObjects {
}
#################################################
################################################################################
# Parse holding / input register / coil Data
# called from ParseResponse which is only called from HandleResponse
# or from HandleRequest (for write requests as slave)
# called from HandleResponse or from HandleRequest (for write requests as slave)
# with logical device hash, data string and the object type/adr to start with
sub ParseDataString {
my $hash = shift;
@ -2793,6 +2834,7 @@ sub HandleRequest {
}
elsif ($hash->{MODE} eq 'relay') {
$request->{RELAYHASH} = $logHash; # remember who to pass the response to
weaken $request->{RELAYHASH};
RelayRequest($hash, $request, $frame); # even if unspported fCode ...
$hash->{EXPECT} = 'request'; # just to be safe, should already be request
}
@ -2963,6 +3005,7 @@ sub GetRelayIO {
return;
}
$relayHash->{MASTERHASH} = $masterHash;
weaken $relayHash->{MASTERHASH};
$relayHash->{RELID} = $masterHash->{MODBUSID};
Log3 $name, 5, "$name: GetRelayIO found $masterIOHash->{NAME} as Modbus relay forward io device for $masterHash->{NAME} with id $masterHash->{MODBUSID}";
#Log3 $name, 5, "$name: GetRelayIO set RELID of $relayHash to $relayHash->{RELID}";
@ -3011,7 +3054,9 @@ sub RelayRequest {
}
my %fRequest = %{$request}; # create a copy to modify and forward
$fRequest{MASTERHASH} = $masterHash;
weaken $fRequest{MASTERHASH};
LogFrame($hash, "RelayRequest via $reIOHash->{NAME}, Proto $reIOHash->{PROTOCOL} with id $id", 4);
if ($reIOHash->{PROTOCOL} eq 'TCP') { # forward as Modbus TCP?
my $tid = int(rand(255));
@ -3047,7 +3092,7 @@ sub RelayResponse {
my $relayHash = $response->{RELAYHASH}; # hash of logical relay device that got the first request
my $ioHash = GetIOHash($relayHash); # the ioHash that received the original request
if (!$ioHash) {
if (!$ioHash || !$relayHash) {
Log3 $name, 4, "$name: RelayResponse failed because slave (=server) side io device disappeared";
return;
}
@ -3111,7 +3156,7 @@ sub CreateResponse {
$response->{VALUES} = $serverId;
Log3 $name, 3, "$name: server id requested, send $serverId";
}
Log3 $name, 5, "$name: CreateResponse calls PackFrame to prepare response pdu";
$response->{FCODE} += 128 if ($response->{ERRCODE});
@ -3193,6 +3238,7 @@ sub DoRequest {
$request->{MASTERHASH} = $hash; # logical device in charge
$request->{TID} = int(rand(255)) if ($hash->{PROTOCOL} eq 'TCP'); # transaction id for Modbus TCP
$request->{FCODE} = GetFC($hash, $request);
weaken $request->{MASTERHASH};
return if (!$request->{FCODE});
# check if defined unpack code matches a corresponding len and log warning if appropriate
@ -3267,7 +3313,9 @@ sub QueueRequest {
return;
}
readingsSingleUpdate($hash, 'QueueLength', ($hash->{QUEUE} ? scalar(@{$hash->{QUEUE}}) : 0), 1) if (AttrVal($name, 'enableQueueLengthReading', 0));
StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0}); # process asap, after delays are over
StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0}); # try to open if closed
# process asap, after delays are over
return;
}
@ -4189,43 +4237,64 @@ sub CountTimeouts {
#################################################################
# set state Reading and STATE internal
# call instead of setting STATE directly and when inactive / disconnected
# when called with
# opened - set to disabled if attr disable is set (after attr IODev, disabled would not be set after successful open)
# set to inactive if state reading is inactive
# otherwise set to opened
# disconnected - set to disabled if attr disable is set (when connection is lost or after define)
# set to inactive if state reading is already inactive
# otherwise set to disconnected
# inactive - set to disabled if attr disable is set (after set inactive)
# otherwise set to inactive
# active - set to disabled if attr disable is set (after set active)
# otherwise set to active temporarily
# after open state will be set again
# disabled - set to disabled (while attr disable is set)
# enabled - set to active temporarily (when attr disable is removed)
# after open state will be set again
sub SetStates {
my $hash = shift;
my $state = shift;
my $setState = shift;
my $name = $hash->{NAME};
my $newState = $state;
my $oldState = ReadingsVal($name, 'state', '');
my $disabled = AttrVal($name, 'disable', 0);
my $newState = $setState;
Log3 $name, 5, "$name: SetStates called from " . FhemCaller() . " with $setState";
if ($setState eq 'disabled') {
#Log3 $name, 5, "$name: SetState called from " . FhemCaller() . " with $state, current state reading is " . ReadingsVal($name, 'state', '');
if ($state ne 'disabled') { # for disabled nothing else matters
if ($state eq 'enabled') {
$newState = 'active'; # enabled (disable removed) becomes active
} elsif ($state ne 'active') {
if (AttrVal($name, 'disable', 0)) { # otherweise check disable attr first
$newState = 'disabled';
} elsif (ReadingsVal($name, 'state', '') eq 'inactive') { # and then check if inactive
$newState = 'inactive';
}
}
elsif ($setState eq 'enabled') { # attr disabled is not cleared yet here
$newState = 'active'; # enabled (disable removed) becomes active
}
elsif ($setState eq 'inactive') {
$newState = 'disabled' if ($disabled);
}
elsif ($setState eq 'active') {
$newState = 'disabled' if ($disabled);
}
elsif ($setState eq 'opened') {
}
elsif ($setState eq 'disconnected') {
$newState = 'inactive' if ($oldState eq 'inactive');
$newState = 'disabled' if ($disabled);
}
else {
Log3 $name, 5, "$name: SetState got unknown parameter $setState";
return;
}
if ($newState =~ /inactive|disabled/) {
if ($hash->{TCPConn}) { # Modbus over TCP connection
DoClose($hash); # close, set Expect, clear Buffer, set state to disconnected
UpdateTimer($hash, \&Modbus::GetUpdate, 'stop');
# other timers are stopped in DoClose
}
else { # connection via serial io device
UnregAtIODev($hash); # unregister at physical device because logical device is disabled
}
UpdateTimer($hash, \&Modbus::GetUpdate, 'stop');
}
elsif ($newState eq 'active') {
if ($hash->{TCPConn} && !IsOpen($hash)) { # closed Modbus over TCP connection
DoOpen($hash) if !AttrVal($name, "closeAfterResponse", 0);
}
else {
my $ioHash = GetIOHash($hash); # get ioHash / check compatibility and set / register if necessary
Log3 $name, 3, "$name: " . ($ioHash ? "using $ioHash->{NAME}" : "no IODev") . " for communication";
}
if ($hash->{MODE} && $hash->{MODE} eq 'master') {
UpdateTimer($hash, \&Modbus::GetUpdate, 'start'); # set / change timer
}
}
Log3 $name, 5, "$name: SetState called from " . FhemCaller() . " with $state sets state and STATE to $newState";
Log3 $name, 5, "$name: SetState sets state and STATE from $oldState to $newState";
$hash->{STATE} = $newState;
return if ($newState eq ReadingsVal($name, 'state', ''));
readingsSingleUpdate($hash, 'state', $newState, 1);
@ -4290,7 +4359,7 @@ sub ResponseTimeout {
if ($request && $relayHash) { # create an error response through the relay
my $origRequest = $relayHash->{REQUEST};
if (!$origRequest->{MODBUSID}) {
Log3 $name, 4, "$name: relaying error response back failed because original request is missing";
Log3 $name, 4, "$name: relaying error response back failed because original request is missing. Maybe master has already closed connection to relay";
}
else {
# adjust Modbus ID for back communication
@ -4300,6 +4369,7 @@ sub ResponseTimeout {
($request->{TID} ? ", tid $request->{TID}" : '');
my $reIoHash = GetIOHash($relayHash); # the physical hash of the relay that received the original request
if (!$reIoHash) {
Log3 $name, $logLvl, "$name: sending timout response back failed because relay slave (=server) side io device disappeared";
}
@ -4382,7 +4452,10 @@ sub CheckDisable {
my $msg;
#Log3 $name, 5, "$name: CheckDisable called from " . FhemCaller();
if ($hash->{TYPE} eq 'Modbus' || $hash->{TCPConn}) { # physical hash
if (!$hash) {
$msg = 'device is unavailable / hash is undef';
}
elsif ($hash->{TYPE} eq 'Modbus' || $hash->{TCPConn}) { # physical hash
if (IsDisabled($name)) {
$msg = 'device is disabled';
}
@ -4405,7 +4478,6 @@ sub CheckDisable {
################################################################
# set the $hash->{IODev} pointer to the physical io device
# and register there
#
# check the name passed or the IODev attr or search for device
#
# called from GetIOHash with the logical hash or from attr IODev
@ -4466,9 +4538,10 @@ sub GetIOHash {
return $hash if ($hash->{TCPConn}); # for TCP/IP connected devices ioHash = hash
return $hash if ($hash->{TYPE} eq 'Modbus'); # this is already the physical device!
return $hash->{IODev} if ($hash->{IODev}
&& IsRegisteredAtIODev($hash, $hash->{IODev})); # $hash->{IODev} is set correctly and $hash is registerd
&& IsRegisteredAtIODev($hash, $hash->{IODev})); # $hash->{IODev} is set correctly and registerd
Log3 $name, 4, "$name: GetIOHash (called from " . FhemCaller() . ") did not find valid IODev hash key, calling SetIODev now";
Log3 $name, 4, "$name: GetIOHash (called from " . FhemCaller() .
") did not find valid IODev hash key, calling SetIODev now";
return $hash->{IODev} if (SetIODev($hash)); # reconstruct pointer to physical device
#Log3 $name, 4, '$name: GetIOHash did not find IODev attribute or matching physical serial Modbus device';
return;
@ -4521,6 +4594,7 @@ sub CheckIOCompat {
################################################################
# check if logical device is registered at io dev
# ony called from GetIOHash
sub IsRegisteredAtIODev {
my $hash = shift;
my $ioHash = shift;