diff --git a/fhem/FHEM/98_Modbus.pm b/fhem/FHEM/98_Modbus.pm index 8b734de5d..0b348d939 100755 --- a/fhem/FHEM/98_Modbus.pm +++ b/fhem/FHEM/98_Modbus.pm @@ -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) = @_; diff --git a/fhem/FHEM/98_ModbusAttr.pm b/fhem/FHEM/98_ModbusAttr.pm index 07facc80e..f691b81f1 100755 --- a/fhem/FHEM/98_ModbusAttr.pm +++ b/fhem/FHEM/98_ModbusAttr.pm @@ -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.
- + Modbus devices offer the following types of data objects: -
- + 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

@@ -212,6 +213,11 @@ ModbusAttr_Initialize($) scanId-5-Response-h770 hex=0064, string=.d, s=25600, s>=100, S=25600, S>=100
  • scanStop
  • stops any running scans. +
  • saveAsModule <name>
  • + experimental: saves the definitions of obj- and dev- attributes in a new fhem module file as /tmp/98_ModbusGen<name>.pm.
    + 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<name> 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.
    @@ -234,7 +240,7 @@ ModbusAttr_Initialize($)
  • enableControlSet
  • enables the built in set commands like interval, stop, start and reread (see above)
    - + please also notice the attributes for the physical modbus interface as documented in 98_Modbus.pm
    @@ -261,7 +267,7 @@ ModbusAttr_Initialize($)
  • obj-[cdih][1-9][0-9]*-expr
  • defines a perl expression that converts the raw value read from the device. -
    +
  • obj-[cdih][1-9][0-9]*-ignoreExpr
  • defines a perl expression that returns 1 if a value should be ignored and the existing reading should not be modified
    @@ -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.
    +
  • obj-[ih][1-9][0-9]*-type
  • + defines that this object has a user defined data type. Data types can be defined using the dev-type- attribues.
    + 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
    + example:
    +
    +            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
    +            
    +
  • obj-[cdih][1-9][0-9]*-showGet
  • 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($)
  • dev-([cdih]-)*defExpr
  • defines a default Perl expression to use for this object type to convert raw values read. -
    +
  • dev-([cdih]-)*defIgnoreExpr
  • defines a default Perl expression to decide when values should be ignored.
    @@ -374,9 +395,22 @@ ModbusAttr_Initialize($)
  • dev-([cdih]-)*defShowGet
  • if set to 1 then all objects of this type will have a visible get by default.
    + +
  • dev-type-XYZ-unpack, -len, -encode, -decode, -revRegs, -bswapRegs, -format, -expr, -map
  • + 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. +
    +
  • dev-([cdih]-)*allowShortResponses
  • if set to 1 the module will accept a response with valid checksum but data lengh < lengh in header
    +
  • dev-h-brokenFC3
  • + 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 +
    +
  • dev-c-brokenFC5
  • + if set the module will use the hex value specified here instead of ff00 as value 1 for setting coils +
  • dev-timing-timeout
  • timeout for the device (defaults to 2 seconds)
    @@ -388,7 +422,7 @@ ModbusAttr_Initialize($)
  • queueMax
  • max length of the send queue, defaults to 100 -
    +
  • nextOpenDelay
  • 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.
  • openTimeout
  • @@ -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
  • maxTimeoutsToReconnect
  • 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. +
  • nonPrioritizedSet
  • + 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. +
  • sortUpdate
  • + if set to 1, the requests during a getUpdate cycle will be sorted before queued. +
  • disable
  • stop communication with the device while this attribute is set to 1. For Modbus over TCP this also closes the TCP connection.