Files
tac2100_solar_mbus2mqtt/env/lib/python3.11/site-packages/pyModbusTCP/utils.py
2025-01-03 15:06:21 +01:00

320 lines
8.8 KiB
Python

""" pyModbusTCP utils functions """
import re
import socket
import struct
###############
# bits function
###############
def get_bits_from_int(val_int, val_size=16):
"""Get the list of bits of val_int integer (default size is 16 bits).
Return bits list, the least significant bit first. Use list.reverse() for msb first.
:param val_int: integer value
:type val_int: int
:param val_size: bit length of integer (word = 16, long = 32) (optional)
:type val_size: int
:returns: list of boolean "bits" (the least significant first)
:rtype: list
"""
bits = []
# populate bits list with bool items of val_int
for i in range(val_size):
bits.append(bool((val_int >> i) & 0x01))
# return bits list
return bits
# short alias
int2bits = get_bits_from_int
def byte_length(bit_length):
"""Return the number of bytes needs to contain a bit_length structure.
:param bit_length: the number of bits
:type bit_length: int
:returns: the number of bytes
:rtype: int
"""
return (bit_length + 7) // 8
def test_bit(value, offset):
"""Test a bit at offset position.
:param value: value of integer to test
:type value: int
:param offset: bit offset (0 is lsb)
:type offset: int
:returns: value of bit at offset position
:rtype: bool
"""
mask = 1 << offset
return bool(value & mask)
def set_bit(value, offset):
"""Set a bit at offset position.
:param value: value of integer where set the bit
:type value: int
:param offset: bit offset (0 is lsb)
:type offset: int
:returns: value of integer with bit set
:rtype: int
"""
mask = 1 << offset
return int(value | mask)
def reset_bit(value, offset):
"""Reset a bit at offset position.
:param value: value of integer where reset the bit
:type value: int
:param offset: bit offset (0 is lsb)
:type offset: int
:returns: value of integer with bit reset
:rtype: int
"""
mask = ~(1 << offset)
return int(value & mask)
def toggle_bit(value, offset):
"""Return an integer with the bit at offset position inverted.
:param value: value of integer where invert the bit
:type value: int
:param offset: bit offset (0 is lsb)
:type offset: int
:returns: value of integer with bit inverted
:rtype: int
"""
mask = 1 << offset
return int(value ^ mask)
########################
# Word convert functions
########################
def word_list_to_long(val_list, big_endian=True, long_long=False):
"""Word list (16 bits) to long (32 bits) or long long (64 bits) list.
By default, word_list_to_long() use big endian order. For use little endian, set
big_endian param to False. Output format could be long long with long_long.
option set to True.
:param val_list: list of 16 bits int value
:type val_list: list
:param big_endian: True for big endian/False for little (optional)
:type big_endian: bool
:param long_long: True for long long 64 bits, default is long 32 bits (optional)
:type long_long: bool
:returns: list of 32 bits int value
:rtype: list
"""
long_list = []
block_size = 4 if long_long else 2
# populate long_list (len is half or quarter of 16 bits val_list) with 32 or 64 bits value
for index in range(int(len(val_list) / block_size)):
start = block_size * index
long = 0
if big_endian:
if long_long:
long += (val_list[start] << 48) + (val_list[start + 1] << 32)
long += (val_list[start + 2] << 16) + (val_list[start + 3])
else:
long += (val_list[start] << 16) + val_list[start + 1]
else:
if long_long:
long += (val_list[start + 3] << 48) + (val_list[start + 2] << 32)
long += (val_list[start + 1] << 16) + val_list[start]
long_list.append(long)
# return long list
return long_list
# short alias
words2longs = word_list_to_long
def long_list_to_word(val_list, big_endian=True, long_long=False):
"""Long (32 bits) or long long (64 bits) list to word (16 bits) list.
By default long_list_to_word() use big endian order. For use little endian, set
big_endian param to False. Input format could be long long with long_long
param to True.
:param val_list: list of 32 bits int value
:type val_list: list
:param big_endian: True for big endian/False for little (optional)
:type big_endian: bool
:param long_long: True for long long 64 bits, default is long 32 bits (optional)
:type long_long: bool
:returns: list of 16 bits int value
:rtype: list
"""
word_list = []
# populate 16 bits word_list with 32 or 64 bits value of val_list
for val in val_list:
block_l = [val & 0xffff, (val >> 16) & 0xffff]
if long_long:
block_l.append((val >> 32) & 0xffff)
block_l.append((val >> 48) & 0xffff)
if big_endian:
block_l.reverse()
word_list.extend(block_l)
# return long list
return word_list
# short alias
longs2words = long_list_to_word
##########################
# 2's complement functions
##########################
def get_2comp(val_int, val_size=16):
"""Get the 2's complement of Python int val_int.
:param val_int: int value to apply 2's complement
:type val_int: int
:param val_size: bit size of int value (word = 16, long = 32) (optional)
:type val_size: int
:returns: 2's complement result
:rtype: int
:raises ValueError: if mismatch between val_int and val_size
"""
# avoid overflow
if not (-1 << val_size - 1) <= val_int < (1 << val_size):
err_msg = 'could not compute two\'s complement for %i on %i bits'
err_msg %= (val_int, val_size)
raise ValueError(err_msg)
# test negative int
if val_int < 0:
val_int += 1 << val_size
# test MSB (do two's comp if set)
elif val_int & (1 << (val_size - 1)):
val_int -= 1 << val_size
return val_int
# short alias
twos_c = get_2comp
def get_list_2comp(val_list, val_size=16):
"""Get the 2's complement of Python list val_list.
:param val_list: list of int value to apply 2's complement
:type val_list: list
:param val_size: bit size of int value (word = 16, long = 32) (optional)
:type val_size: int
:returns: 2's complement result
:rtype: list
"""
return [get_2comp(val, val_size) for val in val_list]
# short alias
twos_c_l = get_list_2comp
###############################
# IEEE floating-point functions
###############################
def decode_ieee(val_int, double=False):
"""Decode Python int (32 bits integer) as an IEEE single or double precision format.
Support NaN.
:param val_int: a 32 or 64 bits integer as an int Python value
:type val_int: int
:param double: set to decode as a 64 bits double precision,
default is 32 bits single (optional)
:type double: bool
:returns: float result
:rtype: float
"""
if double:
return struct.unpack("d", struct.pack("Q", val_int))[0]
else:
return struct.unpack("f", struct.pack("I", val_int))[0]
def encode_ieee(val_float, double=False):
"""Encode Python float to int (32 bits integer) as an IEEE single or double precision format.
Support NaN.
:param val_float: float value to convert
:type val_float: float
:param double: set to encode as a 64 bits double precision,
default is 32 bits single (optional)
:type double: bool
:returns: IEEE 32 bits (single precision) as Python int
:rtype: int
"""
if double:
return struct.unpack("Q", struct.pack("d", val_float))[0]
else:
return struct.unpack("I", struct.pack("f", val_float))[0]
################
# misc functions
################
def crc16(frame):
"""Compute CRC16.
:param frame: frame
:type frame: bytes
:returns: CRC16
:rtype: int
"""
crc = 0xFFFF
for item in frame:
next_byte = item
crc ^= next_byte
for _ in range(8):
lsb = crc & 1
crc >>= 1
if lsb:
crc ^= 0xA001
return crc
def valid_host(host_str):
"""Validate a host string.
Can be an IPv4/6 address or a valid hostname.
:param host_str: the host string to test
:type host_str: str
:returns: True if host_str is valid
:rtype: bool
"""
# IPv4 valid address ?
try:
socket.inet_pton(socket.AF_INET, host_str)
return True
except socket.error:
pass
# IPv6 valid address ?
try:
socket.inet_pton(socket.AF_INET6, host_str)
return True
except socket.error:
pass
# valid hostname ?
if re.match(r'^[a-z][a-z0-9.\-]+$', host_str):
return True
# on invalid host
return False