Compare commits
17 Commits
JsonifyReg
...
ModbusWrit
| Author | SHA1 | Date | |
|---|---|---|---|
| a0efe1129b | |||
|
7a8a3c661d
|
|||
| 122fce519c | |||
|
4200aaf304
|
|||
|
0f037b02ea
|
|||
| ac47ff0ebe | |||
|
264d1cab14
|
|||
| ab91feafd0 | |||
|
2460f570d8
|
|||
| f6d4218e57 | |||
|
bf3475a796
|
|||
| 0bae0f4bb2 | |||
|
b9e0fefe17
|
|||
| 34ca87f734 | |||
|
de01ec20e2
|
|||
|
6821364273
|
|||
| e297149772 |
101
src/CmdServer.py
101
src/CmdServer.py
@@ -51,7 +51,7 @@ class CmdInterpreter(cmd.Cmd):
|
|||||||
|
|
||||||
def do_add_hr(self, arg):
|
def do_add_hr(self, arg):
|
||||||
try:
|
try:
|
||||||
(label, unit, address, count, scanrate, readTopic, writeTopic, feedbackTopic) = self.splitterRe.split(arg)
|
(label, unit, address, count, scanrate, readTopic, writeTopic, feedbackTopic, converter) = self.splitterRe.split(arg)
|
||||||
self.__println("Label: {0}".format(label))
|
self.__println("Label: {0}".format(label))
|
||||||
self.__println("Unit: {0}".format(unit))
|
self.__println("Unit: {0}".format(unit))
|
||||||
self.__println("Address: {0}".format(address))
|
self.__println("Address: {0}".format(address))
|
||||||
@@ -60,6 +60,7 @@ class CmdInterpreter(cmd.Cmd):
|
|||||||
self.__println("ReadTopic: {0}".format(readTopic))
|
self.__println("ReadTopic: {0}".format(readTopic))
|
||||||
self.__println("WriteTopic: {0}".format(writeTopic))
|
self.__println("WriteTopic: {0}".format(writeTopic))
|
||||||
self.__println("FeedbackTopic: {0}".format(feedbackTopic))
|
self.__println("FeedbackTopic: {0}".format(feedbackTopic))
|
||||||
|
self.__println("Converter: {0}".format(converter))
|
||||||
|
|
||||||
if readTopic == 'None':
|
if readTopic == 'None':
|
||||||
readTopic = None
|
readTopic = None
|
||||||
@@ -67,33 +68,22 @@ class CmdInterpreter(cmd.Cmd):
|
|||||||
writeTopic = None
|
writeTopic = None
|
||||||
if feedbackTopic == 'None':
|
if feedbackTopic == 'None':
|
||||||
feedbackTopic = None
|
feedbackTopic = None
|
||||||
|
if converter == 'None':
|
||||||
|
converter = None
|
||||||
unit = parseIntArbitraryBase(unit)
|
unit = parseIntArbitraryBase(unit)
|
||||||
address = parseIntArbitraryBase(address)
|
address = parseIntArbitraryBase(address)
|
||||||
count = parseIntArbitraryBase(count)
|
count = parseIntArbitraryBase(count)
|
||||||
scanrate = float(scanrate)
|
scanrate = float(scanrate)
|
||||||
if scanrate == 0:
|
r = RegisterDatapoint.HoldingRegisterDatapoint(label, unit, address, count, datetime.timedelta(seconds=scanrate), readTopic, writeTopic, feedbackTopic, converter)
|
||||||
if readTopic:
|
|
||||||
raise CmdInterpreterException('readTopic must not be set when scanRate is zero')
|
|
||||||
if not writeTopic:
|
|
||||||
raise CmdInterpreterException('writeTopic must be set when scanRate is zero')
|
|
||||||
if not feedbackTopic:
|
|
||||||
raise CmdInterpreterException('feedbackTopic must be set when scanRate is zero')
|
|
||||||
else:
|
|
||||||
if not readTopic:
|
|
||||||
raise CmdInterpreterException('readTopic must be set when scanRate is zero')
|
|
||||||
if writeTopic:
|
|
||||||
raise CmdInterpreterException('writeTopic must not be set when scanRate is zero')
|
|
||||||
if feedbackTopic:
|
|
||||||
raise CmdInterpreterException('feedbackTopic must not be set when scanRate is zero')
|
|
||||||
r = RegisterDatapoint.HoldingRegisterDatapoint(label, unit, address, count, datetime.timedelta(seconds=scanrate), readTopic, writeTopic, feedbackTopic)
|
|
||||||
self.registers.append(r)
|
self.registers.append(r)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
self.__println("ERROR: {0!s}, {1!s}".format(e.__class__.__name__, e))
|
self.__println("ERROR: {0!s}, {1!s}".format(e.__class__.__name__, e))
|
||||||
|
|
||||||
def help_add_hr(self):
|
def help_add_hr(self):
|
||||||
# HoldingRegisterDatapoint('Voltage', 1, 0x2000, 2, datetime.timedelta(seconds=10), 'Pub/Voltage', None, None),
|
# HoldingRegisterDatapoint('Voltage', 1, 0x2000, 2, datetime.timedelta(seconds=10), 'Pub/Voltage', None, None),
|
||||||
self.__println("Usage: add <Label> <Unit> <Address> <Count> <ScanRate>")
|
self.__println("Usage: add_hr <Label> <Unit> <Address> <Count> <ScanRate>")
|
||||||
self.__println(" <ReadTopic> <WriteTopic> <FeedbackTopic>")
|
self.__println(" <ReadTopic> <WriteTopic> <FeedbackTopic>")
|
||||||
|
self.__println(" <Converter>")
|
||||||
self.__println("Adds a holding register")
|
self.__println("Adds a holding register")
|
||||||
self.__println("DO NOT FORGET TO SAVE AFTERWARDS!")
|
self.__println("DO NOT FORGET TO SAVE AFTERWARDS!")
|
||||||
self.__println("---------------------------------------------------------------------")
|
self.__println("---------------------------------------------------------------------")
|
||||||
@@ -107,15 +97,53 @@ class CmdInterpreter(cmd.Cmd):
|
|||||||
self.__println("<WriteTopic> Topic to be subscribe to receive data to be")
|
self.__println("<WriteTopic> Topic to be subscribe to receive data to be")
|
||||||
self.__println(" written")
|
self.__println(" written")
|
||||||
self.__println("<FeedbackTopic> Topic to publish feedback after a write process,")
|
self.__println("<FeedbackTopic> Topic to publish feedback after a write process,")
|
||||||
self.__println("")
|
self.__println("<Converter> Converter for data")
|
||||||
self.__println("For read items the <ScanRate> must be non-zero, a <ReadTopic> must be set and")
|
|
||||||
self.__println("<WriteTopic> and <FeedbackTopic> must be <None>.")
|
|
||||||
self.__println("For write items the <ScanRate> must be zero, <ReadTopic> must be <None> and ")
|
def do_add_coil(self, arg):
|
||||||
self.__println("<WriteTopic> and <FeedbackTopic> must be set.")
|
try:
|
||||||
|
(label, unit, address, scanrate, readTopic, writeTopic, feedbackTopic) = self.splitterRe.split(arg)
|
||||||
|
self.__println("Label: {0}".format(label))
|
||||||
|
self.__println("Unit: {0}".format(unit))
|
||||||
|
self.__println("Address: {0}".format(address))
|
||||||
|
self.__println("ScanRate: {0}".format(scanrate))
|
||||||
|
self.__println("ReadTopic: {0}".format(readTopic))
|
||||||
|
self.__println("WriteTopic: {0}".format(writeTopic))
|
||||||
|
self.__println("FeedbackTopic: {0}".format(feedbackTopic))
|
||||||
|
|
||||||
|
if readTopic == 'None':
|
||||||
|
readTopic = None
|
||||||
|
if writeTopic == 'None':
|
||||||
|
writeTopic = None
|
||||||
|
if feedbackTopic == 'None':
|
||||||
|
feedbackTopic = None
|
||||||
|
unit = parseIntArbitraryBase(unit)
|
||||||
|
address = parseIntArbitraryBase(address)
|
||||||
|
scanrate = float(scanrate)
|
||||||
|
r = RegisterDatapoint.CoilDatapoint(label, unit, address, datetime.timedelta(seconds=scanrate), readTopic, writeTopic, feedbackTopic)
|
||||||
|
self.registers.append(r)
|
||||||
|
except ValueError as e:
|
||||||
|
self.__println("ERROR: {0!s}, {1!s}".format(e.__class__.__name__, e))
|
||||||
|
|
||||||
|
def help_add_coil(self):
|
||||||
|
self.__println("Usage: add_coil <Label> <Unit> <Address> <ScanRate>")
|
||||||
|
self.__println(" <ReadTopic> <WriteTopic> <FeedbackTopic>")
|
||||||
|
self.__println("Adds a coil")
|
||||||
|
self.__println("DO NOT FORGET TO SAVE AFTERWARDS!")
|
||||||
|
self.__println("---------------------------------------------------------------------")
|
||||||
|
self.__println("<Label> Descriptive label")
|
||||||
|
self.__println("<Unit> Modbus address of the device")
|
||||||
|
self.__println("<Address> Register address within the device")
|
||||||
|
self.__println("<ScanRate> Scanrate in seconds (float), for write datapoints")
|
||||||
|
self.__println(" set to zero (0)")
|
||||||
|
self.__println("<ReadTopic> Topic to publish read data")
|
||||||
|
self.__println("<WriteTopic> Topic to be subscribe to receive data to be")
|
||||||
|
self.__println(" written")
|
||||||
|
self.__println("<FeedbackTopic> Topic to publish feedback after a write process,")
|
||||||
|
|
||||||
def do_add_ir(self, arg):
|
def do_add_ir(self, arg):
|
||||||
try:
|
try:
|
||||||
(label, unit, address, count, scanrate, updateOnly, readTopic) = self.splitterRe.split(arg)
|
(label, unit, address, count, scanrate, updateOnly, readTopic, converter) = self.splitterRe.split(arg)
|
||||||
self.__println("Label: {0}".format(label))
|
self.__println("Label: {0}".format(label))
|
||||||
self.__println("Unit: {0}".format(unit))
|
self.__println("Unit: {0}".format(unit))
|
||||||
self.__println("Address: {0}".format(address))
|
self.__println("Address: {0}".format(address))
|
||||||
@@ -123,9 +151,12 @@ class CmdInterpreter(cmd.Cmd):
|
|||||||
self.__println("ScanRate: {0}".format(scanrate))
|
self.__println("ScanRate: {0}".format(scanrate))
|
||||||
self.__println("UpdateOnly: {0}".format(updateOnly))
|
self.__println("UpdateOnly: {0}".format(updateOnly))
|
||||||
self.__println("ReadTopic: {0}".format(readTopic))
|
self.__println("ReadTopic: {0}".format(readTopic))
|
||||||
|
self.__println("Converter: {0}".format(converter))
|
||||||
|
|
||||||
if readTopic == 'None':
|
if readTopic == 'None':
|
||||||
readTopic = None
|
readTopic = None
|
||||||
|
if converter == 'None':
|
||||||
|
converter = None
|
||||||
if updateOnly in ['true', 'True', 'yes', 'Yes']:
|
if updateOnly in ['true', 'True', 'yes', 'Yes']:
|
||||||
updateOnly = True
|
updateOnly = True
|
||||||
elif updateOnly in ['false', 'False', 'no', 'No']:
|
elif updateOnly in ['false', 'False', 'no', 'No']:
|
||||||
@@ -136,16 +167,14 @@ class CmdInterpreter(cmd.Cmd):
|
|||||||
address = parseIntArbitraryBase(address)
|
address = parseIntArbitraryBase(address)
|
||||||
count = parseIntArbitraryBase(count)
|
count = parseIntArbitraryBase(count)
|
||||||
scanrate = float(scanrate)
|
scanrate = float(scanrate)
|
||||||
if scanrate == 0.0:
|
r = RegisterDatapoint.InputRegisterDatapoint(label, unit, address, count, datetime.timedelta(seconds=scanrate), updateOnly, readTopic, converter)
|
||||||
raise CmdInterpreterException('scanRate must not be zero')
|
|
||||||
r = RegisterDatapoint.InputRegisterDatapoint(label, unit, address, count, datetime.timedelta(seconds=scanrate), updateOnly, readTopic)
|
|
||||||
self.registers.append(r)
|
self.registers.append(r)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
self.__println("ERROR: {0!s}, {1!s}".format(e.__class__.__name__, e))
|
self.__println("ERROR: {0!s}, {1!s}".format(e.__class__.__name__, e))
|
||||||
|
|
||||||
def help_add_ir(self):
|
def help_add_ir(self):
|
||||||
self.__println("Usage: add <Label> <Unit> <Address> <Count> <ScanRate>")
|
self.__println("Usage: add_ir <Label> <Unit> <Address> <Count> <ScanRate>")
|
||||||
self.__println(" <UpdateOnly> <ReadTopic>")
|
self.__println(" <UpdateOnly> <ReadTopic> <Converter>")
|
||||||
self.__println("Adds an input register")
|
self.__println("Adds an input register")
|
||||||
self.__println("DO NOT FORGET TO SAVE AFTERWARDS!")
|
self.__println("DO NOT FORGET TO SAVE AFTERWARDS!")
|
||||||
self.__println("---------------------------------------------------------------------")
|
self.__println("---------------------------------------------------------------------")
|
||||||
@@ -156,10 +185,11 @@ class CmdInterpreter(cmd.Cmd):
|
|||||||
self.__println("<ScanRate> Scanrate in seconds (float)")
|
self.__println("<ScanRate> Scanrate in seconds (float)")
|
||||||
self.__println("<UpdateOnly> Publish only when value has changed")
|
self.__println("<UpdateOnly> Publish only when value has changed")
|
||||||
self.__println("<ReadTopic> Topic to publish read data")
|
self.__println("<ReadTopic> Topic to publish read data")
|
||||||
|
self.__println("<Converter> Converter for data")
|
||||||
|
|
||||||
def do_add_di(self, arg):
|
def do_add_di(self, arg):
|
||||||
try:
|
try:
|
||||||
(label, unit, address, count, scanrate, updateOnly, readTopic) = self.splitterRe.split(arg)
|
(label, unit, address, count, scanrate, updateOnly, readTopic, bitCount) = self.splitterRe.split(arg)
|
||||||
self.__println("Label: {0}".format(label))
|
self.__println("Label: {0}".format(label))
|
||||||
self.__println("Unit: {0}".format(unit))
|
self.__println("Unit: {0}".format(unit))
|
||||||
self.__println("Address: {0}".format(address))
|
self.__println("Address: {0}".format(address))
|
||||||
@@ -167,6 +197,7 @@ class CmdInterpreter(cmd.Cmd):
|
|||||||
self.__println("ScanRate: {0}".format(scanrate))
|
self.__println("ScanRate: {0}".format(scanrate))
|
||||||
self.__println("UpdateOnly: {0}".format(updateOnly))
|
self.__println("UpdateOnly: {0}".format(updateOnly))
|
||||||
self.__println("ReadTopic: {0}".format(readTopic))
|
self.__println("ReadTopic: {0}".format(readTopic))
|
||||||
|
self.__println("BitCount: {0}".format(bitCount))
|
||||||
|
|
||||||
if readTopic == 'None':
|
if readTopic == 'None':
|
||||||
readTopic = None
|
readTopic = None
|
||||||
@@ -180,16 +211,15 @@ class CmdInterpreter(cmd.Cmd):
|
|||||||
address = parseIntArbitraryBase(address)
|
address = parseIntArbitraryBase(address)
|
||||||
count = parseIntArbitraryBase(count)
|
count = parseIntArbitraryBase(count)
|
||||||
scanrate = float(scanrate)
|
scanrate = float(scanrate)
|
||||||
if scanrate == 0.0:
|
bitCount = int(bitCount)
|
||||||
raise CmdInterpreterException('scanRate must not be zero')
|
r = RegisterDatapoint.DiscreteInputDatapoint(label, unit, address, count, datetime.timedelta(seconds=scanrate), updateOnly, readTopic, None, bitCount)
|
||||||
r = RegisterDatapoint.DiscreteInputDatapoint(label, unit, address, count, datetime.timedelta(seconds=scanrate), updateOnly, readTopic)
|
|
||||||
self.registers.append(r)
|
self.registers.append(r)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
self.__println("ERROR: {0!s}, {1!s}".format(e.__class__.__name__, e))
|
self.__println("ERROR: {0!s}, {1!s}".format(e.__class__.__name__, e))
|
||||||
|
|
||||||
def help_add_di(self):
|
def help_add_di(self):
|
||||||
self.__println("Usage: add <Label> <Unit> <Address> <Count> <ScanRate>")
|
self.__println("Usage: add_di <Label> <Unit> <Address> <Count> <ScanRate>")
|
||||||
self.__println(" <UpdateOnly> <ReadTopic>")
|
self.__println(" <UpdateOnly> <ReadTopic> <bitCount>")
|
||||||
self.__println("Adds a discrete input")
|
self.__println("Adds a discrete input")
|
||||||
self.__println("DO NOT FORGET TO SAVE AFTERWARDS!")
|
self.__println("DO NOT FORGET TO SAVE AFTERWARDS!")
|
||||||
self.__println("---------------------------------------------------------------------")
|
self.__println("---------------------------------------------------------------------")
|
||||||
@@ -200,6 +230,7 @@ class CmdInterpreter(cmd.Cmd):
|
|||||||
self.__println("<ScanRate> Scanrate in seconds (float)")
|
self.__println("<ScanRate> Scanrate in seconds (float)")
|
||||||
self.__println("<UpdateOnly> Publish only when value has changed")
|
self.__println("<UpdateOnly> Publish only when value has changed")
|
||||||
self.__println("<ReadTopic> Topic to publish read data")
|
self.__println("<ReadTopic> Topic to publish read data")
|
||||||
|
self.__println("<BitCount> Number of bit to be considered")
|
||||||
|
|
||||||
def do_list(self, arg):
|
def do_list(self, arg):
|
||||||
for i, r in enumerate(self.registers):
|
for i, r in enumerate(self.registers):
|
||||||
|
|||||||
18
src/Converters.py
Normal file
18
src/Converters.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# in: from Modbus to MQTT, input is a list of 16bit integers, output shall be the desired format
|
||||||
|
# to be sent in the MQTT message
|
||||||
|
# out: from MQTT to Modbus, input is the format received from MQTT, output shall be a list of
|
||||||
|
# 16bit integers to be written to the Modbus slave
|
||||||
|
|
||||||
|
from struct import pack, unpack
|
||||||
|
|
||||||
|
|
||||||
|
Converters = {
|
||||||
|
"dht20TOFloat": {
|
||||||
|
"in": lambda x : float(x[0]) / 10.0,
|
||||||
|
"out": None
|
||||||
|
},
|
||||||
|
"uint32": {
|
||||||
|
"in": lambda x : unpack('L', pack('HH', *x))[0],
|
||||||
|
"out": lambda x : unpack('HH', pack('L', int(x)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,258 @@ from pymodbus.exceptions import ModbusIOException
|
|||||||
import MqttProcessor
|
import MqttProcessor
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import Converters
|
||||||
|
|
||||||
|
class DatapointException(Exception): pass
|
||||||
|
|
||||||
|
class AbstractModbusDatapoint(object):
|
||||||
|
def __init__(self, label=None, unit=None, address=None, count=None, scanRate=None, converter=None):
|
||||||
|
self.argList = ['label', 'unit', 'address', 'count', 'scanRate', 'converter']
|
||||||
|
self.label = label
|
||||||
|
self.unit = unit
|
||||||
|
self.address = address
|
||||||
|
self.count = count
|
||||||
|
self.converter = converter
|
||||||
|
if type(scanRate) == float:
|
||||||
|
self.scanRate = datetime.timedelta(seconds=scanRate)
|
||||||
|
else:
|
||||||
|
self.scanRate = scanRate
|
||||||
|
self.type = 'abstract data point'
|
||||||
|
self.enqueued = False
|
||||||
|
self.lastContact = None
|
||||||
|
self.errorCount = 0
|
||||||
|
self.processCount = 0
|
||||||
|
if self.scanRate:
|
||||||
|
self.priority = 1
|
||||||
|
else:
|
||||||
|
self.priority = 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("{0}, {1}: unit: {2}, address: {3}, count: {4}, scanRate: {5}, "
|
||||||
|
"enqueued: {6}, lastContact: {7}, errorCount: {8}, processCount: {9}, "
|
||||||
|
"converter: {10}"
|
||||||
|
.format(self.type, self.label, self.unit, self.address, self.count,
|
||||||
|
self.scanRate, self.enqueued, self.lastContact,
|
||||||
|
self.errorCount, self.processCount, self.converter))
|
||||||
|
|
||||||
|
def jsonify(self):
|
||||||
|
return {'type':self.__class__.__name__,
|
||||||
|
'args': { k: getattr(self, k) for k in self.argList }
|
||||||
|
}
|
||||||
|
|
||||||
|
def process(self, client):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class HoldingRegisterDatapoint(AbstractModbusDatapoint):
|
||||||
|
def __init__(self, label=None, unit=None, address=None, count=None, scanRate=None,
|
||||||
|
publishTopic=None, subscribeTopic=None, feedbackTopic=None, converter=None):
|
||||||
|
super().__init__(label, unit, address, count, scanRate, converter)
|
||||||
|
self.argList = self.argList + ['publishTopic', 'subscribeTopic', 'feedbackTopic']
|
||||||
|
self.publishTopic = publishTopic
|
||||||
|
self.subscribeTopic = subscribeTopic
|
||||||
|
self.feedbackTopic = feedbackTopic
|
||||||
|
self.writeRequestValue = None
|
||||||
|
self.type = 'holding register'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("[{0!s}, publishTopic: {1}, subscribeTopic: {2}, feedbackTopic: {3}, "
|
||||||
|
"writeRequestValue: {4!s}"
|
||||||
|
.format(super().__str__(), self.publishTopic, self.subscribeTopic, self.feedbackTopic,
|
||||||
|
self.writeRequestValue))
|
||||||
|
|
||||||
|
def process(self, client, pubQueue):
|
||||||
|
logger = logging.getLogger('HoldingRegisterDatapoint')
|
||||||
|
if self.writeRequestValue:
|
||||||
|
# perform write operation
|
||||||
|
logger.debug("Holding register, perform write operation")
|
||||||
|
self.processCount += 1
|
||||||
|
values = None
|
||||||
|
logger.debug("{0}: raw: {1!s}".format(self.label, self.writeRequestValue))
|
||||||
|
if self.converter and Converters.Converters[self.converter]['out']:
|
||||||
|
try:
|
||||||
|
values = Converters.Converters[self.converter]['out'](self.writeRequestValue)
|
||||||
|
logger.debug("{0}: converted: {1!s}".format(self.label, values))
|
||||||
|
except Exception as e:
|
||||||
|
raise DatapointException("Exception caught when trying to converter modbus data: {0!s}".format(e))
|
||||||
|
else:
|
||||||
|
values = [int(self.writeRequestValue)]
|
||||||
|
result = client.write_registers(address=self.address,
|
||||||
|
unit=self.unit,
|
||||||
|
values=values)
|
||||||
|
logger.debug("Write result: {0!s}".format(result))
|
||||||
|
self.writeRequestValue = None
|
||||||
|
else:
|
||||||
|
# perform read operation
|
||||||
|
logger.debug("Holding register, perform read operation")
|
||||||
|
self.processCount += 1
|
||||||
|
result = client.read_holding_registers(address=self.address,
|
||||||
|
count=self.count,
|
||||||
|
unit=self.unit)
|
||||||
|
if type(result) in [ExceptionResponse, ModbusIOException]:
|
||||||
|
self.errorCount += 1
|
||||||
|
raise DatapointException(result)
|
||||||
|
logger.debug("{0}: {1!s}".format(self.label, result.registers))
|
||||||
|
value = None
|
||||||
|
if self.converter and Converters.Converters[self.converter]['in']:
|
||||||
|
try:
|
||||||
|
value = Converters.Converters[self.converter]['in'](result.registers)
|
||||||
|
logger.debug("{0}: converted: {1!s}".format(self.label, value))
|
||||||
|
except Exception as e:
|
||||||
|
raise DatapointException("Exception caught when trying to converter modbus data: {0!s}".format(e))
|
||||||
|
else:
|
||||||
|
value = result.registers
|
||||||
|
if self.publishTopic:
|
||||||
|
pubQueue.put(MqttProcessor.PublishItem(self.publishTopic, str(value)))
|
||||||
|
self.lastContact = datetime.datetime.now()
|
||||||
|
|
||||||
|
def onMessage(self, value):
|
||||||
|
self.writeRequestValue = value
|
||||||
|
|
||||||
|
|
||||||
|
class CoilDatapoint(AbstractModbusDatapoint):
|
||||||
|
def __init__(self, label=None, unit=None, address=None, scanRate=None, publishTopic=None, subscribeTopic=None,
|
||||||
|
feedbackTopic=None):
|
||||||
|
super().__init__(label, unit, address, 1, scanRate, None)
|
||||||
|
self.argList = ['label', 'unit','address','scanRate','publishTopic', 'subscribeTopic', 'feedbackTopic']
|
||||||
|
self.publishTopic = publishTopic
|
||||||
|
self.subscribeTopic = subscribeTopic
|
||||||
|
self.feedbackTopic = feedbackTopic
|
||||||
|
self.writeRequestValue = None
|
||||||
|
self.type = 'coil'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("{0}, {1}: unit: {2}, address: {3}, scanRate: {4}, "
|
||||||
|
"enqueued: {5}, lastContact: {6}, errorCount: {7}, processCount: {8}, "
|
||||||
|
"publishTopic: {9}, subscribeTopic: {10}, feedbackTopic: {11}"
|
||||||
|
.format(self.type, self.label, self.unit, self.address,
|
||||||
|
self.scanRate, self.enqueued, self.lastContact,
|
||||||
|
self.errorCount, self.processCount,
|
||||||
|
self.publishTopic, self.subscribeTopic, self.feedbackTopic))
|
||||||
|
|
||||||
|
def onMessage(self, value):
|
||||||
|
self.writeRequestValue = value.decode()
|
||||||
|
|
||||||
|
def process(self, client, pubQueue):
|
||||||
|
logger = logging.getLogger('CoilDatapoint')
|
||||||
|
if self.writeRequestValue:
|
||||||
|
# perform write operation
|
||||||
|
logger.debug("Coil, perform write operation")
|
||||||
|
self.processCount += 1
|
||||||
|
logger.debug("{0}: raw: {1!s}".format(self.label, self.writeRequestValue))
|
||||||
|
value=None
|
||||||
|
if self.writeRequestValue in ['true', 'True', 'yes', 'Yes', 'On', 'on']:
|
||||||
|
value = True
|
||||||
|
elif self.writeRequestValue in ['false', 'False', 'no', 'No', 'Off', 'off']:
|
||||||
|
value = False
|
||||||
|
else:
|
||||||
|
self.writeRequestValue = None
|
||||||
|
raise DatapointException('illegal value {0!s} for coil write'.format(self.writeRequestValue))
|
||||||
|
result = client.write_coil(address=self.address,
|
||||||
|
unit=self.unit,
|
||||||
|
value=value)
|
||||||
|
logger.debug("Write result: {0!s}".format(result))
|
||||||
|
self.writeRequestValue = None
|
||||||
|
else:
|
||||||
|
# perform read operation
|
||||||
|
logger.debug("Coil, perform read operation")
|
||||||
|
self.processCount += 1
|
||||||
|
result = client.read_coils(address=self.address,
|
||||||
|
unit=self.unit)
|
||||||
|
if type(result) in [ExceptionResponse, ModbusIOException]:
|
||||||
|
self.errorCount += 1
|
||||||
|
raise DatapointException(result)
|
||||||
|
logger.debug("{0}: {1!s}".format(self.label, result.getBit(0)))
|
||||||
|
value = result.getBit(0)
|
||||||
|
if self.publishTopic:
|
||||||
|
pubQueue.put(MqttProcessor.PublishItem(self.publishTopic, str(value)))
|
||||||
|
self.lastContact = datetime.datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyDatapoint(AbstractModbusDatapoint):
|
||||||
|
def __init__(self, label=None, unit=None, address=None, count=None, scanRate=None, updateOnly=None, publishTopic=None, converter=None):
|
||||||
|
super().__init__(label, unit, address, count, scanRate, converter)
|
||||||
|
self.argList = self.argList + ['updateOnly', 'publishTopic']
|
||||||
|
self.updateOnly = updateOnly
|
||||||
|
self.lastValue = None
|
||||||
|
self.publishTopic = publishTopic
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("[{0!s}, updateOnly: {1}, publishTopic: {2}, lastValue: {3!s}"
|
||||||
|
.format(super().__str__(), self.updateOnly, self.publishTopic,
|
||||||
|
self.lastValue))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class InputRegisterDatapoint(ReadOnlyDatapoint):
|
||||||
|
def __init__(self, label=None, unit=None, address=None, count=None, scanRate=None, updateOnly=None,
|
||||||
|
publishTopic=None, converter=None):
|
||||||
|
super().__init__(label, unit, address, count, scanRate, updateOnly, publishTopic, converter)
|
||||||
|
self.type = 'input register'
|
||||||
|
|
||||||
|
def process(self, client, pubQueue):
|
||||||
|
logger = logging.getLogger('InputRegisterDatapoint')
|
||||||
|
# perform read operation
|
||||||
|
logger.debug("Input register, perform read operation")
|
||||||
|
self.processCount += 1
|
||||||
|
result = client.read_input_registers(address=self.address,
|
||||||
|
count=self.count,
|
||||||
|
unit=self.unit)
|
||||||
|
if type(result) in [ExceptionResponse, ModbusIOException]:
|
||||||
|
self.errorCount += 1
|
||||||
|
raise DatapointException(result)
|
||||||
|
if not self.updateOnly or (result.registers != self.lastValue):
|
||||||
|
self.lastValue = result.registers
|
||||||
|
logger.debug("{0}: raw: {1!s}".format(self.label, result.registers))
|
||||||
|
value = None
|
||||||
|
if self.converter and Converters.Converters[self.converter]['in']:
|
||||||
|
try:
|
||||||
|
value = Converters.Converters[self.converter]['in'](result.registers)
|
||||||
|
logger.debug("{0}: converted: {1!s}".format(self.label, value))
|
||||||
|
except Exception as e:
|
||||||
|
raise DatapointException("Exception caught when trying to converter modbus data: {0!s}".format(e))
|
||||||
|
else:
|
||||||
|
value = result.registers
|
||||||
|
if self.publishTopic:
|
||||||
|
pubQueue.put(MqttProcessor.PublishItem(self.publishTopic, str(value)))
|
||||||
|
self.lastContact = datetime.datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
class DiscreteInputDatapoint(ReadOnlyDatapoint):
|
||||||
|
def __init__(self, label=None, unit=None, address=None, count=None, scanRate=None, updateOnly=None,
|
||||||
|
publishTopic=None, converter=None, bitCount=8):
|
||||||
|
super().__init__(label, unit, address, count, scanRate, updateOnly, publishTopic, converter)
|
||||||
|
self.argList = self.argList + ['bitCount']
|
||||||
|
self.type = 'discrete input'
|
||||||
|
self.bitCount = bitCount
|
||||||
|
self.lastValues = [None] * self.bitCount
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("[{0!s}, bitCount: {1}"
|
||||||
|
.format(super().__str__(), self.bitCount))
|
||||||
|
|
||||||
|
def process(self, client, pubQueue):
|
||||||
|
logger = logging.getLogger('DiscreteInputDatapoint')
|
||||||
|
# perform read operation
|
||||||
|
logger.debug("Discrete input, perform read operation")
|
||||||
|
self.processCount += 1
|
||||||
|
result = client.read_discrete_inputs(address=self.address,
|
||||||
|
count=self.count,
|
||||||
|
unit=self.unit)
|
||||||
|
if type(result) in [ExceptionResponse, ModbusIOException]:
|
||||||
|
self.errorCount += 1
|
||||||
|
raise DatapointException(result)
|
||||||
|
logger.debug("{0}: raw: {1!s}".format(self.label, result.bits))
|
||||||
|
for i in range(self.bitCount):
|
||||||
|
if not self.updateOnly or (result.getBit(i) != self.lastValues[i]):
|
||||||
|
self.lastValues[i] = result.getBit(i)
|
||||||
|
logger.debug("{0}, {1}: changed: {2!s}".format(self.label, i, result.getBit(i)))
|
||||||
|
if self.publishTopic:
|
||||||
|
pubQueue.put(MqttProcessor.PublishItem("{0}/{1}".format(self.publishTopic, i), str(result.getBit(i))))
|
||||||
|
self.lastContact = datetime.datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class JsonifyEncoder(json.JSONEncoder):
|
class JsonifyEncoder(json.JSONEncoder):
|
||||||
@@ -26,151 +278,6 @@ def datapointObjectHook(j):
|
|||||||
else:
|
else:
|
||||||
return j
|
return j
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DatapointException(Exception): pass
|
|
||||||
|
|
||||||
class AbstractModbusDatapoint(object):
|
|
||||||
def __init__(self, label=None, unit=None, address=None, count=None, scanRate=None):
|
|
||||||
self.argList = ['label', 'unit', 'address', 'count', 'scanRate']
|
|
||||||
self.label = label
|
|
||||||
self.unit = unit
|
|
||||||
self.address = address
|
|
||||||
self.count = count
|
|
||||||
if type(scanRate) == float:
|
|
||||||
self.scanRate = datetime.timedelta(seconds=scanRate)
|
|
||||||
else:
|
|
||||||
self.scanRate = scanRate
|
|
||||||
self.type = 'abstract data point'
|
|
||||||
self.enqueued = False
|
|
||||||
self.lastContact = None
|
|
||||||
self.errorCount = 0
|
|
||||||
self.processCount = 0
|
|
||||||
if self.scanRate:
|
|
||||||
self.priority = 1
|
|
||||||
else:
|
|
||||||
self.priority = 0
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return ("{0}, {1}: unit: {2}, address: {3}, count: {4}, scanRate: {5}, "
|
|
||||||
"enqueued: {6}, lastContact: {7}, errorCount: {8}, processCount: {9}"
|
|
||||||
.format(self.type, self.label, self.unit, self.address, self.count,
|
|
||||||
self.scanRate, self.enqueued, self.lastContact,
|
|
||||||
self.errorCount, self.processCount))
|
|
||||||
|
|
||||||
def jsonify(self):
|
|
||||||
return {'type':self.__class__.__name__,
|
|
||||||
'args': { k: getattr(self, k) for k in self.argList }
|
|
||||||
}
|
|
||||||
|
|
||||||
def process(self, client):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HoldingRegisterDatapoint(AbstractModbusDatapoint):
|
|
||||||
def __init__(self, label=None, unit=None, address=None, count=None, scanRate=None,
|
|
||||||
publishTopic=None, subscribeTopic=None, feedbackTopic=None):
|
|
||||||
super().__init__(label, unit, address, count, scanRate)
|
|
||||||
self.argList = self.argList + ['publishTopic', 'subscribeTopic', 'feedbackTopic']
|
|
||||||
self.publishTopic = publishTopic
|
|
||||||
self.subscribeTopic = subscribeTopic
|
|
||||||
self.feedbackTopic = feedbackTopic
|
|
||||||
self.writeRequestValue = None
|
|
||||||
self.type = 'holding register'
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return ("[{0!s}, publishTopic: {1}, subscribeTopic: {2}, feedbackTopic: {3}, "
|
|
||||||
"writeRequestValue: {4!s}"
|
|
||||||
.format(super().__str__(), self.publishTopic, self.subscribeTopic, self.feedbackTopic,
|
|
||||||
self.writeRequestValue))
|
|
||||||
|
|
||||||
def process(self, client, pubQueue):
|
|
||||||
logger = logging.getLogger('HoldingRegisterDatapoint')
|
|
||||||
if self.writeRequestValue:
|
|
||||||
# perform write operation
|
|
||||||
logger.debug("Holding register, perform write operation")
|
|
||||||
self.writeRequestValue = None
|
|
||||||
else:
|
|
||||||
# perform read operation
|
|
||||||
logger.debug("Holding register, perform read operation")
|
|
||||||
self.processCount += 1
|
|
||||||
result = client.read_holding_registers(address=self.address,
|
|
||||||
count=self.count,
|
|
||||||
unit=self.unit)
|
|
||||||
if type(result) in [ExceptionResponse, ModbusIOException]:
|
|
||||||
self.errorCount += 1
|
|
||||||
raise DatapointException(result)
|
|
||||||
logger.debug("{0}: {1!s}".format(self.label, result.registers))
|
|
||||||
pubQueue.put(MqttProcessor.PublishItem(self.publishTopic, str(result.registers)))
|
|
||||||
self.lastContact = datetime.datetime.now()
|
|
||||||
|
|
||||||
def onMessage(self, value):
|
|
||||||
self.writeRequestValue = value
|
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyDatapoint(AbstractModbusDatapoint):
|
|
||||||
def __init__(self, label=None, unit=None, address=None, count=None, scanRate=None, updateOnly=None, publishTopic=None):
|
|
||||||
super().__init__(label, unit, address, count, scanRate)
|
|
||||||
self.argList = self.argList + ['updateOnly', 'publishTopic']
|
|
||||||
self.updateOnly = updateOnly
|
|
||||||
self.lastValue = None
|
|
||||||
self.publishTopic = publishTopic
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return ("[{0!s}, updateOnly: {1}, publishTopic: {2}, lastValue: {3!s}"
|
|
||||||
.format(super().__str__(), self.updateOnly, self.publishTopic,
|
|
||||||
self.lastValue))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InputRegisterDatapoint(ReadOnlyDatapoint):
|
|
||||||
def __init__(self, label=None, unit=None, address=None, count=None, scanRate=None, updateOnly=None, publishTopic=None):
|
|
||||||
super().__init__(label, unit, address, count, scanRate, updateOnly, publishTopic)
|
|
||||||
self.type = 'input register'
|
|
||||||
|
|
||||||
def process(self, client, pubQueue):
|
|
||||||
logger = logging.getLogger('InputRegisterDatapoint')
|
|
||||||
# perform read operation
|
|
||||||
logger.debug("Input register, perform read operation")
|
|
||||||
self.processCount += 1
|
|
||||||
result = client.read_input_registers(address=self.address,
|
|
||||||
count=self.count,
|
|
||||||
unit=self.unit)
|
|
||||||
if type(result) in [ExceptionResponse, ModbusIOException]:
|
|
||||||
self.errorCount += 1
|
|
||||||
raise DatapointException(result)
|
|
||||||
if not self.updateOnly or (result.registers != self.lastValue):
|
|
||||||
self.lastValue = result.registers
|
|
||||||
logger.debug("{0}: {1!s}".format(self.label, result.registers))
|
|
||||||
pubQueue.put(MqttProcessor.PublishItem(self.publishTopic, str(result.registers)))
|
|
||||||
self.lastContact = datetime.datetime.now()
|
|
||||||
|
|
||||||
|
|
||||||
class DiscreteInputDatapoint(ReadOnlyDatapoint):
|
|
||||||
def __init__(self, label=None, unit=None, address=None, count=None, scanRate=None, updateOnly=None, publishTopic=None):
|
|
||||||
super().__init__(label, unit, address, count, scanRate, updateOnly, publishTopic)
|
|
||||||
self.type = 'discrete input'
|
|
||||||
|
|
||||||
def process(self, client, pubQueue):
|
|
||||||
logger = logging.getLogger('DiscreteInputDatapoint')
|
|
||||||
# perform read operation
|
|
||||||
logger.debug("Discrete input, perform read operation")
|
|
||||||
self.processCount += 1
|
|
||||||
result = client.read_discrete_inputs(address=self.address,
|
|
||||||
count=self.count,
|
|
||||||
unit=self.unit)
|
|
||||||
if type(result) in [ExceptionResponse, ModbusIOException]:
|
|
||||||
self.errorCount += 1
|
|
||||||
raise DatapointException(result)
|
|
||||||
if not self.updateOnly or (result.bits != self.lastValue):
|
|
||||||
self.lastValue = result.bits
|
|
||||||
logger.debug("{0}: {1!s}".format(self.label, result.bits))
|
|
||||||
pubQueue.put(MqttProcessor.PublishItem(self.publishTopic, str(result.bits)))
|
|
||||||
self.lastContact = datetime.datetime.now()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def saveRegisterList(registerList, registerListFile):
|
def saveRegisterList(registerList, registerListFile):
|
||||||
js = json.dumps(registerList, cls=JsonifyEncoder, sort_keys=True, indent=4)
|
js = json.dumps(registerList, cls=JsonifyEncoder, sort_keys=True, indent=4)
|
||||||
with open(registerListFile, 'w') as f:
|
with open(registerListFile, 'w') as f:
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
{
|
{
|
||||||
"args": {
|
"args": {
|
||||||
"address": 1,
|
"address": 1,
|
||||||
|
"converter": "dht20TOFloat",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"label": "Temperature",
|
"label": "Temperature",
|
||||||
"publishTopic": "Pub/Temperature",
|
"publishTopic": "Pub/Temperature",
|
||||||
"scanRate": 60.0,
|
"scanRate": 10.0,
|
||||||
"unit": 5,
|
"unit": 5,
|
||||||
"updateOnly": false
|
"updateOnly": false
|
||||||
},
|
},
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
{
|
{
|
||||||
"args": {
|
"args": {
|
||||||
"address": 2,
|
"address": 2,
|
||||||
|
"converter": "dht20TOFloat",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"label": "Humidity",
|
"label": "Humidity",
|
||||||
"publishTopic": "Pub/Humidity",
|
"publishTopic": "Pub/Humidity",
|
||||||
@@ -26,10 +28,12 @@
|
|||||||
{
|
{
|
||||||
"args": {
|
"args": {
|
||||||
"address": 0,
|
"address": 0,
|
||||||
|
"bitCount": 8,
|
||||||
|
"converter": null,
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"label": "Switches",
|
"label": "Switches",
|
||||||
"publishTopic": "Pub/Switches",
|
"publishTopic": "Pub/Switches",
|
||||||
"scanRate": 0.25,
|
"scanRate": null,
|
||||||
"unit": 4,
|
"unit": 4,
|
||||||
"updateOnly": false
|
"updateOnly": false
|
||||||
},
|
},
|
||||||
@@ -38,12 +42,13 @@
|
|||||||
{
|
{
|
||||||
"args": {
|
"args": {
|
||||||
"address": 40010,
|
"address": 40010,
|
||||||
|
"converter": "uint32",
|
||||||
"count": 2,
|
"count": 2,
|
||||||
"feedbackTopic": null,
|
"feedbackTopic": "FB/Counter1",
|
||||||
"label": "Counter1",
|
"label": "Counter1",
|
||||||
"publishTopic": "Pub/Counter1",
|
"publishTopic": "Pub/Counter1",
|
||||||
"scanRate": 60.0,
|
"scanRate": 1.0,
|
||||||
"subscribeTopic": null,
|
"subscribeTopic": "Sub/Counter1",
|
||||||
"unit": 4
|
"unit": 4
|
||||||
},
|
},
|
||||||
"type": "HoldingRegisterDatapoint"
|
"type": "HoldingRegisterDatapoint"
|
||||||
@@ -51,12 +56,13 @@
|
|||||||
{
|
{
|
||||||
"args": {
|
"args": {
|
||||||
"address": 40012,
|
"address": 40012,
|
||||||
|
"converter": "uint32",
|
||||||
"count": 2,
|
"count": 2,
|
||||||
"feedbackTopic": null,
|
"feedbackTopic": "FB/Counter2",
|
||||||
"label": "Counter2",
|
"label": "Counter2",
|
||||||
"publishTopic": "Pub/Counter2",
|
"publishTopic": "Pub/Counter2",
|
||||||
"scanRate": 60.0,
|
"scanRate": null,
|
||||||
"subscribeTopic": null,
|
"subscribeTopic": "Pub/Counter2",
|
||||||
"unit": 4
|
"unit": 4
|
||||||
},
|
},
|
||||||
"type": "HoldingRegisterDatapoint"
|
"type": "HoldingRegisterDatapoint"
|
||||||
@@ -64,12 +70,13 @@
|
|||||||
{
|
{
|
||||||
"args": {
|
"args": {
|
||||||
"address": 40014,
|
"address": 40014,
|
||||||
|
"converter": "uint32",
|
||||||
"count": 2,
|
"count": 2,
|
||||||
"feedbackTopic": null,
|
"feedbackTopic": "FB/Counter3",
|
||||||
"label": "Counter3",
|
"label": "Counter3",
|
||||||
"publishTopic": "Pub/Counter3",
|
"publishTopic": "Pub/Counter3",
|
||||||
"scanRate": 60.0,
|
"scanRate": null,
|
||||||
"subscribeTopic": null,
|
"subscribeTopic": "FB/Counter3",
|
||||||
"unit": 4
|
"unit": 4
|
||||||
},
|
},
|
||||||
"type": "HoldingRegisterDatapoint"
|
"type": "HoldingRegisterDatapoint"
|
||||||
@@ -77,53 +84,39 @@
|
|||||||
{
|
{
|
||||||
"args": {
|
"args": {
|
||||||
"address": 40016,
|
"address": 40016,
|
||||||
|
"converter": "uint32",
|
||||||
"count": 2,
|
"count": 2,
|
||||||
"feedbackTopic": null,
|
"feedbackTopic": "FB/Counter4",
|
||||||
"label": "Counter4",
|
"label": "Counter4",
|
||||||
"publishTopic": "Pub/Counter4",
|
"publishTopic": "Pub/Counter4",
|
||||||
"scanRate": 60.0,
|
|
||||||
"subscribeTopic": null,
|
|
||||||
"unit": 4
|
|
||||||
},
|
|
||||||
"type": "HoldingRegisterDatapoint"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"args": {
|
|
||||||
"address": 40001,
|
|
||||||
"count": 1,
|
|
||||||
"feedbackTopic": null,
|
|
||||||
"label": "Version",
|
|
||||||
"publishTopic": "Pub/Version",
|
|
||||||
"scanRate": 1.0,
|
"scanRate": 1.0,
|
||||||
"subscribeTopic": null,
|
"subscribeTopic": "Sub/Counter4",
|
||||||
"unit": 4
|
"unit": 4
|
||||||
},
|
},
|
||||||
"type": "HoldingRegisterDatapoint"
|
"type": "HoldingRegisterDatapoint"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"args": {
|
"args": {
|
||||||
"address": 40001,
|
"address": 0,
|
||||||
"count": 1,
|
"feedbackTopic": "FB/Coil1",
|
||||||
"feedbackTopic": null,
|
"label": "Coil1",
|
||||||
"label": "Version",
|
"publishTopic": "Pub/Coil1",
|
||||||
"publishTopic": "Pub/Version",
|
"scanRate": 1.0,
|
||||||
"scanRate": 10.0,
|
"subscribeTopic": "Sub/Coil1",
|
||||||
"subscribeTopic": null,
|
|
||||||
"unit": 4
|
"unit": 4
|
||||||
},
|
},
|
||||||
"type": "HoldingRegisterDatapoint"
|
"type": "CoilDatapoint"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"args": {
|
"args": {
|
||||||
"address": 40001,
|
"address": 1,
|
||||||
"count": 1,
|
"feedbackTopic": "FB/Coil2",
|
||||||
"feedbackTopic": "FB/Version",
|
"label": "Coil2",
|
||||||
"label": "Version_W",
|
"publishTopic": "Pub/Coil2",
|
||||||
"publishTopic": null,
|
"scanRate": 1.0,
|
||||||
"scanRate": 0.0,
|
"subscribeTopic": "Sub/Coil2",
|
||||||
"subscribeTopic": "Sub/Version",
|
|
||||||
"unit": 4
|
"unit": 4
|
||||||
},
|
},
|
||||||
"type": "HoldingRegisterDatapoint"
|
"type": "CoilDatapoint"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
Reference in New Issue
Block a user