2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-03-10 09:16:53 +00:00

98_Modbus.pm: Bugfixes und neue Features

git-svn-id: https://svn.fhem.de/fhem/trunk@15034 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
StefanStrobel 2017-09-09 12:01:43 +00:00
parent 682baf5753
commit 96fc9b7051
2 changed files with 248 additions and 34 deletions

View File

@ -105,15 +105,24 @@
# 2017-04-21 optimize call to _send in GetUpdate, new attribute nonPrioritizedSet
# remove unused variables for devInfo / parseInfo in ParseObj
# 2017-05-08 better warning handler restore (see $oldSig)
#
# 2017-07-15 new attribute sortUpdate to sort the requests by their object address
# new attribute brokenFC5 for misbehaving devices that don't understand the normal ff00 to set a coil to 1
# set this attr e.g. to 0100 if the device wants 0100 instead of ff00
# 2017-07-18 started implementing data types (3.6.0)
# 2017-07-25 set saveAsModule
# 2017-08-17 nicer logging of timeouts
#
#
# ToDo / Ideas :
# get reading key (type / adr)
# filterEcho (wie in private post im Forum vorgeschlagen)
# set saveAsModule to save attr definitions as module
# define data types VT_R4 -> revregs, len2, unpack f> ...
# async output for scan? table? with revregs etc.?
# get object-interpretations h123 -> Alle Variationen mit revregs und bswap und unpacks ...
# nonblocking disable attr für xp
#
# attr with a lits of set commands / requests to launch when polling (Helios support)
# attr with a lits of set commands / requests to launch when polling (Helios support)
#
# passive listening to other modbus traffic (state machine, parse requests of others in special queue
#
@ -148,6 +157,7 @@ sub Modbus_ParseFrames($);
sub Modbus_HandleSendQueue($;$);
sub Modbus_TimeoutSend($);
sub Modbus_CRC($);
sub ModbusLD_ObjInfo($$$;$$);
# functions to be used from logical modules
sub ModbusLD_ExpandParseInfo($);
@ -161,7 +171,7 @@ sub ModbusLD_GetUpdate($);
sub ModbusLD_GetIOHash($);
sub ModbusLD_Send($$$;$$$);
my $Modbus_Version = '3.5.25 - 8.5.2017';
my $Modbus_Version = '3.7.0 - 20.8.2017';
my $Modbus_PhysAttrs =
"queueDelay " .
"busDelay " .
@ -181,6 +191,7 @@ my $Modbus_LogAttrs =
"alignTime " .
"enableControlSet:0,1 " .
"nonPrioritizedSet:0,1 " .
"sortUpdate:0,1 " .
"scanDelay ";
my $Modbus_CommonAttrs =
@ -402,6 +413,19 @@ sub ModbusLD_ObjInfo($$$;$$) {
return $parseInfo->{$key}{$oName}
if (defined($parseInfo->{$key}) && defined($parseInfo->{$key}{$oName}));
# check for type entry / attr ...
if ($oName ne "type") {
my $dType = ModbusLD_ObjInfo($hash, $key, 'type', 'noDefaultDevAttrForType', '***NoTypeInfo***');
if ($dType ne '***NoTypeInfo***') {
#Log3 $name, 5, "$name: ObjInfo for $key and $oName found type $dType";
my $typeSpec = ModbusLD_DevInfo($hash, "type-$dType", $oName, '***NoTypeInfo***');
if ($typeSpec ne '***NoTypeInfo***') {
#Log3 $name, 5, "$name: $dType specifies $typeSpec for $oName";
return $typeSpec;
}
}
}
# default for object type in deviceInfo / in attributes for device / type
if ($defName) {
my $type = substr($key, 0, 1);
@ -532,8 +556,9 @@ sub Modbus_ParseObj($$$;$$) {
Log3 $name, 3, "$name: ScanIds got reply from Id $1 - set internal MODBUSID to $1";
} elsif ($op eq 'scanobj') { # scan Modbus objects
if (!$reading) {
$reading = "scan-$key";
CommandAttr(undef, "$name obj-${key}-reading $reading");
my $fKey = $type . sprintf ("%06d", $startAdr);
$reading = "scan-$fKey";
CommandAttr(undef, "$name obj-${fKey}-reading $reading");
}
if ($type =~ "[hi]") {
my $l = length($rest) / 2;
@ -879,13 +904,19 @@ sub Modbus_ParseFrames($)
} elsif ($fCode == 3 || $fCode == 4) { # read holding/input registers, pdu: bytes, registers
($headerLen, $values) = unpack ('Ca*', $data);
if (ModbusLD_DevInfo($logHash, "h", "brokenFC3", 0)) {
Log3 $name, 5, "$name: ParseFrames uses fix for broken fcode 3";
($parseAdr, $values) = unpack ('na*', $data);
$headerLen = 4;
}
$actualLen = length ($values);
} elsif ($fCode == 5) { # write single coil, pdu: adr, coil (FF00)
($parseAdr, $values) = unpack ('nH4', $data);
$values = ($values eq "ff00" ? 1 : 0);
if (ModbusLD_DevInfo($logHash, "c", "brokenFC5", 0)) {
Log3 $name, 5, "$name: ParseFrames uses fix for broken fcode 5";
$values = ($values eq "0000" ? 0 : 1);
} else {
$values = ($values eq "ff00" ? 1 : 0);
}
$quantity = 1;
# length of $data should be 4
} elsif ($fCode == 6) { # write single (holding) register, pdu: adr, register
@ -904,7 +935,7 @@ sub Modbus_ParseFrames($)
my $hexdata = unpack ("H2", $data);
my $hexFCode = unpack ("H*", pack("C", $fCode));
my $errCode = $Modbus_errCodes{$hexdata};
Log3 $name, 5, "$name: ParseFrames got error code $hexFCode / $hexdata" .
Log3 $name, 4, "$name: ParseFrames got error code $hexFCode / $hexdata" .
($errCode ? ", $errCode" : "");
return "device replied with exception code $hexFCode / $hexdata" . ($errCode ? ", $errCode" : "");
} else {
@ -1129,8 +1160,8 @@ sub Modbus_TimeoutSend($)
my $logLvl = AttrVal($name, "timeoutLogLevel", 3);
Log3 $name, $logLvl, "$name: timeout waiting for fc $ioHash->{REQUEST}{FCODE} " .
"from id $ioHash->{REQUEST}{MODBUSID}, " .
"($ioHash->{REQUEST}{TYPE}$ioHash->{REQUEST}{ADR} / $ioHash->{REQUEST}{READING}), " .
"Request was $ioHash->{REQUESTHEX}" .
" ($ioHash->{REQUEST}{TYPE}$ioHash->{REQUEST}{ADR} / $ioHash->{REQUEST}{READING}, len $ioHash->{REQUEST}{LEN})" .
($ioHash->{helper}{buffer} ? ", Buffer contains " . unpack ("H*", $ioHash->{helper}{buffer}) : "");
Modbus_Statistics($ioHash, "Timeouts", 1);
@ -1277,7 +1308,12 @@ sub Modbus_HandleSendQueue($;$)
} elsif ($fCode == 3 || $fCode == 4) { # read holding/input registers, pdu: StartAdr, Len (=number of regs)
$data = pack ('nn', $adr, $len);
} elsif ($fCode == 5) { # write single coil, pdu: StartAdr, Value (1-bit as FF00)
$data = pack ('nH4', $adr, (unpack ('n',$v1) ? "FF00" : "0000"));
if (ModbusLD_DevInfo($logHash, "c", "brokenFC5", 0)) {
my $oneCode = lc ModbusLD_DevInfo($logHash, "c", "brokenFC5");
$data = pack ('nH4', $adr, (unpack ('n',$v1) ? $oneCode : "0000"));
} else {
$data = pack ('nH4', $adr, (unpack ('n',$v1) ? "FF00" : "0000"));
}
} elsif ($fCode == 6) { # write single register, pdu: StartAdr, Value
$data = pack ('n', $adr) . $v1;
} elsif ($fCode == 15) { # write multiple coils, pdu: StartAdr, NumOfCoils, ByteCount, Values
@ -1391,6 +1427,7 @@ sub ModbusLD_Initialize($ )
"obj-[cdih][0-9]+-expr " .
"obj-[cdih][0-9]+-ignoreExpr " .
"obj-[cdih][0-9]+-format " .
"obj-[ih][0-9]+-type " .
"obj-[cdih][0-9]+-showGet " .
"obj-[cdih][0-9]+-poll " .
"obj-[cdih][0-9]+-polldelay ";
@ -1415,6 +1452,17 @@ sub ModbusLD_Initialize($ )
"dev-([cdih]-)*defShowGet " .
"dev-([cdih]-)*defPoll " .
"dev-h-brokenFC3 " .
"dev-c-brokenFC5 " .
"dev-type-[A-Za-z0-9_]+-unpack " .
"dev-type-[A-Za-z0-9_]+-len " .
"dev-type-[A-Za-z0-9_]+-encode " .
"dev-type-[A-Za-z0-9_]+-decode " .
"dev-type-[A-Za-z0-9_]+-revRegs " .
"dev-type-[A-Za-z0-9_]+-bswapRegs " .
"dev-type-[A-Za-z0-9_]+-format " .
"dev-type-[A-Za-z0-9_]+-expr " .
"dev-type-[A-Za-z0-9_]+-map " .
"dev-timing-timeout " .
"dev-timing-sendDelay " .
@ -1818,6 +1866,21 @@ sub ModbusLD_Get($@)
}
sub Modbus_compObjAttrs ($$) {
my ($a, $b) = @_;
my $aType = substr($a, 4, 1);
my $aStart = substr($a, 5);
my $bType = substr($b, 4, 1);
my $bStart = substr($b, 5);
my $result = ($aType cmp $bType);
if ($result) {
return $result;
}
$result = $aStart <=> $bStart;
return $result;
}
#
# SET command - handle predifined control sets
################################################
@ -1922,7 +1985,90 @@ sub ModbusLD_ControlSet($$$)
RemoveInternalTimer ("scan:$name");
InternalTimer($now+$scanDelay, "ModbusLD_ScanObjects", "scan:$name", 0);
return "0";
} elsif ($setName eq 'saveAsModule') {
my $fName = $setVal;
my $out;
my $last = "x";
if (!open($out, ">", "/tmp/98_ModbusGen$fName.pm")) {
Log3 $name, 3, "$name: Cannot create output file $hash->{OUTPUT}";
return;
};
print $out "
##############################################
# \$Id: 98_ModbusGen${fName}.pm \$
# von ModbusAttr generiertes Modul
package main;
use strict;
use warnings;
";
print $out "sub ModbusGen${fName}_Initialize(\$);\n";
print $out "my %ModbusGen${fName}parseInfo = (\n";
foreach my $a (sort keys %{$attr{$name}}) {
if ($a =~ /^obj-([^\-]+)-(.*)$/) {
if ($1 ne $last) {
if ($last ne "x") {
# Abschluss des letzten Eintrags
printf $out "%26s", "},\n";
}
# Neuer Key
printf $out "%2s", " ";
printf $out "%16s%s", "\"$1\"", " => { ";
$last = $1;
} else {
printf $out "%25s", " ";
}
printf $out "%15s%s", "\'".$2."\'", " => \'$attr{$name}{$a}\',\n";
}
}
printf $out "%28s", "}\n";
print $out ");\n\n";
print $out "my %ModbusGen${fName}deviceInfo = (\n";
$last = "x";
foreach my $a (sort keys %{$attr{$name}}) {
if ($a =~ /^dev-((type-)?[^\-]+)-(.*)$/) {
if ($1 ne $last) {
if ($last ne "x") {
printf $out "%26s", "},\n";
}
printf $out "%2s", " ";
printf $out "%16s%s", "\"$1\"", " => { ";
$last = $1;
} else {
printf $out "%25s", " ";
}
printf $out "%15s%s", "\'".$3."\'", " => \'$attr{$name}{$a}\',\n";
}
}
printf $out "%28s", "}\n";
print $out ");\n\n";
print $out "
#####################################
sub ModbusGen${fName}_Initialize(\$)
{
my (\$modHash) = \@_;
require \"\$attr{global}{modpath}/FHEM/98_Modbus.pm\";
\$modHash->{parseInfo} = \\%ModbusGen${fName}parseInfo; # defines registers, inputs, coils etc. for this Modbus Defive
\$modHash->{deviceInfo} = \\%ModbusGen${fName}deviceInfo; # defines properties of the device like defaults and supported function codes
ModbusLD_Initialize(\$modHash); # Generic function of the Modbus module does the rest
\$modHash->{AttrList} = \$modHash->{AttrList} . \" \" . # Standard Attributes like IODEv etc
\$modHash->{ObjAttrList} . \" \" . # Attributes to add or overwrite parseInfo definitions
\$modHash->{DevAttrList} . \" \" . # Attributes to add or overwrite devInfo definitions
\"poll-.* \" . # overwrite poll with poll-ReadingName
\"polldelay-.* \"; # overwrite polldelay with polldelay-ReadingName
}
";
return "0";
}
return undef; # no control set identified - continue with other sets
}
@ -1950,7 +2096,7 @@ sub ModbusLD_ScanObjects($) {
Log3 $name, 5, "$name: ScanObjects waits until queue gets smaller";
return;
}
if ($hash->{scanOAdr}) {
if ($hash->{scanOAdr} || $hash->{scanOAdr} eq "0") {
if ($hash->{scanOAdr} < $hash->{scanOEnd}) {
$hash->{scanOAdr}++;
} else {
@ -2041,13 +2187,21 @@ sub ModbusLD_ScanFormat($$)
", s>=" . unpack("s>", $val) .
", S=" . unpack("S", $val) .
", S>=" . unpack("S>", $val);
if ($len > 2) {
if ($len > 2) {
$ret .= ", i=" . unpack("s", $val) .
", i>=" . unpack("s>", $val) .
", I=" . unpack("S", $val) .
", I>=" . unpack("S>", $val);
$ret .= ", f=" . unpack("f", $val) .
", f>=" . unpack("f>", $val);
#my $r1 = substr($h, 0, 4);
#my $r2 = substr($h, 4, 4);
#my $rev = pack ("H*", $r2 . $r1);
#$ret .= ", revf=" . unpack("f", $rev) .
#", revf>=" . unpack("f>", $rev);
}
return $ret;
}
@ -2302,6 +2456,20 @@ sub ModbusLD_ReadAnswer($;$)
}
sub Modbus_compObjKeys ($$) {
my ($a, $b) = @_;
my $aType = substr($a, 0, 1);
my $aStart = substr($a, 1);
my $bType = substr($b, 0, 1);
my $bStart = substr($b, 1);
my $result = ($aType cmp $bType);
if ($result) {
return $result;
}
$result = $aStart <=> $bStart;
return $result;
}
#####################################
# called via internal timer from
# logical device module with
@ -2347,6 +2515,7 @@ sub ModbusLD_GetUpdate($) {
push @ObjList, keys (%{$parseInfo});
Log3 $name, 5, "$name: GetUpdate full object list: " . join (" ", sort @ObjList);
# create readList by checking delays and poll settings for ObjList
foreach my $objCombi (sort @ObjList) {
#my $type = substr($objCombi, 0, 1);
#my $adr = substr($objCombi, 1);
@ -2380,8 +2549,8 @@ sub ModbusLD_GetUpdate($) {
my $maxLen;
$adr = 0; $type = ""; $span = 0; $nextSpan = 0;
# combine objects in Readlist by increasing the length of a first obejct and removing the second
foreach $nextObj (sort keys %readList) {
# combine objects in Readlist by increasing the length of a first object and removing the second
foreach $nextObj (sort Modbus_compObjKeys keys %readList) {
$nextType = substr($nextObj, 0, 1);
$nextAdr = substr($nextObj, 1);
$nextReading = ModbusLD_ObjInfo($hash, $nextObj, "reading");
@ -2406,14 +2575,20 @@ sub ModbusLD_GetUpdate($) {
$maxLen = ModbusLD_DevInfo($hash, $type, "combine", 1);
# Log3 $name, 5, "$name: GetUpdate: combine for $type is $maxLen";
}
Modbus_Profiler($ioHash, "Idle");
while (my ($objCombi, $span) = each %readList) {
# helios:
# val force len
#ModbusLD_Send($hash, h100, "write", $adr, 0, $span);
#ModbusLD_Send($hash, h100, "read", 0, 0, 20);
ModbusLD_Send($hash, $objCombi, "read", 0, 0, $span);
if (AttrVal($name, "sortUpdate", 0)) {
Log3 $name, 5, "$name: sort objList before sending requests";
foreach my $objCombi (sort Modbus_compObjKeys keys %readList) {
my $span = $readList{$objCombi};
ModbusLD_Send($hash, $objCombi, "read", 0, 0, $span);
}
} else {
Log3 $name, 5, "$name: don't sort objList before sending requests";
while (my ($objCombi, $span) = each %readList) {
ModbusLD_Send($hash, $objCombi, "read", 0, 0, $span);
}
}
Modbus_Profiler($ioHash, "Idle");
return;
}
@ -2494,7 +2669,7 @@ sub Modbus_SwpRegs($$$) {
#####################################
# called from logical device fuctions
# called from logical device functions
# with log dev hash
sub ModbusLD_Send($$$;$$$){
my ($hash, $objCombi, $op, $v1, $force, $reqLen) = @_;

View File

@ -35,15 +35,16 @@
# 2016-12-18 documentation added
# 2016-12-24 documentation added
# 2017-01-02 allowShortResponses documented
# 2017-01-25 documentation for ignoreExpr
# 2017-03-12 fixed documentation for logical attrs that were wrongly defined as physical ones
# 2017-01-25 documentation for ignoreExpr
# 2017-03-12 fixed documentation for logical attrs that were wrongly defined as physical ones
# 2017-07-15 added documentation for new attributes
# 2017-07-25 documentation for data type attributes
#
package main;
use strict;
use warnings;
#####################################
sub
ModbusAttr_Initialize($)
@ -146,16 +147,16 @@ ModbusAttr_Initialize($)
Attributes to define data objects start with obj- followed by a code that identifies the type and address
of the data object. <br>
Modbus devices offer the following types of data objects:
<ul>
<li> holding registers (16 bit objects that can be read and written)</li>
<ul>
<li> holding registers (16 bit objects that can be read and written)</li>
<li> input registers (16 bit objects that can only be read)</li>
<li> coils (single bit objects that can be read and written)</li>
<li> coils (single bit objects that can be read and written)</li>
<li> discrete inputs (single bit objects that can only be read)</li>
</ul>
</ul>
<br>
The module uses the first character of these data object types to define attributes.
Thus h770 refers to a holding register with the decimal address 770 and c120 refers to a coil with address 120.
The address has to be specified as pure decimal number. The address counting starts at address 0<br><br>
@ -212,6 +213,11 @@ ModbusAttr_Initialize($)
<code>scanId-5-Response-h770 hex=0064, string=.d, s=25600, s>=100, S=25600, S>=100</code>
<li><code>scanStop</code></li>
stops any running scans.
<li><code>saveAsModule &lt;name&gt;</code></li>
experimental: saves the definitions of obj- and dev- attributes in a new fhem module file as /tmp/98_ModbusGen&lt;name&gt;.pm.<br>
if this file is copied into the fhem module subdirectory (e.g. /opt/fhem/FHEM) and fhem is restarted then instead of defining a device
as ModbusAttr with all the attributes to define objects, you can just define a device of the new type ModbusGen&lt;name&gt; and all the
objects will be there by default. However all definitions can still be changed / overriden with the attribues defined in ModbusAttr if needed.
</ul>
</ul>
<br>
@ -234,7 +240,7 @@ ModbusAttr_Initialize($)
<li><b>enableControlSet</b></li>
enables the built in set commands like interval, stop, start and reread (see above)
<br>
please also notice the attributes for the physical modbus interface as documented in 98_Modbus.pm
<br>
@ -261,7 +267,7 @@ ModbusAttr_Initialize($)
<br>
<li><b>obj-[cdih][1-9][0-9]*-expr</b></li>
defines a perl expression that converts the raw value read from the device.
<br>
<br>
<li><b>obj-[cdih][1-9][0-9]*-ignoreExpr</b></li>
defines a perl expression that returns 1 if a value should be ignored and the existing reading should not be modified
<br>
@ -309,6 +315,21 @@ ModbusAttr_Initialize($)
defines an encoding to be used in a call to the perl function encode to convert the raw data string read from the device to a reading.
This can be used if the device delivers strings in an encoding like cp850 and after decoding it you want to reencode it to e.g. utf8.
<br>
<li><b>obj-[ih][1-9][0-9]*-type</b></li>
defines that this object has a user defined data type. Data types can be defined using the dev-type- attribues.<br>
If a device with many objects uses for example floating point values that span two swapped registers with the unpack code f>, then instead of specifying the -unpack, -revRegs, -len, -format and other attributes over and over again, you could define a data type with attributes that start with dev-type-VT_R4- and then
use this definition for each object as e.g. obj-h1234-type VT_R4<br>
example:<br>
<pre>
attr WP dev-type-VT_R4-format %.1f
attr WP dev-type-VT_R4-len 2
attr WP dev-type-VT_R4-revRegs 1
attr WP dev-type-VT_R4-unpack f>
attr WP obj-h1234-reading Temp_Ist
attr WP obj-h1234-type VT_R4
</pre>
<br>
<li><b>obj-[cdih][1-9][0-9]*-showGet</b></li>
every reading can also be requested by a get command. However these get commands are not automatically offered in fhemweb.
@ -347,7 +368,7 @@ ModbusAttr_Initialize($)
<br>
<li><b>dev-([cdih]-)*defExpr</b></li>
defines a default Perl expression to use for this object type to convert raw values read.
<br>
<br>
<li><b>dev-([cdih]-)*defIgnoreExpr</b></li>
defines a default Perl expression to decide when values should be ignored.
<br>
@ -374,9 +395,22 @@ ModbusAttr_Initialize($)
<li><b>dev-([cdih]-)*defShowGet</b></li>
if set to 1 then all objects of this type will have a visible get by default.
<br>
<li><b>dev-type-XYZ-unpack, -len, -encode, -decode, -revRegs, -bswapRegs, -format, -expr, -map</b></li>
define the unpack code, length and other details of a user defined data type. XYZ has to be replaced with the name of a user defined data type.
use obj-h123-type XYZ to assign this type to an object.
<br>
<li><b>dev-([cdih]-)*allowShortResponses</b></li>
if set to 1 the module will accept a response with valid checksum but data lengh < lengh in header
<br>
<li><b>dev-h-brokenFC3</b></li>
if set to 1 the module will change the parsing of function code 3 and 4 responses for devices that
send the register address instead of the length in the response
<br>
<li><b>dev-c-brokenFC5</b></li>
if set the module will use the hex value specified here instead of ff00 as value 1 for setting coils
<br>
<li><b>dev-timing-timeout</b></li>
timeout for the device (defaults to 2 seconds)
<br>
@ -388,7 +422,7 @@ ModbusAttr_Initialize($)
<br>
<li><b>queueMax</b></li>
max length of the send queue, defaults to 100
<br>
<br>
<li><b>nextOpenDelay</b></li>
delay for Modbus-TCP connections. This defines how long the module should wait after a failed TCP connection attempt before the next reconnection attempt. This defaults to 60 seconds.
<li><b>openTimeout</b></li>
@ -399,6 +433,11 @@ ModbusAttr_Initialize($)
if set to 1, then it will set the loglevel for "disconnected" and "reappeared" messages to 4 instead of 3
<li><b>maxTimeoutsToReconnect</b></li>
this attribute is only valid for TCP connected devices. In such cases a disconnected device might stay undetected and lead to timeouts until the TCP connection is reopened. This attribute specifies after how many timeouts an automatic reconnect is tried.
<li><b>nonPrioritizedSet</b></li>
if set to 1, then set commands will not be sent on the bus before other queued requests and the response will not be waited for.
<li><b>sortUpdate</b></li>
if set to 1, the requests during a getUpdate cycle will be sorted before queued.
<li><b>disable</b></li>
stop communication with the device while this attribute is set to 1. For Modbus over TCP this also closes the TCP connection.
<br>