Last active
October 28, 2023 01:38
-
-
Save casebeer/91d9063aa8672d9c19567ff20db09e9f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
TUF-2000m | |
Module to test TUF-2000m ultrasonic flow meter | |
via a Modbus RTU to Modbus TCP bridge (eByte NA111-A) | |
''' | |
import csv | |
import sys | |
import os | |
from pymodbus.client import ModbusTcpClient | |
from pymodbus.payload import BinaryPayloadDecoder | |
from pymodbus.constants import Endian | |
# CONSTANTS | |
# IP address of the RTU-to-TCP bridge | |
MODBUS_BRIDGE_ADDRESS = None # FIXME | |
# Modbus RTU bus address of the TUF-2000M | |
TUF2000M_DEVICE_ADDRESS = None # FIXME | |
# Schema file based on copy/pasting the TUF-2000m documentation PDF's Modbus registers table | |
# then cleaning up and making minior tweaks (e.g. adding units to register descriptions) | |
# See https://images-na.ssl-images-amazon.com/images/I/91CvZHsNYBL.pdf | |
TUF2000M_REGISTERS_SCHEMA_FILE = 'tuf2000m_registers.csv' | |
# The TUF-2000m documentation seems to provide register addresses off by one address too high. | |
# This offest will be added to documented register addresss (loaded from the CSV). | |
REGISTER_ADDRESS_OFFSET = -1 | |
# override values from env variables if they're set | |
MODBUS_BRIDGE_ADDRESS = os.environ.get('MODBUS_BRIDGE_ADDRESS', MODBUS_BRIDGE_ADDRESS) | |
TUF2000M_DEVICE_ADDRESS = int(os.environ.get('TUF2000M_DEVICE_ADDRESS', TUF2000M_DEVICE_ADDRESS)) | |
def make_decoder(registers): | |
''' | |
Return a decoder instance loaded with provided register values and | |
correct endianness settings for the TUF-2000M (i.e. big-endian byte order | |
and little-endian word order) | |
''' | |
return BinaryPayloadDecoder.fromRegisters( | |
registers, | |
byteorder=Endian.Big, | |
wordorder=Endian.Little | |
) | |
def bcd_digits(char): | |
'''interpret a char as two single-digit BCDs in big-endian nibbleorder''' | |
return [(char >> 4 & 0xf), (char & 0xf)] | |
def bcd(char): | |
'''interpret a char as a two-digit BCD in big-endian nibbleorder''' | |
tens, ones = bcd_digits(char) | |
return 10 * tens + ones | |
def convert_bcd_registers(registers): | |
''' | |
Interpret a list of two-byte ints as a BCD string, one nibble per decimal digit, | |
in big-endian word order, little-endian byte order, and big-endian nibble order. | |
i.e. [ 0x3412, 0x7856 ] => '12345678' | |
''' | |
def helper(): | |
for reg in registers: | |
# low byte first per documentation | |
for char in [reg & 0xff, reg >> 8 & 0xff]: | |
# bcd_digits() will give us high nibble first for big-endian nibbleorder | |
# preferring two single-digit decimal ints since we're interpreting as string of BCDs | |
for digit in bcd_digits(char): | |
yield digit | |
#yield bcd(char) # convert two nibbles into a two-digit decimal integer | |
# Convert to string | |
# Note that the docs suggest writing _hex_ digits as "BCD" digits, so we'll | |
# convert the digits as BC-hex rather than BC-decimal to support this | |
return ''.join(format(digit, 'x') for digit in helper()) | |
def array_int8_from_int16(num): | |
'''Convert a single int16 to two int8s''' | |
return [ num >> 8 & 0xff, num & 0xff ] | |
# Test struct unpacking of Quality byte for use in Home Assistant as custom modbus sensor | |
#import struct | |
#def convert(registers): | |
# return struct.unpack('>xb', struct.pack('>H', registers[0])) | |
converters = { | |
'BITMASK': lambda registers: [format(reg, '0b') for reg in registers], | |
'INT16': lambda registers: make_decoder(registers).decode_16bit_int(), | |
'ARR_INT8': lambda registers: array_int8_from_int16(make_decoder(registers).decode_16bit_int()), | |
'FLOAT32': lambda registers: make_decoder(registers).decode_32bit_float(), | |
'INT32': lambda registers: make_decoder(registers).decode_32bit_int(), | |
'UINT32': lambda registers: make_decoder(registers).decode_32bit_uint(), | |
#'BCD': lambda registers: [bcd(char) for reg in registers for char in [reg & 0xff, reg >> 8 & 0xff]], | |
'BCD': convert_bcd_registers, | |
} | |
def load_schema(): | |
'''Load register definitions from CSV''' | |
registers = {} | |
with open(TUF2000M_REGISTERS_SCHEMA_FILE, 'r', encoding='utf-8') as csvfile: | |
for row in csv.DictReader(csvfile, delimiter='|'): | |
# n.b. fixing off by one documentation here | |
register = int(row['registers'].split('-')[0]) + REGISTER_ADDRESS_OFFSET | |
count = int(row['count']) | |
#print(f"{row['description']}\t{register}\t{count}") | |
registers[register] = { | |
'register': register, | |
'count': count, | |
'name': row['description'], | |
'format': row['format'], | |
'converter': converters[row['format']], | |
'note': row['note'], | |
} | |
return registers | |
def test_off_by_one_documentation(client): | |
'''Retrieve values from documented and (documented - 1) register addresses to compare''' | |
result = client.read_holding_registers(33, 2, TUF2000M_DEVICE_ADDRESS) | |
if result.isError(): | |
print(result) | |
else: | |
r33 = converters['FLOAT32'](result.registers) | |
print(f"Temp1 from [32] {get(client, 32)}") | |
print(f"Temp1 from [33] {r33} (as documented)") | |
def get(client, register): | |
'''Get a register or register group from the TUF2000M''' | |
schema = registers_schema[register] | |
result = client.read_holding_registers(register, schema['count'], TUF2000M_DEVICE_ADDRESS) | |
if result.isError(): | |
raise Exception(result) | |
return schema['converter'](result.registers) | |
def main(): | |
'''main method''' | |
if len(sys.argv) > 1: | |
# pass register addresses to read as command line arguments | |
registers = [ int(arg) for arg in sys.argv[1:] ] | |
else: | |
# if no registers provided as args, print all documented registers | |
registers = registers_schema.keys() | |
client = ModbusTcpClient(MODBUS_BRIDGE_ADDRESS) | |
client.connect() | |
#test_off_by_one_documentation(client) | |
#return | |
for register in registers: | |
schema = registers_schema[register] | |
print(f"[{register}] {schema['format']} {schema['name']} = {get(client, register)}") | |
registers_schema = load_schema() | |
#pprint(registers_schema) | |
if __name__ == '__main__': | |
main() |
We can make this file beautiful and searchable if this error is corrected: It looks like row 6 should actually have 1 column, instead of 2. in line 5.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
registers|count|description|format|note | |
0001-0002|2|Flow Rate (m^3/h)|FLOAT32|Unit: m3/h | |
0003-0004|2|Energy Flow Rate (GJ/h)|FLOAT32|Unit: GJ/h | |
0005-0006|2|Velocity (m/s)|FLOAT32|Unit: m/s | |
0007-0008|2|Fluid sound speed (m/s)|FLOAT32|Unit: m/s | |
0009-0010|2|Positive accumulator|INT32|Unit is selected by M31, and depends on totalizer multiplier | |
0011-0012|2|Positive decimal fraction|FLOAT32|Same unit as the integer part | |
0013-0014|2|Negative accumulator|INT32|Long is a signed 4-byte integer, lower byte first | |
0015-0016|2|Negative decimal fraction|FLOAT32|FLOAT32 is a format of Singular IEEE-754 number, also called FLOAT | |
0017-0018|2|Positive energy accumulator|INT32| | |
0019-0020|2|Positive energy decimal fraction|FLOAT32| | |
0021-0022|2|Negative energy accumulator|INT32| | |
0023-0024|2|Negative energy decimal fraction|FLOAT32| | |
0025-0026|2|Net accumulator|INT32| | |
0027-0028|2|Net decimal fraction|FLOAT32| | |
0029-0030|2|Net energy accumulator|INT32| | |
0031-0032|2|Net energy decimal fraction|FLOAT32| | |
0033-0034|2|Temperature #1/inlet (°C)|FLOAT32|Unit: C | |
0035-0036|2|Temperature #2/outlet (°C)|FLOAT32|Unit: C | |
0037-0038|2|Analog input AI3|FLOAT32| | |
0039-0040|2|Analog input AI4|FLOAT32| | |
0041-0042|2|Analog input AI5|FLOAT32| | |
0043-0044|2|Current input at AI3 (mA)|FLOAT32|In unit mA | |
0045-0046|2|Current input at AI3 (mA)|FLOAT32|In unit mA | |
0047-0048|2|Current input at AI3 (mA)|FLOAT32|In unit mA | |
0049-0050|2|System password|BCD|Writable。00H for unlock | |
0051|1|Password for hardware|BCD|Writable。“A55Ah” for unlock | |
0053-0055|3|Calendar (date and time)|BCD|Writable。6 Bytes of BCD stands SMHDMY,lower byte first | |
0056|1|Day+Hour for Auto-Save|BCD|Writable。For example 0512H stands Auto-save on 12:00 on 5th。0012H for 12:00 on everyday | |
0059|1|Key to input|INT16|Writable | |
0060|1|Go to Window #|INT16|Writable。 | |
0061|1|LCD Back-lit lights for number of seconds|INT16|Writable。In unit second | |
0062|1|Times for the beeper|INT16|Writable。Max 255 | |
0062|1|Pulses left for OCT|INT16|Writable。Max 65535 | |
0072|1|Error Code|BITMASK|16bits, see note 4 | |
0077-0078|2|PT100 resistance of inlet (Ohm)|FLOAT32|In unit Ohm | |
0079-0080|2|PT100 resistance of outlet (Ohm)|FLOAT32|In unit Ohm | |
0081-0082|2|Total travel time (µs)|FLOAT32|In unit Micro-second | |
0083-0084|2|Delta travel time (ns)|FLOAT32|In unit Nino-second | |
0085-0086|2|Upstream travel time (µs)|FLOAT32|In unit Micro-second | |
0087-0088|2|Downstream travel time (µs)|FLOAT32|In unit Micro-second | |
0089-0090|2|Output current (mA)|FLOAT32|In unit mA | |
0092|1|Working step and Signal Quality|ARR_INT8|The high byte is the step and low for signal quality, range 00-99,the larger the better. | |
0093|1|Upstream strength|INT16|Range 0-2047 | |
0094|1|Downstream strength|INT16|Range 0-2047 | |
0096|1|Language used in user interface|INT16|0:English, 1:Chinese Other language will be supported later | |
0097-0098|2|The rate of the measured travel time by the calculated travel time. (%)|FLOAT32|Normal 100+-3% | |
0099-0100|2|Reynolds number|FLOAT32| | |
0101-0102|2|Pipe Reynolds factor|FLOAT32| | |
0103-0104|2|Working Timer (s)|UINT32|unsigned,in second | |
0105-0106|2|Total working time (s)|UINT32|unsigned,in second | |
0105-0106|2|Total power on-off time|UINT32|Unsigned | |
0113-0114|2|Net accumulator (m^3)|FLOAT32|In Cubic Meter,float | |
0115-0116|2|Positive accumulator (m^3)|FLOAT32|In Cubic Meter,float | |
0117-0118|2|Negative accumulator (m^3)|FLOAT32|In Cubic Meter,float | |
0119-0120|2|Net energy accumulator (GJ)|FLOAT32|In GJ,float | |
0121-0122|2|Positive energy accumulator (GJ)|FLOAT32|In GJ,float | |
0123-0124|2|Negative energy accumulator (GJ)|FLOAT32|In GJ,float | |
0125-0126|2|Flow for today (m^3)|FLOAT32|In Cubic Meter,float | |
0127-0128|2|Flow for this month (m^3)|FLOAT32|In Cubic Meter,float | |
0129-0130|2|Manual accumulator|INT32| | |
0131-0132|2|Manual accumulator decimal fraction|FLOAT32| | |
0133-0134|2|Batch accumulator|INT32| | |
0135-0136|2|Batch accumulator decimal fraction|FLOAT32| | |
0137-0138|2|Flow for today|INT32| | |
0139-0140|2|Flow for today decimal fraction|FLOAT32| | |
0141-0142|2|Flow for this month|INT32| | |
0143-0144|2|Flow for this month decimal fraction|FLOAT32| | |
0145-0146|2|Flow for this year|INT32| | |
0147-0148|2|Flow for this year decimal fraction|FLOAT32| | |
0158|1|Current display window|INT16| | |
0165-0166|2|Failure timer (s)|INT32|In unit second | |
0173-0174|2|Current output frequency (Hz)|FLOAT32|Unit:Hz | |
0175-0176|2|Current output with 4-20mA (mA)|FLOAT32|Unit:mA | |
0181-0182|2|Temperature difference (°C)|FLOAT32|Unit:C | |
0183-0184|2|Lost flow for period of last power off (m^3)|FLOAT32|Unit:Cubic Meter | |
0185-0186|2|Clock coefficient (should be < 0.1)|FLOAT32|Should less than 0.1 | |
0187-0188|2|Total time for Auto-Save|FLOAT32|Time to save by 0056 | |
0189-0190|2|POS flow for Auto-Save|FLOAT32|Time to save by 0056 | |
0191-0192|2|Flow rate for Auto-Save|FLOAT32|Time to save by 0056 | |
0221-0222|2|Inner pipe diameter (mm)|FLOAT32|In millimeter | |
0229-0230|2|Upstream delay (µs)|FLOAT32|In microsecond | |
0231-0232|2|Downstream delay (µs)|FLOAT32|In microsecond | |
0233-0234|2|Calculated travel time (µs)|FLOAT32|In microsecond | |
0257-0288|32|LCD buffer|BCD| | |
0289|1|LCD buffer pointer|INT16| | |
0311|2|Worked time for today (s)|UINT32|Unsigned, in seconds | |
0313|2|Worked time for this month (s)|UINT32|Unsigned, in seconds | |
1437|1|Unit for flow rate|INT16|See note 5 | |
1438|1|Unit for flow totalizer|INT16|Range 0~7,see note 1 | |
1439|1|Multiplier for totalizer|INT16|Range 0~7,see note 1 | |
1440|1|Multiplier for energy accumulator|INT16|Range 0~10,see note 1 | |
1441|1|Unit for energy rate|INT16|0=GJ 1=Kcal 2=KWh,3=BTU | |
1442|1|Device address|INT16| | |
1451|2|User scale factor|FLOAT32| | |
1521|2|Manufacturer scale factor|FLOAT32|Read only | |
1529|2|Electronic serial number|BCD|High byte first |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment