Bitwise operations in a modbus register

Tags: #<Tag:0x00007fc3f7f00e20>

I’ve covered this before, but it got a bit lost in a general thread. @kazemiprince flushed out my errors, thankyou :smiley: so we can present a tested solution here.

Problem
There are few “rules” in Modbus, each manufacturer decides their own way how to present device data and controls.
Even though Modbus provides ‘discrete’ inputs and ‘coil’ outputs for binary controls, such as go/nogo limit switches or on/off relays, sometimes a designer opts to present binary values as individual bits packed into a standard 16-bit Modbus register.
The version 2 Modbus binding provides a convenient means to extract bits to openHAB Items when reading, but writing bits to a register in a device is complicated. More on why later.

Reading packed bits
Let us imagine a Modbus device where the manual tells us

Holding register 8 represents temperature in degrees C
Holding register 10 bit 0 represents “Fan running”
Holding register 10 bit 1 represents “Vent open”
Holding register 10 bit 2 represents “Damper open”

Always take care with Modbus manuals; sometimes they talk register numbers (which start at “1”) and sometimes register addresses (which start at “0”)
Our binding Things configuration uses addresses (starting at 0), so sometimes you will need to “take one off” to hit the correct register.

Our binding counts the least significant bit in a byte/register/word as address 0, luckily there is usually less confusion about this part.

Well, we have enough to configure some Modbus Things, as per the binding docs. I give the example in text files, but you could use PaperUI if you prefer.

modbus.things

Bridge modbus:tcp:HVAC [ host="192.168.0.12", port=502, id=2 ]
{
	Bridge poller hvacpoll [ start=8, length=3, refresh=800, type="holding" ]
        {
           Thing data reg08 [ readStart="8", readValueType="int16" ]
           Thing data di0 [ readStart="10.0", readValueType="bit" ]
           Thing data di1 [ readStart="10.1", readValueType="bit" ]
           Thing data di2 [ readStart="10.2", readValueType="bit" ]
         }
}

The slave Bridge is configured in the usual way.
The poller also, to fetch registers 8-10.
The data Things introduce a new address notation, “10.0” means register 10 bit 0
Note - that we can mix how we use different registers from one poller
Note - that do not have to “use up” all the registers the poller fetches; in this case we ignore register 9 but use 8 and 10.

hvac.items

Number HVAC_Temp  "Temp [%d]" { channel="modbus:data:HVAC:hvacpoll:reg08:number" }
Switch HVAC_Fan   "Fan [%s]"  { channel="modbus:data:HVAC:hvacpoll:di0:switch" }
Contact HVAC_Vent   "Vent [%s]"  { channel="modbus:data:HVAC:hvacpoll:di1:contact" }
Contact HVAC_Damper   "Damper [%s]"  { channel="modbus:data:HVAC:hvacpoll:di2:contact" }

So, that is reading packed bits, not too difficult.

Writing packed bits
Modbus protocol specifies a complicated “write mask register” method with FC22 that is capable of writing to individual bits in a slave register.
Unfortunately openHAB binding does not support FC22 - but I’ve never seen a device yet that actually supports it either.

That leaves no other way to write to a single bit in a 16-bit holding register. We must (re)write the whole register to get the bit you want set or unset.
Now, we have to make a decision about what to do about the rest of the bits in that register - should we read how the register is set just before we write it? Or should we keep a record in openHAB of how we think it ought to be set, and use that?

The method presented here is to maintain an image of the whole register in openHAB. It does get read from the slave by polling, so should be well up to date.

Using our HVAC example, let us now imagine we want to write to those bits to control the fan on or off etc.

modbus.things

Bridge modbus:tcp:HVAC [ host="192.168.0.12", port=502, id=2 ]
{
	Bridge poller hvacpoll [ start=8, length=3, refresh=800, type="holding" ]
        {
           Thing data reg08 [ readStart="8", readValueType="int16" ]
           Thing data reg10 [ readStart="10", readValueType="uint16", writeStart="10", writeValueType="uint16", writeType="holding" ]
           Thing data di0 [ readStart="10.0", readValueType="bit" ]
           Thing data di1 [ readStart="10.1", readValueType="bit" ]
           Thing data di2 [ readStart="10.2", readValueType="bit" ]
         }
}

