Merge remote-tracking branch 'refs/remotes/Cat/master'
# Conflicts: # scenes/настройки/настройки.gd
This commit is contained in:
3
connect-to-uf.sh
Normal file
3
connect-to-uf.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
xfreerdp /u:user /p:12345678 /v:10.1.1.2 /f
|
||||
@@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Драйвер клиента для интерфейса Modbus работающего через последовательный порт.
|
||||
(приложение | UDP | JSON) <-> (этот драйвер клиента) <-> (сервер | Serial | Modbus RTU)
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
import time
|
||||
from socket import *
|
||||
|
||||
import logger
|
||||
from pymodbus.client.sync import ModbusSerialClient
|
||||
|
||||
DEF_SERIAL = 'COM3' if os.name == 'nt' else '/dev/tty0'
|
||||
log_file_dir = os.path.join(os.path.expanduser('~'))
|
||||
log = logger.get_logger(__file__, log_file_dir)
|
||||
EXCEPT_MSGS = {
|
||||
'write': 'ошибка: запись в регистры не выполнена',
|
||||
'read_holding': 'ошибка: чтение регистров хранения не выполнено',
|
||||
'read_input': 'ошибка: чтение регистров входов не выполнено',
|
||||
'<unknown>': 'ошибка: получена неверная команда'}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('--modbus-unit', default=1, type=int, help='Номер устройства Modbus')
|
||||
parser.add_argument('--serial-baud', default=115200, type=int, help='Скорость последовательного порта, бод')
|
||||
parser.add_argument('--serial-port', default=DEF_SERIAL, type=str, help='Последовательный порт для подключения к Modbus серверу')
|
||||
parser.add_argument('--udp-addr', default='localhost', type=str, help='Адрес для отправки сообщений приложению')
|
||||
parser.add_argument('--udp-port-rx', default=44100, type=int, help='Порт для приёма сообщений от приложения')
|
||||
parser.add_argument('--udp-port-tx', default=48000, type=int, help='Порт для отправки сообщений приложению')
|
||||
opts, opts_unk = parser.parse_known_args()
|
||||
print_opts_error(opts_unk)
|
||||
while True:
|
||||
run_sync_client(opts.udp_addr, opts.udp_port_rx, opts.udp_port_tx, opts.serial_port, opts.serial_baud, opts.modbus_unit)
|
||||
time.sleep(3)
|
||||
log.warning('Переподключение к серверу')
|
||||
|
||||
|
||||
def run_sync_client(udp_addr, udp_port_rx, udp_port_tx, serial_port, serial_baud, unit_num):
|
||||
sock = socket(AF_INET, SOCK_DGRAM)
|
||||
sock.bind((udp_addr, udp_port_rx))
|
||||
log.info('Привязан к адресу %s:%d' % (udp_addr, udp_port_rx))
|
||||
|
||||
json_decoder = json.JSONDecoder()
|
||||
json_encoder = json.JSONEncoder()
|
||||
|
||||
client = ModbusSerialClient(method='rtu', port=serial_port, baudrate=int(serial_baud))
|
||||
while not client.connect(): # Ждать подключения к серверу
|
||||
time.sleep(3)
|
||||
|
||||
log.info('Подключен к серверу через \"%s\" на скорости %d бод' % (serial_port, serial_baud))
|
||||
tx_data = json_encoder.encode({'code': 'info', 'data': 'ok'})
|
||||
sock.sendto(bytes(tx_data, 'utf-8'), (udp_addr, udp_port_tx))
|
||||
while True:
|
||||
try:
|
||||
udp_data = sock.recv(2048) # Ждать команду от приложения
|
||||
except:
|
||||
return
|
||||
udp_data = udp_data.decode('utf-8')
|
||||
message = json_decoder.decode(udp_data) # type: dict
|
||||
|
||||
if 'code' not in message:
|
||||
message['code'] = '<unknown>'
|
||||
|
||||
if 'data' not in message:
|
||||
message['data'] = []
|
||||
|
||||
if 'unit' not in message:
|
||||
message['unit'] = unit_num
|
||||
|
||||
rc = False
|
||||
if message['code'] == 'write':
|
||||
data = message['data']
|
||||
rq = client.write_registers(data[0], data[1:], unit=message['unit'])
|
||||
rc = rq.isError()
|
||||
if not rc:
|
||||
tx_data = json_encoder.encode({'code': 'resp_write', 'data': ''})
|
||||
sock.sendto(bytes(tx_data, 'utf-8'), (udp_addr, udp_port_tx))
|
||||
elif message['code'] == 'read_holding':
|
||||
data = message['data']
|
||||
rq = client.read_holding_registers(data[0], data[1], unit=message['unit'])
|
||||
rc = rq.isError()
|
||||
if not rc:
|
||||
tx_data = json_encoder.encode({'code': 'resp_holding', 'data': [data[0]] + rq.registers})
|
||||
sock.sendto(bytes(tx_data, 'utf-8'), (udp_addr, udp_port_tx))
|
||||
elif message['code'] == 'read_input':
|
||||
data = message['data']
|
||||
rq = client.read_input_registers(data[0], data[1], unit=message['unit'])
|
||||
rc = rq.isError()
|
||||
if not rc:
|
||||
tx_data = json_encoder.encode({'code': 'resp_input', 'data': [data[0]] + rq.registers})
|
||||
sock.sendto(bytes(tx_data, 'utf-8'), (udp_addr, udp_port_tx))
|
||||
elif message['code'] == '<unknown>':
|
||||
rc = False
|
||||
|
||||
if rc:
|
||||
code = message['code']
|
||||
data = message['data']
|
||||
rc_msg = EXCEPT_MSGS[code]
|
||||
tx_data = json_encoder.encode({'code': 'exception', 'data': rc_msg, 'unit': message['unit']})
|
||||
sock.sendto(bytes(tx_data, 'utf-8'), (udp_addr, udp_port_tx))
|
||||
log.error('Команда \"%s\" не выполнена. данные: \"%s\", порт: \"%s\", скорость: %s'
|
||||
% (code, data, serial_port, serial_baud))
|
||||
|
||||
|
||||
def print_opts_error(opts: list) -> None:
|
||||
for opt in opts:
|
||||
log.warning('Неизвестный аргумент: \"%s\"' % opt)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
def get_logger(source_file, log_dir='logs'):
|
||||
import logging
|
||||
import os
|
||||
source_file = os.path.split(source_file)[-1]
|
||||
log_format = '[%(asctime)s] %(module)s:%(lineno)s %(message)s'
|
||||
logging.basicConfig(format=log_format)
|
||||
log = logging.getLogger()
|
||||
log.setLevel(logging.INFO)
|
||||
file_handler = logging.FileHandler(f'{log_dir}/{source_file}.log')
|
||||
file_handler.setFormatter(logging.Formatter(log_format))
|
||||
log.addHandler(file_handler)
|
||||
return log
|
||||
@@ -1,31 +0,0 @@
|
||||
'''
|
||||
Pymodbus: Modbus Protocol Implementation
|
||||
-----------------------------------------
|
||||
|
||||
TwistedModbus is built on top of the code developed by:
|
||||
|
||||
Copyright (c) 2001-2005 S.W.A.C. GmbH, Germany.
|
||||
Copyright (c) 2001-2005 S.W.A.C. Bohemia s.r.o., Czech Republic.
|
||||
Hynek Petrak <hynek@swac.cz>
|
||||
|
||||
Released under the the BSD license
|
||||
'''
|
||||
|
||||
import pymodbus.version as __version
|
||||
__version__ = __version.version.short()
|
||||
__author__ = 'Galen Collins'
|
||||
__maintainer__ = 'dhoomakethu'
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Block unhandled logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging as __logging
|
||||
try:
|
||||
from logging import NullHandler as __null
|
||||
except ImportError:
|
||||
class __null(__logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
__logging.getLogger(__name__).addHandler(__null())
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
"""
|
||||
Bit Reading Request/Response messages
|
||||
--------------------------------------
|
||||
|
||||
"""
|
||||
import struct
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
from pymodbus.compat import byte2int
|
||||
|
||||
|
||||
class ReadBitsRequestBase(ModbusRequest):
|
||||
''' Base class for Messages Requesting bit values '''
|
||||
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address, count, **kwargs):
|
||||
''' Initializes the read request data
|
||||
|
||||
:param address: The start address to read from
|
||||
:param count: The number of bits after 'address' to read
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
''' Encodes a request pdu
|
||||
|
||||
:returns: The encoded pdu
|
||||
'''
|
||||
return struct.pack('>HH', self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a request pdu
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, self.count = struct.unpack('>HH', data)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Byte Count(1 byte) + Quantity of Coils (n Bytes)/8,
|
||||
if the remainder is different of 0 then N = N+1
|
||||
:return:
|
||||
"""
|
||||
count = self.count//8
|
||||
if self.count % 8:
|
||||
count += 1
|
||||
|
||||
return 1 + 1 + count
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "ReadBitRequest(%d,%d)" % (self.address, self.count)
|
||||
|
||||
|
||||
class ReadBitsResponseBase(ModbusResponse):
|
||||
''' Base class for Messages responding to bit-reading values '''
|
||||
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, values, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param values: The requested values to be returned
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.bits = values or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes response pdu
|
||||
|
||||
:returns: The encoded packet message
|
||||
'''
|
||||
result = pack_bitstring(self.bits)
|
||||
packet = struct.pack(">B", len(result)) + result
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes response pdu
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.byte_count = byte2int(data[0])
|
||||
self.bits = unpack_bitstring(data[1:])
|
||||
|
||||
def setBit(self, address, value=1):
|
||||
''' Helper function to set the specified bit
|
||||
|
||||
:param address: The bit to set
|
||||
:param value: The value to set the bit to
|
||||
'''
|
||||
self.bits[address] = (value != 0)
|
||||
|
||||
def resetBit(self, address):
|
||||
''' Helper function to set the specified bit to 0
|
||||
|
||||
:param address: The bit to reset
|
||||
'''
|
||||
self.setBit(address, 0)
|
||||
|
||||
def getBit(self, address):
|
||||
''' Helper function to get the specified bit's value
|
||||
|
||||
:param address: The bit to query
|
||||
:returns: The value of the requested bit
|
||||
'''
|
||||
return self.bits[address]
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "%s(%d)" % (self.__class__.__name__, len(self.bits))
|
||||
|
||||
|
||||
class ReadCoilsRequest(ReadBitsRequestBase):
|
||||
'''
|
||||
This function code is used to read from 1 to 2000(0x7d0) contiguous status
|
||||
of coils in a remote device. The Request PDU specifies the starting
|
||||
address, ie the address of the first coil specified, and the number of
|
||||
coils. In the PDU Coils are addressed starting at zero. Therefore coils
|
||||
numbered 1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 1
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start reading from
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
ReadBitsRequestBase.__init__(self, address, count, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read coils request against a datastore
|
||||
|
||||
Before running the request, we make sure that the request is in
|
||||
the max valid range (0x001-0x7d0). Next we make sure that the
|
||||
request is valid against the current datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The initializes response message, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.count <= 0x7d0):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
return ReadCoilsResponse(values)
|
||||
|
||||
|
||||
class ReadCoilsResponse(ReadBitsResponseBase):
|
||||
'''
|
||||
The coils in the response message are packed as one coil per bit of
|
||||
the data field. Status is indicated as 1= ON and 0= OFF. The LSB of the
|
||||
first data byte contains the output addressed in the query. The other
|
||||
coils follow toward the high order end of this byte, and from low order
|
||||
to high order in subsequent bytes.
|
||||
|
||||
If the returned output quantity is not a multiple of eight, the
|
||||
remaining bits in the final data byte will be padded with zeros
|
||||
(toward the high order end of the byte). The Byte Count field specifies
|
||||
the quantity of complete bytes of data.
|
||||
'''
|
||||
function_code = 1
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Intializes a new instance
|
||||
|
||||
:param values: The request values to respond with
|
||||
'''
|
||||
ReadBitsResponseBase.__init__(self, values, **kwargs)
|
||||
|
||||
|
||||
class ReadDiscreteInputsRequest(ReadBitsRequestBase):
|
||||
'''
|
||||
This function code is used to read from 1 to 2000(0x7d0) contiguous status
|
||||
of discrete inputs in a remote device. The Request PDU specifies the
|
||||
starting address, ie the address of the first input specified, and the
|
||||
number of inputs. In the PDU Discrete Inputs are addressed starting at
|
||||
zero. Therefore Discrete inputs numbered 1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 2
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Intializes a new instance
|
||||
|
||||
:param address: The address to start reading from
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
ReadBitsRequestBase.__init__(self, address, count, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read discrete input request against a datastore
|
||||
|
||||
Before running the request, we make sure that the request is in
|
||||
the max valid range (0x001-0x7d0). Next we make sure that the
|
||||
request is valid against the current datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The initializes response message, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.count <= 0x7d0):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
return ReadDiscreteInputsResponse(values)
|
||||
|
||||
|
||||
class ReadDiscreteInputsResponse(ReadBitsResponseBase):
|
||||
'''
|
||||
The discrete inputs in the response message are packed as one input per
|
||||
bit of the data field. Status is indicated as 1= ON; 0= OFF. The LSB of
|
||||
the first data byte contains the input addressed in the query. The other
|
||||
inputs follow toward the high order end of this byte, and from low order
|
||||
to high order in subsequent bytes.
|
||||
|
||||
If the returned input quantity is not a multiple of eight, the
|
||||
remaining bits in the final data byte will be padded with zeros
|
||||
(toward the high order end of the byte). The Byte Count field specifies
|
||||
the quantity of complete bytes of data.
|
||||
'''
|
||||
function_code = 2
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Intializes a new instance
|
||||
|
||||
:param values: The request values to respond with
|
||||
'''
|
||||
ReadBitsResponseBase.__init__(self, values, **kwargs)
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ReadCoilsRequest", "ReadCoilsResponse",
|
||||
"ReadDiscreteInputsRequest", "ReadDiscreteInputsResponse",
|
||||
]
|
||||
@@ -1,270 +0,0 @@
|
||||
"""
|
||||
Bit Writing Request/Response
|
||||
------------------------------
|
||||
|
||||
TODO write mask request/response
|
||||
"""
|
||||
import struct
|
||||
from pymodbus.constants import ModbusStatus
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Local Constants
|
||||
#---------------------------------------------------------------------------#
|
||||
# These are defined in the spec to turn a coil on/off
|
||||
#---------------------------------------------------------------------------#
|
||||
_turn_coil_on = struct.pack(">H", ModbusStatus.On)
|
||||
_turn_coil_off = struct.pack(">H", ModbusStatus.Off)
|
||||
|
||||
|
||||
class WriteSingleCoilRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to write a single output to either ON or OFF
|
||||
in a remote device.
|
||||
|
||||
The requested ON/OFF state is specified by a constant in the request
|
||||
data field. A value of FF 00 hex requests the output to be ON. A value
|
||||
of 00 00 requests it to be OFF. All other values are illegal and will
|
||||
not affect the output.
|
||||
|
||||
The Request PDU specifies the address of the coil to be forced. Coils
|
||||
are addressed starting at zero. Therefore coil numbered 1 is addressed
|
||||
as 0. The requested ON/OFF state is specified by a constant in the Coil
|
||||
Value field. A value of 0XFF00 requests the coil to be ON. A value of
|
||||
0X0000 requests the coil to be off. All other values are illegal and
|
||||
will not affect the coil.
|
||||
'''
|
||||
function_code = 5
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The variable address to write
|
||||
:param value: The value to write at address
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.value = bool(value)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes write coil request
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
result = struct.pack('>H', self.address)
|
||||
if self.value: result += _turn_coil_on
|
||||
else: result += _turn_coil_off
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a write coil request
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, value = struct.unpack('>HH', data)
|
||||
self.value = (value == ModbusStatus.On)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a write coil request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response or exception message
|
||||
'''
|
||||
#if self.value not in [ModbusStatus.Off, ModbusStatus.On]:
|
||||
# return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, 1):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
context.setValues(self.function_code, self.address, [self.value])
|
||||
values = context.getValues(self.function_code, self.address, 1)
|
||||
return WriteSingleCoilResponse(self.address, values[0])
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Output Address (2 byte) + Output Value (2 Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:return: A string representation of the instance
|
||||
'''
|
||||
return "WriteCoilRequest(%d, %s) => " % (self.address, self.value)
|
||||
|
||||
|
||||
class WriteSingleCoilResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response is an echo of the request, returned after the coil
|
||||
state has been written.
|
||||
'''
|
||||
function_code = 5
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The variable address written to
|
||||
:param value: The value written at address
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.value = value
|
||||
|
||||
def encode(self):
|
||||
''' Encodes write coil response
|
||||
|
||||
:return: The byte encoded message
|
||||
'''
|
||||
result = struct.pack('>H', self.address)
|
||||
if self.value: result += _turn_coil_on
|
||||
else: result += _turn_coil_off
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a write coil response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, value = struct.unpack('>HH', data)
|
||||
self.value = (value == ModbusStatus.On)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "WriteCoilResponse(%d) => %d" % (self.address, self.value)
|
||||
|
||||
|
||||
class WriteMultipleCoilsRequest(ModbusRequest):
|
||||
'''
|
||||
"This function code is used to force each coil in a sequence of coils to
|
||||
either ON or OFF in a remote device. The Request PDU specifies the coil
|
||||
references to be forced. Coils are addressed starting at zero. Therefore
|
||||
coil numbered 1 is addressed as 0.
|
||||
|
||||
The requested ON/OFF states are specified by contents of the request
|
||||
data field. A logical '1' in a bit position of the field requests the
|
||||
corresponding output to be ON. A logical '0' requests it to be OFF."
|
||||
'''
|
||||
function_code = 15
|
||||
_rtu_byte_count_pos = 6
|
||||
|
||||
def __init__(self, address=None, values=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The starting request address
|
||||
:param values: The values to write
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
if not values: values = []
|
||||
elif not hasattr(values, '__iter__'): values = [values]
|
||||
self.values = values
|
||||
self.byte_count = (len(self.values) + 7) // 8
|
||||
|
||||
def encode(self):
|
||||
''' Encodes write coils request
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
count = len(self.values)
|
||||
self.byte_count = (count + 7) // 8
|
||||
packet = struct.pack('>HHB', self.address, count, self.byte_count)
|
||||
packet += pack_bitstring(self.values)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a write coils request
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, count, self.byte_count = struct.unpack('>HHB', data[0:5])
|
||||
values = unpack_bitstring(data[5:])
|
||||
self.values = values[:count]
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a write coils request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response or exception message
|
||||
'''
|
||||
count = len(self.values)
|
||||
if not (1 <= count <= 0x07b0):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if (self.byte_count != (count + 7) // 8):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
context.setValues(self.function_code, self.address, self.values)
|
||||
return WriteMultipleCoilsResponse(self.address, count)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
params = (self.address, len(self.values))
|
||||
return "WriteNCoilRequest (%d) => %d " % params
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Output Address (2 byte) + Quantity of Outputs (2 Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
|
||||
class WriteMultipleCoilsResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response returns the function code, starting address, and
|
||||
quantity of coils forced.
|
||||
'''
|
||||
function_code = 15
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The starting variable address written to
|
||||
:param count: The number of values written
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
''' Encodes write coils response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
return struct.pack('>HH', self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a write coils response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, self.count = struct.unpack('>HH', data)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "WriteNCoilResponse(%d, %d)" % (self.address, self.count)
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"WriteSingleCoilRequest", "WriteSingleCoilResponse",
|
||||
"WriteMultipleCoilsRequest", "WriteMultipleCoilsResponse",
|
||||
]
|
||||
@@ -1,43 +0,0 @@
|
||||
"""
|
||||
Async Modbus Client implementation based on Twisted, tornado and asyncio
|
||||
------------------------------------------------------------------------
|
||||
|
||||
Example run::
|
||||
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
|
||||
# Import The clients
|
||||
|
||||
from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient as Client
|
||||
from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient as Client
|
||||
from pymodbus.client.asynchronous.udp import AsyncModbusUDPClient as Client
|
||||
|
||||
# For tornado based asynchronous client use
|
||||
event_loop, future = Client(schedulers.IO_LOOP, port=5020)
|
||||
|
||||
# For twisted based asynchronous client use
|
||||
event_loop, future = Client(schedulers.REACTOR, port=5020)
|
||||
|
||||
# For asyncio based asynchronous client use
|
||||
event_loop, client = Client(schedulers.ASYNC_IO, port=5020)
|
||||
|
||||
# Here event_loop is a thread which would control the backend and future is
|
||||
# a Future/deffered object which would be used to
|
||||
# add call backs to run asynchronously.
|
||||
|
||||
# The Actual client could be accessed with future.result() with Tornado
|
||||
# and future.result when using twisted
|
||||
|
||||
# For asyncio the actual client is returned and event loop is asyncio loop
|
||||
|
||||
"""
|
||||
from pymodbus.compat import is_installed
|
||||
|
||||
installed = is_installed('twisted')
|
||||
if installed:
|
||||
# Import deprecated async client only if twisted is installed #338
|
||||
from pymodbus.client.asynchronous.deprecated.asynchronous import *
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning("Importing deprecated clients. "
|
||||
"Dependency Twisted is Installed")
|
||||
@@ -1,890 +0,0 @@
|
||||
"""
|
||||
Asynchronous framework adapter for asyncio.
|
||||
"""
|
||||
import socket
|
||||
import asyncio
|
||||
import functools
|
||||
import ssl
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.client.asynchronous.mixins import AsyncModbusClientMixin
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from pymodbus.transaction import FifoTransactionManager
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DGRAM_TYPE = socket.SocketKind.SOCK_DGRAM
|
||||
|
||||
|
||||
class BaseModbusAsyncClientProtocol(AsyncModbusClientMixin):
|
||||
"""
|
||||
Asyncio specific implementation of asynchronous modbus client protocol.
|
||||
"""
|
||||
|
||||
#: Factory that created this instance.
|
||||
factory = None
|
||||
transport = None
|
||||
|
||||
async def execute(self, request=None):
|
||||
"""
|
||||
Executes requests asynchronously
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
req = self._execute(request)
|
||||
resp = await asyncio.wait_for(req, timeout=self._timeout)
|
||||
return resp
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""
|
||||
Called when a connection is made.
|
||||
|
||||
The transport argument is the transport representing the connection.
|
||||
:param transport:
|
||||
:return:
|
||||
"""
|
||||
self.transport = transport
|
||||
self._connectionMade()
|
||||
|
||||
if self.factory:
|
||||
self.factory.protocol_made_connection(self)
|
||||
|
||||
def connection_lost(self, reason):
|
||||
"""
|
||||
Called when the connection is lost or closed.
|
||||
|
||||
The argument is either an exception object or None
|
||||
:param reason:
|
||||
:return:
|
||||
"""
|
||||
self.transport = None
|
||||
self._connectionLost(reason)
|
||||
|
||||
if self.factory:
|
||||
self.factory.protocol_lost_connection(self)
|
||||
|
||||
def data_received(self, data):
|
||||
"""
|
||||
Called when some data is received.
|
||||
data is a non-empty bytes object containing the incoming data.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
self._dataReceived(data)
|
||||
|
||||
def create_future(self):
|
||||
"""
|
||||
Helper function to create asyncio Future object
|
||||
:return:
|
||||
"""
|
||||
return asyncio.Future()
|
||||
|
||||
def resolve_future(self, f, result):
|
||||
"""
|
||||
Resolves the completed future and sets the result
|
||||
:param f:
|
||||
:param result:
|
||||
:return:
|
||||
"""
|
||||
if not f.done():
|
||||
f.set_result(result)
|
||||
|
||||
def raise_future(self, f, exc):
|
||||
"""
|
||||
Sets exception of a future if not done
|
||||
:param f:
|
||||
:param exc:
|
||||
:return:
|
||||
"""
|
||||
if not f.done():
|
||||
f.set_exception(exc)
|
||||
|
||||
def _connectionMade(self):
|
||||
"""
|
||||
Called upon a successful client connection.
|
||||
"""
|
||||
_logger.debug("Client connected to modbus server")
|
||||
self._connected = True
|
||||
|
||||
def _connectionLost(self, reason):
|
||||
"""
|
||||
Called upon a client disconnect
|
||||
|
||||
:param reason: The reason for the disconnect
|
||||
"""
|
||||
_logger.debug(
|
||||
"Client disconnected from modbus server: %s" % reason)
|
||||
self._connected = False
|
||||
for tid in list(self.transaction):
|
||||
self.raise_future(self.transaction.getTransaction(tid),
|
||||
ConnectionException(
|
||||
'Connection lost during request'))
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""
|
||||
Return connection status.
|
||||
"""
|
||||
return self._connected
|
||||
|
||||
def write_transport(self, packet):
|
||||
return self.transport.write(packet)
|
||||
|
||||
def _execute(self, request, **kwargs):
|
||||
"""
|
||||
Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
_logger.debug("send: " + hexlify_packets(packet))
|
||||
self.write_transport(packet)
|
||||
return self._buildResponse(request.transaction_id)
|
||||
|
||||
def _dataReceived(self, data):
|
||||
''' Get response, check for valid message, decode result
|
||||
|
||||
:param data: The data returned from the server
|
||||
'''
|
||||
_logger.debug("recv: " + hexlify_packets(data))
|
||||
unit = self.framer.decode_data(data).get("unit", 0)
|
||||
self.framer.processIncomingPacket(data, self._handleResponse, unit=unit)
|
||||
|
||||
def _handleResponse(self, reply, **kwargs):
|
||||
"""
|
||||
Handle the processed response and link to correct deferred
|
||||
|
||||
:param reply: The reply to process
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
handler = self.transaction.getTransaction(tid)
|
||||
if handler:
|
||||
self.resolve_future(handler, reply)
|
||||
else:
|
||||
_logger.debug("Unrequested message: " + str(reply))
|
||||
|
||||
def _buildResponse(self, tid):
|
||||
"""
|
||||
Helper method to return a deferred response
|
||||
for the current request.
|
||||
|
||||
:param tid: The transaction identifier for this response
|
||||
:returns: A defer linked to the latest request
|
||||
"""
|
||||
f = self.create_future()
|
||||
if not self._connected:
|
||||
self.raise_future(f, ConnectionException(
|
||||
'Client is not connected'))
|
||||
else:
|
||||
self.transaction.addTransaction(f, tid)
|
||||
return f
|
||||
|
||||
def close(self):
|
||||
self.transport.close()
|
||||
self._connected = False
|
||||
|
||||
|
||||
class ModbusClientProtocol(BaseModbusAsyncClientProtocol, asyncio.Protocol):
|
||||
"""
|
||||
Asyncio specific implementation of asynchronous modbus client protocol.
|
||||
"""
|
||||
|
||||
#: Factory that created this instance.
|
||||
factory = None
|
||||
transport = None
|
||||
|
||||
def data_received(self, data):
|
||||
"""
|
||||
Called when some data is received.
|
||||
data is a non-empty bytes object containing the incoming data.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
self._dataReceived(data)
|
||||
|
||||
|
||||
class ModbusUdpClientProtocol(BaseModbusAsyncClientProtocol,
|
||||
asyncio.DatagramProtocol):
|
||||
"""
|
||||
Asyncio specific implementation of asynchronous modbus udp client protocol.
|
||||
"""
|
||||
|
||||
#: Factory that created this instance.
|
||||
factory = None
|
||||
|
||||
def __init__(self, host=None, port=0, **kwargs):
|
||||
self.host = host
|
||||
self.port = port
|
||||
super(self.__class__, self).__init__(**kwargs)
|
||||
|
||||
def datagram_received(self, data, addr):
|
||||
self._dataReceived(data)
|
||||
|
||||
def write_transport(self, packet):
|
||||
return self.transport.sendto(packet)
|
||||
|
||||
|
||||
class ReconnectingAsyncioModbusTcpClient(object):
|
||||
"""
|
||||
Client to connect to modbus device repeatedly over TCP/IP."
|
||||
"""
|
||||
#: Minimum delay in milli seconds before reconnect is attempted.
|
||||
DELAY_MIN_MS = 100
|
||||
#: Maximum delay in milli seconds before reconnect is attempted.
|
||||
DELAY_MAX_MS = 1000 * 60 * 5
|
||||
|
||||
def __init__(self, protocol_class=None, loop=None, **kwargs):
|
||||
"""
|
||||
Initialize ReconnectingAsyncioModbusTcpClient
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param loop: Event loop to use
|
||||
"""
|
||||
#: Protocol used to talk to modbus device.
|
||||
self.protocol_class = protocol_class or ModbusClientProtocol
|
||||
#: Current protocol instance.
|
||||
self.protocol = None
|
||||
#: Event loop to use.
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
self.host = None
|
||||
self.port = 0
|
||||
self.connected = False
|
||||
#: Reconnect delay in milli seconds.
|
||||
self.delay_ms = self.DELAY_MIN_MS
|
||||
self._proto_args = kwargs
|
||||
|
||||
def reset_delay(self):
|
||||
"""
|
||||
Resets wait before next reconnect to minimal period.
|
||||
"""
|
||||
self.delay_ms = self.DELAY_MIN_MS
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, host, port=502):
|
||||
"""
|
||||
Initiates connection to start client
|
||||
:param host:
|
||||
:param port:
|
||||
:return:
|
||||
"""
|
||||
# force reconnect if required:
|
||||
self.stop()
|
||||
|
||||
_logger.debug('Connecting to %s:%s.' % (host, port))
|
||||
self.host = host
|
||||
self.port = port
|
||||
yield from self._connect()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops client
|
||||
:return:
|
||||
"""
|
||||
# prevent reconnect:
|
||||
self.host = None
|
||||
|
||||
if self.connected:
|
||||
if self.protocol:
|
||||
if self.protocol.transport:
|
||||
self.protocol.transport.close()
|
||||
|
||||
def _create_protocol(self):
|
||||
"""
|
||||
Factory function to create initialized protocol instance.
|
||||
"""
|
||||
protocol = self.protocol_class(**self._proto_args)
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
@asyncio.coroutine
|
||||
def _connect(self):
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
yield from self.loop.create_connection(self._create_protocol,
|
||||
self.host,
|
||||
self.port)
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s' % ex)
|
||||
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
||||
self.reset_delay()
|
||||
|
||||
def protocol_made_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of successful connection.
|
||||
"""
|
||||
_logger.info('Protocol made connection.')
|
||||
if not self.connected:
|
||||
self.connected = True
|
||||
self.protocol = protocol
|
||||
else:
|
||||
_logger.error('Factory protocol connect '
|
||||
'callback called while connected.')
|
||||
|
||||
def protocol_lost_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of lost connection.
|
||||
"""
|
||||
if self.connected:
|
||||
_logger.info('Protocol lost connection.')
|
||||
if protocol is not self.protocol:
|
||||
_logger.error('Factory protocol callback called '
|
||||
'from unexpected protocol instance.')
|
||||
|
||||
self.connected = False
|
||||
self.protocol = None
|
||||
if self.host:
|
||||
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.error('Factory protocol disconnect callback called while not connected.')
|
||||
|
||||
@asyncio.coroutine
|
||||
def _reconnect(self):
|
||||
_logger.debug('Waiting %d ms before next '
|
||||
'connection attempt.' % self.delay_ms)
|
||||
yield from asyncio.sleep(self.delay_ms / 1000)
|
||||
self.delay_ms = min(2 * self.delay_ms, self.DELAY_MAX_MS)
|
||||
yield from self._connect()
|
||||
|
||||
|
||||
class AsyncioModbusTcpClient(object):
|
||||
"""Client to connect to modbus device over TCP/IP."""
|
||||
|
||||
def __init__(self, host=None, port=502, protocol_class=None, loop=None, **kwargs):
|
||||
"""
|
||||
Initializes Asyncio Modbus Tcp Client
|
||||
:param host: Host IP address
|
||||
:param port: Port to connect
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param loop: Asyncio Event loop
|
||||
"""
|
||||
#: Protocol used to talk to modbus device.
|
||||
self.protocol_class = protocol_class or ModbusClientProtocol
|
||||
#: Current protocol instance.
|
||||
self.protocol = None
|
||||
#: Event loop to use.
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self.connected = False
|
||||
self._proto_args = kwargs
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops the client
|
||||
:return:
|
||||
"""
|
||||
if self.connected:
|
||||
if self.protocol:
|
||||
if self.protocol.transport:
|
||||
self.protocol.transport.close()
|
||||
|
||||
def _create_protocol(self):
|
||||
"""
|
||||
Factory function to create initialized protocol instance.
|
||||
"""
|
||||
protocol = self.protocol_class(**self._proto_args)
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self):
|
||||
"""
|
||||
Connect and start Async client
|
||||
:return:
|
||||
"""
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
yield from self.loop.create_connection(self._create_protocol,
|
||||
self.host,
|
||||
self.port)
|
||||
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s' % ex)
|
||||
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
||||
|
||||
def protocol_made_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of successful connection.
|
||||
"""
|
||||
_logger.info('Protocol made connection.')
|
||||
if not self.connected:
|
||||
self.connected = True
|
||||
self.protocol = protocol
|
||||
else:
|
||||
_logger.error('Factory protocol connect '
|
||||
'callback called while connected.')
|
||||
|
||||
def protocol_lost_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of lost connection.
|
||||
"""
|
||||
if self.connected:
|
||||
_logger.info('Protocol lost connection.')
|
||||
if protocol is not self.protocol:
|
||||
_logger.error('Factory protocol callback called'
|
||||
' from unexpected protocol instance.')
|
||||
|
||||
self.connected = False
|
||||
self.protocol = None
|
||||
# if self.host:
|
||||
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.error('Factory protocol disconnect'
|
||||
' callback called while not connected.')
|
||||
|
||||
|
||||
class ReconnectingAsyncioModbusTlsClient(ReconnectingAsyncioModbusTcpClient):
|
||||
"""
|
||||
Client to connect to modbus device repeatedly over TLS."
|
||||
"""
|
||||
def __init__(self, protocol_class=None, loop=None, framer=None, **kwargs):
|
||||
"""
|
||||
Initialize ReconnectingAsyncioModbusTcpClient
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param loop: Event loop to use
|
||||
"""
|
||||
self.framer = framer
|
||||
ReconnectingAsyncioModbusTcpClient.__init__(self, protocol_class, loop, **kwargs)
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, host, port=802, sslctx=None, server_hostname=None):
|
||||
"""
|
||||
Initiates connection to start client
|
||||
:param host:
|
||||
:param port:
|
||||
:param sslctx:
|
||||
:param server_hostname:
|
||||
:return:
|
||||
"""
|
||||
self.sslctx = sslctx
|
||||
if self.sslctx is None:
|
||||
self.sslctx = ssl.create_default_context()
|
||||
# According to MODBUS/TCP Security Protocol Specification, it is
|
||||
# TLSv2 at least
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1_1
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv3
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv2
|
||||
self.server_hostname = server_hostname
|
||||
yield from ReconnectingAsyncioModbusTcpClient.start(self, host, port)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _connect(self):
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
yield from self.loop.create_connection(self._create_protocol,
|
||||
self.host,
|
||||
self.port,
|
||||
ssl=self.sslctx,
|
||||
server_hostname=self.server_hostname)
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s' % ex)
|
||||
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
||||
self.reset_delay()
|
||||
|
||||
def _create_protocol(self):
|
||||
"""
|
||||
Factory function to create initialized protocol instance.
|
||||
"""
|
||||
protocol = self.protocol_class(framer=self.framer, **self._proto_args)
|
||||
protocol.transaction = FifoTransactionManager(self)
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
class ReconnectingAsyncioModbusUdpClient(object):
|
||||
"""
|
||||
Client to connect to modbus device repeatedly over UDP.
|
||||
"""
|
||||
|
||||
#: Reconnect delay in milli seconds.
|
||||
delay_ms = 0
|
||||
|
||||
#: Maximum delay in milli seconds before reconnect is attempted.
|
||||
DELAY_MAX_MS = 1000 * 60 * 5
|
||||
|
||||
def __init__(self, protocol_class=None, loop=None, **kwargs):
|
||||
"""
|
||||
Initializes ReconnectingAsyncioModbusUdpClient
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param loop: Asyncio Event loop
|
||||
"""
|
||||
#: Protocol used to talk to modbus device.
|
||||
self.protocol_class = protocol_class or ModbusUdpClientProtocol
|
||||
#: Current protocol instance.
|
||||
self.protocol = None
|
||||
#: Event loop to use.
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
|
||||
self.host = None
|
||||
self.port = 0
|
||||
|
||||
self.connected = False
|
||||
self._proto_args = kwargs
|
||||
self.reset_delay()
|
||||
|
||||
def reset_delay(self):
|
||||
"""
|
||||
Resets wait before next reconnect to minimal period.
|
||||
"""
|
||||
self.delay_ms = 100
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, host, port=502):
|
||||
"""
|
||||
Start reconnecting asynchronous udp client
|
||||
:param host: Host IP to connect
|
||||
:param port: Host port to connect
|
||||
:return:
|
||||
"""
|
||||
# force reconnect if required:
|
||||
self.stop()
|
||||
|
||||
_logger.debug('Connecting to %s:%s.' % (host, port))
|
||||
|
||||
# getaddrinfo returns a list of tuples
|
||||
# - [(family, type, proto, canonname, sockaddr),]
|
||||
# We want sockaddr which is a (ip, port) tuple
|
||||
# udp needs ip addresses, not hostnames
|
||||
addrinfo = yield from self.loop.getaddrinfo(host,
|
||||
port,
|
||||
type=DGRAM_TYPE)
|
||||
self.host, self.port = addrinfo[0][-1]
|
||||
|
||||
yield from self._connect()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops connection and prevents reconnect
|
||||
:return:
|
||||
"""
|
||||
# prevent reconnect:
|
||||
self.host = None
|
||||
|
||||
if self.connected:
|
||||
if self.protocol:
|
||||
if self.protocol.transport:
|
||||
self.protocol.transport.close()
|
||||
|
||||
def _create_protocol(self, host=None, port=0):
|
||||
"""
|
||||
Factory function to create initialized protocol instance.
|
||||
"""
|
||||
protocol = self.protocol_class(**self._proto_args)
|
||||
protocol.host = host
|
||||
protocol.port = port
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
@asyncio.coroutine
|
||||
def _connect(self):
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
yield from self.loop.create_datagram_endpoint(
|
||||
functools.partial(self._create_protocol,
|
||||
host=self.host,
|
||||
port=self.port),
|
||||
remote_addr=(self.host, self.port)
|
||||
)
|
||||
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s' % ex)
|
||||
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
||||
|
||||
def protocol_made_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of successful connection.
|
||||
"""
|
||||
_logger.info('Protocol made connection.')
|
||||
if not self.connected:
|
||||
self.connected = True
|
||||
self.protocol = protocol
|
||||
else:
|
||||
_logger.error('Factory protocol connect callback '
|
||||
'called while connected.')
|
||||
|
||||
def protocol_lost_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of lost connection.
|
||||
"""
|
||||
if self.connected:
|
||||
_logger.info('Protocol lost connection.')
|
||||
if protocol is not self.protocol:
|
||||
_logger.error('Factory protocol callback called '
|
||||
'from unexpected protocol instance.')
|
||||
|
||||
self.connected = False
|
||||
self.protocol = None
|
||||
if self.host:
|
||||
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.error('Factory protocol disconnect '
|
||||
'callback called while not connected.')
|
||||
|
||||
@asyncio.coroutine
|
||||
def _reconnect(self):
|
||||
_logger.debug('Waiting %d ms before next '
|
||||
'connection attempt.' % self.delay_ms)
|
||||
yield from asyncio.sleep(self.delay_ms / 1000)
|
||||
self.delay_ms = min(2 * self.delay_ms, self.DELAY_MAX_MS)
|
||||
yield from self._connect()
|
||||
|
||||
|
||||
class AsyncioModbusUdpClient(object):
|
||||
"""
|
||||
Client to connect to modbus device over UDP.
|
||||
"""
|
||||
|
||||
def __init__(self, host=None, port=502, protocol_class=None, loop=None, **kwargs):
|
||||
"""
|
||||
Initializes Asyncio Modbus UDP Client
|
||||
:param host: Host IP address
|
||||
:param port: Port to connect
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param loop: Asyncio Event loop
|
||||
"""
|
||||
#: Protocol used to talk to modbus device.
|
||||
self.protocol_class = protocol_class or ModbusUdpClientProtocol
|
||||
#: Current protocol instance.
|
||||
self.protocol = None
|
||||
#: Event loop to use.
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self.connected = False
|
||||
self._proto_args = kwargs
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops connection
|
||||
:return:
|
||||
"""
|
||||
# prevent reconnect:
|
||||
# self.host = None
|
||||
|
||||
if self.connected:
|
||||
if self.protocol:
|
||||
if self.protocol.transport:
|
||||
self.protocol.transport.close()
|
||||
|
||||
def _create_protocol(self, host=None, port=0):
|
||||
"""
|
||||
Factory function to create initialized protocol instance.
|
||||
"""
|
||||
protocol = self.protocol_class(**self._proto_args)
|
||||
protocol.host = host
|
||||
protocol.port = port
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self):
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
addrinfo = yield from self.loop.getaddrinfo(
|
||||
self.host,
|
||||
self.port,
|
||||
type=DGRAM_TYPE)
|
||||
_host, _port = addrinfo[0][-1]
|
||||
yield from self.loop.create_datagram_endpoint(
|
||||
functools.partial(self._create_protocol,
|
||||
host=_host, port=_port),
|
||||
remote_addr=(self.host, self.port)
|
||||
)
|
||||
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s' % ex)
|
||||
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
||||
|
||||
def protocol_made_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of successful connection.
|
||||
"""
|
||||
_logger.info('Protocol made connection.')
|
||||
if not self.connected:
|
||||
self.connected = True
|
||||
self.protocol = protocol
|
||||
else:
|
||||
_logger.error('Factory protocol connect '
|
||||
'callback called while connected.')
|
||||
|
||||
def protocol_lost_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of lost connection.
|
||||
"""
|
||||
if self.connected:
|
||||
_logger.info('Protocol lost connection.')
|
||||
if protocol is not self.protocol:
|
||||
_logger.error('Factory protocol callback '
|
||||
'called from unexpected protocol instance.')
|
||||
|
||||
self.connected = False
|
||||
self.protocol = None
|
||||
# if self.host:
|
||||
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.error('Factory protocol disconnect '
|
||||
'callback called while not connected.')
|
||||
|
||||
|
||||
class AsyncioModbusSerialClient(object):
|
||||
"""
|
||||
Client to connect to modbus device over serial.
|
||||
"""
|
||||
transport = None
|
||||
framer = None
|
||||
|
||||
def __init__(self, port, protocol_class=None, framer=None, loop=None,
|
||||
baudrate=9600, bytesize=8, parity='N', stopbits=1, **serial_kwargs):
|
||||
"""
|
||||
Initializes Asyncio Modbus Serial Client
|
||||
:param port: Port to connect
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param framer: Framer to use
|
||||
:param loop: Asyncio Event loop
|
||||
"""
|
||||
#: Protocol used to talk to modbus device.
|
||||
self.protocol_class = protocol_class or ModbusClientProtocol
|
||||
#: Current protocol instance.
|
||||
self.protocol = None
|
||||
#: Event loop to use.
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.bytesize = bytesize
|
||||
self.parity = parity
|
||||
self.stopbits = stopbits
|
||||
self.framer = framer
|
||||
self._extra_serial_kwargs = serial_kwargs
|
||||
self._connected_event = asyncio.Event()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops connection
|
||||
:return:
|
||||
"""
|
||||
if self._connected:
|
||||
if self.protocol:
|
||||
if self.protocol.transport:
|
||||
self.protocol.transport.close()
|
||||
|
||||
def _create_protocol(self):
|
||||
protocol = self.protocol_class(framer=self.framer)
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
@property
|
||||
def _connected(self):
|
||||
return self._connected_event.is_set()
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self):
|
||||
"""
|
||||
Connect Async client
|
||||
:return:
|
||||
"""
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
from serial_asyncio import create_serial_connection
|
||||
|
||||
yield from create_serial_connection(
|
||||
self.loop, self._create_protocol, self.port, baudrate=self.baudrate,
|
||||
bytesize=self.bytesize, stopbits=self.stopbits, parity=self.parity, **self._extra_serial_kwargs
|
||||
)
|
||||
yield from self._connected_event.wait()
|
||||
_logger.info('Connected to %s', self.port)
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s', ex)
|
||||
|
||||
def protocol_made_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of successful connection.
|
||||
"""
|
||||
_logger.info('Protocol made connection.')
|
||||
if not self._connected:
|
||||
self._connected_event.set()
|
||||
self.protocol = protocol
|
||||
else:
|
||||
_logger.error('Factory protocol connect '
|
||||
'callback called while connected.')
|
||||
|
||||
def protocol_lost_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of lost connection.
|
||||
"""
|
||||
if self._connected:
|
||||
_logger.info('Protocol lost connection.')
|
||||
if protocol is not self.protocol:
|
||||
_logger.error('Factory protocol callback called'
|
||||
' from unexpected protocol instance.')
|
||||
|
||||
self._connected_event.clear()
|
||||
self.protocol = None
|
||||
# if self.host:
|
||||
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.error('Factory protocol disconnect callback '
|
||||
'called while not connected.')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def init_tcp_client(proto_cls, loop, host, port, **kwargs):
|
||||
"""
|
||||
Helper function to initialize tcp client
|
||||
:param proto_cls:
|
||||
:param loop:
|
||||
:param host:
|
||||
:param port:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
client = ReconnectingAsyncioModbusTcpClient(protocol_class=proto_cls,
|
||||
loop=loop, **kwargs)
|
||||
yield from client.start(host, port)
|
||||
return client
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def init_tls_client(proto_cls, loop, host, port, sslctx=None,
|
||||
server_hostname=None, framer=None, **kwargs):
|
||||
"""
|
||||
Helper function to initialize tcp client
|
||||
:param proto_cls:
|
||||
:param loop:
|
||||
:param host:
|
||||
:param port:
|
||||
:param sslctx:
|
||||
:param server_hostname:
|
||||
:param framer:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
client = ReconnectingAsyncioModbusTlsClient(protocol_class=proto_cls,
|
||||
loop=loop, framer=framer,
|
||||
**kwargs)
|
||||
yield from client.start(host, port, sslctx, server_hostname)
|
||||
return client
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def init_udp_client(proto_cls, loop, host, port, **kwargs):
|
||||
"""
|
||||
Helper function to initialize UDP client
|
||||
:param proto_cls:
|
||||
:param loop:
|
||||
:param host:
|
||||
:param port:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
client = ReconnectingAsyncioModbusUdpClient(protocol_class=proto_cls,
|
||||
loop=loop, **kwargs)
|
||||
yield from client.start(host, port)
|
||||
return client
|
||||
@@ -1,47 +0,0 @@
|
||||
import warnings
|
||||
warnings.simplefilter('always', DeprecationWarning)
|
||||
|
||||
WARNING = """
|
||||
Usage of '{}' is deprecated from 2.0.0 and will be removed in future releases.
|
||||
Use the new Async Modbus Client implementation based on Twisted, tornado
|
||||
and asyncio
|
||||
------------------------------------------------------------------------
|
||||
|
||||
Example run::
|
||||
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
|
||||
# Import The clients
|
||||
|
||||
from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient as Client
|
||||
from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient as Client
|
||||
from pymodbus.client.asynchronous.udp import AsyncModbusUDPClient as Client
|
||||
|
||||
# For tornado based asynchronous client use
|
||||
event_loop, future = Client(schedulers.IO_LOOP, port=5020)
|
||||
|
||||
# For twisted based asynchronous client use
|
||||
event_loop, deferred = Client(schedulers.REACTOR, port=5020)
|
||||
|
||||
# For asyncio based asynchronous client use
|
||||
event_loop, client = Client(schedulers.ASYNC_IO, port=5020)
|
||||
|
||||
# Here event_loop is a thread which would control the backend and future is
|
||||
# a Future/deffered object which would be used to
|
||||
# add call backs to run asynchronously.
|
||||
|
||||
# The Actual client could be accessed with future.result() with Tornado
|
||||
# and future.result when using twisted
|
||||
|
||||
# For asyncio the actual client is returned and event loop is asyncio loop
|
||||
|
||||
Refer:
|
||||
https://pymodbus.readthedocs.io/en/dev/source/example/async_twisted_client.html
|
||||
https://pymodbus.readthedocs.io/en/dev/source/example/async_tornado_client.html
|
||||
https://pymodbus.readthedocs.io/en/dev/source/example/async_asyncio_client.html
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def deprecated(name): # pragma: no cover
|
||||
warnings.warn(WARNING.format(name), DeprecationWarning)
|
||||
@@ -1,231 +0,0 @@
|
||||
"""
|
||||
Implementation of a Modbus Client Using Twisted
|
||||
--------------------------------------------------
|
||||
|
||||
Example run::
|
||||
|
||||
from twisted.internet import reactor, protocol
|
||||
from pymodbus.client.asynchronous import ModbusClientProtocol
|
||||
|
||||
def printResult(result):
|
||||
print "Result: %d" % result.bits[0]
|
||||
|
||||
def process(client):
|
||||
result = client.write_coil(1, True)
|
||||
result.addCallback(printResult)
|
||||
reactor.callLater(1, reactor.stop)
|
||||
|
||||
defer = protocol.ClientCreator(reactor, ModbusClientProtocol
|
||||
).connectTCP("localhost", 502)
|
||||
defer.addCallback(process)
|
||||
|
||||
Another example::
|
||||
|
||||
from twisted.internet import reactor
|
||||
from pymodbus.client.asynchronous import ModbusClientFactory
|
||||
|
||||
def process():
|
||||
factory = reactor.connectTCP("localhost", 502, ModbusClientFactory())
|
||||
reactor.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
reactor.callLater(1, process)
|
||||
reactor.run()
|
||||
"""
|
||||
import logging
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.transaction import ModbusSocketFramer
|
||||
from pymodbus.transaction import FifoTransactionManager
|
||||
from pymodbus.transaction import DictTransactionManager
|
||||
from pymodbus.client.common import ModbusClientMixin
|
||||
from pymodbus.client.asynchronous.deprecated import deprecated
|
||||
from twisted.internet import defer, protocol
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Connected Client Protocols
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusClientProtocol(protocol.Protocol, ModbusClientMixin): # pragma: no cover
|
||||
"""
|
||||
This represents the base modbus client protocol. All the application
|
||||
layer code is deferred to a higher level wrapper.
|
||||
"""
|
||||
|
||||
def __init__(self, framer=None, **kwargs):
|
||||
""" Initializes the framer module
|
||||
|
||||
:param framer: The framer to use for the protocol
|
||||
"""
|
||||
deprecated(self.__class__.__name__)
|
||||
self._connected = False
|
||||
self.framer = framer or ModbusSocketFramer(ClientDecoder())
|
||||
if isinstance(self.framer, type):
|
||||
# Framer class not instance
|
||||
self.framer = self.framer(ClientDecoder(), client=None)
|
||||
if isinstance(self.framer, ModbusSocketFramer):
|
||||
self.transaction = DictTransactionManager(self, **kwargs)
|
||||
else:
|
||||
self.transaction = FifoTransactionManager(self, **kwargs)
|
||||
|
||||
def connectionMade(self):
|
||||
""" Called upon a successful client connection.
|
||||
"""
|
||||
_logger.debug("Client connected to modbus server")
|
||||
self._connected = True
|
||||
|
||||
def connectionLost(self, reason):
|
||||
""" Called upon a client disconnect
|
||||
|
||||
:param reason: The reason for the disconnect
|
||||
"""
|
||||
_logger.debug("Client disconnected from modbus server: %s" % reason)
|
||||
self._connected = False
|
||||
for tid in list(self.transaction):
|
||||
self.transaction.getTransaction(tid).errback(Failure(
|
||||
ConnectionException('Connection lost during request')))
|
||||
|
||||
def dataReceived(self, data):
|
||||
""" Get response, check for valid message, decode result
|
||||
|
||||
:param data: The data returned from the server
|
||||
"""
|
||||
unit = self.framer.decode_data(data).get("uid", 0)
|
||||
self.framer.processIncomingPacket(data, self._handleResponse, unit=unit)
|
||||
|
||||
def execute(self, request):
|
||||
""" Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
self.transport.write(packet)
|
||||
return self._buildResponse(request.transaction_id)
|
||||
|
||||
def _handleResponse(self, reply):
|
||||
""" Handle the processed response and link to correct deferred
|
||||
|
||||
:param reply: The reply to process
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
handler = self.transaction.getTransaction(tid)
|
||||
if handler:
|
||||
handler.callback(reply)
|
||||
else:
|
||||
_logger.debug("Unrequested message: " + str(reply))
|
||||
|
||||
def _buildResponse(self, tid):
|
||||
""" Helper method to return a deferred response
|
||||
for the current request.
|
||||
|
||||
:param tid: The transaction identifier for this response
|
||||
:returns: A defer linked to the latest request
|
||||
"""
|
||||
if not self._connected:
|
||||
return defer.fail(Failure(
|
||||
ConnectionException('Client is not connected')))
|
||||
|
||||
d = defer.Deferred()
|
||||
self.transaction.addTransaction(d, tid)
|
||||
return d
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Extra Functions
|
||||
# ---------------------------------------------------------------------- #
|
||||
# if send_failed:
|
||||
# if self.retry > 0:
|
||||
# deferLater(clock, self.delay, send, message)
|
||||
# self.retry -= 1
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Not Connected Client Protocol
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusUdpClientProtocol(protocol.DatagramProtocol, ModbusClientMixin): # pragma: no cover
|
||||
"""
|
||||
This represents the base modbus client protocol. All the application
|
||||
layer code is deferred to a higher level wrapper.
|
||||
"""
|
||||
|
||||
def __init__(self, framer=None, **kwargs):
|
||||
""" Initializes the framer module
|
||||
|
||||
:param framer: The framer to use for the protocol
|
||||
"""
|
||||
deprecated(self.__class__.__name__)
|
||||
self.framer = framer or ModbusSocketFramer(ClientDecoder())
|
||||
if isinstance(self.framer, ModbusSocketFramer):
|
||||
self.transaction = DictTransactionManager(self, **kwargs)
|
||||
else: self.transaction = FifoTransactionManager(self, **kwargs)
|
||||
|
||||
def datagramReceived(self, data, params):
|
||||
""" Get response, check for valid message, decode result
|
||||
|
||||
:param data: The data returned from the server
|
||||
:param params: The host parameters sending the datagram
|
||||
"""
|
||||
_logger.debug("Datagram from: %s:%d" % params)
|
||||
unit = self.framer.decode_data(data).get("uid", 0)
|
||||
self.framer.processIncomingPacket(data, self._handleResponse, unit=unit)
|
||||
|
||||
def execute(self, request):
|
||||
""" Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
self.transport.write(packet)
|
||||
return self._buildResponse(request.transaction_id)
|
||||
|
||||
def _handleResponse(self, reply):
|
||||
""" Handle the processed response and link to correct deferred
|
||||
|
||||
:param reply: The reply to process
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
handler = self.transaction.getTransaction(tid)
|
||||
if handler:
|
||||
handler.callback(reply)
|
||||
else: _logger.debug("Unrequested message: " + str(reply))
|
||||
|
||||
def _buildResponse(self, tid):
|
||||
""" Helper method to return a deferred response
|
||||
for the current request.
|
||||
|
||||
:param tid: The transaction identifier for this response
|
||||
:returns: A defer linked to the latest request
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
self.transaction.addTransaction(d, tid)
|
||||
return d
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Client Factories
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusClientFactory(protocol.ReconnectingClientFactory): # pragma: no cover
|
||||
""" Simple client protocol factory """
|
||||
|
||||
protocol = ModbusClientProtocol
|
||||
|
||||
def __init__(self):
|
||||
deprecated(self.__class__.__name__)
|
||||
protocol.ReconnectingClientFactory.__init__(self)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ModbusClientProtocol", "ModbusUdpClientProtocol", "ModbusClientFactory"
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
@@ -1,130 +0,0 @@
|
||||
"""
|
||||
Factory to create asynchronous serial clients based on twisted/tornado/asyncio
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import time
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
from pymodbus.client.asynchronous.thread import EventLoopThread
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def reactor_factory(port, framer, **kwargs):
|
||||
"""
|
||||
Factory to create twisted serial asynchronous client
|
||||
:param port: Serial port
|
||||
:param framer: Modbus Framer
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and twisted serial client
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.serialport import SerialPort
|
||||
from twisted.internet.protocol import ClientFactory
|
||||
from pymodbus.factory import ClientDecoder
|
||||
|
||||
class SerialClientFactory(ClientFactory):
|
||||
def __init__(self, framer, proto_cls):
|
||||
''' Remember things necessary for building a protocols '''
|
||||
self.proto_cls = proto_cls
|
||||
self.framer = framer
|
||||
|
||||
def buildProtocol(self):
|
||||
''' Create a protocol and start the reading cycle '''
|
||||
proto = self.proto_cls(self.framer)
|
||||
proto.factory = self
|
||||
return proto
|
||||
|
||||
class SerialModbusClient(SerialPort):
|
||||
|
||||
def __init__(self, framer, *args, **kwargs):
|
||||
''' Setup the client and start listening on the serial port
|
||||
|
||||
:param factory: The factory to build clients with
|
||||
'''
|
||||
self.decoder = ClientDecoder()
|
||||
proto_cls = kwargs.pop("proto_cls", None)
|
||||
proto = SerialClientFactory(framer, proto_cls).buildProtocol()
|
||||
SerialPort.__init__(self, proto, *args, **kwargs)
|
||||
|
||||
proto = EventLoopThread("reactor", reactor.run, reactor.stop,
|
||||
installSignalHandlers=0)
|
||||
ser_client = SerialModbusClient(framer, port, reactor, **kwargs)
|
||||
|
||||
return proto, ser_client
|
||||
|
||||
|
||||
def io_loop_factory(port=None, framer=None, **kwargs):
|
||||
"""
|
||||
Factory to create Tornado based asynchronous serial clients
|
||||
:param port: Serial port
|
||||
:param framer: Modbus Framer
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and tornado future
|
||||
"""
|
||||
|
||||
from tornado.ioloop import IOLoop
|
||||
from pymodbus.client.asynchronous.tornado import (AsyncModbusSerialClient as
|
||||
Client)
|
||||
|
||||
ioloop = IOLoop()
|
||||
protocol = EventLoopThread("ioloop", ioloop.start, ioloop.stop)
|
||||
protocol.start()
|
||||
client = Client(port=port, framer=framer, ioloop=ioloop, **kwargs)
|
||||
|
||||
future = client.connect()
|
||||
|
||||
return protocol, future
|
||||
|
||||
|
||||
def async_io_factory(port=None, framer=None, **kwargs):
|
||||
"""
|
||||
Factory to create asyncio based asynchronous serial clients
|
||||
:param port: Serial port
|
||||
:param framer: Modbus Framer
|
||||
:param kwargs: Serial port options
|
||||
:return: asyncio event loop and serial client
|
||||
"""
|
||||
import asyncio
|
||||
from pymodbus.client.asynchronous.async_io import (ModbusClientProtocol,
|
||||
AsyncioModbusSerialClient)
|
||||
loop = kwargs.pop("loop", None) or asyncio.get_event_loop()
|
||||
proto_cls = kwargs.pop("proto_cls", None) or ModbusClientProtocol
|
||||
|
||||
try:
|
||||
from serial_asyncio import create_serial_connection
|
||||
except ImportError:
|
||||
LOGGER.critical("pyserial-asyncio is not installed, "
|
||||
"install with 'pip install pyserial-asyncio")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
|
||||
client = AsyncioModbusSerialClient(port, proto_cls, framer, loop, **kwargs)
|
||||
coro = client.connect()
|
||||
if loop.is_running():
|
||||
future = asyncio.run_coroutine_threadsafe(coro, loop=loop)
|
||||
future.result()
|
||||
else:
|
||||
loop.run_until_complete(coro)
|
||||
return loop, client
|
||||
|
||||
|
||||
def get_factory(scheduler):
|
||||
"""
|
||||
Gets protocol factory based on the backend scheduler being used
|
||||
:param scheduler: REACTOR/IO_LOOP/ASYNC_IO
|
||||
:return:
|
||||
"""
|
||||
if scheduler == schedulers.REACTOR:
|
||||
return reactor_factory
|
||||
elif scheduler == schedulers.IO_LOOP:
|
||||
return io_loop_factory
|
||||
elif scheduler == schedulers.ASYNC_IO:
|
||||
return async_io_factory
|
||||
else:
|
||||
LOGGER.warning("Allowed Schedulers: {}, {}, {}".format(
|
||||
schedulers.REACTOR, schedulers.IO_LOOP, schedulers.ASYNC_IO
|
||||
))
|
||||
raise Exception("Invalid Scheduler '{}'".format(scheduler))
|
||||
@@ -1,123 +0,0 @@
|
||||
"""
|
||||
Factory to create asynchronous tcp clients based on twisted/tornado/asyncio
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
from pymodbus.client.asynchronous.thread import EventLoopThread
|
||||
from pymodbus.constants import Defaults
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def reactor_factory(host="127.0.0.1", port=Defaults.Port, framer=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Factory to create twisted tcp asynchronous client
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and twisted_deferred
|
||||
"""
|
||||
from twisted.internet import reactor, protocol
|
||||
from pymodbus.client.asynchronous.twisted import ModbusTcpClientProtocol
|
||||
|
||||
deferred = protocol.ClientCreator(
|
||||
reactor, ModbusTcpClientProtocol
|
||||
).connectTCP(host, port, timeout=timeout, bindAddress=source_address)
|
||||
|
||||
callback = kwargs.get("callback")
|
||||
errback = kwargs.get("errback")
|
||||
|
||||
if callback:
|
||||
deferred.addCallback(callback)
|
||||
|
||||
if errback:
|
||||
deferred.addErrback(errback)
|
||||
|
||||
protocol = EventLoopThread("reactor", reactor.run, reactor.stop,
|
||||
installSignalHandlers=0)
|
||||
protocol.start()
|
||||
|
||||
return protocol, deferred
|
||||
|
||||
|
||||
def io_loop_factory(host="127.0.0.1", port=Defaults.Port, framer=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Factory to create Tornado based asynchronous tcp clients
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and tornado future
|
||||
"""
|
||||
from tornado.ioloop import IOLoop
|
||||
from pymodbus.client.asynchronous.tornado import AsyncModbusTCPClient as \
|
||||
Client
|
||||
|
||||
ioloop = IOLoop()
|
||||
protocol = EventLoopThread("ioloop", ioloop.start, ioloop.stop)
|
||||
protocol.start()
|
||||
|
||||
client = Client(host=host, port=port, framer=framer,
|
||||
source_address=source_address,
|
||||
timeout=timeout, ioloop=ioloop, **kwargs)
|
||||
|
||||
future = client.connect()
|
||||
|
||||
return protocol, future
|
||||
|
||||
|
||||
def async_io_factory(host="127.0.0.1", port=Defaults.Port, **kwargs):
|
||||
"""
|
||||
Factory to create asyncio based asynchronous tcp clients
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: asyncio event loop and tcp client
|
||||
"""
|
||||
import asyncio
|
||||
from pymodbus.client.asynchronous.async_io import init_tcp_client
|
||||
loop = kwargs.pop("loop", None) or asyncio.new_event_loop()
|
||||
proto_cls = kwargs.pop("proto_cls", None)
|
||||
if not loop.is_running():
|
||||
asyncio.set_event_loop(loop)
|
||||
cor = init_tcp_client(proto_cls, loop, host, port, **kwargs)
|
||||
client = loop.run_until_complete(asyncio.gather(cor))[0]
|
||||
else:
|
||||
cor = init_tcp_client(proto_cls, loop, host, port, **kwargs)
|
||||
future = asyncio.run_coroutine_threadsafe(cor, loop=loop)
|
||||
client = future.result()
|
||||
|
||||
return loop, client
|
||||
|
||||
|
||||
def get_factory(scheduler):
|
||||
"""
|
||||
Gets protocol factory based on the backend scheduler being used
|
||||
:param scheduler: REACTOR/IO_LOOP/ASYNC_IO
|
||||
:return
|
||||
"""
|
||||
if scheduler == schedulers.REACTOR:
|
||||
return reactor_factory
|
||||
elif scheduler == schedulers.IO_LOOP:
|
||||
return io_loop_factory
|
||||
elif scheduler == schedulers.ASYNC_IO:
|
||||
return async_io_factory
|
||||
else:
|
||||
LOGGER.warning("Allowed Schedulers: {}, {}, {}".format(
|
||||
schedulers.REACTOR, schedulers.IO_LOOP, schedulers.ASYNC_IO
|
||||
))
|
||||
raise Exception("Invalid Scheduler '{}'".format(scheduler))
|
||||
@@ -1,59 +0,0 @@
|
||||
"""
|
||||
Factory to create asynchronous tls clients based on asyncio
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
from pymodbus.client.asynchronous.thread import EventLoopThread
|
||||
from pymodbus.constants import Defaults
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def async_io_factory(host="127.0.0.1", port=Defaults.TLSPort, sslctx=None,
|
||||
server_hostname=None, framer=None, **kwargs):
|
||||
"""
|
||||
Factory to create asyncio based asynchronous tls clients
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto create)
|
||||
:param server_hostname: Target server's name matched for certificate
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: asyncio event loop and tcp client
|
||||
"""
|
||||
import asyncio
|
||||
from pymodbus.client.asynchronous.async_io import init_tls_client
|
||||
loop = kwargs.pop("loop", None) or asyncio.new_event_loop()
|
||||
proto_cls = kwargs.pop("proto_cls", None)
|
||||
if not loop.is_running():
|
||||
asyncio.set_event_loop(loop)
|
||||
cor = init_tls_client(proto_cls, loop, host, port, sslctx, server_hostname,
|
||||
framer, **kwargs)
|
||||
client = loop.run_until_complete(asyncio.gather(cor))[0]
|
||||
else:
|
||||
cor = init_tls_client(proto_cls, loop, host, port, sslctx, server_hostname,
|
||||
framer, **kwargs)
|
||||
future = asyncio.run_coroutine_threadsafe(cor, loop=loop)
|
||||
client = future.result()
|
||||
|
||||
return loop, client
|
||||
|
||||
|
||||
def get_factory(scheduler):
|
||||
"""
|
||||
Gets protocol factory based on the backend scheduler being used
|
||||
:param scheduler: ASYNC_IO
|
||||
:return
|
||||
"""
|
||||
if scheduler == schedulers.ASYNC_IO:
|
||||
return async_io_factory
|
||||
else:
|
||||
LOGGER.warning("Allowed Schedulers: {}".format(
|
||||
schedulers.ASYNC_IO
|
||||
))
|
||||
raise Exception("Invalid Scheduler '{}'".format(scheduler))
|
||||
@@ -1,96 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
from pymodbus.client.asynchronous.thread import EventLoopThread
|
||||
from pymodbus.constants import Defaults
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def reactor_factory(host="127.0.0.1", port=Defaults.Port, framer=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Factory to create twisted udp asynchronous client
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and twisted_deferred
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def io_loop_factory(host="127.0.0.1", port=Defaults.Port, framer=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Factory to create Tornado based asynchronous udp clients
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and tornado future
|
||||
"""
|
||||
from tornado.ioloop import IOLoop
|
||||
from pymodbus.client.asynchronous.tornado import AsyncModbusUDPClient as \
|
||||
Client
|
||||
|
||||
client = Client(host=host, port=port, framer=framer,
|
||||
source_address=source_address,
|
||||
timeout=timeout, **kwargs)
|
||||
protocol = EventLoopThread("ioloop", IOLoop.current().start,
|
||||
IOLoop.current().stop)
|
||||
protocol.start()
|
||||
future = client.connect()
|
||||
|
||||
return protocol, future
|
||||
|
||||
|
||||
def async_io_factory(host="127.0.0.1", port=Defaults.Port, **kwargs):
|
||||
"""
|
||||
Factory to create asyncio based asynchronous udp clients
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: asyncio event loop and udp client
|
||||
"""
|
||||
import asyncio
|
||||
from pymodbus.client.asynchronous.async_io import init_udp_client
|
||||
loop = kwargs.pop("loop", None) or asyncio.get_event_loop()
|
||||
proto_cls = kwargs.pop("proto_cls", None)
|
||||
cor = init_udp_client(proto_cls, loop, host, port, **kwargs)
|
||||
if not loop.is_running():
|
||||
client = loop.run_until_complete(asyncio.gather(cor))[0]
|
||||
else:
|
||||
client = asyncio.run_coroutine_threadsafe(cor, loop=loop)
|
||||
client = client.result()
|
||||
return loop, client
|
||||
|
||||
|
||||
def get_factory(scheduler):
|
||||
"""
|
||||
Gets protocol factory based on the backend scheduler being used
|
||||
:param scheduler: REACTOR/IO_LOOP/ASYNC_IO
|
||||
:return
|
||||
"""
|
||||
if scheduler == schedulers.REACTOR:
|
||||
return reactor_factory
|
||||
elif scheduler == schedulers.IO_LOOP:
|
||||
return io_loop_factory
|
||||
elif scheduler == schedulers.ASYNC_IO:
|
||||
return async_io_factory
|
||||
else:
|
||||
LOGGER.warning("Allowed Schedulers: {}, {}, {}".format(
|
||||
schedulers.REACTOR, schedulers.IO_LOOP, schedulers.ASYNC_IO
|
||||
))
|
||||
raise Exception("Invalid Scheduler '{}'".format(scheduler))
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import logging
|
||||
from pymodbus.client.sync import BaseModbusClient
|
||||
# from pymodbus.bit_read_message import *
|
||||
# from pymodbus.bit_write_message import *
|
||||
# from pymodbus.register_read_message import *
|
||||
# from pymodbus.register_write_message import *
|
||||
# from pymodbus.diag_message import *
|
||||
# from pymodbus.file_message import *
|
||||
# from pymodbus.other_message import *
|
||||
from pymodbus.constants import Defaults
|
||||
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.transaction import ModbusSocketFramer
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseAsyncModbusClient(BaseModbusClient):
|
||||
"""
|
||||
This represents the base ModbusAsyncClient.
|
||||
"""
|
||||
|
||||
def __init__(self, framer=None, timeout=2, **kwargs):
|
||||
""" Initializes the framer module
|
||||
|
||||
:param framer: The framer to use for the protocol. Default:
|
||||
ModbusSocketFramer
|
||||
:type framer: pymodbus.transaction.ModbusSocketFramer
|
||||
"""
|
||||
self._connected = False
|
||||
self._timeout = timeout
|
||||
|
||||
super(BaseAsyncModbusClient, self).__init__(
|
||||
framer or ModbusSocketFramer(ClientDecoder()), **kwargs
|
||||
)
|
||||
|
||||
|
||||
class AsyncModbusClientMixin(BaseAsyncModbusClient):
|
||||
"""
|
||||
Async Modbus client mixing for UDP and TCP clients
|
||||
"""
|
||||
def __init__(self, host="127.0.0.1", port=Defaults.Port, framer=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Initializes a Modbus TCP/UDP asynchronous client
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Framer to use
|
||||
:param source_address: Specific to underlying client being used
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs: Extra arguments
|
||||
"""
|
||||
super(AsyncModbusClientMixin, self).__init__(framer=framer, **kwargs)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.source_address = source_address or ("", 0)
|
||||
self._timeout = timeout if timeout is not None else Defaults.Timeout
|
||||
|
||||
|
||||
class AsyncModbusSerialClientMixin(BaseAsyncModbusClient):
|
||||
"""
|
||||
Async Modbus Serial Client Mixing
|
||||
"""
|
||||
def __init__(self, framer=None, port=None, **kwargs):
|
||||
"""
|
||||
Initializes a Async Modbus Serial Client
|
||||
:param framer: Modbus Framer
|
||||
:param port: Serial port to use
|
||||
:param kwargs: Extra arguments if any
|
||||
"""
|
||||
super(AsyncModbusSerialClientMixin, self).__init__(framer=framer)
|
||||
self.port = port
|
||||
self.serial_settings = kwargs
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
"""
|
||||
Backend schedulers to use with generic Async clients
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
REACTOR = "reactor"
|
||||
IO_LOOP = "io_loop"
|
||||
ASYNC_IO = "async_io"
|
||||
@@ -1,76 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from pymodbus.client.asynchronous.factory.serial import get_factory
|
||||
from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer, ModbusBinaryFramer, ModbusSocketFramer
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.exceptions import ParameterException
|
||||
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
|
||||
from pymodbus.client.asynchronous.schedulers import ASYNC_IO
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncModbusSerialClient(object):
|
||||
"""
|
||||
Actual Async Serial Client to be used.
|
||||
|
||||
To use do::
|
||||
|
||||
from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient
|
||||
"""
|
||||
@classmethod
|
||||
def _framer(cls, method):
|
||||
"""
|
||||
Returns the requested framer
|
||||
|
||||
:method: The serial framer to instantiate
|
||||
:returns: The requested serial framer
|
||||
"""
|
||||
method = method.lower()
|
||||
if method == 'ascii':
|
||||
return ModbusAsciiFramer(ClientDecoder())
|
||||
elif method == 'rtu':
|
||||
return ModbusRtuFramer(ClientDecoder())
|
||||
elif method == 'binary':
|
||||
return ModbusBinaryFramer(ClientDecoder())
|
||||
elif method == 'socket':
|
||||
return ModbusSocketFramer(ClientDecoder())
|
||||
|
||||
raise ParameterException("Invalid framer method requested")
|
||||
|
||||
def __new__(cls, scheduler, method, port, **kwargs):
|
||||
"""
|
||||
Scheduler to use:
|
||||
- reactor (Twisted)
|
||||
- io_loop (Tornado)
|
||||
- async_io (asyncio)
|
||||
The methods to connect are::
|
||||
|
||||
- ascii
|
||||
- rtu
|
||||
- binary
|
||||
: param scheduler: Backend to use
|
||||
:param method: The method to use for connection
|
||||
:param port: The serial port to attach to
|
||||
:param stopbits: The number of stop bits to use
|
||||
:param bytesize: The bytesize of the serial messages
|
||||
:param parity: Which kind of parity to use
|
||||
:param baudrate: The baud rate to use for the serial device
|
||||
:param timeout: The timeout between serial requests (default 3s)
|
||||
:param scheduler:
|
||||
:param method:
|
||||
:param port:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
if (not (IS_PYTHON3 and PYTHON_VERSION >= (3, 4))
|
||||
and scheduler == ASYNC_IO):
|
||||
logger.critical("ASYNCIO is supported only on python3")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
factory_class = get_factory(scheduler)
|
||||
framer = cls._framer(method)
|
||||
yieldable = factory_class(framer=framer, port=port, **kwargs)
|
||||
return yieldable
|
||||
@@ -1,47 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from pymodbus.client.asynchronous.factory.tcp import get_factory
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
|
||||
from pymodbus.client.asynchronous.schedulers import ASYNC_IO
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncModbusTCPClient(object):
|
||||
"""
|
||||
Actual Async Serial Client to be used.
|
||||
|
||||
To use do::
|
||||
|
||||
from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient
|
||||
"""
|
||||
def __new__(cls, scheduler, host="127.0.0.1", port=Defaults.Port,
|
||||
framer=None, source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Scheduler to use:
|
||||
- reactor (Twisted)
|
||||
- io_loop (Tornado)
|
||||
- async_io (asyncio)
|
||||
:param scheduler: Backend to use
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer to use
|
||||
:param source_address: source address specific to underlying backend
|
||||
:param timeout: Time out in seconds
|
||||
:param kwargs: Other extra args specific to Backend being used
|
||||
:return:
|
||||
"""
|
||||
if (not (IS_PYTHON3 and PYTHON_VERSION >= (3, 4))
|
||||
and scheduler == ASYNC_IO):
|
||||
logger.critical("ASYNCIO is supported only on python3")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
factory_class = get_factory(scheduler)
|
||||
yieldable = factory_class(host=host, port=port, framer=framer,
|
||||
source_address=source_address,
|
||||
timeout=timeout, **kwargs)
|
||||
return yieldable
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
from threading import Thread
|
||||
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventLoopThread(object):
|
||||
"""
|
||||
Event loop controlling the backend event loops (io_loop for tornado,
|
||||
reactor for twisted and event_loop for Asyncio)
|
||||
"""
|
||||
def __init__(self, name, start, stop, *args, **kwargs):
|
||||
"""
|
||||
Initialize Event loop thread
|
||||
:param name: Name of the event loop
|
||||
:param start: Start method to start the backend event loop
|
||||
:param stop: Stop method to stop the backend event loop
|
||||
:param args:
|
||||
:param kwargs:
|
||||
"""
|
||||
self._name = name
|
||||
self._start_loop = start
|
||||
self._stop_loop = stop
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
self._event_loop = Thread(name=self._name, target=self._start)
|
||||
self._event_loop.daemon = True
|
||||
|
||||
def _start(self):
|
||||
"""
|
||||
Starts the backend event loop
|
||||
:return:
|
||||
"""
|
||||
self._start_loop(*self._args, **self._kwargs)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts the backend event loop
|
||||
:return:
|
||||
"""
|
||||
LOGGER.info("Starting Event Loop: 'PyModbus_{}'".format(self._name))
|
||||
self._event_loop.start()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops the backend event loop
|
||||
:return:
|
||||
"""
|
||||
LOGGER.info("Stopping Event Loop: 'PyModbus_{}'".format(self._name))
|
||||
self._stop_loop()
|
||||
@@ -1,52 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from pymodbus.client.asynchronous.factory.tls import get_factory
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
|
||||
from pymodbus.client.asynchronous.schedulers import ASYNC_IO
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.transaction import ModbusTlsFramer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncModbusTLSClient(object):
|
||||
"""
|
||||
Actual Async TLS Client to be used.
|
||||
|
||||
To use do::
|
||||
|
||||
from pymodbus.client.asynchronous.tls import AsyncModbusTLSClient
|
||||
"""
|
||||
def __new__(cls, scheduler, host="127.0.0.1", port=Defaults.TLSPort,
|
||||
framer=None, sslctx=None, server_hostname=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Scheduler to use:
|
||||
- async_io (asyncio)
|
||||
:param scheduler: Backend to use
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer to use
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto create)
|
||||
:param server_hostname: Target server's name matched for certificate
|
||||
:param source_address: source address specific to underlying backend
|
||||
:param timeout: Time out in seconds
|
||||
:param kwargs: Other extra args specific to Backend being used
|
||||
:return:
|
||||
"""
|
||||
if (not (IS_PYTHON3 and PYTHON_VERSION >= (3, 4))
|
||||
and scheduler == ASYNC_IO):
|
||||
logger.critical("ASYNCIO is supported only on python3")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
framer = framer or ModbusTlsFramer(ClientDecoder())
|
||||
factory_class = get_factory(scheduler)
|
||||
yieldable = factory_class(host=host, port=port, sslctx=sslctx,
|
||||
server_hostname=server_hostname,
|
||||
framer=framer, source_address=source_address,
|
||||
timeout=timeout, **kwargs)
|
||||
return yieldable
|
||||
|
||||
@@ -1,498 +0,0 @@
|
||||
"""
|
||||
Asynchronous framework adapter for tornado.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import abc
|
||||
|
||||
import logging
|
||||
|
||||
import time
|
||||
import socket
|
||||
from serial import Serial
|
||||
from tornado import gen
|
||||
from tornado.concurrent import Future
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.iostream import IOStream
|
||||
from tornado.iostream import BaseIOStream
|
||||
|
||||
from pymodbus.client.asynchronous.mixins import (AsyncModbusClientMixin,
|
||||
AsyncModbusSerialClientMixin)
|
||||
|
||||
from pymodbus.compat import byte2int
|
||||
from pymodbus.exceptions import (ConnectionException,
|
||||
ModbusIOException,
|
||||
TimeOutException)
|
||||
from pymodbus.utilities import (hexlify_packets,
|
||||
ModbusTransactionState)
|
||||
from pymodbus.constants import Defaults
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseTornadoClient(AsyncModbusClientMixin):
|
||||
"""
|
||||
Base Tornado client
|
||||
"""
|
||||
stream = None
|
||||
io_loop = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initializes BaseTornadoClient.
|
||||
ioloop to be passed as part of kwargs ('ioloop')
|
||||
:param args:
|
||||
:param kwargs:
|
||||
"""
|
||||
self.io_loop = kwargs.pop("ioloop", None)
|
||||
super(BaseTornadoClient, self).__init__(*args, **kwargs)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_socket(self):
|
||||
"""
|
||||
return instance of the socket to connect to
|
||||
"""
|
||||
|
||||
@gen.coroutine
|
||||
def connect(self):
|
||||
"""
|
||||
Connect to the socket identified by host and port
|
||||
|
||||
:returns: Future
|
||||
:rtype: tornado.concurrent.Future
|
||||
"""
|
||||
conn = self.get_socket()
|
||||
self.stream = IOStream(conn, io_loop=self.io_loop or IOLoop.current())
|
||||
self.stream.connect((self.host, self.port))
|
||||
self.stream.read_until_close(None,
|
||||
streaming_callback=self.on_receive)
|
||||
self._connected = True
|
||||
LOGGER.debug("Client connected")
|
||||
|
||||
raise gen.Return(self)
|
||||
|
||||
def on_receive(self, *args):
|
||||
"""
|
||||
On data recieve call back
|
||||
:param args: data received
|
||||
:return:
|
||||
"""
|
||||
data = args[0] if len(args) > 0 else None
|
||||
|
||||
if not data:
|
||||
return
|
||||
LOGGER.debug("recv: " + hexlify_packets(data))
|
||||
unit = self.framer.decode_data(data).get("unit", 0)
|
||||
self.framer.processIncomingPacket(data, self._handle_response, unit=unit)
|
||||
|
||||
def execute(self, request=None):
|
||||
"""
|
||||
Executes a transaction
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
LOGGER.debug("send: " + hexlify_packets(packet))
|
||||
self.stream.write(packet)
|
||||
return self._build_response(request.transaction_id)
|
||||
|
||||
def _handle_response(self, reply, **kwargs):
|
||||
"""
|
||||
Handle response received
|
||||
:param reply:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
future = self.transaction.getTransaction(tid)
|
||||
if future:
|
||||
future.set_result(reply)
|
||||
else:
|
||||
LOGGER.debug("Unrequested message: {}".format(reply))
|
||||
|
||||
def _build_response(self, tid):
|
||||
"""
|
||||
Builds a future response
|
||||
:param tid:
|
||||
:return:
|
||||
"""
|
||||
f = Future()
|
||||
|
||||
if not self._connected:
|
||||
f.set_exception(ConnectionException("Client is not connected"))
|
||||
return f
|
||||
|
||||
self.transaction.addTransaction(f, tid)
|
||||
return f
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the underlying IOStream
|
||||
"""
|
||||
LOGGER.debug("Client disconnected")
|
||||
if self.stream:
|
||||
self.stream.close_fd()
|
||||
|
||||
self.stream = None
|
||||
self._connected = False
|
||||
|
||||
|
||||
class BaseTornadoSerialClient(AsyncModbusSerialClientMixin):
|
||||
"""
|
||||
Base Tonado serial client
|
||||
"""
|
||||
stream = None
|
||||
io_loop = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initializes BaseTornadoSerialClient.
|
||||
ioloop to be passed as part of kwargs ('ioloop')
|
||||
:param args:
|
||||
:param kwargs:
|
||||
"""
|
||||
self.io_loop = kwargs.pop("ioloop", None)
|
||||
super(BaseTornadoSerialClient, self).__init__(*args, **kwargs)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_socket(self):
|
||||
"""
|
||||
return instance of the socket to connect to
|
||||
"""
|
||||
|
||||
def on_receive(self, *args):
|
||||
# Will be handled ine execute method
|
||||
pass
|
||||
|
||||
def execute(self, request=None):
|
||||
"""
|
||||
Executes a transaction
|
||||
:param request: Request to be written on to the bus
|
||||
:return:
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
|
||||
def callback(*args):
|
||||
LOGGER.debug("in callback - {}".format(request.transaction_id))
|
||||
while True:
|
||||
waiting = self.stream.connection.in_waiting
|
||||
if waiting:
|
||||
data = self.stream.connection.read(waiting)
|
||||
LOGGER.debug(
|
||||
"recv: " + hexlify_packets(data))
|
||||
unit = self.framer.decode_data(data).get("uid", 0)
|
||||
self.framer.processIncomingPacket(
|
||||
data,
|
||||
self._handle_response,
|
||||
unit,
|
||||
tid=request.transaction_id
|
||||
)
|
||||
break
|
||||
|
||||
packet = self.framer.buildPacket(request)
|
||||
LOGGER.debug("send: " + hexlify_packets(packet))
|
||||
self.stream.write(packet, callback=callback)
|
||||
f = self._build_response(request.transaction_id)
|
||||
return f
|
||||
|
||||
def _handle_response(self, reply, **kwargs):
|
||||
"""
|
||||
Handles a received response and updates a future
|
||||
:param reply: Reply received
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
future = self.transaction.getTransaction(tid)
|
||||
if future:
|
||||
future.set_result(reply)
|
||||
else:
|
||||
LOGGER.debug("Unrequested message: {}".format(reply))
|
||||
|
||||
def _build_response(self, tid):
|
||||
"""
|
||||
Prepare for a response, returns a future
|
||||
:param tid:
|
||||
:return: Future
|
||||
"""
|
||||
f = Future()
|
||||
|
||||
if not self._connected:
|
||||
f.set_exception(ConnectionException("Client is not connected"))
|
||||
return f
|
||||
|
||||
self.transaction.addTransaction(f, tid)
|
||||
return f
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the underlying IOStream
|
||||
"""
|
||||
LOGGER.debug("Client disconnected")
|
||||
if self.stream:
|
||||
self.stream.close_fd()
|
||||
|
||||
self.stream = None
|
||||
self._connected = False
|
||||
|
||||
|
||||
class SerialIOStream(BaseIOStream):
|
||||
"""
|
||||
Serial IO Stream class to control and handle serial connections
|
||||
over tornado
|
||||
"""
|
||||
def __init__(self, connection, *args, **kwargs):
|
||||
"""
|
||||
Initializes Serial IO Stream
|
||||
:param connection: serial object
|
||||
:param args:
|
||||
:param kwargs:
|
||||
"""
|
||||
self.connection = connection
|
||||
super(SerialIOStream, self).__init__(*args, **kwargs)
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
Returns serial fd
|
||||
:return:
|
||||
"""
|
||||
return self.connection.fileno()
|
||||
|
||||
def close_fd(self):
|
||||
"""
|
||||
Closes a serial Fd
|
||||
:return:
|
||||
"""
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
self.connection = None
|
||||
|
||||
def read_from_fd(self):
|
||||
"""
|
||||
Reads from a fd
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
chunk = self.connection.readline()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return chunk
|
||||
|
||||
def write_to_fd(self, data):
|
||||
"""
|
||||
Writes to a fd
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
return self.connection.write(data)
|
||||
except Exception as e:
|
||||
LOGGER.error(e)
|
||||
|
||||
|
||||
class AsyncModbusSerialClient(BaseTornadoSerialClient):
|
||||
"""
|
||||
Tornado based asynchronous serial client
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initializes AsyncModbusSerialClient.
|
||||
:param args:
|
||||
:param kwargs:
|
||||
"""
|
||||
self.state = ModbusTransactionState.IDLE
|
||||
self.timeout = kwargs.get('timeout', Defaults.Timeout)
|
||||
self.baudrate = kwargs.get('baudrate', Defaults.Baudrate)
|
||||
if self.baudrate > 19200:
|
||||
self.silent_interval = 1.75 / 1000 # ms
|
||||
else:
|
||||
self._t0 = float((1 + 8 + 2)) / self.baudrate
|
||||
self.silent_interval = 3.5 * self._t0
|
||||
self.silent_interval = round(self.silent_interval, 6)
|
||||
self.last_frame_end = 0.0
|
||||
super(AsyncModbusSerialClient, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_socket(self):
|
||||
"""
|
||||
Creates Pyserial object
|
||||
:return: serial object
|
||||
"""
|
||||
return Serial(port=self.port, **self.serial_settings)
|
||||
|
||||
@gen.coroutine
|
||||
def connect(self):
|
||||
"""Connect to the socket identified by host and port
|
||||
|
||||
:returns: Future
|
||||
:rtype: tornado.concurrent.Future
|
||||
"""
|
||||
conn = self.get_socket()
|
||||
if self.io_loop is None:
|
||||
self.io_loop = IOLoop.current()
|
||||
try:
|
||||
self.stream = SerialIOStream(conn, io_loop=self.io_loop)
|
||||
except Exception as e:
|
||||
LOGGER.exception(e)
|
||||
|
||||
self._connected = True
|
||||
LOGGER.debug("Client connected")
|
||||
|
||||
raise gen.Return(self)
|
||||
|
||||
def execute(self, request):
|
||||
"""
|
||||
Executes a transaction
|
||||
:param request: Request to be written on to the bus
|
||||
:return:
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
|
||||
def _clear_timer():
|
||||
"""
|
||||
Clear serial waiting timeout
|
||||
"""
|
||||
if self.timeout_handle:
|
||||
self.io_loop.remove_timeout(self.timeout_handle)
|
||||
self.timeout_handle = None
|
||||
|
||||
def _on_timeout():
|
||||
"""
|
||||
Got timeout while waiting data from serial port
|
||||
"""
|
||||
LOGGER.warning("serial receive timeout")
|
||||
_clear_timer()
|
||||
if self.stream:
|
||||
self.io_loop.remove_handler(self.stream.fileno())
|
||||
self.framer.resetFrame()
|
||||
transaction = self.transaction.getTransaction(request.transaction_id)
|
||||
if transaction:
|
||||
transaction.set_exception(TimeOutException())
|
||||
|
||||
def _on_write_done(*args):
|
||||
"""
|
||||
Set up reader part after sucessful write to the serial
|
||||
"""
|
||||
LOGGER.debug("frame sent, waiting for a reply")
|
||||
self.last_frame_end = round(time.time(), 6)
|
||||
self.state = ModbusTransactionState.WAITING_FOR_REPLY
|
||||
self.io_loop.add_handler(self.stream.fileno(), _on_receive, IOLoop.READ)
|
||||
|
||||
def _on_fd_error(fd, *args):
|
||||
_clear_timer()
|
||||
self.io_loop.remove_handler(fd)
|
||||
self.close()
|
||||
self.transaction.getTransaction(request.transaction_id).set_exception(ModbusIOException(*args))
|
||||
|
||||
def _on_receive(fd, events):
|
||||
"""
|
||||
New data in serial buffer to read or serial port closed
|
||||
"""
|
||||
if events & IOLoop.ERROR:
|
||||
_on_fd_error(fd)
|
||||
return
|
||||
|
||||
try:
|
||||
waiting = self.stream.connection.in_waiting
|
||||
if waiting:
|
||||
data = self.stream.connection.read(waiting)
|
||||
LOGGER.debug(
|
||||
"recv: " + hexlify_packets(data))
|
||||
self.last_frame_end = round(time.time(), 6)
|
||||
except OSError as ex:
|
||||
_on_fd_error(fd, ex)
|
||||
return
|
||||
|
||||
self.framer.addToFrame(data)
|
||||
|
||||
# check if we have regular frame or modbus exception
|
||||
fcode = self.framer.decode_data(self.framer.getRawFrame()).get("fcode", 0)
|
||||
if fcode and (
|
||||
(fcode > 0x80 and len(self.framer.getRawFrame()) == exception_response_length)
|
||||
or
|
||||
(len(self.framer.getRawFrame()) == expected_response_length)
|
||||
):
|
||||
_clear_timer()
|
||||
self.io_loop.remove_handler(fd)
|
||||
self.state = ModbusTransactionState.IDLE
|
||||
self.framer.processIncomingPacket(
|
||||
b'', # already sent via addToFrame()
|
||||
self._handle_response,
|
||||
0, # don't care when `single=True`
|
||||
single=True,
|
||||
tid=request.transaction_id
|
||||
)
|
||||
|
||||
packet = self.framer.buildPacket(request)
|
||||
f = self._build_response(request.transaction_id)
|
||||
|
||||
response_pdu_size = request.get_response_pdu_size()
|
||||
expected_response_length = self.transaction._calculate_response_length(response_pdu_size)
|
||||
LOGGER.debug("expected_response_length = %d", expected_response_length)
|
||||
|
||||
exception_response_length = self.transaction._calculate_exception_length() # TODO: calculate once
|
||||
|
||||
if self.timeout:
|
||||
self.timeout_handle = self.io_loop.add_timeout(time.time() + self.timeout, _on_timeout)
|
||||
self._sendPacket(packet, callback=_on_write_done)
|
||||
|
||||
return f
|
||||
|
||||
def _sendPacket(self, message, callback):
|
||||
"""
|
||||
Sends packets on the bus with 3.5char delay between frames
|
||||
:param message: Message to be sent over the bus
|
||||
:return:
|
||||
"""
|
||||
@gen.coroutine
|
||||
def sleep(timeout):
|
||||
yield gen.sleep(timeout)
|
||||
|
||||
try:
|
||||
waiting = self.stream.connection.in_waiting
|
||||
if waiting:
|
||||
result = self.stream.connection.read(waiting)
|
||||
LOGGER.info(
|
||||
"Cleanup recv buffer before send: " + hexlify_packets(result))
|
||||
except OSError as e:
|
||||
self.transaction.getTransaction(
|
||||
message.transaction_id).set_exception(ModbusIOException(e))
|
||||
return
|
||||
|
||||
start = time.time()
|
||||
if self.last_frame_end:
|
||||
waittime = self.last_frame_end + self.silent_interval - start
|
||||
if waittime > 0:
|
||||
LOGGER.debug("Waiting for 3.5 char before next send - %f ms", waittime)
|
||||
sleep(waittime)
|
||||
|
||||
self.state = ModbusTransactionState.SENDING
|
||||
LOGGER.debug("send: " + hexlify_packets(message))
|
||||
self.stream.write(message, callback)
|
||||
|
||||
class AsyncModbusTCPClient(BaseTornadoClient):
|
||||
"""
|
||||
Tornado based Async tcp client
|
||||
"""
|
||||
def get_socket(self):
|
||||
"""
|
||||
Creates socket object
|
||||
:return: socket
|
||||
"""
|
||||
return socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
|
||||
|
||||
|
||||
class AsyncModbusUDPClient(BaseTornadoClient):
|
||||
"""
|
||||
Tornado based Async UDP client
|
||||
"""
|
||||
def get_socket(self):
|
||||
"""
|
||||
Create socket object
|
||||
:return: socket
|
||||
"""
|
||||
return socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
|
||||
@@ -1,251 +0,0 @@
|
||||
"""
|
||||
Implementation of a Modbus Client Using Twisted
|
||||
--------------------------------------------------
|
||||
|
||||
Example run::
|
||||
|
||||
from twisted.internet import reactor, protocol
|
||||
from pymodbus.client.asynchronous import ModbusClientProtocol
|
||||
|
||||
def printResult(result):
|
||||
print "Result: %d" % result.bits[0]
|
||||
|
||||
def process(client):
|
||||
result = client.write_coil(1, True)
|
||||
result.addCallback(printResult)
|
||||
reactor.callLater(1, reactor.stop)
|
||||
|
||||
defer = protocol.ClientCreator(reactor, ModbusClientProtocol
|
||||
).connectTCP("localhost", 502)
|
||||
defer.addCallback(process)
|
||||
|
||||
Another example::
|
||||
|
||||
from twisted.internet import reactor
|
||||
from pymodbus.client.asynchronous import ModbusClientFactory
|
||||
|
||||
def process():
|
||||
factory = reactor.connectTCP("localhost", 502, ModbusClientFactory())
|
||||
reactor.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
reactor.callLater(1, process)
|
||||
reactor.run()
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from twisted.internet import defer, protocol
|
||||
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.client.asynchronous.mixins import AsyncModbusClientMixin
|
||||
from pymodbus.transaction import FifoTransactionManager, DictTransactionManager
|
||||
from pymodbus.transaction import ModbusSocketFramer, ModbusRtuFramer
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Connected Client Protocols
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusClientProtocol(protocol.Protocol,
|
||||
AsyncModbusClientMixin):
|
||||
"""
|
||||
This represents the base modbus client protocol. All the application
|
||||
layer code is deferred to a higher level wrapper.
|
||||
"""
|
||||
framer = None
|
||||
|
||||
def __init__(self, framer=None, **kwargs):
|
||||
self._connected = False
|
||||
self.framer = framer or ModbusSocketFramer(ClientDecoder())
|
||||
if isinstance(self.framer, type):
|
||||
# Framer class not instance
|
||||
self.framer = self.framer(ClientDecoder(), client=None)
|
||||
if isinstance(self.framer, ModbusSocketFramer):
|
||||
self.transaction = DictTransactionManager(self, **kwargs)
|
||||
else:
|
||||
self.transaction = FifoTransactionManager(self, **kwargs)
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
Called upon a successful client connection.
|
||||
"""
|
||||
_logger.debug("Client connected to modbus server")
|
||||
self._connected = True
|
||||
|
||||
def connectionLost(self, reason=None):
|
||||
"""
|
||||
Called upon a client disconnect
|
||||
|
||||
:param reason: The reason for the disconnect
|
||||
"""
|
||||
_logger.debug("Client disconnected from modbus server: %s" % reason)
|
||||
self._connected = False
|
||||
for tid in list(self.transaction):
|
||||
self.transaction.getTransaction(tid).errback(Failure(
|
||||
ConnectionException('Connection lost during request')))
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
Get response, check for valid message, decode result
|
||||
|
||||
:param data: The data returned from the server
|
||||
"""
|
||||
unit = self.framer.decode_data(data).get("unit", 0)
|
||||
self.framer.processIncomingPacket(data, self._handleResponse,
|
||||
unit=unit)
|
||||
|
||||
def execute(self, request):
|
||||
"""
|
||||
Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
_logger.debug("send: " + hexlify_packets(packet))
|
||||
self.transport.write(packet)
|
||||
return self._buildResponse(request.transaction_id)
|
||||
|
||||
def _handleResponse(self, reply, **kwargs):
|
||||
"""
|
||||
Handle the processed response and link to correct deferred
|
||||
|
||||
:param reply: The reply to process
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
handler = self.transaction.getTransaction(tid)
|
||||
if handler:
|
||||
handler.callback(reply)
|
||||
else:
|
||||
_logger.debug("Unrequested message: " + str(reply))
|
||||
|
||||
def _buildResponse(self, tid):
|
||||
"""
|
||||
Helper method to return a deferred response
|
||||
for the current request.
|
||||
|
||||
:param tid: The transaction identifier for this response
|
||||
:returns: A defer linked to the latest request
|
||||
"""
|
||||
if not self._connected:
|
||||
return defer.fail(Failure(
|
||||
ConnectionException('Client is not connected')))
|
||||
|
||||
d = defer.Deferred()
|
||||
self.transaction.addTransaction(d, tid)
|
||||
return d
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes underlying transport layer ,essentially closing the client
|
||||
:return:
|
||||
"""
|
||||
if self.transport and hasattr(self.transport, "close"):
|
||||
self.transport.close()
|
||||
self._connected = False
|
||||
|
||||
|
||||
class ModbusTcpClientProtocol(ModbusClientProtocol):
|
||||
"""
|
||||
Async TCP Client protocol based on twisted.
|
||||
|
||||
Default framer: ModbusSocketFramer
|
||||
"""
|
||||
framer = ModbusSocketFramer(ClientDecoder())
|
||||
|
||||
|
||||
class ModbusSerClientProtocol(ModbusClientProtocol):
|
||||
"""
|
||||
Async Serial Client protocol based on twisted
|
||||
|
||||
Default framer: ModbusRtuFramer
|
||||
"""
|
||||
def __init__(self, framer=None, **kwargs):
|
||||
framer = framer or ModbusRtuFramer(ClientDecoder())
|
||||
super(ModbusSerClientProtocol, self).__init__(framer, **kwargs)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Not Connected Client Protocol
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusUdpClientProtocol(protocol.DatagramProtocol,
|
||||
AsyncModbusClientMixin):
|
||||
"""
|
||||
This represents the base modbus client protocol. All the application
|
||||
layer code is deferred to a higher level wrapper.
|
||||
"""
|
||||
|
||||
def datagramReceived(self, data, params):
|
||||
"""
|
||||
Get response, check for valid message, decode result
|
||||
|
||||
:param data: The data returned from the server
|
||||
:param params: The host parameters sending the datagram
|
||||
"""
|
||||
_logger.debug("Datagram from: %s:%d" % params)
|
||||
unit = self.framer.decode_data(data).get("uid", 0)
|
||||
self.framer.processIncomingPacket(data, self._handleResponse, unit=unit)
|
||||
|
||||
def execute(self, request):
|
||||
"""
|
||||
Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
self.transport.write(packet, (self.host, self.port))
|
||||
return self._buildResponse(request.transaction_id)
|
||||
|
||||
def _handleResponse(self, reply, **kwargs):
|
||||
"""
|
||||
Handle the processed response and link to correct deferred
|
||||
|
||||
:param reply: The reply to process
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
handler = self.transaction.getTransaction(tid)
|
||||
if handler:
|
||||
handler.callback(reply)
|
||||
else: _logger.debug("Unrequested message: " + str(reply))
|
||||
|
||||
def _buildResponse(self, tid):
|
||||
"""
|
||||
Helper method to return a deferred response
|
||||
for the current request.
|
||||
|
||||
:param tid: The transaction identifier for this response
|
||||
:returns: A defer linked to the latest request
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
self.transaction.addTransaction(d, tid)
|
||||
return d
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Client Factories
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusClientFactory(protocol.ReconnectingClientFactory):
|
||||
""" Simple client protocol factory """
|
||||
|
||||
protocol = ModbusClientProtocol
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ModbusClientProtocol",
|
||||
"ModbusUdpClientProtocol",
|
||||
"ModbusClientFactory"
|
||||
]
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
|
||||
from pymodbus.client.asynchronous.schedulers import ASYNC_IO
|
||||
from pymodbus.client.asynchronous.factory.udp import get_factory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncModbusUDPClient(object):
|
||||
"""
|
||||
Actual Async UDP Client to be used.
|
||||
|
||||
To use do::
|
||||
|
||||
from pymodbus.client.asynchronous.tcp import AsyncModbusUDPClient
|
||||
"""
|
||||
def __new__(cls, scheduler, host="127.0.0.1", port=Defaults.Port,
|
||||
framer=None, source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Scheduler to use:
|
||||
- reactor (Twisted)
|
||||
- io_loop (Tornado)
|
||||
- async_io (asyncio)
|
||||
:param scheduler: Backend to use
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer to use
|
||||
:param source_address: source address specific to underlying backend
|
||||
:param timeout: Time out in seconds
|
||||
:param kwargs: Other extra args specific to Backend being used
|
||||
:return:
|
||||
"""
|
||||
if (not (IS_PYTHON3 and PYTHON_VERSION >= (3, 4))
|
||||
and scheduler == ASYNC_IO):
|
||||
logger.critical("ASYNCIO is supported only on python3")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
factory_class = get_factory(scheduler)
|
||||
yieldable = factory_class(host=host, port=port, framer=framer,
|
||||
source_address=source_address,
|
||||
timeout=timeout, **kwargs)
|
||||
return yieldable
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
'''
|
||||
Modbus Client Common
|
||||
----------------------------------
|
||||
|
||||
This is a common client mixin that can be used by
|
||||
both the synchronous and asynchronous clients to
|
||||
simplify the interface.
|
||||
'''
|
||||
from pymodbus.bit_read_message import *
|
||||
from pymodbus.bit_write_message import *
|
||||
from pymodbus.register_read_message import *
|
||||
from pymodbus.register_write_message import *
|
||||
from pymodbus.diag_message import *
|
||||
from pymodbus.file_message import *
|
||||
from pymodbus.other_message import *
|
||||
|
||||
from pymodbus.utilities import ModbusTransactionState
|
||||
|
||||
|
||||
class ModbusClientMixin(object):
|
||||
'''
|
||||
This is a modbus client mixin that provides additional factory
|
||||
methods for all the current modbus methods. This can be used
|
||||
instead of the normal pattern of::
|
||||
|
||||
# instead of this
|
||||
client = ModbusClient(...)
|
||||
request = ReadCoilsRequest(1,10)
|
||||
response = client.execute(request)
|
||||
|
||||
# now like this
|
||||
client = ModbusClient(...)
|
||||
response = client.read_coils(1, 10)
|
||||
'''
|
||||
state = ModbusTransactionState.IDLE
|
||||
last_frame_end = 0
|
||||
silent_interval = 0
|
||||
|
||||
def read_coils(self, address, count=1, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of coils to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = ReadCoilsRequest(address, count, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def read_discrete_inputs(self, address, count=1, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of discretes to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = ReadDiscreteInputsRequest(address, count, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def write_coil(self, address, value, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to write to
|
||||
:param value: The value to write to the specified address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = WriteSingleCoilRequest(address, value, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def write_coils(self, address, values, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to write to
|
||||
:param values: The values to write to the specified address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = WriteMultipleCoilsRequest(address, values, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def write_register(self, address, value, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to write to
|
||||
:param value: The value to write to the specified address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = WriteSingleRegisterRequest(address, value, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def write_registers(self, address, values, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to write to
|
||||
:param values: The values to write to the specified address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = WriteMultipleRegistersRequest(address, values, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def read_holding_registers(self, address, count=1, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of registers to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = ReadHoldingRegistersRequest(address, count, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def read_input_registers(self, address, count=1, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of registers to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = ReadInputRegistersRequest(address, count, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def readwrite_registers(self, *args, **kwargs):
|
||||
'''
|
||||
|
||||
:param read_address: The address to start reading from
|
||||
:param read_count: The number of registers to read from address
|
||||
:param write_address: The address to start writing to
|
||||
:param write_registers: The registers to write to the specified address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = ReadWriteMultipleRegistersRequest(*args, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def mask_write_register(self, *args, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The address of the register to write
|
||||
:param and_mask: The and bitmask to apply to the register address
|
||||
:param or_mask: The or bitmask to apply to the register address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = MaskWriteRegisterRequest(*args, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [ 'ModbusClientMixin' ]
|
||||
@@ -1,779 +0,0 @@
|
||||
import socket
|
||||
import select
|
||||
import serial
|
||||
import time
|
||||
import ssl
|
||||
import sys
|
||||
from functools import partial
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.utilities import hexlify_packets, ModbusTransactionState
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.exceptions import NotImplementedException, ParameterException
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.transaction import FifoTransactionManager
|
||||
from pymodbus.transaction import DictTransactionManager
|
||||
from pymodbus.transaction import ModbusSocketFramer, ModbusBinaryFramer
|
||||
from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer
|
||||
from pymodbus.transaction import ModbusTlsFramer
|
||||
from pymodbus.client.common import ModbusClientMixin
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# The Synchronous Clients
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class BaseModbusClient(ModbusClientMixin):
|
||||
"""
|
||||
Inteface for a modbus synchronous client. Defined here are all the
|
||||
methods for performing the related request methods. Derived classes
|
||||
simply need to implement the transport methods and set the correct
|
||||
framer.
|
||||
"""
|
||||
def __init__(self, framer, **kwargs):
|
||||
""" Initialize a client instance
|
||||
|
||||
:param framer: The modbus framer implementation to use
|
||||
"""
|
||||
self.framer = framer
|
||||
self.transaction = DictTransactionManager(self, **kwargs)
|
||||
self._debug = False
|
||||
self._debugfd = None
|
||||
self.broadcast_enable = kwargs.get('broadcast_enable', Defaults.broadcast_enable)
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Client interface
|
||||
# ----------------------------------------------------------------------- #
|
||||
def connect(self):
|
||||
""" Connect to the modbus remote host
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
raise NotImplementedException("Method not implemented by derived class")
|
||||
|
||||
def close(self):
|
||||
""" Closes the underlying socket connection
|
||||
"""
|
||||
pass
|
||||
|
||||
def is_socket_open(self):
|
||||
"""
|
||||
Check whether the underlying socket/serial is open or not.
|
||||
|
||||
:returns: True if socket/serial is open, False otherwise
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"is_socket_open() not implemented by {}".format(self.__str__())
|
||||
)
|
||||
|
||||
def send(self, request):
|
||||
if self.state != ModbusTransactionState.RETRYING:
|
||||
_logger.debug("New Transaction state 'SENDING'")
|
||||
self.state = ModbusTransactionState.SENDING
|
||||
return self._send(request)
|
||||
|
||||
def _send(self, request):
|
||||
""" Sends data on the underlying socket
|
||||
|
||||
:param request: The encoded request to send
|
||||
:return: The number of bytes written
|
||||
"""
|
||||
raise NotImplementedException("Method not implemented by derived class")
|
||||
|
||||
def recv(self, size):
|
||||
return self._recv(size)
|
||||
|
||||
def _recv(self, size):
|
||||
""" Reads data from the underlying descriptor
|
||||
|
||||
:param size: The number of bytes to read
|
||||
:return: The bytes read
|
||||
"""
|
||||
raise NotImplementedException("Method not implemented by derived class")
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Modbus client methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
def execute(self, request=None):
|
||||
"""
|
||||
:param request: The request to process
|
||||
:returns: The result of the request execution
|
||||
"""
|
||||
if not self.connect():
|
||||
raise ConnectionException("Failed to connect[%s]" % (self.__str__()))
|
||||
return self.transaction.execute(request)
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# The magic methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
def __enter__(self):
|
||||
""" Implement the client with enter block
|
||||
|
||||
:returns: The current instance of the client
|
||||
"""
|
||||
if not self.connect():
|
||||
raise ConnectionException("Failed to connect[%s]" % (self.__str__()))
|
||||
return self
|
||||
|
||||
def __exit__(self, klass, value, traceback):
|
||||
""" Implement the client with exit block """
|
||||
self.close()
|
||||
|
||||
def idle_time(self):
|
||||
"""
|
||||
Bus Idle Time to initiate next transaction
|
||||
:return: time stamp
|
||||
"""
|
||||
if self.last_frame_end is None or self.silent_interval is None:
|
||||
return 0
|
||||
return self.last_frame_end + self.silent_interval
|
||||
|
||||
def debug_enabled(self):
|
||||
"""
|
||||
Returns a boolean indicating if debug is enabled.
|
||||
"""
|
||||
return self._debug
|
||||
|
||||
def set_debug(self, debug):
|
||||
"""
|
||||
Sets the current debug flag.
|
||||
"""
|
||||
self._debug = debug
|
||||
|
||||
def trace(self, writeable):
|
||||
if writeable:
|
||||
self.set_debug(True)
|
||||
self._debugfd = writeable
|
||||
|
||||
def _dump(self, data, direction):
|
||||
fd = self._debugfd if self._debugfd else sys.stdout
|
||||
try:
|
||||
fd.write(hexlify_packets(data))
|
||||
except Exception as e:
|
||||
_logger.debug(hexlify_packets(data))
|
||||
_logger.exception(e)
|
||||
|
||||
def register(self, function):
|
||||
"""
|
||||
Registers a function and sub function class with the decoder
|
||||
:param function: Custom function class to register
|
||||
:return:
|
||||
"""
|
||||
self.framer.decoder.register(function)
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "Null Transport"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TCP Client Transport Implementation
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusTcpClient(BaseModbusClient):
|
||||
""" Implementation of a modbus tcp client
|
||||
"""
|
||||
|
||||
def __init__(self, host='127.0.0.1', port=Defaults.Port,
|
||||
framer=ModbusSocketFramer, **kwargs):
|
||||
""" Initialize a client instance
|
||||
|
||||
:param host: The host to connect to (default 127.0.0.1)
|
||||
:param port: The modbus port to connect to (default 502)
|
||||
:param source_address: The source address tuple to bind to (default ('', 0))
|
||||
:param timeout: The timeout to use for this socket (default Defaults.Timeout)
|
||||
:param framer: The modbus framer to use (default ModbusSocketFramer)
|
||||
|
||||
.. note:: The host argument will accept ipv4 and ipv6 hosts
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.source_address = kwargs.get('source_address', ('', 0))
|
||||
self.socket = None
|
||||
self.timeout = kwargs.get('timeout', Defaults.Timeout)
|
||||
BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **kwargs)
|
||||
|
||||
def connect(self):
|
||||
""" Connect to the modbus tcp server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
self.socket = socket.create_connection(
|
||||
(self.host, self.port),
|
||||
timeout=self.timeout,
|
||||
source_address=self.source_address)
|
||||
_logger.debug("Connection to Modbus server established. "
|
||||
"Socket {}".format(self.socket.getsockname()))
|
||||
except socket.error as msg:
|
||||
_logger.error('Connection to (%s, %s) '
|
||||
'failed: %s' % (self.host, self.port, msg))
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self):
|
||||
""" Closes the underlying socket connection
|
||||
"""
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _check_read_buffer(self, recv_size=None):
|
||||
time_ = time.time()
|
||||
end = time_ + self.timeout
|
||||
data = None
|
||||
ready = select.select([self.socket], [], [], end - time_)
|
||||
if ready[0]:
|
||||
data = self.socket.recv(1024)
|
||||
return data
|
||||
|
||||
def _send(self, request):
|
||||
""" Sends data on the underlying socket
|
||||
|
||||
:param request: The encoded request to send
|
||||
:return: The number of bytes written
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
if self.state == ModbusTransactionState.RETRYING:
|
||||
data = self._check_read_buffer()
|
||||
if data:
|
||||
return data
|
||||
|
||||
if request:
|
||||
return self.socket.send(request)
|
||||
return 0
|
||||
|
||||
def _recv(self, size):
|
||||
""" Reads data from the underlying descriptor
|
||||
|
||||
:param size: The number of bytes to read
|
||||
:return: The bytes read if the peer sent a response, or a zero-length
|
||||
response if no data packets were received from the client at
|
||||
all.
|
||||
:raises: ConnectionException if the socket is not initialized, or the
|
||||
peer either has closed the connection before this method is
|
||||
invoked or closes it before sending any data before timeout.
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
|
||||
# socket.recv(size) waits until it gets some data from the host but
|
||||
# not necessarily the entire response that can be fragmented in
|
||||
# many packets.
|
||||
# To avoid the splitted responses to be recognized as invalid
|
||||
# messages and to be discarded, loops socket.recv until full data
|
||||
# is received or timeout is expired.
|
||||
# If timeout expires returns the read data, also if its length is
|
||||
# less than the expected size.
|
||||
self.socket.setblocking(0)
|
||||
|
||||
timeout = self.timeout
|
||||
|
||||
# If size isn't specified read up to 4096 bytes at a time.
|
||||
if size is None:
|
||||
recv_size = 4096
|
||||
else:
|
||||
recv_size = size
|
||||
|
||||
data = []
|
||||
data_length = 0
|
||||
time_ = time.time()
|
||||
end = time_ + timeout
|
||||
while recv_size > 0:
|
||||
ready = select.select([self.socket], [], [], end - time_)
|
||||
if ready[0]:
|
||||
recv_data = self.socket.recv(recv_size)
|
||||
if recv_data == b'':
|
||||
return self._handle_abrupt_socket_close(
|
||||
size, data, time.time() - time_)
|
||||
data.append(recv_data)
|
||||
data_length += len(recv_data)
|
||||
time_ = time.time()
|
||||
|
||||
# If size isn't specified continue to read until timeout expires.
|
||||
if size:
|
||||
recv_size = size - data_length
|
||||
|
||||
# Timeout is reduced also if some data has been received in order
|
||||
# to avoid infinite loops when there isn't an expected response
|
||||
# size and the slave sends noisy data continuously.
|
||||
if time_ > end:
|
||||
break
|
||||
|
||||
return b"".join(data)
|
||||
|
||||
def _handle_abrupt_socket_close(self, size, data, duration):
|
||||
""" Handle unexpected socket close by remote end
|
||||
|
||||
Intended to be invoked after determining that the remote end
|
||||
has unexpectedly closed the connection, to clean up and handle
|
||||
the situation appropriately.
|
||||
|
||||
:param size: The number of bytes that was attempted to read
|
||||
:param data: The actual data returned
|
||||
:param duration: Duration from the read was first attempted
|
||||
until it was determined that the remote closed the
|
||||
socket
|
||||
:return: The more than zero bytes read from the remote end
|
||||
:raises: ConnectionException If the remote end didn't send any
|
||||
data at all before closing the connection.
|
||||
"""
|
||||
self.close()
|
||||
readsize = ("read of %s bytes" % size if size
|
||||
else "unbounded read")
|
||||
msg = ("%s: Connection unexpectedly closed "
|
||||
"%.6f seconds into %s" % (self, duration, readsize))
|
||||
if data:
|
||||
result = b"".join(data)
|
||||
msg += " after returning %s bytes" % len(result)
|
||||
_logger.warning(msg)
|
||||
return result
|
||||
msg += " without response from unit before it closed connection"
|
||||
raise ConnectionException(msg)
|
||||
|
||||
def is_socket_open(self):
|
||||
return True if self.socket is not None else False
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "ModbusTcpClient(%s:%s)" % (self.host, self.port)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<{} at {} socket={self.socket}, ipaddr={self.host}, "
|
||||
"port={self.port}, timeout={self.timeout}>"
|
||||
).format(self.__class__.__name__, hex(id(self)), self=self)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TLS Client Transport Implementation
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusTlsClient(ModbusTcpClient):
|
||||
""" Implementation of a modbus tls client
|
||||
"""
|
||||
|
||||
def __init__(self, host='localhost', port=Defaults.TLSPort, sslctx=None,
|
||||
framer=ModbusTlsFramer, **kwargs):
|
||||
""" Initialize a client instance
|
||||
|
||||
:param host: The host to connect to (default localhost)
|
||||
:param port: The modbus port to connect to (default 802)
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto create)
|
||||
:param source_address: The source address tuple to bind to (default ('', 0))
|
||||
:param timeout: The timeout to use for this socket (default Defaults.Timeout)
|
||||
:param framer: The modbus framer to use (default ModbusSocketFramer)
|
||||
|
||||
.. note:: The host argument will accept ipv4 and ipv6 hosts
|
||||
"""
|
||||
self.sslctx = sslctx
|
||||
if self.sslctx is None:
|
||||
self.sslctx = ssl.create_default_context()
|
||||
# According to MODBUS/TCP Security Protocol Specification, it is
|
||||
# TLSv2 at least
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1_1
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv3
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv2
|
||||
ModbusTcpClient.__init__(self, host, port, framer, **kwargs)
|
||||
|
||||
def connect(self):
|
||||
""" Connect to the modbus tls server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
if self.socket: return True
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(self.source_address)
|
||||
self.socket = self.sslctx.wrap_socket(sock, server_side=False,
|
||||
server_hostname=self.host)
|
||||
self.socket.settimeout(self.timeout)
|
||||
self.socket.connect((self.host, self.port))
|
||||
except socket.error as msg:
|
||||
_logger.error('Connection to (%s, %s) '
|
||||
'failed: %s' % (self.host, self.port, msg))
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def _recv(self, size):
|
||||
""" Reads data from the underlying descriptor
|
||||
|
||||
:param size: The number of bytes to read
|
||||
:return: The bytes read
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
|
||||
# socket.recv(size) waits until it gets some data from the host but
|
||||
# not necessarily the entire response that can be fragmented in
|
||||
# many packets.
|
||||
# To avoid the splitted responses to be recognized as invalid
|
||||
# messages and to be discarded, loops socket.recv until full data
|
||||
# is received or timeout is expired.
|
||||
# If timeout expires returns the read data, also if its length is
|
||||
# less than the expected size.
|
||||
timeout = self.timeout
|
||||
|
||||
# If size isn't specified read 1 byte at a time.
|
||||
if size is None:
|
||||
recv_size = 1
|
||||
else:
|
||||
recv_size = size
|
||||
|
||||
data = b''
|
||||
time_ = time.time()
|
||||
end = time_ + timeout
|
||||
while recv_size > 0:
|
||||
data += self.socket.recv(recv_size)
|
||||
time_ = time.time()
|
||||
|
||||
# If size isn't specified continue to read until timeout expires.
|
||||
if size:
|
||||
recv_size = size - len(data)
|
||||
|
||||
# Timeout is reduced also if some data has been received in order
|
||||
# to avoid infinite loops when there isn't an expected response
|
||||
# size and the slave sends noisy data continuously.
|
||||
if time_ > end:
|
||||
break
|
||||
|
||||
return data
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "ModbusTlsClient(%s:%s)" % (self.host, self.port)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<{} at {} socket={self.socket}, ipaddr={self.host}, "
|
||||
"port={self.port}, sslctx={self.sslctx}, timeout={self.timeout}>"
|
||||
).format(self.__class__.__name__, hex(id(self)), self=self)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus UDP Client Transport Implementation
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusUdpClient(BaseModbusClient):
|
||||
""" Implementation of a modbus udp client
|
||||
"""
|
||||
|
||||
def __init__(self, host='127.0.0.1', port=Defaults.Port,
|
||||
framer=ModbusSocketFramer, **kwargs):
|
||||
""" Initialize a client instance
|
||||
|
||||
:param host: The host to connect to (default 127.0.0.1)
|
||||
:param port: The modbus port to connect to (default 502)
|
||||
:param framer: The modbus framer to use (default ModbusSocketFramer)
|
||||
:param timeout: The timeout to use for this socket (default None)
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.socket = None
|
||||
self.timeout = kwargs.get('timeout', None)
|
||||
BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _get_address_family(cls, address):
|
||||
""" A helper method to get the correct address family
|
||||
for a given address.
|
||||
|
||||
:param address: The address to get the af for
|
||||
:returns: AF_INET for ipv4 and AF_INET6 for ipv6
|
||||
"""
|
||||
try:
|
||||
_ = socket.inet_pton(socket.AF_INET6, address)
|
||||
except socket.error: # not a valid ipv6 address
|
||||
return socket.AF_INET
|
||||
return socket.AF_INET6
|
||||
|
||||
def connect(self):
|
||||
""" Connect to the modbus tcp server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
family = ModbusUdpClient._get_address_family(self.host)
|
||||
self.socket = socket.socket(family, socket.SOCK_DGRAM)
|
||||
self.socket.settimeout(self.timeout)
|
||||
except socket.error as ex:
|
||||
_logger.error('Unable to create udp socket %s' % ex)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self):
|
||||
""" Closes the underlying socket connection
|
||||
"""
|
||||
self.socket = None
|
||||
|
||||
def _send(self, request):
|
||||
""" Sends data on the underlying socket
|
||||
|
||||
:param request: The encoded request to send
|
||||
:return: The number of bytes written
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
if request:
|
||||
return self.socket.sendto(request, (self.host, self.port))
|
||||
return 0
|
||||
|
||||
def _recv(self, size):
|
||||
""" Reads data from the underlying descriptor
|
||||
|
||||
:param size: The number of bytes to read
|
||||
:return: The bytes read
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
return self.socket.recvfrom(size)[0]
|
||||
|
||||
def is_socket_open(self):
|
||||
if self.socket:
|
||||
return True
|
||||
return self.connect()
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "ModbusUdpClient(%s:%s)" % (self.host, self.port)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<{} at {} socket={self.socket}, ipaddr={self.host}, "
|
||||
"port={self.port}, timeout={self.timeout}>"
|
||||
).format(self.__class__.__name__, hex(id(self)), self=self)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus Serial Client Transport Implementation
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusSerialClient(BaseModbusClient):
|
||||
""" Implementation of a modbus serial client
|
||||
"""
|
||||
state = ModbusTransactionState.IDLE
|
||||
inter_char_timeout = 0
|
||||
silent_interval = 0
|
||||
|
||||
def __init__(self, method='ascii', **kwargs):
|
||||
""" Initialize a serial client instance
|
||||
|
||||
The methods to connect are::
|
||||
|
||||
- ascii
|
||||
- rtu
|
||||
- binary
|
||||
|
||||
:param method: The method to use for connection
|
||||
:param port: The serial port to attach to
|
||||
:param stopbits: The number of stop bits to use
|
||||
:param bytesize: The bytesize of the serial messages
|
||||
:param parity: Which kind of parity to use
|
||||
:param baudrate: The baud rate to use for the serial device
|
||||
:param timeout: The timeout between serial requests (default 3s)
|
||||
:param strict: Use Inter char timeout for baudrates <= 19200 (adhere
|
||||
to modbus standards)
|
||||
:param handle_local_echo: Handle local echo of the USB-to-RS485 adaptor
|
||||
"""
|
||||
self.method = method
|
||||
self.socket = None
|
||||
BaseModbusClient.__init__(self, self.__implementation(method, self),
|
||||
**kwargs)
|
||||
|
||||
self.port = kwargs.get('port', 0)
|
||||
self.stopbits = kwargs.get('stopbits', Defaults.Stopbits)
|
||||
self.bytesize = kwargs.get('bytesize', Defaults.Bytesize)
|
||||
self.parity = kwargs.get('parity', Defaults.Parity)
|
||||
self.baudrate = kwargs.get('baudrate', Defaults.Baudrate)
|
||||
self.timeout = kwargs.get('timeout', Defaults.Timeout)
|
||||
self._strict = kwargs.get("strict", False)
|
||||
self.last_frame_end = None
|
||||
self.handle_local_echo = kwargs.get("handle_local_echo", False)
|
||||
if self.method == "rtu":
|
||||
if self.baudrate > 19200:
|
||||
self.silent_interval = 1.75 / 1000 # ms
|
||||
else:
|
||||
self._t0 = float((1 + 8 + 2)) / self.baudrate
|
||||
self.inter_char_timeout = 1.5 * self._t0
|
||||
self.silent_interval = 3.5 * self._t0
|
||||
self.silent_interval = round(self.silent_interval, 6)
|
||||
|
||||
@staticmethod
|
||||
def __implementation(method, client):
|
||||
""" Returns the requested framer
|
||||
|
||||
:method: The serial framer to instantiate
|
||||
:returns: The requested serial framer
|
||||
"""
|
||||
method = method.lower()
|
||||
if method == 'ascii':
|
||||
return ModbusAsciiFramer(ClientDecoder(), client)
|
||||
elif method == 'rtu':
|
||||
return ModbusRtuFramer(ClientDecoder(), client)
|
||||
elif method == 'binary':
|
||||
return ModbusBinaryFramer(ClientDecoder(), client)
|
||||
elif method == 'socket':
|
||||
return ModbusSocketFramer(ClientDecoder(), client)
|
||||
raise ParameterException("Invalid framer method requested")
|
||||
|
||||
def connect(self):
|
||||
""" Connect to the modbus serial server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
import serial
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
self.socket = serial.serial_for_url(self.port,
|
||||
timeout=self.timeout,
|
||||
bytesize=self.bytesize,
|
||||
stopbits=self.stopbits,
|
||||
baudrate=self.baudrate,
|
||||
parity=self.parity)
|
||||
if self.method == "rtu":
|
||||
if self._strict:
|
||||
self.socket.interCharTimeout = self.inter_char_timeout
|
||||
self.last_frame_end = None
|
||||
except serial.SerialException as msg:
|
||||
_logger.error(msg)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self):
|
||||
""" Closes the underlying socket connection
|
||||
"""
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _in_waiting(self):
|
||||
in_waiting = ("in_waiting" if hasattr(
|
||||
self.socket, "in_waiting") else "inWaiting")
|
||||
|
||||
if in_waiting == "in_waiting":
|
||||
waitingbytes = getattr(self.socket, in_waiting)
|
||||
else:
|
||||
waitingbytes = getattr(self.socket, in_waiting)()
|
||||
return waitingbytes
|
||||
|
||||
def _send(self, request):
|
||||
""" Sends data on the underlying socket
|
||||
|
||||
If receive buffer still holds some data then flush it.
|
||||
|
||||
Sleep if last send finished less than 3.5 character
|
||||
times ago.
|
||||
|
||||
:param request: The encoded request to send
|
||||
:return: The number of bytes written
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
if request:
|
||||
try:
|
||||
waitingbytes = self._in_waiting()
|
||||
if waitingbytes:
|
||||
result = self.socket.read(waitingbytes)
|
||||
if self.state == ModbusTransactionState.RETRYING:
|
||||
_logger.debug("Sending available data in recv "
|
||||
"buffer {}".format(
|
||||
hexlify_packets(result)))
|
||||
return result
|
||||
if _logger.isEnabledFor(logging.WARNING):
|
||||
_logger.warning("Cleanup recv buffer before "
|
||||
"send: " + hexlify_packets(result))
|
||||
except NotImplementedError:
|
||||
pass
|
||||
if self.state != ModbusTransactionState.SENDING:
|
||||
_logger.debug("New Transaction state 'SENDING'")
|
||||
self.state = ModbusTransactionState.SENDING
|
||||
size = self.socket.write(request)
|
||||
return size
|
||||
return 0
|
||||
|
||||
def _wait_for_data(self):
|
||||
size = 0
|
||||
more_data = False
|
||||
if self.timeout is not None and self.timeout != 0:
|
||||
condition = partial(lambda start, timeout:
|
||||
(time.time() - start) <= timeout,
|
||||
timeout=self.timeout)
|
||||
else:
|
||||
condition = partial(lambda dummy1, dummy2: True, dummy2=None)
|
||||
start = time.time()
|
||||
while condition(start):
|
||||
avaialble = self._in_waiting()
|
||||
if (more_data and not avaialble) or (more_data and avaialble == size):
|
||||
break
|
||||
if avaialble and avaialble != size:
|
||||
more_data = True
|
||||
size = avaialble
|
||||
time.sleep(0.01)
|
||||
return size
|
||||
|
||||
def _recv(self, size):
|
||||
""" Reads data from the underlying descriptor
|
||||
|
||||
:param size: The number of bytes to read
|
||||
:return: The bytes read
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
if size is None:
|
||||
size = self._wait_for_data()
|
||||
result = self.socket.read(size)
|
||||
return result
|
||||
|
||||
def is_socket_open(self):
|
||||
if self.socket:
|
||||
if hasattr(self.socket, "is_open"):
|
||||
return self.socket.is_open
|
||||
else:
|
||||
return self.socket.isOpen()
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "ModbusSerialClient(%s baud[%s])" % (self.method, self.baudrate)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<{} at {} socket={self.socket}, method={self.method}, "
|
||||
"timeout={self.timeout}>"
|
||||
).format(self.__class__.__name__, hex(id(self)), self=self)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ModbusTcpClient", "ModbusTlsClient", "ModbusUdpClient", "ModbusSerialClient"
|
||||
]
|
||||
@@ -1,167 +0,0 @@
|
||||
import socket
|
||||
import logging
|
||||
import time
|
||||
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.client.sync import ModbusTcpClient
|
||||
from pymodbus.transaction import ModbusSocketFramer
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
LOG_MSGS = {
|
||||
'conn_msg': 'Connecting to modbus device %s',
|
||||
'connfail_msg': 'Connection to (%s, %s) failed: %s',
|
||||
'discon_msg': 'Disconnecting from modbus device %s',
|
||||
'timelimit_read_msg':
|
||||
'Modbus device read took %.4f seconds, '
|
||||
'returned %s bytes in timelimit read',
|
||||
'timeout_msg':
|
||||
'Modbus device timeout after %.4f seconds, '
|
||||
'returned %s bytes %s',
|
||||
'delay_msg':
|
||||
'Modbus device read took %.4f seconds, '
|
||||
'returned %s bytes of %s expected',
|
||||
'read_msg':
|
||||
'Modbus device read took %.4f seconds, '
|
||||
'returned %s bytes of %s expected',
|
||||
'unexpected_dc_msg': '%s %s'}
|
||||
|
||||
|
||||
class ModbusTcpDiagClient(ModbusTcpClient):
|
||||
"""
|
||||
Variant of pymodbus.client.sync.ModbusTcpClient with additional
|
||||
logging to diagnose network issues.
|
||||
|
||||
The following events are logged:
|
||||
|
||||
+---------+-----------------------------------------------------------------+
|
||||
| Level | Events |
|
||||
+=========+=================================================================+
|
||||
| ERROR | Failure to connect to modbus unit; unexpected disconnect by |
|
||||
| | modbus unit |
|
||||
+---------+-----------------------------------------------------------------+
|
||||
| WARNING | Timeout on normal read; read took longer than warn_delay_limit |
|
||||
+---------+-----------------------------------------------------------------+
|
||||
| INFO | Connection attempt to modbus unit; disconnection from modbus |
|
||||
| | unit; each time limited read |
|
||||
+---------+-----------------------------------------------------------------+
|
||||
| DEBUG | Normal read with timing information |
|
||||
+---------+-----------------------------------------------------------------+
|
||||
|
||||
Reads are differentiated between "normal", which reads a specified number of
|
||||
bytes, and "time limited", which reads all data for a duration equal to the
|
||||
timeout period configured for this instance.
|
||||
"""
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
def __init__(self, host='127.0.0.1', port=Defaults.Port,
|
||||
framer=ModbusSocketFramer, **kwargs):
|
||||
""" Initialize a client instance
|
||||
|
||||
The keys of LOG_MSGS can be used in kwargs to customize the messages.
|
||||
|
||||
:param host: The host to connect to (default 127.0.0.1)
|
||||
:param port: The modbus port to connect to (default 502)
|
||||
:param source_address: The source address tuple to bind to (default ('', 0))
|
||||
:param timeout: The timeout to use for this socket (default Defaults.Timeout)
|
||||
:param warn_delay_limit: Log reads that take longer than this as warning.
|
||||
Default True sets it to half of "timeout". None never logs these as
|
||||
warning, 0 logs everything as warning.
|
||||
:param framer: The modbus framer to use (default ModbusSocketFramer)
|
||||
|
||||
.. note:: The host argument will accept ipv4 and ipv6 hosts
|
||||
"""
|
||||
self.warn_delay_limit = kwargs.get('warn_delay_limit', True)
|
||||
super(ModbusTcpDiagClient, self).__init__(host, port, framer, **kwargs)
|
||||
if self.warn_delay_limit is True:
|
||||
self.warn_delay_limit = self.timeout / 2
|
||||
|
||||
# Set logging messages, defaulting to LOG_MSGS
|
||||
for (k, v) in LOG_MSGS.items():
|
||||
self.__dict__[k] = kwargs.get(k, v)
|
||||
|
||||
def connect(self):
|
||||
""" Connect to the modbus tcp server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
_logger.info(self.conn_msg, self)
|
||||
self.socket = socket.create_connection(
|
||||
(self.host, self.port),
|
||||
timeout=self.timeout,
|
||||
source_address=self.source_address)
|
||||
except socket.error as msg:
|
||||
_logger.error(self.connfail_msg, self.host, self.port, msg)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self):
|
||||
""" Closes the underlying socket connection
|
||||
"""
|
||||
if self.socket:
|
||||
_logger.info(self.discon_msg, self)
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _recv(self, size):
|
||||
try:
|
||||
start = time.time()
|
||||
|
||||
result = super(ModbusTcpDiagClient, self)._recv(size)
|
||||
|
||||
delay = time.time() - start
|
||||
if self.warn_delay_limit is not None and delay >= self.warn_delay_limit:
|
||||
self._log_delayed_response(len(result), size, delay)
|
||||
elif not size:
|
||||
_logger.debug(self.timelimit_read_msg, delay, len(result))
|
||||
else:
|
||||
_logger.debug(self.read_msg, delay, len(result), size)
|
||||
|
||||
return result
|
||||
except ConnectionException as ex:
|
||||
# Only log actual network errors, "if not self.socket" then it's a internal code issue
|
||||
if 'Connection unexpectedly closed' in ex.string:
|
||||
_logger.error(self.unexpected_dc_msg, self, ex)
|
||||
raise ex
|
||||
|
||||
def _log_delayed_response(self, result_len, size, delay):
|
||||
if not size and result_len > 0:
|
||||
_logger.info(self.timelimit_read_msg, delay, result_len)
|
||||
elif (result_len == 0 or (size and result_len < size)) and delay >= self.timeout:
|
||||
read_type = ("of %i expected" % size) if size else "in timelimit read"
|
||||
_logger.warning(self.timeout_msg, delay, result_len, read_type)
|
||||
else:
|
||||
_logger.warning(self.delay_msg, delay, result_len, size)
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "ModbusTcpDiagClient(%s:%s)" % (self.host, self.port)
|
||||
|
||||
|
||||
def get_client():
|
||||
""" Returns an appropriate client based on logging level
|
||||
|
||||
This will be ModbusTcpDiagClient by default, or the parent class
|
||||
if the log level is such that the diagnostic client will not log
|
||||
anything.
|
||||
|
||||
:returns: ModbusTcpClient or a child class thereof
|
||||
"""
|
||||
return ModbusTcpDiagClient if _logger.isEnabledFor(logging.ERROR) else ModbusTcpClient
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
__all__ = [
|
||||
"ModbusTcpDiagClient", "get_client"
|
||||
]
|
||||
@@ -1,98 +0,0 @@
|
||||
"""
|
||||
Python 2.x/3.x Compatibility Layer
|
||||
-------------------------------------------------
|
||||
|
||||
This is mostly based on the jinja2 compat code:
|
||||
|
||||
Some py2/py3 compatibility support based on a stripped down
|
||||
version of six so we don't have to depend on a specific version
|
||||
of it.
|
||||
|
||||
:copyright: Copyright 2013 by the Jinja team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
import sys
|
||||
import six
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# python version checks
|
||||
# --------------------------------------------------------------------------- #
|
||||
PYTHON_VERSION = sys.version_info
|
||||
IS_PYTHON2 = six.PY2
|
||||
IS_PYTHON3 = six.PY3
|
||||
IS_PYPY = hasattr(sys, 'pypy_translation_info')
|
||||
IS_JYTHON = sys.platform.startswith('java')
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# python > 3.3 compatibility layer
|
||||
# --------------------------------------------------------------------------- #
|
||||
# ----------------------------------------------------------------------- #
|
||||
# portable builtins
|
||||
# ----------------------------------------------------------------------- #
|
||||
int2byte = six.int2byte
|
||||
unichr = six.unichr
|
||||
range_type = six.moves.range
|
||||
text_type = six.string_types
|
||||
string_types = six.string_types
|
||||
iterkeys = six.iterkeys
|
||||
itervalues = six.itervalues
|
||||
iteritems = six.iteritems
|
||||
get_next = six.next
|
||||
unicode_string = six.u
|
||||
|
||||
NativeStringIO = six.StringIO
|
||||
ifilter = six.moves.filter
|
||||
imap = six.moves.map
|
||||
izip = six.moves.zip
|
||||
intern = six.moves.intern
|
||||
|
||||
if not IS_PYTHON2:
|
||||
# ----------------------------------------------------------------------- #
|
||||
# module renames
|
||||
# ----------------------------------------------------------------------- #
|
||||
import socketserver
|
||||
# #609 monkey patch for socket server memory leaks
|
||||
# Refer https://bugs.python.org/issue37193
|
||||
socketserver.ThreadingMixIn.daemon_threads = True
|
||||
# ----------------------------------------------------------------------- #
|
||||
# decorators
|
||||
# ----------------------------------------------------------------------- #
|
||||
implements_to_string = lambda x: x
|
||||
|
||||
byte2int = lambda b: b
|
||||
if PYTHON_VERSION >= (3, 4):
|
||||
def is_installed(module):
|
||||
import importlib.util
|
||||
found = importlib.util.find_spec(module)
|
||||
return found
|
||||
else:
|
||||
def is_installed(module):
|
||||
import importlib
|
||||
found = importlib.find_loader(module)
|
||||
return found
|
||||
# --------------------------------------------------------------------------- #
|
||||
# python > 2.5 compatability layer
|
||||
# --------------------------------------------------------------------------- #
|
||||
else:
|
||||
byte2int = six.byte2int
|
||||
# ----------------------------------------------------------------------- #
|
||||
# module renames
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
import SocketServer as socketserver
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# decorators
|
||||
# ----------------------------------------------------------------------- #
|
||||
def implements_to_string(klass):
|
||||
klass.__unicode__ = klass.__str__
|
||||
klass.__str__ = lambda x: x.__unicode__().encode('utf-8')
|
||||
return klass
|
||||
|
||||
def is_installed(module):
|
||||
import imp
|
||||
try:
|
||||
imp.find_module(module)
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
@@ -1,268 +0,0 @@
|
||||
'''
|
||||
Constants For Modbus Server/Client
|
||||
----------------------------------
|
||||
|
||||
This is the single location for storing default
|
||||
values for the servers and clients.
|
||||
'''
|
||||
from pymodbus.interfaces import Singleton
|
||||
|
||||
|
||||
class Defaults(Singleton):
|
||||
''' A collection of modbus default values
|
||||
|
||||
.. attribute:: Port
|
||||
|
||||
The default modbus tcp server port (502)
|
||||
|
||||
.. attribute:: TLSPort
|
||||
|
||||
The default modbus tcp over tls server port (802)
|
||||
|
||||
|
||||
.. attribute:: Backoff
|
||||
|
||||
The default exponential backoff delay (0.3 seconds)
|
||||
|
||||
.. attribute:: Retries
|
||||
|
||||
The default number of times a client should retry the given
|
||||
request before failing (3)
|
||||
|
||||
.. attribute:: RetryOnEmpty
|
||||
|
||||
A flag indicating if a transaction should be retried in the
|
||||
case that an empty response is received. This is useful for
|
||||
slow clients that may need more time to process a request.
|
||||
|
||||
.. attribute:: RetryOnInvalid
|
||||
|
||||
A flag indicating if a transaction should be retried in the
|
||||
case that an invalid response is received.
|
||||
|
||||
.. attribute:: Timeout
|
||||
|
||||
The default amount of time a client should wait for a request
|
||||
to be processed (3 seconds)
|
||||
|
||||
.. attribute:: Reconnects
|
||||
|
||||
The default number of times a client should attempt to reconnect
|
||||
before deciding the server is down (0)
|
||||
|
||||
.. attribute:: TransactionId
|
||||
|
||||
The starting transaction identifier number (0)
|
||||
|
||||
.. attribute:: ProtocolId
|
||||
|
||||
The modbus protocol id. Currently this is set to 0 in all
|
||||
but proprietary implementations.
|
||||
|
||||
.. attribute:: UnitId
|
||||
|
||||
The modbus slave addrss. Currently this is set to 0x00 which
|
||||
means this request should be broadcast to all the slave devices
|
||||
(really means that all the devices should respons).
|
||||
|
||||
.. attribute:: Baudrate
|
||||
|
||||
The speed at which the data is transmitted over the serial line.
|
||||
This defaults to 19200.
|
||||
|
||||
.. attribute:: Parity
|
||||
|
||||
The type of checksum to use to verify data integrity. This can be
|
||||
on of the following::
|
||||
|
||||
- (E)ven - 1 0 1 0 | P(0)
|
||||
- (O)dd - 1 0 1 0 | P(1)
|
||||
- (N)one - 1 0 1 0 | no parity
|
||||
|
||||
This defaults to (N)one.
|
||||
|
||||
.. attribute:: Bytesize
|
||||
|
||||
The number of bits in a byte of serial data. This can be one of
|
||||
5, 6, 7, or 8. This defaults to 8.
|
||||
|
||||
.. attribute:: Stopbits
|
||||
|
||||
The number of bits sent after each character in a message to
|
||||
indicate the end of the byte. This defaults to 1.
|
||||
|
||||
.. attribute:: ZeroMode
|
||||
|
||||
Indicates if the slave datastore should use indexing at 0 or 1.
|
||||
More about this can be read in section 4.4 of the modbus specification.
|
||||
|
||||
.. attribute:: IgnoreMissingSlaves
|
||||
|
||||
In case a request is made to a missing slave, this defines if an error
|
||||
should be returned or simply ignored. This is useful for the case of a
|
||||
serial server emulater where a request to a non-existant slave on a bus
|
||||
will never respond. The client in this case will simply timeout.
|
||||
|
||||
.. attribute:: broadcast_enable
|
||||
|
||||
When False unit_id 0 will be treated as any other unit_id. When True and
|
||||
the unit_id is 0 the server will execute all requests on all server
|
||||
contexts and not respond and the client will skip trying to receive a
|
||||
response. Default value False does not conform to Modbus spec but maintains
|
||||
legacy behavior for existing pymodbus users.
|
||||
|
||||
'''
|
||||
Port = 502
|
||||
TLSPort = 802
|
||||
Backoff = 0.3
|
||||
Retries = 3
|
||||
RetryOnEmpty = False
|
||||
RetryOnInvalid = False
|
||||
Timeout = 3
|
||||
Reconnects = 0
|
||||
TransactionId = 0
|
||||
ProtocolId = 0
|
||||
UnitId = 0x00
|
||||
Baudrate = 19200
|
||||
Parity = 'N'
|
||||
Bytesize = 8
|
||||
Stopbits = 1
|
||||
ZeroMode = False
|
||||
IgnoreMissingSlaves = False
|
||||
ReadSize = 1024
|
||||
broadcast_enable = False
|
||||
|
||||
class ModbusStatus(Singleton):
|
||||
'''
|
||||
These represent various status codes in the modbus
|
||||
protocol.
|
||||
|
||||
.. attribute:: Waiting
|
||||
|
||||
This indicates that a modbus device is currently
|
||||
waiting for a given request to finish some running task.
|
||||
|
||||
.. attribute:: Ready
|
||||
|
||||
This indicates that a modbus device is currently
|
||||
free to perform the next request task.
|
||||
|
||||
.. attribute:: On
|
||||
|
||||
This indicates that the given modbus entity is on
|
||||
|
||||
.. attribute:: Off
|
||||
|
||||
This indicates that the given modbus entity is off
|
||||
|
||||
.. attribute:: SlaveOn
|
||||
|
||||
This indicates that the given modbus slave is running
|
||||
|
||||
.. attribute:: SlaveOff
|
||||
|
||||
This indicates that the given modbus slave is not running
|
||||
'''
|
||||
Waiting = 0xffff
|
||||
Ready = 0x0000
|
||||
On = 0xff00
|
||||
Off = 0x0000
|
||||
SlaveOn = 0xff
|
||||
SlaveOff = 0x00
|
||||
|
||||
|
||||
class Endian(Singleton):
|
||||
''' An enumeration representing the various byte endianess.
|
||||
|
||||
.. attribute:: Auto
|
||||
|
||||
This indicates that the byte order is chosen by the
|
||||
current native environment.
|
||||
|
||||
.. attribute:: Big
|
||||
|
||||
This indicates that the bytes are in little endian format
|
||||
|
||||
.. attribute:: Little
|
||||
|
||||
This indicates that the bytes are in big endian format
|
||||
|
||||
.. note:: I am simply borrowing the format strings from the
|
||||
python struct module for my convenience.
|
||||
'''
|
||||
Auto = '@'
|
||||
Big = '>'
|
||||
Little = '<'
|
||||
|
||||
|
||||
class ModbusPlusOperation(Singleton):
|
||||
''' Represents the type of modbus plus request
|
||||
|
||||
.. attribute:: GetStatistics
|
||||
|
||||
Operation requesting that the current modbus plus statistics
|
||||
be returned in the response.
|
||||
|
||||
.. attribute:: ClearStatistics
|
||||
|
||||
Operation requesting that the current modbus plus statistics
|
||||
be cleared and not returned in the response.
|
||||
'''
|
||||
GetStatistics = 0x0003
|
||||
ClearStatistics = 0x0004
|
||||
|
||||
|
||||
class DeviceInformation(Singleton):
|
||||
''' Represents what type of device information to read
|
||||
|
||||
.. attribute:: Basic
|
||||
|
||||
This is the basic (required) device information to be returned.
|
||||
This includes VendorName, ProductCode, and MajorMinorRevision
|
||||
code.
|
||||
|
||||
.. attribute:: Regular
|
||||
|
||||
In addition to basic data objects, the device provides additional
|
||||
and optional identification and description data objects. All of
|
||||
the objects of this category are defined in the standard but their
|
||||
implementation is optional.
|
||||
|
||||
.. attribute:: Extended
|
||||
|
||||
In addition to regular data objects, the device provides additional
|
||||
and optional identification and description private data about the
|
||||
physical device itself. All of these data are device dependent.
|
||||
|
||||
.. attribute:: Specific
|
||||
|
||||
Request to return a single data object.
|
||||
'''
|
||||
Basic = 0x01
|
||||
Regular = 0x02
|
||||
Extended = 0x03
|
||||
Specific = 0x04
|
||||
|
||||
|
||||
class MoreData(Singleton):
|
||||
''' Represents the more follows condition
|
||||
|
||||
.. attribute:: Nothing
|
||||
|
||||
This indiates that no more objects are going to be returned.
|
||||
|
||||
.. attribute:: KeepReading
|
||||
|
||||
This indicates that there are more objects to be returned.
|
||||
'''
|
||||
Nothing = 0x00
|
||||
KeepReading = 0xFF
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported Identifiers
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"Defaults", "ModbusStatus", "Endian",
|
||||
"ModbusPlusOperation",
|
||||
"DeviceInformation", "MoreData",
|
||||
]
|
||||
@@ -1,12 +0,0 @@
|
||||
from pymodbus.datastore.store import ModbusSequentialDataBlock
|
||||
from pymodbus.datastore.store import ModbusSparseDataBlock
|
||||
from pymodbus.datastore.context import ModbusSlaveContext
|
||||
from pymodbus.datastore.context import ModbusServerContext
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ModbusSequentialDataBlock", "ModbusSparseDataBlock",
|
||||
"ModbusSlaveContext", "ModbusServerContext",
|
||||
]
|
||||
@@ -1,184 +0,0 @@
|
||||
from pymodbus.exceptions import ParameterException, NoSuchSlaveException
|
||||
from pymodbus.interfaces import IModbusSlaveContext
|
||||
from pymodbus.datastore.store import ModbusSequentialDataBlock
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.compat import iteritems, itervalues
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Slave Contexts
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusSlaveContext(IModbusSlaveContext):
|
||||
'''
|
||||
This creates a modbus data model with each data access
|
||||
stored in its own personal block
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
''' Initializes the datastores, defaults to fully populated
|
||||
sequential data blocks if none are passed in.
|
||||
|
||||
:param kwargs: Each element is a ModbusDataBlock
|
||||
|
||||
'di' - Discrete Inputs initializer
|
||||
'co' - Coils initializer
|
||||
'hr' - Holding Register initializer
|
||||
'ir' - Input Registers iniatializer
|
||||
'''
|
||||
self.store = dict()
|
||||
self.store['d'] = kwargs.get('di', ModbusSequentialDataBlock.create())
|
||||
self.store['c'] = kwargs.get('co', ModbusSequentialDataBlock.create())
|
||||
self.store['i'] = kwargs.get('ir', ModbusSequentialDataBlock.create())
|
||||
self.store['h'] = kwargs.get('hr', ModbusSequentialDataBlock.create())
|
||||
self.zero_mode = kwargs.get('zero_mode', Defaults.ZeroMode)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the context
|
||||
|
||||
:returns: A string representation of the context
|
||||
'''
|
||||
return "Modbus Slave Context"
|
||||
|
||||
def reset(self):
|
||||
''' Resets all the datastores to their default values '''
|
||||
for datastore in itervalues(self.store):
|
||||
datastore.reset()
|
||||
|
||||
def validate(self, fx, address, count=1):
|
||||
''' Validates the request to make sure it is in range
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
if not self.zero_mode:
|
||||
address = address + 1
|
||||
_logger.debug("validate: fc-[%d] address-%d: count-%d" % (fx, address,
|
||||
count))
|
||||
return self.store[self.decode(fx)].validate(address, count)
|
||||
|
||||
def getValues(self, fx, address, count=1):
|
||||
''' Get `count` values from datastore
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
if not self.zero_mode:
|
||||
address = address + 1
|
||||
_logger.debug("getValues fc-[%d] address-%d: count-%d" % (fx, address,
|
||||
count))
|
||||
return self.store[self.decode(fx)].getValues(address, count)
|
||||
|
||||
def setValues(self, fx, address, values):
|
||||
''' Sets the datastore with the supplied values
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
'''
|
||||
if not self.zero_mode:
|
||||
address = address + 1
|
||||
_logger.debug("setValues[%d] %d:%d" % (fx, address, len(values)))
|
||||
self.store[self.decode(fx)].setValues(address, values)
|
||||
|
||||
def register(self, fc, fx, datablock=None):
|
||||
"""
|
||||
Registers a datablock with the slave context
|
||||
:param fc: function code (int)
|
||||
:param fx: string representation of function code (e.g 'cf' )
|
||||
:param datablock: datablock to associate with this function code
|
||||
:return:
|
||||
"""
|
||||
self.store[fx] = datablock or ModbusSequentialDataBlock.create()
|
||||
self._IModbusSlaveContext__fx_mapper[fc] = fx
|
||||
|
||||
|
||||
class ModbusServerContext(object):
|
||||
''' This represents a master collection of slave contexts.
|
||||
If single is set to true, it will be treated as a single
|
||||
context so every unit-id returns the same context. If single
|
||||
is set to false, it will be interpreted as a collection of
|
||||
slave contexts.
|
||||
'''
|
||||
|
||||
def __init__(self, slaves=None, single=True):
|
||||
''' Initializes a new instance of a modbus server context.
|
||||
|
||||
:param slaves: A dictionary of client contexts
|
||||
:param single: Set to true to treat this as a single context
|
||||
'''
|
||||
self.single = single
|
||||
self._slaves = slaves or {}
|
||||
if self.single:
|
||||
self._slaves = {Defaults.UnitId: self._slaves}
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the current collection of slave
|
||||
contexts.
|
||||
|
||||
:returns: An iterator over the slave contexts
|
||||
'''
|
||||
return iteritems(self._slaves)
|
||||
|
||||
def __contains__(self, slave):
|
||||
''' Check if the given slave is in this list
|
||||
|
||||
:param slave: slave The slave to check for existence
|
||||
:returns: True if the slave exists, False otherwise
|
||||
'''
|
||||
if self.single and self._slaves:
|
||||
return True
|
||||
else:
|
||||
return slave in self._slaves
|
||||
|
||||
def __setitem__(self, slave, context):
|
||||
''' Used to set a new slave context
|
||||
|
||||
:param slave: The slave context to set
|
||||
:param context: The new context to set for this slave
|
||||
'''
|
||||
if self.single:
|
||||
slave = Defaults.UnitId
|
||||
if 0xf7 >= slave >= 0x00:
|
||||
self._slaves[slave] = context
|
||||
else:
|
||||
raise NoSuchSlaveException('slave index :{} '
|
||||
'out of range'.format(slave))
|
||||
|
||||
def __delitem__(self, slave):
|
||||
''' Wrapper used to access the slave context
|
||||
|
||||
:param slave: The slave context to remove
|
||||
'''
|
||||
if not self.single and (0xf7 >= slave >= 0x00):
|
||||
del self._slaves[slave]
|
||||
else:
|
||||
raise NoSuchSlaveException('slave index: {} '
|
||||
'out of range'.format(slave))
|
||||
|
||||
def __getitem__(self, slave):
|
||||
''' Used to get access to a slave context
|
||||
|
||||
:param slave: The slave context to get
|
||||
:returns: The requested slave context
|
||||
'''
|
||||
if self.single:
|
||||
slave = Defaults.UnitId
|
||||
if slave in self._slaves:
|
||||
return self._slaves.get(slave)
|
||||
else:
|
||||
raise NoSuchSlaveException("slave - {} does not exist, "
|
||||
"or is out of range".format(slave))
|
||||
|
||||
def slaves(self):
|
||||
# Python3 now returns keys() as iterable
|
||||
return list(self._slaves.keys())
|
||||
@@ -1,7 +0,0 @@
|
||||
from pymodbus.datastore.database.sql_datastore import SqlSlaveContext
|
||||
from pymodbus.datastore.database.redis_datastore import RedisSlaveContext
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = ["SqlSlaveContext", "RedisSlaveContext"]
|
||||
@@ -1,243 +0,0 @@
|
||||
import redis
|
||||
from pymodbus.interfaces import IModbusSlaveContext
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Context
|
||||
#---------------------------------------------------------------------------#
|
||||
class RedisSlaveContext(IModbusSlaveContext):
|
||||
'''
|
||||
This is a modbus slave context using redis as a backing
|
||||
store.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes the datastores
|
||||
|
||||
:param host: The host to connect to
|
||||
:param port: The port to connect to
|
||||
:param prefix: A prefix for the keys
|
||||
'''
|
||||
host = kwargs.get('host', 'localhost')
|
||||
port = kwargs.get('port', 6379)
|
||||
self.prefix = kwargs.get('prefix', 'pymodbus')
|
||||
self.client = kwargs.get('client', redis.Redis(host=host, port=port))
|
||||
self._build_mapping()
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the context
|
||||
|
||||
:returns: A string representation of the context
|
||||
'''
|
||||
return "Redis Slave Context %s" % self.client
|
||||
|
||||
def reset(self):
|
||||
''' Resets all the datastores to their default values '''
|
||||
self.client.flushall()
|
||||
|
||||
def validate(self, fx, address, count=1):
|
||||
''' Validates the request to make sure it is in range
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("validate[%d] %d:%d" % (fx, address, count))
|
||||
return self._val_callbacks[self.decode(fx)](address, count)
|
||||
|
||||
def getValues(self, fx, address, count=1):
|
||||
''' Get `count` values from datastore
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("getValues[%d] %d:%d" % (fx, address, count))
|
||||
return self._get_callbacks[self.decode(fx)](address, count)
|
||||
|
||||
def setValues(self, fx, address, values):
|
||||
''' Sets the datastore with the supplied values
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
'''
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("setValues[%d] %d:%d" % (fx, address, len(values)))
|
||||
self._set_callbacks[self.decode(fx)](address, values)
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Redis Helper Methods
|
||||
#--------------------------------------------------------------------------#
|
||||
def _get_prefix(self, key):
|
||||
''' This is a helper to abstract getting bit values
|
||||
|
||||
:param key: The key prefix to use
|
||||
:returns: The key prefix to redis
|
||||
'''
|
||||
return "%s:%s" % (self.prefix, key)
|
||||
|
||||
def _build_mapping(self):
|
||||
'''
|
||||
A quick helper method to build the function
|
||||
code mapper.
|
||||
'''
|
||||
self._val_callbacks = {
|
||||
'd': lambda o, c: self._val_bit('d', o, c),
|
||||
'c': lambda o, c: self._val_bit('c', o, c),
|
||||
'h': lambda o, c: self._val_reg('h', o, c),
|
||||
'i': lambda o, c: self._val_reg('i', o, c),
|
||||
}
|
||||
self._get_callbacks = {
|
||||
'd': lambda o, c: self._get_bit('d', o, c),
|
||||
'c': lambda o, c: self._get_bit('c', o, c),
|
||||
'h': lambda o, c: self._get_reg('h', o, c),
|
||||
'i': lambda o, c: self._get_reg('i', o, c),
|
||||
}
|
||||
self._set_callbacks = {
|
||||
'd': lambda o, v: self._set_bit('d', o, v),
|
||||
'c': lambda o, v: self._set_bit('c', o, v),
|
||||
'h': lambda o, v: self._set_reg('h', o, v),
|
||||
'i': lambda o, v: self._set_reg('i', o, v),
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Redis discrete implementation
|
||||
#--------------------------------------------------------------------------#
|
||||
_bit_size = 16
|
||||
_bit_default = '\x00' * (_bit_size % 8)
|
||||
|
||||
def _get_bit_values(self, key, offset, count):
|
||||
''' This is a helper to abstract getting bit values
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
key = self._get_prefix(key)
|
||||
s = divmod(offset, self._bit_size)[0]
|
||||
e = divmod(offset + count, self._bit_size)[0]
|
||||
|
||||
request = ('%s:%s' % (key, v) for v in range(s, e + 1))
|
||||
response = self.client.mget(request)
|
||||
return response
|
||||
|
||||
def _val_bit(self, key, offset, count):
|
||||
''' Validates that the given range is currently set in redis.
|
||||
If any of the keys return None, then it is invalid.
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
response = self._get_bit_values(key, offset, count)
|
||||
return True if None not in response else False
|
||||
|
||||
def _get_bit(self, key, offset, count):
|
||||
'''
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
response = self._get_bit_values(key, offset, count)
|
||||
response = (r or self._bit_default for r in response)
|
||||
result = ''.join(response)
|
||||
result = unpack_bitstring(result)
|
||||
return result[offset:offset + count]
|
||||
|
||||
def _set_bit(self, key, offset, values):
|
||||
'''
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param values: The values to set
|
||||
'''
|
||||
count = len(values)
|
||||
s = divmod(offset, self._bit_size)[0]
|
||||
e = divmod(offset + count, self._bit_size)[0]
|
||||
value = pack_bitstring(values)
|
||||
|
||||
current = self._get_bit_values(key, offset, count)
|
||||
current = (r or self._bit_default for r in current)
|
||||
current = ''.join(current)
|
||||
current = current[0:offset] + value.decode('utf-8') + current[offset + count:]
|
||||
final = (current[s:s + self._bit_size] for s in range(0, count, self._bit_size))
|
||||
|
||||
key = self._get_prefix(key)
|
||||
request = ('%s:%s' % (key, v) for v in range(s, e + 1))
|
||||
request = dict(zip(request, final))
|
||||
self.client.mset(request)
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Redis register implementation
|
||||
#--------------------------------------------------------------------------#
|
||||
_reg_size = 16
|
||||
_reg_default = '\x00' * (_reg_size % 8)
|
||||
|
||||
def _get_reg_values(self, key, offset, count):
|
||||
''' This is a helper to abstract getting register values
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
key = self._get_prefix(key)
|
||||
#s = divmod(offset, self.__reg_size)[0]
|
||||
#e = divmod(offset+count, self.__reg_size)[0]
|
||||
|
||||
#request = ('%s:%s' % (key, v) for v in range(s, e + 1))
|
||||
request = ('%s:%s' % (key, v) for v in range(offset, count + 1))
|
||||
response = self.client.mget(request)
|
||||
return response
|
||||
|
||||
def _val_reg(self, key, offset, count):
|
||||
''' Validates that the given range is currently set in redis.
|
||||
If any of the keys return None, then it is invalid.
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
response = self._get_reg_values(key, offset, count)
|
||||
return None not in response
|
||||
|
||||
def _get_reg(self, key, offset, count):
|
||||
'''
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
response = self._get_reg_values(key, offset, count)
|
||||
response = [r or self._reg_default for r in response]
|
||||
return response[offset:offset + count]
|
||||
|
||||
def _set_reg(self, key, offset, values):
|
||||
'''
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param values: The values to set
|
||||
'''
|
||||
count = len(values)
|
||||
#s = divmod(offset, self.__reg_size)
|
||||
#e = divmod(offset+count, self.__reg_size)
|
||||
|
||||
#current = self.__get_reg_values(key, offset, count)
|
||||
|
||||
key = self._get_prefix(key)
|
||||
request = ('%s:%s' % (key, v) for v in range(offset, count + 1))
|
||||
request = dict(zip(request, values))
|
||||
self.client.mset(request)
|
||||
@@ -1,184 +0,0 @@
|
||||
import sqlalchemy
|
||||
import sqlalchemy.types as sqltypes
|
||||
from sqlalchemy.sql import and_
|
||||
from sqlalchemy.schema import UniqueConstraint
|
||||
from sqlalchemy.sql.expression import bindparam
|
||||
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.interfaces import IModbusSlaveContext
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Context
|
||||
# --------------------------------------------------------------------------- #
|
||||
class SqlSlaveContext(IModbusSlaveContext):
|
||||
"""
|
||||
This creates a modbus data model with each data access
|
||||
stored in its own personal block
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Initializes the datastores
|
||||
|
||||
:param kwargs: Each element is a ModbusDataBlock
|
||||
"""
|
||||
self.table = kwargs.get('table', 'pymodbus')
|
||||
self.database = kwargs.get('database', 'sqlite:///pymodbus.db')
|
||||
self._db_create(self.table, self.database)
|
||||
|
||||
def __str__(self):
|
||||
""" Returns a string representation of the context
|
||||
|
||||
:returns: A string representation of the context
|
||||
"""
|
||||
return "Modbus Slave Context"
|
||||
|
||||
def reset(self):
|
||||
""" Resets all the datastores to their default values """
|
||||
self._metadata.drop_all()
|
||||
self._db_create(self.table, self.database)
|
||||
|
||||
def validate(self, fx, address, count=1):
|
||||
""" Validates the request to make sure it is in range
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
"""
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("validate[%d] %d:%d" % (fx, address, count))
|
||||
return self._validate(self.decode(fx), address, count)
|
||||
|
||||
def getValues(self, fx, address, count=1):
|
||||
""" Get `count` values from datastore
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
"""
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("get-values[%d] %d:%d" % (fx, address, count))
|
||||
return self._get(self.decode(fx), address, count)
|
||||
|
||||
def setValues(self, fx, address, values, update=True):
|
||||
""" Sets the datastore with the supplied values
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
:param update: Update existing register in the db
|
||||
"""
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("set-values[%d] %d:%d" % (fx, address, len(values)))
|
||||
if update:
|
||||
self._update(self.decode(fx), address, values)
|
||||
else:
|
||||
self._set(self.decode(fx), address, values)
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Sqlite Helper Methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
def _db_create(self, table, database):
|
||||
""" A helper method to initialize the database and handles
|
||||
|
||||
:param table: The table name to create
|
||||
:param database: The database uri to use
|
||||
"""
|
||||
self._engine = sqlalchemy.create_engine(database, echo=False)
|
||||
self._metadata = sqlalchemy.MetaData(self._engine)
|
||||
self._table = sqlalchemy.Table(table, self._metadata,
|
||||
sqlalchemy.Column('type', sqltypes.String(1)),
|
||||
sqlalchemy.Column('index', sqltypes.Integer),
|
||||
sqlalchemy.Column('value', sqltypes.Integer),
|
||||
UniqueConstraint('type', 'index', name='key'))
|
||||
self._table.create(checkfirst=True)
|
||||
self._connection = self._engine.connect()
|
||||
|
||||
def _get(self, type, offset, count):
|
||||
"""
|
||||
:param type: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
:returns: The resulting values
|
||||
"""
|
||||
query = self._table.select(and_(
|
||||
self._table.c.type == type,
|
||||
self._table.c.index >= offset,
|
||||
self._table.c.index <= offset + count - 1)
|
||||
)
|
||||
query = query.order_by(self._table.c.index.asc())
|
||||
result = self._connection.execute(query).fetchall()
|
||||
return [row.value for row in result]
|
||||
|
||||
def _build_set(self, type, offset, values, prefix=''):
|
||||
""" A helper method to generate the sql update context
|
||||
|
||||
:param type: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param values: The values to set
|
||||
:param prefix: Prefix fields index and type, defaults to empty string
|
||||
"""
|
||||
result = []
|
||||
for index, value in enumerate(values):
|
||||
result.append({
|
||||
prefix + 'type': type,
|
||||
prefix + 'index': offset + index,
|
||||
'value': value
|
||||
})
|
||||
return result
|
||||
|
||||
def _check(self, type, offset, values):
|
||||
result = self._get(type, offset, count=1)
|
||||
return False if len(result) > 0 else True
|
||||
|
||||
def _set(self, type, offset, values):
|
||||
"""
|
||||
|
||||
:param key: The type prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param values: The values to set
|
||||
"""
|
||||
if self._check(type, offset, values):
|
||||
context = self._build_set(type, offset, values)
|
||||
query = self._table.insert()
|
||||
result = self._connection.execute(query, context)
|
||||
return result.rowcount == len(values)
|
||||
else:
|
||||
return False
|
||||
|
||||
def _update(self, type, offset, values):
|
||||
"""
|
||||
|
||||
:param type: The type prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param values: The values to set
|
||||
"""
|
||||
context = self._build_set(type, offset, values, prefix='x_')
|
||||
query = self._table.update().values(value='value')
|
||||
query = query.where(and_(
|
||||
self._table.c.type == bindparam('x_type'),
|
||||
self._table.c.index == bindparam('x_index')))
|
||||
result = self._connection.execute(query, context)
|
||||
return result.rowcount == len(values)
|
||||
|
||||
def _validate(self, type, offset, count):
|
||||
"""
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
:returns: The result of the validation
|
||||
"""
|
||||
query = self._table.select(and_(
|
||||
self._table.c.type == type,
|
||||
self._table.c.index >= offset,
|
||||
self._table.c.index <= offset + count - 1))
|
||||
result = self._connection.execute(query).fetchall()
|
||||
return len(result) == count
|
||||
@@ -1,108 +0,0 @@
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.interfaces import IModbusSlaveContext
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Context
|
||||
#---------------------------------------------------------------------------#
|
||||
class RemoteSlaveContext(IModbusSlaveContext):
|
||||
''' TODO
|
||||
This creates a modbus data model that connects to
|
||||
a remote device (depending on the client used)
|
||||
'''
|
||||
|
||||
def __init__(self, client, unit=None):
|
||||
''' Initializes the datastores
|
||||
|
||||
:param client: The client to retrieve values with
|
||||
:param unit: Unit ID of the remote slave
|
||||
'''
|
||||
self._client = client
|
||||
self.unit = unit
|
||||
self.__build_mapping()
|
||||
|
||||
def reset(self):
|
||||
''' Resets all the datastores to their default values '''
|
||||
raise NotImplementedException()
|
||||
|
||||
def validate(self, fx, address, count=1):
|
||||
''' Validates the request to make sure it is in range
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
_logger.debug("validate[%d] %d:%d" % (fx, address, count))
|
||||
result = self.__get_callbacks[self.decode(fx)](address, count)
|
||||
return not result.isError()
|
||||
|
||||
def getValues(self, fx, address, count=1):
|
||||
''' Get `count` values from datastore
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
# TODO deal with deferreds
|
||||
_logger.debug("get values[%d] %d:%d" % (fx, address, count))
|
||||
result = self.__get_callbacks[self.decode(fx)](address, count)
|
||||
return self.__extract_result(self.decode(fx), result)
|
||||
|
||||
def setValues(self, fx, address, values):
|
||||
''' Sets the datastore with the supplied values
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
'''
|
||||
# TODO deal with deferreds
|
||||
_logger.debug("set values[%d] %d:%d" % (fx, address, len(values)))
|
||||
self.__set_callbacks[self.decode(fx)](address, values)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the context
|
||||
|
||||
:returns: A string representation of the context
|
||||
'''
|
||||
return "Remote Slave Context(%s)" % self._client
|
||||
|
||||
def __build_mapping(self):
|
||||
'''
|
||||
A quick helper method to build the function
|
||||
code mapper.
|
||||
'''
|
||||
kwargs = {}
|
||||
if self.unit:
|
||||
kwargs["unit"] = self.unit
|
||||
self.__get_callbacks = {
|
||||
'd': lambda a, c: self._client.read_discrete_inputs(a, c, **kwargs),
|
||||
'c': lambda a, c: self._client.read_coils(a, c, **kwargs),
|
||||
'h': lambda a, c: self._client.read_holding_registers(a, c, **kwargs),
|
||||
'i': lambda a, c: self._client.read_input_registers(a, c, **kwargs),
|
||||
}
|
||||
self.__set_callbacks = {
|
||||
'd': lambda a, v: self._client.write_coils(a, v, **kwargs),
|
||||
'c': lambda a, v: self._client.write_coils(a, v, **kwargs),
|
||||
'h': lambda a, v: self._client.write_registers(a, v, **kwargs),
|
||||
'i': lambda a, v: self._client.write_registers(a, v, **kwargs),
|
||||
}
|
||||
|
||||
def __extract_result(self, fx, result):
|
||||
''' A helper method to extract the values out of
|
||||
a response. TODO make this consistent (values?)
|
||||
'''
|
||||
if not result.isError():
|
||||
if fx in ['d', 'c']:
|
||||
return result.bits
|
||||
if fx in ['h', 'i']:
|
||||
return result.registers
|
||||
else:
|
||||
return result
|
||||
@@ -1,313 +0,0 @@
|
||||
"""
|
||||
Modbus Server Datastore
|
||||
-------------------------
|
||||
|
||||
For each server, you will create a ModbusServerContext and pass
|
||||
in the default address space for each data access. The class
|
||||
will create and manage the data.
|
||||
|
||||
Further modification of said data accesses should be performed
|
||||
with [get,set][access]Values(address, count)
|
||||
|
||||
Datastore Implementation
|
||||
-------------------------
|
||||
|
||||
There are two ways that the server datastore can be implemented.
|
||||
The first is a complete range from 'address' start to 'count'
|
||||
number of indecies. This can be thought of as a straight array::
|
||||
|
||||
data = range(1, 1 + count)
|
||||
[1,2,3,...,count]
|
||||
|
||||
The other way that the datastore can be implemented (and how
|
||||
many devices implement it) is a associate-array::
|
||||
|
||||
data = {1:'1', 3:'3', ..., count:'count'}
|
||||
[1,3,...,count]
|
||||
|
||||
The difference between the two is that the latter will allow
|
||||
arbitrary gaps in its datastore while the former will not.
|
||||
This is seen quite commonly in some modbus implementations.
|
||||
What follows is a clear example from the field:
|
||||
|
||||
Say a company makes two devices to monitor power usage on a rack.
|
||||
One works with three-phase and the other with a single phase. The
|
||||
company will dictate a modbus data mapping such that registers::
|
||||
|
||||
n: phase 1 power
|
||||
n+1: phase 2 power
|
||||
n+2: phase 3 power
|
||||
|
||||
Using this, layout, the first device will implement n, n+1, and n+2,
|
||||
however, the second device may set the latter two values to 0 or
|
||||
will simply not implmented the registers thus causing a single read
|
||||
or a range read to fail.
|
||||
|
||||
I have both methods implemented, and leave it up to the user to change
|
||||
based on their preference.
|
||||
"""
|
||||
from pymodbus.exceptions import NotImplementedException, ParameterException
|
||||
from pymodbus.compat import iteritems, iterkeys, itervalues, get_next
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Datablock Storage
|
||||
#---------------------------------------------------------------------------#
|
||||
class BaseModbusDataBlock(object):
|
||||
'''
|
||||
Base class for a modbus datastore
|
||||
|
||||
Derived classes must create the following fields:
|
||||
@address The starting address point
|
||||
@defult_value The default value of the datastore
|
||||
@values The actual datastore values
|
||||
|
||||
Derived classes must implemented the following methods:
|
||||
validate(self, address, count=1)
|
||||
getValues(self, address, count=1)
|
||||
setValues(self, address, values)
|
||||
'''
|
||||
|
||||
def default(self, count, value=False):
|
||||
''' Used to initialize a store to one value
|
||||
|
||||
:param count: The number of fields to set
|
||||
:param value: The default value to set to the fields
|
||||
'''
|
||||
self.default_value = value
|
||||
self.values = [self.default_value] * count
|
||||
self.address = 0x00
|
||||
|
||||
def reset(self):
|
||||
''' Resets the datastore to the initialized default value '''
|
||||
self.values = [self.default_value] * len(self.values)
|
||||
|
||||
def validate(self, address, count=1):
|
||||
''' Checks to see if the request is in range
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test for
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
raise NotImplementedException("Datastore Address Check")
|
||||
|
||||
def getValues(self, address, count=1):
|
||||
''' Returns the requested values from the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
raise NotImplementedException("Datastore Value Retrieve")
|
||||
|
||||
def setValues(self, address, values):
|
||||
''' Returns the requested values from the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param values: The values to store
|
||||
'''
|
||||
raise NotImplementedException("Datastore Value Retrieve")
|
||||
|
||||
def __str__(self):
|
||||
''' Build a representation of the datastore
|
||||
|
||||
:returns: A string representation of the datastore
|
||||
'''
|
||||
return "DataStore(%d, %d)" % (len(self.values), self.default_value)
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the data block data
|
||||
|
||||
:returns: An iterator of the data block data
|
||||
'''
|
||||
if isinstance(self.values, dict):
|
||||
return iteritems(self.values)
|
||||
return enumerate(self.values, self.address)
|
||||
|
||||
|
||||
class ModbusSequentialDataBlock(BaseModbusDataBlock):
|
||||
''' Creates a sequential modbus datastore '''
|
||||
|
||||
def __init__(self, address, values):
|
||||
''' Initializes the datastore
|
||||
|
||||
:param address: The starting address of the datastore
|
||||
:param values: Either a list or a dictionary of values
|
||||
'''
|
||||
self.address = address
|
||||
if hasattr(values, '__iter__'):
|
||||
self.values = list(values)
|
||||
else:
|
||||
self.values = [values]
|
||||
self.default_value = self.values[0].__class__()
|
||||
|
||||
@classmethod
|
||||
def create(klass):
|
||||
''' Factory method to create a datastore with the
|
||||
full address space initialized to 0x00
|
||||
|
||||
:returns: An initialized datastore
|
||||
'''
|
||||
return klass(0x00, [0x00] * 65536)
|
||||
|
||||
def validate(self, address, count=1):
|
||||
''' Checks to see if the request is in range
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test for
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
result = (self.address <= address)
|
||||
result &= ((self.address + len(self.values)) >= (address + count))
|
||||
return result
|
||||
|
||||
def getValues(self, address, count=1):
|
||||
''' Returns the requested values of the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
start = address - self.address
|
||||
return self.values[start:start + count]
|
||||
|
||||
def setValues(self, address, values):
|
||||
''' Sets the requested values of the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
'''
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
start = address - self.address
|
||||
self.values[start:start + len(values)] = values
|
||||
|
||||
|
||||
class ModbusSparseDataBlock(BaseModbusDataBlock):
|
||||
"""
|
||||
Creates a sparse modbus datastore
|
||||
|
||||
E.g Usage.
|
||||
sparse = ModbusSparseDataBlock({10: [3, 5, 6, 8], 30: 1, 40: [0]*20})
|
||||
|
||||
This would create a datablock with 3 blocks starting at
|
||||
offset 10 with length 4 , 30 with length 1 and 40 with length 20
|
||||
|
||||
sparse = ModbusSparseDataBlock([10]*100)
|
||||
Creates a sparse datablock of length 100 starting at offset 0 and default value of 10
|
||||
|
||||
sparse = ModbusSparseDataBlock() --> Create Empty datablock
|
||||
sparse.setValues(0, [10]*10) --> Add block 1 at offset 0 with length 10 (default value 10)
|
||||
sparse.setValues(30, [20]*5) --> Add block 2 at offset 30 with length 5 (default value 20)
|
||||
|
||||
if mutable is set to True during initialization, the datablock can not be altered with
|
||||
setValues (new datablocks can not be added)
|
||||
"""
|
||||
|
||||
def __init__(self, values=None, mutable=True):
|
||||
"""
|
||||
Initializes a sparse datastore. Will only answer to addresses
|
||||
registered, either initially here, or later via setValues()
|
||||
|
||||
:param values: Either a list or a dictionary of values
|
||||
:param mutable: The data-block can be altered later with setValues(i.e add more blocks)
|
||||
|
||||
If values are list , This is as good as sequential datablock.
|
||||
Values as dictionary should be in {offset: <values>} format, if values
|
||||
is a list, a sparse datablock is created starting at offset with the length of values.
|
||||
If values is a integer, then the value is set for the corresponding offset.
|
||||
|
||||
"""
|
||||
self.values = {}
|
||||
self._process_values(values)
|
||||
self.mutable = mutable
|
||||
self.default_value = self.values.copy()
|
||||
self.address = get_next(iterkeys(self.values), None)
|
||||
|
||||
@classmethod
|
||||
def create(klass, values=None):
|
||||
''' Factory method to create sparse datastore.
|
||||
Use setValues to initialize registers.
|
||||
|
||||
:param values: Either a list or a dictionary of values
|
||||
:returns: An initialized datastore
|
||||
'''
|
||||
return klass(values)
|
||||
|
||||
def reset(self):
|
||||
''' Reset the store to the initially provided defaults'''
|
||||
self.values = self.default_value.copy()
|
||||
|
||||
def validate(self, address, count=1):
|
||||
''' Checks to see if the request is in range
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test for
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
if count == 0:
|
||||
return False
|
||||
handle = set(range(address, address + count))
|
||||
return handle.issubset(set(iterkeys(self.values)))
|
||||
|
||||
def getValues(self, address, count=1):
|
||||
''' Returns the requested values of the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
return [self.values[i] for i in range(address, address + count)]
|
||||
|
||||
def _process_values(self, values):
|
||||
def _process_as_dict(values):
|
||||
for idx, val in iteritems(values):
|
||||
if isinstance(val, (list, tuple)):
|
||||
for i, v in enumerate(val):
|
||||
self.values[idx + i] = v
|
||||
else:
|
||||
self.values[idx] = int(val)
|
||||
if isinstance(values, dict):
|
||||
_process_as_dict(values)
|
||||
return
|
||||
if hasattr(values, '__iter__'):
|
||||
values = dict(enumerate(values))
|
||||
elif values is None:
|
||||
values = {} # Must make a new dict here per instance
|
||||
else:
|
||||
raise ParameterException("Values for datastore must "
|
||||
"be a list or dictionary")
|
||||
_process_as_dict(values)
|
||||
|
||||
def setValues(self, address, values, use_as_default=False):
|
||||
''' Sets the requested values of the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
:param use_as_default: Use the values as default
|
||||
'''
|
||||
if isinstance(values, dict):
|
||||
new_offsets = list(set(list(values.keys())) - set(list(self.values.keys())))
|
||||
if new_offsets and not self.mutable:
|
||||
raise ParameterException("Offsets {} not "
|
||||
"in range".format(new_offsets))
|
||||
self._process_values(values)
|
||||
else:
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
for idx, val in enumerate(values):
|
||||
if address+idx not in self.values and not self.mutable:
|
||||
raise ParameterException("Offset {} not "
|
||||
"in range".format(address+idx))
|
||||
self.values[address + idx] = val
|
||||
if not self.address:
|
||||
self.address = get_next(iterkeys(self.values), None)
|
||||
if use_as_default:
|
||||
for idx, val in iteritems(self.values):
|
||||
self.default_value[idx] = val
|
||||
@@ -1,626 +0,0 @@
|
||||
"""
|
||||
Modbus Device Controller
|
||||
-------------------------
|
||||
|
||||
These are the device management handlers. They should be
|
||||
maintained in the server context and the various methods
|
||||
should be inserted in the correct locations.
|
||||
"""
|
||||
from pymodbus.constants import DeviceInformation
|
||||
from pymodbus.interfaces import Singleton
|
||||
from pymodbus.utilities import dict_property
|
||||
from pymodbus.compat import iteritems, itervalues, izip, int2byte
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Network Access Control
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusAccessControl(Singleton):
|
||||
'''
|
||||
This is a simple implementation of a Network Management System table.
|
||||
Its purpose is to control access to the server (if it is used).
|
||||
We assume that if an entry is in the table, it is allowed accesses to
|
||||
resources. However, if the host does not appear in the table (all
|
||||
unknown hosts) its connection will simply be closed.
|
||||
|
||||
Since it is a singleton, only one version can possible exist and all
|
||||
instances pull from here.
|
||||
'''
|
||||
__nmstable = [
|
||||
"127.0.0.1",
|
||||
]
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the network access table
|
||||
|
||||
:returns: An iterator of the network access table
|
||||
'''
|
||||
return self.__nmstable.__iter__()
|
||||
|
||||
def __contains__(self, host):
|
||||
''' Check if a host is allowed to access resources
|
||||
|
||||
:param host: The host to check
|
||||
'''
|
||||
return host in self.__nmstable
|
||||
|
||||
def add(self, host):
|
||||
''' Add allowed host(s) from the NMS table
|
||||
|
||||
:param host: The host to add
|
||||
'''
|
||||
if not isinstance(host, list):
|
||||
host = [host]
|
||||
for entry in host:
|
||||
if entry not in self.__nmstable:
|
||||
self.__nmstable.append(entry)
|
||||
|
||||
def remove(self, host):
|
||||
''' Remove allowed host(s) from the NMS table
|
||||
|
||||
:param host: The host to remove
|
||||
'''
|
||||
if not isinstance(host, list):
|
||||
host = [host]
|
||||
for entry in host:
|
||||
if entry in self.__nmstable:
|
||||
self.__nmstable.remove(entry)
|
||||
|
||||
def check(self, host):
|
||||
''' Check if a host is allowed to access resources
|
||||
|
||||
:param host: The host to check
|
||||
'''
|
||||
return host in self.__nmstable
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Modbus Plus Statistics
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusPlusStatistics(object):
|
||||
'''
|
||||
This is used to maintain the current modbus plus statistics count. As of
|
||||
right now this is simply a stub to complete the modbus implementation.
|
||||
For more information, see the modbus implementation guide page 87.
|
||||
'''
|
||||
|
||||
__data = OrderedDict({
|
||||
'node_type_id' : [0x00] * 2, # 00
|
||||
'software_version_number' : [0x00] * 2, # 01
|
||||
'network_address' : [0x00] * 2, # 02
|
||||
'mac_state_variable' : [0x00] * 2, # 03
|
||||
'peer_status_code' : [0x00] * 2, # 04
|
||||
'token_pass_counter' : [0x00] * 2, # 05
|
||||
'token_rotation_time' : [0x00] * 2, # 06
|
||||
|
||||
'program_master_token_failed' : [0x00], # 07 hi
|
||||
'data_master_token_failed' : [0x00], # 07 lo
|
||||
'program_master_token_owner' : [0x00], # 08 hi
|
||||
'data_master_token_owner' : [0x00], # 08 lo
|
||||
'program_slave_token_owner' : [0x00], # 09 hi
|
||||
'data_slave_token_owner' : [0x00], # 09 lo
|
||||
'data_slave_command_transfer' : [0x00], # 10 hi
|
||||
'__unused_10_lowbit' : [0x00], # 10 lo
|
||||
|
||||
'program_slave_command_transfer' : [0x00], # 11 hi
|
||||
'program_master_rsp_transfer' : [0x00], # 11 lo
|
||||
'program_slave_auto_logout' : [0x00], # 12 hi
|
||||
'program_master_connect_status' : [0x00], # 12 lo
|
||||
'receive_buffer_dma_overrun' : [0x00], # 13 hi
|
||||
'pretransmit_deferral_error' : [0x00], # 13 lo
|
||||
'frame_size_error' : [0x00], # 14 hi
|
||||
'repeated_command_received' : [0x00], # 14 lo
|
||||
'receiver_alignment_error' : [0x00], # 15 hi
|
||||
'receiver_collision_abort_error' : [0x00], # 15 lo
|
||||
'bad_packet_length_error' : [0x00], # 16 hi
|
||||
'receiver_crc_error' : [0x00], # 16 lo
|
||||
'transmit_buffer_dma_underrun' : [0x00], # 17 hi
|
||||
'bad_link_address_error' : [0x00], # 17 lo
|
||||
|
||||
'bad_mac_function_code_error' : [0x00], # 18 hi
|
||||
'internal_packet_length_error' : [0x00], # 18 lo
|
||||
'communication_failed_error' : [0x00], # 19 hi
|
||||
'communication_retries' : [0x00], # 19 lo
|
||||
'no_response_error' : [0x00], # 20 hi
|
||||
'good_receive_packet' : [0x00], # 20 lo
|
||||
'unexpected_path_error' : [0x00], # 21 hi
|
||||
'exception_response_error' : [0x00], # 21 lo
|
||||
'forgotten_transaction_error' : [0x00], # 22 hi
|
||||
'unexpected_response_error' : [0x00], # 22 lo
|
||||
|
||||
'active_station_bit_map' : [0x00] * 8, # 23-26
|
||||
'token_station_bit_map' : [0x00] * 8, # 27-30
|
||||
'global_data_bit_map' : [0x00] * 8, # 31-34
|
||||
'receive_buffer_use_bit_map' : [0x00] * 8, # 35-37
|
||||
'data_master_output_path' : [0x00] * 8, # 38-41
|
||||
'data_slave_input_path' : [0x00] * 8, # 42-45
|
||||
'program_master_outptu_path' : [0x00] * 8, # 46-49
|
||||
'program_slave_input_path' : [0x00] * 8, # 50-53
|
||||
})
|
||||
|
||||
def __init__(self):
|
||||
'''
|
||||
Initialize the modbus plus statistics with the default
|
||||
information.
|
||||
'''
|
||||
self.reset()
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the statistics
|
||||
|
||||
:returns: An iterator of the modbus plus statistics
|
||||
'''
|
||||
return iteritems(self.__data)
|
||||
|
||||
def reset(self):
|
||||
''' This clears all of the modbus plus statistics
|
||||
'''
|
||||
for key in self.__data:
|
||||
self.__data[key] = [0x00] * len(self.__data[key])
|
||||
|
||||
def summary(self):
|
||||
''' Returns a summary of the modbus plus statistics
|
||||
|
||||
:returns: 54 16-bit words representing the status
|
||||
'''
|
||||
return itervalues(self.__data)
|
||||
|
||||
def encode(self):
|
||||
''' Returns a summary of the modbus plus statistics
|
||||
|
||||
:returns: 54 16-bit words representing the status
|
||||
'''
|
||||
total, values = [], sum(self.__data.values(), [])
|
||||
for c in range(0, len(values), 2):
|
||||
total.append((values[c] << 8) | values[c+1])
|
||||
return total
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Device Information Control
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusDeviceIdentification(object):
|
||||
'''
|
||||
This is used to supply the device identification
|
||||
for the readDeviceIdentification function
|
||||
|
||||
For more information read section 6.21 of the modbus
|
||||
application protocol.
|
||||
'''
|
||||
__data = {
|
||||
0x00: '', # VendorName
|
||||
0x01: '', # ProductCode
|
||||
0x02: '', # MajorMinorRevision
|
||||
0x03: '', # VendorUrl
|
||||
0x04: '', # ProductName
|
||||
0x05: '', # ModelName
|
||||
0x06: '', # UserApplicationName
|
||||
0x07: '', # reserved
|
||||
0x08: '', # reserved
|
||||
# 0x80 -> 0xFF are private
|
||||
}
|
||||
|
||||
__names = [
|
||||
'VendorName',
|
||||
'ProductCode',
|
||||
'MajorMinorRevision',
|
||||
'VendorUrl',
|
||||
'ProductName',
|
||||
'ModelName',
|
||||
'UserApplicationName',
|
||||
]
|
||||
|
||||
def __init__(self, info=None):
|
||||
'''
|
||||
Initialize the datastore with the elements you need.
|
||||
(note acceptable range is [0x00-0x06,0x80-0xFF] inclusive)
|
||||
|
||||
:param information: A dictionary of {int:string} of values
|
||||
'''
|
||||
if isinstance(info, dict):
|
||||
for key in info:
|
||||
if (0x06 >= key >= 0x00) or (0xFF >= key >= 0x80):
|
||||
self.__data[key] = info[key]
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the device information
|
||||
|
||||
:returns: An iterator of the device information
|
||||
'''
|
||||
return iteritems(self.__data)
|
||||
|
||||
def summary(self):
|
||||
''' Return a summary of the main items
|
||||
|
||||
:returns: An dictionary of the main items
|
||||
'''
|
||||
return dict(zip(self.__names, itervalues(self.__data)))
|
||||
|
||||
def update(self, value):
|
||||
''' Update the values of this identity
|
||||
using another identify as the value
|
||||
|
||||
:param value: The value to copy values from
|
||||
'''
|
||||
self.__data.update(value)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
''' Wrapper used to access the device information
|
||||
|
||||
:param key: The register to set
|
||||
:param value: The new value for referenced register
|
||||
'''
|
||||
if key not in [0x07, 0x08]:
|
||||
self.__data[key] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
''' Wrapper used to access the device information
|
||||
|
||||
:param key: The register to read
|
||||
'''
|
||||
return self.__data.setdefault(key, '')
|
||||
|
||||
def __str__(self):
|
||||
''' Build a representation of the device
|
||||
|
||||
:returns: A string representation of the device
|
||||
'''
|
||||
return "DeviceIdentity"
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
VendorName = dict_property(lambda s: s.__data, 0)
|
||||
ProductCode = dict_property(lambda s: s.__data, 1)
|
||||
MajorMinorRevision = dict_property(lambda s: s.__data, 2)
|
||||
VendorUrl = dict_property(lambda s: s.__data, 3)
|
||||
ProductName = dict_property(lambda s: s.__data, 4)
|
||||
ModelName = dict_property(lambda s: s.__data, 5)
|
||||
UserApplicationName = dict_property(lambda s: s.__data, 6)
|
||||
|
||||
|
||||
class DeviceInformationFactory(Singleton):
|
||||
''' This is a helper factory that really just hides
|
||||
some of the complexity of processing the device information
|
||||
requests (function code 0x2b 0x0e).
|
||||
'''
|
||||
|
||||
__lookup = {
|
||||
DeviceInformation.Basic: lambda c, r, i: c.__gets(r, list(range(i, 0x03))),
|
||||
DeviceInformation.Regular: lambda c, r, i: c.__gets(r, list(range(i, 0x07))
|
||||
if c.__get(r, i)[i] else list(range(0, 0x07))),
|
||||
DeviceInformation.Extended: lambda c, r, i: c.__gets(r,
|
||||
[x for x in range(i, 0x100) if x not in range(0x07, 0x80)]
|
||||
if c.__get(r, i)[i] else
|
||||
[x for x in range(0, 0x100) if x not in range(0x07, 0x80)]),
|
||||
DeviceInformation.Specific: lambda c, r, i: c.__get(r, i),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get(cls, control, read_code=DeviceInformation.Basic, object_id=0x00):
|
||||
''' Get the requested device data from the system
|
||||
|
||||
:param control: The control block to pull data from
|
||||
:param read_code: The read code to process
|
||||
:param object_id: The specific object_id to read
|
||||
:returns: The requested data (id, length, value)
|
||||
'''
|
||||
identity = control.Identity
|
||||
return cls.__lookup[read_code](cls, identity, object_id)
|
||||
|
||||
@classmethod
|
||||
def __get(cls, identity, object_id):
|
||||
''' Read a single object_id from the device information
|
||||
|
||||
:param identity: The identity block to pull data from
|
||||
:param object_id: The specific object id to read
|
||||
:returns: The requested data (id, length, value)
|
||||
'''
|
||||
return { object_id:identity[object_id] }
|
||||
|
||||
@classmethod
|
||||
def __gets(cls, identity, object_ids):
|
||||
''' Read multiple object_ids from the device information
|
||||
|
||||
:param identity: The identity block to pull data from
|
||||
:param object_ids: The specific object ids to read
|
||||
:returns: The requested data (id, length, value)
|
||||
'''
|
||||
return dict((oid, identity[oid]) for oid in object_ids if identity[oid])
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Counters Handler
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusCountersHandler(object):
|
||||
'''
|
||||
This is a helper class to simplify the properties for the counters::
|
||||
|
||||
0x0B 1 Return Bus Message Count
|
||||
|
||||
Quantity of messages that the remote
|
||||
device has detected on the communications system since its
|
||||
last restart, clear counters operation, or power-up. Messages
|
||||
with bad CRC are not taken into account.
|
||||
|
||||
0x0C 2 Return Bus Communication Error Count
|
||||
|
||||
Quantity of CRC errors encountered by the remote device since its
|
||||
last restart, clear counters operation, or power-up. In case of
|
||||
an error detected on the character level, (overrun, parity error),
|
||||
or in case of a message length < 3 bytes, the receiving device is
|
||||
not able to calculate the CRC. In such cases, this counter is
|
||||
also incremented.
|
||||
|
||||
0x0D 3 Return Slave Exception Error Count
|
||||
|
||||
Quantity of MODBUS exception error detected by the remote device
|
||||
since its last restart, clear counters operation, or power-up. It
|
||||
comprises also the error detected in broadcast messages even if an
|
||||
exception message is not returned in this case.
|
||||
Exception errors are described and listed in "MODBUS Application
|
||||
Protocol Specification" document.
|
||||
|
||||
0xOE 4 Return Slave Message Count
|
||||
|
||||
Quantity of messages addressed to the remote device, including
|
||||
broadcast messages, that the remote device has processed since its
|
||||
last restart, clear counters operation, or power-up.
|
||||
|
||||
0x0F 5 Return Slave No Response Count
|
||||
|
||||
Quantity of messages received by the remote device for which it
|
||||
returned no response (neither a normal response nor an exception
|
||||
response), since its last restart, clear counters operation, or
|
||||
power-up. Then, this counter counts the number of broadcast
|
||||
messages it has received.
|
||||
|
||||
0x10 6 Return Slave NAK Count
|
||||
|
||||
Quantity of messages addressed to the remote device for which it
|
||||
returned a Negative Acknowledge (NAK) exception response, since
|
||||
its last restart, clear counters operation, or power-up. Exception
|
||||
responses are described and listed in "MODBUS Application Protocol
|
||||
Specification" document.
|
||||
|
||||
0x11 7 Return Slave Busy Count
|
||||
|
||||
Quantity of messages addressed to the remote device for which it
|
||||
returned a Slave Device Busy exception response, since its last
|
||||
restart, clear counters operation, or power-up. Exception
|
||||
responses are described and listed in "MODBUS Application
|
||||
Protocol Specification" document.
|
||||
|
||||
0x12 8 Return Bus Character Overrun Count
|
||||
|
||||
Quantity of messages addressed to the remote device that it could
|
||||
not handle due to a character overrun condition, since its last
|
||||
restart, clear counters operation, or power-up. A character
|
||||
overrun is caused by data characters arriving at the port faster
|
||||
than they can.
|
||||
|
||||
.. note:: I threw the event counter in here for convinience
|
||||
'''
|
||||
__data = dict([(i, 0x0000) for i in range(9)])
|
||||
__names = [
|
||||
'BusMessage',
|
||||
'BusCommunicationError',
|
||||
'SlaveExceptionError',
|
||||
'SlaveMessage',
|
||||
'SlaveNoResponse',
|
||||
'SlaveNAK',
|
||||
'SlaveBusy',
|
||||
'BusCharacterOverrun'
|
||||
'Event '
|
||||
]
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the device counters
|
||||
|
||||
:returns: An iterator of the device counters
|
||||
'''
|
||||
return izip(self.__names, itervalues(self.__data))
|
||||
|
||||
def update(self, values):
|
||||
''' Update the values of this identity
|
||||
using another identify as the value
|
||||
|
||||
:param values: The value to copy values from
|
||||
'''
|
||||
for k, v in iteritems(values):
|
||||
v += self.__getattribute__(k)
|
||||
self.__setattr__(k, v)
|
||||
|
||||
def reset(self):
|
||||
''' This clears all of the system counters
|
||||
'''
|
||||
self.__data = dict([(i, 0x0000) for i in range(9)])
|
||||
|
||||
def summary(self):
|
||||
''' Returns a summary of the counters current status
|
||||
|
||||
:returns: A byte with each bit representing each counter
|
||||
'''
|
||||
count, result = 0x01, 0x00
|
||||
for i in itervalues(self.__data):
|
||||
if i != 0x00: result |= count
|
||||
count <<= 1
|
||||
return result
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
BusMessage = dict_property(lambda s: s.__data, 0)
|
||||
BusCommunicationError = dict_property(lambda s: s.__data, 1)
|
||||
BusExceptionError = dict_property(lambda s: s.__data, 2)
|
||||
SlaveMessage = dict_property(lambda s: s.__data, 3)
|
||||
SlaveNoResponse = dict_property(lambda s: s.__data, 4)
|
||||
SlaveNAK = dict_property(lambda s: s.__data, 5)
|
||||
SlaveBusy = dict_property(lambda s: s.__data, 6)
|
||||
BusCharacterOverrun = dict_property(lambda s: s.__data, 7)
|
||||
Event = dict_property(lambda s: s.__data, 8)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Main server control block
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusControlBlock(Singleton):
|
||||
'''
|
||||
This is a global singleton that controls all system information
|
||||
|
||||
All activity should be logged here and all diagnostic requests
|
||||
should come from here.
|
||||
'''
|
||||
|
||||
__mode = 'ASCII'
|
||||
__diagnostic = [False] * 16
|
||||
__instance = None
|
||||
__listen_only = False
|
||||
__delimiter = '\r'
|
||||
__counters = ModbusCountersHandler()
|
||||
__identity = ModbusDeviceIdentification()
|
||||
__plus = ModbusPlusStatistics()
|
||||
__events = []
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Magic
|
||||
#-------------------------------------------------------------------------#
|
||||
def __str__(self):
|
||||
''' Build a representation of the control block
|
||||
|
||||
:returns: A string representation of the control block
|
||||
'''
|
||||
return "ModbusControl"
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the device counters
|
||||
|
||||
:returns: An iterator of the device counters
|
||||
'''
|
||||
return self.__counters.__iter__()
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Events
|
||||
#-------------------------------------------------------------------------#
|
||||
def addEvent(self, event):
|
||||
''' Adds a new event to the event log
|
||||
|
||||
:param event: A new event to add to the log
|
||||
'''
|
||||
self.__events.insert(0, event)
|
||||
self.__events = self.__events[0:64] # chomp to 64 entries
|
||||
self.Counter.Event += 1
|
||||
|
||||
def getEvents(self):
|
||||
''' Returns an encoded collection of the event log.
|
||||
|
||||
:returns: The encoded events packet
|
||||
'''
|
||||
events = [event.encode() for event in self.__events]
|
||||
return b''.join(events)
|
||||
|
||||
def clearEvents(self):
|
||||
''' Clears the current list of events
|
||||
'''
|
||||
self.__events = []
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Other Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
Identity = property(lambda s: s.__identity)
|
||||
Counter = property(lambda s: s.__counters)
|
||||
Events = property(lambda s: s.__events)
|
||||
Plus = property(lambda s: s.__plus)
|
||||
|
||||
def reset(self):
|
||||
''' This clears all of the system counters and the
|
||||
diagnostic register
|
||||
'''
|
||||
self.__events = []
|
||||
self.__counters.reset()
|
||||
self.__diagnostic = [False] * 16
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Listen Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
def _setListenOnly(self, value):
|
||||
''' This toggles the listen only status
|
||||
|
||||
:param value: The value to set the listen status to
|
||||
'''
|
||||
self.__listen_only = bool(value)
|
||||
|
||||
ListenOnly = property(lambda s: s.__listen_only, _setListenOnly)
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Mode Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
def _setMode(self, mode):
|
||||
''' This toggles the current serial mode
|
||||
|
||||
:param mode: The data transfer method in (RTU, ASCII)
|
||||
'''
|
||||
if mode in ['ASCII', 'RTU']:
|
||||
self.__mode = mode
|
||||
|
||||
Mode = property(lambda s: s.__mode, _setMode)
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Delimiter Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
def _setDelimiter(self, char):
|
||||
''' This changes the serial delimiter character
|
||||
|
||||
:param char: The new serial delimiter character
|
||||
'''
|
||||
if isinstance(char, str):
|
||||
self.__delimiter = char.encode()
|
||||
if isinstance(char, bytes):
|
||||
self.__delimiter = char
|
||||
elif isinstance(char, int):
|
||||
self.__delimiter = int2byte(char)
|
||||
|
||||
Delimiter = property(lambda s: s.__delimiter, _setDelimiter)
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Diagnostic Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
def setDiagnostic(self, mapping):
|
||||
''' This sets the value in the diagnostic register
|
||||
|
||||
:param mapping: Dictionary of key:value pairs to set
|
||||
'''
|
||||
for entry in iteritems(mapping):
|
||||
if entry[0] >= 0 and entry[0] < len(self.__diagnostic):
|
||||
self.__diagnostic[entry[0]] = (entry[1] != 0)
|
||||
|
||||
def getDiagnostic(self, bit):
|
||||
''' This gets the value in the diagnostic register
|
||||
|
||||
:param bit: The bit to get
|
||||
:returns: The current value of the requested bit
|
||||
'''
|
||||
try:
|
||||
if bit and bit >= 0 and bit < len(self.__diagnostic):
|
||||
return self.__diagnostic[bit]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def getDiagnosticRegister(self):
|
||||
''' This gets the entire diagnostic register
|
||||
|
||||
:returns: The diagnostic register collection
|
||||
'''
|
||||
return self.__diagnostic
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported Identifiers
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ModbusAccessControl",
|
||||
"ModbusPlusStatistics",
|
||||
"ModbusDeviceIdentification",
|
||||
"DeviceInformationFactory",
|
||||
"ModbusControlBlock"
|
||||
]
|
||||
@@ -1,790 +0,0 @@
|
||||
'''
|
||||
Diagnostic Record Read/Write
|
||||
------------------------------
|
||||
|
||||
These need to be tied into a the current server context
|
||||
or linked to the appropriate data
|
||||
'''
|
||||
import struct
|
||||
|
||||
from pymodbus.constants import ModbusStatus, ModbusPlusOperation
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.device import ModbusControlBlock
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.utilities import pack_bitstring
|
||||
|
||||
_MCB = ModbusControlBlock()
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Function Codes Base Classes
|
||||
# diagnostic 08, 00-18,20
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make sure all the data is decoded from the response
|
||||
#---------------------------------------------------------------------------#
|
||||
class DiagnosticStatusRequest(ModbusRequest):
|
||||
'''
|
||||
This is a base class for all of the diagnostic request functions
|
||||
'''
|
||||
function_code = 0x08
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
'''
|
||||
Base initializer for a diagnostic request
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.message = None
|
||||
|
||||
def encode(self):
|
||||
'''
|
||||
Base encoder for a diagnostic response
|
||||
we encode the data set in self.message
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
packet = struct.pack('>H', self.sub_function_code)
|
||||
if self.message is not None:
|
||||
if isinstance(self.message, str):
|
||||
packet += self.message.encode()
|
||||
elif isinstance(self.message, bytes):
|
||||
packet += self.message
|
||||
elif isinstance(self.message, list):
|
||||
for piece in self.message:
|
||||
packet += struct.pack('>H', piece)
|
||||
elif isinstance(self.message, int):
|
||||
packet += struct.pack('>H', self.message)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Base decoder for a diagnostic request
|
||||
|
||||
:param data: The data to decode into the function code
|
||||
'''
|
||||
self.sub_function_code, self.message = struct.unpack('>HH', data)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Sub function code (2 byte) + Data (2 * N bytes)
|
||||
:return:
|
||||
"""
|
||||
if not isinstance(self.message,list):
|
||||
self.message = [self.message]
|
||||
return 1 + 2 + 2 * len(self.message)
|
||||
|
||||
|
||||
class DiagnosticStatusResponse(ModbusResponse):
|
||||
'''
|
||||
This is a base class for all of the diagnostic response functions
|
||||
|
||||
It works by performing all of the encoding and decoding of variable
|
||||
data and lets the higher classes define what extra data to append
|
||||
and how to execute a request
|
||||
'''
|
||||
function_code = 0x08
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
'''
|
||||
Base initializer for a diagnostic response
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.message = None
|
||||
|
||||
def encode(self):
|
||||
'''
|
||||
Base encoder for a diagnostic response
|
||||
we encode the data set in self.message
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
packet = struct.pack('>H', self.sub_function_code)
|
||||
if self.message is not None:
|
||||
if isinstance(self.message, str):
|
||||
packet += self.message.encode()
|
||||
elif isinstance(self.message, bytes):
|
||||
packet += self.message
|
||||
elif isinstance(self.message, list):
|
||||
for piece in self.message:
|
||||
packet += struct.pack('>H', piece)
|
||||
elif isinstance(self.message, int):
|
||||
packet += struct.pack('>H', self.message)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Base decoder for a diagnostic response
|
||||
|
||||
:param data: The data to decode into the function code
|
||||
'''
|
||||
word_len = len(data)//2
|
||||
if len(data) % 2:
|
||||
word_len += 1
|
||||
data = data + b'0'
|
||||
data = struct.unpack('>' + 'H'*word_len, data)
|
||||
self.sub_function_code, self.message = data[0], data[1:]
|
||||
|
||||
|
||||
class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest):
|
||||
'''
|
||||
A large majority of the diagnostic functions are simple
|
||||
status request functions. They work by sending 0x0000
|
||||
as data and their function code and they are returned
|
||||
2 bytes of data.
|
||||
|
||||
If a function inherits this, they only need to implement
|
||||
the execute method
|
||||
'''
|
||||
|
||||
def __init__(self, data=0x0000, **kwargs):
|
||||
'''
|
||||
General initializer for a simple diagnostic request
|
||||
|
||||
The data defaults to 0x0000 if not provided as over half
|
||||
of the functions require it.
|
||||
|
||||
:param data: The data to send along with the request
|
||||
'''
|
||||
DiagnosticStatusRequest.__init__(self, **kwargs)
|
||||
self.message = data
|
||||
|
||||
def execute(self, *args):
|
||||
''' Base function to raise if not implemented '''
|
||||
raise NotImplementedException("Diagnostic Message Has No Execute Method")
|
||||
|
||||
|
||||
class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse):
|
||||
'''
|
||||
A large majority of the diagnostic functions are simple
|
||||
status request functions. They work by sending 0x0000
|
||||
as data and their function code and they are returned
|
||||
2 bytes of data.
|
||||
'''
|
||||
|
||||
def __init__(self, data=0x0000, **kwargs):
|
||||
''' General initializer for a simple diagnostic response
|
||||
|
||||
:param data: The resulting data to return to the client
|
||||
'''
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
self.message = data
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 00
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnQueryDataRequest(DiagnosticStatusRequest):
|
||||
'''
|
||||
The data passed in the request data field is to be returned (looped back)
|
||||
in the response. The entire response message should be identical to the
|
||||
request.
|
||||
'''
|
||||
sub_function_code = 0x0000
|
||||
|
||||
def __init__(self, message=0x0000, **kwargs):
|
||||
''' Initializes a new instance of the request
|
||||
|
||||
:param message: The message to send to loopback
|
||||
'''
|
||||
DiagnosticStatusRequest.__init__(self, **kwargs)
|
||||
if isinstance(message, list):
|
||||
self.message = message
|
||||
else:
|
||||
self.message = [message]
|
||||
|
||||
def execute(self, *args):
|
||||
''' Executes the loopback request (builds the response)
|
||||
|
||||
:returns: The populated loopback response message
|
||||
'''
|
||||
return ReturnQueryDataResponse(self.message)
|
||||
|
||||
|
||||
class ReturnQueryDataResponse(DiagnosticStatusResponse):
|
||||
'''
|
||||
The data passed in the request data field is to be returned (looped back)
|
||||
in the response. The entire response message should be identical to the
|
||||
request.
|
||||
'''
|
||||
sub_function_code = 0x0000
|
||||
|
||||
def __init__(self, message=0x0000, **kwargs):
|
||||
''' Initializes a new instance of the response
|
||||
|
||||
:param message: The message to loopback
|
||||
'''
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
if isinstance(message, list):
|
||||
self.message = message
|
||||
else: self.message = [message]
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 01
|
||||
#---------------------------------------------------------------------------#
|
||||
class RestartCommunicationsOptionRequest(DiagnosticStatusRequest):
|
||||
'''
|
||||
The remote device serial line port must be initialized and restarted, and
|
||||
all of its communications event counters are cleared. If the port is
|
||||
currently in Listen Only Mode, no response is returned. This function is
|
||||
the only one that brings the port out of Listen Only Mode. If the port is
|
||||
not currently in Listen Only Mode, a normal response is returned. This
|
||||
occurs before the restart is executed.
|
||||
'''
|
||||
sub_function_code = 0x0001
|
||||
|
||||
def __init__(self, toggle=False, **kwargs):
|
||||
''' Initializes a new request
|
||||
|
||||
:param toggle: Set to True to toggle, False otherwise
|
||||
'''
|
||||
DiagnosticStatusRequest.__init__(self, **kwargs)
|
||||
if toggle:
|
||||
self.message = [ModbusStatus.On]
|
||||
else: self.message = [ModbusStatus.Off]
|
||||
|
||||
def execute(self, *args):
|
||||
''' Clear event log and restart
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
#if _MCB.ListenOnly:
|
||||
return RestartCommunicationsOptionResponse(self.message)
|
||||
|
||||
class RestartCommunicationsOptionResponse(DiagnosticStatusResponse):
|
||||
'''
|
||||
The remote device serial line port must be initialized and restarted, and
|
||||
all of its communications event counters are cleared. If the port is
|
||||
currently in Listen Only Mode, no response is returned. This function is
|
||||
the only one that brings the port out of Listen Only Mode. If the port is
|
||||
not currently in Listen Only Mode, a normal response is returned. This
|
||||
occurs before the restart is executed.
|
||||
'''
|
||||
sub_function_code = 0x0001
|
||||
|
||||
def __init__(self, toggle=False, **kwargs):
|
||||
''' Initializes a new response
|
||||
|
||||
:param toggle: Set to True if we toggled, False otherwise
|
||||
'''
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
if toggle:
|
||||
self.message = [ModbusStatus.On]
|
||||
else: self.message = [ModbusStatus.Off]
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 02
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnDiagnosticRegisterRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The contents of the remote device's 16-bit diagnostic register are
|
||||
returned in the response
|
||||
'''
|
||||
sub_function_code = 0x0002
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
#if _MCB.isListenOnly():
|
||||
register = pack_bitstring(_MCB.getDiagnosticRegister())
|
||||
return ReturnDiagnosticRegisterResponse(register)
|
||||
|
||||
|
||||
class ReturnDiagnosticRegisterResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The contents of the remote device's 16-bit diagnostic register are
|
||||
returned in the response
|
||||
'''
|
||||
sub_function_code = 0x0002
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 03
|
||||
#---------------------------------------------------------------------------#
|
||||
class ChangeAsciiInputDelimiterRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The character 'CHAR' passed in the request data field becomes the end of
|
||||
message delimiter for future messages (replacing the default LF
|
||||
character). This function is useful in cases of a Line Feed is not
|
||||
required at the end of ASCII messages.
|
||||
'''
|
||||
sub_function_code = 0x0003
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
char = (self.message & 0xff00) >> 8
|
||||
_MCB.Delimiter = char
|
||||
return ChangeAsciiInputDelimiterResponse(self.message)
|
||||
|
||||
|
||||
class ChangeAsciiInputDelimiterResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The character 'CHAR' passed in the request data field becomes the end of
|
||||
message delimiter for future messages (replacing the default LF
|
||||
character). This function is useful in cases of a Line Feed is not
|
||||
required at the end of ASCII messages.
|
||||
'''
|
||||
sub_function_code = 0x0003
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 04
|
||||
#---------------------------------------------------------------------------#
|
||||
class ForceListenOnlyModeRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
Forces the addressed remote device to its Listen Only Mode for MODBUS
|
||||
communications. This isolates it from the other devices on the network,
|
||||
allowing them to continue communicating without interruption from the
|
||||
addressed remote device. No response is returned.
|
||||
'''
|
||||
sub_function_code = 0x0004
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
_MCB.ListenOnly = True
|
||||
return ForceListenOnlyModeResponse()
|
||||
|
||||
|
||||
class ForceListenOnlyModeResponse(DiagnosticStatusResponse):
|
||||
'''
|
||||
Forces the addressed remote device to its Listen Only Mode for MODBUS
|
||||
communications. This isolates it from the other devices on the network,
|
||||
allowing them to continue communicating without interruption from the
|
||||
addressed remote device. No response is returned.
|
||||
|
||||
This does not send a response
|
||||
'''
|
||||
sub_function_code = 0x0004
|
||||
should_respond = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializer to block a return response
|
||||
'''
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
self.message = []
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 10
|
||||
#---------------------------------------------------------------------------#
|
||||
class ClearCountersRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The goal is to clear ll counters and the diagnostic register.
|
||||
Also, counters are cleared upon power-up
|
||||
'''
|
||||
sub_function_code = 0x000A
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
_MCB.reset()
|
||||
return ClearCountersResponse(self.message)
|
||||
|
||||
|
||||
class ClearCountersResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The goal is to clear ll counters and the diagnostic register.
|
||||
Also, counters are cleared upon power-up
|
||||
'''
|
||||
sub_function_code = 0x000A
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 11
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnBusMessageCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages that the
|
||||
remote device has detected on the communications systems since its last
|
||||
restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000B
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.BusMessage
|
||||
return ReturnBusMessageCountResponse(count)
|
||||
|
||||
|
||||
class ReturnBusMessageCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages that the
|
||||
remote device has detected on the communications systems since its last
|
||||
restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000B
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 12
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnBusCommunicationErrorCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of CRC errors encountered
|
||||
by the remote device since its last restart, clear counter operation, or
|
||||
power-up
|
||||
'''
|
||||
sub_function_code = 0x000C
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.BusCommunicationError
|
||||
return ReturnBusCommunicationErrorCountResponse(count)
|
||||
|
||||
|
||||
class ReturnBusCommunicationErrorCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of CRC errors encountered
|
||||
by the remote device since its last restart, clear counter operation, or
|
||||
power-up
|
||||
'''
|
||||
sub_function_code = 0x000C
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 13
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnBusExceptionErrorCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of modbus exception
|
||||
responses returned by the remote device since its last restart,
|
||||
clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000D
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.BusExceptionError
|
||||
return ReturnBusExceptionErrorCountResponse(count)
|
||||
|
||||
|
||||
class ReturnBusExceptionErrorCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of modbus exception
|
||||
responses returned by the remote device since its last restart,
|
||||
clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000D
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 14
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnSlaveMessageCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000E
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.SlaveMessage
|
||||
return ReturnSlaveMessageCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveMessageCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000E
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 15
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnSlaveNoResponseCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000F
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.SlaveNoResponse
|
||||
return ReturnSlaveNoReponseCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveNoReponseCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000F
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 16
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnSlaveNAKCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Negative Acknowledge (NAK) exception
|
||||
response, since its last restart, clear counters operation, or power-up.
|
||||
Exception responses are described and listed in section 7 .
|
||||
'''
|
||||
sub_function_code = 0x0010
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.SlaveNAK
|
||||
return ReturnSlaveNAKCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveNAKCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Negative Acknowledge (NAK) exception
|
||||
response, since its last restart, clear counters operation, or power-up.
|
||||
Exception responses are described and listed in section 7.
|
||||
'''
|
||||
sub_function_code = 0x0010
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 17
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnSlaveBusyCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Slave Device Busy exception response,
|
||||
since its last restart, clear counters operation, or power-up.
|
||||
'''
|
||||
sub_function_code = 0x0011
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.SlaveBusy
|
||||
return ReturnSlaveBusyCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveBusyCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Slave Device Busy exception response,
|
||||
since its last restart, clear counters operation, or power-up.
|
||||
'''
|
||||
sub_function_code = 0x0011
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 18
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnSlaveBusCharacterOverrunCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device that it could not handle due to a character overrun condition,
|
||||
since its last restart, clear counters operation, or power-up. A character
|
||||
overrun is caused by data characters arriving at the port faster than they
|
||||
can be stored, or by the loss of a character due to a hardware malfunction.
|
||||
'''
|
||||
sub_function_code = 0x0012
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.BusCharacterOverrun
|
||||
return ReturnSlaveBusCharacterOverrunCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveBusCharacterOverrunCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device that it could not handle due to a character overrun condition,
|
||||
since its last restart, clear counters operation, or power-up. A character
|
||||
overrun is caused by data characters arriving at the port faster than they
|
||||
can be stored, or by the loss of a character due to a hardware malfunction.
|
||||
'''
|
||||
sub_function_code = 0x0012
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 19
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnIopOverrunCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
An IOP overrun is caused by data characters arriving at the port
|
||||
faster than they can be stored, or by the loss of a character due
|
||||
to a hardware malfunction. This function is specific to the 884.
|
||||
'''
|
||||
sub_function_code = 0x0013
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.BusCharacterOverrun
|
||||
return ReturnIopOverrunCountResponse(count)
|
||||
|
||||
|
||||
class ReturnIopOverrunCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages
|
||||
addressed to the slave that it could not handle due to an 884
|
||||
IOP overrun condition, since its last restart, clear counters
|
||||
operation, or power-up.
|
||||
'''
|
||||
sub_function_code = 0x0013
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 20
|
||||
#---------------------------------------------------------------------------#
|
||||
class ClearOverrunCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
Clears the overrun error counter and reset the error flag
|
||||
|
||||
An error flag should be cleared, but nothing else in the
|
||||
specification mentions is, so it is ignored.
|
||||
'''
|
||||
sub_function_code = 0x0014
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
_MCB.Counter.BusCharacterOverrun = 0x0000
|
||||
return ClearOverrunCountResponse(self.message)
|
||||
|
||||
|
||||
class ClearOverrunCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
Clears the overrun error counter and reset the error flag
|
||||
'''
|
||||
sub_function_code = 0x0014
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 21
|
||||
#---------------------------------------------------------------------------#
|
||||
class GetClearModbusPlusRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
In addition to the Function code (08) and Subfunction code
|
||||
(00 15 hex) in the query, a two-byte Operation field is used
|
||||
to specify either a 'Get Statistics' or a 'Clear Statistics'
|
||||
operation. The two operations are exclusive - the 'Get'
|
||||
operation cannot clear the statistics, and the 'Clear'
|
||||
operation does not return statistics prior to clearing
|
||||
them. Statistics are also cleared on power-up of the slave
|
||||
device.
|
||||
'''
|
||||
sub_function_code = 0x0015
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(GetClearModbusPlusRequest, self).__init__(**kwargs)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Returns a series of 54 16-bit words (108 bytes) in the data field of the response
|
||||
(this function differs from the usual two-byte length of the data field). The data
|
||||
contains the statistics for the Modbus Plus peer processor in the slave device.
|
||||
Func_code (1 byte) + Sub function code (2 byte) + Operation (2 byte) + Data (108 bytes)
|
||||
:return:
|
||||
"""
|
||||
if self.message == ModbusPlusOperation.GetStatistics:
|
||||
data = 2 + 108 # byte count(2) + data (54*2)
|
||||
else:
|
||||
data = 0
|
||||
return 1 + 2 + 2 + 2+ data
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
message = None # the clear operation does not return info
|
||||
if self.message == ModbusPlusOperation.ClearStatistics:
|
||||
_MCB.Plus.reset()
|
||||
message = self.message
|
||||
else:
|
||||
message = [self.message]
|
||||
message += _MCB.Plus.encode()
|
||||
return GetClearModbusPlusResponse(message)
|
||||
|
||||
def encode(self):
|
||||
'''
|
||||
Base encoder for a diagnostic response
|
||||
we encode the data set in self.message
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
packet = struct.pack('>H', self.sub_function_code)
|
||||
packet += struct.pack('>H', self.message)
|
||||
return packet
|
||||
|
||||
|
||||
class GetClearModbusPlusResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
Returns a series of 54 16-bit words (108 bytes) in the data field
|
||||
of the response (this function differs from the usual two-byte
|
||||
length of the data field). The data contains the statistics for
|
||||
the Modbus Plus peer processor in the slave device.
|
||||
'''
|
||||
sub_function_code = 0x0015
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"DiagnosticStatusRequest", "DiagnosticStatusResponse",
|
||||
"ReturnQueryDataRequest", "ReturnQueryDataResponse",
|
||||
"RestartCommunicationsOptionRequest", "RestartCommunicationsOptionResponse",
|
||||
"ReturnDiagnosticRegisterRequest", "ReturnDiagnosticRegisterResponse",
|
||||
"ChangeAsciiInputDelimiterRequest", "ChangeAsciiInputDelimiterResponse",
|
||||
"ForceListenOnlyModeRequest", "ForceListenOnlyModeResponse",
|
||||
"ClearCountersRequest", "ClearCountersResponse",
|
||||
"ReturnBusMessageCountRequest", "ReturnBusMessageCountResponse",
|
||||
"ReturnBusCommunicationErrorCountRequest", "ReturnBusCommunicationErrorCountResponse",
|
||||
"ReturnBusExceptionErrorCountRequest", "ReturnBusExceptionErrorCountResponse",
|
||||
"ReturnSlaveMessageCountRequest", "ReturnSlaveMessageCountResponse",
|
||||
"ReturnSlaveNoResponseCountRequest", "ReturnSlaveNoReponseCountResponse",
|
||||
"ReturnSlaveNAKCountRequest", "ReturnSlaveNAKCountResponse",
|
||||
"ReturnSlaveBusyCountRequest", "ReturnSlaveBusyCountResponse",
|
||||
"ReturnSlaveBusCharacterOverrunCountRequest", "ReturnSlaveBusCharacterOverrunCountResponse",
|
||||
"ReturnIopOverrunCountRequest", "ReturnIopOverrunCountResponse",
|
||||
"ClearOverrunCountRequest", "ClearOverrunCountResponse",
|
||||
"GetClearModbusPlusRequest", "GetClearModbusPlusResponse",
|
||||
]
|
||||
@@ -1,197 +0,0 @@
|
||||
'''
|
||||
Modbus Remote Events
|
||||
------------------------------------------------------------
|
||||
|
||||
An event byte returned by the Get Communications Event Log function
|
||||
can be any one of four types. The type is defined by bit 7
|
||||
(the high-order bit) in each byte. It may be further defined by bit 6.
|
||||
'''
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.exceptions import ParameterException
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
|
||||
|
||||
class ModbusEvent(object):
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the status bits to an event message
|
||||
|
||||
:returns: The encoded event message
|
||||
'''
|
||||
raise NotImplementedException()
|
||||
|
||||
def decode(self, event):
|
||||
''' Decodes the event message to its status bits
|
||||
|
||||
:param event: The event to decode
|
||||
'''
|
||||
raise NotImplementedException()
|
||||
|
||||
|
||||
class RemoteReceiveEvent(ModbusEvent):
|
||||
''' Remote device MODBUS Receive Event
|
||||
|
||||
The remote device stores this type of event byte when a query message
|
||||
is received. It is stored before the remote device processes the message.
|
||||
This event is defined by bit 7 set to logic '1'. The other bits will be
|
||||
set to a logic '1' if the corresponding condition is TRUE. The bit layout
|
||||
is::
|
||||
|
||||
Bit Contents
|
||||
----------------------------------
|
||||
0 Not Used
|
||||
2 Not Used
|
||||
3 Not Used
|
||||
4 Character Overrun
|
||||
5 Currently in Listen Only Mode
|
||||
6 Broadcast Receive
|
||||
7 1
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initialize a new event instance
|
||||
'''
|
||||
self.overrun = kwargs.get('overrun', False)
|
||||
self.listen = kwargs.get('listen', False)
|
||||
self.broadcast = kwargs.get('broadcast', False)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the status bits to an event message
|
||||
|
||||
:returns: The encoded event message
|
||||
'''
|
||||
bits = [False] * 3
|
||||
bits += [self.overrun, self.listen, self.broadcast, True]
|
||||
packet = pack_bitstring(bits)
|
||||
return packet
|
||||
|
||||
def decode(self, event):
|
||||
''' Decodes the event message to its status bits
|
||||
|
||||
:param event: The event to decode
|
||||
'''
|
||||
bits = unpack_bitstring(event)
|
||||
self.overrun = bits[4]
|
||||
self.listen = bits[5]
|
||||
self.broadcast = bits[6]
|
||||
|
||||
|
||||
class RemoteSendEvent(ModbusEvent):
|
||||
''' Remote device MODBUS Send Event
|
||||
|
||||
The remote device stores this type of event byte when it finishes
|
||||
processing a request message. It is stored if the remote device
|
||||
returned a normal or exception response, or no response.
|
||||
|
||||
This event is defined by bit 7 set to a logic '0', with bit 6 set to a '1'.
|
||||
The other bits will be set to a logic '1' if the corresponding
|
||||
condition is TRUE. The bit layout is::
|
||||
|
||||
Bit Contents
|
||||
-----------------------------------------------------------
|
||||
0 Read Exception Sent (Exception Codes 1-3)
|
||||
1 Slave Abort Exception Sent (Exception Code 4)
|
||||
2 Slave Busy Exception Sent (Exception Codes 5-6)
|
||||
3 Slave Program NAK Exception Sent (Exception Code 7)
|
||||
4 Write Timeout Error Occurred
|
||||
5 Currently in Listen Only Mode
|
||||
6 1
|
||||
7 0
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initialize a new event instance
|
||||
'''
|
||||
self.read = kwargs.get('read', False)
|
||||
self.slave_abort = kwargs.get('slave_abort', False)
|
||||
self.slave_busy = kwargs.get('slave_busy', False)
|
||||
self.slave_nak = kwargs.get('slave_nak', False)
|
||||
self.write_timeout = kwargs.get('write_timeout', False)
|
||||
self.listen = kwargs.get('listen', False)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the status bits to an event message
|
||||
|
||||
:returns: The encoded event message
|
||||
'''
|
||||
bits = [self.read, self.slave_abort, self.slave_busy,
|
||||
self.slave_nak, self.write_timeout, self.listen]
|
||||
bits += [True, False]
|
||||
packet = pack_bitstring(bits)
|
||||
return packet
|
||||
|
||||
def decode(self, event):
|
||||
''' Decodes the event message to its status bits
|
||||
|
||||
:param event: The event to decode
|
||||
'''
|
||||
# todo fix the start byte count
|
||||
bits = unpack_bitstring(event)
|
||||
self.read = bits[0]
|
||||
self.slave_abort = bits[1]
|
||||
self.slave_busy = bits[2]
|
||||
self.slave_nak = bits[3]
|
||||
self.write_timeout = bits[4]
|
||||
self.listen = bits[5]
|
||||
|
||||
|
||||
class EnteredListenModeEvent(ModbusEvent):
|
||||
''' Remote device Entered Listen Only Mode
|
||||
|
||||
The remote device stores this type of event byte when it enters
|
||||
the Listen Only Mode. The event is defined by a content of 04 hex.
|
||||
'''
|
||||
|
||||
value = 0x04
|
||||
__encoded = b'\x04'
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the status bits to an event message
|
||||
|
||||
:returns: The encoded event message
|
||||
'''
|
||||
return self.__encoded
|
||||
|
||||
def decode(self, event):
|
||||
''' Decodes the event message to its status bits
|
||||
|
||||
:param event: The event to decode
|
||||
'''
|
||||
if event != self.__encoded:
|
||||
raise ParameterException('Invalid decoded value')
|
||||
|
||||
|
||||
class CommunicationRestartEvent(ModbusEvent):
|
||||
''' Remote device Initiated Communication Restart
|
||||
|
||||
The remote device stores this type of event byte when its communications
|
||||
port is restarted. The remote device can be restarted by the Diagnostics
|
||||
function (code 08), with sub-function Restart Communications Option
|
||||
(code 00 01).
|
||||
|
||||
That function also places the remote device into a 'Continue on Error'
|
||||
or 'Stop on Error' mode. If the remote device is placed into 'Continue on
|
||||
Error' mode, the event byte is added to the existing event log. If the
|
||||
remote device is placed into 'Stop on Error' mode, the byte is added to
|
||||
the log and the rest of the log is cleared to zeros.
|
||||
|
||||
The event is defined by a content of zero.
|
||||
'''
|
||||
|
||||
value = 0x00
|
||||
__encoded = b'\x00'
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the status bits to an event message
|
||||
|
||||
:returns: The encoded event message
|
||||
'''
|
||||
return self.__encoded
|
||||
|
||||
def decode(self, event):
|
||||
''' Decodes the event message to its status bits
|
||||
|
||||
:param event: The event to decode
|
||||
'''
|
||||
if event != self.__encoded:
|
||||
raise ParameterException('Invalid decoded value')
|
||||
@@ -1,130 +0,0 @@
|
||||
"""
|
||||
Pymodbus Exceptions
|
||||
--------------------
|
||||
|
||||
Custom exceptions to be used in the Modbus code.
|
||||
"""
|
||||
|
||||
|
||||
class ModbusException(Exception):
|
||||
""" Base modbus exception """
|
||||
|
||||
def __init__(self, string):
|
||||
""" Initialize the exception
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
self.string = string
|
||||
|
||||
def __str__(self):
|
||||
return 'Modbus Error: %s' % self.string
|
||||
|
||||
def isError(self):
|
||||
"""Error"""
|
||||
return True
|
||||
|
||||
|
||||
class ModbusIOException(ModbusException):
|
||||
""" Error resulting from data i/o """
|
||||
|
||||
def __init__(self, string="", function_code=None):
|
||||
""" Initialize the exception
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
self.fcode = function_code
|
||||
self.message = "[Input/Output] %s" % string
|
||||
ModbusException.__init__(self, self.message)
|
||||
|
||||
|
||||
class ParameterException(ModbusException):
|
||||
""" Error resulting from invalid parameter """
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[Invalid Parameter] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class NoSuchSlaveException(ModbusException):
|
||||
""" Error resulting from making a request to a slave
|
||||
that does not exist """
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[No Such Slave] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class NotImplementedException(ModbusException):
|
||||
""" Error resulting from not implemented function """
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[Not Implemented] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class ConnectionException(ModbusException):
|
||||
""" Error resulting from a bad connection """
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[Connection] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class InvalidMessageReceivedException(ModbusException):
|
||||
"""
|
||||
Error resulting from invalid response received or decoded
|
||||
"""
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[Invalid Message] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class MessageRegisterException(ModbusException):
|
||||
"""
|
||||
Error resulting from failing to register a custom message request/response
|
||||
"""
|
||||
def __init__(self, string=""):
|
||||
message = '[Error registering message] %s' % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class TimeOutException(ModbusException):
|
||||
""" Error resulting from modbus response timeout """
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[Timeout] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
__all__ = [
|
||||
"ModbusException", "ModbusIOException",
|
||||
"ParameterException", "NotImplementedException",
|
||||
"ConnectionException", "NoSuchSlaveException",
|
||||
"InvalidMessageReceivedException",
|
||||
"MessageRegisterException", "TimeOutException"
|
||||
]
|
||||
@@ -1,312 +0,0 @@
|
||||
"""
|
||||
Modbus Request/Response Decoder Factories
|
||||
-------------------------------------------
|
||||
|
||||
The following factories make it easy to decode request/response messages.
|
||||
To add a new request/response pair to be decodeable by the library, simply
|
||||
add them to the respective function lookup table (order doesn't matter, but
|
||||
it does help keep things organized).
|
||||
|
||||
Regardless of how many functions are added to the lookup, O(1) behavior is
|
||||
kept as a result of a pre-computed lookup dictionary.
|
||||
"""
|
||||
|
||||
from pymodbus.pdu import IllegalFunctionRequest
|
||||
from pymodbus.pdu import ExceptionResponse
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as ecode
|
||||
from pymodbus.interfaces import IModbusDecoder
|
||||
from pymodbus.exceptions import ModbusException, MessageRegisterException
|
||||
from pymodbus.bit_read_message import *
|
||||
from pymodbus.bit_write_message import *
|
||||
from pymodbus.diag_message import *
|
||||
from pymodbus.file_message import *
|
||||
from pymodbus.other_message import *
|
||||
from pymodbus.mei_message import *
|
||||
from pymodbus.register_read_message import *
|
||||
from pymodbus.register_write_message import *
|
||||
from pymodbus.compat import byte2int
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Server Decoder
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ServerDecoder(IModbusDecoder):
|
||||
""" Request Message Factory (Server)
|
||||
|
||||
To add more implemented functions, simply add them to the list
|
||||
"""
|
||||
__function_table = [
|
||||
ReadHoldingRegistersRequest,
|
||||
ReadDiscreteInputsRequest,
|
||||
ReadInputRegistersRequest,
|
||||
ReadCoilsRequest,
|
||||
WriteMultipleCoilsRequest,
|
||||
WriteMultipleRegistersRequest,
|
||||
WriteSingleRegisterRequest,
|
||||
WriteSingleCoilRequest,
|
||||
ReadWriteMultipleRegistersRequest,
|
||||
DiagnosticStatusRequest,
|
||||
ReadExceptionStatusRequest,
|
||||
GetCommEventCounterRequest,
|
||||
GetCommEventLogRequest,
|
||||
ReportSlaveIdRequest,
|
||||
ReadFileRecordRequest,
|
||||
WriteFileRecordRequest,
|
||||
MaskWriteRegisterRequest,
|
||||
ReadFifoQueueRequest,
|
||||
ReadDeviceInformationRequest,
|
||||
]
|
||||
__sub_function_table = [
|
||||
ReturnQueryDataRequest,
|
||||
RestartCommunicationsOptionRequest,
|
||||
ReturnDiagnosticRegisterRequest,
|
||||
ChangeAsciiInputDelimiterRequest,
|
||||
ForceListenOnlyModeRequest,
|
||||
ClearCountersRequest,
|
||||
ReturnBusMessageCountRequest,
|
||||
ReturnBusCommunicationErrorCountRequest,
|
||||
ReturnBusExceptionErrorCountRequest,
|
||||
ReturnSlaveMessageCountRequest,
|
||||
ReturnSlaveNoResponseCountRequest,
|
||||
ReturnSlaveNAKCountRequest,
|
||||
ReturnSlaveBusyCountRequest,
|
||||
ReturnSlaveBusCharacterOverrunCountRequest,
|
||||
ReturnIopOverrunCountRequest,
|
||||
ClearOverrunCountRequest,
|
||||
GetClearModbusPlusRequest,
|
||||
ReadDeviceInformationRequest,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
""" Initializes the client lookup tables
|
||||
"""
|
||||
functions = set(f.function_code for f in self.__function_table)
|
||||
self.__lookup = dict([(f.function_code, f) for f in self.__function_table])
|
||||
self.__sub_lookup = dict((f, {}) for f in functions)
|
||||
for f in self.__sub_function_table:
|
||||
self.__sub_lookup[f.function_code][f.sub_function_code] = f
|
||||
|
||||
def decode(self, message):
|
||||
""" Wrapper to decode a request packet
|
||||
|
||||
:param message: The raw modbus request packet
|
||||
:return: The decoded modbus message or None if error
|
||||
"""
|
||||
try:
|
||||
return self._helper(message)
|
||||
except ModbusException as er:
|
||||
_logger.warning("Unable to decode request %s" % er)
|
||||
return None
|
||||
|
||||
def lookupPduClass(self, function_code):
|
||||
""" Use `function_code` to determine the class of the PDU.
|
||||
|
||||
:param function_code: The function code specified in a frame.
|
||||
:returns: The class of the PDU that has a matching `function_code`.
|
||||
"""
|
||||
return self.__lookup.get(function_code, ExceptionResponse)
|
||||
|
||||
def _helper(self, data):
|
||||
"""
|
||||
This factory is used to generate the correct request object
|
||||
from a valid request packet. This decodes from a list of the
|
||||
currently implemented request types.
|
||||
|
||||
:param data: The request packet to decode
|
||||
:returns: The decoded request or illegal function request object
|
||||
"""
|
||||
function_code = byte2int(data[0])
|
||||
request = self.__lookup.get(function_code, lambda: None)()
|
||||
if not request:
|
||||
_logger.debug("Factory Request[%d]" % function_code)
|
||||
request = IllegalFunctionRequest(function_code)
|
||||
else:
|
||||
fc_string = "%s: %s" % (
|
||||
str(self.__lookup[function_code]).split('.')[-1].rstrip(
|
||||
"'>"),
|
||||
function_code
|
||||
)
|
||||
_logger.debug("Factory Request[%s]" % fc_string)
|
||||
request.decode(data[1:])
|
||||
|
||||
if hasattr(request, 'sub_function_code'):
|
||||
lookup = self.__sub_lookup.get(request.function_code, {})
|
||||
subtype = lookup.get(request.sub_function_code, None)
|
||||
if subtype: request.__class__ = subtype
|
||||
|
||||
return request
|
||||
|
||||
def register(self, function=None):
|
||||
"""
|
||||
Registers a function and sub function class with the decoder
|
||||
:param function: Custom function class to register
|
||||
:return:
|
||||
"""
|
||||
if function and not issubclass(function, ModbusRequest):
|
||||
raise MessageRegisterException("'{}' is Not a valid Modbus Message"
|
||||
". Class needs to be derived from "
|
||||
"`pymodbus.pdu.ModbusRequest` "
|
||||
"".format(
|
||||
function.__class__.__name__
|
||||
))
|
||||
self.__lookup[function.function_code] = function
|
||||
if hasattr(function, "sub_function_code"):
|
||||
if function.function_code not in self.__sub_lookup:
|
||||
self.__sub_lookup[function.function_code] = dict()
|
||||
self.__sub_lookup[function.function_code][
|
||||
function.sub_function_code] = function
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Client Decoder
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ClientDecoder(IModbusDecoder):
|
||||
""" Response Message Factory (Client)
|
||||
|
||||
To add more implemented functions, simply add them to the list
|
||||
"""
|
||||
__function_table = [
|
||||
ReadHoldingRegistersResponse,
|
||||
ReadDiscreteInputsResponse,
|
||||
ReadInputRegistersResponse,
|
||||
ReadCoilsResponse,
|
||||
WriteMultipleCoilsResponse,
|
||||
WriteMultipleRegistersResponse,
|
||||
WriteSingleRegisterResponse,
|
||||
WriteSingleCoilResponse,
|
||||
ReadWriteMultipleRegistersResponse,
|
||||
DiagnosticStatusResponse,
|
||||
ReadExceptionStatusResponse,
|
||||
GetCommEventCounterResponse,
|
||||
GetCommEventLogResponse,
|
||||
ReportSlaveIdResponse,
|
||||
ReadFileRecordResponse,
|
||||
WriteFileRecordResponse,
|
||||
MaskWriteRegisterResponse,
|
||||
ReadFifoQueueResponse,
|
||||
ReadDeviceInformationResponse,
|
||||
]
|
||||
__sub_function_table = [
|
||||
ReturnQueryDataResponse,
|
||||
RestartCommunicationsOptionResponse,
|
||||
ReturnDiagnosticRegisterResponse,
|
||||
ChangeAsciiInputDelimiterResponse,
|
||||
ForceListenOnlyModeResponse,
|
||||
ClearCountersResponse,
|
||||
ReturnBusMessageCountResponse,
|
||||
ReturnBusCommunicationErrorCountResponse,
|
||||
ReturnBusExceptionErrorCountResponse,
|
||||
ReturnSlaveMessageCountResponse,
|
||||
ReturnSlaveNoReponseCountResponse,
|
||||
ReturnSlaveNAKCountResponse,
|
||||
ReturnSlaveBusyCountResponse,
|
||||
ReturnSlaveBusCharacterOverrunCountResponse,
|
||||
ReturnIopOverrunCountResponse,
|
||||
ClearOverrunCountResponse,
|
||||
GetClearModbusPlusResponse,
|
||||
ReadDeviceInformationResponse,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
""" Initializes the client lookup tables
|
||||
"""
|
||||
functions = set(f.function_code for f in self.__function_table)
|
||||
self.__lookup = dict([(f.function_code, f)
|
||||
for f in self.__function_table])
|
||||
self.__sub_lookup = dict((f, {}) for f in functions)
|
||||
for f in self.__sub_function_table:
|
||||
self.__sub_lookup[f.function_code][f.sub_function_code] = f
|
||||
|
||||
def lookupPduClass(self, function_code):
|
||||
""" Use `function_code` to determine the class of the PDU.
|
||||
|
||||
:param function_code: The function code specified in a frame.
|
||||
:returns: The class of the PDU that has a matching `function_code`.
|
||||
"""
|
||||
return self.__lookup.get(function_code, ExceptionResponse)
|
||||
|
||||
def decode(self, message):
|
||||
""" Wrapper to decode a response packet
|
||||
|
||||
:param message: The raw packet to decode
|
||||
:return: The decoded modbus message or None if error
|
||||
"""
|
||||
try:
|
||||
return self._helper(message)
|
||||
except ModbusException as er:
|
||||
_logger.error("Unable to decode response %s" % er)
|
||||
|
||||
except Exception as ex:
|
||||
_logger.error(ex)
|
||||
return None
|
||||
|
||||
def _helper(self, data):
|
||||
"""
|
||||
This factory is used to generate the correct response object
|
||||
from a valid response packet. This decodes from a list of the
|
||||
currently implemented request types.
|
||||
|
||||
:param data: The response packet to decode
|
||||
:returns: The decoded request or an exception response object
|
||||
"""
|
||||
fc_string = function_code = byte2int(data[0])
|
||||
if function_code in self.__lookup:
|
||||
fc_string = "%s: %s" % (
|
||||
str(self.__lookup[function_code]).split('.')[-1].rstrip("'>"),
|
||||
function_code
|
||||
)
|
||||
_logger.debug("Factory Response[%s]" % fc_string)
|
||||
response = self.__lookup.get(function_code, lambda: None)()
|
||||
if function_code > 0x80:
|
||||
code = function_code & 0x7f # strip error portion
|
||||
response = ExceptionResponse(code, ecode.IllegalFunction)
|
||||
if not response:
|
||||
raise ModbusException("Unknown response %d" % function_code)
|
||||
response.decode(data[1:])
|
||||
|
||||
if hasattr(response, 'sub_function_code'):
|
||||
lookup = self.__sub_lookup.get(response.function_code, {})
|
||||
subtype = lookup.get(response.sub_function_code, None)
|
||||
if subtype: response.__class__ = subtype
|
||||
|
||||
return response
|
||||
|
||||
def register(self, function=None, sub_function=None, force=False):
|
||||
"""
|
||||
Registers a function and sub function class with the decoder
|
||||
:param function: Custom function class to register
|
||||
:param sub_function: Custom sub function class to register
|
||||
:param force: Force update the existing class
|
||||
:return:
|
||||
"""
|
||||
if function and not issubclass(function, ModbusResponse):
|
||||
raise MessageRegisterException("'{}' is Not a valid Modbus Message"
|
||||
". Class needs to be derived from "
|
||||
"`pymodbus.pdu.ModbusResponse` "
|
||||
"".format(
|
||||
function.__class__.__name__
|
||||
))
|
||||
self.__lookup[function.function_code] = function
|
||||
if hasattr(function, "sub_function_code"):
|
||||
if function.function_code not in self.__sub_lookup:
|
||||
self.__sub_lookup[function.function_code] = dict()
|
||||
self.__sub_lookup[function.function_code][
|
||||
function.sub_function_code] = function
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = ['ServerDecoder', 'ClientDecoder']
|
||||
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
'''
|
||||
File Record Read/Write Messages
|
||||
-------------------------------
|
||||
|
||||
Currently none of these messages are implemented
|
||||
'''
|
||||
import struct
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.compat import byte2int
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# File Record Types
|
||||
#---------------------------------------------------------------------------#
|
||||
class FileRecord(object):
|
||||
''' Represents a file record and its relevant data.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:params reference_type: Defaults to 0x06 (must be)
|
||||
:params file_number: Indicates which file number we are reading
|
||||
:params record_number: Indicates which record in the file
|
||||
:params record_data: The actual data of the record
|
||||
:params record_length: The length in registers of the record
|
||||
:params response_length: The length in bytes of the record
|
||||
'''
|
||||
self.reference_type = kwargs.get('reference_type', 0x06)
|
||||
self.file_number = kwargs.get('file_number', 0x00)
|
||||
self.record_number = kwargs.get('record_number', 0x00)
|
||||
self.record_data = kwargs.get('record_data', '')
|
||||
|
||||
self.record_length = kwargs.get('record_length', len(self.record_data) // 2)
|
||||
self.response_length = kwargs.get('response_length', len(self.record_data) + 1)
|
||||
|
||||
def __eq__(self, relf):
|
||||
''' Compares the left object to the right
|
||||
'''
|
||||
return self.reference_type == relf.reference_type \
|
||||
and self.file_number == relf.file_number \
|
||||
and self.record_number == relf.record_number \
|
||||
and self.record_length == relf.record_length \
|
||||
and self.record_data == relf.record_data
|
||||
|
||||
def __ne__(self, relf):
|
||||
''' Compares the left object to the right
|
||||
'''
|
||||
return not self.__eq__(relf)
|
||||
|
||||
def __repr__(self):
|
||||
''' Gives a representation of the file record
|
||||
'''
|
||||
params = (self.file_number, self.record_number, self.record_length)
|
||||
return 'FileRecord(file=%d, record=%d, length=%d)' % params
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# File Requests/Responses
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReadFileRecordRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to perform a file record read. All request
|
||||
data lengths are provided in terms of number of bytes and all record
|
||||
lengths are provided in terms of registers.
|
||||
|
||||
A file is an organization of records. Each file contains 10000 records,
|
||||
addressed 0000 to 9999 decimal or 0x0000 to 0x270f. For example, record
|
||||
12 is addressed as 12. The function can read multiple groups of
|
||||
references. The groups can be separating (non-contiguous), but the
|
||||
references within each group must be sequential. Each group is defined
|
||||
in a seperate 'sub-request' field that contains seven bytes::
|
||||
|
||||
The reference type: 1 byte (must be 0x06)
|
||||
The file number: 2 bytes
|
||||
The starting record number within the file: 2 bytes
|
||||
The length of the record to be read: 2 bytes
|
||||
|
||||
The quantity of registers to be read, combined with all other fields
|
||||
in the expected response, must not exceed the allowable length of the
|
||||
MODBUS PDU: 235 bytes.
|
||||
'''
|
||||
function_code = 0x14
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param records: The file record requests to be read
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The byte encoded packet
|
||||
'''
|
||||
packet = struct.pack('B', len(self.records) * 7)
|
||||
for record in self.records:
|
||||
packet += struct.pack('>BHHH', 0x06, record.file_number,
|
||||
record.record_number, record.record_length)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes the incoming request
|
||||
|
||||
:param data: The data to decode into the address
|
||||
'''
|
||||
self.records = []
|
||||
byte_count = byte2int(data[0])
|
||||
for count in range(1, byte_count, 7):
|
||||
decoded = struct.unpack('>BHHH', data[count:count+7])
|
||||
record = FileRecord(file_number=decoded[1],
|
||||
record_number=decoded[2], record_length=decoded[3])
|
||||
if decoded[0] == 0x06: self.records.append(record)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
'''
|
||||
# TODO do some new context operation here
|
||||
# if file number, record number, or address + length
|
||||
# is too big, return an error.
|
||||
files = []
|
||||
return ReadFileRecordResponse(files)
|
||||
|
||||
|
||||
class ReadFileRecordResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response is a series of 'sub-responses,' one for each
|
||||
'sub-request.' The byte count field is the total combined count of
|
||||
bytes in all 'sub-responses.' In addition, each 'sub-response'
|
||||
contains a field that shows its own byte count.
|
||||
'''
|
||||
function_code = 0x14
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param records: The requested file records
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
total = sum(record.response_length + 1 for record in self.records)
|
||||
packet = struct.pack('B', total)
|
||||
for record in self.records:
|
||||
packet += struct.pack('>BB', 0x06, record.record_length)
|
||||
packet += record.record_data
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
count, self.records = 1, []
|
||||
byte_count = byte2int(data[0])
|
||||
while count < byte_count:
|
||||
response_length, reference_type = struct.unpack('>BB', data[count:count+2])
|
||||
count += response_length + 1 # the count is not included
|
||||
record = FileRecord(response_length=response_length,
|
||||
record_data=data[count - response_length + 1:count])
|
||||
if reference_type == 0x06: self.records.append(record)
|
||||
|
||||
|
||||
class WriteFileRecordRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to perform a file record write. All
|
||||
request data lengths are provided in terms of number of bytes
|
||||
and all record lengths are provided in terms of the number of 16
|
||||
bit words.
|
||||
'''
|
||||
function_code = 0x15
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param records: The file record requests to be read
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The byte encoded packet
|
||||
'''
|
||||
total_length = sum((record.record_length * 2) + 7 for record in self.records)
|
||||
packet = struct.pack('B', total_length)
|
||||
for record in self.records:
|
||||
packet += struct.pack('>BHHH', 0x06, record.file_number,
|
||||
record.record_number, record.record_length)
|
||||
packet += record.record_data
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes the incoming request
|
||||
|
||||
:param data: The data to decode into the address
|
||||
'''
|
||||
count, self.records = 1, []
|
||||
byte_count = byte2int(data[0])
|
||||
while count < byte_count:
|
||||
decoded = struct.unpack('>BHHH', data[count:count+7])
|
||||
response_length = decoded[3] * 2
|
||||
count += response_length + 7
|
||||
record = FileRecord(record_length=decoded[3],
|
||||
file_number=decoded[1], record_number=decoded[2],
|
||||
record_data=data[count - response_length:count])
|
||||
if decoded[0] == 0x06: self.records.append(record)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run the write file record request against the context
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
'''
|
||||
# TODO do some new context operation here
|
||||
# if file number, record number, or address + length
|
||||
# is too big, return an error.
|
||||
return WriteFileRecordResponse(self.records)
|
||||
|
||||
|
||||
class WriteFileRecordResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response is an echo of the request.
|
||||
'''
|
||||
function_code = 0x15
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param records: The file record requests to be read
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
total_length = sum((record.record_length * 2) + 7 for record in self.records)
|
||||
packet = struct.pack('B', total_length)
|
||||
for record in self.records:
|
||||
packet += struct.pack('>BHHH', 0x06, record.file_number,
|
||||
record.record_number, record.record_length)
|
||||
packet += record.record_data
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes the incoming request
|
||||
|
||||
:param data: The data to decode into the address
|
||||
'''
|
||||
count, self.records = 1, []
|
||||
byte_count = byte2int(data[0])
|
||||
while count < byte_count:
|
||||
decoded = struct.unpack('>BHHH', data[count:count+7])
|
||||
response_length = decoded[3] * 2
|
||||
count += response_length + 7
|
||||
record = FileRecord(record_length=decoded[3],
|
||||
file_number=decoded[1], record_number=decoded[2],
|
||||
record_data=data[count - response_length:count])
|
||||
if decoded[0] == 0x06: self.records.append(record)
|
||||
|
||||
|
||||
class ReadFifoQueueRequest(ModbusRequest):
|
||||
'''
|
||||
This function code allows to read the contents of a First-In-First-Out
|
||||
(FIFO) queue of register in a remote device. The function returns a
|
||||
count of the registers in the queue, followed by the queued data.
|
||||
Up to 32 registers can be read: the count, plus up to 31 queued data
|
||||
registers.
|
||||
|
||||
The queue count register is returned first, followed by the queued data
|
||||
registers. The function reads the queue contents, but does not clear
|
||||
them.
|
||||
'''
|
||||
function_code = 0x18
|
||||
_rtu_frame_size = 6
|
||||
|
||||
def __init__(self, address=0x0000, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The fifo pointer address (0x0000 to 0xffff)
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.values = [] # this should be added to the context
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The byte encoded packet
|
||||
'''
|
||||
return struct.pack('>H', self.address)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes the incoming request
|
||||
|
||||
:param data: The data to decode into the address
|
||||
'''
|
||||
self.address = struct.unpack('>H', data)[0]
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
'''
|
||||
if not (0x0000 <= self.address <= 0xffff):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if len(self.values) > 31:
|
||||
return self.doException(merror.IllegalValue)
|
||||
# TODO pull the values from some context
|
||||
return ReadFifoQueueResponse(self.values)
|
||||
|
||||
|
||||
class ReadFifoQueueResponse(ModbusResponse):
|
||||
'''
|
||||
In a normal response, the byte count shows the quantity of bytes to
|
||||
follow, including the queue count bytes and value register bytes
|
||||
(but not including the error check field). The queue count is the
|
||||
quantity of data registers in the queue (not including the count register).
|
||||
|
||||
If the queue count exceeds 31, an exception response is returned with an
|
||||
error code of 03 (Illegal Data Value).
|
||||
'''
|
||||
function_code = 0x18
|
||||
|
||||
@classmethod
|
||||
def calculateRtuFrameSize(cls, buffer):
|
||||
''' Calculates the size of the message
|
||||
|
||||
:param buffer: A buffer containing the data that have been received.
|
||||
:returns: The number of bytes in the response.
|
||||
'''
|
||||
hi_byte = byte2int(buffer[2])
|
||||
lo_byte = byte2int(buffer[3])
|
||||
return (hi_byte << 16) + lo_byte + 6
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param values: The list of values of the fifo to return
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.values = values or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
length = len(self.values) * 2
|
||||
packet = struct.pack('>HH', 2 + length, length)
|
||||
for value in self.values:
|
||||
packet += struct.pack('>H', value)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.values = []
|
||||
_, count = struct.unpack('>HH', data[0:4])
|
||||
for index in range(0, count - 4):
|
||||
idx = 4 + index * 2
|
||||
self.values.append(struct.unpack('>H', data[idx:idx + 2])[0])
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"FileRecord",
|
||||
"ReadFileRecordRequest", "ReadFileRecordResponse",
|
||||
"WriteFileRecordRequest", "WriteFileRecordResponse",
|
||||
"ReadFifoQueueRequest", "ReadFifoQueueResponse",
|
||||
]
|
||||
@@ -1,49 +0,0 @@
|
||||
from pymodbus.interfaces import IModbusFramer
|
||||
import struct
|
||||
|
||||
# Unit ID, Function Code
|
||||
BYTE_ORDER = '>'
|
||||
FRAME_HEADER = 'BB'
|
||||
|
||||
# Transaction Id, Protocol ID, Length, Unit ID, Function Code
|
||||
SOCKET_FRAME_HEADER = BYTE_ORDER + 'HHH' + FRAME_HEADER
|
||||
|
||||
# Function Code
|
||||
TLS_FRAME_HEADER = BYTE_ORDER + 'B'
|
||||
|
||||
class ModbusFramer(IModbusFramer):
|
||||
"""
|
||||
Base Framer class
|
||||
"""
|
||||
|
||||
def _validate_unit_id(self, units, single):
|
||||
"""
|
||||
Validates if the received data is valid for the client
|
||||
:param units: list of unit id for which the transaction is valid
|
||||
:param single: Set to true to treat this as a single context
|
||||
:return: """
|
||||
|
||||
if single:
|
||||
return True
|
||||
else:
|
||||
if 0 in units or 0xFF in units:
|
||||
# Handle Modbus TCP unit identifier (0x00 0r 0xFF)
|
||||
# in asynchronous requests
|
||||
return True
|
||||
return self._header['uid'] in units
|
||||
|
||||
def sendPacket(self, message):
|
||||
"""
|
||||
Sends packets on the bus with 3.5char delay between frames
|
||||
:param message: Message to be sent over the bus
|
||||
:return:
|
||||
"""
|
||||
return self.client.send(message)
|
||||
|
||||
def recvPacket(self, size):
|
||||
"""
|
||||
Receives packet from the bus with specified len
|
||||
:param size: Number of bytes to read
|
||||
:return:
|
||||
"""
|
||||
return self.client.recv(size)
|
||||
@@ -1,208 +0,0 @@
|
||||
import struct
|
||||
from binascii import b2a_hex, a2b_hex
|
||||
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.utilities import checkLRC, computeLRC
|
||||
from pymodbus.framer import ModbusFramer, FRAME_HEADER, BYTE_ORDER
|
||||
|
||||
|
||||
ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus ASCII Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusAsciiFramer(ModbusFramer):
|
||||
"""
|
||||
Modbus ASCII Frame Controller::
|
||||
|
||||
[ Start ][Address ][ Function ][ Data ][ LRC ][ End ]
|
||||
1c 2c 2c Nc 2c 2c
|
||||
|
||||
* data can be 0 - 2x252 chars
|
||||
* end is '\\r\\n' (Carriage return line feed), however the line feed
|
||||
character can be changed via a special command
|
||||
* start is ':'
|
||||
|
||||
This framer is used for serial transmission. Unlike the RTU protocol,
|
||||
the data in this framer is transferred in plain text ascii.
|
||||
"""
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
""" Initializes a new instance of the framer
|
||||
|
||||
:param decoder: The decoder implementation to use
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'lrc': '0000', 'len': 0, 'uid': 0x00}
|
||||
self._hsize = 0x02
|
||||
self._start = b':'
|
||||
self._end = b"\r\n"
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
if len(data) > 1:
|
||||
uid = int(data[1:3], 16)
|
||||
fcode = int(data[3:5], 16)
|
||||
return dict(unit=uid, fcode=fcode)
|
||||
return dict()
|
||||
|
||||
def checkFrame(self):
|
||||
""" Check and decode the next frame
|
||||
|
||||
:returns: True if we successful, False otherwise
|
||||
"""
|
||||
start = self._buffer.find(self._start)
|
||||
if start == -1:
|
||||
return False
|
||||
if start > 0: # go ahead and skip old bad data
|
||||
self._buffer = self._buffer[start:]
|
||||
start = 0
|
||||
|
||||
end = self._buffer.find(self._end)
|
||||
if end != -1:
|
||||
self._header['len'] = end
|
||||
self._header['uid'] = int(self._buffer[1:3], 16)
|
||||
self._header['lrc'] = int(self._buffer[end - 2:end], 16)
|
||||
data = a2b_hex(self._buffer[start + 1:end - 2])
|
||||
return checkLRC(data, self._header['lrc'])
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
""" Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = self._buffer[self._header['len'] + 2:]
|
||||
self._header = {'lrc': '0000', 'len': 0, 'uid': 0x00}
|
||||
|
||||
def isFrameReady(self):
|
||||
""" Check if we should continue decode logic
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > 1
|
||||
|
||||
def addToFrame(self, message):
|
||||
""" Add the next message to the frame buffer
|
||||
This should be used before the decoding while loop to add the received
|
||||
data to the buffer handle.
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
self._buffer += message
|
||||
|
||||
def getFrame(self):
|
||||
""" Get the next frame from the buffer
|
||||
|
||||
:returns: The frame data or ''
|
||||
"""
|
||||
start = self._hsize + 1
|
||||
end = self._header['len'] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
return a2b_hex(buffer)
|
||||
return b''
|
||||
|
||||
def resetFrame(self):
|
||||
""" Reset the entire message frame.
|
||||
This allows us to skip ovver errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'lrc': '0000', 'len': 0, 'uid': 0x00}
|
||||
|
||||
def populateResult(self, result):
|
||||
""" Populates the modbus result header
|
||||
|
||||
The serial packets do not have any header information
|
||||
that is copied.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.unit_id = self._header['uid']
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def processIncomingPacket(self, data, callback, unit, **kwargs):
|
||||
"""
|
||||
The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param unit: Process if unit id matches, ignore otherwise (could be a
|
||||
list of unit ids (server) or single unit id(client/server))
|
||||
:param single: True or False (If True, ignore unit address validation)
|
||||
|
||||
"""
|
||||
if not isinstance(unit, (list, tuple)):
|
||||
unit = [unit]
|
||||
single = kwargs.get('single', False)
|
||||
self.addToFrame(data)
|
||||
while self.isFrameReady():
|
||||
if self.checkFrame():
|
||||
if self._validate_unit_id(unit, single):
|
||||
frame = self.getFrame()
|
||||
result = self.decoder.decode(frame)
|
||||
if result is None:
|
||||
raise ModbusIOException("Unable to decode response")
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer this
|
||||
else:
|
||||
_logger.error("Not a valid unit id - {}, "
|
||||
"ignoring!!".format(self._header['uid']))
|
||||
self.resetFrame()
|
||||
else:
|
||||
break
|
||||
|
||||
def buildPacket(self, message):
|
||||
""" Creates a ready to send modbus packet
|
||||
Built off of a modbus request/response
|
||||
|
||||
:param message: The request/response to send
|
||||
:return: The encoded packet
|
||||
"""
|
||||
encoded = message.encode()
|
||||
buffer = struct.pack(ASCII_FRAME_HEADER, message.unit_id,
|
||||
message.function_code)
|
||||
checksum = computeLRC(encoded + buffer)
|
||||
|
||||
packet = bytearray()
|
||||
params = (message.unit_id, message.function_code)
|
||||
packet.extend(self._start)
|
||||
packet.extend(('%02x%02x' % params).encode())
|
||||
packet.extend(b2a_hex(encoded))
|
||||
packet.extend(('%02x' % checksum).encode())
|
||||
packet.extend(self._end)
|
||||
return bytes(packet).upper()
|
||||
|
||||
|
||||
# __END__
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
import struct
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.utilities import checkCRC, computeCRC
|
||||
from pymodbus.framer import ModbusFramer, FRAME_HEADER, BYTE_ORDER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
BINARY_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus Binary Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusBinaryFramer(ModbusFramer):
|
||||
"""
|
||||
Modbus Binary Frame Controller::
|
||||
|
||||
[ Start ][Address ][ Function ][ Data ][ CRC ][ End ]
|
||||
1b 1b 1b Nb 2b 1b
|
||||
|
||||
* data can be 0 - 2x252 chars
|
||||
* end is '}'
|
||||
* start is '{'
|
||||
|
||||
The idea here is that we implement the RTU protocol, however,
|
||||
instead of using timing for message delimiting, we use start
|
||||
and end of message characters (in this case { and }). Basically,
|
||||
this is a binary framer.
|
||||
|
||||
The only case we have to watch out for is when a message contains
|
||||
the { or } characters. If we encounter these characters, we
|
||||
simply duplicate them. Hopefully we will not encounter those
|
||||
characters that often and will save a little bit of bandwitch
|
||||
without a real-time system.
|
||||
|
||||
Protocol defined by jamod.sourceforge.net.
|
||||
"""
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
""" Initializes a new instance of the framer
|
||||
|
||||
:param decoder: The decoder implementation to use
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'crc': 0x0000, 'len': 0, 'uid': 0x00}
|
||||
self._hsize = 0x01
|
||||
self._start = b'\x7b' # {
|
||||
self._end = b'\x7d' # }
|
||||
self._repeat = [b'}'[0], b'{'[0]] # python3 hack
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
if len(data) > self._hsize:
|
||||
uid = struct.unpack('>B', data[1:2])[0]
|
||||
fcode = struct.unpack('>B', data[2:3])[0]
|
||||
return dict(unit=uid, fcode=fcode)
|
||||
return dict()
|
||||
|
||||
def checkFrame(self):
|
||||
""" Check and decode the next frame
|
||||
|
||||
:returns: True if we are successful, False otherwise
|
||||
"""
|
||||
start = self._buffer.find(self._start)
|
||||
if start == -1:
|
||||
return False
|
||||
if start > 0: # go ahead and skip old bad data
|
||||
self._buffer = self._buffer[start:]
|
||||
|
||||
end = self._buffer.find(self._end)
|
||||
if end != -1:
|
||||
self._header['len'] = end
|
||||
self._header['uid'] = struct.unpack('>B', self._buffer[1:2])[0]
|
||||
self._header['crc'] = struct.unpack('>H', self._buffer[end - 2:end])[0]
|
||||
data = self._buffer[start + 1:end - 2]
|
||||
return checkCRC(data, self._header['crc'])
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
""" Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = self._buffer[self._header['len'] + 2:]
|
||||
self._header = {'crc':0x0000, 'len':0, 'uid':0x00}
|
||||
|
||||
def isFrameReady(self):
|
||||
""" Check if we should continue decode logic
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > 1
|
||||
|
||||
def addToFrame(self, message):
|
||||
""" Add the next message to the frame buffer
|
||||
This should be used before the decoding while loop to add the received
|
||||
data to the buffer handle.
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
self._buffer += message
|
||||
|
||||
def getFrame(self):
|
||||
""" Get the next frame from the buffer
|
||||
|
||||
:returns: The frame data or ''
|
||||
"""
|
||||
start = self._hsize + 1
|
||||
end = self._header['len'] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
return buffer
|
||||
return b''
|
||||
|
||||
def populateResult(self, result):
|
||||
""" Populates the modbus result header
|
||||
|
||||
The serial packets do not have any header information
|
||||
that is copied.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.unit_id = self._header['uid']
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def processIncomingPacket(self, data, callback, unit, **kwargs):
|
||||
"""
|
||||
The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param unit: Process if unit id matches, ignore otherwise (could be a
|
||||
list of unit ids (server) or single unit id(client/server)
|
||||
:param single: True or False (If True, ignore unit address validation)
|
||||
|
||||
"""
|
||||
self.addToFrame(data)
|
||||
if not isinstance(unit, (list, tuple)):
|
||||
unit = [unit]
|
||||
single = kwargs.get('single', False)
|
||||
while self.isFrameReady():
|
||||
if self.checkFrame():
|
||||
if self._validate_unit_id(unit, single):
|
||||
result = self.decoder.decode(self.getFrame())
|
||||
if result is None:
|
||||
raise ModbusIOException("Unable to decode response")
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
else:
|
||||
_logger.debug("Not a valid unit id - {}, "
|
||||
"ignoring!!".format(self._header['uid']))
|
||||
self.resetFrame()
|
||||
break
|
||||
|
||||
else:
|
||||
_logger.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
break
|
||||
|
||||
def buildPacket(self, message):
|
||||
""" Creates a ready to send modbus packet
|
||||
|
||||
:param message: The request/response to send
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
data = self._preflight(message.encode())
|
||||
packet = struct.pack(BINARY_FRAME_HEADER,
|
||||
message.unit_id,
|
||||
message.function_code) + data
|
||||
packet += struct.pack(">H", computeCRC(packet))
|
||||
packet = self._start + packet + self._end
|
||||
return packet
|
||||
|
||||
def _preflight(self, data):
|
||||
"""
|
||||
Preflight buffer test
|
||||
|
||||
This basically scans the buffer for start and end
|
||||
tags and if found, escapes them.
|
||||
|
||||
:param data: The message to escape
|
||||
:returns: the escaped packet
|
||||
"""
|
||||
array = bytearray()
|
||||
for d in data:
|
||||
if d in self._repeat:
|
||||
array.append(d)
|
||||
array.append(d)
|
||||
return bytes(array)
|
||||
|
||||
def resetFrame(self):
|
||||
""" Reset the entire message frame.
|
||||
This allows us to skip ovver errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'crc': 0x0000, 'len': 0, 'uid': 0x00}
|
||||
|
||||
|
||||
# __END__
|
||||
@@ -1,334 +0,0 @@
|
||||
import struct
|
||||
import time
|
||||
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.exceptions import InvalidMessageReceivedException
|
||||
from pymodbus.utilities import checkCRC, computeCRC
|
||||
from pymodbus.utilities import hexlify_packets, ModbusTransactionState
|
||||
from pymodbus.compat import byte2int
|
||||
from pymodbus.framer import ModbusFramer, FRAME_HEADER, BYTE_ORDER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
RTU_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus RTU Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusRtuFramer(ModbusFramer):
|
||||
"""
|
||||
Modbus RTU Frame controller::
|
||||
|
||||
[ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ]
|
||||
3.5 chars 1b 1b Nb 2b 3.5 chars
|
||||
|
||||
Wait refers to the amount of time required to transmit at least x many
|
||||
characters. In this case it is 3.5 characters. Also, if we receive a
|
||||
wait of 1.5 characters at any point, we must trigger an error message.
|
||||
Also, it appears as though this message is little endian. The logic is
|
||||
simplified as the following::
|
||||
|
||||
block-on-read:
|
||||
read until 3.5 delay
|
||||
check for errors
|
||||
decode
|
||||
|
||||
The following table is a listing of the baud wait times for the specified
|
||||
baud rates::
|
||||
|
||||
------------------------------------------------------------------
|
||||
Baud 1.5c (18 bits) 3.5c (38 bits)
|
||||
------------------------------------------------------------------
|
||||
1200 13333.3 us 31666.7 us
|
||||
4800 3333.3 us 7916.7 us
|
||||
9600 1666.7 us 3958.3 us
|
||||
19200 833.3 us 1979.2 us
|
||||
38400 416.7 us 989.6 us
|
||||
------------------------------------------------------------------
|
||||
1 Byte = start + 8 bits + parity + stop = 11 bits
|
||||
(1/Baud)(bits) = delay seconds
|
||||
"""
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
""" Initializes a new instance of the framer
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
|
||||
self._hsize = 0x01
|
||||
self._end = b'\x0d\x0a'
|
||||
self._min_frame_size = 4
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
if len(data) > self._hsize:
|
||||
uid = byte2int(data[0])
|
||||
fcode = byte2int(data[1])
|
||||
return dict(unit=uid, fcode=fcode)
|
||||
return dict()
|
||||
|
||||
def checkFrame(self):
|
||||
"""
|
||||
Check if the next frame is available.
|
||||
Return True if we were successful.
|
||||
|
||||
1. Populate header
|
||||
2. Discard frame if UID does not match
|
||||
"""
|
||||
try:
|
||||
self.populateHeader()
|
||||
frame_size = self._header['len']
|
||||
data = self._buffer[:frame_size - 2]
|
||||
crc = self._header['crc']
|
||||
crc_val = (byte2int(crc[0]) << 8) + byte2int(crc[1])
|
||||
return checkCRC(data, crc_val)
|
||||
except (IndexError, KeyError, struct.error):
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
"""
|
||||
Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
|
||||
self._buffer = self._buffer[self._header['len']:]
|
||||
_logger.debug("Frame advanced, resetting header!!")
|
||||
self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
|
||||
|
||||
def resetFrame(self):
|
||||
"""
|
||||
Reset the entire message frame.
|
||||
This allows us to skip over errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
_logger.debug("Resetting frame - Current Frame in "
|
||||
"buffer - {}".format(hexlify_packets(self._buffer)))
|
||||
self._buffer = b''
|
||||
self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
|
||||
|
||||
def isFrameReady(self):
|
||||
"""
|
||||
Check if we should continue decode logic
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
if len(self._buffer) <= self._hsize:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Frame is ready only if populateHeader() successfully populates crc field which finishes RTU frame
|
||||
# Otherwise, if buffer is not yet long enough, populateHeader() raises IndexError
|
||||
self.populateHeader()
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def populateHeader(self, data=None):
|
||||
"""
|
||||
Try to set the headers `uid`, `len` and `crc`.
|
||||
|
||||
This method examines `self._buffer` and writes meta
|
||||
information into `self._header`.
|
||||
|
||||
Beware that this method will raise an IndexError if
|
||||
`self._buffer` is not yet long enough.
|
||||
"""
|
||||
data = data if data is not None else self._buffer
|
||||
self._header['uid'] = byte2int(data[0])
|
||||
func_code = byte2int(data[1])
|
||||
pdu_class = self.decoder.lookupPduClass(func_code)
|
||||
size = pdu_class.calculateRtuFrameSize(data)
|
||||
self._header['len'] = size
|
||||
|
||||
if len(data) < size:
|
||||
# crc yet not available
|
||||
raise IndexError
|
||||
self._header['crc'] = data[size - 2:size]
|
||||
|
||||
def addToFrame(self, message):
|
||||
"""
|
||||
This should be used before the decoding while loop to add the received
|
||||
data to the buffer handle.
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
self._buffer += message
|
||||
|
||||
def getFrame(self):
|
||||
"""
|
||||
Get the next frame from the buffer
|
||||
|
||||
:returns: The frame data or ''
|
||||
"""
|
||||
start = self._hsize
|
||||
end = self._header['len'] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
_logger.debug("Getting Frame - {}".format(hexlify_packets(buffer)))
|
||||
return buffer
|
||||
return b''
|
||||
|
||||
def populateResult(self, result):
|
||||
"""
|
||||
Populates the modbus result header
|
||||
|
||||
The serial packets do not have any header information
|
||||
that is copied.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.unit_id = self._header['uid']
|
||||
result.transaction_id = self._header['uid']
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def processIncomingPacket(self, data, callback, unit, **kwargs):
|
||||
"""
|
||||
The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param unit: Process if unit id matches, ignore otherwise (could be a
|
||||
list of unit ids (server) or single unit id(client/server)
|
||||
:param single: True or False (If True, ignore unit address validation)
|
||||
|
||||
"""
|
||||
if not isinstance(unit, (list, tuple)):
|
||||
unit = [unit]
|
||||
self.addToFrame(data)
|
||||
single = kwargs.get("single", False)
|
||||
if self.isFrameReady():
|
||||
if self.checkFrame():
|
||||
if self._validate_unit_id(unit, single):
|
||||
self._process(callback)
|
||||
else:
|
||||
_logger.debug("Not a valid unit id - {}, "
|
||||
"ignoring!!".format(self._header['uid']))
|
||||
self.resetFrame()
|
||||
else:
|
||||
_logger.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
else:
|
||||
_logger.debug("Frame - [{}] not ready".format(data))
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""
|
||||
Creates a ready to send modbus packet
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = struct.pack(RTU_FRAME_HEADER,
|
||||
message.unit_id,
|
||||
message.function_code) + data
|
||||
packet += struct.pack(">H", computeCRC(packet))
|
||||
message.transaction_id = message.unit_id # Ensure that transaction is actually the unit id for serial comms
|
||||
return packet
|
||||
|
||||
def sendPacket(self, message):
|
||||
"""
|
||||
Sends packets on the bus with 3.5char delay between frames
|
||||
:param message: Message to be sent over the bus
|
||||
:return:
|
||||
"""
|
||||
start = time.time()
|
||||
timeout = start + self.client.timeout
|
||||
while self.client.state != ModbusTransactionState.IDLE:
|
||||
if self.client.state == ModbusTransactionState.TRANSACTION_COMPLETE:
|
||||
ts = round(time.time(), 6)
|
||||
_logger.debug("Changing state to IDLE - Last Frame End - {}, "
|
||||
"Current Time stamp - {}".format(
|
||||
self.client.last_frame_end, ts)
|
||||
)
|
||||
|
||||
if self.client.last_frame_end:
|
||||
idle_time = self.client.idle_time()
|
||||
if round(ts - idle_time, 6) <= self.client.silent_interval:
|
||||
_logger.debug("Waiting for 3.5 char before next "
|
||||
"send - {} ms".format(
|
||||
self.client.silent_interval * 1000)
|
||||
)
|
||||
time.sleep(self.client.silent_interval)
|
||||
else:
|
||||
# Recovering from last error ??
|
||||
time.sleep(self.client.silent_interval)
|
||||
self.client.state = ModbusTransactionState.IDLE
|
||||
elif self.client.state == ModbusTransactionState.RETRYING:
|
||||
# Simple lets settle down!!!
|
||||
# To check for higher baudrates
|
||||
time.sleep(self.client.timeout)
|
||||
break
|
||||
else:
|
||||
if time.time() > timeout:
|
||||
_logger.debug("Spent more time than the read time out, "
|
||||
"resetting the transaction to IDLE")
|
||||
self.client.state = ModbusTransactionState.IDLE
|
||||
else:
|
||||
_logger.debug("Sleeping")
|
||||
time.sleep(self.client.silent_interval)
|
||||
size = self.client.send(message)
|
||||
self.client.last_frame_end = round(time.time(), 6)
|
||||
return size
|
||||
|
||||
def recvPacket(self, size):
|
||||
"""
|
||||
Receives packet from the bus with specified len
|
||||
:param size: Number of bytes to read
|
||||
:return:
|
||||
"""
|
||||
result = self.client.recv(size)
|
||||
self.client.last_frame_end = round(time.time(), 6)
|
||||
return result
|
||||
|
||||
def _process(self, callback, error=False):
|
||||
"""
|
||||
Process incoming packets irrespective error condition
|
||||
"""
|
||||
data = self.getRawFrame() if error else self.getFrame()
|
||||
result = self.decoder.decode(data)
|
||||
if result is None:
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
elif error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
else:
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def getRawFrame(self):
|
||||
"""
|
||||
Returns the complete buffer
|
||||
"""
|
||||
_logger.debug("Getting Raw Frame - "
|
||||
"{}".format(hexlify_packets(self._buffer)))
|
||||
return self._buffer
|
||||
|
||||
# __END__
|
||||
@@ -1,217 +0,0 @@
|
||||
import struct
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.exceptions import InvalidMessageReceivedException
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from pymodbus.framer import ModbusFramer, SOCKET_FRAME_HEADER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TCP Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusSocketFramer(ModbusFramer):
|
||||
""" Modbus Socket Frame controller
|
||||
|
||||
Before each modbus TCP message is an MBAP header which is used as a
|
||||
message frame. It allows us to easily separate messages as follows::
|
||||
|
||||
[ MBAP Header ] [ Function Code] [ Data ] \
|
||||
[ tid ][ pid ][ length ][ uid ]
|
||||
2b 2b 2b 1b 1b Nb
|
||||
|
||||
while len(message) > 0:
|
||||
tid, pid, length`, uid = struct.unpack(">HHHB", message)
|
||||
request = message[0:7 + length - 1`]
|
||||
message = [7 + length - 1:]
|
||||
|
||||
* length = uid + function code + data
|
||||
* The -1 is to account for the uid byte
|
||||
"""
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
""" Initializes a new instance of the framer
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'tid': 0, 'pid': 0, 'len': 0, 'uid': 0}
|
||||
self._hsize = 0x07
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def checkFrame(self):
|
||||
"""
|
||||
Check and decode the next frame Return true if we were successful
|
||||
"""
|
||||
if self.isFrameReady():
|
||||
(self._header['tid'], self._header['pid'],
|
||||
self._header['len'], self._header['uid']) = struct.unpack(
|
||||
'>HHHB', self._buffer[0:self._hsize])
|
||||
|
||||
# someone sent us an error? ignore it
|
||||
if self._header['len'] < 2:
|
||||
self.advanceFrame()
|
||||
# we have at least a complete message, continue
|
||||
elif len(self._buffer) - self._hsize + 1 >= self._header['len']:
|
||||
return True
|
||||
# we don't have enough of a message yet, wait
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
""" Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
length = self._hsize + self._header['len'] - 1
|
||||
self._buffer = self._buffer[length:]
|
||||
self._header = {'tid': 0, 'pid': 0, 'len': 0, 'uid': 0}
|
||||
|
||||
def isFrameReady(self):
|
||||
""" Check if we should continue decode logic
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder factory know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > self._hsize
|
||||
|
||||
def addToFrame(self, message):
|
||||
""" Adds new packet data to the current frame buffer
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
self._buffer += message
|
||||
|
||||
def getFrame(self):
|
||||
""" Return the next frame from the buffered data
|
||||
|
||||
:returns: The next full frame buffer
|
||||
"""
|
||||
length = self._hsize + self._header['len'] - 1
|
||||
return self._buffer[self._hsize:length]
|
||||
|
||||
def populateResult(self, result):
|
||||
"""
|
||||
Populates the modbus result with the transport specific header
|
||||
information (pid, tid, uid, checksum, etc)
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.transaction_id = self._header['tid']
|
||||
result.protocol_id = self._header['pid']
|
||||
result.unit_id = self._header['uid']
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
if len(data) > self._hsize:
|
||||
tid, pid, length, uid, fcode = struct.unpack(SOCKET_FRAME_HEADER,
|
||||
data[0:self._hsize+1])
|
||||
return dict(tid=tid, pid=pid, length=length, unit=uid, fcode=fcode)
|
||||
return dict()
|
||||
|
||||
def processIncomingPacket(self, data, callback, unit, **kwargs):
|
||||
"""
|
||||
The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param unit: Process if unit id matches, ignore otherwise (could be a
|
||||
list of unit ids (server) or single unit id(client/server)
|
||||
:param single: True or False (If True, ignore unit address validation)
|
||||
:return:
|
||||
"""
|
||||
if not isinstance(unit, (list, tuple)):
|
||||
unit = [unit]
|
||||
single = kwargs.get("single", False)
|
||||
_logger.debug("Processing: " + hexlify_packets(data))
|
||||
self.addToFrame(data)
|
||||
while True:
|
||||
if self.isFrameReady():
|
||||
if self.checkFrame():
|
||||
if self._validate_unit_id(unit, single):
|
||||
self._process(callback)
|
||||
else:
|
||||
_logger.debug("Not a valid unit id - {}, "
|
||||
"ignoring!!".format(self._header['uid']))
|
||||
self.resetFrame()
|
||||
else:
|
||||
_logger.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
else:
|
||||
if len(self._buffer):
|
||||
# Possible error ???
|
||||
if self._header['len'] < 2:
|
||||
self._process(callback, error=True)
|
||||
break
|
||||
|
||||
def _process(self, callback, error=False):
|
||||
"""
|
||||
Process incoming packets irrespective error condition
|
||||
"""
|
||||
data = self.getRawFrame() if error else self.getFrame()
|
||||
result = self.decoder.decode(data)
|
||||
if result is None:
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
elif error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
else:
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def resetFrame(self):
|
||||
"""
|
||||
Reset the entire message frame.
|
||||
This allows us to skip ovver errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'tid': 0, 'pid': 0, 'len': 0, 'uid': 0}
|
||||
|
||||
def getRawFrame(self):
|
||||
"""
|
||||
Returns the complete buffer
|
||||
"""
|
||||
return self._buffer
|
||||
|
||||
def buildPacket(self, message):
|
||||
""" Creates a ready to send modbus packet
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = struct.pack(SOCKET_FRAME_HEADER,
|
||||
message.transaction_id,
|
||||
message.protocol_id,
|
||||
len(data) + 2,
|
||||
message.unit_id,
|
||||
message.function_code)
|
||||
packet += data
|
||||
return packet
|
||||
|
||||
|
||||
# __END__
|
||||
@@ -1,185 +0,0 @@
|
||||
import struct
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.exceptions import InvalidMessageReceivedException
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from pymodbus.framer import ModbusFramer, TLS_FRAME_HEADER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TLS Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusTlsFramer(ModbusFramer):
|
||||
""" Modbus TLS Frame controller
|
||||
|
||||
No prefix MBAP header before decrypted PDU is used as a message frame for
|
||||
Modbus Security Application Protocol. It allows us to easily separate
|
||||
decrypted messages which is PDU as follows:
|
||||
|
||||
[ Function Code] [ Data ]
|
||||
1b Nb
|
||||
"""
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
""" Initializes a new instance of the framer
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {}
|
||||
self._hsize = 0x0
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def checkFrame(self):
|
||||
"""
|
||||
Check and decode the next frame Return true if we were successful
|
||||
"""
|
||||
if self.isFrameReady():
|
||||
# we have at least a complete message, continue
|
||||
if len(self._buffer) - self._hsize >= 1:
|
||||
return True
|
||||
# we don't have enough of a message yet, wait
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
""" Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {}
|
||||
|
||||
def isFrameReady(self):
|
||||
""" Check if we should continue decode logic
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder factory know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > self._hsize
|
||||
|
||||
def addToFrame(self, message):
|
||||
""" Adds new packet data to the current frame buffer
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
self._buffer += message
|
||||
|
||||
def getFrame(self):
|
||||
""" Return the next frame from the buffered data
|
||||
|
||||
:returns: The next full frame buffer
|
||||
"""
|
||||
return self._buffer[self._hsize:]
|
||||
|
||||
def populateResult(self, result):
|
||||
"""
|
||||
Populates the modbus result with the transport specific header
|
||||
information (no header before PDU in decrypted message)
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
return
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
if len(data) > self._hsize:
|
||||
(fcode,) = struct.unpack(TLS_FRAME_HEADER, data[0:self._hsize+1])
|
||||
return dict(fcode=fcode)
|
||||
return dict()
|
||||
|
||||
def processIncomingPacket(self, data, callback, unit, **kwargs):
|
||||
"""
|
||||
The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param unit: Process if unit id matches, ignore otherwise (could be a
|
||||
list of unit ids (server) or single unit id(client/server)
|
||||
:param single: True or False (If True, ignore unit address validation)
|
||||
:return:
|
||||
"""
|
||||
if not isinstance(unit, (list, tuple)):
|
||||
unit = [unit]
|
||||
# no unit id for Modbus Security Application Protocol
|
||||
single = kwargs.get("single", True)
|
||||
_logger.debug("Processing: " + hexlify_packets(data))
|
||||
self.addToFrame(data)
|
||||
|
||||
if self.isFrameReady():
|
||||
if self.checkFrame():
|
||||
if self._validate_unit_id(unit, single):
|
||||
self._process(callback)
|
||||
else:
|
||||
_logger.debug("Not in valid unit id - {}, "
|
||||
"ignoring!!".format(unit))
|
||||
self.resetFrame()
|
||||
else:
|
||||
_logger.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
|
||||
def _process(self, callback, error=False):
|
||||
"""
|
||||
Process incoming packets irrespective error condition
|
||||
"""
|
||||
data = self.getRawFrame() if error else self.getFrame()
|
||||
result = self.decoder.decode(data)
|
||||
if result is None:
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
elif error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
else:
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def resetFrame(self):
|
||||
"""
|
||||
Reset the entire message frame.
|
||||
This allows us to skip ovver errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
self._buffer = b''
|
||||
|
||||
def getRawFrame(self):
|
||||
"""
|
||||
Returns the complete buffer
|
||||
"""
|
||||
return self._buffer
|
||||
|
||||
def buildPacket(self, message):
|
||||
""" Creates a ready to send modbus packet
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = struct.pack(TLS_FRAME_HEADER, message.function_code)
|
||||
packet += data
|
||||
return packet
|
||||
|
||||
# __END__
|
||||
@@ -1,248 +0,0 @@
|
||||
"""
|
||||
Pymodbus Interfaces
|
||||
---------------------
|
||||
|
||||
A collection of base classes that are used throughout
|
||||
the pymodbus library.
|
||||
"""
|
||||
from pymodbus.exceptions import (NotImplementedException,
|
||||
MessageRegisterException)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Generic
|
||||
# --------------------------------------------------------------------------- #
|
||||
class Singleton(object):
|
||||
"""
|
||||
Singleton base class
|
||||
http://mail.python.org/pipermail/python-list/2007-July/450681.html
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
""" Create a new instance
|
||||
"""
|
||||
if '_inst' not in vars(cls):
|
||||
cls._inst = object.__new__(cls)
|
||||
return cls._inst
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Project Specific
|
||||
# --------------------------------------------------------------------------- #
|
||||
class IModbusDecoder(object):
|
||||
""" Modbus Decoder Base Class
|
||||
|
||||
This interface must be implemented by a modbus message
|
||||
decoder factory. These factories are responsible for
|
||||
abstracting away converting a raw packet into a request / response
|
||||
message object.
|
||||
"""
|
||||
|
||||
def decode(self, message):
|
||||
""" Wrapper to decode a given packet
|
||||
|
||||
:param message: The raw modbus request packet
|
||||
:return: The decoded modbus message or None if error
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def lookupPduClass(self, function_code):
|
||||
""" Use `function_code` to determine the class of the PDU.
|
||||
|
||||
:param function_code: The function code specified in a frame.
|
||||
:returns: The class of the PDU that has a matching `function_code`.
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def register(self, function=None):
|
||||
"""
|
||||
Registers a function and sub function class with the decoder
|
||||
:param function: Custom function class to register
|
||||
:return:
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
|
||||
class IModbusFramer(object):
|
||||
"""
|
||||
A framer strategy interface. The idea is that we abstract away all the
|
||||
detail about how to detect if a current message frame exists, decoding
|
||||
it, sending it, etc so that we can plug in a new Framer object (tcp,
|
||||
rtu, ascii).
|
||||
"""
|
||||
|
||||
def checkFrame(self):
|
||||
""" Check and decode the next frame
|
||||
|
||||
:returns: True if we successful, False otherwise
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def advanceFrame(self):
|
||||
""" Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def addToFrame(self, message):
|
||||
""" Add the next message to the frame buffer
|
||||
|
||||
This should be used before the decoding while loop to add the received
|
||||
data to the buffer handle.
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def isFrameReady(self):
|
||||
""" Check if we should continue decode logic
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def getFrame(self):
|
||||
""" Get the next frame from the buffer
|
||||
|
||||
:returns: The frame data or ''
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def populateResult(self, result):
|
||||
""" Populates the modbus result with current frame header
|
||||
|
||||
We basically copy the data back over from the current header
|
||||
to the result header. This may not be needed for serial messages.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def processIncomingPacket(self, data, callback):
|
||||
""" The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 / N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def buildPacket(self, message):
|
||||
""" Creates a ready to send modbus packet
|
||||
|
||||
The raw packet is built off of a fully populated modbus
|
||||
request / response message.
|
||||
|
||||
:param message: The request/response to send
|
||||
:returns: The built packet
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
|
||||
class IModbusSlaveContext(object):
|
||||
"""
|
||||
Interface for a modbus slave data context
|
||||
|
||||
Derived classes must implemented the following methods:
|
||||
reset(self)
|
||||
validate(self, fx, address, count=1)
|
||||
getValues(self, fx, address, count=1)
|
||||
setValues(self, fx, address, values)
|
||||
"""
|
||||
__fx_mapper = {2: 'd', 4: 'i'}
|
||||
__fx_mapper.update([(i, 'h') for i in [3, 6, 16, 22, 23]])
|
||||
__fx_mapper.update([(i, 'c') for i in [1, 5, 15]])
|
||||
|
||||
def decode(self, fx):
|
||||
""" Converts the function code to the datastore to
|
||||
|
||||
:param fx: The function we are working with
|
||||
:returns: one of [d(iscretes),i(nputs),h(olding),c(oils)
|
||||
"""
|
||||
return self.__fx_mapper[fx]
|
||||
|
||||
def reset(self):
|
||||
""" Resets all the datastores to their default values
|
||||
"""
|
||||
raise NotImplementedException("Context Reset")
|
||||
|
||||
def validate(self, fx, address, count=1):
|
||||
""" Validates the request to make sure it is in range
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
"""
|
||||
raise NotImplementedException("validate context values")
|
||||
|
||||
def getValues(self, fx, address, count=1):
|
||||
""" Get `count` values from datastore
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
"""
|
||||
raise NotImplementedException("get context values")
|
||||
|
||||
def setValues(self, fx, address, values):
|
||||
""" Sets the datastore with the supplied values
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
"""
|
||||
raise NotImplementedException("set context values")
|
||||
|
||||
|
||||
class IPayloadBuilder(object):
|
||||
"""
|
||||
This is an interface to a class that can build a payload
|
||||
for a modbus register write command. It should abstract
|
||||
the codec for encoding data to the required format
|
||||
(bcd, binary, char, etc).
|
||||
"""
|
||||
|
||||
def build(self):
|
||||
""" Return the payload buffer as a list
|
||||
|
||||
This list is two bytes per element and can
|
||||
thus be treated as a list of registers.
|
||||
|
||||
:returns: The payload buffer as a list
|
||||
"""
|
||||
raise NotImplementedException("set context values")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
__all__ = [
|
||||
'Singleton',
|
||||
'IModbusDecoder', 'IModbusFramer', 'IModbusSlaveContext',
|
||||
'IPayloadBuilder',
|
||||
]
|
||||
@@ -1,42 +0,0 @@
|
||||
'''
|
||||
A collection of twisted utility code
|
||||
'''
|
||||
from pymodbus.compat import IS_PYTHON2, IS_PYTHON3
|
||||
if IS_PYTHON2:
|
||||
from twisted.cred import portal, checkers
|
||||
from twisted.conch import manhole, manhole_ssh
|
||||
from twisted.conch.insults import insults
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Twisted Helper Methods
|
||||
#---------------------------------------------------------------------------#
|
||||
def InstallManagementConsole(namespace, users={'admin': 'admin'}, port=503):
|
||||
''' Helper method to start an ssh management console
|
||||
for the modbus server.
|
||||
|
||||
:param namespace: The data to constrain the server to
|
||||
:param users: The users to login with
|
||||
:param port: The port to host the server on
|
||||
'''
|
||||
if IS_PYTHON3:
|
||||
raise NotImplemented("This code currently doesn't work on python3")
|
||||
from twisted.internet import reactor
|
||||
|
||||
def build_protocol():
|
||||
p = insults.ServerProtocol(manhole.ColoredManhole, namespace)
|
||||
return p
|
||||
|
||||
r = manhole_ssh.TerminalRealm()
|
||||
r.chainedProtocolFactory = build_protocol
|
||||
c = checkers.InMemoryUsernamePasswordDatabaseDontUse(**users)
|
||||
p = portal.Portal(r, [c])
|
||||
factory = manhole_ssh.ConchFactory(p)
|
||||
reactor.listenTCP(port, factory)
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
'''
|
||||
Encapsulated Interface (MEI) Transport Messages
|
||||
-----------------------------------------------
|
||||
|
||||
'''
|
||||
import struct
|
||||
from pymodbus.constants import DeviceInformation, MoreData
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.device import ModbusControlBlock
|
||||
from pymodbus.device import DeviceInformationFactory
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.compat import iteritems, byte2int, IS_PYTHON3
|
||||
|
||||
_MCB = ModbusControlBlock()
|
||||
|
||||
|
||||
class _OutOfSpaceException(Exception):
|
||||
# This exception exists here as a simple, local way to manage response
|
||||
# length control for the only MODBUS command which requires it under
|
||||
# standard, non-error conditions. It and the structures associated with
|
||||
# it should ideally be refactored and applied to all responses, however,
|
||||
# since a Client can make requests which result in disallowed conditions,
|
||||
# such as, for instance, requesting a register read of more registers
|
||||
# than will fit in a single PDU. As per the specification, the PDU is
|
||||
# restricted to 253 bytes, irrespective of the transport used.
|
||||
#
|
||||
# See Page 5/50 of MODBUS Application Protocol Specification V1.1b3.
|
||||
def __init__(self, oid):
|
||||
self.oid = oid
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Read Device Information
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReadDeviceInformationRequest(ModbusRequest):
|
||||
'''
|
||||
This function code allows reading the identification and additional
|
||||
information relative to the physical and functional description of a
|
||||
remote device, only.
|
||||
|
||||
The Read Device Identification interface is modeled as an address space
|
||||
composed of a set of addressable data elements. The data elements are
|
||||
called objects and an object Id identifies them.
|
||||
'''
|
||||
function_code = 0x2b
|
||||
sub_function_code = 0x0e
|
||||
_rtu_frame_size = 7
|
||||
|
||||
def __init__(self, read_code=None, object_id=0x00, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param read_code: The device information read code
|
||||
:param object_id: The object to read from
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.read_code = read_code or DeviceInformation.Basic
|
||||
self.object_id = object_id
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The byte encoded packet
|
||||
'''
|
||||
packet = struct.pack('>BBB', self.sub_function_code,
|
||||
self.read_code, self.object_id)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
'''
|
||||
params = struct.unpack('>BBB', data)
|
||||
self.sub_function_code, self.read_code, self.object_id = params
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
'''
|
||||
if not (0x00 <= self.object_id <= 0xff):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not (0x00 <= self.read_code <= 0x04):
|
||||
return self.doException(merror.IllegalValue)
|
||||
|
||||
information = DeviceInformationFactory.get(_MCB,
|
||||
self.read_code, self.object_id)
|
||||
return ReadDeviceInformationResponse(self.read_code, information)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the request
|
||||
|
||||
:returns: The string representation of the request
|
||||
'''
|
||||
params = (self.read_code, self.object_id)
|
||||
return "ReadDeviceInformationRequest(%d,%d)" % params
|
||||
|
||||
|
||||
class ReadDeviceInformationResponse(ModbusResponse):
|
||||
'''
|
||||
'''
|
||||
function_code = 0x2b
|
||||
sub_function_code = 0x0e
|
||||
|
||||
@classmethod
|
||||
def calculateRtuFrameSize(cls, buffer):
|
||||
''' Calculates the size of the message
|
||||
|
||||
:param buffer: A buffer containing the data that have been received.
|
||||
:returns: The number of bytes in the response.
|
||||
'''
|
||||
size = 8 # skip the header information
|
||||
count = byte2int(buffer[7])
|
||||
|
||||
while count > 0:
|
||||
_, object_length = struct.unpack('>BB', buffer[size:size+2])
|
||||
size += object_length + 2
|
||||
count -= 1
|
||||
return size + 2
|
||||
|
||||
def __init__(self, read_code=None, information=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param read_code: The device information read code
|
||||
:param information: The requested information request
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.read_code = read_code or DeviceInformation.Basic
|
||||
self.information = information or {}
|
||||
self.number_of_objects = 0
|
||||
self.conformity = 0x83 # I support everything right now
|
||||
self.next_object_id = 0x00
|
||||
self.more_follows = MoreData.Nothing
|
||||
self.space_left = None
|
||||
|
||||
def _encode_object(self, object_id, data):
|
||||
self.space_left -= (2 + len(data))
|
||||
if self.space_left <= 0:
|
||||
raise _OutOfSpaceException(object_id)
|
||||
encoded_obj = struct.pack('>BB', object_id, len(data))
|
||||
if IS_PYTHON3:
|
||||
if isinstance(data, bytes):
|
||||
encoded_obj += data
|
||||
else:
|
||||
encoded_obj += data.encode()
|
||||
else:
|
||||
encoded_obj += data.encode()
|
||||
self.number_of_objects += 1
|
||||
return encoded_obj
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
packet = struct.pack('>BBB', self.sub_function_code,
|
||||
self.read_code, self.conformity)
|
||||
self.space_left = 253 - 6
|
||||
objects = b''
|
||||
try:
|
||||
for (object_id, data) in iteritems(self.information):
|
||||
if isinstance(data, list):
|
||||
for item in data:
|
||||
objects += self._encode_object(object_id, item)
|
||||
else:
|
||||
objects += self._encode_object(object_id, data)
|
||||
except _OutOfSpaceException as e:
|
||||
self.next_object_id = e.oid
|
||||
self.more_follows = MoreData.KeepReading
|
||||
|
||||
packet += struct.pack('>BBB', self.more_follows, self.next_object_id,
|
||||
self.number_of_objects)
|
||||
packet += objects
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
params = struct.unpack('>BBBBBB', data[0:6])
|
||||
self.sub_function_code, self.read_code = params[0:2]
|
||||
self.conformity, self.more_follows = params[2:4]
|
||||
self.next_object_id, self.number_of_objects = params[4:6]
|
||||
self.information, count = {}, 6 # skip the header information
|
||||
|
||||
while count < len(data):
|
||||
object_id, object_length = struct.unpack('>BB', data[count:count+2])
|
||||
count += object_length + 2
|
||||
if object_id not in self.information.keys():
|
||||
self.information[object_id] = data[count-object_length:count]
|
||||
else:
|
||||
if isinstance(self.information[object_id], list):
|
||||
self.information[object_id].append(data[count-object_length:count])
|
||||
else:
|
||||
self.information[object_id] = [self.information[object_id],
|
||||
data[count - object_length:count]]
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the response
|
||||
|
||||
:returns: The string representation of the response
|
||||
'''
|
||||
return "ReadDeviceInformationResponse(%d)" % self.read_code
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ReadDeviceInformationRequest", "ReadDeviceInformationResponse",
|
||||
]
|
||||
@@ -1,453 +0,0 @@
|
||||
'''
|
||||
Diagnostic record read/write
|
||||
|
||||
Currently not all implemented
|
||||
'''
|
||||
import struct
|
||||
from pymodbus.constants import ModbusStatus
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.device import ModbusControlBlock, DeviceInformationFactory
|
||||
from pymodbus.compat import byte2int, int2byte
|
||||
|
||||
_MCB = ModbusControlBlock()
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReadExceptionStatusRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to read the contents of eight Exception Status
|
||||
outputs in a remote device. The function provides a simple method for
|
||||
accessing this information, because the Exception Output references are
|
||||
known (no output reference is needed in the function).
|
||||
'''
|
||||
function_code = 0x07
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the message
|
||||
'''
|
||||
return b''
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
'''
|
||||
pass
|
||||
|
||||
def execute(self, context=None):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:returns: The populated response
|
||||
'''
|
||||
status = _MCB.Counter.summary()
|
||||
return ReadExceptionStatusResponse(status)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the request
|
||||
|
||||
:returns: The string representation of the request
|
||||
'''
|
||||
return "ReadExceptionStatusRequest(%d)" % (self.function_code)
|
||||
|
||||
|
||||
class ReadExceptionStatusResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response contains the status of the eight Exception Status
|
||||
outputs. The outputs are packed into one data byte, with one bit
|
||||
per output. The status of the lowest output reference is contained
|
||||
in the least significant bit of the byte. The contents of the eight
|
||||
Exception Status outputs are device specific.
|
||||
'''
|
||||
function_code = 0x07
|
||||
_rtu_frame_size = 5
|
||||
|
||||
def __init__(self, status=0x00, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param status: The status response to report
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.status = status
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
return struct.pack('>B', self.status)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.status = byte2int(data[0])
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the response
|
||||
|
||||
:returns: The string representation of the response
|
||||
'''
|
||||
arguments = (self.function_code, self.status)
|
||||
return "ReadExceptionStatusResponse(%d, %s)" % arguments
|
||||
|
||||
# Encapsulate interface transport 43, 14
|
||||
# CANopen general reference 43, 13
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial
|
||||
#---------------------------------------------------------------------------#
|
||||
class GetCommEventCounterRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to get a status word and an event count from
|
||||
the remote device's communication event counter.
|
||||
|
||||
By fetching the current count before and after a series of messages, a
|
||||
client can determine whether the messages were handled normally by the
|
||||
remote device.
|
||||
|
||||
The device's event counter is incremented once for each successful
|
||||
message completion. It is not incremented for exception responses,
|
||||
poll commands, or fetch event counter commands.
|
||||
|
||||
The event counter can be reset by means of the Diagnostics function
|
||||
(code 08), with a subfunction of Restart Communications Option
|
||||
(code 00 01) or Clear Counters and Diagnostic Register (code 00 0A).
|
||||
'''
|
||||
function_code = 0x0b
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the message
|
||||
'''
|
||||
return b''
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
'''
|
||||
pass
|
||||
|
||||
def execute(self, context=None):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:returns: The populated response
|
||||
'''
|
||||
status = _MCB.Counter.Event
|
||||
return GetCommEventCounterResponse(status)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the request
|
||||
|
||||
:returns: The string representation of the request
|
||||
'''
|
||||
return "GetCommEventCounterRequest(%d)" % (self.function_code)
|
||||
|
||||
|
||||
class GetCommEventCounterResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response contains a two-byte status word, and a two-byte
|
||||
event count. The status word will be all ones (FF FF hex) if a
|
||||
previously-issued program command is still being processed by the
|
||||
remote device (a busy condition exists). Otherwise, the status word
|
||||
will be all zeros.
|
||||
'''
|
||||
function_code = 0x0b
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, count=0x0000, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param count: The current event counter value
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.count = count
|
||||
self.status = True # this means we are ready, not waiting
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
if self.status: ready = ModbusStatus.Ready
|
||||
else: ready = ModbusStatus.Waiting
|
||||
return struct.pack('>HH', ready, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
ready, self.count = struct.unpack('>HH', data)
|
||||
self.status = (ready == ModbusStatus.Ready)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the response
|
||||
|
||||
:returns: The string representation of the response
|
||||
'''
|
||||
arguments = (self.function_code, self.count, self.status)
|
||||
return "GetCommEventCounterResponse(%d, %d, %d)" % arguments
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial
|
||||
#---------------------------------------------------------------------------#
|
||||
class GetCommEventLogRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to get a status word, event count, message
|
||||
count, and a field of event bytes from the remote device.
|
||||
|
||||
The status word and event counts are identical to that returned by
|
||||
the Get Communications Event Counter function (11, 0B hex).
|
||||
|
||||
The message counter contains the quantity of messages processed by the
|
||||
remote device since its last restart, clear counters operation, or
|
||||
power-up. This count is identical to that returned by the Diagnostic
|
||||
function (code 08), sub-function Return Bus Message Count (code 11,
|
||||
0B hex).
|
||||
|
||||
The event bytes field contains 0-64 bytes, with each byte corresponding
|
||||
to the status of one MODBUS send or receive operation for the remote
|
||||
device. The remote device enters the events into the field in
|
||||
chronological order. Byte 0 is the most recent event. Each new byte
|
||||
flushes the oldest byte from the field.
|
||||
'''
|
||||
function_code = 0x0c
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the message
|
||||
'''
|
||||
return b''
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
'''
|
||||
pass
|
||||
|
||||
def execute(self, context=None):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:returns: The populated response
|
||||
'''
|
||||
results = {
|
||||
'status' : True,
|
||||
'message_count' : _MCB.Counter.BusMessage,
|
||||
'event_count' : _MCB.Counter.Event,
|
||||
'events' : _MCB.getEvents(),
|
||||
}
|
||||
return GetCommEventLogResponse(**results)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the request
|
||||
|
||||
:returns: The string representation of the request
|
||||
'''
|
||||
return "GetCommEventLogRequest(%d)" % self.function_code
|
||||
|
||||
|
||||
class GetCommEventLogResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response contains a two-byte status word field,
|
||||
a two-byte event count field, a two-byte message count field,
|
||||
and a field containing 0-64 bytes of events. A byte count
|
||||
field defines the total length of the data in these four field
|
||||
'''
|
||||
function_code = 0x0c
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param status: The status response to report
|
||||
:param message_count: The current message count
|
||||
:param event_count: The current event count
|
||||
:param events: The collection of events to send
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.status = kwargs.get('status', True)
|
||||
self.message_count = kwargs.get('message_count', 0)
|
||||
self.event_count = kwargs.get('event_count', 0)
|
||||
self.events = kwargs.get('events', [])
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
if self.status: ready = ModbusStatus.Ready
|
||||
else: ready = ModbusStatus.Waiting
|
||||
packet = struct.pack('>B', 6 + len(self.events))
|
||||
packet += struct.pack('>H', ready)
|
||||
packet += struct.pack('>HH', self.event_count, self.message_count)
|
||||
packet += b''.join(struct.pack('>B', e) for e in self.events)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
length = byte2int(data[0])
|
||||
status = struct.unpack('>H', data[1:3])[0]
|
||||
self.status = (status == ModbusStatus.Ready)
|
||||
self.event_count = struct.unpack('>H', data[3:5])[0]
|
||||
self.message_count = struct.unpack('>H', data[5:7])[0]
|
||||
|
||||
self.events = []
|
||||
for e in range(7, length + 1):
|
||||
self.events.append(byte2int(data[e]))
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the response
|
||||
|
||||
:returns: The string representation of the response
|
||||
'''
|
||||
arguments = (self.function_code, self.status, self.message_count, self.event_count)
|
||||
return "GetCommEventLogResponse(%d, %d, %d, %d)" % arguments
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReportSlaveIdRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to read the description of the type, the
|
||||
current status, and other information specific to a remote device.
|
||||
'''
|
||||
function_code = 0x11
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the message
|
||||
'''
|
||||
return b''
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
'''
|
||||
pass
|
||||
|
||||
def execute(self, context=None):
|
||||
''' Run a report slave id request against the store
|
||||
|
||||
:returns: The populated response
|
||||
'''
|
||||
reportSlaveIdData = None
|
||||
if context:
|
||||
reportSlaveIdData = getattr(context, 'reportSlaveIdData', None)
|
||||
if not reportSlaveIdData:
|
||||
information = DeviceInformationFactory.get(_MCB)
|
||||
identifier = "-".join(information.values()).encode()
|
||||
identifier = identifier or b'Pymodbus'
|
||||
reportSlaveIdData = identifier
|
||||
return ReportSlaveIdResponse(reportSlaveIdData)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the request
|
||||
|
||||
:returns: The string representation of the request
|
||||
'''
|
||||
return "ReportSlaveIdRequest(%d)" % self.function_code
|
||||
|
||||
|
||||
class ReportSlaveIdResponse(ModbusResponse):
|
||||
'''
|
||||
The format of a normal response is shown in the following example.
|
||||
The data contents are specific to each type of device.
|
||||
'''
|
||||
function_code = 0x11
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, identifier=b'\x00', status=True, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param identifier: The identifier of the slave
|
||||
:param status: The status response to report
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.identifier = identifier
|
||||
self.status = status
|
||||
self.byte_count = None
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
if self.status:
|
||||
status = ModbusStatus.SlaveOn
|
||||
else:
|
||||
status = ModbusStatus.SlaveOff
|
||||
length = len(self.identifier) + 1
|
||||
packet = int2byte(length)
|
||||
packet += self.identifier # we assume it is already encoded
|
||||
packet += int2byte(status)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
Since the identifier is device dependent, we just return the
|
||||
raw value that a user can decode to whatever it should be.
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.byte_count = byte2int(data[0])
|
||||
self.identifier = data[1:self.byte_count + 1]
|
||||
status = byte2int(data[-1])
|
||||
self.status = status == ModbusStatus.SlaveOn
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the response
|
||||
|
||||
:returns: The string representation of the response
|
||||
'''
|
||||
arguments = (self.function_code, self.identifier, self.status)
|
||||
return "ReportSlaveIdResponse(%s, %s, %s)" % arguments
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial
|
||||
#---------------------------------------------------------------------------#
|
||||
# report device identification 43, 14
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ReadExceptionStatusRequest", "ReadExceptionStatusResponse",
|
||||
"GetCommEventCounterRequest", "GetCommEventCounterResponse",
|
||||
"GetCommEventLogRequest", "GetCommEventLogResponse",
|
||||
"ReportSlaveIdRequest", "ReportSlaveIdResponse",
|
||||
]
|
||||
@@ -1,509 +0,0 @@
|
||||
"""
|
||||
Modbus Payload Builders
|
||||
------------------------
|
||||
|
||||
A collection of utilities for building and decoding
|
||||
modbus messages payloads.
|
||||
|
||||
|
||||
"""
|
||||
from struct import pack, unpack
|
||||
from pymodbus.interfaces import IPayloadBuilder
|
||||
from pymodbus.constants import Endian
|
||||
from pymodbus.utilities import pack_bitstring
|
||||
from pymodbus.utilities import unpack_bitstring
|
||||
from pymodbus.utilities import make_byte_string
|
||||
from pymodbus.exceptions import ParameterException
|
||||
from pymodbus.compat import unicode_string, IS_PYTHON3, PYTHON_VERSION
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
WC = {
|
||||
"b": 1,
|
||||
"h": 2,
|
||||
"e": 2,
|
||||
"i": 4,
|
||||
"l": 4,
|
||||
"q": 8,
|
||||
"f": 4,
|
||||
"d": 8
|
||||
}
|
||||
|
||||
|
||||
class BinaryPayloadBuilder(IPayloadBuilder):
|
||||
"""
|
||||
A utility that helps build payload messages to be
|
||||
written with the various modbus messages. It really is just
|
||||
a simple wrapper around the struct module, however it saves
|
||||
time looking up the format strings. What follows is a simple
|
||||
example::
|
||||
|
||||
builder = BinaryPayloadBuilder(byteorder=Endian.Little)
|
||||
builder.add_8bit_uint(1)
|
||||
builder.add_16bit_uint(2)
|
||||
payload = builder.build()
|
||||
"""
|
||||
|
||||
def __init__(self, payload=None, byteorder=Endian.Little,
|
||||
wordorder=Endian.Big, repack=False):
|
||||
""" Initialize a new instance of the payload builder
|
||||
|
||||
:param payload: Raw binary payload data to initialize with
|
||||
:param byteorder: The endianess of the bytes in the words
|
||||
:param wordorder: The endianess of the word (when wordcount is >= 2)
|
||||
:param repack: Repack the provided payload based on BO
|
||||
"""
|
||||
self._payload = payload or []
|
||||
self._byteorder = byteorder
|
||||
self._wordorder = wordorder
|
||||
self._repack = repack
|
||||
|
||||
def _pack_words(self, fstring, value):
|
||||
"""
|
||||
Packs Words based on the word order and byte order
|
||||
|
||||
# ---------------------------------------------- #
|
||||
# pack in to network ordered value #
|
||||
# unpack in to network ordered unsigned integer #
|
||||
# Change Word order if little endian word order #
|
||||
# Pack values back based on correct byte order #
|
||||
# ---------------------------------------------- #
|
||||
|
||||
:param value: Value to be packed
|
||||
:return:
|
||||
"""
|
||||
value = pack("!{}".format(fstring), value)
|
||||
wc = WC.get(fstring.lower())//2
|
||||
up = "!{}H".format(wc)
|
||||
payload = unpack(up, value)
|
||||
|
||||
if self._wordorder == Endian.Little:
|
||||
payload = list(reversed(payload))
|
||||
|
||||
fstring = self._byteorder + "H"
|
||||
payload = [pack(fstring, word) for word in payload]
|
||||
payload = b''.join(payload)
|
||||
|
||||
return payload
|
||||
|
||||
def to_string(self):
|
||||
""" Return the payload buffer as a string
|
||||
|
||||
:returns: The payload buffer as a string
|
||||
"""
|
||||
return b''.join(self._payload)
|
||||
|
||||
def __str__(self):
|
||||
""" Return the payload buffer as a string
|
||||
|
||||
:returns: The payload buffer as a string
|
||||
"""
|
||||
return self.to_string().decode('utf-8')
|
||||
|
||||
def reset(self):
|
||||
""" Reset the payload buffer
|
||||
"""
|
||||
self._payload = []
|
||||
|
||||
def to_registers(self):
|
||||
""" Convert the payload buffer into a register
|
||||
layout that can be used as a context block.
|
||||
|
||||
:returns: The register layout to use as a block
|
||||
"""
|
||||
# fstring = self._byteorder+'H'
|
||||
fstring = '!H'
|
||||
payload = self.build()
|
||||
if self._repack:
|
||||
payload = [unpack(self._byteorder+"H", value)[0] for value in payload]
|
||||
else:
|
||||
payload = [unpack(fstring, value)[0] for value in payload]
|
||||
_logger.debug(payload)
|
||||
return payload
|
||||
|
||||
def to_coils(self):
|
||||
"""Convert the payload buffer into a coil
|
||||
layout that can be used as a context block.
|
||||
|
||||
:returns: The coil layout to use as a block
|
||||
"""
|
||||
payload = self.to_registers()
|
||||
coils = [bool(int(bit)) for reg
|
||||
in payload for bit in format(reg, '016b')]
|
||||
return coils
|
||||
|
||||
def build(self):
|
||||
""" Return the payload buffer as a list
|
||||
|
||||
This list is two bytes per element and can
|
||||
thus be treated as a list of registers.
|
||||
|
||||
:returns: The payload buffer as a list
|
||||
"""
|
||||
string = self.to_string()
|
||||
length = len(string)
|
||||
string = string + (b'\x00' * (length % 2))
|
||||
return [string[i:i+2] for i in range(0, length, 2)]
|
||||
|
||||
def add_bits(self, values):
|
||||
""" Adds a collection of bits to be encoded
|
||||
|
||||
If these are less than a multiple of eight,
|
||||
they will be left padded with 0 bits to make
|
||||
it so.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
value = pack_bitstring(values)
|
||||
self._payload.append(value)
|
||||
|
||||
def add_8bit_uint(self, value):
|
||||
""" Adds a 8 bit unsigned int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + 'B'
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_16bit_uint(self, value):
|
||||
""" Adds a 16 bit unsigned int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + 'H'
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_32bit_uint(self, value):
|
||||
""" Adds a 32 bit unsigned int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'I'
|
||||
# fstring = self._byteorder + 'I'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_64bit_uint(self, value):
|
||||
""" Adds a 64 bit unsigned int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'Q'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_8bit_int(self, value):
|
||||
""" Adds a 8 bit signed int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + 'b'
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_16bit_int(self, value):
|
||||
""" Adds a 16 bit signed int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + 'h'
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_32bit_int(self, value):
|
||||
""" Adds a 32 bit signed int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'i'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_64bit_int(self, value):
|
||||
""" Adds a 64 bit signed int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'q'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_16bit_float(self, value):
|
||||
""" Adds a 16 bit float to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
if IS_PYTHON3 and PYTHON_VERSION.minor >= 6:
|
||||
fstring = 'e'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
else:
|
||||
_logger.warning("float16 only supported on python3.6 and above!!!")
|
||||
|
||||
def add_32bit_float(self, value):
|
||||
""" Adds a 32 bit float to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'f'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_64bit_float(self, value):
|
||||
""" Adds a 64 bit float(double) to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'd'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_string(self, value):
|
||||
""" Adds a string to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
value = make_byte_string(value)
|
||||
fstring = self._byteorder + str(len(value)) + 's'
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
|
||||
class BinaryPayloadDecoder(object):
|
||||
"""
|
||||
A utility that helps decode payload messages from a modbus
|
||||
response message. It really is just a simple wrapper around
|
||||
the struct module, however it saves time looking up the format
|
||||
strings. What follows is a simple example::
|
||||
|
||||
decoder = BinaryPayloadDecoder(payload)
|
||||
first = decoder.decode_8bit_uint()
|
||||
second = decoder.decode_16bit_uint()
|
||||
"""
|
||||
|
||||
def __init__(self, payload, byteorder=Endian.Little, wordorder=Endian.Big):
|
||||
""" Initialize a new payload decoder
|
||||
|
||||
:param payload: The payload to decode with
|
||||
:param byteorder: The endianess of the payload
|
||||
:param wordorder: The endianess of the word (when wordcount is >= 2)
|
||||
"""
|
||||
self._payload = payload
|
||||
self._pointer = 0x00
|
||||
self._byteorder = byteorder
|
||||
self._wordorder = wordorder
|
||||
|
||||
@classmethod
|
||||
def fromRegisters(klass, registers, byteorder=Endian.Little,
|
||||
wordorder=Endian.Big):
|
||||
""" Initialize a payload decoder with the result of
|
||||
reading a collection of registers from a modbus device.
|
||||
|
||||
The registers are treated as a list of 2 byte values.
|
||||
We have to do this because of how the data has already
|
||||
been decoded by the rest of the library.
|
||||
|
||||
:param registers: The register results to initialize with
|
||||
:param byteorder: The Byte order of each word
|
||||
:param wordorder: The endianess of the word (when wordcount is >= 2)
|
||||
:returns: An initialized PayloadDecoder
|
||||
"""
|
||||
_logger.debug(registers)
|
||||
if isinstance(registers, list): # repack into flat binary
|
||||
payload = b''.join(pack('!H', x) for x in registers)
|
||||
return klass(payload, byteorder, wordorder)
|
||||
raise ParameterException('Invalid collection of registers supplied')
|
||||
|
||||
@classmethod
|
||||
def bit_chunks(cls, coils, size=8):
|
||||
chunks = [coils[i: i + size] for i in range(0, len(coils), size)]
|
||||
return chunks
|
||||
|
||||
@classmethod
|
||||
def fromCoils(klass, coils, byteorder=Endian.Little, wordorder=Endian.Big):
|
||||
""" Initialize a payload decoder with the result of
|
||||
reading a collection of coils from a modbus device.
|
||||
|
||||
The coils are treated as a list of bit(boolean) values.
|
||||
|
||||
:param coils: The coil results to initialize with
|
||||
:param byteorder: The endianess of the payload
|
||||
:returns: An initialized PayloadDecoder
|
||||
"""
|
||||
if isinstance(coils, list):
|
||||
payload = b''
|
||||
padding = len(coils) % 8
|
||||
if padding: # Pad zero's
|
||||
extra = [False] * padding
|
||||
coils = extra + coils
|
||||
chunks = klass.bit_chunks(coils)
|
||||
for chunk in chunks:
|
||||
payload += pack_bitstring(chunk[::-1])
|
||||
return klass(payload, byteorder)
|
||||
raise ParameterException('Invalid collection of coils supplied')
|
||||
|
||||
def _unpack_words(self, fstring, handle):
|
||||
"""
|
||||
Un Packs Words based on the word order and byte order
|
||||
|
||||
# ---------------------------------------------- #
|
||||
# Unpack in to network ordered unsigned integer #
|
||||
# Change Word order if little endian word order #
|
||||
# Pack values back based on correct byte order #
|
||||
# ---------------------------------------------- #
|
||||
:param handle: Value to be unpacked
|
||||
:return:
|
||||
"""
|
||||
handle = make_byte_string(handle)
|
||||
wc = WC.get(fstring.lower()) // 2
|
||||
up = "!{}H".format(wc)
|
||||
handle = unpack(up, handle)
|
||||
if self._wordorder == Endian.Little:
|
||||
handle = list(reversed(handle))
|
||||
|
||||
# Repack as unsigned Integer
|
||||
pk = self._byteorder + 'H'
|
||||
handle = [pack(pk, p) for p in handle]
|
||||
_logger.debug(handle)
|
||||
handle = b''.join(handle)
|
||||
return handle
|
||||
|
||||
def reset(self):
|
||||
""" Reset the decoder pointer back to the start
|
||||
"""
|
||||
self._pointer = 0x00
|
||||
|
||||
def decode_8bit_uint(self):
|
||||
""" Decodes a 8 bit unsigned int from the buffer
|
||||
"""
|
||||
self._pointer += 1
|
||||
fstring = self._byteorder + 'B'
|
||||
handle = self._payload[self._pointer - 1:self._pointer]
|
||||
handle = make_byte_string(handle)
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_bits(self):
|
||||
""" Decodes a byte worth of bits from the buffer
|
||||
"""
|
||||
self._pointer += 1
|
||||
# fstring = self._endian + 'B'
|
||||
handle = self._payload[self._pointer - 1:self._pointer]
|
||||
handle = make_byte_string(handle)
|
||||
return unpack_bitstring(handle)
|
||||
|
||||
def decode_16bit_uint(self):
|
||||
""" Decodes a 16 bit unsigned int from the buffer
|
||||
"""
|
||||
self._pointer += 2
|
||||
fstring = self._byteorder + 'H'
|
||||
handle = self._payload[self._pointer - 2:self._pointer]
|
||||
handle = make_byte_string(handle)
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_32bit_uint(self):
|
||||
""" Decodes a 32 bit unsigned int from the buffer
|
||||
"""
|
||||
self._pointer += 4
|
||||
fstring = 'I'
|
||||
# fstring = 'I'
|
||||
handle = self._payload[self._pointer - 4:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_64bit_uint(self):
|
||||
""" Decodes a 64 bit unsigned int from the buffer
|
||||
"""
|
||||
self._pointer += 8
|
||||
fstring = 'Q'
|
||||
handle = self._payload[self._pointer - 8:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_8bit_int(self):
|
||||
""" Decodes a 8 bit signed int from the buffer
|
||||
"""
|
||||
self._pointer += 1
|
||||
fstring = self._byteorder + 'b'
|
||||
handle = self._payload[self._pointer - 1:self._pointer]
|
||||
handle = make_byte_string(handle)
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_16bit_int(self):
|
||||
""" Decodes a 16 bit signed int from the buffer
|
||||
"""
|
||||
self._pointer += 2
|
||||
fstring = self._byteorder + 'h'
|
||||
handle = self._payload[self._pointer - 2:self._pointer]
|
||||
handle = make_byte_string(handle)
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_32bit_int(self):
|
||||
""" Decodes a 32 bit signed int from the buffer
|
||||
"""
|
||||
self._pointer += 4
|
||||
fstring = 'i'
|
||||
handle = self._payload[self._pointer - 4:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_64bit_int(self):
|
||||
""" Decodes a 64 bit signed int from the buffer
|
||||
"""
|
||||
self._pointer += 8
|
||||
fstring = 'q'
|
||||
handle = self._payload[self._pointer - 8:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_16bit_float(self):
|
||||
""" Decodes a 16 bit float from the buffer
|
||||
"""
|
||||
if IS_PYTHON3 and PYTHON_VERSION.minor >= 6:
|
||||
self._pointer += 2
|
||||
fstring = 'e'
|
||||
handle = self._payload[self._pointer - 2:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
else:
|
||||
_logger.warning("float16 only supported on python3.6 and above!!!")
|
||||
|
||||
def decode_32bit_float(self):
|
||||
""" Decodes a 32 bit float from the buffer
|
||||
"""
|
||||
self._pointer += 4
|
||||
fstring = 'f'
|
||||
handle = self._payload[self._pointer - 4:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_64bit_float(self):
|
||||
""" Decodes a 64 bit float(double) from the buffer
|
||||
"""
|
||||
self._pointer += 8
|
||||
fstring = 'd'
|
||||
handle = self._payload[self._pointer - 8:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_string(self, size=1):
|
||||
""" Decodes a string from the buffer
|
||||
|
||||
:param size: The size of the string to decode
|
||||
"""
|
||||
self._pointer += size
|
||||
s = self._payload[self._pointer - size:self._pointer]
|
||||
return s
|
||||
|
||||
def skip_bytes(self, nbytes):
|
||||
""" Skip n bytes in the buffer
|
||||
|
||||
:param nbytes: The number of bytes to skip
|
||||
"""
|
||||
self._pointer += nbytes
|
||||
return None
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported Identifiers
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = ["BinaryPayloadBuilder", "BinaryPayloadDecoder"]
|
||||
@@ -1,247 +0,0 @@
|
||||
"""
|
||||
Contains base classes for modbus request/response/error packets
|
||||
"""
|
||||
from pymodbus.interfaces import Singleton
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.utilities import rtuFrameSize
|
||||
from pymodbus.compat import iteritems, int2byte, byte2int
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Base PDU's
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusPDU(object):
|
||||
"""
|
||||
Base class for all Modbus messages
|
||||
|
||||
.. attribute:: transaction_id
|
||||
|
||||
This value is used to uniquely identify a request
|
||||
response pair. It can be implemented as a simple counter
|
||||
|
||||
.. attribute:: protocol_id
|
||||
|
||||
This is a constant set at 0 to indicate Modbus. It is
|
||||
put here for ease of expansion.
|
||||
|
||||
.. attribute:: unit_id
|
||||
|
||||
This is used to route the request to the correct child. In
|
||||
the TCP modbus, it is used for routing (or not used at all. However,
|
||||
for the serial versions, it is used to specify which child to perform
|
||||
the requests against. The value 0x00 represents the broadcast address
|
||||
(also 0xff).
|
||||
|
||||
.. attribute:: check
|
||||
|
||||
This is used for LRC/CRC in the serial modbus protocols
|
||||
|
||||
.. attribute:: skip_encode
|
||||
|
||||
This is used when the message payload has already been encoded.
|
||||
Generally this will occur when the PayloadBuilder is being used
|
||||
to create a complicated message. By setting this to True, the
|
||||
request will pass the currently encoded message through instead
|
||||
of encoding it again.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Initializes the base data for a modbus request """
|
||||
self.transaction_id = kwargs.get('transaction', Defaults.TransactionId)
|
||||
self.protocol_id = kwargs.get('protocol', Defaults.ProtocolId)
|
||||
self.unit_id = kwargs.get('unit', Defaults.UnitId)
|
||||
self.skip_encode = kwargs.get('skip_encode', False)
|
||||
self.check = 0x0000
|
||||
|
||||
def encode(self):
|
||||
""" Encodes the message
|
||||
|
||||
:raises: A not implemented exception
|
||||
"""
|
||||
raise NotImplementedException()
|
||||
|
||||
def decode(self, data):
|
||||
""" Decodes data part of the message.
|
||||
|
||||
:param data: is a string object
|
||||
:raises: A not implemented exception
|
||||
"""
|
||||
raise NotImplementedException()
|
||||
|
||||
@classmethod
|
||||
def calculateRtuFrameSize(cls, buffer):
|
||||
""" Calculates the size of a PDU.
|
||||
|
||||
:param buffer: A buffer containing the data that have been received.
|
||||
:returns: The number of bytes in the PDU.
|
||||
"""
|
||||
if hasattr(cls, '_rtu_frame_size'):
|
||||
return cls._rtu_frame_size
|
||||
elif hasattr(cls, '_rtu_byte_count_pos'):
|
||||
return rtuFrameSize(buffer, cls._rtu_byte_count_pos)
|
||||
else: raise NotImplementedException(
|
||||
"Cannot determine RTU frame size for %s" % cls.__name__)
|
||||
|
||||
|
||||
class ModbusRequest(ModbusPDU):
|
||||
""" Base class for a modbus request PDU """
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Proxy to the lower level initializer """
|
||||
ModbusPDU.__init__(self, **kwargs)
|
||||
|
||||
def doException(self, exception):
|
||||
""" Builds an error response based on the function
|
||||
|
||||
:param exception: The exception to return
|
||||
:raises: An exception response
|
||||
"""
|
||||
exc = ExceptionResponse(self.function_code, exception)
|
||||
_logger.error(exc)
|
||||
return exc
|
||||
|
||||
|
||||
class ModbusResponse(ModbusPDU):
|
||||
""" Base class for a modbus response PDU
|
||||
|
||||
.. attribute:: should_respond
|
||||
|
||||
A flag that indicates if this response returns a result back
|
||||
to the client issuing the request
|
||||
|
||||
.. attribute:: _rtu_frame_size
|
||||
|
||||
Indicates the size of the modbus rtu response used for
|
||||
calculating how much to read.
|
||||
"""
|
||||
|
||||
should_respond = True
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Proxy to the lower level initializer """
|
||||
ModbusPDU.__init__(self, **kwargs)
|
||||
|
||||
def isError(self):
|
||||
"""Checks if the error is a success or failure"""
|
||||
return self.function_code > 0x80
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exception PDU's
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusExceptions(Singleton):
|
||||
"""
|
||||
An enumeration of the valid modbus exceptions
|
||||
"""
|
||||
IllegalFunction = 0x01
|
||||
IllegalAddress = 0x02
|
||||
IllegalValue = 0x03
|
||||
SlaveFailure = 0x04
|
||||
Acknowledge = 0x05
|
||||
SlaveBusy = 0x06
|
||||
MemoryParityError = 0x08
|
||||
GatewayPathUnavailable = 0x0A
|
||||
GatewayNoResponse = 0x0B
|
||||
|
||||
@classmethod
|
||||
def decode(cls, code):
|
||||
""" Given an error code, translate it to a
|
||||
string error name.
|
||||
|
||||
:param code: The code number to translate
|
||||
"""
|
||||
values = dict((v, k) for k, v in iteritems(cls.__dict__)
|
||||
if not k.startswith('__') and not callable(v))
|
||||
return values.get(code, None)
|
||||
|
||||
|
||||
class ExceptionResponse(ModbusResponse):
|
||||
""" Base class for a modbus exception PDU """
|
||||
ExceptionOffset = 0x80
|
||||
_rtu_frame_size = 5
|
||||
|
||||
def __init__(self, function_code, exception_code=None, **kwargs):
|
||||
""" Initializes the modbus exception response
|
||||
|
||||
:param function_code: The function to build an exception response for
|
||||
:param exception_code: The specific modbus exception to return
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.original_code = function_code
|
||||
self.function_code = function_code | self.ExceptionOffset
|
||||
self.exception_code = exception_code
|
||||
|
||||
def encode(self):
|
||||
""" Encodes a modbus exception response
|
||||
|
||||
:returns: The encoded exception packet
|
||||
"""
|
||||
return int2byte(self.exception_code)
|
||||
|
||||
def decode(self, data):
|
||||
""" Decodes a modbus exception response
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.exception_code = byte2int(data[0])
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a representation of an exception response
|
||||
|
||||
:returns: The string representation of an exception response
|
||||
"""
|
||||
message = ModbusExceptions.decode(self.exception_code)
|
||||
parameters = (self.function_code, self.original_code, message)
|
||||
return "Exception Response(%d, %d, %s)" % parameters
|
||||
|
||||
|
||||
class IllegalFunctionRequest(ModbusRequest):
|
||||
"""
|
||||
Defines the Modbus slave exception type 'Illegal Function'
|
||||
This exception code is returned if the slave::
|
||||
|
||||
- does not implement the function code **or**
|
||||
- is not in a state that allows it to process the function
|
||||
"""
|
||||
ErrorCode = 1
|
||||
|
||||
def __init__(self, function_code, **kwargs):
|
||||
""" Initializes a IllegalFunctionRequest
|
||||
|
||||
:param function_code: The function we are erroring on
|
||||
"""
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.function_code = function_code
|
||||
|
||||
def decode(self, data):
|
||||
""" This is here so this failure will run correctly
|
||||
|
||||
:param data: Not used
|
||||
"""
|
||||
pass
|
||||
|
||||
def execute(self, context):
|
||||
""" Builds an illegal function request error response
|
||||
|
||||
:param context: The current context for the message
|
||||
:returns: The error response packet
|
||||
"""
|
||||
return ExceptionResponse(self.function_code, self.ErrorCode)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ModbusRequest', 'ModbusResponse', 'ModbusExceptions',
|
||||
'ExceptionResponse', 'IllegalFunctionRequest',
|
||||
]
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
'''
|
||||
Register Reading Request/Response
|
||||
---------------------------------
|
||||
'''
|
||||
import struct
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.compat import int2byte, byte2int
|
||||
|
||||
|
||||
class ReadRegistersRequestBase(ModbusRequest):
|
||||
'''
|
||||
Base class for reading a modbus register
|
||||
'''
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address, count, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start the read from
|
||||
:param count: The number of registers to read
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:return: The encoded packet
|
||||
'''
|
||||
return struct.pack('>HH', self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a register request packet
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.address, self.count = struct.unpack('>HH', data)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Byte Count(1 byte) + 2 * Quantity of Coils (n Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 1 + 2 * self.count
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "ReadRegisterRequest (%d,%d)" % (self.address, self.count)
|
||||
|
||||
|
||||
class ReadRegistersResponseBase(ModbusResponse):
|
||||
'''
|
||||
Base class for responsing to a modbus register read
|
||||
'''
|
||||
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, values, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param values: The values to write to
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.registers = values or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response packet
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
result = int2byte(len(self.registers) * 2)
|
||||
for register in self.registers:
|
||||
result += struct.pack('>H', register)
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a register response packet
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
byte_count = byte2int(data[0])
|
||||
self.registers = []
|
||||
for i in range(1, byte_count + 1, 2):
|
||||
self.registers.append(struct.unpack('>H', data[i:i + 2])[0])
|
||||
|
||||
def getRegister(self, index):
|
||||
''' Get the requested register
|
||||
|
||||
:param index: The indexed register to retrieve
|
||||
:returns: The request register
|
||||
'''
|
||||
return self.registers[index]
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "%s (%d)" % (self.__class__.__name__, len(self.registers))
|
||||
|
||||
|
||||
class ReadHoldingRegistersRequest(ReadRegistersRequestBase):
|
||||
'''
|
||||
This function code is used to read the contents of a contiguous block
|
||||
of holding registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore registers numbered
|
||||
1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 3
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Initializes a new instance of the request
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of registers to read from address
|
||||
'''
|
||||
ReadRegistersRequestBase.__init__(self, address, count, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read holding request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.count <= 0x7d):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
return ReadHoldingRegistersResponse(values)
|
||||
|
||||
|
||||
class ReadHoldingRegistersResponse(ReadRegistersResponseBase):
|
||||
'''
|
||||
This function code is used to read the contents of a contiguous block
|
||||
of holding registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore registers numbered
|
||||
1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 3
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Initializes a new response instance
|
||||
|
||||
:param values: The resulting register values
|
||||
'''
|
||||
ReadRegistersResponseBase.__init__(self, values, **kwargs)
|
||||
|
||||
|
||||
class ReadInputRegistersRequest(ReadRegistersRequestBase):
|
||||
'''
|
||||
This function code is used to read from 1 to approx. 125 contiguous
|
||||
input registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore input registers
|
||||
numbered 1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 4
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Initializes a new instance of the request
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of registers to read from address
|
||||
'''
|
||||
ReadRegistersRequestBase.__init__(self, address, count, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read input request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.count <= 0x7d):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
return ReadInputRegistersResponse(values)
|
||||
|
||||
|
||||
class ReadInputRegistersResponse(ReadRegistersResponseBase):
|
||||
'''
|
||||
This function code is used to read from 1 to approx. 125 contiguous
|
||||
input registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore input registers
|
||||
numbered 1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 4
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Initializes a new response instance
|
||||
|
||||
:param values: The resulting register values
|
||||
'''
|
||||
ReadRegistersResponseBase.__init__(self, values, **kwargs)
|
||||
|
||||
|
||||
class ReadWriteMultipleRegistersRequest(ModbusRequest):
|
||||
'''
|
||||
This function code performs a combination of one read operation and one
|
||||
write operation in a single MODBUS transaction. The write
|
||||
operation is performed before the read.
|
||||
|
||||
Holding registers are addressed starting at zero. Therefore holding
|
||||
registers 1-16 are addressed in the PDU as 0-15.
|
||||
|
||||
The request specifies the starting address and number of holding
|
||||
registers to be read as well as the starting address, number of holding
|
||||
registers, and the data to be written. The byte count specifies the
|
||||
number of bytes to follow in the write data field."
|
||||
'''
|
||||
function_code = 23
|
||||
_rtu_byte_count_pos = 10
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new request message
|
||||
|
||||
:param read_address: The address to start reading from
|
||||
:param read_count: The number of registers to read from address
|
||||
:param write_address: The address to start writing to
|
||||
:param write_registers: The registers to write to the specified address
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.read_address = kwargs.get('read_address', 0x00)
|
||||
self.read_count = kwargs.get('read_count', 0)
|
||||
self.write_address = kwargs.get('write_address', 0x00)
|
||||
self.write_registers = kwargs.get('write_registers', None)
|
||||
if not hasattr(self.write_registers, '__iter__'):
|
||||
self.write_registers = [self.write_registers]
|
||||
self.write_count = len(self.write_registers)
|
||||
self.write_byte_count = self.write_count * 2
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
result = struct.pack('>HHHHB',
|
||||
self.read_address, self.read_count, \
|
||||
self.write_address, self.write_count, self.write_byte_count)
|
||||
for register in self.write_registers:
|
||||
result += struct.pack('>H', register)
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode the register request packet
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.read_address, self.read_count, \
|
||||
self.write_address, self.write_count, \
|
||||
self.write_byte_count = struct.unpack('>HHHHB', data[:9])
|
||||
self.write_registers = []
|
||||
for i in range(9, self.write_byte_count + 9, 2):
|
||||
register = struct.unpack('>H', data[i:i + 2])[0]
|
||||
self.write_registers.append(register)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a write single register request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.read_count <= 0x07d):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not (1 <= self.write_count <= 0x079):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if (self.write_byte_count != self.write_count * 2):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.write_address,
|
||||
self.write_count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
if not context.validate(self.function_code, self.read_address,
|
||||
self.read_count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
context.setValues(self.function_code, self.write_address,
|
||||
self.write_registers)
|
||||
registers = context.getValues(self.function_code, self.read_address,
|
||||
self.read_count)
|
||||
return ReadWriteMultipleRegistersResponse(registers)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Byte Count(1 byte) + 2 * Quantity of Coils (n Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 1 + 2 * self.read_count
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
params = (self.read_address, self.read_count, self.write_address,
|
||||
self.write_count)
|
||||
return "ReadWriteNRegisterRequest R(%d,%d) W(%d,%d)" % params
|
||||
|
||||
|
||||
class ReadWriteMultipleRegistersResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response contains the data from the group of registers that
|
||||
were read. The byte count field specifies the quantity of bytes to
|
||||
follow in the read data field.
|
||||
'''
|
||||
function_code = 23
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param values: The register values to write
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.registers = values or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response packet
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
result = int2byte(len(self.registers) * 2)
|
||||
for register in self.registers:
|
||||
result += struct.pack('>H', register)
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode the register response packet
|
||||
|
||||
:param data: The response to decode
|
||||
'''
|
||||
bytecount = byte2int(data[0])
|
||||
for i in range(1, bytecount, 2):
|
||||
self.registers.append(struct.unpack('>H', data[i:i + 2])[0])
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "ReadWriteNRegisterResponse (%d)" % len(self.registers)
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ReadHoldingRegistersRequest", "ReadHoldingRegistersResponse",
|
||||
"ReadInputRegistersRequest", "ReadInputRegistersResponse",
|
||||
"ReadWriteMultipleRegistersRequest", "ReadWriteMultipleRegistersResponse",
|
||||
]
|
||||
@@ -1,354 +0,0 @@
|
||||
'''
|
||||
Register Writing Request/Response Messages
|
||||
-------------------------------------------
|
||||
'''
|
||||
import struct
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
|
||||
|
||||
class WriteSingleRegisterRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to write a single holding register in a
|
||||
remote device.
|
||||
|
||||
The Request PDU specifies the address of the register to
|
||||
be written. Registers are addressed starting at zero. Therefore register
|
||||
numbered 1 is addressed as 0.
|
||||
'''
|
||||
function_code = 6
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start writing add
|
||||
:param value: The values to write
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.value = value
|
||||
|
||||
def encode(self):
|
||||
''' Encode a write single register packet packet request
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
packet = struct.pack('>H', self.address)
|
||||
if self.skip_encode:
|
||||
packet += self.value
|
||||
else:
|
||||
packet += struct.pack('>H', self.value)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a write single register packet packet request
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.address, self.value = struct.unpack('>HH', data)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a write single register request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
'''
|
||||
if not (0 <= self.value <= 0xffff):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, 1):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
context.setValues(self.function_code, self.address, [self.value])
|
||||
values = context.getValues(self.function_code, self.address, 1)
|
||||
return WriteSingleRegisterResponse(self.address, values[0])
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Register Address(2 byte) + Register Value (2 bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "WriteRegisterRequest %d" % self.address
|
||||
|
||||
|
||||
class WriteSingleRegisterResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response is an echo of the request, returned after the
|
||||
register contents have been written.
|
||||
'''
|
||||
function_code = 6
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start writing add
|
||||
:param value: The values to write
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.value = value
|
||||
|
||||
def encode(self):
|
||||
''' Encode a write single register packet packet request
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
return struct.pack('>HH', self.address, self.value)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a write single register packet packet request
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.address, self.value = struct.unpack('>HH', data)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
params = (self.address, self.value)
|
||||
return "WriteRegisterResponse %d => %d" % params
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Write Multiple Registers
|
||||
#---------------------------------------------------------------------------#
|
||||
class WriteMultipleRegistersRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to write a block of contiguous registers (1
|
||||
to approx. 120 registers) in a remote device.
|
||||
|
||||
The requested written values are specified in the request data field.
|
||||
Data is packed as two bytes per register.
|
||||
'''
|
||||
function_code = 16
|
||||
_rtu_byte_count_pos = 6
|
||||
_pdu_length = 5 #func + adress1 + adress2 + outputQuant1 + outputQuant2
|
||||
|
||||
def __init__(self, address=None, values=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start writing to
|
||||
:param values: The values to write
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
if values is None:
|
||||
values = []
|
||||
elif not hasattr(values, '__iter__'):
|
||||
values = [values]
|
||||
self.values = values
|
||||
self.count = len(self.values)
|
||||
self.byte_count = self.count * 2
|
||||
|
||||
def encode(self):
|
||||
''' Encode a write single register packet packet request
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
packet = struct.pack('>HHB', self.address, self.count, self.byte_count)
|
||||
if self.skip_encode:
|
||||
return packet + b''.join(self.values)
|
||||
|
||||
for value in self.values:
|
||||
packet += struct.pack('>H', value)
|
||||
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a write single register packet packet request
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.address, self.count, \
|
||||
self.byte_count = struct.unpack('>HHB', data[:5])
|
||||
self.values = [] # reset
|
||||
for idx in range(5, (self.count * 2) + 5, 2):
|
||||
self.values.append(struct.unpack('>H', data[idx:idx + 2])[0])
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a write single register request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.count <= 0x07b):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if (self.byte_count != self.count * 2):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
context.setValues(self.function_code, self.address, self.values)
|
||||
return WriteMultipleRegistersResponse(self.address, self.count)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Starting Address (2 byte) + Quantity of Reggisters (2 Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
params = (self.address, self.count)
|
||||
return "WriteMultipleRegisterRequest %d => %d" % params
|
||||
|
||||
|
||||
class WriteMultipleRegistersResponse(ModbusResponse):
|
||||
'''
|
||||
"The normal response returns the function code, starting address, and
|
||||
quantity of registers written.
|
||||
'''
|
||||
function_code = 16
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start writing to
|
||||
:param count: The number of registers to write to
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
''' Encode a write single register packet packet request
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
return struct.pack('>HH', self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a write single register packet packet request
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.address, self.count = struct.unpack('>HH', data)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
params = (self.address, self.count)
|
||||
return "WriteMultipleRegisterResponse (%d,%d)" % params
|
||||
|
||||
class MaskWriteRegisterRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to modify the contents of a specified holding
|
||||
register using a combination of an AND mask, an OR mask, and the
|
||||
register's current contents. The function can be used to set or clear
|
||||
individual bits in the register.
|
||||
'''
|
||||
function_code = 0x16
|
||||
_rtu_frame_size = 10
|
||||
|
||||
def __init__(self, address=0x0000, and_mask=0xffff, or_mask=0x0000,
|
||||
**kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The mask pointer address (0x0000 to 0xffff)
|
||||
:param and_mask: The and bitmask to apply to the register address
|
||||
:param or_mask: The or bitmask to apply to the register address
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.and_mask = and_mask
|
||||
self.or_mask = or_mask
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The byte encoded packet
|
||||
'''
|
||||
return struct.pack('>HHH', self.address, self.and_mask,
|
||||
self.or_mask)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes the incoming request
|
||||
|
||||
:param data: The data to decode into the address
|
||||
'''
|
||||
self.address, self.and_mask, self.or_mask = struct.unpack('>HHH',
|
||||
data)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a mask write register request against the store
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
'''
|
||||
if not (0x0000 <= self.and_mask <= 0xffff):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not (0x0000 <= self.or_mask <= 0xffff):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, 1):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, 1)[0]
|
||||
values = ((values & self.and_mask) | self.or_mask)
|
||||
context.setValues(self.function_code, self.address, [values])
|
||||
return MaskWriteRegisterResponse(self.address, self.and_mask,
|
||||
self.or_mask)
|
||||
|
||||
|
||||
class MaskWriteRegisterResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response is an echo of the request. The response is returned
|
||||
after the register has been written.
|
||||
'''
|
||||
function_code = 0x16
|
||||
_rtu_frame_size = 10
|
||||
|
||||
def __init__(self, address=0x0000, and_mask=0xffff, or_mask=0x0000,
|
||||
**kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The mask pointer address (0x0000 to 0xffff)
|
||||
:param and_mask: The and bitmask applied to the register address
|
||||
:param or_mask: The or bitmask applied to the register address
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.and_mask = and_mask
|
||||
self.or_mask = or_mask
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
return struct.pack('>HHH', self.address, self.and_mask,
|
||||
self.or_mask)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, self.and_mask, self.or_mask = struct.unpack('>HHH',
|
||||
data)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"WriteSingleRegisterRequest", "WriteSingleRegisterResponse",
|
||||
"WriteMultipleRegistersRequest", "WriteMultipleRegistersResponse",
|
||||
"MaskWriteRegisterRequest", "MaskWriteRegisterResponse"
|
||||
]
|
||||
@@ -1,315 +0,0 @@
|
||||
# Pymodbus REPL
|
||||
|
||||
## Dependencies
|
||||
|
||||
Depends on [prompt_toolkit](https://python-prompt-toolkit.readthedocs.io/en/stable/index.html) and [click](http://click.pocoo.org/6/quickstart/)
|
||||
|
||||
Install dependencies
|
||||
```
|
||||
$ pip install click prompt_toolkit --upgrade
|
||||
```
|
||||
|
||||
Or
|
||||
Install pymodbus with repl support
|
||||
```
|
||||
$ pip install pymodbus[repl] --upgrade
|
||||
```
|
||||
|
||||
## Usage Instructions
|
||||
RTU and TCP are supported as of now
|
||||
|
||||
```
|
||||
✗ pymodbus.console --help
|
||||
Usage: pymodbus.console [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
--version Show the version and exit.
|
||||
--verbose Verbose logs
|
||||
--broadcast-support Support broadcast messages
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
serial
|
||||
tcp
|
||||
|
||||
|
||||
```
|
||||
TCP Options
|
||||
|
||||
```
|
||||
✗ pymodbus.console tcp --help
|
||||
Usage: pymodbus.console tcp [OPTIONS]
|
||||
|
||||
Options:
|
||||
--host TEXT Modbus TCP IP
|
||||
--port INTEGER Modbus TCP port
|
||||
--framer TEXT Override the default packet framer tcp|rtu
|
||||
--help Show this message and exit.
|
||||
|
||||
```
|
||||
|
||||
SERIAL Options
|
||||
```
|
||||
✗ pymodbus.console serial --help
|
||||
Usage: pymodbus.console serial [OPTIONS]
|
||||
|
||||
Options:
|
||||
--method TEXT Modbus Serial Mode (rtu/ascii)
|
||||
--port TEXT Modbus RTU port
|
||||
--baudrate INTEGER Modbus RTU serial baudrate to use. Defaults to 9600
|
||||
--bytesize [5|6|7|8] Modbus RTU serial Number of data bits. Possible
|
||||
values: FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS.
|
||||
Defaults to 8
|
||||
|
||||
--parity [N|E|O|M|S] Modbus RTU serial parity. Enable parity checking.
|
||||
Possible values: PARITY_NONE, PARITY_EVEN, PARITY_ODD
|
||||
PARITY_MARK, PARITY_SPACE. Default to 'N'
|
||||
|
||||
--stopbits [1|1.5|2] Modbus RTU serial stop bits. Number of stop bits.
|
||||
Possible values: STOPBITS_ONE,
|
||||
STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO. Default to '1'
|
||||
|
||||
--xonxoff INTEGER Modbus RTU serial xonxoff. Enable software flow
|
||||
control.Defaults to 0
|
||||
|
||||
--rtscts INTEGER Modbus RTU serial rtscts. Enable hardware (RTS/CTS)
|
||||
flow control. Defaults to 0
|
||||
|
||||
--dsrdtr INTEGER Modbus RTU serial dsrdtr. Enable hardware (DSR/DTR)
|
||||
flow control. Defaults to 0
|
||||
|
||||
--timeout FLOAT Modbus RTU serial read timeout. Defaults to 0.025 sec
|
||||
--write-timeout FLOAT Modbus RTU serial write timeout. Defaults to 2 sec
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
To view all available commands type `help`
|
||||
|
||||
TCP
|
||||
```
|
||||
$ pymodbus.console tcp --host 192.168.128.126 --port 5020
|
||||
|
||||
> help
|
||||
Available commands:
|
||||
client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests.
|
||||
client.clear_counters Diagnostic sub command, Clear all counters and diag registers.
|
||||
client.clear_overrun_count Diagnostic sub command, Clear over run counter.
|
||||
client.close Closes the underlying socket connection
|
||||
client.connect Connect to the modbus tcp server
|
||||
client.debug_enabled Returns a boolean indicating if debug is enabled.
|
||||
client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode.
|
||||
client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device.
|
||||
client.get_com_event_counter Read status word and an event count from the remote device's communication event counter.
|
||||
client.get_com_event_log Read status word, event count, message count, and a field of event bytes from the remote device.
|
||||
client.host Read Only!
|
||||
client.idle_time Bus Idle Time to initiate next transaction
|
||||
client.is_socket_open Check whether the underlying socket/serial is open or not.
|
||||
client.last_frame_end Read Only!
|
||||
client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask`.
|
||||
client.port Read Only!
|
||||
client.read_coils Reads `count` coils from a given slave starting at `address`.
|
||||
client.read_device_information Read the identification and additional information of remote slave.
|
||||
client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address`.
|
||||
client.read_exception_status Read the contents of eight Exception Status outputs in a remote device.
|
||||
client.read_holding_registers Read `count` number of holding registers starting at `address`.
|
||||
client.read_input_registers Read `count` number of input registers starting at `address`.
|
||||
client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address`.
|
||||
client.report_slave_id Report information about remote slave ID.
|
||||
client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters .
|
||||
client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave.
|
||||
client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave.
|
||||
client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave.
|
||||
client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register.
|
||||
client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave.
|
||||
client.return_query_data Diagnostic sub command , Loop back data sent in response.
|
||||
client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition.
|
||||
client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave.
|
||||
client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave.
|
||||
client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave.
|
||||
client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave.
|
||||
client.silent_interval Read Only!
|
||||
client.state Read Only!
|
||||
client.timeout Read Only!
|
||||
client.write_coil Write `value` to coil at `address`.
|
||||
client.write_coils Write `value` to coil at `address`.
|
||||
client.write_register Write `value` to register at `address`.
|
||||
client.write_registers Write list of `values` to registers starting at `address`.
|
||||
```
|
||||
|
||||
SERIAL
|
||||
```
|
||||
$ pymodbus.console serial --port /dev/ttyUSB0 --baudrate 19200 --timeout 2
|
||||
> help
|
||||
Available commands:
|
||||
client.baudrate Read Only!
|
||||
client.bytesize Read Only!
|
||||
client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests.
|
||||
client.clear_counters Diagnostic sub command, Clear all counters and diag registers.
|
||||
client.clear_overrun_count Diagnostic sub command, Clear over run counter.
|
||||
client.close Closes the underlying socket connection
|
||||
client.connect Connect to the modbus serial server
|
||||
client.debug_enabled Returns a boolean indicating if debug is enabled.
|
||||
client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode.
|
||||
client.get_baudrate Serial Port baudrate.
|
||||
client.get_bytesize Number of data bits.
|
||||
client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device.
|
||||
client.get_com_event_counter Read status word and an event count from the remote device's communication event counter.
|
||||
client.get_com_event_log Read status word, event count, message count, and a field of event bytes from the remote device.
|
||||
client.get_parity Enable Parity Checking.
|
||||
client.get_port Serial Port.
|
||||
client.get_serial_settings Gets Current Serial port settings.
|
||||
client.get_stopbits Number of stop bits.
|
||||
client.get_timeout Serial Port Read timeout.
|
||||
client.idle_time Bus Idle Time to initiate next transaction
|
||||
client.inter_char_timeout Read Only!
|
||||
client.is_socket_open c l i e n t . i s s o c k e t o p e n
|
||||
client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask`.
|
||||
client.method Read Only!
|
||||
client.parity Read Only!
|
||||
client.port Read Only!
|
||||
client.read_coils Reads `count` coils from a given slave starting at `address`.
|
||||
client.read_device_information Read the identification and additional information of remote slave.
|
||||
client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address`.
|
||||
client.read_exception_status Read the contents of eight Exception Status outputs in a remote device.
|
||||
client.read_holding_registers Read `count` number of holding registers starting at `address`.
|
||||
client.read_input_registers Read `count` number of input registers starting at `address`.
|
||||
client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address`.
|
||||
client.report_slave_id Report information about remote slave ID.
|
||||
client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters .
|
||||
client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave.
|
||||
client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave.
|
||||
client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave.
|
||||
client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register.
|
||||
client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave.
|
||||
client.return_query_data Diagnostic sub command , Loop back data sent in response.
|
||||
client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition.
|
||||
client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave.
|
||||
client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave.
|
||||
client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave.
|
||||
client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave.
|
||||
client.set_baudrate Baudrate setter.
|
||||
client.set_bytesize Byte size setter.
|
||||
client.set_parity Parity Setter.
|
||||
client.set_port Serial Port setter.
|
||||
client.set_stopbits Stop bit setter.
|
||||
client.set_timeout Read timeout setter.
|
||||
client.silent_interval Read Only!
|
||||
client.state Read Only!
|
||||
client.stopbits Read Only!
|
||||
client.timeout Read Only!
|
||||
client.write_coil Write `value` to coil at `address`.
|
||||
client.write_coils Write `value` to coil at `address`.
|
||||
client.write_register Write `value` to register at `address`.
|
||||
client.write_registers Write list of `values` to registers starting at `address`.
|
||||
result.decode Decode the register response to known formatters.
|
||||
result.raw Return raw result dict.
|
||||
|
||||
```
|
||||
|
||||
Every command has auto suggetion on the arguments supported , supply arg and value are to be supplied in `arg=val` format.
|
||||
```
|
||||
|
||||
> client.read_holding_registers count=4 address=9 unit=1
|
||||
{
|
||||
"registers": [
|
||||
60497,
|
||||
47134,
|
||||
34091,
|
||||
15424
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The last result could be accessed with `result.raw` command
|
||||
```
|
||||
> result.raw
|
||||
{
|
||||
"registers": [
|
||||
15626,
|
||||
55203,
|
||||
28733,
|
||||
18368
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For Holding and Input register reads, the decoded value could be viewed with `result.decode`
|
||||
```
|
||||
> result.decode word_order=little byte_order=little formatters=float64
|
||||
28.17
|
||||
|
||||
>
|
||||
```
|
||||
|
||||
Client settings could be retrieved and altered as well.
|
||||
```
|
||||
> # For serial settings
|
||||
|
||||
> # Check the serial mode
|
||||
> client.method
|
||||
"rtu"
|
||||
|
||||
> client.get_serial_settings
|
||||
{
|
||||
"t1.5": 0.00171875,
|
||||
"baudrate": 9600,
|
||||
"read timeout": 0.5,
|
||||
"port": "/dev/ptyp0",
|
||||
"t3.5": 0.00401,
|
||||
"bytesize": 8,
|
||||
"parity": "N",
|
||||
"stopbits": 1.0
|
||||
}
|
||||
> client.set_timeout value=1
|
||||
null
|
||||
|
||||
> client.get_timeout
|
||||
1.0
|
||||
|
||||
> client.get_serial_settings
|
||||
{
|
||||
"t1.5": 0.00171875,
|
||||
"baudrate": 9600,
|
||||
"read timeout": 1.0,
|
||||
"port": "/dev/ptyp0",
|
||||
"t3.5": 0.00401,
|
||||
"bytesize": 8,
|
||||
"parity": "N",
|
||||
"stopbits": 1.0
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To Send broadcast requests, use `--broadcast-support` and send requests with unit id as `0`.
|
||||
`write_coil`, `write_coils`, `write_register`, `write_registers` are supported.
|
||||
|
||||
```
|
||||
✗ pymodbus.console --broadcast-support tcp --host 192.168.1.8 --port 5020
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
__________ _____ .___ __________ .__
|
||||
\______ \___.__. / \ ____ __| _/ \______ \ ____ ______ | |
|
||||
| ___< | |/ \ / \ / _ \ / __ | | _// __ \\____ \| |
|
||||
| | \___ / Y ( <_> ) /_/ | | | \ ___/| |_> > |__
|
||||
|____| / ____\____|__ /\____/\____ | /\ |____|_ /\___ > __/|____/
|
||||
\/ \/ \/ \/ \/ \/|__|
|
||||
v1.2.0 - [pymodbus, version 2.4.0]
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
> client.write_registers address=0 values=10,20,30,40 unit=0
|
||||
{
|
||||
"broadcasted": true
|
||||
}
|
||||
|
||||
> client.write_registers address=0 values=10,20,30,40 unit=1
|
||||
{
|
||||
"address": 0,
|
||||
"count": 4
|
||||
}
|
||||
```
|
||||
|
||||
## DEMO
|
||||
|
||||
[](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o)
|
||||
[](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
Pymodbus REPL Module.
|
||||
|
||||
Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
@@ -1,4 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2020 by RiptideIO
|
||||
All rights reserved.
|
||||
"""
|
||||
@@ -1,156 +0,0 @@
|
||||
"""
|
||||
Command Completion for pymodbus REPL.
|
||||
|
||||
Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.filters import Condition
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from pymodbus.repl.client.helper import get_commands
|
||||
from pymodbus.compat import string_types
|
||||
|
||||
|
||||
@Condition
|
||||
def has_selected_completion():
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
return (complete_state is not None and
|
||||
complete_state.current_completion is not None)
|
||||
|
||||
|
||||
style = Style.from_dict({
|
||||
'completion-menu.completion': 'bg:#008888 #ffffff',
|
||||
'completion-menu.completion.current': 'bg:#00aaaa #000000',
|
||||
'scrollbar.background': 'bg:#88aaaa',
|
||||
'scrollbar.button': 'bg:#222222',
|
||||
})
|
||||
|
||||
|
||||
class CmdCompleter(Completer):
|
||||
"""
|
||||
Completer for Pymodbus REPL.
|
||||
"""
|
||||
|
||||
def __init__(self, client=None, commands=None, ignore_case=True):
|
||||
"""
|
||||
|
||||
:param client: Modbus Client
|
||||
:param commands: Commands to be added for Completion (list)
|
||||
:param ignore_case: Ignore Case while looking up for commands
|
||||
"""
|
||||
self._commands = commands or get_commands(client)
|
||||
self._commands['help'] = ""
|
||||
self._command_names = self._commands.keys()
|
||||
self.ignore_case = ignore_case
|
||||
|
||||
@property
|
||||
def commands(self):
|
||||
return self._commands
|
||||
|
||||
@property
|
||||
def command_names(self):
|
||||
return self._commands.keys()
|
||||
|
||||
def completing_command(self, words, word_before_cursor):
|
||||
"""
|
||||
Determine if we are dealing with supported command.
|
||||
|
||||
:param words: Input text broken in to word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return:
|
||||
"""
|
||||
if len(words) == 1 and word_before_cursor != '':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def completing_arg(self, words, word_before_cursor):
|
||||
"""
|
||||
Determine if we are currently completing an argument.
|
||||
|
||||
:param words: The input text broken into word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return: Specifies whether we are currently completing an arg.
|
||||
"""
|
||||
if len(words) > 1 and word_before_cursor != '':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def arg_completions(self, words, word_before_cursor):
|
||||
"""
|
||||
Generates arguments completions based on the input.
|
||||
|
||||
:param words: The input text broken into word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return: A list of completions.
|
||||
"""
|
||||
cmd = words[0].strip()
|
||||
cmd = self._commands.get(cmd, None)
|
||||
if cmd:
|
||||
return cmd
|
||||
|
||||
def _get_completions(self, word, word_before_cursor):
|
||||
if self.ignore_case:
|
||||
word_before_cursor = word_before_cursor.lower()
|
||||
return self.word_matches(word, word_before_cursor)
|
||||
|
||||
def word_matches(self, word, word_before_cursor):
|
||||
"""
|
||||
Match the word and word before cursor
|
||||
|
||||
:param words: The input text broken into word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return: True if matched.
|
||||
|
||||
"""
|
||||
if self.ignore_case:
|
||||
word = word.lower()
|
||||
return word.startswith(word_before_cursor)
|
||||
|
||||
def get_completions(self, document, complete_event):
|
||||
"""
|
||||
Get completions for the current scope.
|
||||
|
||||
:param document: An instance of `prompt_toolkit.Document`.
|
||||
:param complete_event: (Unused).
|
||||
:return: Yields an instance of `prompt_toolkit.completion.Completion`.
|
||||
"""
|
||||
word_before_cursor = document.get_word_before_cursor(WORD=True)
|
||||
text = document.text_before_cursor.lstrip()
|
||||
words = document.text.strip().split()
|
||||
meta = None
|
||||
commands = []
|
||||
if len(words) == 0:
|
||||
# yield commands
|
||||
pass
|
||||
if self.completing_command(words, word_before_cursor):
|
||||
commands = self._command_names
|
||||
c_meta = {
|
||||
k: v.help_text
|
||||
if not isinstance(v, string_types)
|
||||
else v for k, v in self._commands.items()
|
||||
}
|
||||
meta = lambda x: (x, c_meta.get(x, ''))
|
||||
else:
|
||||
if not list(filter(lambda cmd: any(x == cmd for x in words),
|
||||
self._command_names)):
|
||||
# yield commands
|
||||
pass
|
||||
|
||||
if ' ' in text:
|
||||
command = self.arg_completions(words, word_before_cursor)
|
||||
commands = list(command.get_completion())
|
||||
commands = list(filter(lambda cmd: not(any(cmd in x for x in words)), commands))
|
||||
meta = command.get_meta
|
||||
for a in commands:
|
||||
if self._get_completions(a, word_before_cursor):
|
||||
cmd, display_meta = meta(a) if meta else ('', '')
|
||||
yield Completion(a, -len(word_before_cursor),
|
||||
display_meta=display_meta)
|
||||
@@ -1,327 +0,0 @@
|
||||
"""
|
||||
Helper Module for REPL actions.
|
||||
|
||||
Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import json
|
||||
import pygments
|
||||
import inspect
|
||||
from collections import OrderedDict
|
||||
from pygments.lexers.data import JsonLexer
|
||||
from prompt_toolkit.formatted_text import PygmentsTokens, HTML
|
||||
from prompt_toolkit import print_formatted_text
|
||||
|
||||
from pymodbus.payload import BinaryPayloadDecoder, Endian
|
||||
from pymodbus.compat import PYTHON_VERSION, IS_PYTHON2, string_types, izip
|
||||
|
||||
predicate = inspect.ismethod
|
||||
if IS_PYTHON2 or PYTHON_VERSION < (3, 3):
|
||||
argspec = inspect.getargspec
|
||||
else:
|
||||
predicate = inspect.isfunction
|
||||
argspec = inspect.signature
|
||||
|
||||
|
||||
FORMATTERS = {
|
||||
'int8': 'decode_8bit_int',
|
||||
'int16': 'decode_16bit_int',
|
||||
'int32': 'decode_32bit_int',
|
||||
'int64': 'decode_64bit_int',
|
||||
'uint8': 'decode_8bit_uint',
|
||||
'uint16': 'decode_16bit_uint',
|
||||
'uint32': 'decode_32bit_uint',
|
||||
'uint64': 'decode_64bit_int',
|
||||
'float16': 'decode_16bit_float',
|
||||
'float32': 'decode_32bit_float',
|
||||
'float64': 'decode_64bit_float',
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_KWARGS = {
|
||||
'unit': 'Slave address'
|
||||
}
|
||||
|
||||
OTHER_COMMANDS = {
|
||||
"result.raw": "Show RAW Result",
|
||||
"result.decode": "Decode register response to known formats",
|
||||
}
|
||||
EXCLUDE = ['execute', 'recv', 'send', 'trace', 'set_debug']
|
||||
CLIENT_METHODS = [
|
||||
'connect', 'close', 'idle_time', 'is_socket_open', 'get_port', 'set_port',
|
||||
'get_stopbits', 'set_stopbits', 'get_bytesize', 'set_bytesize',
|
||||
'get_parity', 'set_parity', 'get_baudrate', 'set_baudrate', 'get_timeout',
|
||||
'set_timeout', 'get_serial_settings'
|
||||
|
||||
]
|
||||
CLIENT_ATTRIBUTES = []
|
||||
|
||||
|
||||
class Command(object):
|
||||
"""
|
||||
Class representing Commands to be consumed by Completer.
|
||||
"""
|
||||
def __init__(self, name, signature, doc, unit=False):
|
||||
"""
|
||||
|
||||
:param name: Name of the command
|
||||
:param signature: inspect object
|
||||
:param doc: Doc string for the command
|
||||
:param unit: Use unit as additional argument in the command .
|
||||
"""
|
||||
self.name = name
|
||||
self.doc = doc.split("\n") if doc else " ".join(name.split("_"))
|
||||
self.help_text = self._create_help()
|
||||
self.param_help = self._create_arg_help()
|
||||
if signature:
|
||||
if IS_PYTHON2:
|
||||
self._params = signature
|
||||
else:
|
||||
self._params = signature.parameters
|
||||
self.args = self.create_completion()
|
||||
else:
|
||||
self._params = ''
|
||||
|
||||
if self.name.startswith("client.") and unit:
|
||||
self.args.update(**DEFAULT_KWARGS)
|
||||
|
||||
def _create_help(self):
|
||||
doc = filter(lambda d: d, self.doc)
|
||||
cmd_help = list(filter(
|
||||
lambda x: not x.startswith(":param") and not x.startswith(
|
||||
":return"), doc))
|
||||
return " ".join(cmd_help).strip()
|
||||
|
||||
def _create_arg_help(self):
|
||||
param_dict = {}
|
||||
params = list(filter(lambda d: d.strip().startswith(":param"),
|
||||
self.doc))
|
||||
for param in params:
|
||||
param, help = param.split(":param")[1].strip().split(":")
|
||||
param_dict[param] = help
|
||||
return param_dict
|
||||
|
||||
def create_completion(self):
|
||||
"""
|
||||
Create command completion meta data.
|
||||
|
||||
:return:
|
||||
"""
|
||||
words = {}
|
||||
|
||||
def _create(entry, default):
|
||||
if entry not in ['self', 'kwargs']:
|
||||
if isinstance(default, (int, string_types)):
|
||||
entry += "={}".format(default)
|
||||
return entry
|
||||
|
||||
if IS_PYTHON2:
|
||||
if not self._params.defaults:
|
||||
defaults = [None]*len(self._params.args)
|
||||
else:
|
||||
defaults = list(self._params.defaults)
|
||||
missing = len(self._params.args) - len(defaults)
|
||||
if missing > 1:
|
||||
defaults.extend([None]*missing)
|
||||
defaults.insert(0, None)
|
||||
for arg, default in izip(self._params.args, defaults):
|
||||
entry = _create(arg, default)
|
||||
if entry:
|
||||
entry, meta = self.get_meta(entry)
|
||||
words[entry] = help
|
||||
else:
|
||||
for arg in self._params.values():
|
||||
entry = _create(arg.name, arg.default)
|
||||
if entry:
|
||||
entry, meta = self.get_meta(entry)
|
||||
words[entry] = meta
|
||||
|
||||
return words
|
||||
|
||||
def get_completion(self):
|
||||
"""
|
||||
Gets a list of completions.
|
||||
|
||||
:return:
|
||||
"""
|
||||
return self.args.keys()
|
||||
|
||||
def get_meta(self, cmd):
|
||||
"""
|
||||
Get Meta info of a given command.
|
||||
|
||||
:param cmd: Name of command.
|
||||
:return: Dict containing meta info.
|
||||
"""
|
||||
cmd = cmd.strip()
|
||||
cmd = cmd.split("=")[0].strip()
|
||||
return cmd, self.param_help.get(cmd, '')
|
||||
|
||||
def __str__(self):
|
||||
if self.doc:
|
||||
return "Command {:>50}{:<20}".format(self.name, self.doc)
|
||||
return "Command {}".format(self.name)
|
||||
|
||||
|
||||
def _get_requests(members):
|
||||
commands = list(filter(lambda x: (x[0] not in EXCLUDE
|
||||
and x[0] not in CLIENT_METHODS
|
||||
and callable(x[1])),
|
||||
members))
|
||||
commands = {
|
||||
"client.{}".format(c[0]):
|
||||
Command("client.{}".format(c[0]),
|
||||
argspec(c[1]), inspect.getdoc(c[1]), unit=True)
|
||||
for c in commands if not c[0].startswith("_")
|
||||
}
|
||||
return commands
|
||||
|
||||
|
||||
def _get_client_methods(members):
|
||||
commands = list(filter(lambda x: (x[0] not in EXCLUDE
|
||||
and x[0] in CLIENT_METHODS),
|
||||
members))
|
||||
commands = {
|
||||
"client.{}".format(c[0]):
|
||||
Command("client.{}".format(c[0]),
|
||||
argspec(c[1]), inspect.getdoc(c[1]), unit=False)
|
||||
for c in commands if not c[0].startswith("_")
|
||||
}
|
||||
return commands
|
||||
|
||||
|
||||
def _get_client_properties(members):
|
||||
global CLIENT_ATTRIBUTES
|
||||
commands = list(filter(lambda x: not callable(x[1]), members))
|
||||
commands = {
|
||||
"client.{}".format(c[0]):
|
||||
Command("client.{}".format(c[0]), None, "Read Only!", unit=False)
|
||||
for c in commands if (not c[0].startswith("_")
|
||||
and isinstance(c[1], (string_types, int, float)))
|
||||
}
|
||||
CLIENT_ATTRIBUTES.extend(list(commands.keys()))
|
||||
return commands
|
||||
|
||||
|
||||
def get_commands(client):
|
||||
"""
|
||||
Helper method to retrieve all required methods and attributes of a client \
|
||||
object and convert it to commands.
|
||||
|
||||
:param client: Modbus Client object.
|
||||
:return:
|
||||
"""
|
||||
commands = dict()
|
||||
members = inspect.getmembers(client)
|
||||
requests = _get_requests(members)
|
||||
client_methods = _get_client_methods(members)
|
||||
client_attr = _get_client_properties(members)
|
||||
|
||||
result_commands = inspect.getmembers(Result, predicate=predicate)
|
||||
result_commands = {
|
||||
"result.{}".format(c[0]):
|
||||
Command("result.{}".format(c[0]), argspec(c[1]),
|
||||
inspect.getdoc(c[1]))
|
||||
for c in result_commands if (not c[0].startswith("_")
|
||||
and c[0] != "print_result")
|
||||
}
|
||||
commands.update(requests)
|
||||
commands.update(client_methods)
|
||||
commands.update(client_attr)
|
||||
commands.update(result_commands)
|
||||
return commands
|
||||
|
||||
|
||||
class Result(object):
|
||||
"""
|
||||
Represent result command.
|
||||
"""
|
||||
function_code = None
|
||||
data = None
|
||||
|
||||
def __init__(self, result):
|
||||
"""
|
||||
:param result: Response of a modbus command.
|
||||
"""
|
||||
if isinstance(result, dict): # Modbus response
|
||||
self.function_code = result.pop('function_code', None)
|
||||
self.data = dict(result)
|
||||
else:
|
||||
self.data = result
|
||||
|
||||
def decode(self, formatters, byte_order='big', word_order='big'):
|
||||
"""
|
||||
Decode the register response to known formatters.
|
||||
|
||||
:param formatters: int8/16/32/64, uint8/16/32/64, float32/64
|
||||
:param byte_order: little/big
|
||||
:param word_order: little/big
|
||||
:return: Decoded Value
|
||||
"""
|
||||
# Read Holding Registers (3)
|
||||
# Read Input Registers (4)
|
||||
# Read Write Registers (23)
|
||||
if not isinstance(formatters, (list, tuple)):
|
||||
formatters = [formatters]
|
||||
|
||||
if self.function_code not in [3, 4, 23]:
|
||||
print_formatted_text(
|
||||
HTML("<red>Decoder works only for registers!!</red>"))
|
||||
return
|
||||
byte_order = (Endian.Little if byte_order.strip().lower() == "little"
|
||||
else Endian.Big)
|
||||
word_order = (Endian.Little if word_order.strip().lower() == "little"
|
||||
else Endian.Big)
|
||||
decoder = BinaryPayloadDecoder.fromRegisters(self.data.get('registers'),
|
||||
byteorder=byte_order,
|
||||
wordorder=word_order)
|
||||
for formatter in formatters:
|
||||
formatter = FORMATTERS.get(formatter)
|
||||
if not formatter:
|
||||
print_formatted_text(
|
||||
HTML("<red>Invalid Formatter - {}"
|
||||
"!!</red>".format(formatter)))
|
||||
return
|
||||
decoded = getattr(decoder, formatter)()
|
||||
self.print_result(decoded)
|
||||
|
||||
def raw(self):
|
||||
"""
|
||||
Return raw result dict.
|
||||
|
||||
:return:
|
||||
"""
|
||||
self.print_result()
|
||||
|
||||
def _process_dict(self, d):
|
||||
new_dict = OrderedDict()
|
||||
for k, v in d.items():
|
||||
if isinstance(v, bytes):
|
||||
v = v.decode('utf-8')
|
||||
elif isinstance(v, dict):
|
||||
v = self._process_dict(v)
|
||||
elif isinstance(v, (list, tuple)):
|
||||
v = [v1.decode('utf-8') if isinstance(v1, bytes) else v1
|
||||
for v1 in v ]
|
||||
new_dict[k] = v
|
||||
return new_dict
|
||||
|
||||
def print_result(self, data=None):
|
||||
"""
|
||||
Prettu print result object.
|
||||
|
||||
:param data: Data to be printed.
|
||||
:return:
|
||||
"""
|
||||
data = data or self.data
|
||||
if isinstance(data, dict):
|
||||
data = self._process_dict(data)
|
||||
elif isinstance(data, (list, tuple)):
|
||||
data = [v.decode('utf-8') if isinstance(v, bytes) else v
|
||||
for v in data]
|
||||
elif isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
tokens = list(pygments.lex(json.dumps(data, indent=4),
|
||||
lexer=JsonLexer()))
|
||||
print_formatted_text(PygmentsTokens(tokens))
|
||||
@@ -1,384 +0,0 @@
|
||||
"""
|
||||
Pymodbus REPL Entry point.
|
||||
|
||||
Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
try:
|
||||
import click
|
||||
except ImportError:
|
||||
print("click not installed!! Install with 'pip install click'")
|
||||
exit(1)
|
||||
try:
|
||||
from prompt_toolkit import PromptSession, print_formatted_text
|
||||
except ImportError:
|
||||
print("prompt toolkit is not installed!! "
|
||||
"Install with 'pip install prompt_toolkit --upgrade'")
|
||||
exit(1)
|
||||
|
||||
from prompt_toolkit.lexers import PygmentsLexer
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
|
||||
from pygments.lexers.python import PythonLexer
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from pymodbus.version import version
|
||||
from pymodbus.repl.client.completer import CmdCompleter, has_selected_completion
|
||||
from pymodbus.repl.client.helper import Result, CLIENT_ATTRIBUTES
|
||||
|
||||
click.disable_unicode_literals_warning = True
|
||||
|
||||
TITLE = """
|
||||
----------------------------------------------------------------------------
|
||||
__________ _____ .___ __________ .__
|
||||
\______ \___.__. / \ ____ __| _/ \______ \ ____ ______ | |
|
||||
| ___< | |/ \ / \ / _ \ / __ | | _// __ \\\____ \| |
|
||||
| | \___ / Y ( <_> ) /_/ | | | \ ___/| |_> > |__
|
||||
|____| / ____\____|__ /\____/\____ | /\ |____|_ /\___ > __/|____/
|
||||
\/ \/ \/ \/ \/ \/|__|
|
||||
v{} - {}
|
||||
----------------------------------------------------------------------------
|
||||
""".format("1.3.0", version)
|
||||
|
||||
log = None
|
||||
|
||||
style = Style.from_dict({
|
||||
'completion-menu.completion': 'bg:#008888 #ffffff',
|
||||
'completion-menu.completion.current': 'bg:#00aaaa #000000',
|
||||
'scrollbar.background': 'bg:#88aaaa',
|
||||
'scrollbar.button': 'bg:#222222',
|
||||
})
|
||||
|
||||
|
||||
def bottom_toolbar():
|
||||
"""
|
||||
Console toolbar.
|
||||
:return:
|
||||
"""
|
||||
return HTML('Press <b><style bg="ansired">CTRL+D or exit </style></b>'
|
||||
' to exit! Type "help" for list of available commands')
|
||||
|
||||
|
||||
class CaseInsenstiveChoice(click.Choice):
|
||||
"""
|
||||
Case Insensitive choice for click commands and options
|
||||
"""
|
||||
def convert(self, value, param, ctx):
|
||||
"""
|
||||
Convert args to uppercase for evaluation.
|
||||
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
return super(CaseInsenstiveChoice, self).convert(
|
||||
value.strip().upper(), param, ctx)
|
||||
|
||||
|
||||
class NumericChoice(click.Choice):
|
||||
"""
|
||||
Numeric choice for click arguments and options.
|
||||
"""
|
||||
def __init__(self, choices, typ):
|
||||
self.typ = typ
|
||||
super(NumericChoice, self).__init__(choices)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
# Exact match
|
||||
if value in self.choices:
|
||||
return self.typ(value)
|
||||
|
||||
if ctx is not None and ctx.token_normalize_func is not None:
|
||||
value = ctx.token_normalize_func(value)
|
||||
for choice in self.casted_choices:
|
||||
if ctx.token_normalize_func(choice) == value:
|
||||
return choice
|
||||
|
||||
self.fail('invalid choice: %s. (choose from %s)' %
|
||||
(value, ', '.join(self.choices)), param, ctx)
|
||||
|
||||
|
||||
def cli(client):
|
||||
kb = KeyBindings()
|
||||
|
||||
@kb.add('c-space')
|
||||
def _(event):
|
||||
"""
|
||||
Initialize autocompletion, or select the next completion.
|
||||
"""
|
||||
buff = event.app.current_buffer
|
||||
if buff.complete_state:
|
||||
buff.complete_next()
|
||||
else:
|
||||
buff.start_completion(select_first=False)
|
||||
|
||||
@kb.add('enter', filter=has_selected_completion)
|
||||
def _(event):
|
||||
"""
|
||||
Makes the enter key work as the tab key only when showing the menu.
|
||||
"""
|
||||
|
||||
event.current_buffer.complete_state = None
|
||||
b = event.cli.current_buffer
|
||||
b.complete_state = None
|
||||
|
||||
def _process_args(args, string=True):
|
||||
kwargs = {}
|
||||
execute = True
|
||||
skip_index = None
|
||||
for i, arg in enumerate(args):
|
||||
if i == skip_index:
|
||||
continue
|
||||
arg = arg.strip()
|
||||
if "=" in arg:
|
||||
a, val = arg.split("=")
|
||||
if not string:
|
||||
if "," in val:
|
||||
val = val.split(",")
|
||||
val = [int(v) for v in val]
|
||||
else:
|
||||
val = int(val)
|
||||
kwargs[a] = val
|
||||
else:
|
||||
a, val = arg, args[i + 1]
|
||||
try:
|
||||
if not string:
|
||||
if "," in val:
|
||||
val = val.split(",")
|
||||
val = [int(v) for v in val]
|
||||
else:
|
||||
val = int(val)
|
||||
kwargs[a] = val
|
||||
skip_index = i + 1
|
||||
except TypeError:
|
||||
click.secho("Error parsing arguments!",
|
||||
fg='yellow')
|
||||
execute = False
|
||||
break
|
||||
except ValueError:
|
||||
click.secho("Error parsing argument",
|
||||
fg='yellow')
|
||||
execute = False
|
||||
break
|
||||
return kwargs, execute
|
||||
|
||||
session = PromptSession(lexer=PygmentsLexer(PythonLexer),
|
||||
completer=CmdCompleter(client), style=style,
|
||||
complete_while_typing=True,
|
||||
bottom_toolbar=bottom_toolbar,
|
||||
key_bindings=kb,
|
||||
history=FileHistory('../.pymodhis'),
|
||||
auto_suggest=AutoSuggestFromHistory())
|
||||
click.secho("{}".format(TITLE), fg='green')
|
||||
result = None
|
||||
while True:
|
||||
try:
|
||||
|
||||
text = session.prompt('> ', complete_while_typing=True)
|
||||
if text.strip().lower() == 'help':
|
||||
print_formatted_text(HTML("<u>Available commands:</u>"))
|
||||
for cmd, obj in sorted(session.completer.commands.items()):
|
||||
if cmd != 'help':
|
||||
print_formatted_text(
|
||||
HTML("<skyblue>{:45s}</skyblue>"
|
||||
"<seagreen>{:100s}"
|
||||
"</seagreen>".format(cmd, obj.help_text)))
|
||||
|
||||
continue
|
||||
elif text.strip().lower() == 'exit':
|
||||
raise EOFError()
|
||||
elif text.strip().lower().startswith("client."):
|
||||
try:
|
||||
text = text.strip().split()
|
||||
cmd = text[0].split(".")[1]
|
||||
args = text[1:]
|
||||
kwargs, execute = _process_args(args, string=False)
|
||||
if execute:
|
||||
if text[0] in CLIENT_ATTRIBUTES:
|
||||
result = Result(getattr(client, cmd))
|
||||
else:
|
||||
result = Result(getattr(client, cmd)(**kwargs))
|
||||
result.print_result()
|
||||
except Exception as e:
|
||||
click.secho(repr(e), fg='red')
|
||||
elif text.strip().lower().startswith("result."):
|
||||
if result:
|
||||
words = text.lower().split()
|
||||
if words[0] == 'result.raw':
|
||||
result.raw()
|
||||
if words[0] == 'result.decode':
|
||||
args = words[1:]
|
||||
kwargs, execute = _process_args(args)
|
||||
if execute:
|
||||
result.decode(**kwargs)
|
||||
except KeyboardInterrupt:
|
||||
continue # Control-C pressed. Try again.
|
||||
except EOFError:
|
||||
break # Control-D pressed.
|
||||
except Exception as e: # Handle all other exceptions
|
||||
click.secho(str(e), fg='red')
|
||||
|
||||
click.secho('GoodBye!', fg='blue')
|
||||
|
||||
|
||||
@click.group('pymodbus-repl')
|
||||
@click.version_option(version, message=TITLE)
|
||||
@click.option("--verbose", is_flag=True, default=False, help="Verbose logs")
|
||||
@click.option("--broadcast-support", is_flag=True, default=False,
|
||||
help="Support broadcast messages")
|
||||
@click.option("--retry-on-empty", is_flag=True, default=False,
|
||||
help="Retry on empty response")
|
||||
@click.option("--retry-on-error", is_flag=True, default=False,
|
||||
help="Retry on error response")
|
||||
@click.option("--retries", default=3, help="Retry count")
|
||||
@click.option("--reset-socket/--no-reset-socket", default=True, help="Reset client socket on error")
|
||||
@click.pass_context
|
||||
def main(ctx, verbose, broadcast_support, retry_on_empty,
|
||||
retry_on_error, retries, reset_socket):
|
||||
if verbose:
|
||||
global log
|
||||
import logging
|
||||
format = ('%(asctime)-15s %(threadName)-15s '
|
||||
'%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
|
||||
log = logging.getLogger('pymodbus')
|
||||
logging.basicConfig(format=format)
|
||||
log.setLevel(logging.DEBUG)
|
||||
ctx.obj = {
|
||||
"broadcast": broadcast_support,
|
||||
"retry_on_empty": retry_on_empty,
|
||||
"retry_on_invalid": retry_on_error,
|
||||
"retries": retries,
|
||||
"reset_socket": reset_socket
|
||||
}
|
||||
|
||||
|
||||
@main.command("tcp")
|
||||
@click.pass_context
|
||||
@click.option(
|
||||
"--host",
|
||||
default='localhost',
|
||||
help="Modbus TCP IP "
|
||||
)
|
||||
@click.option(
|
||||
"--port",
|
||||
default=502,
|
||||
type=int,
|
||||
help="Modbus TCP port",
|
||||
)
|
||||
@click.option(
|
||||
"--framer",
|
||||
default='tcp',
|
||||
type=str,
|
||||
help="Override the default packet framer tcp|rtu",
|
||||
)
|
||||
def tcp(ctx, host, port, framer):
|
||||
from pymodbus.repl.client.mclient import ModbusTcpClient
|
||||
kwargs = dict(host=host, port=port)
|
||||
kwargs.update(**ctx.obj)
|
||||
if framer == 'rtu':
|
||||
from pymodbus.framer.rtu_framer import ModbusRtuFramer
|
||||
kwargs['framer'] = ModbusRtuFramer
|
||||
client = ModbusTcpClient(**kwargs)
|
||||
cli(client)
|
||||
|
||||
|
||||
@main.command("serial")
|
||||
@click.pass_context
|
||||
@click.option(
|
||||
"--method",
|
||||
default='rtu',
|
||||
type=str,
|
||||
help="Modbus Serial Mode (rtu/ascii)",
|
||||
)
|
||||
@click.option(
|
||||
"--port",
|
||||
default=None,
|
||||
type=str,
|
||||
help="Modbus RTU port",
|
||||
)
|
||||
@click.option(
|
||||
"--baudrate",
|
||||
help="Modbus RTU serial baudrate to use. Defaults to 9600",
|
||||
default=9600,
|
||||
type=int
|
||||
)
|
||||
@click.option(
|
||||
"--bytesize",
|
||||
help="Modbus RTU serial Number of data bits. "
|
||||
"Possible values: FIVEBITS, SIXBITS, SEVENBITS, "
|
||||
"EIGHTBITS. Defaults to 8",
|
||||
type=NumericChoice(["5", "6", "7", "8"], int),
|
||||
default="8"
|
||||
)
|
||||
@click.option(
|
||||
"--parity",
|
||||
help="Modbus RTU serial parity. "
|
||||
" Enable parity checking. Possible values: "
|
||||
"PARITY_NONE, PARITY_EVEN, PARITY_ODD PARITY_MARK, "
|
||||
"PARITY_SPACE. Default to 'N'",
|
||||
default='N',
|
||||
type=CaseInsenstiveChoice(['N', 'E', 'O', 'M', 'S'])
|
||||
)
|
||||
@click.option(
|
||||
"--stopbits",
|
||||
help="Modbus RTU serial stop bits. "
|
||||
"Number of stop bits. Possible values: STOPBITS_ONE, "
|
||||
"STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO. Default to '1'",
|
||||
default="1",
|
||||
type=NumericChoice(["1", "1.5", "2"], float),
|
||||
)
|
||||
@click.option(
|
||||
"--xonxoff",
|
||||
help="Modbus RTU serial xonxoff. Enable software flow control."
|
||||
"Defaults to 0",
|
||||
default=0,
|
||||
type=int
|
||||
)
|
||||
@click.option(
|
||||
"--rtscts",
|
||||
help="Modbus RTU serial rtscts. Enable hardware (RTS/CTS) flow "
|
||||
"control. Defaults to 0",
|
||||
default=0,
|
||||
type=int
|
||||
)
|
||||
@click.option(
|
||||
"--dsrdtr",
|
||||
help="Modbus RTU serial dsrdtr. Enable hardware (DSR/DTR) flow "
|
||||
"control. Defaults to 0",
|
||||
default=0,
|
||||
type=int
|
||||
)
|
||||
@click.option(
|
||||
"--timeout",
|
||||
help="Modbus RTU serial read timeout. Defaults to 0.025 sec",
|
||||
default=0.25,
|
||||
type=float
|
||||
)
|
||||
@click.option(
|
||||
"--write-timeout",
|
||||
help="Modbus RTU serial write timeout. Defaults to 2 sec",
|
||||
default=2,
|
||||
type=float
|
||||
)
|
||||
def serial(ctx, method, port, baudrate, bytesize, parity, stopbits, xonxoff,
|
||||
rtscts, dsrdtr, timeout, write_timeout):
|
||||
from pymodbus.repl.client.mclient import ModbusSerialClient
|
||||
client = ModbusSerialClient(method=method,
|
||||
port=port,
|
||||
baudrate=baudrate,
|
||||
bytesize=bytesize,
|
||||
parity=parity,
|
||||
stopbits=stopbits,
|
||||
xonxoff=xonxoff,
|
||||
rtscts=rtscts,
|
||||
dsrdtr=dsrdtr,
|
||||
timeout=timeout,
|
||||
write_timeout=write_timeout,
|
||||
**ctx.obj)
|
||||
cli(client)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,734 +0,0 @@
|
||||
"""
|
||||
Modbus Clients to be used with REPL.
|
||||
|
||||
Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import functools
|
||||
from pymodbus.pdu import ModbusExceptions, ExceptionResponse
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.client.sync import ModbusSerialClient as _ModbusSerialClient
|
||||
from pymodbus.client.sync import ModbusTcpClient as _ModbusTcpClient
|
||||
from pymodbus.mei_message import ReadDeviceInformationRequest
|
||||
from pymodbus.other_message import (ReadExceptionStatusRequest,
|
||||
ReportSlaveIdRequest,
|
||||
GetCommEventCounterRequest,
|
||||
GetCommEventLogRequest)
|
||||
from pymodbus.diag_message import (
|
||||
ReturnQueryDataRequest,
|
||||
RestartCommunicationsOptionRequest,
|
||||
ReturnDiagnosticRegisterRequest,
|
||||
ChangeAsciiInputDelimiterRequest,
|
||||
ForceListenOnlyModeRequest,
|
||||
ClearCountersRequest,
|
||||
ReturnBusMessageCountRequest,
|
||||
ReturnBusCommunicationErrorCountRequest,
|
||||
ReturnBusExceptionErrorCountRequest,
|
||||
ReturnSlaveMessageCountRequest,
|
||||
ReturnSlaveNoResponseCountRequest,
|
||||
ReturnSlaveNAKCountRequest,
|
||||
ReturnSlaveBusyCountRequest,
|
||||
ReturnSlaveBusCharacterOverrunCountRequest,
|
||||
ReturnIopOverrunCountRequest,
|
||||
ClearOverrunCountRequest,
|
||||
GetClearModbusPlusRequest)
|
||||
|
||||
|
||||
def make_response_dict(resp):
|
||||
rd = {
|
||||
'function_code': resp.function_code,
|
||||
'address': resp.address
|
||||
}
|
||||
if hasattr(resp, "value"):
|
||||
rd['value'] = resp.value
|
||||
elif hasattr(resp, 'values'):
|
||||
rd['values'] = resp.values
|
||||
elif hasattr(resp, 'count'):
|
||||
rd['count'] = resp.count
|
||||
|
||||
return rd
|
||||
|
||||
|
||||
def handle_brodcast(func):
|
||||
@functools.wraps(func)
|
||||
def _wrapper(*args, **kwargs):
|
||||
self = args[0]
|
||||
resp = func(*args, **kwargs)
|
||||
if kwargs.get("unit") == 0 and self.broadcast_enable:
|
||||
return {
|
||||
'broadcasted': True
|
||||
}
|
||||
if not resp.isError():
|
||||
return make_response_dict(resp)
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp, **kwargs)
|
||||
return _wrapper
|
||||
|
||||
|
||||
class ExtendedRequestSupport(object):
|
||||
|
||||
@staticmethod
|
||||
def _process_exception(resp, **kwargs):
|
||||
unit = kwargs.get("unit")
|
||||
if unit == 0:
|
||||
err = {
|
||||
"message": "Broadcast message, ignoring errors!!!"
|
||||
}
|
||||
else:
|
||||
if isinstance(resp, ExceptionResponse):
|
||||
err = {
|
||||
'original_function_code': "{} ({})".format(
|
||||
resp.original_code, hex(resp.original_code)),
|
||||
'error_function_code': "{} ({})".format(
|
||||
resp.function_code, hex(resp.function_code)),
|
||||
'exception code': resp.exception_code,
|
||||
'message': ModbusExceptions.decode(resp.exception_code)
|
||||
}
|
||||
elif isinstance(resp, ModbusIOException):
|
||||
err = {
|
||||
'original_function_code': "{} ({})".format(
|
||||
resp.fcode, hex(resp.fcode)),
|
||||
'error': resp.message
|
||||
}
|
||||
else:
|
||||
err = {
|
||||
'error': str(resp)
|
||||
}
|
||||
return err
|
||||
|
||||
def read_coils(self, address, count=1, **kwargs):
|
||||
"""
|
||||
Reads `count` coils from a given slave starting at `address`.
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of coils to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: List of register values
|
||||
"""
|
||||
resp = super(ExtendedRequestSupport, self).read_coils(address,
|
||||
count, **kwargs)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'bits': resp.bits
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def read_discrete_inputs(self, address, count=1, **kwargs):
|
||||
"""
|
||||
Reads `count` number of discrete inputs starting at offset `address`.
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of coils to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return: List of bits
|
||||
"""
|
||||
resp = super(ExtendedRequestSupport,
|
||||
self).read_discrete_inputs(address, count, **kwargs)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'bits': resp.bits
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
@handle_brodcast
|
||||
def write_coil(self, address, value, **kwargs):
|
||||
"""
|
||||
Write `value` to coil at `address`.
|
||||
|
||||
:param address: coil offset to write to
|
||||
:param value: bit value to write
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
resp = super(ExtendedRequestSupport, self).write_coil(
|
||||
address, value, **kwargs)
|
||||
return resp
|
||||
|
||||
@handle_brodcast
|
||||
def write_coils(self, address, values, **kwargs):
|
||||
"""
|
||||
Write `value` to coil at `address`.
|
||||
|
||||
:param address: coil offset to write to
|
||||
:param values: list of bit values to write (comma separated)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
resp = super(ExtendedRequestSupport, self).write_coils(
|
||||
address, values, **kwargs)
|
||||
return resp
|
||||
|
||||
@handle_brodcast
|
||||
def write_register(self, address, value, **kwargs):
|
||||
"""
|
||||
Write `value` to register at `address`.
|
||||
|
||||
:param address: register offset to write to
|
||||
:param value: register value to write
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
resp = super(ExtendedRequestSupport, self).write_register(
|
||||
address, value, **kwargs)
|
||||
return resp
|
||||
|
||||
@handle_brodcast
|
||||
def write_registers(self, address, values, **kwargs):
|
||||
"""
|
||||
Write list of `values` to registers starting at `address`.
|
||||
|
||||
:param address: register offset to write to
|
||||
:param values: list of register value to write (comma separated)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
resp = super(ExtendedRequestSupport, self).write_registers(
|
||||
address, values, **kwargs)
|
||||
return resp
|
||||
|
||||
def read_holding_registers(self, address, count=1, **kwargs):
|
||||
"""
|
||||
Read `count` number of holding registers starting at `address`.
|
||||
|
||||
:param address: starting register offset to read from
|
||||
:param count: Number of registers to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
resp = super(ExtendedRequestSupport, self).read_holding_registers(
|
||||
address, count, **kwargs)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'registers': resp.registers
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def read_input_registers(self, address, count=1, **kwargs):
|
||||
"""
|
||||
Read `count` number of input registers starting at `address`.
|
||||
|
||||
:param address: starting register offset to read from to
|
||||
:param count: Number of registers to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
resp = super(ExtendedRequestSupport, self).read_input_registers(
|
||||
address, count, **kwargs)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'registers': resp.registers
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def readwrite_registers(self, read_address, read_count, write_address,
|
||||
write_registers, **kwargs):
|
||||
"""
|
||||
Read `read_count` number of holding registers starting at \
|
||||
`read_address` and write `write_registers` \
|
||||
starting at `write_address`.
|
||||
|
||||
:param read_address: register offset to read from
|
||||
:param read_count: Number of registers to read
|
||||
:param write_address: register offset to write to
|
||||
:param write_registers: List of register values to write (comma separated)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
resp = super(ExtendedRequestSupport, self).readwrite_registers(
|
||||
read_address=read_address,
|
||||
read_count=read_count,
|
||||
write_address=write_address,
|
||||
write_registers=write_registers,
|
||||
**kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'registers': resp.registers
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def mask_write_register(self, address=0x0000,
|
||||
and_mask=0xffff, or_mask=0x0000, **kwargs):
|
||||
"""
|
||||
Mask content of holding register at `address` \
|
||||
with `and_mask` and `or_mask`.
|
||||
|
||||
:param address: Reference address of register
|
||||
:param and_mask: And Mask
|
||||
:param or_mask: OR Mask
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
resp = super(ExtendedRequestSupport, self).read_input_registers(
|
||||
address=address, and_mask=and_mask, or_mask=or_mask, **kwargs)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'address': resp.address,
|
||||
'and mask': resp.and_mask,
|
||||
'or mask': resp.or_mask
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def read_device_information(self, read_code=None,
|
||||
object_id=0x00, **kwargs):
|
||||
"""
|
||||
Read the identification and additional information of remote slave.
|
||||
|
||||
:param read_code: Read Device ID code (0x01/0x02/0x03/0x04)
|
||||
:param object_id: Identification of the first object to obtain.
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReadDeviceInformationRequest(read_code, object_id, **kwargs)
|
||||
resp = self.execute(request)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'information': resp.information,
|
||||
'object count': resp.number_of_objects,
|
||||
'conformity': resp.conformity,
|
||||
'next object id': resp.next_object_id,
|
||||
'more follows': resp.more_follows,
|
||||
'space left': resp.space_left
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def report_slave_id(self, **kwargs):
|
||||
"""
|
||||
Report information about remote slave ID.
|
||||
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReportSlaveIdRequest(**kwargs)
|
||||
resp = self.execute(request)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'identifier': resp.identifier.decode('cp1252'),
|
||||
'status': resp.status,
|
||||
'byte count': resp.byte_count
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def read_exception_status(self, **kwargs):
|
||||
"""
|
||||
Read the contents of eight Exception Status outputs in a remote \
|
||||
device.
|
||||
|
||||
:param unit: The slave unit this request is targeting
|
||||
|
||||
:return:
|
||||
|
||||
"""
|
||||
request = ReadExceptionStatusRequest(**kwargs)
|
||||
resp = self.execute(request)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'status': resp.status
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def get_com_event_counter(self, **kwargs):
|
||||
"""
|
||||
Read status word and an event count from the remote device's \
|
||||
communication event counter.
|
||||
|
||||
:param unit: The slave unit this request is targeting
|
||||
|
||||
:return:
|
||||
|
||||
"""
|
||||
request = GetCommEventCounterRequest(**kwargs)
|
||||
resp = self.execute(request)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'status': resp.status,
|
||||
'count': resp.count
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def get_com_event_log(self, **kwargs):
|
||||
"""
|
||||
Read status word, event count, message count, and a field of event
|
||||
bytes from the remote device.
|
||||
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = GetCommEventLogRequest(**kwargs)
|
||||
resp = self.execute(request)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function_code': resp.function_code,
|
||||
'status': resp.status,
|
||||
'message count': resp.message_count,
|
||||
'event count': resp.event_count,
|
||||
'events': resp.events,
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def _execute_diagnostic_request(self, request):
|
||||
resp = self.execute(request)
|
||||
if not resp.isError():
|
||||
return {
|
||||
'function code': resp.function_code,
|
||||
'sub function code': resp.sub_function_code,
|
||||
'message': resp.message
|
||||
}
|
||||
else:
|
||||
return ExtendedRequestSupport._process_exception(resp)
|
||||
|
||||
def return_query_data(self, message=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command , Loop back data sent in response.
|
||||
|
||||
:param message: Message to be looped back
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnQueryDataRequest(message, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def restart_comm_option(self, toggle=False, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, initialize and restart remote devices serial \
|
||||
interface and clear all of its communications event counters .
|
||||
|
||||
:param toggle: Toggle Status [ON(0xff00)/OFF(0x0000]
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = RestartCommunicationsOptionRequest(toggle, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_diagnostic_register(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Read 16-bit diagnostic register.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnDiagnosticRegisterRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def change_ascii_input_delimiter(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Change message delimiter for future requests.
|
||||
|
||||
:param data: New delimiter character
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ChangeAsciiInputDelimiterRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def force_listen_only_mode(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Forces the addressed remote device to \
|
||||
its Listen Only Mode.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ForceListenOnlyModeRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def clear_counters(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Clear all counters and diag registers.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ClearCountersRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_bus_message_count(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Return count of message detected on bus \
|
||||
by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnBusMessageCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_bus_com_error_count(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Return count of CRC errors \
|
||||
received by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnBusCommunicationErrorCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_bus_exception_error_count(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Return count of Modbus exceptions \
|
||||
returned by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnBusExceptionErrorCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_message_count(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Return count of messages addressed to \
|
||||
remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveMessageCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_no_response_count(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Return count of No responses by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveNoResponseCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_no_ack_count(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Return count of NO ACK exceptions sent \
|
||||
by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveNAKCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_busy_count(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Return count of server busy exceptions sent \
|
||||
by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveBusyCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_bus_char_overrun_count(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Return count of messages not handled \
|
||||
by remote slave due to character overrun condition.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveBusCharacterOverrunCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_iop_overrun_count(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Return count of iop overrun errors \
|
||||
by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ReturnIopOverrunCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def clear_overrun_count(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Clear over run counter.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = ClearOverrunCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def get_clear_modbus_plus(self, data=0, **kwargs):
|
||||
"""
|
||||
Diagnostic sub command, Get or clear stats of remote \
|
||||
modbus plus device.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param unit: The slave unit this request is targeting
|
||||
:return:
|
||||
"""
|
||||
request = GetClearModbusPlusRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
|
||||
class ModbusSerialClient(ExtendedRequestSupport, _ModbusSerialClient):
|
||||
def __init__(self, method, **kwargs):
|
||||
super(ModbusSerialClient, self).__init__(method, **kwargs)
|
||||
|
||||
def get_port(self):
|
||||
"""
|
||||
Serial Port.
|
||||
|
||||
:return: Current Serial port
|
||||
"""
|
||||
return self.port
|
||||
|
||||
def set_port(self, value):
|
||||
"""
|
||||
Serial Port setter.
|
||||
|
||||
:param value: New port
|
||||
"""
|
||||
self.port = value
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_stopbits(self):
|
||||
"""
|
||||
Number of stop bits.
|
||||
|
||||
:return: Current Stop bits
|
||||
"""
|
||||
return self.stopbits
|
||||
|
||||
def set_stopbits(self, value):
|
||||
"""
|
||||
Stop bit setter.
|
||||
|
||||
:param value: Possible values (1, 1.5, 2)
|
||||
"""
|
||||
self.stopbits = float(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_bytesize(self):
|
||||
"""
|
||||
Number of data bits.
|
||||
|
||||
:return: Current bytesize
|
||||
"""
|
||||
return self.bytesize
|
||||
|
||||
def set_bytesize(self, value):
|
||||
"""
|
||||
Byte size setter.
|
||||
|
||||
:param value: Possible values (5, 6, 7, 8)
|
||||
|
||||
"""
|
||||
self.bytesize = int(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_parity(self):
|
||||
"""
|
||||
Enable Parity Checking.
|
||||
|
||||
:return: Current parity setting
|
||||
"""
|
||||
return self.parity
|
||||
|
||||
def set_parity(self, value):
|
||||
"""
|
||||
Parity Setter.
|
||||
|
||||
:param value: Possible values ('N', 'E', 'O', 'M', 'S')
|
||||
"""
|
||||
self.parity = value
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_baudrate(self):
|
||||
"""
|
||||
Serial Port baudrate.
|
||||
|
||||
:return: Current baudrate
|
||||
"""
|
||||
return self.baudrate
|
||||
|
||||
def set_baudrate(self, value):
|
||||
"""
|
||||
Baudrate setter.
|
||||
|
||||
:param value: <supported baudrate>
|
||||
"""
|
||||
self.baudrate = int(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_timeout(self):
|
||||
"""
|
||||
Serial Port Read timeout.
|
||||
|
||||
:return: Current read imeout.
|
||||
"""
|
||||
return self.timeout
|
||||
|
||||
def set_timeout(self, value):
|
||||
"""
|
||||
Read timeout setter.
|
||||
|
||||
:param value: Read Timeout in seconds
|
||||
"""
|
||||
self.timeout = float(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_serial_settings(self):
|
||||
"""
|
||||
Gets Current Serial port settings.
|
||||
|
||||
:return: Current Serial settings as dict.
|
||||
"""
|
||||
return {
|
||||
'baudrate': self.baudrate,
|
||||
'port': self.port,
|
||||
'parity': self.parity,
|
||||
'stopbits': self.stopbits,
|
||||
'bytesize': self.bytesize,
|
||||
'read timeout': self.timeout,
|
||||
't1.5': self.inter_char_timeout,
|
||||
't3.5': self.silent_interval
|
||||
}
|
||||
|
||||
|
||||
class ModbusTcpClient(ExtendedRequestSupport, _ModbusTcpClient):
|
||||
def __init__(self, **kwargs):
|
||||
super(ModbusTcpClient, self).__init__(**kwargs)
|
||||
@@ -1,4 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2020 by RiptideIO
|
||||
All rights reserved.
|
||||
"""
|
||||
@@ -1,208 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2020 by RiptideIO
|
||||
All rights reserved.
|
||||
"""
|
||||
import json
|
||||
import click
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
from prompt_toolkit.shortcuts import clear
|
||||
from prompt_toolkit.shortcuts.progress_bar import formatters
|
||||
from prompt_toolkit.styles import Style
|
||||
|
||||
from prompt_toolkit import PromptSession, print_formatted_text
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from prompt_toolkit.completion import NestedCompleter
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TITLE = """
|
||||
__________ .______. _________
|
||||
\______ \___.__. _____ ____ __| _/\_ |__ __ __ ______ / _____/ ______________ __ ___________
|
||||
| ___< | |/ \ / _ \ / __ | | __ \| | \/ ___/ \_____ \_/ __ \_ __ \ \/ // __ \_ __ \\
|
||||
| | \___ | Y Y ( <_> ) /_/ | | \_\ \ | /\___ \ / \ ___/| | \/\ /\ ___/| | \/
|
||||
|____| / ____|__|_| /\____/\____ | |___ /____//____ > /_______ /\___ >__| \_/ \___ >__|
|
||||
\/ \/ \/ \/ \/ \/ \/ \/
|
||||
"""
|
||||
|
||||
SMALL_TITLE = "Pymodbus server..."
|
||||
BOTTOM_TOOLBAR = HTML('(MODBUS SERVER) <b><style bg="ansired">Press Ctrl+C or '
|
||||
'type "exit" to quit</style></b> Type "help" '
|
||||
'for list of available commands')
|
||||
COMMAND_ARGS = ["response_type", "error_code", "delay_by",
|
||||
"clear_after", "data_len"]
|
||||
RESPONSE_TYPES = ["normal", "error", "delayed", "empty", "stray"]
|
||||
COMMANDS = {
|
||||
"manipulator": {
|
||||
"response_type": None,
|
||||
"error_code": None,
|
||||
"delay_by": None,
|
||||
"clear_after": None
|
||||
},
|
||||
"exit": None,
|
||||
"help": None,
|
||||
"clear": None
|
||||
}
|
||||
USAGE = "manipulator response_type=|normal|error|delayed|empty|stray \n" \
|
||||
"\tAdditional parameters\n" \
|
||||
"\t\terror_code=<int> \n\t\tdelay_by=<in seconds> \n\t\t" \
|
||||
"clear_after=<clear after n messages int>" \
|
||||
"\n\t\tdata_len=<length of stray data (int)>\n" \
|
||||
"\n\tExample usage: \n\t" \
|
||||
"1. Send error response 3 for 4 requests\n\t" \
|
||||
" <ansiblue>manipulator response_type=error error_code=3 clear_after=4</ansiblue>\n\t" \
|
||||
"2. Delay outgoing response by 5 seconds indefinitely\n\t" \
|
||||
" <ansiblue>manipulator response_type=delayed delay_by=5</ansiblue>\n\t" \
|
||||
"3. Send empty response\n\t" \
|
||||
" <ansiblue>manipulator response_type=empty</ansiblue>\n\t" \
|
||||
"4. Send stray response of lenght 12 and revert to normal after 2 responses\n\t" \
|
||||
" <ansiblue>manipulator response_type=stray data_len=11 clear_after=2</ansiblue>\n\t" \
|
||||
"5. To disable response manipulation\n\t" \
|
||||
" <ansiblue>manipulator response_type=normal</ansiblue>"
|
||||
COMMAND_HELPS = {
|
||||
"manipulator": "Manipulate response from server.\nUsage: {}".format(USAGE),
|
||||
"clear": "Clears screen"
|
||||
|
||||
}
|
||||
|
||||
|
||||
STYLE = Style.from_dict({"": "cyan"})
|
||||
CUSTOM_FORMATTERS = [
|
||||
formatters.Label(suffix=": "),
|
||||
formatters.Bar(start="|", end="|", sym_a="#", sym_b="#", sym_c="-"),
|
||||
formatters.Text(" "),
|
||||
formatters.Text(" "),
|
||||
formatters.TimeElapsed(),
|
||||
formatters.Text(" "),
|
||||
]
|
||||
|
||||
|
||||
def info(message):
|
||||
if not isinstance(message, str):
|
||||
message = str(message)
|
||||
click.secho(message, fg="green")
|
||||
|
||||
|
||||
def warning(message):
|
||||
click.secho(str(message), fg="yellow")
|
||||
|
||||
|
||||
def error(message):
|
||||
click.secho(str(message), fg="red")
|
||||
|
||||
|
||||
def get_terminal_width():
|
||||
return shutil.get_terminal_size()[0]
|
||||
|
||||
|
||||
def print_help():
|
||||
print_formatted_text(HTML("<u>Available commands:</u>"))
|
||||
for cmd, hlp in sorted(COMMAND_HELPS.items()):
|
||||
print_formatted_text(
|
||||
HTML("<skyblue>{:45s}</skyblue><seagreen>{:100s}</seagreen>".format(cmd, hlp))
|
||||
)
|
||||
|
||||
|
||||
async def interactive_shell(server):
|
||||
"""
|
||||
CLI interactive shell
|
||||
"""
|
||||
col = get_terminal_width()
|
||||
max_len = max([len(t) for t in TITLE.split("\n")])
|
||||
if col > max_len:
|
||||
info(TITLE)
|
||||
else:
|
||||
print_formatted_text(HTML('<u><b><style color="green">{}'
|
||||
'</style></b></u>'.format(SMALL_TITLE)))
|
||||
info("")
|
||||
completer = NestedCompleter.from_nested_dict(COMMANDS)
|
||||
session = PromptSession("SERVER > ",
|
||||
completer=completer,
|
||||
bottom_toolbar=BOTTOM_TOOLBAR)
|
||||
|
||||
# Run echo loop. Read text from stdin, and reply it back.
|
||||
while True:
|
||||
try:
|
||||
invalid_command = False
|
||||
result = await session.prompt_async()
|
||||
if result == "exit":
|
||||
await server.web_app.shutdown()
|
||||
break
|
||||
if result == "help":
|
||||
print_help()
|
||||
continue
|
||||
if result == "clear":
|
||||
clear()
|
||||
continue
|
||||
command = result.split()
|
||||
if command:
|
||||
if command[0] not in COMMANDS:
|
||||
invalid_command = True
|
||||
if invalid_command:
|
||||
warning("Invalid command or invalid usage of command - {}".format(command))
|
||||
continue
|
||||
if len(command) == 1:
|
||||
warning("Usage: '{}'".format(USAGE))
|
||||
else:
|
||||
args = command[1:]
|
||||
skip_next = False
|
||||
val_dict = {}
|
||||
for index, arg in enumerate(args):
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if "=" in arg:
|
||||
arg, value = arg.split("=")
|
||||
else:
|
||||
if arg in COMMAND_ARGS:
|
||||
try:
|
||||
value = args[index+1]
|
||||
skip_next = True
|
||||
except IndexError:
|
||||
error("Missing value "
|
||||
"for argument - {}".format(arg))
|
||||
warning("Usage: '{}'".format(USAGE))
|
||||
break
|
||||
valid = True
|
||||
if arg == "response_type":
|
||||
if value not in RESPONSE_TYPES:
|
||||
warning("Invalid response "
|
||||
"type request - {}".format(value))
|
||||
warning("Choose from {}".format(RESPONSE_TYPES))
|
||||
valid = False
|
||||
elif arg in ["error_code", "delay_by",
|
||||
"clear_after", "data_len"]:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
warning("Expected integer "
|
||||
"value for {}".format(arg))
|
||||
valid = False
|
||||
|
||||
if valid:
|
||||
val_dict[arg] = value
|
||||
if val_dict:
|
||||
server.update_manipulator_config(val_dict)
|
||||
# server.manipulator_config = val_dict
|
||||
# result = await run_command(tester, *command)
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
return
|
||||
|
||||
|
||||
async def main(server):
|
||||
# with patch_stdout():
|
||||
try:
|
||||
await interactive_shell(server)
|
||||
finally:
|
||||
pass
|
||||
warning("Bye Bye!!!")
|
||||
|
||||
|
||||
async def run_repl(server):
|
||||
await main(server)
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2020 by RiptideIO
|
||||
All rights reserved.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import click
|
||||
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
|
||||
from pymodbus.framer.socket_framer import ModbusSocketFramer
|
||||
from pymodbus.server.reactive.main import (
|
||||
ReactiveServer, DEFAULT_FRAMER, DEFUALT_HANDLERS)
|
||||
from pymodbus.server.reactive.default_config import DEFUALT_CONFIG
|
||||
from pymodbus.repl.server.cli import run_repl
|
||||
|
||||
if IS_PYTHON3 and PYTHON_VERSION > (3, 7):
|
||||
CANCELLED_ERROR = asyncio.exceptions.CancelledError
|
||||
else:
|
||||
CANCELLED_ERROR = asyncio.CancelledError
|
||||
|
||||
|
||||
@click.group("ReactiveModbusServer")
|
||||
@click.option("--host", default="localhost", help="Host address")
|
||||
@click.option("--web-port", default=8080, help="Web app port")
|
||||
@click.option("--broadcast-support", is_flag=True,
|
||||
default=False, help="Support broadcast messages")
|
||||
@click.option("--repl/--no-repl", is_flag=True,
|
||||
default=True, help="Enable/Disable repl for server")
|
||||
@click.option("--verbose", is_flag=True,
|
||||
help="Run with debug logs enabled for pymodbus")
|
||||
@click.pass_context
|
||||
def server(ctx, host, web_port, broadcast_support, repl, verbose):
|
||||
global logger
|
||||
import logging
|
||||
FORMAT = ('%(asctime)-15s %(threadName)-15s'
|
||||
' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
|
||||
pymodbus_logger = logging.getLogger("pymodbus")
|
||||
logging.basicConfig(format=FORMAT)
|
||||
logger = logging.getLogger(__name__)
|
||||
if verbose:
|
||||
pymodbus_logger.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
pymodbus_logger.setLevel(logging.ERROR)
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
ctx.obj = {"repl": repl, "host": host, "web_port": web_port,
|
||||
"broadcast": broadcast_support}
|
||||
|
||||
|
||||
@server.command("run")
|
||||
@click.option("--modbus-server", default="tcp",
|
||||
type=click.Choice(["tcp", "serial", "tls", "udp"],
|
||||
case_sensitive=False),
|
||||
help="Modbus server")
|
||||
@click.option("--modbus-framer", default="socket",
|
||||
type=click.Choice(["socket", "rtu", "tls", "ascii", "binary"],
|
||||
case_sensitive=False),
|
||||
help="Modbus framer to use")
|
||||
@click.option("--modbus-port", default="5020", help="Modbus port")
|
||||
@click.option("--modbus-unit-id", default=[1], type=int,
|
||||
multiple=True, help="Modbus unit id")
|
||||
@click.option("--modbus-config", type=click.Path(exists=True),
|
||||
help="Path to additional modbus server config")
|
||||
@click.option("-r", "--randomize", default=0, help="Randomize every `r` reads."
|
||||
" 0=never, 1=always, "
|
||||
"2=every-second-read, "
|
||||
"and so on. "
|
||||
"Applicable IR and DI.")
|
||||
@click.pass_context
|
||||
def run(ctx, modbus_server, modbus_framer, modbus_port, modbus_unit_id,
|
||||
modbus_config, randomize):
|
||||
"""
|
||||
Run Reactive Modbus server exposing REST endpoint
|
||||
for response manipulation.
|
||||
"""
|
||||
if not IS_PYTHON3:
|
||||
click.secho("Pymodbus Server REPL not supported on python2", fg="read")
|
||||
exit(1)
|
||||
repl = ctx.obj.pop("repl")
|
||||
web_app_config = ctx.obj
|
||||
loop = asyncio.get_event_loop()
|
||||
framer = DEFAULT_FRAMER.get(modbus_framer, ModbusSocketFramer)
|
||||
if modbus_config:
|
||||
with open(modbus_config) as f:
|
||||
modbus_config = json.load(f)
|
||||
else:
|
||||
modbus_config = DEFUALT_CONFIG
|
||||
modbus_config = modbus_config.get(modbus_server, {})
|
||||
if modbus_server != "serial":
|
||||
modbus_port = int(modbus_port)
|
||||
handler = modbus_config.pop("handler", "ModbusConnectedRequestHandler")
|
||||
else:
|
||||
handler = modbus_config.pop("handler", "ModbusSingleRequestHandler")
|
||||
handler = DEFUALT_HANDLERS.get(handler.strip())
|
||||
|
||||
modbus_config["handler"] = handler
|
||||
modbus_config["randomize"] = randomize
|
||||
app = ReactiveServer.factory(modbus_server, framer,
|
||||
modbus_port=modbus_port,
|
||||
unit=modbus_unit_id,
|
||||
loop=loop,
|
||||
**web_app_config, **modbus_config)
|
||||
try:
|
||||
if repl:
|
||||
loop.run_until_complete(app.run_async())
|
||||
|
||||
loop.run_until_complete(run_repl(app))
|
||||
loop.run_forever()
|
||||
else:
|
||||
app.run()
|
||||
|
||||
except CANCELLED_ERROR:
|
||||
print("Done!!!!!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
server()
|
||||
@@ -1,2 +0,0 @@
|
||||
'''
|
||||
'''
|
||||
@@ -1,963 +0,0 @@
|
||||
"""
|
||||
Implementation of a Threaded Modbus Server
|
||||
------------------------------------------
|
||||
|
||||
"""
|
||||
from binascii import b2a_hex
|
||||
import serial
|
||||
from serial_asyncio import create_serial_connection
|
||||
import ssl
|
||||
import traceback
|
||||
|
||||
import asyncio
|
||||
from pymodbus.compat import PYTHON_VERSION
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from pymodbus.factory import ServerDecoder
|
||||
from pymodbus.datastore import ModbusServerContext
|
||||
from pymodbus.device import ModbusControlBlock
|
||||
from pymodbus.device import ModbusDeviceIdentification
|
||||
from pymodbus.transaction import *
|
||||
from pymodbus.exceptions import NotImplementedException, NoSuchSlaveException
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.compat import socketserver, byte2int
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Protocol Handlers
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class ModbusBaseRequestHandler(asyncio.BaseProtocol):
|
||||
""" Implements modbus slave wire protocol
|
||||
This uses the asyncio.Protocol to implement the client handler.
|
||||
|
||||
When a connection is established, the asyncio.Protocol.connection_made
|
||||
callback is called. This callback will setup the connection and
|
||||
create and schedule an asyncio.Task and assign it to running_task.
|
||||
|
||||
running_task will be canceled upon connection_lost event.
|
||||
"""
|
||||
def __init__(self, owner):
|
||||
self.server = owner
|
||||
self.running = False
|
||||
self.receive_queue = asyncio.Queue()
|
||||
self.handler_task = None # coroutine to be run on asyncio loop
|
||||
|
||||
def _log_exception(self):
|
||||
if isinstance(self, ModbusConnectedRequestHandler):
|
||||
_logger.error(
|
||||
"Handler for stream [%s:%s] has "
|
||||
"been canceled" % self.client_address[:2])
|
||||
elif isinstance(self, ModbusSingleRequestHandler):
|
||||
_logger.error(
|
||||
"Handler for serial port has been cancelled")
|
||||
else:
|
||||
sock_name = self.protocol._sock.getsockname()
|
||||
_logger.error("Handler for UDP socket [%s] has "
|
||||
"been canceled" % sock_name[1])
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""
|
||||
asyncio.BaseProtocol callback for socket establish
|
||||
|
||||
For streamed protocols (TCP) this will also correspond to an
|
||||
entire conversation; however for datagram protocols (UDP) this
|
||||
corresponds to the socket being opened
|
||||
"""
|
||||
try:
|
||||
sockname = transport.get_extra_info('sockname')
|
||||
if sockname is not None:
|
||||
_logger.debug(
|
||||
"Socket [%s:%s] opened" % transport.get_extra_info(
|
||||
'sockname')[:2])
|
||||
else:
|
||||
if hasattr(transport, 'serial'):
|
||||
_logger.debug(
|
||||
"Serial connection opened on port: {}".format(
|
||||
transport.serial.port)
|
||||
)
|
||||
self.transport = transport
|
||||
self.running = True
|
||||
self.framer = self.server.framer(self.server.decoder, client=None)
|
||||
|
||||
# schedule the connection handler on the event loop
|
||||
if PYTHON_VERSION >= (3, 7):
|
||||
self.handler_task = asyncio.create_task(self.handle())
|
||||
else:
|
||||
self.handler_task = asyncio.ensure_future(self.handle())
|
||||
except Exception as ex: # pragma: no cover
|
||||
_logger.error("Datastore unable to fulfill request: "
|
||||
"%s; %s", ex, traceback.format_exc())
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""
|
||||
asyncio.BaseProtocol callback for socket tear down
|
||||
|
||||
For streamed protocols any break in the network connection will
|
||||
be reported here; for datagram protocols, only a teardown of the
|
||||
socket itself will result in this call.
|
||||
"""
|
||||
try:
|
||||
self.handler_task.cancel()
|
||||
if exc is None:
|
||||
self._log_exception()
|
||||
else: # pragma: no cover
|
||||
if hasattr(self, "client_address"): # TCP connection
|
||||
_logger.debug("Client Disconnection {} due "
|
||||
"to {}".format(*self.client_address, exc))
|
||||
|
||||
self.running = False
|
||||
|
||||
except Exception as ex: # pragma: no cover
|
||||
_logger.error("Datastore unable to fulfill request: "
|
||||
"%s; %s", ex, traceback.format_exc())
|
||||
|
||||
async def handle(self):
|
||||
"""Asyncio coroutine which represents a single conversation between
|
||||
the modbus slave and master
|
||||
|
||||
Once the client connection is established, the data chunks will be
|
||||
fed to this coroutine via the asyncio.Queue object which is fed by
|
||||
the ModbusBaseRequestHandler class's callback Future.
|
||||
|
||||
This callback future gets data from either
|
||||
asyncio.DatagramProtocol.datagram_received or
|
||||
from asyncio.BaseProtocol.data_received.
|
||||
|
||||
This function will execute without blocking in the while-loop and
|
||||
yield to the asyncio event loop when the frame is exhausted.
|
||||
As a result, multiple clients can be interleaved without any
|
||||
interference between them.
|
||||
|
||||
For ModbusConnectedRequestHandler, each connection will be given an
|
||||
instance of the handle() coroutine and this instance will be put in the
|
||||
active_connections dict. Calling server_close will individually cancel
|
||||
each running handle() task.
|
||||
|
||||
For ModbusDisconnectedRequestHandler, a single handle() coroutine will
|
||||
be started and maintained. Calling server_close will cancel that task.
|
||||
|
||||
"""
|
||||
reset_frame = False
|
||||
while self.running:
|
||||
try:
|
||||
units = self.server.context.slaves()
|
||||
# this is an asyncio.Queue await, it will never fail
|
||||
data = await self._recv_()
|
||||
if isinstance(data, tuple):
|
||||
# addr is populated when talking over UDP
|
||||
data, *addr = data
|
||||
else:
|
||||
addr = (None,) # empty tuple
|
||||
|
||||
if not isinstance(units, (list, tuple)):
|
||||
units = [units]
|
||||
# if broadcast is enabled make sure to
|
||||
# process requests to address 0
|
||||
if self.server.broadcast_enable: # pragma: no cover
|
||||
if 0 not in units:
|
||||
units.append(0)
|
||||
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug('Handling data: ' + hexlify_packets(data))
|
||||
|
||||
single = self.server.context.single
|
||||
self.framer.processIncomingPacket(
|
||||
data=data, callback=lambda x: self.execute(x, *addr),
|
||||
unit=units, single=single)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
# catch and ignore cancelation errors
|
||||
self._log_exception()
|
||||
except Exception as e:
|
||||
# force TCP socket termination as processIncomingPacket
|
||||
# should handle applicaiton layer errors
|
||||
# for UDP sockets, simply reset the frame
|
||||
if isinstance(self, ModbusConnectedRequestHandler):
|
||||
client_addr = self.client_address[:2]
|
||||
_logger.error("Unknown exception '{}' on stream {} "
|
||||
"forcing disconnect".format(e, client_addr))
|
||||
self.transport.close()
|
||||
else:
|
||||
_logger.error("Unknown error occurred %s" % e)
|
||||
reset_frame = True # graceful recovery
|
||||
finally:
|
||||
if reset_frame:
|
||||
self.framer.resetFrame()
|
||||
reset_frame = False
|
||||
|
||||
def execute(self, request, *addr):
|
||||
""" The callback to call with the resulting message
|
||||
|
||||
:param request: The decoded request message
|
||||
"""
|
||||
broadcast = False
|
||||
try:
|
||||
if self.server.broadcast_enable and request.unit_id == 0:
|
||||
broadcast = True
|
||||
# if broadcasting then execute on all slave contexts,
|
||||
# note response will be ignored
|
||||
for unit_id in self.server.context.slaves():
|
||||
response = request.execute(self.server.context[unit_id])
|
||||
else:
|
||||
context = self.server.context[request.unit_id]
|
||||
response = request.execute(context)
|
||||
except NoSuchSlaveException as ex:
|
||||
_logger.error("requested slave does "
|
||||
"not exist: %s" % request.unit_id)
|
||||
if self.server.ignore_missing_slaves:
|
||||
return # the client will simply timeout waiting for a response
|
||||
response = request.doException(merror.GatewayNoResponse)
|
||||
except Exception as ex:
|
||||
_logger.error("Datastore unable to fulfill request: "
|
||||
"%s; %s", ex, traceback.format_exc())
|
||||
response = request.doException(merror.SlaveFailure)
|
||||
# no response when broadcasting
|
||||
if not broadcast:
|
||||
response.transaction_id = request.transaction_id
|
||||
response.unit_id = request.unit_id
|
||||
skip_encoding = False
|
||||
if self.server.response_manipulator:
|
||||
response, skip_encoding = self.server.response_manipulator(response)
|
||||
self.send(response, *addr, skip_encoding=skip_encoding)
|
||||
|
||||
def send(self, message, *addr, **kwargs):
|
||||
def __send(msg, *addr):
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug('send: [%s]- %s' % (message, b2a_hex(msg)))
|
||||
if addr == (None,):
|
||||
self._send_(msg)
|
||||
else:
|
||||
self._send_(msg, *addr)
|
||||
skip_encoding = kwargs.get("skip_encoding", False)
|
||||
if skip_encoding:
|
||||
__send(message, *addr)
|
||||
elif message.should_respond:
|
||||
# self.server.control.Counter.BusMessage += 1
|
||||
pdu = self.framer.buildPacket(message)
|
||||
__send(pdu, *addr)
|
||||
else:
|
||||
_logger.debug("Skipping sending response!!")
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Derived class implementations
|
||||
# ----------------------------------------------------------------------- #
|
||||
|
||||
def _send_(self, data): # pragma: no cover
|
||||
""" Send a request (string) to the network
|
||||
|
||||
:param message: The unencoded modbus response
|
||||
"""
|
||||
raise NotImplementedException("Method not implemented "
|
||||
"by derived class")
|
||||
|
||||
async def _recv_(self): # pragma: no cover
|
||||
""" Receive data from the network
|
||||
|
||||
:return:
|
||||
"""
|
||||
raise NotImplementedException("Method not implemented "
|
||||
"by derived class")
|
||||
|
||||
|
||||
class ModbusConnectedRequestHandler(ModbusBaseRequestHandler,asyncio.Protocol):
|
||||
""" Implements the modbus server protocol
|
||||
|
||||
This uses asyncio.Protocol to implement
|
||||
the client handler for a connected protocol (TCP).
|
||||
"""
|
||||
|
||||
def connection_made(self, transport):
|
||||
""" asyncio.BaseProtocol: Called when a connection is made. """
|
||||
super().connection_made(transport)
|
||||
|
||||
self.client_address = transport.get_extra_info('peername')
|
||||
self.server.active_connections[self.client_address] = self
|
||||
_logger.debug("TCP client connection established "
|
||||
"[%s:%s]" % self.client_address[:2])
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""
|
||||
asyncio.BaseProtocol: Called when the connection is lost or closed.
|
||||
"""
|
||||
super().connection_lost(exc)
|
||||
client_addr = self.client_address[:2]
|
||||
_logger.debug("TCP client disconnected [%s:%s]" % client_addr)
|
||||
if self.client_address in self.server.active_connections:
|
||||
self.server.active_connections.pop(self.client_address)
|
||||
|
||||
def data_received(self, data):
|
||||
"""
|
||||
asyncio.Protocol: (TCP) Called when some data is received.
|
||||
data is a non-empty bytes object containing the incoming data.
|
||||
"""
|
||||
self.receive_queue.put_nowait(data)
|
||||
|
||||
async def _recv_(self):
|
||||
return await self.receive_queue.get()
|
||||
|
||||
def _send_(self, data):
|
||||
""" tcp send """
|
||||
self.transport.write(data)
|
||||
|
||||
|
||||
class ModbusDisconnectedRequestHandler(ModbusBaseRequestHandler,
|
||||
asyncio.DatagramProtocol):
|
||||
""" Implements the modbus server protocol
|
||||
|
||||
This uses the socketserver.BaseRequestHandler to implement
|
||||
the client handler for a disconnected protocol (UDP). The
|
||||
only difference is that we have to specify who to send the
|
||||
resulting packet data to.
|
||||
"""
|
||||
def __init__(self,owner):
|
||||
super().__init__(owner)
|
||||
_future = asyncio.get_event_loop().create_future()
|
||||
self.server.on_connection_terminated = _future
|
||||
|
||||
def connection_lost(self,exc):
|
||||
super().connection_lost(exc)
|
||||
self.server.on_connection_terminated.set_result(True)
|
||||
|
||||
def datagram_received(self,data, addr):
|
||||
"""
|
||||
asyncio.DatagramProtocol: Called when a datagram is received.
|
||||
data is a bytes object containing the incoming data. addr
|
||||
is the address of the peer sending the data; the exact
|
||||
format depends on the transport.
|
||||
"""
|
||||
self.receive_queue.put_nowait((data, addr))
|
||||
|
||||
def error_received(self,exc): # pragma: no cover
|
||||
"""
|
||||
asyncio.DatagramProtocol: Called when a previous send
|
||||
or receive operation raises an OSError. exc is the
|
||||
OSError instance.
|
||||
|
||||
This method is called in rare conditions,
|
||||
when the transport (e.g. UDP) detects that a datagram could
|
||||
not be delivered to its recipient. In many conditions
|
||||
though, undeliverable datagrams will be silently dropped.
|
||||
"""
|
||||
_logger.error("datagram connection error [%s]" % exc)
|
||||
|
||||
async def _recv_(self):
|
||||
return await self.receive_queue.get()
|
||||
|
||||
def _send_(self, data, addr):
|
||||
self.transport.sendto(data, addr=addr)
|
||||
|
||||
|
||||
class ModbusServerFactory:
|
||||
"""
|
||||
Builder class for a modbus server
|
||||
|
||||
This also holds the server datastore so that it is persisted between connections
|
||||
"""
|
||||
|
||||
def __init__(self, store, framer=None, identity=None, **kwargs):
|
||||
import warnings
|
||||
warnings.warn("deprecated API for asyncio. ServerFactory's are a "
|
||||
"twisted construct and don't have an equivalent in "
|
||||
"asyncio",
|
||||
DeprecationWarning)
|
||||
|
||||
|
||||
class ModbusSingleRequestHandler(ModbusBaseRequestHandler, asyncio.Protocol):
|
||||
""" Implements the modbus server protocol
|
||||
This uses asyncio.Protocol to implement
|
||||
the client handler for a serial connection.
|
||||
"""
|
||||
def connection_made(self, transport):
|
||||
super().connection_made(transport)
|
||||
|
||||
_logger.debug("Serial connection established")
|
||||
|
||||
def connection_lost(self, exc):
|
||||
super().connection_lost(exc)
|
||||
_logger.debug("Serial conection lost")
|
||||
if hasattr(self.server, 'on_connection_lost'):
|
||||
self.server.on_connection_lost()
|
||||
|
||||
def data_received(self, data):
|
||||
self.receive_queue.put_nowait(data)
|
||||
|
||||
async def _recv_(self):
|
||||
return await self.receive_queue.get()
|
||||
|
||||
def _send_(self, data):
|
||||
if self.transport is not None:
|
||||
self.transport.write(data)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Server Implementations
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class ModbusTcpServer:
|
||||
"""
|
||||
A modbus threaded tcp socket server
|
||||
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
context,
|
||||
framer=None,
|
||||
identity=None,
|
||||
address=None,
|
||||
handler=None,
|
||||
allow_reuse_address=False,
|
||||
allow_reuse_port=False,
|
||||
defer_start=False,
|
||||
backlog=20,
|
||||
loop=None,
|
||||
**kwargs):
|
||||
""" Overloaded initializer for the socket server
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param handler: A handler for each client session; default is
|
||||
ModbusConnectedRequestHandler. The handler class
|
||||
receives connection create/teardown events
|
||||
:param allow_reuse_address: Whether the server will allow the
|
||||
reuse of an address.
|
||||
:param allow_reuse_port: Whether the server will allow the
|
||||
reuse of a port.
|
||||
:param backlog: is the maximum number of queued connections
|
||||
passed to listen(). Defaults to 20, increase if many
|
||||
connections are being made and broken to your Modbus slave
|
||||
:param loop: optional asyncio event loop to run in. Will default to
|
||||
asyncio.get_event_loop() supplied value if None.
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
|
||||
False to treat 0 as any other unit_id
|
||||
:param response_manipulator: Callback method for manipulating the
|
||||
response
|
||||
"""
|
||||
self.active_connections = {}
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
self.allow_reuse_address = allow_reuse_address
|
||||
self.decoder = ServerDecoder()
|
||||
self.framer = framer or ModbusSocketFramer
|
||||
self.context = context or ModbusServerContext()
|
||||
self.control = ModbusControlBlock()
|
||||
self.address = address or ("", Defaults.Port)
|
||||
self.handler = handler or ModbusConnectedRequestHandler
|
||||
self.handler.server = self
|
||||
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
|
||||
Defaults.IgnoreMissingSlaves)
|
||||
self.broadcast_enable = kwargs.get('broadcast_enable',
|
||||
Defaults.broadcast_enable)
|
||||
self.response_manipulator = kwargs.get("response_manipulator", None)
|
||||
if isinstance(identity, ModbusDeviceIdentification):
|
||||
self.control.Identity.update(identity)
|
||||
|
||||
# asyncio future that will be done once server has started
|
||||
self.serving = self.loop.create_future()
|
||||
# constructors cannot be declared async, so we have to
|
||||
# defer the initialization of the server
|
||||
self.server = None
|
||||
if PYTHON_VERSION >= (3, 7):
|
||||
# start_serving is new in version 3.7
|
||||
self.server_factory = self.loop.create_server(
|
||||
lambda: self.handler(self),
|
||||
*self.address,
|
||||
reuse_address=allow_reuse_address,
|
||||
reuse_port=allow_reuse_port,
|
||||
backlog=backlog,
|
||||
start_serving=not defer_start
|
||||
)
|
||||
else:
|
||||
self.server_factory = self.loop.create_server(
|
||||
lambda: self.handler(self),
|
||||
*self.address,
|
||||
reuse_address=allow_reuse_address,
|
||||
reuse_port=allow_reuse_port,
|
||||
backlog=backlog
|
||||
)
|
||||
|
||||
async def serve_forever(self):
|
||||
if self.server is None:
|
||||
self.server = await self.server_factory
|
||||
self.serving.set_result(True)
|
||||
await self.server.serve_forever()
|
||||
else:
|
||||
raise RuntimeError("Can't call serve_forever on "
|
||||
"an already running server object")
|
||||
|
||||
def server_close(self):
|
||||
for k, v in self.active_connections.items():
|
||||
_logger.warning("aborting active session {}".format(k))
|
||||
v.handler_task.cancel()
|
||||
self.active_connections = {}
|
||||
self.server.close()
|
||||
|
||||
|
||||
class ModbusTlsServer(ModbusTcpServer):
|
||||
"""
|
||||
A modbus threaded tls socket server
|
||||
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
context,
|
||||
framer=None,
|
||||
identity=None,
|
||||
address=None,
|
||||
sslctx=None,
|
||||
certfile=None,
|
||||
keyfile=None,
|
||||
handler=None,
|
||||
allow_reuse_address=False,
|
||||
allow_reuse_port=False,
|
||||
defer_start=False,
|
||||
backlog=20,
|
||||
loop=None,
|
||||
**kwargs):
|
||||
""" Overloaded initializer for the socket server
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto
|
||||
create)
|
||||
:param certfile: The cert file path for TLS (used if sslctx is None)
|
||||
:param keyfile: The key file path for TLS (used if sslctx is None)
|
||||
:param handler: A handler for each client session; default is
|
||||
ModbusConnectedRequestHandler. The handler class
|
||||
receives connection create/teardown events
|
||||
:param allow_reuse_address: Whether the server will allow the
|
||||
reuse of an address.
|
||||
:param allow_reuse_port: Whether the server will allow the
|
||||
reuse of a port.
|
||||
:param backlog: is the maximum number of queued connections
|
||||
passed to listen(). Defaults to 20, increase if many
|
||||
connections are being made and broken to your Modbus slave
|
||||
:param loop: optional asyncio event loop to run in. Will default to
|
||||
asyncio.get_event_loop() supplied value if None.
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
|
||||
False to treat 0 as any other unit_id
|
||||
:param response_manipulator: Callback method for
|
||||
manipulating the response
|
||||
"""
|
||||
self.active_connections = {}
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
self.allow_reuse_address = allow_reuse_address
|
||||
self.decoder = ServerDecoder()
|
||||
self.framer = framer or ModbusTlsFramer
|
||||
self.context = context or ModbusServerContext()
|
||||
self.control = ModbusControlBlock()
|
||||
self.address = address or ("", Defaults.Port)
|
||||
self.handler = handler or ModbusConnectedRequestHandler
|
||||
self.handler.server = self
|
||||
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
|
||||
Defaults.IgnoreMissingSlaves)
|
||||
self.broadcast_enable = kwargs.get('broadcast_enable',
|
||||
Defaults.broadcast_enable)
|
||||
self.response_manipulator = kwargs.get("response_manipulator", None)
|
||||
|
||||
if isinstance(identity, ModbusDeviceIdentification):
|
||||
self.control.Identity.update(identity)
|
||||
|
||||
self.sslctx = sslctx
|
||||
if self.sslctx is None:
|
||||
self.sslctx = ssl.create_default_context()
|
||||
self.sslctx.load_cert_chain(certfile=certfile, keyfile=keyfile)
|
||||
# According to MODBUS/TCP Security Protocol Specification, it is
|
||||
# TLSv2 at least
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1_1
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv3
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv2
|
||||
self.sslctx.verify_mode = ssl.CERT_OPTIONAL
|
||||
self.sslctx.check_hostname = False
|
||||
# asyncio future that will be done once server has started
|
||||
self.serving = self.loop.create_future()
|
||||
# constructors cannot be declared async, so we have to
|
||||
# defer the initialization of the server
|
||||
self.server = None
|
||||
if PYTHON_VERSION >= (3, 7):
|
||||
# start_serving is new in version 3.7
|
||||
self.server_factory = self.loop.create_server(
|
||||
lambda: self.handler(self),
|
||||
*self.address,
|
||||
ssl=self.sslctx,
|
||||
reuse_address=allow_reuse_address,
|
||||
reuse_port=allow_reuse_port,
|
||||
backlog=backlog,
|
||||
start_serving=not defer_start
|
||||
)
|
||||
else:
|
||||
self.server_factory = self.loop.create_server(
|
||||
lambda: self.handler(self),
|
||||
*self.address,
|
||||
ssl=self.sslctx,
|
||||
reuse_address=allow_reuse_address,
|
||||
reuse_port=allow_reuse_port,
|
||||
backlog=backlog
|
||||
)
|
||||
|
||||
|
||||
class ModbusUdpServer:
|
||||
"""
|
||||
A modbus threaded udp socket server
|
||||
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
def __init__(self, context, framer=None, identity=None, address=None,
|
||||
handler=None, allow_reuse_address=False,
|
||||
allow_reuse_port=False,
|
||||
defer_start=False,
|
||||
backlog=20,
|
||||
loop=None,
|
||||
**kwargs):
|
||||
""" Overloaded initializer for the socket server
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param handler: A handler for each client session; default is
|
||||
ModbusDisonnectedRequestHandler
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
|
||||
False to treat 0 as any other unit_id
|
||||
:param response_manipulator: Callback method for
|
||||
manipulating the response
|
||||
"""
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
self.decoder = ServerDecoder()
|
||||
self.framer = framer or ModbusSocketFramer
|
||||
self.context = context or ModbusServerContext()
|
||||
self.control = ModbusControlBlock()
|
||||
self.address = address or ("", Defaults.Port)
|
||||
self.handler = handler or ModbusDisconnectedRequestHandler
|
||||
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
|
||||
Defaults.IgnoreMissingSlaves)
|
||||
self.broadcast_enable = kwargs.get('broadcast_enable',
|
||||
Defaults.broadcast_enable)
|
||||
self.response_manipulator = kwargs.get("response_manipulator", None)
|
||||
|
||||
if isinstance(identity, ModbusDeviceIdentification):
|
||||
self.control.Identity.update(identity)
|
||||
|
||||
self.protocol = None
|
||||
self.endpoint = None
|
||||
self.on_connection_terminated = None
|
||||
self.stop_serving = self.loop.create_future()
|
||||
# asyncio future that will be done once server has started
|
||||
self.serving = self.loop.create_future()
|
||||
self.server_factory = self.loop.create_datagram_endpoint(
|
||||
lambda: self.handler(self),
|
||||
local_addr=self.address,
|
||||
reuse_address=allow_reuse_address,
|
||||
reuse_port=allow_reuse_port,
|
||||
allow_broadcast=True
|
||||
)
|
||||
|
||||
async def serve_forever(self):
|
||||
if self.protocol is None:
|
||||
self.protocol, self.endpoint = await self.server_factory
|
||||
self.serving.set_result(True)
|
||||
await self.stop_serving
|
||||
else:
|
||||
raise RuntimeError("Can't call serve_forever on an "
|
||||
"already running server object")
|
||||
|
||||
def server_close(self):
|
||||
self.stop_serving.set_result(True)
|
||||
if self.endpoint.handler_task is not None:
|
||||
self.endpoint.handler_task.cancel()
|
||||
|
||||
self.protocol.close()
|
||||
|
||||
|
||||
class ModbusSerialServer(object):
|
||||
"""
|
||||
A modbus threaded serial socket server
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
handler = None
|
||||
|
||||
def __init__(self, context, framer=None, identity=None, **kwargs): # pragma: no cover
|
||||
""" Overloaded initializer for the socket server
|
||||
If the identity structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param port: The serial port to attach to
|
||||
:param stopbits: The number of stop bits to use
|
||||
:param bytesize: The bytesize of the serial messages
|
||||
:param parity: Which kind of parity to use
|
||||
:param baudrate: The baud rate to use for the serial device
|
||||
:param timeout: The timeout to use for the serial device
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
|
||||
False to treat 0 as any other unit_id
|
||||
:param autoreonnect: True to enable automatic reconnection,
|
||||
False otherwise
|
||||
:param reconnect_delay: reconnect delay in seconds
|
||||
:param response_manipulator: Callback method for
|
||||
manipulating the response
|
||||
"""
|
||||
self.device = kwargs.get('port', 0)
|
||||
self.stopbits = kwargs.get('stopbits', Defaults.Stopbits)
|
||||
self.bytesize = kwargs.get('bytesize', Defaults.Bytesize)
|
||||
self.parity = kwargs.get('parity', Defaults.Parity)
|
||||
self.baudrate = kwargs.get('baudrate', Defaults.Baudrate)
|
||||
self.timeout = kwargs.get('timeout', Defaults.Timeout)
|
||||
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
|
||||
Defaults.IgnoreMissingSlaves)
|
||||
self.broadcast_enable = kwargs.get('broadcast_enable',
|
||||
Defaults.broadcast_enable)
|
||||
self.auto_reconnect = kwargs.get('auto_reconnect', False)
|
||||
self.reconnect_delay = kwargs.get('reconnect_delay', 2)
|
||||
self.reconnecting_task = None
|
||||
|
||||
self.handler = kwargs.get("handler") or ModbusSingleRequestHandler
|
||||
self.framer = framer or ModbusRtuFramer
|
||||
self.decoder = ServerDecoder()
|
||||
self.context = context or ModbusServerContext()
|
||||
self.response_manipulator = kwargs.get("response_manipulator", None)
|
||||
self.control = ModbusControlBlock()
|
||||
if isinstance(identity, ModbusDeviceIdentification):
|
||||
self.control.Identity.update(identity)
|
||||
|
||||
self.protocol = None
|
||||
self.transport = None
|
||||
|
||||
async def start(self):
|
||||
await self._connect()
|
||||
|
||||
def _protocol_factory(self):
|
||||
return self.handler(self)
|
||||
|
||||
async def _delayed_connect(self):
|
||||
await asyncio.sleep(self.reconnect_delay)
|
||||
await self._connect()
|
||||
|
||||
async def _connect(self):
|
||||
if self.reconnecting_task is not None:
|
||||
self.reconnecting_task = None
|
||||
|
||||
try:
|
||||
self.transport, self.protocol = await create_serial_connection(
|
||||
asyncio.get_event_loop(),
|
||||
self._protocol_factory,
|
||||
self.device,
|
||||
baudrate=self.baudrate,
|
||||
bytesize=self.bytesize,
|
||||
parity=self.parity,
|
||||
stopbits=self.stopbits,
|
||||
timeout=self.timeout
|
||||
)
|
||||
except serial.serialutil.SerialException as e:
|
||||
_logger.debug("Failed to open serial port: {}".format(self.device))
|
||||
if not self.auto_reconnect:
|
||||
raise e
|
||||
|
||||
self._check_reconnect()
|
||||
|
||||
except Exception as e:
|
||||
_logger.debug("Exception while create - {}".format(e))
|
||||
|
||||
def on_connection_lost(self):
|
||||
if self.transport is not None:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
self.protocol = None
|
||||
|
||||
self._check_reconnect()
|
||||
|
||||
def _check_reconnect(self):
|
||||
_logger.debug("checkking autoreconnect {} {}".format(
|
||||
self.auto_reconnect, self.reconnecting_task))
|
||||
if self.auto_reconnect and (self.reconnecting_task is None):
|
||||
_logger.debug("Scheduling serial connection reconnect")
|
||||
loop = asyncio.get_event_loop()
|
||||
self.reconnecting_task = loop.create_task(self._delayed_connect())
|
||||
|
||||
async def serve_forever(self):
|
||||
while True:
|
||||
await asyncio.sleep(360)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Creation Factories
|
||||
# --------------------------------------------------------------------------- #
|
||||
async def StartTcpServer(context=None, identity=None, address=None,
|
||||
custom_functions=[], defer_start=True, **kwargs):
|
||||
""" A factory to start and run a tcp modbus server
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param defer_start: if set, a coroutine which can be started and stopped
|
||||
will be returned. Otherwise, the server will be immediately spun
|
||||
up without the ability to shut it off from within the asyncio loop
|
||||
:param ignore_missing_slaves: True to not send errors on a request to a
|
||||
missing slave
|
||||
:return: an initialized but inactive server object coroutine
|
||||
"""
|
||||
framer = kwargs.pop("framer", ModbusSocketFramer)
|
||||
server = ModbusTcpServer(context, framer, identity, address, **kwargs)
|
||||
|
||||
for f in custom_functions:
|
||||
server.decoder.register(f) # pragma: no cover
|
||||
|
||||
if not defer_start:
|
||||
await server.serve_forever()
|
||||
|
||||
return server
|
||||
|
||||
|
||||
async def StartTlsServer(context=None, identity=None, address=None,
|
||||
sslctx=None,
|
||||
certfile=None, keyfile=None,
|
||||
allow_reuse_address=False,
|
||||
allow_reuse_port=False,
|
||||
custom_functions=[],
|
||||
defer_start=True, **kwargs):
|
||||
""" A factory to start and run a tls modbus server
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto create)
|
||||
:param certfile: The cert file path for TLS (used if sslctx is None)
|
||||
:param keyfile: The key file path for TLS (used if sslctx is None)
|
||||
:param allow_reuse_address: Whether the server will allow the reuse of an
|
||||
address.
|
||||
:param allow_reuse_port: Whether the server will allow the reuse of a port.
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param defer_start: if set, a coroutine which can be started and stopped
|
||||
will be returned. Otherwise, the server will be immediately spun
|
||||
up without the ability to shut it off from within the asyncio loop
|
||||
:param ignore_missing_slaves: True to not send errors on a request to a
|
||||
missing slave
|
||||
:return: an initialized but inactive server object coroutine
|
||||
"""
|
||||
framer = kwargs.pop("framer", ModbusTlsFramer)
|
||||
server = ModbusTlsServer(context, framer, identity, address, sslctx,
|
||||
certfile, keyfile,
|
||||
allow_reuse_address=allow_reuse_address,
|
||||
allow_reuse_port=allow_reuse_port, **kwargs)
|
||||
|
||||
for f in custom_functions:
|
||||
server.decoder.register(f) # pragma: no cover
|
||||
|
||||
if not defer_start:
|
||||
await server.serve_forever()
|
||||
|
||||
return server
|
||||
|
||||
|
||||
async def StartUdpServer(context=None, identity=None, address=None,
|
||||
custom_functions=[], defer_start=True, **kwargs):
|
||||
""" A factory to start and run a udp modbus server
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param framer: The framer to operate with (default ModbusSocketFramer)
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
"""
|
||||
framer = kwargs.pop('framer', ModbusSocketFramer)
|
||||
server = ModbusUdpServer(context, framer, identity, address, **kwargs)
|
||||
|
||||
for f in custom_functions:
|
||||
server.decoder.register(f) # pragma: no cover
|
||||
|
||||
if not defer_start:
|
||||
await server.serve_forever() # pragma: no cover
|
||||
|
||||
return server
|
||||
|
||||
|
||||
async def StartSerialServer(context=None, identity=None,
|
||||
custom_functions=[], **kwargs): # pragma: no cover
|
||||
""" A factory to start and run a serial modbus server
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param framer: The framer to operate with (default ModbusAsciiFramer)
|
||||
:param port: The serial port to attach to
|
||||
:param stopbits: The number of stop bits to use
|
||||
:param bytesize: The bytesize of the serial messages
|
||||
:param parity: Which kind of parity to use
|
||||
:param baudrate: The baud rate to use for the serial device
|
||||
:param timeout: The timeout to use for the serial device
|
||||
:param ignore_missing_slaves: True to not send errors on a request to a
|
||||
missing slave
|
||||
"""
|
||||
framer = kwargs.pop('framer', ModbusAsciiFramer)
|
||||
server = ModbusSerialServer(context, framer, identity, **kwargs)
|
||||
for f in custom_functions:
|
||||
server.decoder.register(f)
|
||||
await server.start()
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
def StopServer():
|
||||
"""
|
||||
Helper method to stop Async Server
|
||||
"""
|
||||
import warnings
|
||||
warnings.warn("deprecated API for asyncio. Call server_close() on "
|
||||
"server object returned by StartXxxServer",
|
||||
DeprecationWarning)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
"StartTcpServer", "StartTlsServer", "StartUdpServer", "StartSerialServer"
|
||||
|
||||
]
|
||||
@@ -1,363 +0,0 @@
|
||||
"""
|
||||
Implementation of a Twisted Modbus Server
|
||||
------------------------------------------
|
||||
|
||||
"""
|
||||
from binascii import b2a_hex
|
||||
from twisted.internet import protocol
|
||||
from twisted.internet.protocol import ServerFactory
|
||||
from twisted.internet import reactor
|
||||
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from pymodbus.factory import ServerDecoder
|
||||
from pymodbus.datastore import ModbusServerContext
|
||||
from pymodbus.device import ModbusControlBlock
|
||||
from pymodbus.device import ModbusAccessControl
|
||||
from pymodbus.device import ModbusDeviceIdentification
|
||||
from pymodbus.exceptions import NoSuchSlaveException
|
||||
from pymodbus.transaction import (ModbusSocketFramer,
|
||||
ModbusRtuFramer,
|
||||
ModbusAsciiFramer,
|
||||
ModbusBinaryFramer)
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.internal.ptwisted import InstallManagementConsole
|
||||
from pymodbus.compat import IS_PYTHON3
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TCP Server
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusTcpProtocol(protocol.Protocol):
|
||||
""" Implements a modbus server in twisted """
|
||||
|
||||
def connectionMade(self):
|
||||
""" Callback for when a client connects
|
||||
|
||||
..note:: since the protocol factory cannot be accessed from the
|
||||
protocol __init__, the client connection made is essentially
|
||||
our __init__ method.
|
||||
"""
|
||||
_logger.debug("Client Connected [%s]" % self.transport.getHost())
|
||||
self.framer = self.factory.framer(decoder=self.factory.decoder,
|
||||
client=None)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
""" Callback for when a client disconnects
|
||||
|
||||
:param reason: The client's reason for disconnecting
|
||||
"""
|
||||
_logger.debug("Client Disconnected: %s" % reason)
|
||||
|
||||
def dataReceived(self, data):
|
||||
""" Callback when we receive any data
|
||||
|
||||
:param data: The data sent by the client
|
||||
"""
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug('Data Received: ' + hexlify_packets(data))
|
||||
if not self.factory.control.ListenOnly:
|
||||
units = self.factory.store.slaves()
|
||||
single = self.factory.store.single
|
||||
self.framer.processIncomingPacket(data, self._execute,
|
||||
single=single,
|
||||
unit=units)
|
||||
|
||||
def _execute(self, request):
|
||||
""" Executes the request and returns the result
|
||||
|
||||
:param request: The decoded request message
|
||||
"""
|
||||
try:
|
||||
context = self.factory.store[request.unit_id]
|
||||
response = request.execute(context)
|
||||
except NoSuchSlaveException as ex:
|
||||
_logger.debug("requested slave does not exist: %s" % request.unit_id )
|
||||
if self.factory.ignore_missing_slaves:
|
||||
return # the client will simply timeout waiting for a response
|
||||
response = request.doException(merror.GatewayNoResponse)
|
||||
except Exception as ex:
|
||||
_logger.debug("Datastore unable to fulfill request: %s" % ex)
|
||||
response = request.doException(merror.SlaveFailure)
|
||||
|
||||
response.transaction_id = request.transaction_id
|
||||
response.unit_id = request.unit_id
|
||||
self._send(response)
|
||||
|
||||
def _send(self, message):
|
||||
""" Send a request (string) to the network
|
||||
|
||||
:param message: The unencoded modbus response
|
||||
"""
|
||||
if message.should_respond:
|
||||
self.factory.control.Counter.BusMessage += 1
|
||||
pdu = self.framer.buildPacket(message)
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug('send: %s' % b2a_hex(pdu))
|
||||
return self.transport.write(pdu)
|
||||
|
||||
|
||||
class ModbusServerFactory(ServerFactory):
|
||||
"""
|
||||
Builder class for a modbus server
|
||||
|
||||
This also holds the server datastore so that it is
|
||||
persisted between connections
|
||||
"""
|
||||
|
||||
protocol = ModbusTcpProtocol
|
||||
|
||||
def __init__(self, store, framer=None, identity=None, **kwargs):
|
||||
""" Overloaded initializer for the modbus factory
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param store: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param ignore_missing_slaves: True to not send errors on a request to a missing slave
|
||||
"""
|
||||
self.decoder = ServerDecoder()
|
||||
self.framer = framer or ModbusSocketFramer
|
||||
self.store = store or ModbusServerContext()
|
||||
self.control = ModbusControlBlock()
|
||||
self.access = ModbusAccessControl()
|
||||
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves)
|
||||
|
||||
if isinstance(identity, ModbusDeviceIdentification):
|
||||
self.control.Identity.update(identity)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus UDP Server
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusUdpProtocol(protocol.DatagramProtocol):
|
||||
""" Implements a modbus udp server in twisted """
|
||||
|
||||
def __init__(self, store, framer=None, identity=None, **kwargs):
|
||||
""" Overloaded initializer for the modbus factory
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param store: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param ignore_missing_slaves: True to not send errors on a request to
|
||||
a missing slave
|
||||
"""
|
||||
framer = framer or ModbusSocketFramer
|
||||
self.decoder = ServerDecoder()
|
||||
self.framer = framer(self.decoder)
|
||||
self.store = store or ModbusServerContext()
|
||||
self.control = ModbusControlBlock()
|
||||
self.access = ModbusAccessControl()
|
||||
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
|
||||
Defaults.IgnoreMissingSlaves)
|
||||
|
||||
if isinstance(identity, ModbusDeviceIdentification):
|
||||
self.control.Identity.update(identity)
|
||||
|
||||
def datagramReceived(self, data, addr):
|
||||
""" Callback when we receive any data
|
||||
|
||||
:param data: The data sent by the client
|
||||
"""
|
||||
_logger.debug("Client Connected [%s]" % addr)
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug("Datagram Received: "+ hexlify_packets(data))
|
||||
if not self.control.ListenOnly:
|
||||
continuation = lambda request: self._execute(request, addr)
|
||||
self.framer.processIncomingPacket(data, continuation)
|
||||
|
||||
def _execute(self, request, addr):
|
||||
""" Executes the request and returns the result
|
||||
|
||||
:param request: The decoded request message
|
||||
"""
|
||||
try:
|
||||
context = self.store[request.unit_id]
|
||||
response = request.execute(context)
|
||||
except NoSuchSlaveException as ex:
|
||||
_logger.debug("requested slave does not exist: "
|
||||
"%s" % request.unit_id )
|
||||
if self.ignore_missing_slaves:
|
||||
return # the client will simply timeout waiting for a response
|
||||
response = request.doException(merror.GatewayNoResponse)
|
||||
except Exception as ex:
|
||||
_logger.debug("Datastore unable to fulfill request: %s" % ex)
|
||||
response = request.doException(merror.SlaveFailure)
|
||||
#self.framer.populateResult(response)
|
||||
response.transaction_id = request.transaction_id
|
||||
response.unit_id = request.unit_id
|
||||
self._send(response, addr)
|
||||
|
||||
def _send(self, message, addr):
|
||||
""" Send a request (string) to the network
|
||||
|
||||
:param message: The unencoded modbus response
|
||||
:param addr: The (host, port) to send the message to
|
||||
"""
|
||||
self.control.Counter.BusMessage += 1
|
||||
pdu = self.framer.buildPacket(message)
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug('send: %s' % b2a_hex(pdu))
|
||||
return self.transport.write(pdu, addr)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Starting Factories
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _is_main_thread():
|
||||
import threading
|
||||
|
||||
if IS_PYTHON3:
|
||||
if threading.current_thread() != threading.main_thread():
|
||||
_logger.debug("Running in spawned thread")
|
||||
return False
|
||||
else:
|
||||
if not isinstance(threading.current_thread(), threading._MainThread):
|
||||
_logger.debug("Running in spawned thread")
|
||||
return False
|
||||
_logger.debug("Running in Main thread")
|
||||
return True
|
||||
|
||||
|
||||
def StartTcpServer(context, identity=None, address=None,
|
||||
console=False, defer_reactor_run=False, custom_functions=[],
|
||||
**kwargs):
|
||||
"""
|
||||
Helper method to start the Modbus Async TCP server
|
||||
|
||||
:param context: The server data context
|
||||
:param identify: The server identity to use (default empty)
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param console: A flag indicating if you want the debug console
|
||||
:param ignore_missing_slaves: True to not send errors on a request \
|
||||
to a missing slave
|
||||
:param defer_reactor_run: True/False defer running reactor.run() as part \
|
||||
of starting server, to be explictly started by the user
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
|
||||
address = address or ("", Defaults.Port)
|
||||
framer = kwargs.pop("framer", ModbusSocketFramer)
|
||||
factory = ModbusServerFactory(context, framer, identity, **kwargs)
|
||||
for f in custom_functions:
|
||||
factory.decoder.register(f)
|
||||
if console:
|
||||
InstallManagementConsole({'factory': factory})
|
||||
|
||||
_logger.info("Starting Modbus TCP Server on %s:%s" % address)
|
||||
reactor.listenTCP(address[1], factory, interface=address[0])
|
||||
if not defer_reactor_run:
|
||||
reactor.run(installSignalHandlers=_is_main_thread())
|
||||
|
||||
|
||||
def StartUdpServer(context, identity=None, address=None,
|
||||
defer_reactor_run=False, custom_functions=[], **kwargs):
|
||||
"""
|
||||
Helper method to start the Modbus Async Udp server
|
||||
|
||||
:param context: The server data context
|
||||
:param identify: The server identity to use (default empty)
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param ignore_missing_slaves: True to not send errors on a request \
|
||||
to a missing slave
|
||||
:param defer_reactor_run: True/False defer running reactor.run() as part \
|
||||
of starting server, to be explictly started by the user
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
|
||||
address = address or ("", Defaults.Port)
|
||||
framer = kwargs.pop("framer", ModbusSocketFramer)
|
||||
server = ModbusUdpProtocol(context, framer, identity, **kwargs)
|
||||
for f in custom_functions:
|
||||
server.decoder.register(f)
|
||||
|
||||
_logger.info("Starting Modbus UDP Server on %s:%s" % address)
|
||||
reactor.listenUDP(address[1], server, interface=address[0])
|
||||
if not defer_reactor_run:
|
||||
reactor.run(installSignalHandlers=_is_main_thread())
|
||||
|
||||
|
||||
def StartSerialServer(context, identity=None, framer=ModbusAsciiFramer,
|
||||
defer_reactor_run=False, custom_functions=[], **kwargs):
|
||||
"""
|
||||
Helper method to start the Modbus Async Serial server
|
||||
|
||||
:param context: The server data context
|
||||
:param identify: The server identity to use (default empty)
|
||||
:param framer: The framer to use (default ModbusAsciiFramer)
|
||||
:param port: The serial port to attach to
|
||||
:param baudrate: The baud rate to use for the serial device
|
||||
:param console: A flag indicating if you want the debug console
|
||||
:param ignore_missing_slaves: True to not send errors on a request to a
|
||||
missing slave
|
||||
:param defer_reactor_run: True/False defer running reactor.run() as part
|
||||
of starting server, to be explictly started by the user
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.serialport import SerialPort
|
||||
|
||||
port = kwargs.get('port', '/dev/ttyS0')
|
||||
baudrate = kwargs.get('baudrate', Defaults.Baudrate)
|
||||
console = kwargs.get('console', False)
|
||||
bytesize = kwargs.get("bytesize", Defaults.Bytesize)
|
||||
stopbits = kwargs.get("stopbits", Defaults.Stopbits)
|
||||
parity = kwargs.get("parity", Defaults.Parity)
|
||||
timeout = kwargs.get("timeout", 0)
|
||||
xonxoff = kwargs.get("xonxoff", 0)
|
||||
rtscts = kwargs.get("rtscts", 0)
|
||||
|
||||
_logger.info("Starting Modbus Serial Server on %s" % port)
|
||||
factory = ModbusServerFactory(context, framer, identity, **kwargs)
|
||||
for f in custom_functions:
|
||||
factory.decoder.register(f)
|
||||
if console:
|
||||
InstallManagementConsole({'factory': factory})
|
||||
if console:
|
||||
InstallManagementConsole({'factory': factory})
|
||||
|
||||
protocol = factory.buildProtocol(None)
|
||||
SerialPort.getHost = lambda self: port # hack for logging
|
||||
SerialPort(protocol, port, reactor, baudrate=baudrate, parity=parity,
|
||||
stopbits=stopbits, timeout=timeout, xonxoff=xonxoff,
|
||||
rtscts=rtscts, bytesize=bytesize)
|
||||
if not defer_reactor_run:
|
||||
reactor.run(installSignalHandlers=_is_main_thread())
|
||||
|
||||
|
||||
def StopServer():
|
||||
"""
|
||||
Helper method to stop Async Server
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
if _is_main_thread():
|
||||
reactor.stop()
|
||||
_logger.debug("Stopping server from main thread")
|
||||
else:
|
||||
reactor.callFromThread(reactor.stop)
|
||||
_logger.debug("Stopping Server from another thread")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
__all__ = [
|
||||
"StartTcpServer", "StartUdpServer", "StartSerialServer", "StopServer"
|
||||
]
|
||||
@@ -1,4 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2020 by RiptideIO
|
||||
All rights reserved.
|
||||
"""
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"tcp": {
|
||||
"handler": "ModbusConnectedRequestHandler",
|
||||
"allow_reuse_address": true,
|
||||
"allow_reuse_port": true,
|
||||
"backlog": 20,
|
||||
"ignore_missing_slaves": false
|
||||
},
|
||||
"rtu": {
|
||||
"handler": "ModbusSingleRequestHandler",
|
||||
"stopbits": 1,
|
||||
"bytesize": 8,
|
||||
"parity": "N",
|
||||
"baudrate": 9600,
|
||||
"timeout": 3,
|
||||
"auto_reconnect": false,
|
||||
"reconnect_delay": 2
|
||||
},
|
||||
"tls": {
|
||||
"handler": "ModbusConnectedRequestHandler",
|
||||
"certfile": null,
|
||||
"keyfile": null,
|
||||
"allow_reuse_address": true,
|
||||
"allow_reuse_port": true,
|
||||
"backlog": 20,
|
||||
"ignore_missing_slaves": false
|
||||
},
|
||||
"udp": {
|
||||
"handler": "ModbusDisonnectedRequestHandler",
|
||||
"ignore_missing_slaves": false
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2020 by RiptideIO
|
||||
All rights reserved.
|
||||
"""
|
||||
|
||||
DEFUALT_CONFIG = {
|
||||
"tcp": {
|
||||
"handler": "ModbusConnectedRequestHandler",
|
||||
"allow_reuse_address": True,
|
||||
"allow_reuse_port": True,
|
||||
"backlog": 20,
|
||||
"ignore_missing_slaves": False
|
||||
},
|
||||
"serial": {
|
||||
"handler": "ModbusSingleRequestHandler",
|
||||
"stopbits": 1,
|
||||
"bytesize": 8,
|
||||
"parity": "N",
|
||||
"baudrate": 9600,
|
||||
"timeout": 3,
|
||||
"auto_reconnect": False,
|
||||
"reconnect_delay": 2
|
||||
},
|
||||
"tls": {
|
||||
"handler": "ModbusConnectedRequestHandler",
|
||||
"certfile": None,
|
||||
"keyfile": None,
|
||||
"allow_reuse_address": True,
|
||||
"allow_reuse_port": True,
|
||||
"backlog": 20,
|
||||
"ignore_missing_slaves": False
|
||||
},
|
||||
"udp": {
|
||||
"handler": "ModbusDisonnectedRequestHandler",
|
||||
"ignore_missing_slaves": False
|
||||
}
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2020 by RiptideIO
|
||||
All rights reserved.
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import time
|
||||
import random
|
||||
import logging
|
||||
from pymodbus.version import version as pymodbus_version
|
||||
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
|
||||
from pymodbus.pdu import ExceptionResponse, ModbusExceptions
|
||||
from pymodbus.datastore.store import (ModbusSparseDataBlock,
|
||||
ModbusSequentialDataBlock)
|
||||
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
|
||||
from pymodbus.device import ModbusDeviceIdentification
|
||||
|
||||
if not IS_PYTHON3 or PYTHON_VERSION < (3, 6):
|
||||
print(f"You are running {PYTHON_VERSION}."
|
||||
"Reactive server requires python3.6 or above")
|
||||
exit()
|
||||
|
||||
|
||||
try:
|
||||
from aiohttp import web
|
||||
except ImportError as e:
|
||||
print("Reactive server requires aiohttp. "
|
||||
"Please install with 'pip install aiohttp' and try again.")
|
||||
exit(1)
|
||||
|
||||
from pymodbus.server.async_io import (ModbusTcpServer,
|
||||
ModbusTlsServer,
|
||||
ModbusSerialServer,
|
||||
ModbusUdpServer,
|
||||
ModbusSingleRequestHandler,
|
||||
ModbusConnectedRequestHandler,
|
||||
ModbusDisconnectedRequestHandler)
|
||||
from pymodbus.transaction import (ModbusRtuFramer,
|
||||
ModbusSocketFramer,
|
||||
ModbusTlsFramer,
|
||||
ModbusAsciiFramer,
|
||||
ModbusBinaryFramer)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SERVER_MAPPER = {
|
||||
"tcp": ModbusTcpServer,
|
||||
"serial": ModbusSerialServer,
|
||||
"udp": ModbusUdpServer,
|
||||
"tls": ModbusTlsServer
|
||||
}
|
||||
|
||||
DEFAULT_FRAMER = {
|
||||
"tcp": ModbusSocketFramer,
|
||||
"rtu": ModbusRtuFramer,
|
||||
"tls": ModbusTlsFramer,
|
||||
"udp": ModbusSocketFramer,
|
||||
"ascii": ModbusAsciiFramer,
|
||||
"binary": ModbusBinaryFramer
|
||||
}
|
||||
|
||||
DEFAULT_MANIPULATOR = {
|
||||
"response_type": "normal", # normal, error, delayed, empty
|
||||
"delay_by": 0,
|
||||
"error_code": ModbusExceptions.IllegalAddress,
|
||||
"clear_after": 5 # request count
|
||||
|
||||
}
|
||||
DEFUALT_HANDLERS = {
|
||||
"ModbusSingleRequestHandler": ModbusSingleRequestHandler,
|
||||
"ModbusConnectedRequestHandler": ModbusConnectedRequestHandler,
|
||||
"ModbusDisconnectedRequestHandler": ModbusDisconnectedRequestHandler
|
||||
}
|
||||
DEFAULT_MODBUS_MAP = {"start_offset": 0,
|
||||
"count": 100,
|
||||
"value": 0, "sparse": False}
|
||||
DEFAULT_DATA_BLOCK = {
|
||||
"co": DEFAULT_MODBUS_MAP,
|
||||
"di": DEFAULT_MODBUS_MAP,
|
||||
"ir": DEFAULT_MODBUS_MAP,
|
||||
"hr": DEFAULT_MODBUS_MAP
|
||||
|
||||
}
|
||||
|
||||
HINT = """
|
||||
Reactive Modbus Server started.
|
||||
{}
|
||||
|
||||
===========================================================================
|
||||
Example Usage:
|
||||
curl -X POST http://{}:{} -d '{{"response_type": "error", "error_code": 4}}'
|
||||
===========================================================================
|
||||
"""
|
||||
|
||||
|
||||
class ReactiveServer:
|
||||
"""
|
||||
Modbus Asynchronous Server which can manipulate the response dynamically.
|
||||
Useful for testing
|
||||
"""
|
||||
def __init__(self, host, port, modbus_server, loop=None):
|
||||
self._web_app = web.Application()
|
||||
self._runner = web.AppRunner(self._web_app)
|
||||
self._host = host
|
||||
self._port = int(port)
|
||||
self._modbus_server = modbus_server
|
||||
self._loop = loop
|
||||
self._add_routes()
|
||||
self._counter = 0
|
||||
self._modbus_server.response_manipulator = self.manipulate_response
|
||||
self._manipulator_config = dict(**DEFAULT_MANIPULATOR)
|
||||
self._web_app.on_startup.append(self.start_modbus_server)
|
||||
self._web_app.on_shutdown.append(self.stop_modbus_server)
|
||||
|
||||
@property
|
||||
def web_app(self):
|
||||
return self._web_app
|
||||
|
||||
@property
|
||||
def manipulator_config(self):
|
||||
return self._manipulator_config
|
||||
|
||||
@manipulator_config.setter
|
||||
def manipulator_config(self, value):
|
||||
if isinstance(value, dict):
|
||||
self._manipulator_config.update(**value)
|
||||
|
||||
def _add_routes(self):
|
||||
self._web_app.add_routes([
|
||||
web.post('/', self._response_manipulator)])
|
||||
|
||||
async def start_modbus_server(self, app):
|
||||
"""
|
||||
Start Modbus server as asyncio task after startup
|
||||
:param app: Webapp
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
if hasattr(asyncio, "create_task"):
|
||||
if isinstance(self._modbus_server, ModbusSerialServer):
|
||||
app["modbus_serial_server"] = asyncio.create_task(
|
||||
self._modbus_server.start())
|
||||
app["modbus_server"] = asyncio.create_task(
|
||||
self._modbus_server.serve_forever())
|
||||
else:
|
||||
if isinstance(self._modbus_server, ModbusSerialServer):
|
||||
app["modbus_serial_server"] = asyncio.ensure_future(
|
||||
self._modbus_server.start()
|
||||
)
|
||||
app["modbus_server"] = asyncio.ensure_future(
|
||||
self._modbus_server.serve_forever())
|
||||
|
||||
logger.info("Modbus server started")
|
||||
except Exception as e:
|
||||
logger.error("Error starting modbus server")
|
||||
logger.error(e)
|
||||
|
||||
async def stop_modbus_server(self, app):
|
||||
"""
|
||||
Stop modbus server
|
||||
:param app: Webapp
|
||||
:return:
|
||||
"""
|
||||
logger.info("Stopping modbus server")
|
||||
if isinstance(self._modbus_server, ModbusSerialServer):
|
||||
app["modbus_serial_server"].cancel()
|
||||
app["modbus_server"].cancel()
|
||||
await app["modbus_server"]
|
||||
logger.info("Modbus server Stopped")
|
||||
|
||||
async def _response_manipulator(self, request):
|
||||
"""
|
||||
POST request Handler for response manipulation end point
|
||||
Payload is a dict with following fields
|
||||
:response_type : One among (normal, delayed, error, empty, stray)
|
||||
:error_code: Modbus error code for error response
|
||||
:delay_by: Delay sending response by <n> seconds
|
||||
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
data = await request.json()
|
||||
self._manipulator_config.update(data)
|
||||
return web.json_response(data=data)
|
||||
|
||||
def update_manipulator_config(self, config):
|
||||
"""
|
||||
Updates manipulator config. Resets previous counters
|
||||
:param config: Manipulator config (dict)
|
||||
:return:
|
||||
"""
|
||||
self._counter = 0
|
||||
self._manipulator_config = config
|
||||
|
||||
def manipulate_response(self, response):
|
||||
"""
|
||||
Manipulates the actual response according to the required error state.
|
||||
:param response: Modbus response object
|
||||
:return: Modbus response
|
||||
"""
|
||||
skip_encoding = False
|
||||
if not self._manipulator_config:
|
||||
return response
|
||||
else:
|
||||
clear_after = self._manipulator_config.get("clear_after")
|
||||
if clear_after and self._counter > clear_after:
|
||||
logger.info("Resetting manipulator"
|
||||
" after {} responses".format(clear_after))
|
||||
self.update_manipulator_config(dict(DEFAULT_MANIPULATOR))
|
||||
return response
|
||||
response_type = self._manipulator_config.get("response_type")
|
||||
if response_type == "error":
|
||||
error_code = self._manipulator_config.get("error_code")
|
||||
logger.warning(
|
||||
"Sending error response for all incoming requests")
|
||||
err_response = ExceptionResponse(response.function_code, error_code)
|
||||
err_response.transaction_id = response.transaction_id
|
||||
err_response.unit_id = response.unit_id
|
||||
response = err_response
|
||||
self._counter += 1
|
||||
elif response_type == "delayed":
|
||||
delay_by = self._manipulator_config.get("delay_by")
|
||||
logger.warning(
|
||||
"Delaying response by {}s for "
|
||||
"all incoming requests".format(delay_by))
|
||||
time.sleep(delay_by)
|
||||
self._counter += 1
|
||||
elif response_type == "empty":
|
||||
logger.warning("Sending empty response")
|
||||
self._counter += 1
|
||||
response.should_respond = False
|
||||
elif response_type == "stray":
|
||||
data_len = self._manipulator_config.get("data_len", 10)
|
||||
if data_len <= 0:
|
||||
logger.warning(f"Invalid data_len {data_len}. "
|
||||
f"Using default lenght 10")
|
||||
data_len = 10
|
||||
response = os.urandom(data_len)
|
||||
self._counter += 1
|
||||
skip_encoding = True
|
||||
return response, skip_encoding
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run Web app
|
||||
:return:
|
||||
"""
|
||||
def _info(message):
|
||||
msg = HINT.format(message, self._host, self._port)
|
||||
print(msg)
|
||||
# print(message)
|
||||
web.run_app(self._web_app, host=self._host, port=self._port,
|
||||
print=_info)
|
||||
|
||||
async def run_async(self):
|
||||
"""
|
||||
Run Web app
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
await self._runner.setup()
|
||||
site = web.TCPSite(self._runner, self._host, self._port)
|
||||
await site.start()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
@classmethod
|
||||
def create_identity(cls, vendor="Pymodbus", product_code="PM",
|
||||
vendor_url='http://github.com/riptideio/pymodbus/',
|
||||
product_name="Pymodbus Server",
|
||||
model_name="Reactive Server",
|
||||
version=pymodbus_version.short()):
|
||||
"""
|
||||
Create modbus identity
|
||||
:param vendor:
|
||||
:param product_code:
|
||||
:param vendor_url:
|
||||
:param product_name:
|
||||
:param model_name:
|
||||
:param version:
|
||||
:return: ModbusIdentity object
|
||||
"""
|
||||
identity = ModbusDeviceIdentification()
|
||||
identity.VendorName = vendor
|
||||
identity.ProductCode = product_code
|
||||
identity.VendorUrl = vendor_url
|
||||
identity.ProductName = product_name
|
||||
identity.ModelName = model_name
|
||||
identity.MajorMinorRevision = version
|
||||
|
||||
return identity
|
||||
|
||||
@classmethod
|
||||
def create_context(cls, data_block=None, unit=1,
|
||||
single=False):
|
||||
"""
|
||||
Create Modbus context.
|
||||
:param data_block: Datablock (dict) Refer DEFAULT_DATA_BLOCK
|
||||
:param unit: Unit id for the slave
|
||||
:param single: To run as a single slave
|
||||
:return: ModbusServerContext object
|
||||
"""
|
||||
block = dict()
|
||||
data_block = data_block or DEFAULT_DATA_BLOCK
|
||||
for modbus_entity, block_desc in data_block.items():
|
||||
start_address = block_desc.get("start_address", 0)
|
||||
default_count = block_desc.get("count", 0)
|
||||
default_value = block_desc.get("value", 0)
|
||||
default_values = [default_value]*default_count
|
||||
sparse = block_desc.get("sparse", False)
|
||||
db = ModbusSequentialDataBlock if not sparse else ModbusSparseDataBlock
|
||||
if sparse:
|
||||
address_map = block_desc.get("address_map")
|
||||
if not address_map:
|
||||
address_map = random.sample(
|
||||
range(start_address+1, default_count), default_count-1)
|
||||
address_map.insert(0, 0)
|
||||
block[modbus_entity] = {add: val for add in sorted(address_map) for val in default_values}
|
||||
else:
|
||||
block[modbus_entity] =db(start_address, default_values)
|
||||
|
||||
slave_context = ModbusSlaveContext(**block, zero_mode=True)
|
||||
if not single:
|
||||
slaves = {}
|
||||
for i in unit:
|
||||
slaves[i] = slave_context
|
||||
else:
|
||||
slaves = slave_context
|
||||
server_context = ModbusServerContext(slaves, single=single)
|
||||
return server_context
|
||||
|
||||
@classmethod
|
||||
def factory(cls, server, framer=None, context=None, unit=1, single=False,
|
||||
host="localhost", modbus_port=5020, web_port=8080,
|
||||
data_block=DEFAULT_DATA_BLOCK, identity=None, loop=None, **kwargs):
|
||||
"""
|
||||
Factory to create ReactiveModbusServer
|
||||
:param server: Modbus server type (tcp, rtu, tls, udp)
|
||||
:param framer: Modbus framer (ModbusSocketFramer, ModbusRTUFramer, ModbusTLSFramer)
|
||||
:param context: Modbus server context to use
|
||||
:param unit: Modbus unit id
|
||||
:param single: Run in single mode
|
||||
:param host: Host address to use for both web app and modbus server (default localhost)
|
||||
:param modbus_port: Modbus port for TCP and UDP server(default: 5020)
|
||||
:param web_port: Web App port (default: 8080)
|
||||
:param data_block: Datablock (refer DEFAULT_DATA_BLOCK)
|
||||
:param identity: Modbus identity object
|
||||
:param loop: Asyncio loop to use
|
||||
:param kwargs: Other server specific keyword arguments, refer corresponding servers documentation
|
||||
:return: ReactiveServer object
|
||||
"""
|
||||
if server.lower() not in SERVER_MAPPER:
|
||||
logger.error(f"Invalid server {server}", server)
|
||||
exit(1)
|
||||
server = SERVER_MAPPER.get(server)
|
||||
if not framer:
|
||||
framer = DEFAULT_FRAMER.get(server)
|
||||
if not context:
|
||||
context = cls.create_context(data_block=data_block,
|
||||
unit=unit, single=single)
|
||||
if not identity:
|
||||
identity = cls.create_identity()
|
||||
if server == ModbusSerialServer:
|
||||
kwargs["port"] = modbus_port
|
||||
server = server(context, framer=framer, identity=identity,
|
||||
**kwargs)
|
||||
else:
|
||||
server = server(context, framer=framer, identity=identity,
|
||||
address=(host, modbus_port), defer_start=False,
|
||||
**kwargs)
|
||||
return ReactiveServer(host, web_port, server, loop)
|
||||
|
||||
# __END__
|
||||
@@ -1,703 +0,0 @@
|
||||
"""
|
||||
Implementation of a Threaded Modbus Server
|
||||
------------------------------------------
|
||||
|
||||
"""
|
||||
from binascii import b2a_hex
|
||||
import serial
|
||||
import socket
|
||||
import ssl
|
||||
import traceback
|
||||
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from pymodbus.factory import ServerDecoder
|
||||
from pymodbus.datastore import ModbusServerContext
|
||||
from pymodbus.device import ModbusControlBlock
|
||||
from pymodbus.device import ModbusDeviceIdentification
|
||||
from pymodbus.transaction import *
|
||||
from pymodbus.exceptions import NotImplementedException, NoSuchSlaveException
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.compat import socketserver, byte2int
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Protocol Handlers
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class ModbusBaseRequestHandler(socketserver.BaseRequestHandler):
|
||||
""" Implements the modbus server protocol
|
||||
|
||||
This uses the socketserver.BaseRequestHandler to implement
|
||||
the client handler.
|
||||
"""
|
||||
running = False
|
||||
framer = None
|
||||
|
||||
def setup(self):
|
||||
""" Callback for when a client connects
|
||||
"""
|
||||
_logger.debug("Client Connected [%s:%s]" % self.client_address)
|
||||
self.running = True
|
||||
self.framer = self.server.framer(self.server.decoder, client=None)
|
||||
self.server.threads.append(self)
|
||||
|
||||
def finish(self):
|
||||
""" Callback for when a client disconnects
|
||||
"""
|
||||
_logger.debug("Client Disconnected [%s:%s]" % self.client_address)
|
||||
self.server.threads.remove(self)
|
||||
|
||||
def execute(self, request):
|
||||
""" The callback to call with the resulting message
|
||||
|
||||
:param request: The decoded request message
|
||||
"""
|
||||
broadcast = False
|
||||
try:
|
||||
if self.server.broadcast_enable and request.unit_id == 0:
|
||||
broadcast = True
|
||||
# if broadcasting then execute on all slave contexts, note response will be ignored
|
||||
for unit_id in self.server.context.slaves():
|
||||
response = request.execute(self.server.context[unit_id])
|
||||
else:
|
||||
context = self.server.context[request.unit_id]
|
||||
response = request.execute(context)
|
||||
except NoSuchSlaveException as ex:
|
||||
_logger.debug("requested slave does "
|
||||
"not exist: %s" % request.unit_id )
|
||||
if self.server.ignore_missing_slaves:
|
||||
return # the client will simply timeout waiting for a response
|
||||
response = request.doException(merror.GatewayNoResponse)
|
||||
except Exception as ex:
|
||||
_logger.debug("Datastore unable to fulfill request: "
|
||||
"%s; %s", ex, traceback.format_exc())
|
||||
response = request.doException(merror.SlaveFailure)
|
||||
# no response when broadcasting
|
||||
if not broadcast:
|
||||
response.transaction_id = request.transaction_id
|
||||
response.unit_id = request.unit_id
|
||||
self.send(response)
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Base class implementations
|
||||
# ----------------------------------------------------------------------- #
|
||||
def handle(self):
|
||||
""" Callback when we receive any data
|
||||
"""
|
||||
raise NotImplementedException("Method not implemented"
|
||||
" by derived class")
|
||||
|
||||
def send(self, message):
|
||||
""" Send a request (string) to the network
|
||||
|
||||
:param message: The unencoded modbus response
|
||||
"""
|
||||
raise NotImplementedException("Method not implemented "
|
||||
"by derived class")
|
||||
|
||||
|
||||
class ModbusSingleRequestHandler(ModbusBaseRequestHandler):
|
||||
""" Implements the modbus server protocol
|
||||
|
||||
This uses the socketserver.BaseRequestHandler to implement
|
||||
the client handler for a single client(serial clients)
|
||||
"""
|
||||
def handle(self):
|
||||
""" Callback when we receive any data
|
||||
"""
|
||||
while self.running:
|
||||
try:
|
||||
data = self.request.recv(1024)
|
||||
if data:
|
||||
units = self.server.context.slaves()
|
||||
if not isinstance(units, (list, tuple)):
|
||||
units = [units]
|
||||
# if broadcast is enabled make sure to process requests to address 0
|
||||
if self.server.broadcast_enable:
|
||||
if 0 not in units:
|
||||
units.append(0)
|
||||
single = self.server.context.single
|
||||
self.framer.processIncomingPacket(data, self.execute,
|
||||
units, single=single)
|
||||
except Exception as msg:
|
||||
# Since we only have a single socket, we cannot exit
|
||||
# Clear frame buffer
|
||||
self.framer.resetFrame()
|
||||
_logger.debug("Error: Socket error occurred %s" % msg)
|
||||
|
||||
def send(self, message):
|
||||
""" Send a request (string) to the network
|
||||
|
||||
:param message: The unencoded modbus response
|
||||
"""
|
||||
if message.should_respond:
|
||||
# self.server.control.Counter.BusMessage += 1
|
||||
pdu = self.framer.buildPacket(message)
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug('send: [%s]- %s' % (message, b2a_hex(pdu)))
|
||||
return self.request.send(pdu)
|
||||
|
||||
|
||||
class CustomSingleRequestHandler(ModbusSingleRequestHandler):
|
||||
|
||||
def __init__(self, request, client_address, server):
|
||||
self.request = request
|
||||
self.client_address = client_address
|
||||
self.server = server
|
||||
self.running = True
|
||||
self.setup()
|
||||
|
||||
|
||||
class ModbusConnectedRequestHandler(ModbusBaseRequestHandler):
|
||||
""" Implements the modbus server protocol
|
||||
|
||||
This uses the socketserver.BaseRequestHandler to implement
|
||||
the client handler for a connected protocol (TCP).
|
||||
"""
|
||||
|
||||
def handle(self):
|
||||
"""Callback when we receive any data, until self.running becomes False.
|
||||
Blocks indefinitely awaiting data. If shutdown is required, then the
|
||||
global socket.settimeout(<seconds>) may be used, to allow timely
|
||||
checking of self.running. However, since this also affects socket
|
||||
connects, if there are outgoing socket connections used in the same
|
||||
program, then these will be prevented, if the specfied timeout is too
|
||||
short. Hence, this is unreliable.
|
||||
|
||||
To respond to Modbus...Server.server_close() (which clears each
|
||||
handler's self.running), derive from this class to provide an
|
||||
alternative handler that awakens from time to time when no input is
|
||||
available and checks self.running.
|
||||
Use Modbus...Server( handler=... ) keyword to supply the alternative
|
||||
request handler class.
|
||||
|
||||
"""
|
||||
reset_frame = False
|
||||
while self.running:
|
||||
try:
|
||||
units = self.server.context.slaves()
|
||||
data = self.request.recv(1024)
|
||||
if not data:
|
||||
self.running = False
|
||||
else:
|
||||
if not isinstance(units, (list, tuple)):
|
||||
units = [units]
|
||||
# if broadcast is enabled make sure to
|
||||
# process requests to address 0
|
||||
if self.server.broadcast_enable:
|
||||
if 0 not in units:
|
||||
units.append(0)
|
||||
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug('Handling data: ' + hexlify_packets(data))
|
||||
single = self.server.context.single
|
||||
self.framer.processIncomingPacket(data, self.execute, units,
|
||||
single=single)
|
||||
except socket.timeout as msg:
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug("Socket timeout occurred %s", msg)
|
||||
reset_frame = True
|
||||
except socket.error as msg:
|
||||
_logger.error("Socket error occurred %s" % msg)
|
||||
self.running = False
|
||||
except:
|
||||
_logger.error("Socket exception occurred "
|
||||
"%s" % traceback.format_exc() )
|
||||
self.running = False
|
||||
reset_frame = True
|
||||
finally:
|
||||
if reset_frame:
|
||||
self.framer.resetFrame()
|
||||
reset_frame = False
|
||||
|
||||
def send(self, message):
|
||||
""" Send a request (string) to the network
|
||||
|
||||
:param message: The unencoded modbus response
|
||||
"""
|
||||
if message.should_respond:
|
||||
# self.server.control.Counter.BusMessage += 1
|
||||
pdu = self.framer.buildPacket(message)
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug('send: [%s]- %s' % (message, b2a_hex(pdu)))
|
||||
return self.request.send(pdu)
|
||||
|
||||
|
||||
class ModbusDisconnectedRequestHandler(ModbusBaseRequestHandler):
|
||||
""" Implements the modbus server protocol
|
||||
|
||||
This uses the socketserver.BaseRequestHandler to implement
|
||||
the client handler for a disconnected protocol (UDP). The
|
||||
only difference is that we have to specify who to send the
|
||||
resulting packet data to.
|
||||
"""
|
||||
socket = None
|
||||
|
||||
def handle(self):
|
||||
""" Callback when we receive any data
|
||||
"""
|
||||
reset_frame = False
|
||||
while self.running:
|
||||
try:
|
||||
data, self.socket = self.request
|
||||
if not data:
|
||||
self.running = False
|
||||
data = b''
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug('Handling data: ' + hexlify_packets(data))
|
||||
# if not self.server.control.ListenOnly:
|
||||
units = self.server.context.slaves()
|
||||
single = self.server.context.single
|
||||
self.framer.processIncomingPacket(data, self.execute,
|
||||
units, single=single)
|
||||
except socket.timeout: pass
|
||||
except socket.error as msg:
|
||||
_logger.error("Socket error occurred %s" % msg)
|
||||
self.running = False
|
||||
reset_frame = True
|
||||
except Exception as msg:
|
||||
_logger.error(msg)
|
||||
self.running = False
|
||||
reset_frame = True
|
||||
finally:
|
||||
# Reset data after processing
|
||||
self.request = (None, self.socket)
|
||||
if reset_frame:
|
||||
self.framer.resetFrame()
|
||||
reset_frame = False
|
||||
|
||||
def send(self, message):
|
||||
""" Send a request (string) to the network
|
||||
|
||||
:param message: The unencoded modbus response
|
||||
"""
|
||||
if message.should_respond:
|
||||
#self.server.control.Counter.BusMessage += 1
|
||||
pdu = self.framer.buildPacket(message)
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug('send: [%s]- %s' % (message, b2a_hex(pdu)))
|
||||
return self.socket.sendto(pdu, self.client_address)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Server Implementations
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusTcpServer(socketserver.ThreadingTCPServer):
|
||||
"""
|
||||
A modbus threaded tcp socket server
|
||||
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
def __init__(self, context, framer=None, identity=None,
|
||||
address=None, handler=None, allow_reuse_address=False,
|
||||
**kwargs):
|
||||
""" Overloaded initializer for the socket server
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param handler: A handler for each client session; default is
|
||||
ModbusConnectedRequestHandler
|
||||
:param allow_reuse_address: Whether the server will allow the
|
||||
reuse of an address.
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
|
||||
False to treat 0 as any other unit_id
|
||||
"""
|
||||
self.threads = []
|
||||
self.allow_reuse_address = allow_reuse_address
|
||||
self.decoder = ServerDecoder()
|
||||
self.framer = framer or ModbusSocketFramer
|
||||
self.context = context or ModbusServerContext()
|
||||
self.control = ModbusControlBlock()
|
||||
self.address = address or ("", Defaults.Port)
|
||||
self.handler = handler or ModbusConnectedRequestHandler
|
||||
self.ignore_missing_slaves = kwargs.pop('ignore_missing_slaves',
|
||||
Defaults.IgnoreMissingSlaves)
|
||||
self.broadcast_enable = kwargs.pop('broadcast_enable',
|
||||
Defaults.broadcast_enable)
|
||||
|
||||
if isinstance(identity, ModbusDeviceIdentification):
|
||||
self.control.Identity.update(identity)
|
||||
|
||||
socketserver.ThreadingTCPServer.__init__(self, self.address,
|
||||
self.handler,
|
||||
**kwargs)
|
||||
|
||||
def process_request(self, request, client):
|
||||
""" Callback for connecting a new client thread
|
||||
|
||||
:param request: The request to handle
|
||||
:param client: The address of the client
|
||||
"""
|
||||
_logger.debug("Started thread to serve client at " + str(client))
|
||||
socketserver.ThreadingTCPServer.process_request(self, request, client)
|
||||
|
||||
def shutdown(self):
|
||||
""" Stops the serve_forever loop.
|
||||
|
||||
Overridden to signal handlers to stop.
|
||||
"""
|
||||
for thread in self.threads:
|
||||
thread.running = False
|
||||
socketserver.ThreadingTCPServer.shutdown(self)
|
||||
|
||||
def server_close(self):
|
||||
""" Callback for stopping the running server
|
||||
"""
|
||||
_logger.debug("Modbus server stopped")
|
||||
self.socket.close()
|
||||
for thread in self.threads:
|
||||
thread.running = False
|
||||
|
||||
|
||||
class ModbusTlsServer(ModbusTcpServer):
|
||||
"""
|
||||
A modbus threaded TLS server
|
||||
|
||||
We inherit and overload the ModbusTcpServer so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
def __init__(self, context, framer=None, identity=None,
|
||||
address=None, handler=None, allow_reuse_address=False,
|
||||
sslctx=None, certfile=None, keyfile=None, **kwargs):
|
||||
""" Overloaded initializer for the ModbusTcpServer
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param handler: A handler for each client session; default is
|
||||
ModbusConnectedRequestHandler
|
||||
:param allow_reuse_address: Whether the server will allow the
|
||||
reuse of an address.
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto
|
||||
create)
|
||||
:param certfile: The cert file path for TLS (used if sslctx is None)
|
||||
:param keyfile: The key file path for TLS (used if sslctx is None)
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
|
||||
False to treat 0 as any other unit_id
|
||||
"""
|
||||
self.sslctx = sslctx
|
||||
if self.sslctx is None:
|
||||
self.sslctx = ssl.create_default_context()
|
||||
self.sslctx.load_cert_chain(certfile=certfile, keyfile=keyfile)
|
||||
# According to MODBUS/TCP Security Protocol Specification, it is
|
||||
# TLSv2 at least
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1_1
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv3
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv2
|
||||
self.sslctx.verify_mode = ssl.CERT_OPTIONAL
|
||||
self.sslctx.check_hostname = False
|
||||
|
||||
ModbusTcpServer.__init__(self, context, framer, identity, address,
|
||||
handler, allow_reuse_address, **kwargs)
|
||||
|
||||
def server_activate(self):
|
||||
""" Callback for starting listening over TLS connection
|
||||
"""
|
||||
self.socket = self.sslctx.wrap_socket(self.socket, server_side=True)
|
||||
socketserver.ThreadingTCPServer.server_activate(self)
|
||||
|
||||
|
||||
class ModbusUdpServer(socketserver.ThreadingUDPServer):
|
||||
"""
|
||||
A modbus threaded udp socket server
|
||||
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
def __init__(self, context, framer=None, identity=None, address=None,
|
||||
handler=None, **kwargs):
|
||||
""" Overloaded initializer for the socket server
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param handler: A handler for each client session; default is
|
||||
ModbusDisonnectedRequestHandler
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
|
||||
False to treat 0 as any other unit_id
|
||||
"""
|
||||
self.threads = []
|
||||
self.decoder = ServerDecoder()
|
||||
self.framer = framer or ModbusSocketFramer
|
||||
self.context = context or ModbusServerContext()
|
||||
self.control = ModbusControlBlock()
|
||||
self.address = address or ("", Defaults.Port)
|
||||
self.handler = handler or ModbusDisconnectedRequestHandler
|
||||
self.ignore_missing_slaves = kwargs.pop('ignore_missing_slaves',
|
||||
Defaults.IgnoreMissingSlaves)
|
||||
self.broadcast_enable = kwargs.pop('broadcast_enable',
|
||||
Defaults.broadcast_enable)
|
||||
|
||||
if isinstance(identity, ModbusDeviceIdentification):
|
||||
self.control.Identity.update(identity)
|
||||
|
||||
socketserver.ThreadingUDPServer.__init__(self,
|
||||
self.address, self.handler, **kwargs)
|
||||
# self._BaseServer__shutdown_request = True
|
||||
|
||||
def process_request(self, request, client):
|
||||
""" Callback for connecting a new client thread
|
||||
|
||||
:param request: The request to handle
|
||||
:param client: The address of the client
|
||||
"""
|
||||
packet, socket = request # TODO I might have to rewrite
|
||||
_logger.debug("Started thread to serve client at " + str(client))
|
||||
socketserver.ThreadingUDPServer.process_request(self, request, client)
|
||||
|
||||
def server_close(self):
|
||||
""" Callback for stopping the running server
|
||||
"""
|
||||
_logger.debug("Modbus server stopped")
|
||||
self.socket.close()
|
||||
for thread in self.threads:
|
||||
thread.running = False
|
||||
|
||||
|
||||
class ModbusSerialServer(object):
|
||||
"""
|
||||
A modbus threaded serial socket server
|
||||
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
handler = None
|
||||
|
||||
def __init__(self, context, framer=None, identity=None, **kwargs):
|
||||
""" Overloaded initializer for the socket server
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param port: The serial port to attach to
|
||||
:param stopbits: The number of stop bits to use
|
||||
:param bytesize: The bytesize of the serial messages
|
||||
:param parity: Which kind of parity to use
|
||||
:param baudrate: The baud rate to use for the serial device
|
||||
:param timeout: The timeout to use for the serial device
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
|
||||
False to treat 0 as any other unit_id
|
||||
"""
|
||||
self.threads = []
|
||||
self.decoder = ServerDecoder()
|
||||
self.framer = framer or ModbusAsciiFramer
|
||||
self.context = context or ModbusServerContext()
|
||||
self.control = ModbusControlBlock()
|
||||
|
||||
if isinstance(identity, ModbusDeviceIdentification):
|
||||
self.control.Identity.update(identity)
|
||||
|
||||
self.device = kwargs.get('port', 0)
|
||||
self.stopbits = kwargs.get('stopbits', Defaults.Stopbits)
|
||||
self.bytesize = kwargs.get('bytesize', Defaults.Bytesize)
|
||||
self.parity = kwargs.get('parity', Defaults.Parity)
|
||||
self.baudrate = kwargs.get('baudrate', Defaults.Baudrate)
|
||||
self.timeout = kwargs.get('timeout', Defaults.Timeout)
|
||||
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
|
||||
Defaults.IgnoreMissingSlaves)
|
||||
self.broadcast_enable = kwargs.get('broadcast_enable',
|
||||
Defaults.broadcast_enable)
|
||||
self.socket = None
|
||||
if self._connect():
|
||||
self.is_running = True
|
||||
self._build_handler()
|
||||
|
||||
def _connect(self):
|
||||
""" Connect to the serial server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
if self.socket: return True
|
||||
try:
|
||||
self.socket = serial.Serial(port=self.device,
|
||||
timeout=self.timeout,
|
||||
bytesize=self.bytesize,
|
||||
stopbits=self.stopbits,
|
||||
baudrate=self.baudrate,
|
||||
parity=self.parity)
|
||||
except serial.SerialException as msg:
|
||||
_logger.error(msg)
|
||||
return self.socket is not None
|
||||
|
||||
def _build_handler(self):
|
||||
""" A helper method to create and monkeypatch
|
||||
a serial handler.
|
||||
|
||||
:returns: A patched handler
|
||||
"""
|
||||
|
||||
request = self.socket
|
||||
request.send = request.write
|
||||
request.recv = request.read
|
||||
self.handler = CustomSingleRequestHandler(request,
|
||||
(self.device, self.device),
|
||||
self)
|
||||
|
||||
def serve_forever(self):
|
||||
""" Callback for connecting a new client thread
|
||||
"""
|
||||
if self._connect():
|
||||
_logger.debug("Started thread to serve client")
|
||||
if not self.handler:
|
||||
self._build_handler()
|
||||
while self.is_running:
|
||||
if hasattr(self.handler, "response_manipulator"):
|
||||
self.handler.response_manipulator()
|
||||
else:
|
||||
self.handler.handle()
|
||||
else:
|
||||
_logger.error("Error opening serial port , "
|
||||
"Unable to start server!!")
|
||||
|
||||
def server_close(self):
|
||||
""" Callback for stopping the running server
|
||||
"""
|
||||
_logger.debug("Modbus server stopped")
|
||||
self.is_running = False
|
||||
self.handler.finish()
|
||||
self.handler.running = False
|
||||
self.handler = None
|
||||
self.socket.close()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Creation Factories
|
||||
# --------------------------------------------------------------------------- #
|
||||
def StartTcpServer(context=None, identity=None, address=None,
|
||||
custom_functions=[], **kwargs):
|
||||
""" A factory to start and run a tcp modbus server
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param ignore_missing_slaves: True to not send errors on a request to a
|
||||
missing slave
|
||||
"""
|
||||
framer = kwargs.pop("framer", ModbusSocketFramer)
|
||||
server = ModbusTcpServer(context, framer, identity, address, **kwargs)
|
||||
|
||||
for f in custom_functions:
|
||||
server.decoder.register(f)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
def StartTlsServer(context=None, identity=None, address=None, sslctx=None,
|
||||
certfile=None, keyfile=None, custom_functions=[], **kwargs):
|
||||
""" A factory to start and run a tls modbus server
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto create)
|
||||
:param certfile: The cert file path for TLS (used if sslctx is None)
|
||||
:param keyfile: The key file path for TLS (used if sslctx is None)
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param ignore_missing_slaves: True to not send errors on a request to a
|
||||
missing slave
|
||||
"""
|
||||
framer = kwargs.pop("framer", ModbusTlsFramer)
|
||||
server = ModbusTlsServer(context, framer, identity, address, sslctx=sslctx,
|
||||
certfile=certfile, keyfile=keyfile, **kwargs)
|
||||
|
||||
for f in custom_functions:
|
||||
server.decoder.register(f)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
def StartUdpServer(context=None, identity=None, address=None,
|
||||
custom_functions=[], **kwargs):
|
||||
""" A factory to start and run a udp modbus server
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param framer: The framer to operate with (default ModbusSocketFramer)
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
"""
|
||||
framer = kwargs.pop('framer', ModbusSocketFramer)
|
||||
server = ModbusUdpServer(context, framer, identity, address, **kwargs)
|
||||
for f in custom_functions:
|
||||
server.decoder.register(f)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
def StartSerialServer(context=None, identity=None, custom_functions=[],
|
||||
**kwargs):
|
||||
""" A factory to start and run a serial modbus server
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param framer: The framer to operate with (default ModbusAsciiFramer)
|
||||
:param port: The serial port to attach to
|
||||
:param stopbits: The number of stop bits to use
|
||||
:param bytesize: The bytesize of the serial messages
|
||||
:param parity: Which kind of parity to use
|
||||
:param baudrate: The baud rate to use for the serial device
|
||||
:param timeout: The timeout to use for the serial device
|
||||
:param ignore_missing_slaves: True to not send errors on a request to a
|
||||
missing slave
|
||||
"""
|
||||
framer = kwargs.pop('framer', ModbusAsciiFramer)
|
||||
server = ModbusSerialServer(context, framer, identity, **kwargs)
|
||||
for f in custom_functions:
|
||||
server.decoder.register(f)
|
||||
server.serve_forever()
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = [
|
||||
"StartTcpServer", "StartTlsServer", "StartUdpServer", "StartSerialServer"
|
||||
]
|
||||
|
||||
@@ -1,539 +0,0 @@
|
||||
"""
|
||||
Collection of transaction based abstractions
|
||||
|
||||
"""
|
||||
|
||||
import struct
|
||||
import socket
|
||||
import time
|
||||
from threading import RLock
|
||||
from functools import partial
|
||||
|
||||
from pymodbus.exceptions import ModbusIOException, NotImplementedException
|
||||
from pymodbus.exceptions import InvalidMessageReceivedException
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.framer.ascii_framer import ModbusAsciiFramer
|
||||
from pymodbus.framer.rtu_framer import ModbusRtuFramer
|
||||
from pymodbus.framer.socket_framer import ModbusSocketFramer
|
||||
from pymodbus.framer.tls_framer import ModbusTlsFramer
|
||||
from pymodbus.framer.binary_framer import ModbusBinaryFramer
|
||||
from pymodbus.utilities import hexlify_packets, ModbusTransactionState
|
||||
from pymodbus.compat import iterkeys, byte2int
|
||||
|
||||
|
||||
# Python 2 compatibility.
|
||||
try:
|
||||
TimeoutError
|
||||
except NameError:
|
||||
TimeoutError = socket.timeout
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# The Global Transaction Manager
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusTransactionManager(object):
|
||||
""" Implements a transaction for a manager
|
||||
|
||||
The transaction protocol can be represented by the following pseudo code::
|
||||
|
||||
count = 0
|
||||
do
|
||||
result = send(message)
|
||||
if (timeout or result == bad)
|
||||
count++
|
||||
else break
|
||||
while (count < 3)
|
||||
|
||||
This module helps to abstract this away from the framer and protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
""" Initializes an instance of the ModbusTransactionManager
|
||||
|
||||
:param client: The client socket wrapper
|
||||
:param retry_on_empty: Should the client retry on empty
|
||||
:param retries: The number of retries to allow
|
||||
"""
|
||||
self.tid = Defaults.TransactionId
|
||||
self.client = client
|
||||
self.backoff = kwargs.get('backoff', Defaults.Backoff) or 0.3
|
||||
self.retry_on_empty = kwargs.get('retry_on_empty',
|
||||
Defaults.RetryOnEmpty)
|
||||
self.retry_on_invalid = kwargs.get('retry_on_invalid',
|
||||
Defaults.RetryOnInvalid)
|
||||
self.retries = kwargs.get('retries', Defaults.Retries) or 1
|
||||
self.reset_socket = kwargs.get('reset_socket', True)
|
||||
self._transaction_lock = RLock()
|
||||
self._no_response_devices = []
|
||||
if client:
|
||||
self._set_adu_size()
|
||||
|
||||
def _set_adu_size(self):
|
||||
# base ADU size of modbus frame in bytes
|
||||
if isinstance(self.client.framer, ModbusSocketFramer):
|
||||
self.base_adu_size = 7 # tid(2), pid(2), length(2), uid(1)
|
||||
elif isinstance(self.client.framer, ModbusRtuFramer):
|
||||
self.base_adu_size = 3 # address(1), CRC(2)
|
||||
elif isinstance(self.client.framer, ModbusAsciiFramer):
|
||||
self.base_adu_size = 7 # start(1)+ Address(2), LRC(2) + end(2)
|
||||
elif isinstance(self.client.framer, ModbusBinaryFramer):
|
||||
self.base_adu_size = 5 # start(1) + Address(1), CRC(2) + end(1)
|
||||
elif isinstance(self.client.framer, ModbusTlsFramer):
|
||||
self.base_adu_size = 0 # no header and footer
|
||||
else:
|
||||
self.base_adu_size = -1
|
||||
|
||||
def _calculate_response_length(self, expected_pdu_size):
|
||||
if self.base_adu_size == -1:
|
||||
return None
|
||||
else:
|
||||
return self.base_adu_size + expected_pdu_size
|
||||
|
||||
def _calculate_exception_length(self):
|
||||
""" Returns the length of the Modbus Exception Response according to
|
||||
the type of Framer.
|
||||
"""
|
||||
if isinstance(self.client.framer, (ModbusSocketFramer,
|
||||
ModbusTlsFramer)):
|
||||
return self.base_adu_size + 2 # Fcode(1), ExcecptionCode(1)
|
||||
elif isinstance(self.client.framer, ModbusAsciiFramer):
|
||||
return self.base_adu_size + 4 # Fcode(2), ExcecptionCode(2)
|
||||
elif isinstance(self.client.framer, (ModbusRtuFramer,
|
||||
ModbusBinaryFramer)):
|
||||
return self.base_adu_size + 2 # Fcode(1), ExcecptionCode(1)
|
||||
|
||||
return None
|
||||
|
||||
def _validate_response(self, request, response, exp_resp_len):
|
||||
"""
|
||||
Validate Incoming response against request
|
||||
:param request: Request sent
|
||||
:param response: Response received
|
||||
:param exp_resp_len: Expected response length
|
||||
:return: New transactions state
|
||||
"""
|
||||
if not response:
|
||||
return False
|
||||
|
||||
mbap = self.client.framer.decode_data(response)
|
||||
if mbap.get('unit') != request.unit_id or mbap.get('fcode') & 0x7F != request.function_code:
|
||||
return False
|
||||
|
||||
if 'length' in mbap and exp_resp_len:
|
||||
return mbap.get('length') == exp_resp_len
|
||||
return True
|
||||
|
||||
def execute(self, request):
|
||||
""" Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
with self._transaction_lock:
|
||||
try:
|
||||
_logger.debug("Current transaction state - {}".format(
|
||||
ModbusTransactionState.to_string(self.client.state))
|
||||
)
|
||||
retries = self.retries
|
||||
request.transaction_id = self.getNextTID()
|
||||
_logger.debug("Running transaction "
|
||||
"{}".format(request.transaction_id))
|
||||
_buffer = hexlify_packets(self.client.framer._buffer)
|
||||
if _buffer:
|
||||
_logger.debug("Clearing current Frame "
|
||||
": - {}".format(_buffer))
|
||||
self.client.framer.resetFrame()
|
||||
broadcast = (self.client.broadcast_enable
|
||||
and request.unit_id == 0)
|
||||
if broadcast:
|
||||
self._transact(request, None, broadcast=True)
|
||||
response = b'Broadcast write sent - no response expected'
|
||||
else:
|
||||
expected_response_length = None
|
||||
if not isinstance(self.client.framer, ModbusSocketFramer):
|
||||
if hasattr(request, "get_response_pdu_size"):
|
||||
response_pdu_size = request.get_response_pdu_size()
|
||||
if isinstance(self.client.framer, ModbusAsciiFramer):
|
||||
response_pdu_size = response_pdu_size * 2
|
||||
if response_pdu_size:
|
||||
expected_response_length = self._calculate_response_length(response_pdu_size)
|
||||
if request.unit_id in self._no_response_devices:
|
||||
full = True
|
||||
else:
|
||||
full = False
|
||||
c_str = str(self.client)
|
||||
if "modbusudpclient" in c_str.lower().strip():
|
||||
full = True
|
||||
if not expected_response_length:
|
||||
expected_response_length = Defaults.ReadSize
|
||||
response, last_exception = self._transact(
|
||||
request,
|
||||
expected_response_length,
|
||||
full=full,
|
||||
broadcast=broadcast
|
||||
)
|
||||
while retries > 0:
|
||||
valid_response = self._validate_response(
|
||||
request, response, expected_response_length
|
||||
)
|
||||
if valid_response:
|
||||
if request.unit_id in self._no_response_devices and response:
|
||||
self._no_response_devices.remove(request.unit_id)
|
||||
_logger.debug("Got response!!!")
|
||||
break
|
||||
else:
|
||||
if not response:
|
||||
if request.unit_id not in self._no_response_devices:
|
||||
self._no_response_devices.append(request.unit_id)
|
||||
if self.retry_on_empty:
|
||||
response, last_exception = self._retry_transaction(retries, "empty", request, expected_response_length, full=full)
|
||||
retries -= 1
|
||||
else:
|
||||
# No response received and retries not enabled
|
||||
break
|
||||
else:
|
||||
if self.retry_on_invalid:
|
||||
response, last_exception = self._retry_transaction(retries, "invalid", request, expected_response_length, full=full)
|
||||
retries -= 1
|
||||
else:
|
||||
break
|
||||
# full = False
|
||||
addTransaction = partial(self.addTransaction,
|
||||
tid=request.transaction_id)
|
||||
self.client.framer.processIncomingPacket(response,
|
||||
addTransaction,
|
||||
request.unit_id)
|
||||
response = self.getTransaction(request.transaction_id)
|
||||
if not response:
|
||||
if len(self.transactions):
|
||||
response = self.getTransaction(tid=0)
|
||||
else:
|
||||
last_exception = last_exception or (
|
||||
"No Response received from the remote unit"
|
||||
"/Unable to decode response")
|
||||
response = ModbusIOException(last_exception,
|
||||
request.function_code)
|
||||
if self.reset_socket:
|
||||
self.client.close()
|
||||
if hasattr(self.client, "state"):
|
||||
_logger.debug("Changing transaction state from "
|
||||
"'PROCESSING REPLY' to "
|
||||
"'TRANSACTION_COMPLETE'")
|
||||
self.client.state = (
|
||||
ModbusTransactionState.TRANSACTION_COMPLETE)
|
||||
|
||||
return response
|
||||
except ModbusIOException as ex:
|
||||
# Handle decode errors in processIncomingPacket method
|
||||
_logger.exception(ex)
|
||||
self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE
|
||||
if self.reset_socket:
|
||||
self.client.close()
|
||||
return ex
|
||||
|
||||
def _retry_transaction(self, retries, reason,
|
||||
packet, response_length, full=False):
|
||||
_logger.debug("Retry on {} response - {}".format(reason, retries))
|
||||
_logger.debug("Changing transaction state from "
|
||||
"'WAITING_FOR_REPLY' to 'RETRYING'")
|
||||
self.client.state = ModbusTransactionState.RETRYING
|
||||
if self.backoff:
|
||||
delay = 2 ** (self.retries - retries) * self.backoff
|
||||
time.sleep(delay)
|
||||
_logger.debug("Sleeping {}".format(delay))
|
||||
self.client.connect()
|
||||
if hasattr(self.client, "_in_waiting"):
|
||||
in_waiting = self.client._in_waiting()
|
||||
if in_waiting:
|
||||
if response_length == in_waiting:
|
||||
result = self._recv(response_length, full)
|
||||
return result, None
|
||||
return self._transact(packet, response_length, full=full)
|
||||
|
||||
def _transact(self, packet, response_length,
|
||||
full=False, broadcast=False):
|
||||
"""
|
||||
Does a Write and Read transaction
|
||||
:param packet: packet to be sent
|
||||
:param response_length: Expected response length
|
||||
:param full: the target device was notorious for its no response. Dont
|
||||
waste time this time by partial querying
|
||||
:return: response
|
||||
"""
|
||||
last_exception = None
|
||||
try:
|
||||
self.client.connect()
|
||||
packet = self.client.framer.buildPacket(packet)
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug("SEND: " + hexlify_packets(packet))
|
||||
size = self._send(packet)
|
||||
if isinstance(size, bytes) and self.client.state == ModbusTransactionState.RETRYING:
|
||||
_logger.debug("Changing transaction state from "
|
||||
"'RETRYING' to 'PROCESSING REPLY'")
|
||||
self.client.state = ModbusTransactionState.PROCESSING_REPLY
|
||||
return size, None
|
||||
if broadcast:
|
||||
if size:
|
||||
_logger.debug("Changing transaction state from 'SENDING' "
|
||||
"to 'TRANSACTION_COMPLETE'")
|
||||
self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE
|
||||
return b'', None
|
||||
if size:
|
||||
_logger.debug("Changing transaction state from 'SENDING' "
|
||||
"to 'WAITING FOR REPLY'")
|
||||
self.client.state = ModbusTransactionState.WAITING_FOR_REPLY
|
||||
if hasattr(self.client, "handle_local_echo") and self.client.handle_local_echo is True:
|
||||
local_echo_packet = self._recv(size, full)
|
||||
if local_echo_packet != packet:
|
||||
return b'', "Wrong local echo"
|
||||
result = self._recv(response_length, full)
|
||||
# result2 = self._recv(response_length, full)
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug("RECV: " + hexlify_packets(result))
|
||||
|
||||
except (socket.error, ModbusIOException,
|
||||
InvalidMessageReceivedException) as msg:
|
||||
if self.reset_socket:
|
||||
self.client.close()
|
||||
_logger.debug("Transaction failed. (%s) " % msg)
|
||||
last_exception = msg
|
||||
result = b''
|
||||
return result, last_exception
|
||||
|
||||
def _send(self, packet, retrying=False):
|
||||
return self.client.framer.sendPacket(packet)
|
||||
|
||||
def _recv(self, expected_response_length, full):
|
||||
total = None
|
||||
if not full:
|
||||
exception_length = self._calculate_exception_length()
|
||||
if isinstance(self.client.framer, ModbusSocketFramer):
|
||||
min_size = 8
|
||||
elif isinstance(self.client.framer, ModbusRtuFramer):
|
||||
min_size = 2
|
||||
elif isinstance(self.client.framer, ModbusAsciiFramer):
|
||||
min_size = 5
|
||||
elif isinstance(self.client.framer, ModbusBinaryFramer):
|
||||
min_size = 3
|
||||
else:
|
||||
min_size = expected_response_length
|
||||
|
||||
read_min = self.client.framer.recvPacket(min_size)
|
||||
if len(read_min) != min_size:
|
||||
msg_start = "Incomplete message" if read_min else "No response"
|
||||
raise InvalidMessageReceivedException(
|
||||
"%s received, expected at least %d bytes "
|
||||
"(%d received)" % (msg_start, min_size, len(read_min))
|
||||
)
|
||||
if read_min:
|
||||
if isinstance(self.client.framer, ModbusSocketFramer):
|
||||
func_code = byte2int(read_min[-1])
|
||||
elif isinstance(self.client.framer, ModbusRtuFramer):
|
||||
func_code = byte2int(read_min[-1])
|
||||
elif isinstance(self.client.framer, ModbusAsciiFramer):
|
||||
func_code = int(read_min[3:5], 16)
|
||||
elif isinstance(self.client.framer, ModbusBinaryFramer):
|
||||
func_code = byte2int(read_min[-1])
|
||||
else:
|
||||
func_code = -1
|
||||
|
||||
if func_code < 0x80: # Not an error
|
||||
if isinstance(self.client.framer, ModbusSocketFramer):
|
||||
# Ommit UID, which is included in header size
|
||||
h_size = self.client.framer._hsize
|
||||
length = struct.unpack(">H", read_min[4:6])[0] - 1
|
||||
expected_response_length = h_size + length
|
||||
if expected_response_length is not None:
|
||||
expected_response_length -= min_size
|
||||
total = expected_response_length + min_size
|
||||
else:
|
||||
expected_response_length = exception_length - min_size
|
||||
total = expected_response_length + min_size
|
||||
else:
|
||||
total = expected_response_length
|
||||
else:
|
||||
read_min = b''
|
||||
total = expected_response_length
|
||||
result = self.client.framer.recvPacket(expected_response_length)
|
||||
result = read_min + result
|
||||
actual = len(result)
|
||||
if total is not None and actual != total:
|
||||
msg_start = "Incomplete message" if actual else "No response"
|
||||
_logger.debug("{} received, "
|
||||
"Expected {} bytes Recieved "
|
||||
"{} bytes !!!!".format(msg_start, total, actual))
|
||||
elif actual == 0:
|
||||
# If actual == 0 and total is not None then the above
|
||||
# should be triggered, so total must be None here
|
||||
_logger.debug("No response received to unbounded read !!!!")
|
||||
if self.client.state != ModbusTransactionState.PROCESSING_REPLY:
|
||||
_logger.debug("Changing transaction state from "
|
||||
"'WAITING FOR REPLY' to 'PROCESSING REPLY'")
|
||||
self.client.state = ModbusTransactionState.PROCESSING_REPLY
|
||||
return result
|
||||
|
||||
def addTransaction(self, request, tid=None):
|
||||
""" Adds a transaction to the handler
|
||||
|
||||
This holds the request in case it needs to be resent.
|
||||
After being sent, the request is removed.
|
||||
|
||||
:param request: The request to hold on to
|
||||
:param tid: The overloaded transaction id to use
|
||||
"""
|
||||
raise NotImplementedException("addTransaction")
|
||||
|
||||
def getTransaction(self, tid):
|
||||
""" Returns a transaction matching the referenced tid
|
||||
|
||||
If the transaction does not exist, None is returned
|
||||
|
||||
:param tid: The transaction to retrieve
|
||||
"""
|
||||
raise NotImplementedException("getTransaction")
|
||||
|
||||
def delTransaction(self, tid):
|
||||
""" Removes a transaction matching the referenced tid
|
||||
|
||||
:param tid: The transaction to remove
|
||||
"""
|
||||
raise NotImplementedException("delTransaction")
|
||||
|
||||
def getNextTID(self):
|
||||
""" Retrieve the next unique transaction identifier
|
||||
|
||||
This handles incrementing the identifier after
|
||||
retrieval
|
||||
|
||||
:returns: The next unique transaction identifier
|
||||
"""
|
||||
self.tid = (self.tid + 1) & 0xffff
|
||||
return self.tid
|
||||
|
||||
def reset(self):
|
||||
""" Resets the transaction identifier """
|
||||
self.tid = Defaults.TransactionId
|
||||
self.transactions = type(self.transactions)()
|
||||
|
||||
|
||||
class DictTransactionManager(ModbusTransactionManager):
|
||||
""" Impelements a transaction for a manager where the
|
||||
results are keyed based on the supplied transaction id.
|
||||
"""
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
""" Initializes an instance of the ModbusTransactionManager
|
||||
|
||||
:param client: The client socket wrapper
|
||||
"""
|
||||
self.transactions = {}
|
||||
super(DictTransactionManager, self).__init__(client, **kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
""" Iterater over the current managed transactions
|
||||
|
||||
:returns: An iterator of the managed transactions
|
||||
"""
|
||||
return iterkeys(self.transactions)
|
||||
|
||||
def addTransaction(self, request, tid=None):
|
||||
""" Adds a transaction to the handler
|
||||
|
||||
This holds the requets in case it needs to be resent.
|
||||
After being sent, the request is removed.
|
||||
|
||||
:param request: The request to hold on to
|
||||
:param tid: The overloaded transaction id to use
|
||||
"""
|
||||
tid = tid if tid != None else request.transaction_id
|
||||
_logger.debug("Adding transaction %d" % tid)
|
||||
self.transactions[tid] = request
|
||||
|
||||
def getTransaction(self, tid):
|
||||
""" Returns a transaction matching the referenced tid
|
||||
|
||||
If the transaction does not exist, None is returned
|
||||
|
||||
:param tid: The transaction to retrieve
|
||||
|
||||
"""
|
||||
_logger.debug("Getting transaction %d" % tid)
|
||||
|
||||
return self.transactions.pop(tid, None)
|
||||
|
||||
def delTransaction(self, tid):
|
||||
""" Removes a transaction matching the referenced tid
|
||||
|
||||
:param tid: The transaction to remove
|
||||
"""
|
||||
_logger.debug("deleting transaction %d" % tid)
|
||||
|
||||
self.transactions.pop(tid, None)
|
||||
|
||||
|
||||
class FifoTransactionManager(ModbusTransactionManager):
|
||||
""" Impelements a transaction for a manager where the
|
||||
results are returned in a FIFO manner.
|
||||
"""
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
""" Initializes an instance of the ModbusTransactionManager
|
||||
|
||||
:param client: The client socket wrapper
|
||||
"""
|
||||
super(FifoTransactionManager, self).__init__(client, **kwargs)
|
||||
self.transactions = []
|
||||
|
||||
def __iter__(self):
|
||||
""" Iterater over the current managed transactions
|
||||
|
||||
:returns: An iterator of the managed transactions
|
||||
"""
|
||||
return iter(self.transactions)
|
||||
|
||||
def addTransaction(self, request, tid=None):
|
||||
""" Adds a transaction to the handler
|
||||
|
||||
This holds the requets in case it needs to be resent.
|
||||
After being sent, the request is removed.
|
||||
|
||||
:param request: The request to hold on to
|
||||
:param tid: The overloaded transaction id to use
|
||||
"""
|
||||
tid = tid if tid is not None else request.transaction_id
|
||||
_logger.debug("Adding transaction %d" % tid)
|
||||
|
||||
self.transactions.append(request)
|
||||
|
||||
def getTransaction(self, tid):
|
||||
""" Returns a transaction matching the referenced tid
|
||||
|
||||
If the transaction does not exist, None is returned
|
||||
|
||||
:param tid: The transaction to retrieve
|
||||
"""
|
||||
return self.transactions.pop(0) if self.transactions else None
|
||||
|
||||
def delTransaction(self, tid):
|
||||
""" Removes a transaction matching the referenced tid
|
||||
|
||||
:param tid: The transaction to remove
|
||||
"""
|
||||
_logger.debug("Deleting transaction %d" % tid)
|
||||
if self.transactions: self.transactions.pop(0)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
__all__ = [
|
||||
"FifoTransactionManager",
|
||||
"DictTransactionManager",
|
||||
"ModbusSocketFramer", "ModbusTlsFramer", "ModbusRtuFramer",
|
||||
"ModbusAsciiFramer", "ModbusBinaryFramer",
|
||||
]
|
||||
@@ -1,258 +0,0 @@
|
||||
"""
|
||||
Modbus Utilities
|
||||
-----------------
|
||||
|
||||
A collection of utilities for packing data, unpacking
|
||||
data computing checksums, and decode checksums.
|
||||
"""
|
||||
from pymodbus.compat import int2byte, byte2int, IS_PYTHON3
|
||||
from six import string_types
|
||||
|
||||
|
||||
class ModbusTransactionState(object):
|
||||
"""
|
||||
Modbus Client States
|
||||
"""
|
||||
IDLE = 0
|
||||
SENDING = 1
|
||||
WAITING_FOR_REPLY = 2
|
||||
WAITING_TURNAROUND_DELAY = 3
|
||||
PROCESSING_REPLY = 4
|
||||
PROCESSING_ERROR = 5
|
||||
TRANSACTION_COMPLETE = 6
|
||||
RETRYING = 7
|
||||
NO_RESPONSE_STATE = 8
|
||||
|
||||
@classmethod
|
||||
def to_string(cls, state):
|
||||
states = {
|
||||
ModbusTransactionState.IDLE: "IDLE",
|
||||
ModbusTransactionState.SENDING: "SENDING",
|
||||
ModbusTransactionState.WAITING_FOR_REPLY: "WAITING_FOR_REPLY",
|
||||
ModbusTransactionState.WAITING_TURNAROUND_DELAY: "WAITING_TURNAROUND_DELAY",
|
||||
ModbusTransactionState.PROCESSING_REPLY: "PROCESSING_REPLY",
|
||||
ModbusTransactionState.PROCESSING_ERROR: "PROCESSING_ERROR",
|
||||
ModbusTransactionState.TRANSACTION_COMPLETE: "TRANSACTION_COMPLETE",
|
||||
ModbusTransactionState.RETRYING: "RETRYING TRANSACTION",
|
||||
}
|
||||
return states.get(state, None)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Helpers
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def default(value):
|
||||
"""
|
||||
Given a python object, return the default value
|
||||
of that object.
|
||||
|
||||
:param value: The value to get the default of
|
||||
:returns: The default value
|
||||
"""
|
||||
return type(value)()
|
||||
|
||||
|
||||
def dict_property(store, index):
|
||||
""" Helper to create class properties from a dictionary.
|
||||
Basically this allows you to remove a lot of possible
|
||||
boilerplate code.
|
||||
|
||||
:param store: The store store to pull from
|
||||
:param index: The index into the store to close over
|
||||
:returns: An initialized property set
|
||||
"""
|
||||
if hasattr(store, '__call__'):
|
||||
getter = lambda self: store(self)[index]
|
||||
setter = lambda self, value: store(self).__setitem__(index, value)
|
||||
elif isinstance(store, str):
|
||||
getter = lambda self: self.__getattribute__(store)[index]
|
||||
setter = lambda self, value: self.__getattribute__(store).__setitem__(
|
||||
index, value)
|
||||
else:
|
||||
getter = lambda self: store[index]
|
||||
setter = lambda self, value: store.__setitem__(index, value)
|
||||
|
||||
return property(getter, setter)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Bit packing functions
|
||||
# --------------------------------------------------------------------------- #
|
||||
def pack_bitstring(bits):
|
||||
""" Creates a string out of an array of bits
|
||||
|
||||
:param bits: A bit array
|
||||
|
||||
example::
|
||||
|
||||
bits = [False, True, False, True]
|
||||
result = pack_bitstring(bits)
|
||||
"""
|
||||
ret = b''
|
||||
i = packed = 0
|
||||
for bit in bits:
|
||||
if bit:
|
||||
packed += 128
|
||||
i += 1
|
||||
if i == 8:
|
||||
ret += int2byte(packed)
|
||||
i = packed = 0
|
||||
else:
|
||||
packed >>= 1
|
||||
if 0 < i < 8:
|
||||
packed >>= (7 - i)
|
||||
ret += int2byte(packed)
|
||||
return ret
|
||||
|
||||
|
||||
def unpack_bitstring(string):
|
||||
""" Creates bit array out of a string
|
||||
|
||||
:param string: The modbus data packet to decode
|
||||
|
||||
example::
|
||||
|
||||
bytes = 'bytes to decode'
|
||||
result = unpack_bitstring(bytes)
|
||||
"""
|
||||
byte_count = len(string)
|
||||
bits = []
|
||||
for byte in range(byte_count):
|
||||
if IS_PYTHON3:
|
||||
value = byte2int(int(string[byte]))
|
||||
else:
|
||||
value = byte2int(string[byte])
|
||||
for _ in range(8):
|
||||
bits.append((value & 1) == 1)
|
||||
value >>= 1
|
||||
return bits
|
||||
|
||||
|
||||
def make_byte_string(s):
|
||||
"""
|
||||
Returns byte string from a given string, python3 specific fix
|
||||
:param s:
|
||||
:return:
|
||||
"""
|
||||
if IS_PYTHON3 and isinstance(s, string_types):
|
||||
s = s.encode()
|
||||
return s
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Error Detection Functions
|
||||
# --------------------------------------------------------------------------- #
|
||||
def __generate_crc16_table():
|
||||
""" Generates a crc16 lookup table
|
||||
|
||||
.. note:: This will only be generated once
|
||||
"""
|
||||
result = []
|
||||
for byte in range(256):
|
||||
crc = 0x0000
|
||||
for _ in range(8):
|
||||
if (byte ^ crc) & 0x0001:
|
||||
crc = (crc >> 1) ^ 0xa001
|
||||
else: crc >>= 1
|
||||
byte >>= 1
|
||||
result.append(crc)
|
||||
return result
|
||||
|
||||
__crc16_table = __generate_crc16_table()
|
||||
|
||||
|
||||
def computeCRC(data):
|
||||
""" Computes a crc16 on the passed in string. For modbus,
|
||||
this is only used on the binary serial protocols (in this
|
||||
case RTU).
|
||||
|
||||
The difference between modbus's crc16 and a normal crc16
|
||||
is that modbus starts the crc value out at 0xffff.
|
||||
|
||||
:param data: The data to create a crc16 of
|
||||
:returns: The calculated CRC
|
||||
"""
|
||||
crc = 0xffff
|
||||
for a in data:
|
||||
idx = __crc16_table[(crc ^ byte2int(a)) & 0xff]
|
||||
crc = ((crc >> 8) & 0xff) ^ idx
|
||||
swapped = ((crc << 8) & 0xff00) | ((crc >> 8) & 0x00ff)
|
||||
return swapped
|
||||
|
||||
|
||||
def checkCRC(data, check):
|
||||
""" Checks if the data matches the passed in CRC
|
||||
|
||||
:param data: The data to create a crc16 of
|
||||
:param check: The CRC to validate
|
||||
:returns: True if matched, False otherwise
|
||||
"""
|
||||
return computeCRC(data) == check
|
||||
|
||||
|
||||
def computeLRC(data):
|
||||
""" Used to compute the longitudinal redundancy check
|
||||
against a string. This is only used on the serial ASCII
|
||||
modbus protocol. A full description of this implementation
|
||||
can be found in appendex B of the serial line modbus description.
|
||||
|
||||
:param data: The data to apply a lrc to
|
||||
:returns: The calculated LRC
|
||||
|
||||
"""
|
||||
lrc = sum(byte2int(a) for a in data) & 0xff
|
||||
lrc = (lrc ^ 0xff) + 1
|
||||
return lrc & 0xff
|
||||
|
||||
|
||||
def checkLRC(data, check):
|
||||
""" Checks if the passed in data matches the LRC
|
||||
|
||||
:param data: The data to calculate
|
||||
:param check: The LRC to validate
|
||||
:returns: True if matched, False otherwise
|
||||
"""
|
||||
return computeLRC(data) == check
|
||||
|
||||
|
||||
def rtuFrameSize(data, byte_count_pos):
|
||||
""" Calculates the size of the frame based on the byte count.
|
||||
|
||||
:param data: The buffer containing the frame.
|
||||
:param byte_count_pos: The index of the byte count in the buffer.
|
||||
:returns: The size of the frame.
|
||||
|
||||
The structure of frames with a byte count field is always the
|
||||
same:
|
||||
|
||||
- first, there are some header fields
|
||||
- then the byte count field
|
||||
- then as many data bytes as indicated by the byte count,
|
||||
- finally the CRC (two bytes).
|
||||
|
||||
To calculate the frame size, it is therefore sufficient to extract
|
||||
the contents of the byte count field, add the position of this
|
||||
field, and finally increment the sum by three (one byte for the
|
||||
byte count field, two for the CRC).
|
||||
"""
|
||||
return byte2int(data[byte_count_pos]) + byte_count_pos + 3
|
||||
|
||||
|
||||
def hexlify_packets(packet):
|
||||
"""
|
||||
Returns hex representation of bytestring received
|
||||
:param packet:
|
||||
:return:
|
||||
"""
|
||||
if not packet:
|
||||
return ''
|
||||
if IS_PYTHON3:
|
||||
return " ".join([hex(byte2int(x)) for x in packet])
|
||||
else:
|
||||
return u" ".join([hex(byte2int(x)) for x in packet])
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
__all__ = [
|
||||
'pack_bitstring', 'unpack_bitstring', 'default',
|
||||
'computeCRC', 'checkCRC', 'computeLRC', 'checkLRC', 'rtuFrameSize'
|
||||
]
|
||||
@@ -1,52 +0,0 @@
|
||||
"""
|
||||
Handle the version information here; you should only have to
|
||||
change the version tuple.
|
||||
|
||||
Since we are using twisted's version class, we can also query
|
||||
the svn version as well using the local .entries file.
|
||||
"""
|
||||
|
||||
|
||||
class Version(object):
|
||||
|
||||
def __init__(self, package, major, minor, micro, pre=None):
|
||||
"""
|
||||
|
||||
:param package: Name of the package that this is a version of.
|
||||
:param major: The major version number.
|
||||
:param minor: The minor version number.
|
||||
:param micro: The micro version number.
|
||||
:param pre: The pre release tag
|
||||
"""
|
||||
self.package = package
|
||||
self.major = major
|
||||
self.minor = minor
|
||||
self.micro = micro
|
||||
self.pre = pre
|
||||
|
||||
def short(self):
|
||||
""" Return a string in canonical short version format
|
||||
<major>.<minor>.<micro>.<pre>
|
||||
"""
|
||||
if self.pre:
|
||||
return '%d.%d.%d.%s' % (self.major, self.minor, self.micro, self.pre)
|
||||
else:
|
||||
return '%d.%d.%d' % (self.major, self.minor, self.micro)
|
||||
|
||||
def __str__(self):
|
||||
""" Returns a string representation of the object
|
||||
|
||||
:returns: A string representation of this object
|
||||
"""
|
||||
return '[%s, version %s]' % (self.package, self.short())
|
||||
|
||||
|
||||
version = Version('pymodbus', 2, 5, 3)
|
||||
|
||||
version.__name__ = 'pymodbus' # fix epydoc error
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
__all__ = ["version"]
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This is a wrapper module for different platform implementations
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
from serial.serialutil import *
|
||||
#~ SerialBase, SerialException, to_bytes, iterbytes
|
||||
|
||||
__version__ = '3.5'
|
||||
|
||||
VERSION = __version__
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
if sys.platform == 'cli':
|
||||
from serial.serialcli import Serial
|
||||
else:
|
||||
import os
|
||||
# chose an implementation, depending on os
|
||||
if os.name == 'nt': # sys.platform == 'win32':
|
||||
from serial.serialwin32 import Serial
|
||||
elif os.name == 'posix':
|
||||
from serial.serialposix import Serial, PosixPollSerial, VTIMESerial # noqa
|
||||
elif os.name == 'java':
|
||||
from serial.serialjava import Serial
|
||||
else:
|
||||
raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
|
||||
|
||||
|
||||
protocol_handler_packages = [
|
||||
'serial.urlhandler',
|
||||
]
|
||||
|
||||
|
||||
def serial_for_url(url, *args, **kwargs):
|
||||
"""\
|
||||
Get an instance of the Serial class, depending on port/url. The port is not
|
||||
opened when the keyword parameter 'do_not_open' is true, by default it
|
||||
is. All other parameters are directly passed to the __init__ method when
|
||||
the port is instantiated.
|
||||
|
||||
The list of package names that is searched for protocol handlers is kept in
|
||||
``protocol_handler_packages``.
|
||||
|
||||
e.g. we want to support a URL ``foobar://``. A module
|
||||
``my_handlers.protocol_foobar`` is provided by the user. Then
|
||||
``protocol_handler_packages.append("my_handlers")`` would extend the search
|
||||
path so that ``serial_for_url("foobar://"))`` would work.
|
||||
"""
|
||||
# check and remove extra parameter to not confuse the Serial class
|
||||
do_open = not kwargs.pop('do_not_open', False)
|
||||
# the default is to use the native implementation
|
||||
klass = Serial
|
||||
try:
|
||||
url_lowercase = url.lower()
|
||||
except AttributeError:
|
||||
# it's not a string, use default
|
||||
pass
|
||||
else:
|
||||
# if it is an URL, try to import the handler module from the list of possible packages
|
||||
if '://' in url_lowercase:
|
||||
protocol = url_lowercase.split('://', 1)[0]
|
||||
module_name = '.protocol_{}'.format(protocol)
|
||||
for package_name in protocol_handler_packages:
|
||||
try:
|
||||
importlib.import_module(package_name)
|
||||
handler_module = importlib.import_module(module_name, package_name)
|
||||
except ImportError:
|
||||
continue
|
||||
else:
|
||||
if hasattr(handler_module, 'serial_class_for_url'):
|
||||
url, klass = handler_module.serial_class_for_url(url)
|
||||
else:
|
||||
klass = handler_module.Serial
|
||||
break
|
||||
else:
|
||||
raise ValueError('invalid URL, protocol {!r} not known'.format(protocol))
|
||||
# instantiate and open when desired
|
||||
instance = klass(None, *args, **kwargs)
|
||||
instance.port = url
|
||||
if do_open:
|
||||
instance.open()
|
||||
return instance
|
||||
@@ -1,3 +0,0 @@
|
||||
from .tools import miniterm
|
||||
|
||||
miniterm.main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# RS485 support
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""\
|
||||
The settings for RS485 are stored in a dedicated object that can be applied to
|
||||
serial ports (where supported).
|
||||
NOTE: Some implementations may only support a subset of the settings.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import time
|
||||
import serial
|
||||
|
||||
|
||||
class RS485Settings(object):
|
||||
def __init__(
|
||||
self,
|
||||
rts_level_for_tx=True,
|
||||
rts_level_for_rx=False,
|
||||
loopback=False,
|
||||
delay_before_tx=None,
|
||||
delay_before_rx=None):
|
||||
self.rts_level_for_tx = rts_level_for_tx
|
||||
self.rts_level_for_rx = rts_level_for_rx
|
||||
self.loopback = loopback
|
||||
self.delay_before_tx = delay_before_tx
|
||||
self.delay_before_rx = delay_before_rx
|
||||
|
||||
|
||||
class RS485(serial.Serial):
|
||||
"""\
|
||||
A subclass that replaces the write method with one that toggles RTS
|
||||
according to the RS485 settings.
|
||||
|
||||
NOTE: This may work unreliably on some serial ports (control signals not
|
||||
synchronized or delayed compared to data). Using delays may be
|
||||
unreliable (varying times, larger than expected) as the OS may not
|
||||
support very fine grained delays (no smaller than in the order of
|
||||
tens of milliseconds).
|
||||
|
||||
NOTE: Some implementations support this natively. Better performance
|
||||
can be expected when the native version is used.
|
||||
|
||||
NOTE: The loopback property is ignored by this implementation. The actual
|
||||
behavior depends on the used hardware.
|
||||
|
||||
Usage:
|
||||
|
||||
ser = RS485(...)
|
||||
ser.rs485_mode = RS485Settings(...)
|
||||
ser.write(b'hello')
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RS485, self).__init__(*args, **kwargs)
|
||||
self._alternate_rs485_settings = None
|
||||
|
||||
def write(self, b):
|
||||
"""Write to port, controlling RTS before and after transmitting."""
|
||||
if self._alternate_rs485_settings is not None:
|
||||
# apply level for TX and optional delay
|
||||
self.setRTS(self._alternate_rs485_settings.rts_level_for_tx)
|
||||
if self._alternate_rs485_settings.delay_before_tx is not None:
|
||||
time.sleep(self._alternate_rs485_settings.delay_before_tx)
|
||||
# write and wait for data to be written
|
||||
super(RS485, self).write(b)
|
||||
super(RS485, self).flush()
|
||||
# optional delay and apply level for RX
|
||||
if self._alternate_rs485_settings.delay_before_rx is not None:
|
||||
time.sleep(self._alternate_rs485_settings.delay_before_rx)
|
||||
self.setRTS(self._alternate_rs485_settings.rts_level_for_rx)
|
||||
else:
|
||||
super(RS485, self).write(b)
|
||||
|
||||
# redirect where the property stores the settings so that underlying Serial
|
||||
# instance does not see them
|
||||
@property
|
||||
def rs485_mode(self):
|
||||
"""\
|
||||
Enable RS485 mode and apply new settings, set to None to disable.
|
||||
See serial.rs485.RS485Settings for more info about the value.
|
||||
"""
|
||||
return self._alternate_rs485_settings
|
||||
|
||||
@rs485_mode.setter
|
||||
def rs485_mode(self, rs485_settings):
|
||||
self._alternate_rs485_settings = rs485_settings
|
||||
@@ -1,253 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# Backend for .NET/Mono (IronPython), .NET >= 2
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2008-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import System
|
||||
import System.IO.Ports
|
||||
from serial.serialutil import *
|
||||
|
||||
# must invoke function with byte array, make a helper to convert strings
|
||||
# to byte arrays
|
||||
sab = System.Array[System.Byte]
|
||||
|
||||
|
||||
def as_byte_array(string):
|
||||
return sab([ord(x) for x in string]) # XXX will require adaption when run with a 3.x compatible IronPython
|
||||
|
||||
|
||||
class Serial(SerialBase):
|
||||
"""Serial port implementation for .NET/Mono."""
|
||||
|
||||
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||
9600, 19200, 38400, 57600, 115200)
|
||||
|
||||
def open(self):
|
||||
"""\
|
||||
Open port with current settings. This may throw a SerialException
|
||||
if the port cannot be opened.
|
||||
"""
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
try:
|
||||
self._port_handle = System.IO.Ports.SerialPort(self.portstr)
|
||||
except Exception as msg:
|
||||
self._port_handle = None
|
||||
raise SerialException("could not open port %s: %s" % (self.portstr, msg))
|
||||
|
||||
# if RTS and/or DTR are not set before open, they default to True
|
||||
if self._rts_state is None:
|
||||
self._rts_state = True
|
||||
if self._dtr_state is None:
|
||||
self._dtr_state = True
|
||||
|
||||
self._reconfigure_port()
|
||||
self._port_handle.Open()
|
||||
self.is_open = True
|
||||
if not self._dsrdtr:
|
||||
self._update_dtr_state()
|
||||
if not self._rtscts:
|
||||
self._update_rts_state()
|
||||
self.reset_input_buffer()
|
||||
|
||||
def _reconfigure_port(self):
|
||||
"""Set communication parameters on opened port."""
|
||||
if not self._port_handle:
|
||||
raise SerialException("Can only operate on a valid port handle")
|
||||
|
||||
#~ self._port_handle.ReceivedBytesThreshold = 1
|
||||
|
||||
if self._timeout is None:
|
||||
self._port_handle.ReadTimeout = System.IO.Ports.SerialPort.InfiniteTimeout
|
||||
else:
|
||||
self._port_handle.ReadTimeout = int(self._timeout * 1000)
|
||||
|
||||
# if self._timeout != 0 and self._interCharTimeout is not None:
|
||||
# timeouts = (int(self._interCharTimeout * 1000),) + timeouts[1:]
|
||||
|
||||
if self._write_timeout is None:
|
||||
self._port_handle.WriteTimeout = System.IO.Ports.SerialPort.InfiniteTimeout
|
||||
else:
|
||||
self._port_handle.WriteTimeout = int(self._write_timeout * 1000)
|
||||
|
||||
# Setup the connection info.
|
||||
try:
|
||||
self._port_handle.BaudRate = self._baudrate
|
||||
except IOError as e:
|
||||
# catch errors from illegal baudrate settings
|
||||
raise ValueError(str(e))
|
||||
|
||||
if self._bytesize == FIVEBITS:
|
||||
self._port_handle.DataBits = 5
|
||||
elif self._bytesize == SIXBITS:
|
||||
self._port_handle.DataBits = 6
|
||||
elif self._bytesize == SEVENBITS:
|
||||
self._port_handle.DataBits = 7
|
||||
elif self._bytesize == EIGHTBITS:
|
||||
self._port_handle.DataBits = 8
|
||||
else:
|
||||
raise ValueError("Unsupported number of data bits: %r" % self._bytesize)
|
||||
|
||||
if self._parity == PARITY_NONE:
|
||||
self._port_handle.Parity = getattr(System.IO.Ports.Parity, 'None') # reserved keyword in Py3k
|
||||
elif self._parity == PARITY_EVEN:
|
||||
self._port_handle.Parity = System.IO.Ports.Parity.Even
|
||||
elif self._parity == PARITY_ODD:
|
||||
self._port_handle.Parity = System.IO.Ports.Parity.Odd
|
||||
elif self._parity == PARITY_MARK:
|
||||
self._port_handle.Parity = System.IO.Ports.Parity.Mark
|
||||
elif self._parity == PARITY_SPACE:
|
||||
self._port_handle.Parity = System.IO.Ports.Parity.Space
|
||||
else:
|
||||
raise ValueError("Unsupported parity mode: %r" % self._parity)
|
||||
|
||||
if self._stopbits == STOPBITS_ONE:
|
||||
self._port_handle.StopBits = System.IO.Ports.StopBits.One
|
||||
elif self._stopbits == STOPBITS_ONE_POINT_FIVE:
|
||||
self._port_handle.StopBits = System.IO.Ports.StopBits.OnePointFive
|
||||
elif self._stopbits == STOPBITS_TWO:
|
||||
self._port_handle.StopBits = System.IO.Ports.StopBits.Two
|
||||
else:
|
||||
raise ValueError("Unsupported number of stop bits: %r" % self._stopbits)
|
||||
|
||||
if self._rtscts and self._xonxoff:
|
||||
self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSendXOnXOff
|
||||
elif self._rtscts:
|
||||
self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSend
|
||||
elif self._xonxoff:
|
||||
self._port_handle.Handshake = System.IO.Ports.Handshake.XOnXOff
|
||||
else:
|
||||
self._port_handle.Handshake = getattr(System.IO.Ports.Handshake, 'None') # reserved keyword in Py3k
|
||||
|
||||
#~ def __del__(self):
|
||||
#~ self.close()
|
||||
|
||||
def close(self):
|
||||
"""Close port"""
|
||||
if self.is_open:
|
||||
if self._port_handle:
|
||||
try:
|
||||
self._port_handle.Close()
|
||||
except System.IO.Ports.InvalidOperationException:
|
||||
# ignore errors. can happen for unplugged USB serial devices
|
||||
pass
|
||||
self._port_handle = None
|
||||
self.is_open = False
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of characters currently in the input buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
return self._port_handle.BytesToRead
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
# must use single byte reads as this is the only way to read
|
||||
# without applying encodings
|
||||
data = bytearray()
|
||||
while size:
|
||||
try:
|
||||
data.append(self._port_handle.ReadByte())
|
||||
except System.TimeoutException:
|
||||
break
|
||||
else:
|
||||
size -= 1
|
||||
return bytes(data)
|
||||
|
||||
def write(self, data):
|
||||
"""Output the given string over the serial port."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
#~ if not isinstance(data, (bytes, bytearray)):
|
||||
#~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
|
||||
try:
|
||||
# must call overloaded method with byte array argument
|
||||
# as this is the only one not applying encodings
|
||||
self._port_handle.Write(as_byte_array(data), 0, len(data))
|
||||
except System.TimeoutException:
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
return len(data)
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._port_handle.DiscardInBuffer()
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""\
|
||||
Clear output buffer, aborting the current output and
|
||||
discarding all that is in the buffer.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._port_handle.DiscardOutBuffer()
|
||||
|
||||
def _update_break_state(self):
|
||||
"""
|
||||
Set break: Controls TXD. When active, to transmitting is possible.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._port_handle.BreakState = bool(self._break_state)
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._port_handle.RtsEnable = bool(self._rts_state)
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._port_handle.DtrEnable = bool(self._dtr_state)
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
return self._port_handle.CtsHolding
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
return self._port_handle.DsrHolding
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
#~ return self._port_handle.XXX
|
||||
return False # XXX an error would be better
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
return self._port_handle.CDHolding
|
||||
|
||||
# - - platform specific - - - -
|
||||
# none
|
||||
@@ -1,251 +0,0 @@
|
||||
#!jython
|
||||
#
|
||||
# Backend Jython with JavaComm
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2002-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from serial.serialutil import *
|
||||
|
||||
|
||||
def my_import(name):
|
||||
mod = __import__(name)
|
||||
components = name.split('.')
|
||||
for comp in components[1:]:
|
||||
mod = getattr(mod, comp)
|
||||
return mod
|
||||
|
||||
|
||||
def detect_java_comm(names):
|
||||
"""try given list of modules and return that imports"""
|
||||
for name in names:
|
||||
try:
|
||||
mod = my_import(name)
|
||||
mod.SerialPort
|
||||
return mod
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
raise ImportError("No Java Communications API implementation found")
|
||||
|
||||
|
||||
# Java Communications API implementations
|
||||
# http://mho.republika.pl/java/comm/
|
||||
|
||||
comm = detect_java_comm([
|
||||
'javax.comm', # Sun/IBM
|
||||
'gnu.io', # RXTX
|
||||
])
|
||||
|
||||
|
||||
def device(portnumber):
|
||||
"""Turn a port number into a device name"""
|
||||
enum = comm.CommPortIdentifier.getPortIdentifiers()
|
||||
ports = []
|
||||
while enum.hasMoreElements():
|
||||
el = enum.nextElement()
|
||||
if el.getPortType() == comm.CommPortIdentifier.PORT_SERIAL:
|
||||
ports.append(el)
|
||||
return ports[portnumber].getName()
|
||||
|
||||
|
||||
class Serial(SerialBase):
|
||||
"""\
|
||||
Serial port class, implemented with Java Communications API and
|
||||
thus usable with jython and the appropriate java extension.
|
||||
"""
|
||||
|
||||
def open(self):
|
||||
"""\
|
||||
Open port with current settings. This may throw a SerialException
|
||||
if the port cannot be opened.
|
||||
"""
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
if type(self._port) == type(''): # strings are taken directly
|
||||
portId = comm.CommPortIdentifier.getPortIdentifier(self._port)
|
||||
else:
|
||||
portId = comm.CommPortIdentifier.getPortIdentifier(device(self._port)) # numbers are transformed to a comport id obj
|
||||
try:
|
||||
self.sPort = portId.open("python serial module", 10)
|
||||
except Exception as msg:
|
||||
self.sPort = None
|
||||
raise SerialException("Could not open port: %s" % msg)
|
||||
self._reconfigurePort()
|
||||
self._instream = self.sPort.getInputStream()
|
||||
self._outstream = self.sPort.getOutputStream()
|
||||
self.is_open = True
|
||||
|
||||
def _reconfigurePort(self):
|
||||
"""Set communication parameters on opened port."""
|
||||
if not self.sPort:
|
||||
raise SerialException("Can only operate on a valid port handle")
|
||||
|
||||
self.sPort.enableReceiveTimeout(30)
|
||||
if self._bytesize == FIVEBITS:
|
||||
jdatabits = comm.SerialPort.DATABITS_5
|
||||
elif self._bytesize == SIXBITS:
|
||||
jdatabits = comm.SerialPort.DATABITS_6
|
||||
elif self._bytesize == SEVENBITS:
|
||||
jdatabits = comm.SerialPort.DATABITS_7
|
||||
elif self._bytesize == EIGHTBITS:
|
||||
jdatabits = comm.SerialPort.DATABITS_8
|
||||
else:
|
||||
raise ValueError("unsupported bytesize: %r" % self._bytesize)
|
||||
|
||||
if self._stopbits == STOPBITS_ONE:
|
||||
jstopbits = comm.SerialPort.STOPBITS_1
|
||||
elif self._stopbits == STOPBITS_ONE_POINT_FIVE:
|
||||
jstopbits = comm.SerialPort.STOPBITS_1_5
|
||||
elif self._stopbits == STOPBITS_TWO:
|
||||
jstopbits = comm.SerialPort.STOPBITS_2
|
||||
else:
|
||||
raise ValueError("unsupported number of stopbits: %r" % self._stopbits)
|
||||
|
||||
if self._parity == PARITY_NONE:
|
||||
jparity = comm.SerialPort.PARITY_NONE
|
||||
elif self._parity == PARITY_EVEN:
|
||||
jparity = comm.SerialPort.PARITY_EVEN
|
||||
elif self._parity == PARITY_ODD:
|
||||
jparity = comm.SerialPort.PARITY_ODD
|
||||
elif self._parity == PARITY_MARK:
|
||||
jparity = comm.SerialPort.PARITY_MARK
|
||||
elif self._parity == PARITY_SPACE:
|
||||
jparity = comm.SerialPort.PARITY_SPACE
|
||||
else:
|
||||
raise ValueError("unsupported parity type: %r" % self._parity)
|
||||
|
||||
jflowin = jflowout = 0
|
||||
if self._rtscts:
|
||||
jflowin |= comm.SerialPort.FLOWCONTROL_RTSCTS_IN
|
||||
jflowout |= comm.SerialPort.FLOWCONTROL_RTSCTS_OUT
|
||||
if self._xonxoff:
|
||||
jflowin |= comm.SerialPort.FLOWCONTROL_XONXOFF_IN
|
||||
jflowout |= comm.SerialPort.FLOWCONTROL_XONXOFF_OUT
|
||||
|
||||
self.sPort.setSerialPortParams(self._baudrate, jdatabits, jstopbits, jparity)
|
||||
self.sPort.setFlowControlMode(jflowin | jflowout)
|
||||
|
||||
if self._timeout >= 0:
|
||||
self.sPort.enableReceiveTimeout(int(self._timeout*1000))
|
||||
else:
|
||||
self.sPort.disableReceiveTimeout()
|
||||
|
||||
def close(self):
|
||||
"""Close port"""
|
||||
if self.is_open:
|
||||
if self.sPort:
|
||||
self._instream.close()
|
||||
self._outstream.close()
|
||||
self.sPort.close()
|
||||
self.sPort = None
|
||||
self.is_open = False
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of characters currently in the input buffer."""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
return self._instream.available()
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
read = bytearray()
|
||||
if size > 0:
|
||||
while len(read) < size:
|
||||
x = self._instream.read()
|
||||
if x == -1:
|
||||
if self.timeout >= 0:
|
||||
break
|
||||
else:
|
||||
read.append(x)
|
||||
return bytes(read)
|
||||
|
||||
def write(self, data):
|
||||
"""Output the given string over the serial port."""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
if not isinstance(data, (bytes, bytearray)):
|
||||
raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
|
||||
self._outstream.write(data)
|
||||
return len(data)
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self._instream.skip(self._instream.available())
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""\
|
||||
Clear output buffer, aborting the current output and
|
||||
discarding all that is in the buffer.
|
||||
"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self._outstream.flush()
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
"""Send break condition. Timed, returns to idle state after given duration."""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.sendBreak(duration*1000.0)
|
||||
|
||||
def _update_break_state(self):
|
||||
"""Set break: Controls TXD. When active, to transmitting is possible."""
|
||||
if self.fd is None:
|
||||
raise PortNotOpenError()
|
||||
raise SerialException("The _update_break_state function is not implemented in java.")
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.setRTS(self._rts_state)
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.setDTR(self._dtr_state)
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.isCTS()
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.isDSR()
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.isRI()
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.isCD()
|
||||
@@ -1,900 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# backend for serial IO for POSIX compatible systems, like Linux, OSX
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# parts based on code from Grant B. Edwards <grante@visi.com>:
|
||||
# ftp://ftp.visi.com/users/grante/python/PosixSerial.py
|
||||
#
|
||||
# references: http://www.easysw.com/~mike/serial/serial.html
|
||||
|
||||
# Collection of port names (was previously used by number_to_device which was
|
||||
# removed.
|
||||
# - Linux /dev/ttyS%d (confirmed)
|
||||
# - cygwin/win32 /dev/com%d (confirmed)
|
||||
# - openbsd (OpenBSD) /dev/cua%02d
|
||||
# - bsd*, freebsd* /dev/cuad%d
|
||||
# - darwin (OS X) /dev/cuad%d
|
||||
# - netbsd /dev/dty%02d (NetBSD 1.6 testing by Erk)
|
||||
# - irix (IRIX) /dev/ttyf%d (partially tested) names depending on flow control
|
||||
# - hp (HP-UX) /dev/tty%dp0 (not tested)
|
||||
# - sunos (Solaris/SunOS) /dev/tty%c (letters, 'a'..'z') (confirmed)
|
||||
# - aix (AIX) /dev/tty%d
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
import errno
|
||||
import fcntl
|
||||
import os
|
||||
import select
|
||||
import struct
|
||||
import sys
|
||||
import termios
|
||||
|
||||
import serial
|
||||
from serial.serialutil import SerialBase, SerialException, to_bytes, \
|
||||
PortNotOpenError, SerialTimeoutException, Timeout
|
||||
|
||||
|
||||
class PlatformSpecificBase(object):
|
||||
BAUDRATE_CONSTANTS = {}
|
||||
|
||||
def _set_special_baudrate(self, baudrate):
|
||||
raise NotImplementedError('non-standard baudrates are not supported on this platform')
|
||||
|
||||
def _set_rs485_mode(self, rs485_settings):
|
||||
raise NotImplementedError('RS485 not supported on this platform')
|
||||
|
||||
def set_low_latency_mode(self, low_latency_settings):
|
||||
raise NotImplementedError('Low latency not supported on this platform')
|
||||
|
||||
def _update_break_state(self):
|
||||
"""\
|
||||
Set break: Controls TXD. When active, no transmitting is possible.
|
||||
"""
|
||||
if self._break_state:
|
||||
fcntl.ioctl(self.fd, TIOCSBRK)
|
||||
else:
|
||||
fcntl.ioctl(self.fd, TIOCCBRK)
|
||||
|
||||
|
||||
# some systems support an extra flag to enable the two in POSIX unsupported
|
||||
# paritiy settings for MARK and SPACE
|
||||
CMSPAR = 0 # default, for unsupported platforms, override below
|
||||
|
||||
# try to detect the OS so that a device can be selected...
|
||||
# this code block should supply a device() and set_special_baudrate() function
|
||||
# for the platform
|
||||
plat = sys.platform.lower()
|
||||
|
||||
if plat[:5] == 'linux': # Linux (confirmed) # noqa
|
||||
import array
|
||||
|
||||
# extra termios flags
|
||||
CMSPAR = 0o10000000000 # Use "stick" (mark/space) parity
|
||||
|
||||
# baudrate ioctls
|
||||
TCGETS2 = 0x802C542A
|
||||
TCSETS2 = 0x402C542B
|
||||
BOTHER = 0o010000
|
||||
|
||||
# RS485 ioctls
|
||||
TIOCGRS485 = 0x542E
|
||||
TIOCSRS485 = 0x542F
|
||||
SER_RS485_ENABLED = 0b00000001
|
||||
SER_RS485_RTS_ON_SEND = 0b00000010
|
||||
SER_RS485_RTS_AFTER_SEND = 0b00000100
|
||||
SER_RS485_RX_DURING_TX = 0b00010000
|
||||
|
||||
class PlatformSpecific(PlatformSpecificBase):
|
||||
BAUDRATE_CONSTANTS = {
|
||||
0: 0o000000, # hang up
|
||||
50: 0o000001,
|
||||
75: 0o000002,
|
||||
110: 0o000003,
|
||||
134: 0o000004,
|
||||
150: 0o000005,
|
||||
200: 0o000006,
|
||||
300: 0o000007,
|
||||
600: 0o000010,
|
||||
1200: 0o000011,
|
||||
1800: 0o000012,
|
||||
2400: 0o000013,
|
||||
4800: 0o000014,
|
||||
9600: 0o000015,
|
||||
19200: 0o000016,
|
||||
38400: 0o000017,
|
||||
57600: 0o010001,
|
||||
115200: 0o010002,
|
||||
230400: 0o010003,
|
||||
460800: 0o010004,
|
||||
500000: 0o010005,
|
||||
576000: 0o010006,
|
||||
921600: 0o010007,
|
||||
1000000: 0o010010,
|
||||
1152000: 0o010011,
|
||||
1500000: 0o010012,
|
||||
2000000: 0o010013,
|
||||
2500000: 0o010014,
|
||||
3000000: 0o010015,
|
||||
3500000: 0o010016,
|
||||
4000000: 0o010017
|
||||
}
|
||||
|
||||
def set_low_latency_mode(self, low_latency_settings):
|
||||
buf = array.array('i', [0] * 32)
|
||||
|
||||
try:
|
||||
# get serial_struct
|
||||
fcntl.ioctl(self.fd, termios.TIOCGSERIAL, buf)
|
||||
|
||||
# set or unset ASYNC_LOW_LATENCY flag
|
||||
if low_latency_settings:
|
||||
buf[4] |= 0x2000
|
||||
else:
|
||||
buf[4] &= ~0x2000
|
||||
|
||||
# set serial_struct
|
||||
fcntl.ioctl(self.fd, termios.TIOCSSERIAL, buf)
|
||||
except IOError as e:
|
||||
raise ValueError('Failed to update ASYNC_LOW_LATENCY flag to {}: {}'.format(low_latency_settings, e))
|
||||
|
||||
def _set_special_baudrate(self, baudrate):
|
||||
# right size is 44 on x86_64, allow for some growth
|
||||
buf = array.array('i', [0] * 64)
|
||||
try:
|
||||
# get serial_struct
|
||||
fcntl.ioctl(self.fd, TCGETS2, buf)
|
||||
# set custom speed
|
||||
buf[2] &= ~termios.CBAUD
|
||||
buf[2] |= BOTHER
|
||||
buf[9] = buf[10] = baudrate
|
||||
|
||||
# set serial_struct
|
||||
fcntl.ioctl(self.fd, TCSETS2, buf)
|
||||
except IOError as e:
|
||||
raise ValueError('Failed to set custom baud rate ({}): {}'.format(baudrate, e))
|
||||
|
||||
def _set_rs485_mode(self, rs485_settings):
|
||||
buf = array.array('i', [0] * 8) # flags, delaytx, delayrx, padding
|
||||
try:
|
||||
fcntl.ioctl(self.fd, TIOCGRS485, buf)
|
||||
buf[0] |= SER_RS485_ENABLED
|
||||
if rs485_settings is not None:
|
||||
if rs485_settings.loopback:
|
||||
buf[0] |= SER_RS485_RX_DURING_TX
|
||||
else:
|
||||
buf[0] &= ~SER_RS485_RX_DURING_TX
|
||||
if rs485_settings.rts_level_for_tx:
|
||||
buf[0] |= SER_RS485_RTS_ON_SEND
|
||||
else:
|
||||
buf[0] &= ~SER_RS485_RTS_ON_SEND
|
||||
if rs485_settings.rts_level_for_rx:
|
||||
buf[0] |= SER_RS485_RTS_AFTER_SEND
|
||||
else:
|
||||
buf[0] &= ~SER_RS485_RTS_AFTER_SEND
|
||||
if rs485_settings.delay_before_tx is not None:
|
||||
buf[1] = int(rs485_settings.delay_before_tx * 1000)
|
||||
if rs485_settings.delay_before_rx is not None:
|
||||
buf[2] = int(rs485_settings.delay_before_rx * 1000)
|
||||
else:
|
||||
buf[0] = 0 # clear SER_RS485_ENABLED
|
||||
fcntl.ioctl(self.fd, TIOCSRS485, buf)
|
||||
except IOError as e:
|
||||
raise ValueError('Failed to set RS485 mode: {}'.format(e))
|
||||
|
||||
|
||||
elif plat == 'cygwin': # cygwin/win32 (confirmed)
|
||||
|
||||
class PlatformSpecific(PlatformSpecificBase):
|
||||
BAUDRATE_CONSTANTS = {
|
||||
128000: 0x01003,
|
||||
256000: 0x01005,
|
||||
500000: 0x01007,
|
||||
576000: 0x01008,
|
||||
921600: 0x01009,
|
||||
1000000: 0x0100a,
|
||||
1152000: 0x0100b,
|
||||
1500000: 0x0100c,
|
||||
2000000: 0x0100d,
|
||||
2500000: 0x0100e,
|
||||
3000000: 0x0100f
|
||||
}
|
||||
|
||||
|
||||
elif plat[:6] == 'darwin': # OS X
|
||||
import array
|
||||
IOSSIOSPEED = 0x80045402 # _IOW('T', 2, speed_t)
|
||||
|
||||
class PlatformSpecific(PlatformSpecificBase):
|
||||
osx_version = os.uname()[2].split('.')
|
||||
TIOCSBRK = 0x2000747B # _IO('t', 123)
|
||||
TIOCCBRK = 0x2000747A # _IO('t', 122)
|
||||
|
||||
# Tiger or above can support arbitrary serial speeds
|
||||
if int(osx_version[0]) >= 8:
|
||||
def _set_special_baudrate(self, baudrate):
|
||||
# use IOKit-specific call to set up high speeds
|
||||
buf = array.array('i', [baudrate])
|
||||
fcntl.ioctl(self.fd, IOSSIOSPEED, buf, 1)
|
||||
|
||||
def _update_break_state(self):
|
||||
"""\
|
||||
Set break: Controls TXD. When active, no transmitting is possible.
|
||||
"""
|
||||
if self._break_state:
|
||||
fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
|
||||
else:
|
||||
fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)
|
||||
|
||||
elif plat[:3] == 'bsd' or \
|
||||
plat[:7] == 'freebsd' or \
|
||||
plat[:6] == 'netbsd' or \
|
||||
plat[:7] == 'openbsd':
|
||||
|
||||
class ReturnBaudrate(object):
|
||||
def __getitem__(self, key):
|
||||
return key
|
||||
|
||||
class PlatformSpecific(PlatformSpecificBase):
|
||||
# Only tested on FreeBSD:
|
||||
# The baud rate may be passed in as
|
||||
# a literal value.
|
||||
BAUDRATE_CONSTANTS = ReturnBaudrate()
|
||||
|
||||
TIOCSBRK = 0x2000747B # _IO('t', 123)
|
||||
TIOCCBRK = 0x2000747A # _IO('t', 122)
|
||||
|
||||
|
||||
def _update_break_state(self):
|
||||
"""\
|
||||
Set break: Controls TXD. When active, no transmitting is possible.
|
||||
"""
|
||||
if self._break_state:
|
||||
fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
|
||||
else:
|
||||
fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)
|
||||
|
||||
else:
|
||||
class PlatformSpecific(PlatformSpecificBase):
|
||||
pass
|
||||
|
||||
|
||||
# load some constants for later use.
|
||||
# try to use values from termios, use defaults from linux otherwise
|
||||
TIOCMGET = getattr(termios, 'TIOCMGET', 0x5415)
|
||||
TIOCMBIS = getattr(termios, 'TIOCMBIS', 0x5416)
|
||||
TIOCMBIC = getattr(termios, 'TIOCMBIC', 0x5417)
|
||||
TIOCMSET = getattr(termios, 'TIOCMSET', 0x5418)
|
||||
|
||||
# TIOCM_LE = getattr(termios, 'TIOCM_LE', 0x001)
|
||||
TIOCM_DTR = getattr(termios, 'TIOCM_DTR', 0x002)
|
||||
TIOCM_RTS = getattr(termios, 'TIOCM_RTS', 0x004)
|
||||
# TIOCM_ST = getattr(termios, 'TIOCM_ST', 0x008)
|
||||
# TIOCM_SR = getattr(termios, 'TIOCM_SR', 0x010)
|
||||
|
||||
TIOCM_CTS = getattr(termios, 'TIOCM_CTS', 0x020)
|
||||
TIOCM_CAR = getattr(termios, 'TIOCM_CAR', 0x040)
|
||||
TIOCM_RNG = getattr(termios, 'TIOCM_RNG', 0x080)
|
||||
TIOCM_DSR = getattr(termios, 'TIOCM_DSR', 0x100)
|
||||
TIOCM_CD = getattr(termios, 'TIOCM_CD', TIOCM_CAR)
|
||||
TIOCM_RI = getattr(termios, 'TIOCM_RI', TIOCM_RNG)
|
||||
# TIOCM_OUT1 = getattr(termios, 'TIOCM_OUT1', 0x2000)
|
||||
# TIOCM_OUT2 = getattr(termios, 'TIOCM_OUT2', 0x4000)
|
||||
if hasattr(termios, 'TIOCINQ'):
|
||||
TIOCINQ = termios.TIOCINQ
|
||||
else:
|
||||
TIOCINQ = getattr(termios, 'FIONREAD', 0x541B)
|
||||
TIOCOUTQ = getattr(termios, 'TIOCOUTQ', 0x5411)
|
||||
|
||||
TIOCM_zero_str = struct.pack('I', 0)
|
||||
TIOCM_RTS_str = struct.pack('I', TIOCM_RTS)
|
||||
TIOCM_DTR_str = struct.pack('I', TIOCM_DTR)
|
||||
|
||||
TIOCSBRK = getattr(termios, 'TIOCSBRK', 0x5427)
|
||||
TIOCCBRK = getattr(termios, 'TIOCCBRK', 0x5428)
|
||||
|
||||
|
||||
class Serial(SerialBase, PlatformSpecific):
|
||||
"""\
|
||||
Serial port class POSIX implementation. Serial port configuration is
|
||||
done with termios and fcntl. Runs on Linux and many other Un*x like
|
||||
systems.
|
||||
"""
|
||||
|
||||
def open(self):
|
||||
"""\
|
||||
Open port with current settings. This may throw a SerialException
|
||||
if the port cannot be opened."""
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
self.fd = None
|
||||
# open
|
||||
try:
|
||||
self.fd = os.open(self.portstr, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
|
||||
except OSError as msg:
|
||||
self.fd = None
|
||||
raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg))
|
||||
#~ fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # set blocking
|
||||
|
||||
self.pipe_abort_read_r, self.pipe_abort_read_w = None, None
|
||||
self.pipe_abort_write_r, self.pipe_abort_write_w = None, None
|
||||
|
||||
try:
|
||||
self._reconfigure_port(force_update=True)
|
||||
|
||||
try:
|
||||
if not self._dsrdtr:
|
||||
self._update_dtr_state()
|
||||
if not self._rtscts:
|
||||
self._update_rts_state()
|
||||
except IOError as e:
|
||||
# ignore Invalid argument and Inappropriate ioctl
|
||||
if e.errno not in (errno.EINVAL, errno.ENOTTY):
|
||||
raise
|
||||
|
||||
self._reset_input_buffer()
|
||||
|
||||
self.pipe_abort_read_r, self.pipe_abort_read_w = os.pipe()
|
||||
self.pipe_abort_write_r, self.pipe_abort_write_w = os.pipe()
|
||||
fcntl.fcntl(self.pipe_abort_read_r, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
fcntl.fcntl(self.pipe_abort_write_r, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
except BaseException:
|
||||
try:
|
||||
os.close(self.fd)
|
||||
except Exception:
|
||||
# ignore any exception when closing the port
|
||||
# also to keep original exception that happened when setting up
|
||||
pass
|
||||
self.fd = None
|
||||
|
||||
if self.pipe_abort_read_w is not None:
|
||||
os.close(self.pipe_abort_read_w)
|
||||
self.pipe_abort_read_w = None
|
||||
if self.pipe_abort_read_r is not None:
|
||||
os.close(self.pipe_abort_read_r)
|
||||
self.pipe_abort_read_r = None
|
||||
if self.pipe_abort_write_w is not None:
|
||||
os.close(self.pipe_abort_write_w)
|
||||
self.pipe_abort_write_w = None
|
||||
if self.pipe_abort_write_r is not None:
|
||||
os.close(self.pipe_abort_write_r)
|
||||
self.pipe_abort_write_r = None
|
||||
|
||||
raise
|
||||
|
||||
self.is_open = True
|
||||
|
||||
def _reconfigure_port(self, force_update=False):
|
||||
"""Set communication parameters on opened port."""
|
||||
if self.fd is None:
|
||||
raise SerialException("Can only operate on a valid file descriptor")
|
||||
|
||||
# if exclusive lock is requested, create it before we modify anything else
|
||||
if self._exclusive is not None:
|
||||
if self._exclusive:
|
||||
try:
|
||||
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except IOError as msg:
|
||||
raise SerialException(msg.errno, "Could not exclusively lock port {}: {}".format(self._port, msg))
|
||||
else:
|
||||
fcntl.flock(self.fd, fcntl.LOCK_UN)
|
||||
|
||||
custom_baud = None
|
||||
|
||||
vmin = vtime = 0 # timeout is done via select
|
||||
if self._inter_byte_timeout is not None:
|
||||
vmin = 1
|
||||
vtime = int(self._inter_byte_timeout * 10)
|
||||
try:
|
||||
orig_attr = termios.tcgetattr(self.fd)
|
||||
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr
|
||||
except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here
|
||||
raise SerialException("Could not configure port: {}".format(msg))
|
||||
# set up raw mode / no echo / binary
|
||||
cflag |= (termios.CLOCAL | termios.CREAD)
|
||||
lflag &= ~(termios.ICANON | termios.ECHO | termios.ECHOE |
|
||||
termios.ECHOK | termios.ECHONL |
|
||||
termios.ISIG | termios.IEXTEN) # |termios.ECHOPRT
|
||||
for flag in ('ECHOCTL', 'ECHOKE'): # netbsd workaround for Erk
|
||||
if hasattr(termios, flag):
|
||||
lflag &= ~getattr(termios, flag)
|
||||
|
||||
oflag &= ~(termios.OPOST | termios.ONLCR | termios.OCRNL)
|
||||
iflag &= ~(termios.INLCR | termios.IGNCR | termios.ICRNL | termios.IGNBRK)
|
||||
if hasattr(termios, 'IUCLC'):
|
||||
iflag &= ~termios.IUCLC
|
||||
if hasattr(termios, 'PARMRK'):
|
||||
iflag &= ~termios.PARMRK
|
||||
|
||||
# setup baud rate
|
||||
try:
|
||||
ispeed = ospeed = getattr(termios, 'B{}'.format(self._baudrate))
|
||||
except AttributeError:
|
||||
try:
|
||||
ispeed = ospeed = self.BAUDRATE_CONSTANTS[self._baudrate]
|
||||
except KeyError:
|
||||
#~ raise ValueError('Invalid baud rate: %r' % self._baudrate)
|
||||
|
||||
# See if BOTHER is defined for this platform; if it is, use
|
||||
# this for a speed not defined in the baudrate constants list.
|
||||
try:
|
||||
ispeed = ospeed = BOTHER
|
||||
except NameError:
|
||||
# may need custom baud rate, it isn't in our list.
|
||||
ispeed = ospeed = getattr(termios, 'B38400')
|
||||
|
||||
try:
|
||||
custom_baud = int(self._baudrate) # store for later
|
||||
except ValueError:
|
||||
raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate))
|
||||
else:
|
||||
if custom_baud < 0:
|
||||
raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate))
|
||||
|
||||
# setup char len
|
||||
cflag &= ~termios.CSIZE
|
||||
if self._bytesize == 8:
|
||||
cflag |= termios.CS8
|
||||
elif self._bytesize == 7:
|
||||
cflag |= termios.CS7
|
||||
elif self._bytesize == 6:
|
||||
cflag |= termios.CS6
|
||||
elif self._bytesize == 5:
|
||||
cflag |= termios.CS5
|
||||
else:
|
||||
raise ValueError('Invalid char len: {!r}'.format(self._bytesize))
|
||||
# setup stop bits
|
||||
if self._stopbits == serial.STOPBITS_ONE:
|
||||
cflag &= ~(termios.CSTOPB)
|
||||
elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
|
||||
cflag |= (termios.CSTOPB) # XXX same as TWO.. there is no POSIX support for 1.5
|
||||
elif self._stopbits == serial.STOPBITS_TWO:
|
||||
cflag |= (termios.CSTOPB)
|
||||
else:
|
||||
raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits))
|
||||
# setup parity
|
||||
iflag &= ~(termios.INPCK | termios.ISTRIP)
|
||||
if self._parity == serial.PARITY_NONE:
|
||||
cflag &= ~(termios.PARENB | termios.PARODD | CMSPAR)
|
||||
elif self._parity == serial.PARITY_EVEN:
|
||||
cflag &= ~(termios.PARODD | CMSPAR)
|
||||
cflag |= (termios.PARENB)
|
||||
elif self._parity == serial.PARITY_ODD:
|
||||
cflag &= ~CMSPAR
|
||||
cflag |= (termios.PARENB | termios.PARODD)
|
||||
elif self._parity == serial.PARITY_MARK and CMSPAR:
|
||||
cflag |= (termios.PARENB | CMSPAR | termios.PARODD)
|
||||
elif self._parity == serial.PARITY_SPACE and CMSPAR:
|
||||
cflag |= (termios.PARENB | CMSPAR)
|
||||
cflag &= ~(termios.PARODD)
|
||||
else:
|
||||
raise ValueError('Invalid parity: {!r}'.format(self._parity))
|
||||
# setup flow control
|
||||
# xonxoff
|
||||
if hasattr(termios, 'IXANY'):
|
||||
if self._xonxoff:
|
||||
iflag |= (termios.IXON | termios.IXOFF) # |termios.IXANY)
|
||||
else:
|
||||
iflag &= ~(termios.IXON | termios.IXOFF | termios.IXANY)
|
||||
else:
|
||||
if self._xonxoff:
|
||||
iflag |= (termios.IXON | termios.IXOFF)
|
||||
else:
|
||||
iflag &= ~(termios.IXON | termios.IXOFF)
|
||||
# rtscts
|
||||
if hasattr(termios, 'CRTSCTS'):
|
||||
if self._rtscts:
|
||||
cflag |= (termios.CRTSCTS)
|
||||
else:
|
||||
cflag &= ~(termios.CRTSCTS)
|
||||
elif hasattr(termios, 'CNEW_RTSCTS'): # try it with alternate constant name
|
||||
if self._rtscts:
|
||||
cflag |= (termios.CNEW_RTSCTS)
|
||||
else:
|
||||
cflag &= ~(termios.CNEW_RTSCTS)
|
||||
# XXX should there be a warning if setting up rtscts (and xonxoff etc) fails??
|
||||
|
||||
# buffer
|
||||
# vmin "minimal number of characters to be read. 0 for non blocking"
|
||||
if vmin < 0 or vmin > 255:
|
||||
raise ValueError('Invalid vmin: {!r}'.format(vmin))
|
||||
cc[termios.VMIN] = vmin
|
||||
# vtime
|
||||
if vtime < 0 or vtime > 255:
|
||||
raise ValueError('Invalid vtime: {!r}'.format(vtime))
|
||||
cc[termios.VTIME] = vtime
|
||||
# activate settings
|
||||
if force_update or [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] != orig_attr:
|
||||
termios.tcsetattr(
|
||||
self.fd,
|
||||
termios.TCSANOW,
|
||||
[iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
|
||||
|
||||
# apply custom baud rate, if any
|
||||
if custom_baud is not None:
|
||||
self._set_special_baudrate(custom_baud)
|
||||
|
||||
if self._rs485_mode is not None:
|
||||
self._set_rs485_mode(self._rs485_mode)
|
||||
|
||||
def close(self):
|
||||
"""Close port"""
|
||||
if self.is_open:
|
||||
if self.fd is not None:
|
||||
os.close(self.fd)
|
||||
self.fd = None
|
||||
os.close(self.pipe_abort_read_w)
|
||||
os.close(self.pipe_abort_read_r)
|
||||
os.close(self.pipe_abort_write_w)
|
||||
os.close(self.pipe_abort_write_r)
|
||||
self.pipe_abort_read_r, self.pipe_abort_read_w = None, None
|
||||
self.pipe_abort_write_r, self.pipe_abort_write_w = None, None
|
||||
self.is_open = False
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of bytes currently in the input buffer."""
|
||||
#~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str)
|
||||
s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0]
|
||||
|
||||
# select based implementation, proved to work on many systems
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
read = bytearray()
|
||||
timeout = Timeout(self._timeout)
|
||||
while len(read) < size:
|
||||
try:
|
||||
ready, _, _ = select.select([self.fd, self.pipe_abort_read_r], [], [], timeout.time_left())
|
||||
if self.pipe_abort_read_r in ready:
|
||||
os.read(self.pipe_abort_read_r, 1000)
|
||||
break
|
||||
# If select was used with a timeout, and the timeout occurs, it
|
||||
# returns with empty lists -> thus abort read operation.
|
||||
# For timeout == 0 (non-blocking operation) also abort when
|
||||
# there is nothing to read.
|
||||
if not ready:
|
||||
break # timeout
|
||||
buf = os.read(self.fd, size - len(read))
|
||||
except OSError as e:
|
||||
# this is for Python 3.x where select.error is a subclass of
|
||||
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||
# https://www.python.org/dev/peps/pep-0475.
|
||||
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('read failed: {}'.format(e))
|
||||
except select.error as e:
|
||||
# this is for Python 2.x
|
||||
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('read failed: {}'.format(e))
|
||||
else:
|
||||
# read should always return some data as select reported it was
|
||||
# ready to read when we get to this point.
|
||||
if not buf:
|
||||
# Disconnected devices, at least on Linux, show the
|
||||
# behavior that they are always ready to read immediately
|
||||
# but reading returns nothing.
|
||||
raise SerialException(
|
||||
'device reports readiness to read but returned no data '
|
||||
'(device disconnected or multiple access on port?)')
|
||||
read.extend(buf)
|
||||
|
||||
if timeout.expired():
|
||||
break
|
||||
return bytes(read)
|
||||
|
||||
def cancel_read(self):
|
||||
if self.is_open:
|
||||
os.write(self.pipe_abort_read_w, b"x")
|
||||
|
||||
def cancel_write(self):
|
||||
if self.is_open:
|
||||
os.write(self.pipe_abort_write_w, b"x")
|
||||
|
||||
def write(self, data):
|
||||
"""Output the given byte string over the serial port."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
d = to_bytes(data)
|
||||
tx_len = length = len(d)
|
||||
timeout = Timeout(self._write_timeout)
|
||||
while tx_len > 0:
|
||||
try:
|
||||
n = os.write(self.fd, d)
|
||||
if timeout.is_non_blocking:
|
||||
# Zero timeout indicates non-blocking - simply return the
|
||||
# number of bytes of data actually written
|
||||
return n
|
||||
elif not timeout.is_infinite:
|
||||
# when timeout is set, use select to wait for being ready
|
||||
# with the time left as timeout
|
||||
if timeout.expired():
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], timeout.time_left())
|
||||
if abort:
|
||||
os.read(self.pipe_abort_write_r, 1000)
|
||||
break
|
||||
if not ready:
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
else:
|
||||
assert timeout.time_left() is None
|
||||
# wait for write operation
|
||||
abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], None)
|
||||
if abort:
|
||||
os.read(self.pipe_abort_write_r, 1)
|
||||
break
|
||||
if not ready:
|
||||
raise SerialException('write failed (select)')
|
||||
d = d[n:]
|
||||
tx_len -= n
|
||||
except SerialException:
|
||||
raise
|
||||
except OSError as e:
|
||||
# this is for Python 3.x where select.error is a subclass of
|
||||
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||
# https://www.python.org/dev/peps/pep-0475.
|
||||
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('write failed: {}'.format(e))
|
||||
except select.error as e:
|
||||
# this is for Python 2.x
|
||||
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('write failed: {}'.format(e))
|
||||
if not timeout.is_non_blocking and timeout.expired():
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
return length - len(d)
|
||||
|
||||
def flush(self):
|
||||
"""\
|
||||
Flush of file like objects. In this case, wait until all data
|
||||
is written.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
termios.tcdrain(self.fd)
|
||||
|
||||
def _reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
termios.tcflush(self.fd, termios.TCIFLUSH)
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._reset_input_buffer()
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""\
|
||||
Clear output buffer, aborting the current output and discarding all
|
||||
that is in the buffer.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
termios.tcflush(self.fd, termios.TCOFLUSH)
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
"""\
|
||||
Send break condition. Timed, returns to idle state after given
|
||||
duration.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
termios.tcsendbreak(self.fd, int(duration / 0.25))
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
if self._rts_state:
|
||||
fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_RTS_str)
|
||||
else:
|
||||
fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_RTS_str)
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
if self._dtr_state:
|
||||
fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_DTR_str)
|
||||
else:
|
||||
fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_DTR_str)
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0] & TIOCM_CTS != 0
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0] & TIOCM_DSR != 0
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0] & TIOCM_RI != 0
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0] & TIOCM_CD != 0
|
||||
|
||||
# - - platform specific - - - -
|
||||
|
||||
@property
|
||||
def out_waiting(self):
|
||||
"""Return the number of bytes currently in the output buffer."""
|
||||
#~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str)
|
||||
s = fcntl.ioctl(self.fd, TIOCOUTQ, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0]
|
||||
|
||||
def fileno(self):
|
||||
"""\
|
||||
For easier use of the serial port instance with select.
|
||||
WARNING: this function is not portable to different platforms!
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
return self.fd
|
||||
|
||||
def set_input_flow_control(self, enable=True):
|
||||
"""\
|
||||
Manually control flow - when software flow control is enabled.
|
||||
This will send XON (true) or XOFF (false) to the other device.
|
||||
WARNING: this function is not portable to different platforms!
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if enable:
|
||||
termios.tcflow(self.fd, termios.TCION)
|
||||
else:
|
||||
termios.tcflow(self.fd, termios.TCIOFF)
|
||||
|
||||
def set_output_flow_control(self, enable=True):
|
||||
"""\
|
||||
Manually control flow of outgoing data - when hardware or software flow
|
||||
control is enabled.
|
||||
WARNING: this function is not portable to different platforms!
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if enable:
|
||||
termios.tcflow(self.fd, termios.TCOON)
|
||||
else:
|
||||
termios.tcflow(self.fd, termios.TCOOFF)
|
||||
|
||||
def nonblocking(self):
|
||||
"""DEPRECATED - has no use"""
|
||||
import warnings
|
||||
warnings.warn("nonblocking() has no effect, already nonblocking", DeprecationWarning)
|
||||
|
||||
|
||||
class PosixPollSerial(Serial):
|
||||
"""\
|
||||
Poll based read implementation. Not all systems support poll properly.
|
||||
However this one has better handling of errors, such as a device
|
||||
disconnecting while it's in use (e.g. USB-serial unplugged).
|
||||
"""
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
read = bytearray()
|
||||
timeout = Timeout(self._timeout)
|
||||
poll = select.poll()
|
||||
poll.register(self.fd, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL)
|
||||
poll.register(self.pipe_abort_read_r, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL)
|
||||
if size > 0:
|
||||
while len(read) < size:
|
||||
# print "\tread(): size",size, "have", len(read) #debug
|
||||
# wait until device becomes ready to read (or something fails)
|
||||
for fd, event in poll.poll(None if timeout.is_infinite else (timeout.time_left() * 1000)):
|
||||
if fd == self.pipe_abort_read_r:
|
||||
break
|
||||
if event & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
|
||||
raise SerialException('device reports error (poll)')
|
||||
# we don't care if it is select.POLLIN or timeout, that's
|
||||
# handled below
|
||||
if fd == self.pipe_abort_read_r:
|
||||
os.read(self.pipe_abort_read_r, 1000)
|
||||
break
|
||||
buf = os.read(self.fd, size - len(read))
|
||||
read.extend(buf)
|
||||
if timeout.expired() \
|
||||
or (self._inter_byte_timeout is not None and self._inter_byte_timeout > 0) and not buf:
|
||||
break # early abort on timeout
|
||||
return bytes(read)
|
||||
|
||||
|
||||
class VTIMESerial(Serial):
|
||||
"""\
|
||||
Implement timeout using vtime of tty device instead of using select.
|
||||
This means that no inter character timeout can be specified and that
|
||||
the error handling is degraded.
|
||||
|
||||
Overall timeout is disabled when inter-character timeout is used.
|
||||
|
||||
Note that this implementation does NOT support cancel_read(), it will
|
||||
just ignore that.
|
||||
"""
|
||||
|
||||
def _reconfigure_port(self, force_update=True):
|
||||
"""Set communication parameters on opened port."""
|
||||
super(VTIMESerial, self)._reconfigure_port()
|
||||
fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # clear O_NONBLOCK
|
||||
|
||||
if self._inter_byte_timeout is not None:
|
||||
vmin = 1
|
||||
vtime = int(self._inter_byte_timeout * 10)
|
||||
elif self._timeout is None:
|
||||
vmin = 1
|
||||
vtime = 0
|
||||
else:
|
||||
vmin = 0
|
||||
vtime = int(self._timeout * 10)
|
||||
try:
|
||||
orig_attr = termios.tcgetattr(self.fd)
|
||||
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr
|
||||
except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here
|
||||
raise serial.SerialException("Could not configure port: {}".format(msg))
|
||||
|
||||
if vtime < 0 or vtime > 255:
|
||||
raise ValueError('Invalid vtime: {!r}'.format(vtime))
|
||||
cc[termios.VTIME] = vtime
|
||||
cc[termios.VMIN] = vmin
|
||||
|
||||
termios.tcsetattr(
|
||||
self.fd,
|
||||
termios.TCSANOW,
|
||||
[iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
read = bytearray()
|
||||
while len(read) < size:
|
||||
buf = os.read(self.fd, size - len(read))
|
||||
if not buf:
|
||||
break
|
||||
read.extend(buf)
|
||||
return bytes(read)
|
||||
|
||||
# hack to make hasattr return false
|
||||
cancel_read = property()
|
||||
@@ -1,697 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# Base class and support functions used by various backends.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import io
|
||||
import time
|
||||
|
||||
# ``memoryview`` was introduced in Python 2.7 and ``bytes(some_memoryview)``
|
||||
# isn't returning the contents (very unfortunate). Therefore we need special
|
||||
# cases and test for it. Ensure that there is a ``memoryview`` object for older
|
||||
# Python versions. This is easier than making every test dependent on its
|
||||
# existence.
|
||||
try:
|
||||
memoryview
|
||||
except (NameError, AttributeError):
|
||||
# implementation does not matter as we do not really use it.
|
||||
# it just must not inherit from something else we might care for.
|
||||
class memoryview(object): # pylint: disable=redefined-builtin,invalid-name
|
||||
pass
|
||||
|
||||
try:
|
||||
unicode
|
||||
except (NameError, AttributeError):
|
||||
unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name
|
||||
|
||||
try:
|
||||
basestring
|
||||
except (NameError, AttributeError):
|
||||
basestring = (str,) # for Python 3, pylint: disable=redefined-builtin,invalid-name
|
||||
|
||||
|
||||
# "for byte in data" fails for python3 as it returns ints instead of bytes
|
||||
def iterbytes(b):
|
||||
"""Iterate over bytes, returning bytes instead of ints (python3)"""
|
||||
if isinstance(b, memoryview):
|
||||
b = b.tobytes()
|
||||
i = 0
|
||||
while True:
|
||||
a = b[i:i + 1]
|
||||
i += 1
|
||||
if a:
|
||||
yield a
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
# all Python versions prior 3.x convert ``str([17])`` to '[17]' instead of '\x11'
|
||||
# so a simple ``bytes(sequence)`` doesn't work for all versions
|
||||
def to_bytes(seq):
|
||||
"""convert a sequence to a bytes type"""
|
||||
if isinstance(seq, bytes):
|
||||
return seq
|
||||
elif isinstance(seq, bytearray):
|
||||
return bytes(seq)
|
||||
elif isinstance(seq, memoryview):
|
||||
return seq.tobytes()
|
||||
elif isinstance(seq, unicode):
|
||||
raise TypeError('unicode strings are not supported, please encode to bytes: {!r}'.format(seq))
|
||||
else:
|
||||
# handle list of integers and bytes (one or more items) for Python 2 and 3
|
||||
return bytes(bytearray(seq))
|
||||
|
||||
|
||||
# create control bytes
|
||||
XON = to_bytes([17])
|
||||
XOFF = to_bytes([19])
|
||||
|
||||
CR = to_bytes([13])
|
||||
LF = to_bytes([10])
|
||||
|
||||
|
||||
PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = 'N', 'E', 'O', 'M', 'S'
|
||||
STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
|
||||
FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS = (5, 6, 7, 8)
|
||||
|
||||
PARITY_NAMES = {
|
||||
PARITY_NONE: 'None',
|
||||
PARITY_EVEN: 'Even',
|
||||
PARITY_ODD: 'Odd',
|
||||
PARITY_MARK: 'Mark',
|
||||
PARITY_SPACE: 'Space',
|
||||
}
|
||||
|
||||
|
||||
class SerialException(IOError):
|
||||
"""Base class for serial port related exceptions."""
|
||||
|
||||
|
||||
class SerialTimeoutException(SerialException):
|
||||
"""Write timeouts give an exception"""
|
||||
|
||||
|
||||
class PortNotOpenError(SerialException):
|
||||
"""Port is not open"""
|
||||
def __init__(self):
|
||||
super(PortNotOpenError, self).__init__('Attempting to use a port that is not open')
|
||||
|
||||
|
||||
class Timeout(object):
|
||||
"""\
|
||||
Abstraction for timeout operations. Using time.monotonic() if available
|
||||
or time.time() in all other cases.
|
||||
|
||||
The class can also be initialized with 0 or None, in order to support
|
||||
non-blocking and fully blocking I/O operations. The attributes
|
||||
is_non_blocking and is_infinite are set accordingly.
|
||||
"""
|
||||
if hasattr(time, 'monotonic'):
|
||||
# Timeout implementation with time.monotonic(). This function is only
|
||||
# supported by Python 3.3 and above. It returns a time in seconds
|
||||
# (float) just as time.time(), but is not affected by system clock
|
||||
# adjustments.
|
||||
TIME = time.monotonic
|
||||
else:
|
||||
# Timeout implementation with time.time(). This is compatible with all
|
||||
# Python versions but has issues if the clock is adjusted while the
|
||||
# timeout is running.
|
||||
TIME = time.time
|
||||
|
||||
def __init__(self, duration):
|
||||
"""Initialize a timeout with given duration"""
|
||||
self.is_infinite = (duration is None)
|
||||
self.is_non_blocking = (duration == 0)
|
||||
self.duration = duration
|
||||
if duration is not None:
|
||||
self.target_time = self.TIME() + duration
|
||||
else:
|
||||
self.target_time = None
|
||||
|
||||
def expired(self):
|
||||
"""Return a boolean, telling if the timeout has expired"""
|
||||
return self.target_time is not None and self.time_left() <= 0
|
||||
|
||||
def time_left(self):
|
||||
"""Return how many seconds are left until the timeout expires"""
|
||||
if self.is_non_blocking:
|
||||
return 0
|
||||
elif self.is_infinite:
|
||||
return None
|
||||
else:
|
||||
delta = self.target_time - self.TIME()
|
||||
if delta > self.duration:
|
||||
# clock jumped, recalculate
|
||||
self.target_time = self.TIME() + self.duration
|
||||
return self.duration
|
||||
else:
|
||||
return max(0, delta)
|
||||
|
||||
def restart(self, duration):
|
||||
"""\
|
||||
Restart a timeout, only supported if a timeout was already set up
|
||||
before.
|
||||
"""
|
||||
self.duration = duration
|
||||
self.target_time = self.TIME() + duration
|
||||
|
||||
|
||||
class SerialBase(io.RawIOBase):
|
||||
"""\
|
||||
Serial port base class. Provides __init__ function and properties to
|
||||
get/set port settings.
|
||||
"""
|
||||
|
||||
# default values, may be overridden in subclasses that do not support all values
|
||||
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||
9600, 19200, 38400, 57600, 115200, 230400, 460800, 500000,
|
||||
576000, 921600, 1000000, 1152000, 1500000, 2000000, 2500000,
|
||||
3000000, 3500000, 4000000)
|
||||
BYTESIZES = (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS)
|
||||
PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE)
|
||||
STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
|
||||
|
||||
def __init__(self,
|
||||
port=None,
|
||||
baudrate=9600,
|
||||
bytesize=EIGHTBITS,
|
||||
parity=PARITY_NONE,
|
||||
stopbits=STOPBITS_ONE,
|
||||
timeout=None,
|
||||
xonxoff=False,
|
||||
rtscts=False,
|
||||
write_timeout=None,
|
||||
dsrdtr=False,
|
||||
inter_byte_timeout=None,
|
||||
exclusive=None,
|
||||
**kwargs):
|
||||
"""\
|
||||
Initialize comm port object. If a "port" is given, then the port will be
|
||||
opened immediately. Otherwise a Serial port object in closed state
|
||||
is returned.
|
||||
"""
|
||||
|
||||
self.is_open = False
|
||||
self.portstr = None
|
||||
self.name = None
|
||||
# correct values are assigned below through properties
|
||||
self._port = None
|
||||
self._baudrate = None
|
||||
self._bytesize = None
|
||||
self._parity = None
|
||||
self._stopbits = None
|
||||
self._timeout = None
|
||||
self._write_timeout = None
|
||||
self._xonxoff = None
|
||||
self._rtscts = None
|
||||
self._dsrdtr = None
|
||||
self._inter_byte_timeout = None
|
||||
self._rs485_mode = None # disabled by default
|
||||
self._rts_state = True
|
||||
self._dtr_state = True
|
||||
self._break_state = False
|
||||
self._exclusive = None
|
||||
|
||||
# assign values using get/set methods using the properties feature
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.bytesize = bytesize
|
||||
self.parity = parity
|
||||
self.stopbits = stopbits
|
||||
self.timeout = timeout
|
||||
self.write_timeout = write_timeout
|
||||
self.xonxoff = xonxoff
|
||||
self.rtscts = rtscts
|
||||
self.dsrdtr = dsrdtr
|
||||
self.inter_byte_timeout = inter_byte_timeout
|
||||
self.exclusive = exclusive
|
||||
|
||||
# watch for backward compatible kwargs
|
||||
if 'writeTimeout' in kwargs:
|
||||
self.write_timeout = kwargs.pop('writeTimeout')
|
||||
if 'interCharTimeout' in kwargs:
|
||||
self.inter_byte_timeout = kwargs.pop('interCharTimeout')
|
||||
if kwargs:
|
||||
raise ValueError('unexpected keyword arguments: {!r}'.format(kwargs))
|
||||
|
||||
if port is not None:
|
||||
self.open()
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
# to be implemented by subclasses:
|
||||
# def open(self):
|
||||
# def close(self):
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
"""\
|
||||
Get the current port setting. The value that was passed on init or using
|
||||
setPort() is passed back.
|
||||
"""
|
||||
return self._port
|
||||
|
||||
@port.setter
|
||||
def port(self, port):
|
||||
"""\
|
||||
Change the port.
|
||||
"""
|
||||
if port is not None and not isinstance(port, basestring):
|
||||
raise ValueError('"port" must be None or a string, not {}'.format(type(port)))
|
||||
was_open = self.is_open
|
||||
if was_open:
|
||||
self.close()
|
||||
self.portstr = port
|
||||
self._port = port
|
||||
self.name = self.portstr
|
||||
if was_open:
|
||||
self.open()
|
||||
|
||||
@property
|
||||
def baudrate(self):
|
||||
"""Get the current baud rate setting."""
|
||||
return self._baudrate
|
||||
|
||||
@baudrate.setter
|
||||
def baudrate(self, baudrate):
|
||||
"""\
|
||||
Change baud rate. It raises a ValueError if the port is open and the
|
||||
baud rate is not possible. If the port is closed, then the value is
|
||||
accepted and the exception is raised when the port is opened.
|
||||
"""
|
||||
try:
|
||||
b = int(baudrate)
|
||||
except TypeError:
|
||||
raise ValueError("Not a valid baudrate: {!r}".format(baudrate))
|
||||
else:
|
||||
if b < 0:
|
||||
raise ValueError("Not a valid baudrate: {!r}".format(baudrate))
|
||||
self._baudrate = b
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def bytesize(self):
|
||||
"""Get the current byte size setting."""
|
||||
return self._bytesize
|
||||
|
||||
@bytesize.setter
|
||||
def bytesize(self, bytesize):
|
||||
"""Change byte size."""
|
||||
if bytesize not in self.BYTESIZES:
|
||||
raise ValueError("Not a valid byte size: {!r}".format(bytesize))
|
||||
self._bytesize = bytesize
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def exclusive(self):
|
||||
"""Get the current exclusive access setting."""
|
||||
return self._exclusive
|
||||
|
||||
@exclusive.setter
|
||||
def exclusive(self, exclusive):
|
||||
"""Change the exclusive access setting."""
|
||||
self._exclusive = exclusive
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def parity(self):
|
||||
"""Get the current parity setting."""
|
||||
return self._parity
|
||||
|
||||
@parity.setter
|
||||
def parity(self, parity):
|
||||
"""Change parity setting."""
|
||||
if parity not in self.PARITIES:
|
||||
raise ValueError("Not a valid parity: {!r}".format(parity))
|
||||
self._parity = parity
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def stopbits(self):
|
||||
"""Get the current stop bits setting."""
|
||||
return self._stopbits
|
||||
|
||||
@stopbits.setter
|
||||
def stopbits(self, stopbits):
|
||||
"""Change stop bits size."""
|
||||
if stopbits not in self.STOPBITS:
|
||||
raise ValueError("Not a valid stop bit size: {!r}".format(stopbits))
|
||||
self._stopbits = stopbits
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
"""Get the current timeout setting."""
|
||||
return self._timeout
|
||||
|
||||
@timeout.setter
|
||||
def timeout(self, timeout):
|
||||
"""Change timeout setting."""
|
||||
if timeout is not None:
|
||||
try:
|
||||
timeout + 1 # test if it's a number, will throw a TypeError if not...
|
||||
except TypeError:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||
if timeout < 0:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||
self._timeout = timeout
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def write_timeout(self):
|
||||
"""Get the current timeout setting."""
|
||||
return self._write_timeout
|
||||
|
||||
@write_timeout.setter
|
||||
def write_timeout(self, timeout):
|
||||
"""Change timeout setting."""
|
||||
if timeout is not None:
|
||||
if timeout < 0:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||
try:
|
||||
timeout + 1 # test if it's a number, will throw a TypeError if not...
|
||||
except TypeError:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||
|
||||
self._write_timeout = timeout
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def inter_byte_timeout(self):
|
||||
"""Get the current inter-character timeout setting."""
|
||||
return self._inter_byte_timeout
|
||||
|
||||
@inter_byte_timeout.setter
|
||||
def inter_byte_timeout(self, ic_timeout):
|
||||
"""Change inter-byte timeout setting."""
|
||||
if ic_timeout is not None:
|
||||
if ic_timeout < 0:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(ic_timeout))
|
||||
try:
|
||||
ic_timeout + 1 # test if it's a number, will throw a TypeError if not...
|
||||
except TypeError:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(ic_timeout))
|
||||
|
||||
self._inter_byte_timeout = ic_timeout
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def xonxoff(self):
|
||||
"""Get the current XON/XOFF setting."""
|
||||
return self._xonxoff
|
||||
|
||||
@xonxoff.setter
|
||||
def xonxoff(self, xonxoff):
|
||||
"""Change XON/XOFF setting."""
|
||||
self._xonxoff = xonxoff
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def rtscts(self):
|
||||
"""Get the current RTS/CTS flow control setting."""
|
||||
return self._rtscts
|
||||
|
||||
@rtscts.setter
|
||||
def rtscts(self, rtscts):
|
||||
"""Change RTS/CTS flow control setting."""
|
||||
self._rtscts = rtscts
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def dsrdtr(self):
|
||||
"""Get the current DSR/DTR flow control setting."""
|
||||
return self._dsrdtr
|
||||
|
||||
@dsrdtr.setter
|
||||
def dsrdtr(self, dsrdtr=None):
|
||||
"""Change DsrDtr flow control setting."""
|
||||
if dsrdtr is None:
|
||||
# if not set, keep backwards compatibility and follow rtscts setting
|
||||
self._dsrdtr = self._rtscts
|
||||
else:
|
||||
# if defined independently, follow its value
|
||||
self._dsrdtr = dsrdtr
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def rts(self):
|
||||
return self._rts_state
|
||||
|
||||
@rts.setter
|
||||
def rts(self, value):
|
||||
self._rts_state = value
|
||||
if self.is_open:
|
||||
self._update_rts_state()
|
||||
|
||||
@property
|
||||
def dtr(self):
|
||||
return self._dtr_state
|
||||
|
||||
@dtr.setter
|
||||
def dtr(self, value):
|
||||
self._dtr_state = value
|
||||
if self.is_open:
|
||||
self._update_dtr_state()
|
||||
|
||||
@property
|
||||
def break_condition(self):
|
||||
return self._break_state
|
||||
|
||||
@break_condition.setter
|
||||
def break_condition(self, value):
|
||||
self._break_state = value
|
||||
if self.is_open:
|
||||
self._update_break_state()
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# functions useful for RS-485 adapters
|
||||
|
||||
@property
|
||||
def rs485_mode(self):
|
||||
"""\
|
||||
Enable RS485 mode and apply new settings, set to None to disable.
|
||||
See serial.rs485.RS485Settings for more info about the value.
|
||||
"""
|
||||
return self._rs485_mode
|
||||
|
||||
@rs485_mode.setter
|
||||
def rs485_mode(self, rs485_settings):
|
||||
self._rs485_mode = rs485_settings
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
_SAVED_SETTINGS = ('baudrate', 'bytesize', 'parity', 'stopbits', 'xonxoff',
|
||||
'dsrdtr', 'rtscts', 'timeout', 'write_timeout',
|
||||
'inter_byte_timeout')
|
||||
|
||||
def get_settings(self):
|
||||
"""\
|
||||
Get current port settings as a dictionary. For use with
|
||||
apply_settings().
|
||||
"""
|
||||
return dict([(key, getattr(self, '_' + key)) for key in self._SAVED_SETTINGS])
|
||||
|
||||
def apply_settings(self, d):
|
||||
"""\
|
||||
Apply stored settings from a dictionary returned from
|
||||
get_settings(). It's allowed to delete keys from the dictionary. These
|
||||
values will simply left unchanged.
|
||||
"""
|
||||
for key in self._SAVED_SETTINGS:
|
||||
if key in d and d[key] != getattr(self, '_' + key): # check against internal "_" value
|
||||
setattr(self, key, d[key]) # set non "_" value to use properties write function
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of the current port settings and its state."""
|
||||
return '{name}<id=0x{id:x}, open={p.is_open}>(port={p.portstr!r}, ' \
|
||||
'baudrate={p.baudrate!r}, bytesize={p.bytesize!r}, parity={p.parity!r}, ' \
|
||||
'stopbits={p.stopbits!r}, timeout={p.timeout!r}, xonxoff={p.xonxoff!r}, ' \
|
||||
'rtscts={p.rtscts!r}, dsrdtr={p.dsrdtr!r})'.format(
|
||||
name=self.__class__.__name__, id=id(self), p=self)
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# compatibility with io library
|
||||
# pylint: disable=invalid-name,missing-docstring
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def writable(self):
|
||||
return True
|
||||
|
||||
def seekable(self):
|
||||
return False
|
||||
|
||||
def readinto(self, b):
|
||||
data = self.read(len(b))
|
||||
n = len(data)
|
||||
try:
|
||||
b[:n] = data
|
||||
except TypeError as err:
|
||||
import array
|
||||
if not isinstance(b, array.array):
|
||||
raise err
|
||||
b[:n] = array.array('b', data)
|
||||
return n
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# context manager
|
||||
|
||||
def __enter__(self):
|
||||
if self._port is not None and not self.is_open:
|
||||
self.open()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
self.close()
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
"""\
|
||||
Send break condition. Timed, returns to idle state after given
|
||||
duration.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self.break_condition = True
|
||||
time.sleep(duration)
|
||||
self.break_condition = False
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# backwards compatibility / deprecated functions
|
||||
|
||||
def flushInput(self):
|
||||
self.reset_input_buffer()
|
||||
|
||||
def flushOutput(self):
|
||||
self.reset_output_buffer()
|
||||
|
||||
def inWaiting(self):
|
||||
return self.in_waiting
|
||||
|
||||
def sendBreak(self, duration=0.25):
|
||||
self.send_break(duration)
|
||||
|
||||
def setRTS(self, value=1):
|
||||
self.rts = value
|
||||
|
||||
def setDTR(self, value=1):
|
||||
self.dtr = value
|
||||
|
||||
def getCTS(self):
|
||||
return self.cts
|
||||
|
||||
def getDSR(self):
|
||||
return self.dsr
|
||||
|
||||
def getRI(self):
|
||||
return self.ri
|
||||
|
||||
def getCD(self):
|
||||
return self.cd
|
||||
|
||||
def setPort(self, port):
|
||||
self.port = port
|
||||
|
||||
@property
|
||||
def writeTimeout(self):
|
||||
return self.write_timeout
|
||||
|
||||
@writeTimeout.setter
|
||||
def writeTimeout(self, timeout):
|
||||
self.write_timeout = timeout
|
||||
|
||||
@property
|
||||
def interCharTimeout(self):
|
||||
return self.inter_byte_timeout
|
||||
|
||||
@interCharTimeout.setter
|
||||
def interCharTimeout(self, interCharTimeout):
|
||||
self.inter_byte_timeout = interCharTimeout
|
||||
|
||||
def getSettingsDict(self):
|
||||
return self.get_settings()
|
||||
|
||||
def applySettingsDict(self, d):
|
||||
self.apply_settings(d)
|
||||
|
||||
def isOpen(self):
|
||||
return self.is_open
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# additional functionality
|
||||
|
||||
def read_all(self):
|
||||
"""\
|
||||
Read all bytes currently available in the buffer of the OS.
|
||||
"""
|
||||
return self.read(self.in_waiting)
|
||||
|
||||
def read_until(self, expected=LF, size=None):
|
||||
"""\
|
||||
Read until an expected sequence is found ('\n' by default), the size
|
||||
is exceeded or until timeout occurs.
|
||||
"""
|
||||
lenterm = len(expected)
|
||||
line = bytearray()
|
||||
timeout = Timeout(self._timeout)
|
||||
while True:
|
||||
c = self.read(1)
|
||||
if c:
|
||||
line += c
|
||||
if line[-lenterm:] == expected:
|
||||
break
|
||||
if size is not None and len(line) >= size:
|
||||
break
|
||||
else:
|
||||
break
|
||||
if timeout.expired():
|
||||
break
|
||||
return bytes(line)
|
||||
|
||||
def iread_until(self, *args, **kwargs):
|
||||
"""\
|
||||
Read lines, implemented as generator. It will raise StopIteration on
|
||||
timeout (empty read).
|
||||
"""
|
||||
while True:
|
||||
line = self.read_until(*args, **kwargs)
|
||||
if not line:
|
||||
break
|
||||
yield line
|
||||
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
s = SerialBase()
|
||||
sys.stdout.write('port name: {}\n'.format(s.name))
|
||||
sys.stdout.write('baud rates: {}\n'.format(s.BAUDRATES))
|
||||
sys.stdout.write('byte sizes: {}\n'.format(s.BYTESIZES))
|
||||
sys.stdout.write('parities: {}\n'.format(s.PARITIES))
|
||||
sys.stdout.write('stop bits: {}\n'.format(s.STOPBITS))
|
||||
sys.stdout.write('{}\n'.format(s))
|
||||
@@ -1,477 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# backend for Windows ("win32" incl. 32/64 bit support)
|
||||
#
|
||||
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# Initial patch to use ctypes by Giovanni Bajo <rasky@develer.com>
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# pylint: disable=invalid-name,too-few-public-methods
|
||||
import ctypes
|
||||
import time
|
||||
from serial import win32
|
||||
|
||||
import serial
|
||||
from serial.serialutil import SerialBase, SerialException, to_bytes, PortNotOpenError, SerialTimeoutException
|
||||
|
||||
|
||||
class Serial(SerialBase):
|
||||
"""Serial port implementation for Win32 based on ctypes."""
|
||||
|
||||
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||
9600, 19200, 38400, 57600, 115200)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._port_handle = None
|
||||
self._overlapped_read = None
|
||||
self._overlapped_write = None
|
||||
super(Serial, self).__init__(*args, **kwargs)
|
||||
|
||||
def open(self):
|
||||
"""\
|
||||
Open port with current settings. This may throw a SerialException
|
||||
if the port cannot be opened.
|
||||
"""
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
# the "\\.\COMx" format is required for devices other than COM1-COM8
|
||||
# not all versions of windows seem to support this properly
|
||||
# so that the first few ports are used with the DOS device name
|
||||
port = self.name
|
||||
try:
|
||||
if port.upper().startswith('COM') and int(port[3:]) > 8:
|
||||
port = '\\\\.\\' + port
|
||||
except ValueError:
|
||||
# for like COMnotanumber
|
||||
pass
|
||||
self._port_handle = win32.CreateFile(
|
||||
port,
|
||||
win32.GENERIC_READ | win32.GENERIC_WRITE,
|
||||
0, # exclusive access
|
||||
None, # no security
|
||||
win32.OPEN_EXISTING,
|
||||
win32.FILE_ATTRIBUTE_NORMAL | win32.FILE_FLAG_OVERLAPPED,
|
||||
0)
|
||||
if self._port_handle == win32.INVALID_HANDLE_VALUE:
|
||||
self._port_handle = None # 'cause __del__ is called anyway
|
||||
raise SerialException("could not open port {!r}: {!r}".format(self.portstr, ctypes.WinError()))
|
||||
|
||||
try:
|
||||
self._overlapped_read = win32.OVERLAPPED()
|
||||
self._overlapped_read.hEvent = win32.CreateEvent(None, 1, 0, None)
|
||||
self._overlapped_write = win32.OVERLAPPED()
|
||||
#~ self._overlapped_write.hEvent = win32.CreateEvent(None, 1, 0, None)
|
||||
self._overlapped_write.hEvent = win32.CreateEvent(None, 0, 0, None)
|
||||
|
||||
# Setup a 4k buffer
|
||||
win32.SetupComm(self._port_handle, 4096, 4096)
|
||||
|
||||
# Save original timeout values:
|
||||
self._orgTimeouts = win32.COMMTIMEOUTS()
|
||||
win32.GetCommTimeouts(self._port_handle, ctypes.byref(self._orgTimeouts))
|
||||
|
||||
self._reconfigure_port()
|
||||
|
||||
# Clear buffers:
|
||||
# Remove anything that was there
|
||||
win32.PurgeComm(
|
||||
self._port_handle,
|
||||
win32.PURGE_TXCLEAR | win32.PURGE_TXABORT |
|
||||
win32.PURGE_RXCLEAR | win32.PURGE_RXABORT)
|
||||
except:
|
||||
try:
|
||||
self._close()
|
||||
except:
|
||||
# ignore any exception when closing the port
|
||||
# also to keep original exception that happened when setting up
|
||||
pass
|
||||
self._port_handle = None
|
||||
raise
|
||||
else:
|
||||
self.is_open = True
|
||||
|
||||
def _reconfigure_port(self):
|
||||
"""Set communication parameters on opened port."""
|
||||
if not self._port_handle:
|
||||
raise SerialException("Can only operate on a valid port handle")
|
||||
|
||||
# Set Windows timeout values
|
||||
# timeouts is a tuple with the following items:
|
||||
# (ReadIntervalTimeout,ReadTotalTimeoutMultiplier,
|
||||
# ReadTotalTimeoutConstant,WriteTotalTimeoutMultiplier,
|
||||
# WriteTotalTimeoutConstant)
|
||||
timeouts = win32.COMMTIMEOUTS()
|
||||
if self._timeout is None:
|
||||
pass # default of all zeros is OK
|
||||
elif self._timeout == 0:
|
||||
timeouts.ReadIntervalTimeout = win32.MAXDWORD
|
||||
else:
|
||||
timeouts.ReadTotalTimeoutConstant = max(int(self._timeout * 1000), 1)
|
||||
if self._timeout != 0 and self._inter_byte_timeout is not None:
|
||||
timeouts.ReadIntervalTimeout = max(int(self._inter_byte_timeout * 1000), 1)
|
||||
|
||||
if self._write_timeout is None:
|
||||
pass
|
||||
elif self._write_timeout == 0:
|
||||
timeouts.WriteTotalTimeoutConstant = win32.MAXDWORD
|
||||
else:
|
||||
timeouts.WriteTotalTimeoutConstant = max(int(self._write_timeout * 1000), 1)
|
||||
win32.SetCommTimeouts(self._port_handle, ctypes.byref(timeouts))
|
||||
|
||||
win32.SetCommMask(self._port_handle, win32.EV_ERR)
|
||||
|
||||
# Setup the connection info.
|
||||
# Get state and modify it:
|
||||
comDCB = win32.DCB()
|
||||
win32.GetCommState(self._port_handle, ctypes.byref(comDCB))
|
||||
comDCB.BaudRate = self._baudrate
|
||||
|
||||
if self._bytesize == serial.FIVEBITS:
|
||||
comDCB.ByteSize = 5
|
||||
elif self._bytesize == serial.SIXBITS:
|
||||
comDCB.ByteSize = 6
|
||||
elif self._bytesize == serial.SEVENBITS:
|
||||
comDCB.ByteSize = 7
|
||||
elif self._bytesize == serial.EIGHTBITS:
|
||||
comDCB.ByteSize = 8
|
||||
else:
|
||||
raise ValueError("Unsupported number of data bits: {!r}".format(self._bytesize))
|
||||
|
||||
if self._parity == serial.PARITY_NONE:
|
||||
comDCB.Parity = win32.NOPARITY
|
||||
comDCB.fParity = 0 # Disable Parity Check
|
||||
elif self._parity == serial.PARITY_EVEN:
|
||||
comDCB.Parity = win32.EVENPARITY
|
||||
comDCB.fParity = 1 # Enable Parity Check
|
||||
elif self._parity == serial.PARITY_ODD:
|
||||
comDCB.Parity = win32.ODDPARITY
|
||||
comDCB.fParity = 1 # Enable Parity Check
|
||||
elif self._parity == serial.PARITY_MARK:
|
||||
comDCB.Parity = win32.MARKPARITY
|
||||
comDCB.fParity = 1 # Enable Parity Check
|
||||
elif self._parity == serial.PARITY_SPACE:
|
||||
comDCB.Parity = win32.SPACEPARITY
|
||||
comDCB.fParity = 1 # Enable Parity Check
|
||||
else:
|
||||
raise ValueError("Unsupported parity mode: {!r}".format(self._parity))
|
||||
|
||||
if self._stopbits == serial.STOPBITS_ONE:
|
||||
comDCB.StopBits = win32.ONESTOPBIT
|
||||
elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
|
||||
comDCB.StopBits = win32.ONE5STOPBITS
|
||||
elif self._stopbits == serial.STOPBITS_TWO:
|
||||
comDCB.StopBits = win32.TWOSTOPBITS
|
||||
else:
|
||||
raise ValueError("Unsupported number of stop bits: {!r}".format(self._stopbits))
|
||||
|
||||
comDCB.fBinary = 1 # Enable Binary Transmission
|
||||
# Char. w/ Parity-Err are replaced with 0xff (if fErrorChar is set to TRUE)
|
||||
if self._rs485_mode is None:
|
||||
if self._rtscts:
|
||||
comDCB.fRtsControl = win32.RTS_CONTROL_HANDSHAKE
|
||||
else:
|
||||
comDCB.fRtsControl = win32.RTS_CONTROL_ENABLE if self._rts_state else win32.RTS_CONTROL_DISABLE
|
||||
comDCB.fOutxCtsFlow = self._rtscts
|
||||
else:
|
||||
# checks for unsupported settings
|
||||
# XXX verify if platform really does not have a setting for those
|
||||
if not self._rs485_mode.rts_level_for_tx:
|
||||
raise ValueError(
|
||||
'Unsupported value for RS485Settings.rts_level_for_tx: {!r} (only True is allowed)'.format(
|
||||
self._rs485_mode.rts_level_for_tx,))
|
||||
if self._rs485_mode.rts_level_for_rx:
|
||||
raise ValueError(
|
||||
'Unsupported value for RS485Settings.rts_level_for_rx: {!r} (only False is allowed)'.format(
|
||||
self._rs485_mode.rts_level_for_rx,))
|
||||
if self._rs485_mode.delay_before_tx is not None:
|
||||
raise ValueError(
|
||||
'Unsupported value for RS485Settings.delay_before_tx: {!r} (only None is allowed)'.format(
|
||||
self._rs485_mode.delay_before_tx,))
|
||||
if self._rs485_mode.delay_before_rx is not None:
|
||||
raise ValueError(
|
||||
'Unsupported value for RS485Settings.delay_before_rx: {!r} (only None is allowed)'.format(
|
||||
self._rs485_mode.delay_before_rx,))
|
||||
if self._rs485_mode.loopback:
|
||||
raise ValueError(
|
||||
'Unsupported value for RS485Settings.loopback: {!r} (only False is allowed)'.format(
|
||||
self._rs485_mode.loopback,))
|
||||
comDCB.fRtsControl = win32.RTS_CONTROL_TOGGLE
|
||||
comDCB.fOutxCtsFlow = 0
|
||||
|
||||
if self._dsrdtr:
|
||||
comDCB.fDtrControl = win32.DTR_CONTROL_HANDSHAKE
|
||||
else:
|
||||
comDCB.fDtrControl = win32.DTR_CONTROL_ENABLE if self._dtr_state else win32.DTR_CONTROL_DISABLE
|
||||
comDCB.fOutxDsrFlow = self._dsrdtr
|
||||
comDCB.fOutX = self._xonxoff
|
||||
comDCB.fInX = self._xonxoff
|
||||
comDCB.fNull = 0
|
||||
comDCB.fErrorChar = 0
|
||||
comDCB.fAbortOnError = 0
|
||||
comDCB.XonChar = serial.XON
|
||||
comDCB.XoffChar = serial.XOFF
|
||||
|
||||
if not win32.SetCommState(self._port_handle, ctypes.byref(comDCB)):
|
||||
raise SerialException(
|
||||
'Cannot configure port, something went wrong. '
|
||||
'Original message: {!r}'.format(ctypes.WinError()))
|
||||
|
||||
#~ def __del__(self):
|
||||
#~ self.close()
|
||||
|
||||
def _close(self):
|
||||
"""internal close port helper"""
|
||||
if self._port_handle is not None:
|
||||
# Restore original timeout values:
|
||||
win32.SetCommTimeouts(self._port_handle, self._orgTimeouts)
|
||||
if self._overlapped_read is not None:
|
||||
self.cancel_read()
|
||||
win32.CloseHandle(self._overlapped_read.hEvent)
|
||||
self._overlapped_read = None
|
||||
if self._overlapped_write is not None:
|
||||
self.cancel_write()
|
||||
win32.CloseHandle(self._overlapped_write.hEvent)
|
||||
self._overlapped_write = None
|
||||
win32.CloseHandle(self._port_handle)
|
||||
self._port_handle = None
|
||||
|
||||
def close(self):
|
||||
"""Close port"""
|
||||
if self.is_open:
|
||||
self._close()
|
||||
self.is_open = False
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of bytes currently in the input buffer."""
|
||||
flags = win32.DWORD()
|
||||
comstat = win32.COMSTAT()
|
||||
if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
|
||||
raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
|
||||
return comstat.cbInQue
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if size > 0:
|
||||
win32.ResetEvent(self._overlapped_read.hEvent)
|
||||
flags = win32.DWORD()
|
||||
comstat = win32.COMSTAT()
|
||||
if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
|
||||
raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
|
||||
n = min(comstat.cbInQue, size) if self.timeout == 0 else size
|
||||
if n > 0:
|
||||
buf = ctypes.create_string_buffer(n)
|
||||
rc = win32.DWORD()
|
||||
read_ok = win32.ReadFile(
|
||||
self._port_handle,
|
||||
buf,
|
||||
n,
|
||||
ctypes.byref(rc),
|
||||
ctypes.byref(self._overlapped_read))
|
||||
if not read_ok and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
|
||||
raise SerialException("ReadFile failed ({!r})".format(ctypes.WinError()))
|
||||
result_ok = win32.GetOverlappedResult(
|
||||
self._port_handle,
|
||||
ctypes.byref(self._overlapped_read),
|
||||
ctypes.byref(rc),
|
||||
True)
|
||||
if not result_ok:
|
||||
if win32.GetLastError() != win32.ERROR_OPERATION_ABORTED:
|
||||
raise SerialException("GetOverlappedResult failed ({!r})".format(ctypes.WinError()))
|
||||
read = buf.raw[:rc.value]
|
||||
else:
|
||||
read = bytes()
|
||||
else:
|
||||
read = bytes()
|
||||
return bytes(read)
|
||||
|
||||
def write(self, data):
|
||||
"""Output the given byte string over the serial port."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
#~ if not isinstance(data, (bytes, bytearray)):
|
||||
#~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
|
||||
# convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview
|
||||
data = to_bytes(data)
|
||||
if data:
|
||||
#~ win32event.ResetEvent(self._overlapped_write.hEvent)
|
||||
n = win32.DWORD()
|
||||
success = win32.WriteFile(self._port_handle, data, len(data), ctypes.byref(n), self._overlapped_write)
|
||||
if self._write_timeout != 0: # if blocking (None) or w/ write timeout (>0)
|
||||
if not success and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
|
||||
raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
|
||||
|
||||
# Wait for the write to complete.
|
||||
#~ win32.WaitForSingleObject(self._overlapped_write.hEvent, win32.INFINITE)
|
||||
win32.GetOverlappedResult(self._port_handle, self._overlapped_write, ctypes.byref(n), True)
|
||||
if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED:
|
||||
return n.value # canceled IO is no error
|
||||
if n.value != len(data):
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
return n.value
|
||||
else:
|
||||
errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError()
|
||||
if errorcode in (win32.ERROR_INVALID_USER_BUFFER, win32.ERROR_NOT_ENOUGH_MEMORY,
|
||||
win32.ERROR_OPERATION_ABORTED):
|
||||
return 0
|
||||
elif errorcode in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
|
||||
# no info on true length provided by OS function in async mode
|
||||
return len(data)
|
||||
else:
|
||||
raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
|
||||
else:
|
||||
return 0
|
||||
|
||||
def flush(self):
|
||||
"""\
|
||||
Flush of file like objects. In this case, wait until all data
|
||||
is written.
|
||||
"""
|
||||
while self.out_waiting:
|
||||
time.sleep(0.05)
|
||||
# XXX could also use WaitCommEvent with mask EV_TXEMPTY, but it would
|
||||
# require overlapped IO and it's also only possible to set a single mask
|
||||
# on the port---
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
win32.PurgeComm(self._port_handle, win32.PURGE_RXCLEAR | win32.PURGE_RXABORT)
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""\
|
||||
Clear output buffer, aborting the current output and discarding all
|
||||
that is in the buffer.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
win32.PurgeComm(self._port_handle, win32.PURGE_TXCLEAR | win32.PURGE_TXABORT)
|
||||
|
||||
def _update_break_state(self):
|
||||
"""Set break: Controls TXD. When active, to transmitting is possible."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self._break_state:
|
||||
win32.SetCommBreak(self._port_handle)
|
||||
else:
|
||||
win32.ClearCommBreak(self._port_handle)
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
if self._rts_state:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.SETRTS)
|
||||
else:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.CLRRTS)
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
if self._dtr_state:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.SETDTR)
|
||||
else:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.CLRDTR)
|
||||
|
||||
def _GetCommModemStatus(self):
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
stat = win32.DWORD()
|
||||
win32.GetCommModemStatus(self._port_handle, ctypes.byref(stat))
|
||||
return stat.value
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
return win32.MS_CTS_ON & self._GetCommModemStatus() != 0
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
return win32.MS_DSR_ON & self._GetCommModemStatus() != 0
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
return win32.MS_RING_ON & self._GetCommModemStatus() != 0
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
return win32.MS_RLSD_ON & self._GetCommModemStatus() != 0
|
||||
|
||||
# - - platform specific - - - -
|
||||
|
||||
def set_buffer_size(self, rx_size=4096, tx_size=None):
|
||||
"""\
|
||||
Recommend a buffer size to the driver (device driver can ignore this
|
||||
value). Must be called after the port is opened.
|
||||
"""
|
||||
if tx_size is None:
|
||||
tx_size = rx_size
|
||||
win32.SetupComm(self._port_handle, rx_size, tx_size)
|
||||
|
||||
def set_output_flow_control(self, enable=True):
|
||||
"""\
|
||||
Manually control flow - when software flow control is enabled.
|
||||
This will do the same as if XON (true) or XOFF (false) are received
|
||||
from the other device and control the transmission accordingly.
|
||||
WARNING: this function is not portable to different platforms!
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if enable:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.SETXON)
|
||||
else:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.SETXOFF)
|
||||
|
||||
@property
|
||||
def out_waiting(self):
|
||||
"""Return how many bytes the in the outgoing buffer"""
|
||||
flags = win32.DWORD()
|
||||
comstat = win32.COMSTAT()
|
||||
if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
|
||||
raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
|
||||
return comstat.cbOutQue
|
||||
|
||||
def _cancel_overlapped_io(self, overlapped):
|
||||
"""Cancel a blocking read operation, may be called from other thread"""
|
||||
# check if read operation is pending
|
||||
rc = win32.DWORD()
|
||||
err = win32.GetOverlappedResult(
|
||||
self._port_handle,
|
||||
ctypes.byref(overlapped),
|
||||
ctypes.byref(rc),
|
||||
False)
|
||||
if not err and win32.GetLastError() in (win32.ERROR_IO_PENDING, win32.ERROR_IO_INCOMPLETE):
|
||||
# cancel, ignoring any errors (e.g. it may just have finished on its own)
|
||||
win32.CancelIoEx(self._port_handle, overlapped)
|
||||
|
||||
def cancel_read(self):
|
||||
"""Cancel a blocking read operation, may be called from other thread"""
|
||||
self._cancel_overlapped_io(self._overlapped_read)
|
||||
|
||||
def cancel_write(self):
|
||||
"""Cancel a blocking write operation, may be called from other thread"""
|
||||
self._cancel_overlapped_io(self._overlapped_write)
|
||||
|
||||
@SerialBase.exclusive.setter
|
||||
def exclusive(self, exclusive):
|
||||
"""Change the exclusive access setting."""
|
||||
if exclusive is not None and not exclusive:
|
||||
raise ValueError('win32 only supports exclusive access (not: {})'.format(exclusive))
|
||||
else:
|
||||
serial.SerialBase.exclusive.__set__(self, exclusive)
|
||||
@@ -1,297 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Working with threading and pySerial
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015-2016 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
"""\
|
||||
Support threading with serial ports.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import serial
|
||||
import threading
|
||||
|
||||
|
||||
class Protocol(object):
|
||||
"""\
|
||||
Protocol as used by the ReaderThread. This base class provides empty
|
||||
implementations of all methods.
|
||||
"""
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Called when reader thread is started"""
|
||||
|
||||
def data_received(self, data):
|
||||
"""Called with snippets received from the serial port"""
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""\
|
||||
Called when the serial port is closed or the reader loop terminated
|
||||
otherwise.
|
||||
"""
|
||||
if isinstance(exc, Exception):
|
||||
raise exc
|
||||
|
||||
|
||||
class Packetizer(Protocol):
|
||||
"""
|
||||
Read binary packets from serial port. Packets are expected to be terminated
|
||||
with a TERMINATOR byte (null byte by default).
|
||||
|
||||
The class also keeps track of the transport.
|
||||
"""
|
||||
|
||||
TERMINATOR = b'\0'
|
||||
|
||||
def __init__(self):
|
||||
self.buffer = bytearray()
|
||||
self.transport = None
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Store transport"""
|
||||
self.transport = transport
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""Forget transport"""
|
||||
self.transport = None
|
||||
super(Packetizer, self).connection_lost(exc)
|
||||
|
||||
def data_received(self, data):
|
||||
"""Buffer received data, find TERMINATOR, call handle_packet"""
|
||||
self.buffer.extend(data)
|
||||
while self.TERMINATOR in self.buffer:
|
||||
packet, self.buffer = self.buffer.split(self.TERMINATOR, 1)
|
||||
self.handle_packet(packet)
|
||||
|
||||
def handle_packet(self, packet):
|
||||
"""Process packets - to be overridden by subclassing"""
|
||||
raise NotImplementedError('please implement functionality in handle_packet')
|
||||
|
||||
|
||||
class FramedPacket(Protocol):
|
||||
"""
|
||||
Read binary packets. Packets are expected to have a start and stop marker.
|
||||
|
||||
The class also keeps track of the transport.
|
||||
"""
|
||||
|
||||
START = b'('
|
||||
STOP = b')'
|
||||
|
||||
def __init__(self):
|
||||
self.packet = bytearray()
|
||||
self.in_packet = False
|
||||
self.transport = None
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Store transport"""
|
||||
self.transport = transport
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""Forget transport"""
|
||||
self.transport = None
|
||||
self.in_packet = False
|
||||
del self.packet[:]
|
||||
super(FramedPacket, self).connection_lost(exc)
|
||||
|
||||
def data_received(self, data):
|
||||
"""Find data enclosed in START/STOP, call handle_packet"""
|
||||
for byte in serial.iterbytes(data):
|
||||
if byte == self.START:
|
||||
self.in_packet = True
|
||||
elif byte == self.STOP:
|
||||
self.in_packet = False
|
||||
self.handle_packet(bytes(self.packet)) # make read-only copy
|
||||
del self.packet[:]
|
||||
elif self.in_packet:
|
||||
self.packet.extend(byte)
|
||||
else:
|
||||
self.handle_out_of_packet_data(byte)
|
||||
|
||||
def handle_packet(self, packet):
|
||||
"""Process packets - to be overridden by subclassing"""
|
||||
raise NotImplementedError('please implement functionality in handle_packet')
|
||||
|
||||
def handle_out_of_packet_data(self, data):
|
||||
"""Process data that is received outside of packets"""
|
||||
pass
|
||||
|
||||
|
||||
class LineReader(Packetizer):
|
||||
"""
|
||||
Read and write (Unicode) lines from/to serial port.
|
||||
The encoding is applied.
|
||||
"""
|
||||
|
||||
TERMINATOR = b'\r\n'
|
||||
ENCODING = 'utf-8'
|
||||
UNICODE_HANDLING = 'replace'
|
||||
|
||||
def handle_packet(self, packet):
|
||||
self.handle_line(packet.decode(self.ENCODING, self.UNICODE_HANDLING))
|
||||
|
||||
def handle_line(self, line):
|
||||
"""Process one line - to be overridden by subclassing"""
|
||||
raise NotImplementedError('please implement functionality in handle_line')
|
||||
|
||||
def write_line(self, text):
|
||||
"""
|
||||
Write text to the transport. ``text`` is a Unicode string and the encoding
|
||||
is applied before sending ans also the newline is append.
|
||||
"""
|
||||
# + is not the best choice but bytes does not support % or .format in py3 and we want a single write call
|
||||
self.transport.write(text.encode(self.ENCODING, self.UNICODE_HANDLING) + self.TERMINATOR)
|
||||
|
||||
|
||||
class ReaderThread(threading.Thread):
|
||||
"""\
|
||||
Implement a serial port read loop and dispatch to a Protocol instance (like
|
||||
the asyncio.Protocol) but do it with threads.
|
||||
|
||||
Calls to close() will close the serial port but it is also possible to just
|
||||
stop() this thread and continue the serial port instance otherwise.
|
||||
"""
|
||||
|
||||
def __init__(self, serial_instance, protocol_factory):
|
||||
"""\
|
||||
Initialize thread.
|
||||
|
||||
Note that the serial_instance' timeout is set to one second!
|
||||
Other settings are not changed.
|
||||
"""
|
||||
super(ReaderThread, self).__init__()
|
||||
self.daemon = True
|
||||
self.serial = serial_instance
|
||||
self.protocol_factory = protocol_factory
|
||||
self.alive = True
|
||||
self._lock = threading.Lock()
|
||||
self._connection_made = threading.Event()
|
||||
self.protocol = None
|
||||
|
||||
def stop(self):
|
||||
"""Stop the reader thread"""
|
||||
self.alive = False
|
||||
if hasattr(self.serial, 'cancel_read'):
|
||||
self.serial.cancel_read()
|
||||
self.join(2)
|
||||
|
||||
def run(self):
|
||||
"""Reader loop"""
|
||||
if not hasattr(self.serial, 'cancel_read'):
|
||||
self.serial.timeout = 1
|
||||
self.protocol = self.protocol_factory()
|
||||
try:
|
||||
self.protocol.connection_made(self)
|
||||
except Exception as e:
|
||||
self.alive = False
|
||||
self.protocol.connection_lost(e)
|
||||
self._connection_made.set()
|
||||
return
|
||||
error = None
|
||||
self._connection_made.set()
|
||||
while self.alive and self.serial.is_open:
|
||||
try:
|
||||
# read all that is there or wait for one byte (blocking)
|
||||
data = self.serial.read(self.serial.in_waiting or 1)
|
||||
except serial.SerialException as e:
|
||||
# probably some I/O problem such as disconnected USB serial
|
||||
# adapters -> exit
|
||||
error = e
|
||||
break
|
||||
else:
|
||||
if data:
|
||||
# make a separated try-except for called user code
|
||||
try:
|
||||
self.protocol.data_received(data)
|
||||
except Exception as e:
|
||||
error = e
|
||||
break
|
||||
self.alive = False
|
||||
self.protocol.connection_lost(error)
|
||||
self.protocol = None
|
||||
|
||||
def write(self, data):
|
||||
"""Thread safe writing (uses lock)"""
|
||||
with self._lock:
|
||||
return self.serial.write(data)
|
||||
|
||||
def close(self):
|
||||
"""Close the serial port and exit reader thread (uses lock)"""
|
||||
# use the lock to let other threads finish writing
|
||||
with self._lock:
|
||||
# first stop reading, so that closing can be done on idle port
|
||||
self.stop()
|
||||
self.serial.close()
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Wait until connection is set up and return the transport and protocol
|
||||
instances.
|
||||
"""
|
||||
if self.alive:
|
||||
self._connection_made.wait()
|
||||
if not self.alive:
|
||||
raise RuntimeError('connection_lost already called')
|
||||
return (self, self.protocol)
|
||||
else:
|
||||
raise RuntimeError('already stopped')
|
||||
|
||||
# - - context manager, returns protocol
|
||||
|
||||
def __enter__(self):
|
||||
"""\
|
||||
Enter context handler. May raise RuntimeError in case the connection
|
||||
could not be created.
|
||||
"""
|
||||
self.start()
|
||||
self._connection_made.wait()
|
||||
if not self.alive:
|
||||
raise RuntimeError('connection_lost already called')
|
||||
return self.protocol
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Leave context: close port"""
|
||||
self.close()
|
||||
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
# pylint: disable=wrong-import-position
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
#~ PORT = 'spy:///dev/ttyUSB0'
|
||||
PORT = 'loop://'
|
||||
|
||||
class PrintLines(LineReader):
|
||||
def connection_made(self, transport):
|
||||
super(PrintLines, self).connection_made(transport)
|
||||
sys.stdout.write('port opened\n')
|
||||
self.write_line('hello world')
|
||||
|
||||
def handle_line(self, data):
|
||||
sys.stdout.write('line received: {!r}\n'.format(data))
|
||||
|
||||
def connection_lost(self, exc):
|
||||
if exc:
|
||||
traceback.print_exc(exc)
|
||||
sys.stdout.write('port closed\n')
|
||||
|
||||
ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1)
|
||||
with ReaderThread(ser, PrintLines) as protocol:
|
||||
protocol.write_line('hello')
|
||||
time.sleep(2)
|
||||
|
||||
# alternative usage
|
||||
ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1)
|
||||
t = ReaderThread(ser, PrintLines)
|
||||
t.start()
|
||||
transport, protocol = t.connect()
|
||||
protocol.write_line('hello')
|
||||
time.sleep(2)
|
||||
t.close()
|
||||
@@ -1,126 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# This is a codec to create and decode hexdumps with spaces between characters. used by miniterm.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015-2016 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
"""\
|
||||
Python 'hex' Codec - 2-digit hex with spaces content transfer encoding.
|
||||
|
||||
Encode and decode may be a bit missleading at first sight...
|
||||
|
||||
The textual representation is a hex dump: e.g. "40 41"
|
||||
The "encoded" data of this is the binary form, e.g. b"@A"
|
||||
|
||||
Therefore decoding is binary to text and thus converting binary data to hex dump.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import codecs
|
||||
import serial
|
||||
|
||||
|
||||
try:
|
||||
unicode
|
||||
except (NameError, AttributeError):
|
||||
unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name
|
||||
|
||||
|
||||
HEXDIGITS = '0123456789ABCDEF'
|
||||
|
||||
|
||||
# Codec APIs
|
||||
|
||||
def hex_encode(data, errors='strict'):
|
||||
"""'40 41 42' -> b'@ab'"""
|
||||
return (serial.to_bytes([int(h, 16) for h in data.split()]), len(data))
|
||||
|
||||
|
||||
def hex_decode(data, errors='strict'):
|
||||
"""b'@ab' -> '40 41 42'"""
|
||||
return (unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data))), len(data))
|
||||
|
||||
|
||||
class Codec(codecs.Codec):
|
||||
def encode(self, data, errors='strict'):
|
||||
"""'40 41 42' -> b'@ab'"""
|
||||
return serial.to_bytes([int(h, 16) for h in data.split()])
|
||||
|
||||
def decode(self, data, errors='strict'):
|
||||
"""b'@ab' -> '40 41 42'"""
|
||||
return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data)))
|
||||
|
||||
|
||||
class IncrementalEncoder(codecs.IncrementalEncoder):
|
||||
"""Incremental hex encoder"""
|
||||
|
||||
def __init__(self, errors='strict'):
|
||||
self.errors = errors
|
||||
self.state = 0
|
||||
|
||||
def reset(self):
|
||||
self.state = 0
|
||||
|
||||
def getstate(self):
|
||||
return self.state
|
||||
|
||||
def setstate(self, state):
|
||||
self.state = state
|
||||
|
||||
def encode(self, data, final=False):
|
||||
"""\
|
||||
Incremental encode, keep track of digits and emit a byte when a pair
|
||||
of hex digits is found. The space is optional unless the error
|
||||
handling is defined to be 'strict'.
|
||||
"""
|
||||
state = self.state
|
||||
encoded = []
|
||||
for c in data.upper():
|
||||
if c in HEXDIGITS:
|
||||
z = HEXDIGITS.index(c)
|
||||
if state:
|
||||
encoded.append(z + (state & 0xf0))
|
||||
state = 0
|
||||
else:
|
||||
state = 0x100 + (z << 4)
|
||||
elif c == ' ': # allow spaces to separate values
|
||||
if state and self.errors == 'strict':
|
||||
raise UnicodeError('odd number of hex digits')
|
||||
state = 0
|
||||
else:
|
||||
if self.errors == 'strict':
|
||||
raise UnicodeError('non-hex digit found: {!r}'.format(c))
|
||||
self.state = state
|
||||
return serial.to_bytes(encoded)
|
||||
|
||||
|
||||
class IncrementalDecoder(codecs.IncrementalDecoder):
|
||||
"""Incremental decoder"""
|
||||
def decode(self, data, final=False):
|
||||
return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data)))
|
||||
|
||||
|
||||
class StreamWriter(Codec, codecs.StreamWriter):
|
||||
"""Combination of hexlify codec and StreamWriter"""
|
||||
|
||||
|
||||
class StreamReader(Codec, codecs.StreamReader):
|
||||
"""Combination of hexlify codec and StreamReader"""
|
||||
|
||||
|
||||
def getregentry():
|
||||
"""encodings module API"""
|
||||
return codecs.CodecInfo(
|
||||
name='hexlify',
|
||||
encode=hex_encode,
|
||||
decode=hex_decode,
|
||||
incrementalencoder=IncrementalEncoder,
|
||||
incrementaldecoder=IncrementalDecoder,
|
||||
streamwriter=StreamWriter,
|
||||
streamreader=StreamReader,
|
||||
#~ _is_text_encoding=True,
|
||||
)
|
||||
@@ -1,110 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Serial port enumeration. Console tool and backend selection.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""\
|
||||
This module will provide a function called comports that returns an
|
||||
iterable (generator or list) that will enumerate available com ports. Note that
|
||||
on some systems non-existent ports may be listed.
|
||||
|
||||
Additionally a grep function is supplied that can be used to search for ports
|
||||
based on their descriptions or hardware ID.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
# chose an implementation, depending on os
|
||||
#~ if sys.platform == 'cli':
|
||||
#~ else:
|
||||
if os.name == 'nt': # sys.platform == 'win32':
|
||||
from serial.tools.list_ports_windows import comports
|
||||
elif os.name == 'posix':
|
||||
from serial.tools.list_ports_posix import comports
|
||||
#~ elif os.name == 'java':
|
||||
else:
|
||||
raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
|
||||
def grep(regexp, include_links=False):
|
||||
"""\
|
||||
Search for ports using a regular expression. Port name, description and
|
||||
hardware ID are searched. The function returns an iterable that returns the
|
||||
same tuples as comport() would do.
|
||||
"""
|
||||
r = re.compile(regexp, re.I)
|
||||
for info in comports(include_links):
|
||||
port, desc, hwid = info
|
||||
if r.search(port) or r.search(desc) or r.search(hwid):
|
||||
yield info
|
||||
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Serial port enumeration')
|
||||
|
||||
parser.add_argument(
|
||||
'regexp',
|
||||
nargs='?',
|
||||
help='only show ports that match this regex')
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
action='store_true',
|
||||
help='show more messages')
|
||||
|
||||
parser.add_argument(
|
||||
'-q', '--quiet',
|
||||
action='store_true',
|
||||
help='suppress all messages')
|
||||
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
type=int,
|
||||
help='only output the N-th entry')
|
||||
|
||||
parser.add_argument(
|
||||
'-s', '--include-links',
|
||||
action='store_true',
|
||||
help='include entries that are symlinks to real devices')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
hits = 0
|
||||
# get iteraror w/ or w/o filter
|
||||
if args.regexp:
|
||||
if not args.quiet:
|
||||
sys.stderr.write("Filtered list with regexp: {!r}\n".format(args.regexp))
|
||||
iterator = sorted(grep(args.regexp, include_links=args.include_links))
|
||||
else:
|
||||
iterator = sorted(comports(include_links=args.include_links))
|
||||
# list them
|
||||
for n, (port, desc, hwid) in enumerate(iterator, 1):
|
||||
if args.n is None or args.n == n:
|
||||
sys.stdout.write("{:20}\n".format(port))
|
||||
if args.verbose:
|
||||
sys.stdout.write(" desc: {}\n".format(desc))
|
||||
sys.stdout.write(" hwid: {}\n".format(hwid))
|
||||
hits += 1
|
||||
if not args.quiet:
|
||||
if hits:
|
||||
sys.stderr.write("{} ports found\n".format(hits))
|
||||
else:
|
||||
sys.stderr.write("no ports found\n")
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,121 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This is a helper module for the various platform dependent list_port
|
||||
# implementations.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
|
||||
|
||||
def numsplit(text):
|
||||
"""\
|
||||
Convert string into a list of texts and numbers in order to support a
|
||||
natural sorting.
|
||||
"""
|
||||
result = []
|
||||
for group in re.split(r'(\d+)', text):
|
||||
if group:
|
||||
try:
|
||||
group = int(group)
|
||||
except ValueError:
|
||||
pass
|
||||
result.append(group)
|
||||
return result
|
||||
|
||||
|
||||
class ListPortInfo(object):
|
||||
"""Info collection base class for serial ports"""
|
||||
|
||||
def __init__(self, device, skip_link_detection=False):
|
||||
self.device = device
|
||||
self.name = os.path.basename(device)
|
||||
self.description = 'n/a'
|
||||
self.hwid = 'n/a'
|
||||
# USB specific data
|
||||
self.vid = None
|
||||
self.pid = None
|
||||
self.serial_number = None
|
||||
self.location = None
|
||||
self.manufacturer = None
|
||||
self.product = None
|
||||
self.interface = None
|
||||
# special handling for links
|
||||
if not skip_link_detection and device is not None and os.path.islink(device):
|
||||
self.hwid = 'LINK={}'.format(os.path.realpath(device))
|
||||
|
||||
def usb_description(self):
|
||||
"""return a short string to name the port based on USB info"""
|
||||
if self.interface is not None:
|
||||
return '{} - {}'.format(self.product, self.interface)
|
||||
elif self.product is not None:
|
||||
return self.product
|
||||
else:
|
||||
return self.name
|
||||
|
||||
def usb_info(self):
|
||||
"""return a string with USB related information about device"""
|
||||
return 'USB VID:PID={:04X}:{:04X}{}{}'.format(
|
||||
self.vid or 0,
|
||||
self.pid or 0,
|
||||
' SER={}'.format(self.serial_number) if self.serial_number is not None else '',
|
||||
' LOCATION={}'.format(self.location) if self.location is not None else '')
|
||||
|
||||
def apply_usb_info(self):
|
||||
"""update description and hwid from USB data"""
|
||||
self.description = self.usb_description()
|
||||
self.hwid = self.usb_info()
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, ListPortInfo) and self.device == other.device
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.device)
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, ListPortInfo):
|
||||
raise TypeError('unorderable types: {}() and {}()'.format(
|
||||
type(self).__name__,
|
||||
type(other).__name__))
|
||||
return numsplit(self.device) < numsplit(other.device)
|
||||
|
||||
def __str__(self):
|
||||
return '{} - {}'.format(self.device, self.description)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Item access: backwards compatible -> (port, desc, hwid)"""
|
||||
if index == 0:
|
||||
return self.device
|
||||
elif index == 1:
|
||||
return self.description
|
||||
elif index == 2:
|
||||
return self.hwid
|
||||
else:
|
||||
raise IndexError('{} > 2'.format(index))
|
||||
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
def list_links(devices):
|
||||
"""\
|
||||
search all /dev devices and look for symlinks to known ports already
|
||||
listed in devices.
|
||||
"""
|
||||
links = []
|
||||
for device in glob.glob('/dev/*'):
|
||||
if os.path.islink(device) and os.path.realpath(device) in devices:
|
||||
links.append(device)
|
||||
return links
|
||||
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
print(ListPortInfo('dummy'))
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This is a module that gathers a list of serial ports including details on
|
||||
# GNU/Linux systems.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import glob
|
||||
import os
|
||||
from serial.tools import list_ports_common
|
||||
|
||||
|
||||
class SysFS(list_ports_common.ListPortInfo):
|
||||
"""Wrapper for easy sysfs access and device info"""
|
||||
|
||||
def __init__(self, device):
|
||||
super(SysFS, self).__init__(device)
|
||||
# special handling for links
|
||||
if device is not None and os.path.islink(device):
|
||||
device = os.path.realpath(device)
|
||||
is_link = True
|
||||
else:
|
||||
is_link = False
|
||||
self.usb_device_path = None
|
||||
if os.path.exists('/sys/class/tty/{}/device'.format(self.name)):
|
||||
self.device_path = os.path.realpath('/sys/class/tty/{}/device'.format(self.name))
|
||||
self.subsystem = os.path.basename(os.path.realpath(os.path.join(self.device_path, 'subsystem')))
|
||||
else:
|
||||
self.device_path = None
|
||||
self.subsystem = None
|
||||
# check device type
|
||||
if self.subsystem == 'usb-serial':
|
||||
self.usb_interface_path = os.path.dirname(self.device_path)
|
||||
elif self.subsystem == 'usb':
|
||||
self.usb_interface_path = self.device_path
|
||||
else:
|
||||
self.usb_interface_path = None
|
||||
# fill-in info for USB devices
|
||||
if self.usb_interface_path is not None:
|
||||
self.usb_device_path = os.path.dirname(self.usb_interface_path)
|
||||
|
||||
try:
|
||||
num_if = int(self.read_line(self.usb_device_path, 'bNumInterfaces'))
|
||||
except ValueError:
|
||||
num_if = 1
|
||||
|
||||
self.vid = int(self.read_line(self.usb_device_path, 'idVendor'), 16)
|
||||
self.pid = int(self.read_line(self.usb_device_path, 'idProduct'), 16)
|
||||
self.serial_number = self.read_line(self.usb_device_path, 'serial')
|
||||
if num_if > 1: # multi interface devices like FT4232
|
||||
self.location = os.path.basename(self.usb_interface_path)
|
||||
else:
|
||||
self.location = os.path.basename(self.usb_device_path)
|
||||
|
||||
self.manufacturer = self.read_line(self.usb_device_path, 'manufacturer')
|
||||
self.product = self.read_line(self.usb_device_path, 'product')
|
||||
self.interface = self.read_line(self.usb_interface_path, 'interface')
|
||||
|
||||
if self.subsystem in ('usb', 'usb-serial'):
|
||||
self.apply_usb_info()
|
||||
#~ elif self.subsystem in ('pnp', 'amba'): # PCI based devices, raspi
|
||||
elif self.subsystem == 'pnp': # PCI based devices
|
||||
self.description = self.name
|
||||
self.hwid = self.read_line(self.device_path, 'id')
|
||||
elif self.subsystem == 'amba': # raspi
|
||||
self.description = self.name
|
||||
self.hwid = os.path.basename(self.device_path)
|
||||
|
||||
if is_link:
|
||||
self.hwid += ' LINK={}'.format(device)
|
||||
|
||||
def read_line(self, *args):
|
||||
"""\
|
||||
Helper function to read a single line from a file.
|
||||
One or more parameters are allowed, they are joined with os.path.join.
|
||||
Returns None on errors..
|
||||
"""
|
||||
try:
|
||||
with open(os.path.join(*args)) as f:
|
||||
line = f.readline().strip()
|
||||
return line
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
|
||||
def comports(include_links=False):
|
||||
devices = glob.glob('/dev/ttyS*') # built-in serial ports
|
||||
devices.extend(glob.glob('/dev/ttyUSB*')) # usb-serial with own driver
|
||||
devices.extend(glob.glob('/dev/ttyXRUSB*')) # xr-usb-serial port exar (DELL Edge 3001)
|
||||
devices.extend(glob.glob('/dev/ttyACM*')) # usb-serial with CDC-ACM profile
|
||||
devices.extend(glob.glob('/dev/ttyAMA*')) # ARM internal port (raspi)
|
||||
devices.extend(glob.glob('/dev/rfcomm*')) # BT serial devices
|
||||
devices.extend(glob.glob('/dev/ttyAP*')) # Advantech multi-port serial controllers
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [info
|
||||
for info in [SysFS(d) for d in devices]
|
||||
if info.subsystem != "platform"] # hide non-present internal serial ports
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
for info in sorted(comports()):
|
||||
print("{0}: {0.subsystem}".format(info))
|
||||
@@ -1,299 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This is a module that gathers a list of serial ports including details on OSX
|
||||
#
|
||||
# code originally from https://github.com/makerbot/pyserial/tree/master/serial/tools
|
||||
# with contributions from cibomahto, dgs3, FarMcKon, tedbrandston
|
||||
# and modifications by cliechti, hoihu, hardkrash
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2013-2020
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
# List all of the callout devices in OS/X by querying IOKit.
|
||||
|
||||
# See the following for a reference of how to do this:
|
||||
# http://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/WorkingWSerial/WWSerial_SerialDevs/SerialDevices.html#//apple_ref/doc/uid/TP30000384-CIHGEAFD
|
||||
|
||||
# More help from darwin_hid.py
|
||||
|
||||
# Also see the 'IORegistryExplorer' for an idea of what we are actually searching
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import ctypes
|
||||
|
||||
from serial.tools import list_ports_common
|
||||
|
||||
iokit = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/IOKit.framework/IOKit')
|
||||
cf = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation')
|
||||
|
||||
# kIOMasterPortDefault is no longer exported in BigSur but no biggie, using NULL works just the same
|
||||
kIOMasterPortDefault = 0 # WAS: ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault")
|
||||
kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault")
|
||||
|
||||
kCFStringEncodingMacRoman = 0
|
||||
kCFStringEncodingUTF8 = 0x08000100
|
||||
|
||||
# defined in `IOKit/usb/USBSpec.h`
|
||||
kUSBVendorString = 'USB Vendor Name'
|
||||
kUSBSerialNumberString = 'USB Serial Number'
|
||||
|
||||
# `io_name_t` defined as `typedef char io_name_t[128];`
|
||||
# in `device/device_types.h`
|
||||
io_name_size = 128
|
||||
|
||||
# defined in `mach/kern_return.h`
|
||||
KERN_SUCCESS = 0
|
||||
# kern_return_t defined as `typedef int kern_return_t;` in `mach/i386/kern_return.h`
|
||||
kern_return_t = ctypes.c_int
|
||||
|
||||
iokit.IOServiceMatching.restype = ctypes.c_void_p
|
||||
|
||||
iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||
iokit.IOServiceGetMatchingServices.restype = kern_return_t
|
||||
|
||||
iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||
iokit.IOServiceGetMatchingServices.restype = kern_return_t
|
||||
|
||||
iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32]
|
||||
iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p
|
||||
|
||||
iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||
iokit.IORegistryEntryGetPath.restype = kern_return_t
|
||||
|
||||
iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||
iokit.IORegistryEntryGetName.restype = kern_return_t
|
||||
|
||||
iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||
iokit.IOObjectGetClass.restype = kern_return_t
|
||||
|
||||
iokit.IOObjectRelease.argtypes = [ctypes.c_void_p]
|
||||
|
||||
|
||||
cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32]
|
||||
cf.CFStringCreateWithCString.restype = ctypes.c_void_p
|
||||
|
||||
cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
|
||||
cf.CFStringGetCStringPtr.restype = ctypes.c_char_p
|
||||
|
||||
cf.CFStringGetCString.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ctypes.c_uint32]
|
||||
cf.CFStringGetCString.restype = ctypes.c_bool
|
||||
|
||||
cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p]
|
||||
cf.CFNumberGetValue.restype = ctypes.c_void_p
|
||||
|
||||
# void CFRelease ( CFTypeRef cf );
|
||||
cf.CFRelease.argtypes = [ctypes.c_void_p]
|
||||
cf.CFRelease.restype = None
|
||||
|
||||
# CFNumber type defines
|
||||
kCFNumberSInt8Type = 1
|
||||
kCFNumberSInt16Type = 2
|
||||
kCFNumberSInt32Type = 3
|
||||
kCFNumberSInt64Type = 4
|
||||
|
||||
|
||||
def get_string_property(device_type, property):
|
||||
"""
|
||||
Search the given device for the specified string property
|
||||
|
||||
@param device_type Type of Device
|
||||
@param property String to search for
|
||||
@return Python string containing the value, or None if not found.
|
||||
"""
|
||||
key = cf.CFStringCreateWithCString(
|
||||
kCFAllocatorDefault,
|
||||
property.encode("utf-8"),
|
||||
kCFStringEncodingUTF8)
|
||||
|
||||
CFContainer = iokit.IORegistryEntryCreateCFProperty(
|
||||
device_type,
|
||||
key,
|
||||
kCFAllocatorDefault,
|
||||
0)
|
||||
output = None
|
||||
|
||||
if CFContainer:
|
||||
output = cf.CFStringGetCStringPtr(CFContainer, 0)
|
||||
if output is not None:
|
||||
output = output.decode('utf-8')
|
||||
else:
|
||||
buffer = ctypes.create_string_buffer(io_name_size);
|
||||
success = cf.CFStringGetCString(CFContainer, ctypes.byref(buffer), io_name_size, kCFStringEncodingUTF8)
|
||||
if success:
|
||||
output = buffer.value.decode('utf-8')
|
||||
cf.CFRelease(CFContainer)
|
||||
return output
|
||||
|
||||
|
||||
def get_int_property(device_type, property, cf_number_type):
|
||||
"""
|
||||
Search the given device for the specified string property
|
||||
|
||||
@param device_type Device to search
|
||||
@param property String to search for
|
||||
@param cf_number_type CFType number
|
||||
|
||||
@return Python string containing the value, or None if not found.
|
||||
"""
|
||||
key = cf.CFStringCreateWithCString(
|
||||
kCFAllocatorDefault,
|
||||
property.encode("utf-8"),
|
||||
kCFStringEncodingUTF8)
|
||||
|
||||
CFContainer = iokit.IORegistryEntryCreateCFProperty(
|
||||
device_type,
|
||||
key,
|
||||
kCFAllocatorDefault,
|
||||
0)
|
||||
|
||||
if CFContainer:
|
||||
if (cf_number_type == kCFNumberSInt32Type):
|
||||
number = ctypes.c_uint32()
|
||||
elif (cf_number_type == kCFNumberSInt16Type):
|
||||
number = ctypes.c_uint16()
|
||||
cf.CFNumberGetValue(CFContainer, cf_number_type, ctypes.byref(number))
|
||||
cf.CFRelease(CFContainer)
|
||||
return number.value
|
||||
return None
|
||||
|
||||
def IORegistryEntryGetName(device):
|
||||
devicename = ctypes.create_string_buffer(io_name_size);
|
||||
res = iokit.IORegistryEntryGetName(device, ctypes.byref(devicename))
|
||||
if res != KERN_SUCCESS:
|
||||
return None
|
||||
# this works in python2 but may not be valid. Also I don't know if
|
||||
# this encoding is guaranteed. It may be dependent on system locale.
|
||||
return devicename.value.decode('utf-8')
|
||||
|
||||
def IOObjectGetClass(device):
|
||||
classname = ctypes.create_string_buffer(io_name_size)
|
||||
iokit.IOObjectGetClass(device, ctypes.byref(classname))
|
||||
return classname.value
|
||||
|
||||
def GetParentDeviceByType(device, parent_type):
|
||||
""" Find the first parent of a device that implements the parent_type
|
||||
@param IOService Service to inspect
|
||||
@return Pointer to the parent type, or None if it was not found.
|
||||
"""
|
||||
# First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice.
|
||||
parent_type = parent_type.encode('utf-8')
|
||||
while IOObjectGetClass(device) != parent_type:
|
||||
parent = ctypes.c_void_p()
|
||||
response = iokit.IORegistryEntryGetParentEntry(
|
||||
device,
|
||||
"IOService".encode("utf-8"),
|
||||
ctypes.byref(parent))
|
||||
# If we weren't able to find a parent for the device, we're done.
|
||||
if response != KERN_SUCCESS:
|
||||
return None
|
||||
device = parent
|
||||
return device
|
||||
|
||||
|
||||
def GetIOServicesByType(service_type):
|
||||
"""
|
||||
returns iterator over specified service_type
|
||||
"""
|
||||
serial_port_iterator = ctypes.c_void_p()
|
||||
|
||||
iokit.IOServiceGetMatchingServices(
|
||||
kIOMasterPortDefault,
|
||||
iokit.IOServiceMatching(service_type.encode('utf-8')),
|
||||
ctypes.byref(serial_port_iterator))
|
||||
|
||||
services = []
|
||||
while iokit.IOIteratorIsValid(serial_port_iterator):
|
||||
service = iokit.IOIteratorNext(serial_port_iterator)
|
||||
if not service:
|
||||
break
|
||||
services.append(service)
|
||||
iokit.IOObjectRelease(serial_port_iterator)
|
||||
return services
|
||||
|
||||
|
||||
def location_to_string(locationID):
|
||||
"""
|
||||
helper to calculate port and bus number from locationID
|
||||
"""
|
||||
loc = ['{}-'.format(locationID >> 24)]
|
||||
while locationID & 0xf00000:
|
||||
if len(loc) > 1:
|
||||
loc.append('.')
|
||||
loc.append('{}'.format((locationID >> 20) & 0xf))
|
||||
locationID <<= 4
|
||||
return ''.join(loc)
|
||||
|
||||
|
||||
class SuitableSerialInterface(object):
|
||||
pass
|
||||
|
||||
|
||||
def scan_interfaces():
|
||||
"""
|
||||
helper function to scan USB interfaces
|
||||
returns a list of SuitableSerialInterface objects with name and id attributes
|
||||
"""
|
||||
interfaces = []
|
||||
for service in GetIOServicesByType('IOSerialBSDClient'):
|
||||
device = get_string_property(service, "IOCalloutDevice")
|
||||
if device:
|
||||
usb_device = GetParentDeviceByType(service, "IOUSBInterface")
|
||||
if usb_device:
|
||||
name = get_string_property(usb_device, "USB Interface Name") or None
|
||||
locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) or ''
|
||||
i = SuitableSerialInterface()
|
||||
i.id = locationID
|
||||
i.name = name
|
||||
interfaces.append(i)
|
||||
return interfaces
|
||||
|
||||
|
||||
def search_for_locationID_in_interfaces(serial_interfaces, locationID):
|
||||
for interface in serial_interfaces:
|
||||
if (interface.id == locationID):
|
||||
return interface.name
|
||||
return None
|
||||
|
||||
|
||||
def comports(include_links=False):
|
||||
# XXX include_links is currently ignored. are links in /dev even supported here?
|
||||
# Scan for all iokit serial ports
|
||||
services = GetIOServicesByType('IOSerialBSDClient')
|
||||
ports = []
|
||||
serial_interfaces = scan_interfaces()
|
||||
for service in services:
|
||||
# First, add the callout device file.
|
||||
device = get_string_property(service, "IOCalloutDevice")
|
||||
if device:
|
||||
info = list_ports_common.ListPortInfo(device)
|
||||
# If the serial port is implemented by IOUSBDevice
|
||||
# NOTE IOUSBDevice was deprecated as of 10.11 and finally on Apple Silicon
|
||||
# devices has been completely removed. Thanks to @oskay for this patch.
|
||||
usb_device = GetParentDeviceByType(service, "IOUSBHostDevice")
|
||||
if not usb_device:
|
||||
usb_device = GetParentDeviceByType(service, "IOUSBDevice")
|
||||
if usb_device:
|
||||
# fetch some useful informations from properties
|
||||
info.vid = get_int_property(usb_device, "idVendor", kCFNumberSInt16Type)
|
||||
info.pid = get_int_property(usb_device, "idProduct", kCFNumberSInt16Type)
|
||||
info.serial_number = get_string_property(usb_device, kUSBSerialNumberString)
|
||||
# We know this is a usb device, so the
|
||||
# IORegistryEntryName should always be aliased to the
|
||||
# usb product name string descriptor.
|
||||
info.product = IORegistryEntryGetName(usb_device) or 'n/a'
|
||||
info.manufacturer = get_string_property(usb_device, kUSBVendorString)
|
||||
locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type)
|
||||
info.location = location_to_string(locationID)
|
||||
info.interface = search_for_locationID_in_interfaces(serial_interfaces, locationID)
|
||||
info.apply_usb_info()
|
||||
ports.append(info)
|
||||
return ports
|
||||
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
for port, desc, hwid in sorted(comports()):
|
||||
print("{}: {} [{}]".format(port, desc, hwid))
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This is a module that gathers a list of serial ports on POSIXy systems.
|
||||
# For some specific implementations, see also list_ports_linux, list_ports_osx
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""\
|
||||
The ``comports`` function is expected to return an iterable that yields tuples
|
||||
of 3 strings: port name, human readable description and a hardware ID.
|
||||
|
||||
As currently no method is known to get the second two strings easily, they are
|
||||
currently just identical to the port name.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import glob
|
||||
import sys
|
||||
import os
|
||||
from serial.tools import list_ports_common
|
||||
|
||||
# try to detect the OS so that a device can be selected...
|
||||
plat = sys.platform.lower()
|
||||
|
||||
if plat[:5] == 'linux': # Linux (confirmed) # noqa
|
||||
from serial.tools.list_ports_linux import comports
|
||||
|
||||
elif plat[:6] == 'darwin': # OS X (confirmed)
|
||||
from serial.tools.list_ports_osx import comports
|
||||
|
||||
elif plat == 'cygwin': # cygwin/win32
|
||||
# cygwin accepts /dev/com* in many contexts
|
||||
# (such as 'open' call, explicit 'ls'), but 'glob.glob'
|
||||
# and bare 'ls' do not; so use /dev/ttyS* instead
|
||||
def comports(include_links=False):
|
||||
devices = glob.glob('/dev/ttyS*')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:7] == 'openbsd': # OpenBSD
|
||||
def comports(include_links=False):
|
||||
devices = glob.glob('/dev/cua*')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:3] == 'bsd' or plat[:7] == 'freebsd':
|
||||
def comports(include_links=False):
|
||||
devices = glob.glob('/dev/cua*[!.init][!.lock]')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:6] == 'netbsd': # NetBSD
|
||||
def comports(include_links=False):
|
||||
"""scan for available ports. return a list of device names."""
|
||||
devices = glob.glob('/dev/dty*')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:4] == 'irix': # IRIX
|
||||
def comports(include_links=False):
|
||||
"""scan for available ports. return a list of device names."""
|
||||
devices = glob.glob('/dev/ttyf*')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:2] == 'hp': # HP-UX (not tested)
|
||||
def comports(include_links=False):
|
||||
"""scan for available ports. return a list of device names."""
|
||||
devices = glob.glob('/dev/tty*p0')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:5] == 'sunos': # Solaris/SunOS
|
||||
def comports(include_links=False):
|
||||
"""scan for available ports. return a list of device names."""
|
||||
devices = glob.glob('/dev/tty*c')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:3] == 'aix': # AIX
|
||||
def comports(include_links=False):
|
||||
"""scan for available ports. return a list of device names."""
|
||||
devices = glob.glob('/dev/tty*')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
else:
|
||||
# platform detection has failed...
|
||||
import serial
|
||||
sys.stderr.write("""\
|
||||
don't know how to enumerate ttys on this system.
|
||||
! I you know how the serial ports are named send this information to
|
||||
! the author of this module:
|
||||
|
||||
sys.platform = {!r}
|
||||
os.name = {!r}
|
||||
pySerial version = {}
|
||||
|
||||
also add the naming scheme of the serial ports and with a bit luck you can get
|
||||
this module running...
|
||||
""".format(sys.platform, os.name, serial.VERSION))
|
||||
raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
|
||||
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
for port, desc, hwid in sorted(comports()):
|
||||
print("{}: {} [{}]".format(port, desc, hwid))
|
||||
@@ -1,427 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# Enumerate serial ports on Windows including a human readable description
|
||||
# and hardware information.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2016 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# pylint: disable=invalid-name,too-few-public-methods
|
||||
import re
|
||||
import ctypes
|
||||
from ctypes.wintypes import BOOL
|
||||
from ctypes.wintypes import HWND
|
||||
from ctypes.wintypes import DWORD
|
||||
from ctypes.wintypes import WORD
|
||||
from ctypes.wintypes import LONG
|
||||
from ctypes.wintypes import ULONG
|
||||
from ctypes.wintypes import HKEY
|
||||
from ctypes.wintypes import BYTE
|
||||
import serial
|
||||
from serial.win32 import ULONG_PTR
|
||||
from serial.tools import list_ports_common
|
||||
|
||||
|
||||
def ValidHandle(value, func, arguments):
|
||||
if value == 0:
|
||||
raise ctypes.WinError()
|
||||
return value
|
||||
|
||||
|
||||
NULL = 0
|
||||
HDEVINFO = ctypes.c_void_p
|
||||
LPCTSTR = ctypes.c_wchar_p
|
||||
PCTSTR = ctypes.c_wchar_p
|
||||
PTSTR = ctypes.c_wchar_p
|
||||
LPDWORD = PDWORD = ctypes.POINTER(DWORD)
|
||||
#~ LPBYTE = PBYTE = ctypes.POINTER(BYTE)
|
||||
LPBYTE = PBYTE = ctypes.c_void_p # XXX avoids error about types
|
||||
|
||||
ACCESS_MASK = DWORD
|
||||
REGSAM = ACCESS_MASK
|
||||
|
||||
|
||||
class GUID(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('Data1', DWORD),
|
||||
('Data2', WORD),
|
||||
('Data3', WORD),
|
||||
('Data4', BYTE * 8),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return "{{{:08x}-{:04x}-{:04x}-{}-{}}}".format(
|
||||
self.Data1,
|
||||
self.Data2,
|
||||
self.Data3,
|
||||
''.join(["{:02x}".format(d) for d in self.Data4[:2]]),
|
||||
''.join(["{:02x}".format(d) for d in self.Data4[2:]]),
|
||||
)
|
||||
|
||||
|
||||
class SP_DEVINFO_DATA(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('cbSize', DWORD),
|
||||
('ClassGuid', GUID),
|
||||
('DevInst', DWORD),
|
||||
('Reserved', ULONG_PTR),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return "ClassGuid:{} DevInst:{}".format(self.ClassGuid, self.DevInst)
|
||||
|
||||
|
||||
PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA)
|
||||
|
||||
PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p
|
||||
|
||||
setupapi = ctypes.windll.LoadLibrary("setupapi")
|
||||
SetupDiDestroyDeviceInfoList = setupapi.SetupDiDestroyDeviceInfoList
|
||||
SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO]
|
||||
SetupDiDestroyDeviceInfoList.restype = BOOL
|
||||
|
||||
SetupDiClassGuidsFromName = setupapi.SetupDiClassGuidsFromNameW
|
||||
SetupDiClassGuidsFromName.argtypes = [PCTSTR, ctypes.POINTER(GUID), DWORD, PDWORD]
|
||||
SetupDiClassGuidsFromName.restype = BOOL
|
||||
|
||||
SetupDiEnumDeviceInfo = setupapi.SetupDiEnumDeviceInfo
|
||||
SetupDiEnumDeviceInfo.argtypes = [HDEVINFO, DWORD, PSP_DEVINFO_DATA]
|
||||
SetupDiEnumDeviceInfo.restype = BOOL
|
||||
|
||||
SetupDiGetClassDevs = setupapi.SetupDiGetClassDevsW
|
||||
SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD]
|
||||
SetupDiGetClassDevs.restype = HDEVINFO
|
||||
SetupDiGetClassDevs.errcheck = ValidHandle
|
||||
|
||||
SetupDiGetDeviceRegistryProperty = setupapi.SetupDiGetDeviceRegistryPropertyW
|
||||
SetupDiGetDeviceRegistryProperty.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, PBYTE, DWORD, PDWORD]
|
||||
SetupDiGetDeviceRegistryProperty.restype = BOOL
|
||||
|
||||
SetupDiGetDeviceInstanceId = setupapi.SetupDiGetDeviceInstanceIdW
|
||||
SetupDiGetDeviceInstanceId.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, PTSTR, DWORD, PDWORD]
|
||||
SetupDiGetDeviceInstanceId.restype = BOOL
|
||||
|
||||
SetupDiOpenDevRegKey = setupapi.SetupDiOpenDevRegKey
|
||||
SetupDiOpenDevRegKey.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, DWORD, DWORD, REGSAM]
|
||||
SetupDiOpenDevRegKey.restype = HKEY
|
||||
|
||||
advapi32 = ctypes.windll.LoadLibrary("Advapi32")
|
||||
RegCloseKey = advapi32.RegCloseKey
|
||||
RegCloseKey.argtypes = [HKEY]
|
||||
RegCloseKey.restype = LONG
|
||||
|
||||
RegQueryValueEx = advapi32.RegQueryValueExW
|
||||
RegQueryValueEx.argtypes = [HKEY, LPCTSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD]
|
||||
RegQueryValueEx.restype = LONG
|
||||
|
||||
cfgmgr32 = ctypes.windll.LoadLibrary("Cfgmgr32")
|
||||
CM_Get_Parent = cfgmgr32.CM_Get_Parent
|
||||
CM_Get_Parent.argtypes = [PDWORD, DWORD, ULONG]
|
||||
CM_Get_Parent.restype = LONG
|
||||
|
||||
CM_Get_Device_IDW = cfgmgr32.CM_Get_Device_IDW
|
||||
CM_Get_Device_IDW.argtypes = [DWORD, PTSTR, ULONG, ULONG]
|
||||
CM_Get_Device_IDW.restype = LONG
|
||||
|
||||
CM_MapCrToWin32Err = cfgmgr32.CM_MapCrToWin32Err
|
||||
CM_MapCrToWin32Err.argtypes = [DWORD, DWORD]
|
||||
CM_MapCrToWin32Err.restype = DWORD
|
||||
|
||||
|
||||
DIGCF_PRESENT = 2
|
||||
DIGCF_DEVICEINTERFACE = 16
|
||||
INVALID_HANDLE_VALUE = 0
|
||||
ERROR_INSUFFICIENT_BUFFER = 122
|
||||
ERROR_NOT_FOUND = 1168
|
||||
SPDRP_HARDWAREID = 1
|
||||
SPDRP_FRIENDLYNAME = 12
|
||||
SPDRP_LOCATION_PATHS = 35
|
||||
SPDRP_MFG = 11
|
||||
DICS_FLAG_GLOBAL = 1
|
||||
DIREG_DEV = 0x00000001
|
||||
KEY_READ = 0x20019
|
||||
|
||||
|
||||
MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH = 5
|
||||
|
||||
|
||||
def get_parent_serial_number(child_devinst, child_vid, child_pid, depth=0, last_serial_number=None):
|
||||
""" Get the serial number of the parent of a device.
|
||||
|
||||
Args:
|
||||
child_devinst: The device instance handle to get the parent serial number of.
|
||||
child_vid: The vendor ID of the child device.
|
||||
child_pid: The product ID of the child device.
|
||||
depth: The current iteration depth of the USB device tree.
|
||||
"""
|
||||
|
||||
# If the traversal depth is beyond the max, abandon attempting to find the serial number.
|
||||
if depth > MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH:
|
||||
return '' if not last_serial_number else last_serial_number
|
||||
|
||||
# Get the parent device instance.
|
||||
devinst = DWORD()
|
||||
ret = CM_Get_Parent(ctypes.byref(devinst), child_devinst, 0)
|
||||
|
||||
if ret:
|
||||
win_error = CM_MapCrToWin32Err(DWORD(ret), DWORD(0))
|
||||
|
||||
# If there is no parent available, the child was the root device. We cannot traverse
|
||||
# further.
|
||||
if win_error == ERROR_NOT_FOUND:
|
||||
return '' if not last_serial_number else last_serial_number
|
||||
|
||||
raise ctypes.WinError(win_error)
|
||||
|
||||
# Get the ID of the parent device and parse it for vendor ID, product ID, and serial number.
|
||||
parentHardwareID = ctypes.create_unicode_buffer(250)
|
||||
|
||||
ret = CM_Get_Device_IDW(
|
||||
devinst,
|
||||
parentHardwareID,
|
||||
ctypes.sizeof(parentHardwareID) - 1,
|
||||
0)
|
||||
|
||||
if ret:
|
||||
raise ctypes.WinError(CM_MapCrToWin32Err(DWORD(ret), DWORD(0)))
|
||||
|
||||
parentHardwareID_str = parentHardwareID.value
|
||||
m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?',
|
||||
parentHardwareID_str,
|
||||
re.I)
|
||||
|
||||
# return early if we have no matches (likely malformed serial, traversed too far)
|
||||
if not m:
|
||||
return '' if not last_serial_number else last_serial_number
|
||||
|
||||
vid = None
|
||||
pid = None
|
||||
serial_number = None
|
||||
if m.group(1):
|
||||
vid = int(m.group(1), 16)
|
||||
if m.group(3):
|
||||
pid = int(m.group(3), 16)
|
||||
if m.group(7):
|
||||
serial_number = m.group(7)
|
||||
|
||||
# store what we found as a fallback for malformed serial values up the chain
|
||||
found_serial_number = serial_number
|
||||
|
||||
# Check that the USB serial number only contains alpha-numeric characters. It may be a windows
|
||||
# device ID (ephemeral ID).
|
||||
if serial_number and not re.match(r'^\w+$', serial_number):
|
||||
serial_number = None
|
||||
|
||||
if not vid or not pid:
|
||||
# If pid and vid are not available at this device level, continue to the parent.
|
||||
return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number)
|
||||
|
||||
if pid != child_pid or vid != child_vid:
|
||||
# If the VID or PID has changed, we are no longer looking at the same physical device. The
|
||||
# serial number is unknown.
|
||||
return '' if not last_serial_number else last_serial_number
|
||||
|
||||
# In this case, the vid and pid of the parent device are identical to the child. However, if
|
||||
# there still isn't a serial number available, continue to the next parent.
|
||||
if not serial_number:
|
||||
return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number)
|
||||
|
||||
# Finally, the VID and PID are identical to the child and a serial number is present, so return
|
||||
# it.
|
||||
return serial_number
|
||||
|
||||
|
||||
def iterate_comports():
|
||||
"""Return a generator that yields descriptions for serial ports"""
|
||||
PortsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough...
|
||||
ports_guids_size = DWORD()
|
||||
if not SetupDiClassGuidsFromName(
|
||||
"Ports",
|
||||
PortsGUIDs,
|
||||
ctypes.sizeof(PortsGUIDs),
|
||||
ctypes.byref(ports_guids_size)):
|
||||
raise ctypes.WinError()
|
||||
|
||||
ModemsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough...
|
||||
modems_guids_size = DWORD()
|
||||
if not SetupDiClassGuidsFromName(
|
||||
"Modem",
|
||||
ModemsGUIDs,
|
||||
ctypes.sizeof(ModemsGUIDs),
|
||||
ctypes.byref(modems_guids_size)):
|
||||
raise ctypes.WinError()
|
||||
|
||||
GUIDs = PortsGUIDs[:ports_guids_size.value] + ModemsGUIDs[:modems_guids_size.value]
|
||||
|
||||
# repeat for all possible GUIDs
|
||||
for index in range(len(GUIDs)):
|
||||
bInterfaceNumber = None
|
||||
g_hdi = SetupDiGetClassDevs(
|
||||
ctypes.byref(GUIDs[index]),
|
||||
None,
|
||||
NULL,
|
||||
DIGCF_PRESENT) # was DIGCF_PRESENT|DIGCF_DEVICEINTERFACE which misses CDC ports
|
||||
|
||||
devinfo = SP_DEVINFO_DATA()
|
||||
devinfo.cbSize = ctypes.sizeof(devinfo)
|
||||
index = 0
|
||||
while SetupDiEnumDeviceInfo(g_hdi, index, ctypes.byref(devinfo)):
|
||||
index += 1
|
||||
|
||||
# get the real com port name
|
||||
hkey = SetupDiOpenDevRegKey(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
DICS_FLAG_GLOBAL,
|
||||
0,
|
||||
DIREG_DEV, # DIREG_DRV for SW info
|
||||
KEY_READ)
|
||||
port_name_buffer = ctypes.create_unicode_buffer(250)
|
||||
port_name_length = ULONG(ctypes.sizeof(port_name_buffer))
|
||||
RegQueryValueEx(
|
||||
hkey,
|
||||
"PortName",
|
||||
None,
|
||||
None,
|
||||
ctypes.byref(port_name_buffer),
|
||||
ctypes.byref(port_name_length))
|
||||
RegCloseKey(hkey)
|
||||
|
||||
# unfortunately does this method also include parallel ports.
|
||||
# we could check for names starting with COM or just exclude LPT
|
||||
# and hope that other "unknown" names are serial ports...
|
||||
if port_name_buffer.value.startswith('LPT'):
|
||||
continue
|
||||
|
||||
# hardware ID
|
||||
szHardwareID = ctypes.create_unicode_buffer(250)
|
||||
# try to get ID that includes serial number
|
||||
if not SetupDiGetDeviceInstanceId(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
#~ ctypes.byref(szHardwareID),
|
||||
szHardwareID,
|
||||
ctypes.sizeof(szHardwareID) - 1,
|
||||
None):
|
||||
# fall back to more generic hardware ID if that would fail
|
||||
if not SetupDiGetDeviceRegistryProperty(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
SPDRP_HARDWAREID,
|
||||
None,
|
||||
ctypes.byref(szHardwareID),
|
||||
ctypes.sizeof(szHardwareID) - 1,
|
||||
None):
|
||||
# Ignore ERROR_INSUFFICIENT_BUFFER
|
||||
if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
|
||||
raise ctypes.WinError()
|
||||
# stringify
|
||||
szHardwareID_str = szHardwareID.value
|
||||
|
||||
info = list_ports_common.ListPortInfo(port_name_buffer.value, skip_link_detection=True)
|
||||
|
||||
# in case of USB, make a more readable string, similar to that form
|
||||
# that we also generate on other platforms
|
||||
if szHardwareID_str.startswith('USB'):
|
||||
m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?', szHardwareID_str, re.I)
|
||||
if m:
|
||||
info.vid = int(m.group(1), 16)
|
||||
if m.group(3):
|
||||
info.pid = int(m.group(3), 16)
|
||||
if m.group(5):
|
||||
bInterfaceNumber = int(m.group(5))
|
||||
|
||||
# Check that the USB serial number only contains alpha-numeric characters. It
|
||||
# may be a windows device ID (ephemeral ID) for composite devices.
|
||||
if m.group(7) and re.match(r'^\w+$', m.group(7)):
|
||||
info.serial_number = m.group(7)
|
||||
else:
|
||||
info.serial_number = get_parent_serial_number(devinfo.DevInst, info.vid, info.pid)
|
||||
|
||||
# calculate a location string
|
||||
loc_path_str = ctypes.create_unicode_buffer(250)
|
||||
if SetupDiGetDeviceRegistryProperty(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
SPDRP_LOCATION_PATHS,
|
||||
None,
|
||||
ctypes.byref(loc_path_str),
|
||||
ctypes.sizeof(loc_path_str) - 1,
|
||||
None):
|
||||
m = re.finditer(r'USBROOT\((\w+)\)|#USB\((\w+)\)', loc_path_str.value)
|
||||
location = []
|
||||
for g in m:
|
||||
if g.group(1):
|
||||
location.append('{:d}'.format(int(g.group(1)) + 1))
|
||||
else:
|
||||
if len(location) > 1:
|
||||
location.append('.')
|
||||
else:
|
||||
location.append('-')
|
||||
location.append(g.group(2))
|
||||
if bInterfaceNumber is not None:
|
||||
location.append(':{}.{}'.format(
|
||||
'x', # XXX how to determine correct bConfigurationValue?
|
||||
bInterfaceNumber))
|
||||
if location:
|
||||
info.location = ''.join(location)
|
||||
info.hwid = info.usb_info()
|
||||
elif szHardwareID_str.startswith('FTDIBUS'):
|
||||
m = re.search(r'VID_([0-9a-f]{4})\+PID_([0-9a-f]{4})(\+(\w+))?', szHardwareID_str, re.I)
|
||||
if m:
|
||||
info.vid = int(m.group(1), 16)
|
||||
info.pid = int(m.group(2), 16)
|
||||
if m.group(4):
|
||||
info.serial_number = m.group(4)
|
||||
# USB location is hidden by FDTI driver :(
|
||||
info.hwid = info.usb_info()
|
||||
else:
|
||||
info.hwid = szHardwareID_str
|
||||
|
||||
# friendly name
|
||||
szFriendlyName = ctypes.create_unicode_buffer(250)
|
||||
if SetupDiGetDeviceRegistryProperty(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
SPDRP_FRIENDLYNAME,
|
||||
#~ SPDRP_DEVICEDESC,
|
||||
None,
|
||||
ctypes.byref(szFriendlyName),
|
||||
ctypes.sizeof(szFriendlyName) - 1,
|
||||
None):
|
||||
info.description = szFriendlyName.value
|
||||
#~ else:
|
||||
# Ignore ERROR_INSUFFICIENT_BUFFER
|
||||
#~ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
|
||||
#~ raise IOError("failed to get details for %s (%s)" % (devinfo, szHardwareID.value))
|
||||
# ignore errors and still include the port in the list, friendly name will be same as port name
|
||||
|
||||
# manufacturer
|
||||
szManufacturer = ctypes.create_unicode_buffer(250)
|
||||
if SetupDiGetDeviceRegistryProperty(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
SPDRP_MFG,
|
||||
#~ SPDRP_DEVICEDESC,
|
||||
None,
|
||||
ctypes.byref(szManufacturer),
|
||||
ctypes.sizeof(szManufacturer) - 1,
|
||||
None):
|
||||
info.manufacturer = szManufacturer.value
|
||||
yield info
|
||||
SetupDiDestroyDeviceInfoList(g_hdi)
|
||||
|
||||
|
||||
def comports(include_links=False):
|
||||
"""Return a list of info objects about serial ports"""
|
||||
return list(iterate_comports())
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
for port, desc, hwid in sorted(comports()):
|
||||
print("{}: {} [{}]".format(port, desc, hwid))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# This module implements a special URL handler that allows selecting an
|
||||
# alternate implementation provided by some backends.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# URL format: alt://port[?option[=value][&option[=value]]]
|
||||
# options:
|
||||
# - class=X used class named X instead of Serial
|
||||
#
|
||||
# example:
|
||||
# use poll based implementation on Posix (Linux):
|
||||
# python -m serial.tools.miniterm alt:///dev/ttyUSB0?class=PosixPollSerial
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
try:
|
||||
import urlparse
|
||||
except ImportError:
|
||||
import urllib.parse as urlparse
|
||||
|
||||
import serial
|
||||
|
||||
|
||||
def serial_class_for_url(url):
|
||||
"""extract host and port from an URL string"""
|
||||
parts = urlparse.urlsplit(url)
|
||||
if parts.scheme != 'alt':
|
||||
raise serial.SerialException(
|
||||
'expected a string in the form "alt://port[?option[=value][&option[=value]]]": '
|
||||
'not starting with alt:// ({!r})'.format(parts.scheme))
|
||||
class_name = 'Serial'
|
||||
try:
|
||||
for option, values in urlparse.parse_qs(parts.query, True).items():
|
||||
if option == 'class':
|
||||
class_name = values[0]
|
||||
else:
|
||||
raise ValueError('unknown option: {!r}'.format(option))
|
||||
except ValueError as e:
|
||||
raise serial.SerialException(
|
||||
'expected a string in the form '
|
||||
'"alt://port[?option[=value][&option[=value]]]": {!r}'.format(e))
|
||||
if not hasattr(serial, class_name):
|
||||
raise ValueError('unknown class: {!r}'.format(class_name))
|
||||
cls = getattr(serial, class_name)
|
||||
if not issubclass(cls, serial.Serial):
|
||||
raise ValueError('class {!r} is not an instance of Serial'.format(class_name))
|
||||
return (''.join([parts.netloc, parts.path]), cls)
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
if __name__ == '__main__':
|
||||
s = serial.serial_for_url('alt:///dev/ttyS0?class=PosixPollSerial')
|
||||
print(s)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user