320 lines
8.8 KiB
Python
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
|