What we’ve done here is add a Thing to represent the whole register, as well as keeping the bitwise Things.
The whole register Thing also has a write ability, but the bit Things can remain read-only.

hvac.items

Number Reg_Image  "HVAC register [%d]" { channel="modbus:data:HVAC:hvacpoll:reg10:number", autoupdate="false" }
Group HvacBits
Switch HVAC_Fan   "Fan [%s]"  (HvacBits) { channel="modbus:data:HVAC:hvacpoll:di0:switch", autoupdate="false" }
Switch HVAC_Vent   "Vent [%s]"  (HvacBits) { channel="modbus:data:HVAC:hvacpoll:di1:switch", autoupdate="false" }
Switch HVAC_Damper   "Damper [%s]"  (HvacBits) { channel="modbus:data:HVAC:hvacpoll:di2:switch", autoupdate="false"  }

So we add a Number Item to be our whole register image. This will get updated from Modbus polling regularly.
We’ve kept the individual Items (all Switch type now, so that we can send commands from UI or rules) but have put them in a Group to make it easier to handle them together. These Switches we’d put into a sitemap.

A non-obvious trick we have used is to disable autoupdate for the writable Items; because Modbus is regularly updating them, we don’t want autoupdate interfering with its own state predictions.

At this stage, our Switch Items should show what state the HVAC controls are in. But nothing happens when we command them ON/OFF from openHAB UI or rule, the Modbus bit channels are read-only - we need a rule to do that part.

hvac.rules

import java.lang.Math    // we need this for testBit

rule "update hvac register"
when
   Member of HvacBits received command
then
   var BigInteger register = 0  // we need BigInteger type to use testBit
   if ( Reg_Image.state != NULL && Reg_Image.state != UNDEF) { // avoid using invalid register image
      register = (Reg_Image.state  as DecimalType).toBigDecimal.toBigInteger   // use existing image
   }
   switch triggeringItem.name {    // act on switched Item name to change only one bit
      case "HVAC_Fan" : {
         if  (receivedCommand == ON) {
            register = register.setBit(0)
         } else if (receivedCommand == OFF) { // by testing both, we will ignore REFRESH commands
            register = register.clearBit(0)
         }
      }
      case "HVAC_Vent" : {
         if  (receivedCommand == ON) {
            register = register.setBit(1)
         } else if (receivedCommand == OFF) {
            register = register.clearBit(1)
         }
      }
      case "HVAC_Damper" : {
         if  (receivedCommand == ON) {
            register = register.setBit(2)
         } else if (receivedCommand == OFF) {
            register = register.clearBit(2)
         }
      }
   }
    // at last, write to Modbus
    Reg_Image.sendCommand(register)
end

The rule simply sets or resets bits, according to which Item received what command, then sends the whole register image as a command to be dealt with by the Modbus binding.

There is no synchronizing here, and the rule could mess up if you send multiple commands in quick succession from a rule. Safest to have them wait a little more than one polling period between commands.

EDIT - fixed some typos in rule

3 Likes

Well, this is odd. Despite previous testing, the rule given above no longer works for me - the import does not work… Not sure which version of what changed, but here I give an alternative that works. Note, the rule needs a change, as well as the import.

import java.math.BigInteger // we need this for testBit

rule "update hvac register"
when
    Member of HvacBits received command
then
   var register = BigInteger.valueOf(0)  // we need BigInteger type to use testBit
   if ( Reg_Image.state != NULL && Reg_Image.state != UNDEF) { // avoid using invalid register image
       register = (Reg_Image.state as DecimalType).toBigDecimal.toBigInteger // use existing image
   }
// etc etc

Comment, the binding may put WARN messages in your openhab.log when sending commands to your read-only “bit” channels; these can safely be ignored.