Merge remote-tracking branch 'refs/remotes/Cat/master'

# Conflicts:
#	scenes/настройки/настройки.gd
This commit is contained in:
lepshiy
2025-02-25 16:11:20 +03:00
144 changed files with 587 additions and 27439 deletions

3
connect-to-uf.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
xfreerdp /u:user /p:12345678 /v:10.1.1.2 /f

View File

@@ -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()

View File

@@ -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

View File

@@ -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())

View File

@@ -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",
]

View File

@@ -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",
]

View File

@@ -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")

View File

@@ -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

View File

@@ -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)

View File

@@ -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"
]

View File

@@ -1 +0,0 @@
from __future__ import absolute_import, unicode_literals

View File

@@ -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))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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"
]

View File

@@ -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

View File

@@ -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' ]

View File

@@ -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"
]

View File

@@ -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"
]

View File

@@ -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

View File

@@ -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",
]

View File

@@ -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",
]

View File

@@ -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())

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
]

View File

@@ -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",
]

View File

@@ -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')

View File

@@ -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"
]

View File

@@ -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']

View File

@@ -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",
]

View File

@@ -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)

View File

@@ -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__

View File

@@ -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__

View File

@@ -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__

View File

@@ -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__

View File

@@ -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__

View File

@@ -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',
]

View File

@@ -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)

View File

@@ -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",
]

View File

@@ -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",
]

View File

@@ -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"]

View File

@@ -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',
]

View File

@@ -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",
]

View File

@@ -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"
]

View File

@@ -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
[![asciicast](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o.png)](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o)
[![asciicast](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI.png)](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI)

View File

@@ -1,7 +0,0 @@
"""
Pymodbus REPL Module.
Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved.
"""
from __future__ import absolute_import, unicode_literals

View File

@@ -1,4 +0,0 @@
"""
Copyright (c) 2020 by RiptideIO
All rights reserved.
"""

View File

@@ -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)

View File

@@ -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))

View File

@@ -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()

View File

@@ -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)

View File

@@ -1,4 +0,0 @@
"""
Copyright (c) 2020 by RiptideIO
All rights reserved.
"""

View File

@@ -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=&lt;int&gt; \n\t\tdelay_by=&lt;in seconds&gt; \n\t\t" \
"clear_after=&lt;clear after n messages int&gt;" \
"\n\t\tdata_len=&lt;length of stray data (int)&gt;\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)

View File

@@ -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()

View File

@@ -1,2 +0,0 @@
'''
'''

View File

@@ -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"
]

View File

@@ -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"
]

View File

@@ -1,4 +0,0 @@
"""
Copyright (c) 2020 by RiptideIO
All rights reserved.
"""

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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__

View File

@@ -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"
]

View File

@@ -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",
]

View File

@@ -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'
]

View File

@@ -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"]

View File

@@ -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

View File

@@ -1,3 +0,0 @@
from .tools import miniterm
miniterm.main()

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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))

View File

@@ -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)

View File

@@ -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()

View File

@@ -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,
)

View File

@@ -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()

View File

@@ -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'))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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

View File

@@ -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