907 lines
35 KiB
Python
907 lines
35 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Modbus TestKit: Implementation of Modbus protocol in python
|
|
|
|
(C)2009 - Luc Jean - luc.jean@gmail.com
|
|
(C)2009 - Apidev - http://www.apidev.fr
|
|
|
|
This is distributed under GNU LGPL license, see license.txt
|
|
|
|
History:
|
|
2010/01/08 - RD: Update master.execute(..) to calculate lengths automatically based on requested command
|
|
"""
|
|
|
|
from __future__ import with_statement
|
|
import struct
|
|
import logging
|
|
import defines
|
|
import threading
|
|
import utils
|
|
from utils import threadsafe_function
|
|
from hooks import call_hooks
|
|
|
|
#modbus_tk is using the python logging mechanism
|
|
#you can define this logger in your app in order to see its prints logs
|
|
LOGGER = logging.getLogger("modbus_tk")
|
|
|
|
#-------------------------------------------------------------------------------
|
|
#Exceptions
|
|
#-------------------------------------------------------------------------------
|
|
|
|
class ModbusError(Exception):
|
|
"""Exception raised when the modbus slave returns an error"""
|
|
|
|
def __init__(self, exception_code, value=""):
|
|
"""constructor: set the exception code returned by the slave"""
|
|
if not value:
|
|
value = "Modbus Error: Exception code = %d" % (exception_code)
|
|
Exception.__init__(self, value)
|
|
self._exception_code = exception_code
|
|
|
|
def get_exception_code(self):
|
|
"""return the exception code returned by the slave (see defines)"""
|
|
return self._exception_code
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class ModbusFunctionNotSupportedError(Exception):
|
|
"""
|
|
Exception raised when calling a modbus function not supported by modbus_tk
|
|
"""
|
|
pass
|
|
|
|
class DuplicatedKeyError(Exception):
|
|
"""
|
|
Exception raised when trying to add an object with a key that is already
|
|
used for another object
|
|
"""
|
|
pass
|
|
|
|
class MissingKeyError(Exception):
|
|
"""
|
|
Exception raised when trying to get an object with a key that doesn't exist
|
|
"""
|
|
pass
|
|
|
|
class InvalidModbusBlockError(Exception):
|
|
"""Exception raised when a modbus block is not valid"""
|
|
pass
|
|
|
|
class InvalidArgumentError(Exception):
|
|
"""
|
|
Exception raised when one argument of a function doesn't meet
|
|
what is expected
|
|
"""
|
|
pass
|
|
|
|
class OverlapModbusBlockError(Exception):
|
|
"""
|
|
Exception raised when adding modbus block on a memory address
|
|
range already in use
|
|
"""
|
|
pass
|
|
|
|
class OutOfModbusBlockError(Exception):
|
|
"""Exception raised when accessing out of a modbus block"""
|
|
pass
|
|
|
|
class ModbusInvalidResponseError(Exception):
|
|
"""
|
|
Exception raised when the response sent by the slave doesn't fit
|
|
with the expected format
|
|
"""
|
|
pass
|
|
|
|
class ModbusInvalidRequestError(Exception):
|
|
"""
|
|
Exception raised when the request by the master doesn't fit
|
|
with the expected format
|
|
"""
|
|
pass
|
|
|
|
class ModbusNotConnectedError(Exception):
|
|
"""
|
|
Exception raised when the master is not connected to the slave
|
|
"""
|
|
pass
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class Query:
|
|
"""
|
|
Interface to be implemented in subclass for every specific modbus MAC layer
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Constructor"""
|
|
pass
|
|
|
|
def build_request(self, pdu, slave):
|
|
"""
|
|
Get the modbus application protocol request pdu and slave id
|
|
Encapsulate with MAC layer information
|
|
Returns a string
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def parse_response(self, response):
|
|
"""
|
|
Get the full response and extract the modbus application protocol
|
|
response pdu
|
|
Returns a string
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def parse_request(self, request):
|
|
"""
|
|
Get the full request and extract the modbus application protocol
|
|
request pdu
|
|
Returns a string and the slave id
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def build_response(self, response_pdu):
|
|
"""
|
|
Get the modbus application protocol response pdu and encapsulate with
|
|
MAC layer information
|
|
Returns a string
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class Master:
|
|
"""
|
|
This class implements the Modbus Application protocol for a master
|
|
To be subclassed with a class implementing the MAC layer
|
|
"""
|
|
|
|
def __init__(self, timeout_in_sec, hooks=None):
|
|
"""Constructor: can define a timeout"""
|
|
self._timeout = timeout_in_sec
|
|
self._verbose = False
|
|
self._is_opened = False
|
|
|
|
def __del__(self):
|
|
"""Destructor: close the connection"""
|
|
self.close()
|
|
|
|
def set_verbose(self, verbose):
|
|
"""print some more log prints for debug purpose"""
|
|
self._verbose = verbose
|
|
|
|
def open(self):
|
|
"""open the communication with the slave"""
|
|
if not self._is_opened:
|
|
self._do_open()
|
|
self._is_opened = True
|
|
|
|
def close(self):
|
|
"""close the communication with the slave"""
|
|
if self._is_opened:
|
|
self._do_close()
|
|
self._is_opened = False
|
|
|
|
def _do_open(self):
|
|
"""Open the MAC layer"""
|
|
raise NotImplementedError()
|
|
|
|
def _do_close(self):
|
|
"""Close the MAC layer"""
|
|
raise NotImplementedError()
|
|
|
|
def _send(self, buf):
|
|
"""Send data to a slave on the MAC layer"""
|
|
raise NotImplementedError()
|
|
|
|
def _recv(self, expected_length):
|
|
"""
|
|
Receive data from a slave on the MAC layer
|
|
if expected_length is >=0 then consider that the response is done when this
|
|
number of bytes is received
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def _make_query(self):
|
|
"""
|
|
Returns an instance of a Query subclass implementing
|
|
the MAC layer protocol
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@threadsafe_function
|
|
def execute(self, slave, function_code, starting_address,
|
|
quantity_of_x=0, output_value=0, data_format="", expected_length=-1):
|
|
"""
|
|
Execute a modbus query and returns the data part of the answer as a tuple
|
|
The returned tuple depends on the query function code. see modbus protocol
|
|
specification for details
|
|
data_format makes possible to extract the data like defined in the
|
|
struct python module documentation
|
|
"""
|
|
|
|
pdu = ""
|
|
is_read_function = False
|
|
nb_of_digits = 0
|
|
|
|
#open the connection if it is not already done
|
|
self.open()
|
|
|
|
#Build the modbus pdu and the format of the expected data.
|
|
#It depends of function code. see modbus specifications for details.
|
|
if function_code == defines.READ_COILS or function_code == defines.READ_DISCRETE_INPUTS:
|
|
is_read_function = True
|
|
pdu = struct.pack(">BHH", function_code, starting_address, quantity_of_x)
|
|
byte_count = quantity_of_x / 8
|
|
if (quantity_of_x % 8) > 0:
|
|
byte_count += 1
|
|
nb_of_digits = quantity_of_x
|
|
if not data_format:
|
|
data_format = ">"+(byte_count*"B")
|
|
if expected_length < 0: #No lenght was specified and calculated length can be used:
|
|
expected_length = byte_count + 5 #slave + func + bytcodeLen + bytecode + crc1 + crc2
|
|
|
|
elif function_code == defines.READ_INPUT_REGISTERS or function_code == defines.READ_HOLDING_REGISTERS:
|
|
is_read_function = True
|
|
pdu = struct.pack(">BHH", function_code, starting_address, quantity_of_x)
|
|
if not data_format:
|
|
data_format = ">"+(quantity_of_x*"H")
|
|
if expected_length < 0: #No lenght was specified and calculated length can be used:
|
|
expected_length = 2*quantity_of_x + 5 #slave + func + bytcodeLen + bytecode x 2 + crc1 + crc2
|
|
|
|
elif (function_code == defines.WRITE_SINGLE_COIL) or (function_code == defines.WRITE_SINGLE_REGISTER):
|
|
if function_code == defines.WRITE_SINGLE_COIL:
|
|
if output_value != 0:
|
|
output_value = 0xff00
|
|
pdu = struct.pack(">BHH", function_code, starting_address, output_value)
|
|
if not data_format:
|
|
data_format = ">HH"
|
|
if expected_length < 0: #No lenght was specified and calculated length can be used:
|
|
expected_length = 8 #slave + func + adress1 + adress2 + value1+value2 + crc1 + crc2
|
|
|
|
elif function_code == defines.WRITE_MULTIPLE_COILS:
|
|
byte_count = len(output_value) / 8
|
|
if (len(output_value) % 8) > 0:
|
|
byte_count += 1
|
|
pdu = struct.pack(">BHHB", function_code, starting_address, len(output_value), byte_count)
|
|
i, byte_value = 0, 0
|
|
for j in output_value:
|
|
if j > 0:
|
|
byte_value += pow(2, i)
|
|
if i == 7:
|
|
pdu += struct.pack(">B", byte_value)
|
|
i, byte_value = 0, 0
|
|
else:
|
|
i += 1
|
|
if i > 0:
|
|
pdu += struct.pack(">B", byte_value)
|
|
if not data_format:
|
|
data_format = ">HH"
|
|
if expected_length < 0: #No lenght was specified and calculated length can be used:
|
|
expected_length = 8 #slave + func + adress1 + adress2 + outputQuant1 + outputQuant2 + crc1 + crc2
|
|
|
|
elif function_code == defines.WRITE_MULTIPLE_REGISTERS:
|
|
byte_count = 2 * len(output_value)
|
|
pdu = struct.pack(">BHHB", function_code, starting_address, len(output_value), byte_count)
|
|
for j in output_value:
|
|
pdu += struct.pack(">H", j)
|
|
if not data_format:
|
|
data_format = ">HH"
|
|
if expected_length < 0: #No lenght was specified and calculated length can be used:
|
|
expected_length = 8 #slave + func + adress1 + adress2 + outputQuant1 + outputQuant2 + crc1 + crc2
|
|
|
|
else:
|
|
raise ModbusFunctionNotSupportedError("The %d function code is not supported. " % (function_code))
|
|
|
|
# instantiate a query which implements the MAC (TCP or RTU) part of the protocol
|
|
query = self._make_query()
|
|
|
|
# add the mac part of the protocol to the request
|
|
request = query.build_request(pdu, slave)
|
|
|
|
# send the request to the slave
|
|
retval = call_hooks("modbus.Master.before_send", (self, request))
|
|
if retval <> None:
|
|
request = retval
|
|
if self._verbose:
|
|
LOGGER.debug(utils.get_log_buffer("-> ", request))
|
|
self._send(request)
|
|
|
|
call_hooks("modbus.Master.after_send", (self))
|
|
|
|
if slave != 0:
|
|
# receive the data from the slave
|
|
response = self._recv(expected_length)
|
|
retval = call_hooks("modbus.Master.after_recv", (self, response))
|
|
if retval <> None:
|
|
response = retval
|
|
if self._verbose:
|
|
LOGGER.debug(utils.get_log_buffer("<- ", response))
|
|
|
|
# extract the pdu part of the response
|
|
response_pdu = query.parse_response(response)
|
|
|
|
# analyze the received data
|
|
(return_code, byte_2) = struct.unpack(">BB", response_pdu[0:2])
|
|
|
|
if return_code > 0x80:
|
|
# the slave has returned an error
|
|
exception_code = byte_2
|
|
raise ModbusError(exception_code)
|
|
else:
|
|
if is_read_function:
|
|
# get the values returned by the reading function
|
|
byte_count = byte_2
|
|
data = response_pdu[2:]
|
|
if byte_count != len(data):
|
|
# the byte count in the pdu is invalid
|
|
raise ModbusInvalidResponseError, "Byte count is %d while actual number of bytes is %d. " \
|
|
% (byte_count, len(data))
|
|
else:
|
|
# returns what is returned by the slave after a writing function
|
|
data = response_pdu[1:]
|
|
|
|
#returns the data as a tuple according to the data_format
|
|
#(calculated based on the function or user-defined)
|
|
result = struct.unpack(data_format, data)
|
|
if nb_of_digits > 0:
|
|
digits = []
|
|
for byte_val in result:
|
|
for i in xrange(8):
|
|
if (len(digits) >= nb_of_digits):
|
|
break
|
|
digits.append(byte_val % 2)
|
|
byte_val = byte_val >> 1
|
|
result = tuple(digits)
|
|
return result
|
|
|
|
def set_timeout(self, timeout_in_sec):
|
|
"""Defines a timeout on the MAC layer"""
|
|
self._timeout = timeout_in_sec
|
|
|
|
def get_timeout(self):
|
|
"""Gets the current value of the MAC layer timeout"""
|
|
return self._timeout
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class ModbusBlock:
|
|
"""This class represents the values for a range of addresses"""
|
|
|
|
def __init__(self, starting_address, size):
|
|
"""
|
|
Contructor: defines the address range and creates the array of values
|
|
"""
|
|
self.starting_address = starting_address
|
|
self._data = [0]*size
|
|
self.size = len(self._data)
|
|
|
|
def is_in(self, starting_address, size):
|
|
"""
|
|
Returns true if a block with the given address and size
|
|
would overlap this block
|
|
"""
|
|
if starting_address > self.starting_address:
|
|
return (self.starting_address+self.size)>starting_address
|
|
elif starting_address < self.starting_address:
|
|
return (starting_address+size)>self.starting_address
|
|
return True
|
|
|
|
def __getitem__(self, r):
|
|
""""""
|
|
return self._data.__getitem__(r)
|
|
|
|
def __setitem__(self, r, v):
|
|
""""""
|
|
call_hooks("modbus.ModbusBlock.setitem", (self, r, v))
|
|
return self._data.__setitem__(r, v)
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class Slave:
|
|
"""
|
|
This class define a modbus slave which is in charge of making the action
|
|
asked by a modbus query
|
|
"""
|
|
|
|
def __init__(self, id):
|
|
"""Constructor"""
|
|
self._id = id
|
|
self._blocks = {} # the map registring all blocks of the slave
|
|
# a shortcut to find blocks per type
|
|
self._memory = {1:[], 2:[], 3:[], 4:[]}
|
|
# a lock for mutual access to the _blocks and _memory maps
|
|
self._data_lock = threading.Lock()
|
|
#map modbus function code to a function:
|
|
self._fn_code_map = {defines.READ_COILS: self._read_coils,
|
|
defines.READ_DISCRETE_INPUTS: self._read_discrete_inputs,
|
|
defines.READ_INPUT_REGISTERS: self._read_input_registers,
|
|
defines.READ_HOLDING_REGISTERS: self._read_holding_registers,
|
|
defines.WRITE_SINGLE_COIL: self._write_single_coil,
|
|
defines.WRITE_SINGLE_REGISTER: self._write_single_register,
|
|
defines.WRITE_MULTIPLE_COILS: self._write_multiple_coils,
|
|
defines.WRITE_MULTIPLE_REGISTERS: self._write_multiple_registers,
|
|
}
|
|
|
|
def _get_block_and_offset(self, block_type, address, length):
|
|
"""returns the block and offset corresponding to the given address"""
|
|
for block in self._memory[block_type]:
|
|
if address >= block.starting_address:
|
|
offset = address - block.starting_address
|
|
if block.size >= offset+length:
|
|
return block, offset
|
|
|
|
raise ModbusError(defines.ILLEGAL_DATA_ADDRESS)
|
|
|
|
def _read_digital(self, block_type, request_pdu):
|
|
"""read the value of coils and discrete inputs"""
|
|
(starting_address, quantity_of_x) = struct.unpack(">HH", request_pdu[1:5])
|
|
|
|
if (quantity_of_x <= 0) or (quantity_of_x>2000):
|
|
# maximum allowed size is 2000 bits in one reading
|
|
raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
|
block, offset = self._get_block_and_offset(block_type, starting_address, quantity_of_x)
|
|
|
|
values = block[offset:offset+quantity_of_x]
|
|
|
|
#pack bits in bytes
|
|
byte_count = quantity_of_x / 8
|
|
if (quantity_of_x % 8) > 0:
|
|
byte_count += 1
|
|
|
|
# write the response header
|
|
response = struct.pack(">B", byte_count)
|
|
|
|
i, byte_value = 0, 0
|
|
for coil in values:
|
|
if coil:
|
|
byte_value += (1 << i)
|
|
if i >= 7:
|
|
# write the values of 8 bits in a byte
|
|
response += struct.pack(">B", byte_value)
|
|
#reset the counters
|
|
i, byte_value = 0, 0
|
|
else:
|
|
i += 1
|
|
|
|
#if there is remaining bits: add one more byte with their values
|
|
if i > 0:
|
|
response += struct.pack(">B", byte_value)
|
|
return response
|
|
|
|
def _read_coils(self, request_pdu):
|
|
"""handle read coils modbus function"""
|
|
return self._read_digital(defines.COILS, request_pdu)
|
|
|
|
def _read_discrete_inputs(self, request_pdu):
|
|
"""handle read coils modbus function"""
|
|
return self._read_digital(defines.DISCRETE_INPUTS, request_pdu)
|
|
|
|
def _read_registers(self, block_type, request_pdu):
|
|
"""read the value of holding and input registers"""
|
|
(starting_address, quantity_of_x) = struct.unpack(">HH", request_pdu[1:5])
|
|
|
|
if (quantity_of_x <= 0) or (quantity_of_x > 125):
|
|
# maximum allowed size is 125 registers in one reading
|
|
LOGGER.debug("quantity_of_x is %d" % quantity_of_x)
|
|
raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
|
# look for the block corresponding to the request
|
|
block, offset = self._get_block_and_offset(block_type, starting_address, quantity_of_x)
|
|
|
|
#get the values
|
|
values = block[offset:offset+quantity_of_x]
|
|
|
|
#write the response header
|
|
response = struct.pack(">B", 2 * quantity_of_x)
|
|
#add the values of every register on 2 bytes
|
|
for reg in values:
|
|
response += struct.pack(">H", reg)
|
|
return response
|
|
|
|
def _read_holding_registers(self, request_pdu):
|
|
"""handle read coils modbus function"""
|
|
return self._read_registers(defines.HOLDING_REGISTERS, request_pdu)
|
|
|
|
def _read_input_registers(self, request_pdu):
|
|
"""handle read coils modbus function"""
|
|
return self._read_registers(defines.ANALOG_INPUTS, request_pdu)
|
|
|
|
def _write_multiple_registers(self, request_pdu):
|
|
"""execute modbus function 16"""
|
|
# get the starting address and the number of items from the request pdu
|
|
(starting_address, quantity_of_x, byte_count) = struct.unpack(">HHB", request_pdu[1:6])
|
|
|
|
if (quantity_of_x <= 0) or (quantity_of_x > 123) or (byte_count != (quantity_of_x * 2)):
|
|
# maximum allowed size is 123 registers in one reading
|
|
raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
|
# look for the block corresponding to the request
|
|
block, offset = self._get_block_and_offset(defines.HOLDING_REGISTERS, starting_address, quantity_of_x)
|
|
|
|
count = 0
|
|
for i in xrange(quantity_of_x):
|
|
count += 1
|
|
block[offset+i] = struct.unpack(">H", request_pdu[6+2*i:8+2*i])[0]
|
|
|
|
return struct.pack(">HH", starting_address, count)
|
|
|
|
def _write_multiple_coils(self, request_pdu):
|
|
"""execute modbus function 15"""
|
|
# get the starting address and the number of items from the request pdu
|
|
(starting_address, quantity_of_x, byte_count) = struct.unpack(">HHB", request_pdu[1:6])
|
|
|
|
expected_byte_count = quantity_of_x / 8
|
|
if (quantity_of_x % 8) > 0:
|
|
expected_byte_count += 1
|
|
|
|
if (quantity_of_x <= 0) or (quantity_of_x > 1968) or (byte_count != expected_byte_count):
|
|
# maximum allowed size is 1968 coils
|
|
raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
|
# look for the block corresponding to the request
|
|
block, offset = self._get_block_and_offset(defines.COILS, starting_address, quantity_of_x)
|
|
|
|
count = 0
|
|
for i in xrange(byte_count):
|
|
if count >= quantity_of_x:
|
|
break
|
|
(byte_value, ) = struct.unpack(">B", request_pdu[6+i])
|
|
for j in xrange(8):
|
|
if byte_value & (1 << j):
|
|
block[offset+i*8+j] = 1
|
|
else:
|
|
block[offset+i*8+j] = 0
|
|
if count >= quantity_of_x:
|
|
break
|
|
count += 1
|
|
return struct.pack(">HH", starting_address, count)
|
|
|
|
def _write_single_register(self, request_pdu):
|
|
"""execute modbus function 6"""
|
|
(data_address, value) = struct.unpack(">HH", request_pdu[1:5])
|
|
block, offset = self._get_block_and_offset(defines.HOLDING_REGISTERS, data_address, 1)
|
|
block[offset] = value
|
|
return request_pdu[1:] #returns echo of the command
|
|
|
|
def _write_single_coil(self, request_pdu):
|
|
"""execute modbus function 5"""
|
|
(data_address, value) = struct.unpack(">HH", request_pdu[1:5])
|
|
block, offset = self._get_block_and_offset(defines.COILS, data_address, 1)
|
|
if value == 0:
|
|
block[offset] = 0
|
|
elif value == 0xff00:
|
|
block[offset] = 1
|
|
else:
|
|
raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
return request_pdu[1:] #returns echo of the command
|
|
|
|
def handle_request(self, request_pdu, broadcast=False):
|
|
"""
|
|
parse the request pdu, makes the corresponding action
|
|
and returns the response pdu
|
|
"""
|
|
with self._data_lock: #thread-safe
|
|
try:
|
|
retval = call_hooks("modbus.Slave.handle_request", (self, request_pdu))
|
|
if retval <> None:
|
|
return retval
|
|
|
|
# get the function code
|
|
(function_code, ) = struct.unpack(">B", request_pdu[0])
|
|
|
|
# check if the function code is valid. If not returns error response
|
|
if not self._fn_code_map.has_key(function_code):
|
|
raise ModbusError(defines.ILLEGAL_FUNCTION)
|
|
|
|
# if read query is broadcasted raises an error
|
|
cant_be_broadcasted = (defines.READ_COILS, defines.READ_DISCRETE_INPUTS,
|
|
defines.READ_INPUT_REGISTERS, defines.READ_HOLDING_REGISTERS)
|
|
if broadcast and (function_code in cant_be_broadcasted):
|
|
raise ModbusInvalidRequestError("Function %d can not be broadcasted" % function_code)
|
|
|
|
#execute the corresponding function
|
|
response_pdu = self._fn_code_map[function_code](request_pdu)
|
|
if response_pdu:
|
|
if broadcast:
|
|
call_hooks("modbus.Slave.on_handle_broadcast", (self, response_pdu))
|
|
LOGGER.debug("broadcast: %s" % (utils.get_log_buffer("!!", response_pdu)))
|
|
return ""
|
|
else:
|
|
return struct.pack(">B", function_code) + response_pdu
|
|
raise Exception("No response for function %d" % function_code)
|
|
|
|
except ModbusError, excpt:
|
|
LOGGER.debug(str(excpt))
|
|
call_hooks("modbus.Slave.on_exception", (self, function_code, excpt))
|
|
return struct.pack(">BB", function_code+128, excpt.get_exception_code())
|
|
|
|
|
|
def add_block(self, block_name, block_type, starting_address, size):
|
|
"""Add a new block identified by its name"""
|
|
with self._data_lock: #thread-safe
|
|
if size <= 0:
|
|
raise InvalidArgumentError, "size must be a positive number"
|
|
if starting_address < 0:
|
|
raise InvalidArgumentError, "starting address must be zero or positive number"
|
|
if self._blocks.has_key(block_name):
|
|
raise DuplicatedKeyError, "Block %s already exists. " % (block_name)
|
|
|
|
if not self._memory.has_key(block_type):
|
|
raise InvalidModbusBlockError, "Invalid block type %d" % (block_type)
|
|
|
|
# check that the new block doesn't overlap an existing block
|
|
# it means that only 1 block per type must correspond to a given address
|
|
# for example: it must not have 2 holding registers at address 100
|
|
index = 0
|
|
for i in xrange(len(self._memory[block_type])):
|
|
block = self._memory[block_type][i]
|
|
if block.is_in(starting_address, size):
|
|
raise OverlapModbusBlockError, "Overlap block at %d size %d" % (block.starting_address, block.size)
|
|
if block.starting_address > starting_address:
|
|
index = i
|
|
break
|
|
|
|
# if the block is ok: register it
|
|
self._blocks[block_name] = (block_type, starting_address)
|
|
# add it in the 'per type' shortcut
|
|
self._memory[block_type].insert(index, ModbusBlock(starting_address, size))
|
|
|
|
def remove_block(self, block_name):
|
|
"""
|
|
Remove the block with the given name.
|
|
Raise an exception if not found
|
|
"""
|
|
with self._data_lock: #thread safe
|
|
block = self._get_block(block_name)
|
|
|
|
# the block has been found: remove it from the shortcut
|
|
block_type = self._blocks.pop(block_name)[0]
|
|
self._memory[block_type].remove(block)
|
|
|
|
def remove_all_blocks(self):
|
|
"""
|
|
Remove all the blocks
|
|
"""
|
|
with self._data_lock: #thread safe
|
|
self._blocks.clear()
|
|
for key in self._memory:
|
|
self._memory[key] = []
|
|
|
|
def _get_block(self, block_name):
|
|
"""Find a block by its name and raise and exception if not found"""
|
|
if not self._blocks.has_key(block_name):
|
|
raise MissingKeyError, "block %s not found" % (block_name)
|
|
(block_type, starting_address) = self._blocks[block_name]
|
|
for block in self._memory[block_type]:
|
|
if block.starting_address == starting_address:
|
|
return block
|
|
raise Exception, "Bug?: the block %s is not registered properly in memory" % (block_name)
|
|
|
|
def set_values(self, block_name, address, values):
|
|
"""
|
|
Set the values of the items at the given address
|
|
If values is a list or a tuple, the value of every item is written
|
|
If values is a number, only one value is written
|
|
"""
|
|
with self._data_lock: #thread safe
|
|
block = self._get_block(block_name)
|
|
|
|
# the block has been found
|
|
# check that it doesn't write out of the block
|
|
offset = address-block.starting_address
|
|
|
|
size = 1
|
|
if (type(values) is list) or (type(values) is tuple):
|
|
size = len(values)
|
|
|
|
if (offset < 0) or ((offset + size) > block.size):
|
|
raise OutOfModbusBlockError, "address %s size %d is out of block %s" % (address, size, block_name)
|
|
|
|
#if Ok: write the values
|
|
if (type(values) is list) or (type(values) is tuple):
|
|
block[offset:offset+len(values)] = values
|
|
else:
|
|
block[offset] = values
|
|
|
|
def get_values(self, block_name, address, size=1):
|
|
"""
|
|
return the values of n items at the given address of the given block
|
|
"""
|
|
with self._data_lock: #thread safe
|
|
block = self._get_block(block_name)
|
|
|
|
# the block has been found
|
|
# check that it doesn't write out of the block
|
|
offset = address - block.starting_address
|
|
|
|
if (offset<0) or ((offset + size) > block.size):
|
|
raise OutOfModbusBlockError, "address %s size %d is out of block %s" % (address, size, block_name)
|
|
|
|
#returns the values
|
|
if size == 1:
|
|
return (block[offset],)
|
|
else:
|
|
return tuple(block[offset:offset+size])
|
|
|
|
class Databank:
|
|
"""A databank is a shared place containing the data of all slaves"""
|
|
|
|
def __init__(self):
|
|
"""Constructor"""
|
|
self._slaves = {} # the map of slaves by ids
|
|
self._lock = threading.Lock() # protect access to the map of slaves
|
|
|
|
def add_slave(self, slave_id):
|
|
"""Add a new slave with the given id"""
|
|
with self._lock: #thread-safe
|
|
if (slave_id <= 0) or (slave_id > 255):
|
|
raise Exception, "Invalid slave id %d" % (slave_id)
|
|
if not self._slaves.has_key(slave_id):
|
|
self._slaves[slave_id] = Slave(slave_id)
|
|
return self._slaves[slave_id]
|
|
else:
|
|
raise DuplicatedKeyError, "Slave %d already exists" % (slave_id)
|
|
|
|
def get_slave(self, slave_id):
|
|
"""Get the slave with the given id"""
|
|
with self._lock: #thread-safe
|
|
if self._slaves.has_key(slave_id):
|
|
return self._slaves[slave_id]
|
|
else:
|
|
raise MissingKeyError, "Slave %d doesn't exist" % (slave_id)
|
|
|
|
def remove_slave(self, slave_id):
|
|
"""Remove the slave with the given id"""
|
|
with self._lock: #thread-safe
|
|
if self._slaves.has_key(slave_id):
|
|
self._slaves.pop(slave_id)
|
|
else:
|
|
raise MissingKeyError, "Slave %d already exists" % (slave_id)
|
|
|
|
def remove_all_slaves(self):
|
|
"""clean the list of slaves"""
|
|
with self._lock: #thread-safe
|
|
self._slaves.clear()
|
|
|
|
def handle_request(self, query, request):
|
|
"""
|
|
when a request is received, handle it and returns the response pdu
|
|
"""
|
|
request_pdu = ""
|
|
try:
|
|
#extract the pdu and the slave id
|
|
(slave_id, request_pdu) = query.parse_request(request)
|
|
|
|
#get the slave and let him executes the action
|
|
if slave_id == 0:
|
|
#broadcast
|
|
for key in self._slaves:
|
|
self._slaves[key].handle_request(request_pdu, broadcast=True)
|
|
return
|
|
else:
|
|
slave = self.get_slave(slave_id)
|
|
response_pdu = slave.handle_request(request_pdu)
|
|
#make the full response
|
|
response = query.build_response(response_pdu)
|
|
return response
|
|
except Exception, excpt:
|
|
call_hooks("modbus.Databank.on_error", (self, excpt, request_pdu))
|
|
LOGGER.error("handle request failed: " + str(excpt))
|
|
except:
|
|
LOGGER.error("handle request failed: unknown error")
|
|
|
|
#If the request was not handled correctly, return a server error response
|
|
func_code = 1
|
|
if len(request_pdu) > 0:
|
|
(func_code, ) = struct.unpack(">B", request_pdu[0])
|
|
return struct.pack(">BB", func_code+0x80, defines.SLAVE_DEVICE_FAILURE)
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class Server:
|
|
"""
|
|
This class owns several slaves and defines an interface
|
|
to be implemented for a TCP or RTU server
|
|
"""
|
|
|
|
def __init__(self, databank=None):
|
|
"""Constructor"""
|
|
self._databank = databank if databank else Databank() #never use a mutable type as default argument
|
|
self._verbose = False
|
|
self._thread = None
|
|
self._go = None
|
|
self._make_thread()
|
|
|
|
def _do_init(self):
|
|
"""executed before the server starts: to be overridden"""
|
|
pass
|
|
|
|
def _do_exit(self):
|
|
"""executed after the server stops: to be overridden"""
|
|
pass
|
|
|
|
def _do_run(self):
|
|
"""main function of the server: to be overridden"""
|
|
pass
|
|
|
|
def _make_thread(self):
|
|
"""create the main thread of the server"""
|
|
self._thread = threading.Thread(target=Server._run_server, args=(self,))
|
|
self._go = threading.Event()
|
|
|
|
def set_verbose(self, verbose):
|
|
"""if verbose is true the sent and received packets will be logged"""
|
|
self._verbose = verbose
|
|
|
|
def get_db(self):
|
|
"""returns the databank"""
|
|
return self._databank
|
|
|
|
def add_slave(self, slave_id):
|
|
"""add slave to the server"""
|
|
return self._databank.add_slave(slave_id)
|
|
|
|
def get_slave(self, slave_id):
|
|
"""get the slave with the given id"""
|
|
return self._databank.get_slave(slave_id)
|
|
|
|
def remove_slave(self, slave_id):
|
|
"""remove the slave with the given id"""
|
|
self._databank.remove_slave(slave_id)
|
|
|
|
def remove_all_slaves(self):
|
|
"""remove the slave with the given id"""
|
|
self._databank.remove_all_slaves()
|
|
|
|
def _make_query(self):
|
|
"""
|
|
Returns an instance of a Query subclass implementing
|
|
the MAC layer protocol
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def start(self):
|
|
"""Start the server. It will handle request"""
|
|
self._go.set()
|
|
self._thread.start()
|
|
|
|
def stop(self):
|
|
"""stop the server. It doesn't handle request anymore"""
|
|
if self._thread.isAlive():
|
|
self._go.clear()
|
|
self._thread.join()
|
|
|
|
def _run_server(self):
|
|
"""main function of the main thread"""
|
|
try:
|
|
self._do_init()
|
|
while self._go.isSet():
|
|
self._do_run()
|
|
LOGGER.info("%s has stopped" % self.__class__)
|
|
self._do_exit()
|
|
except Exception, excpt:
|
|
LOGGER.error("server error: %s" % str(excpt))
|
|
self._make_thread() #make possible to rerun in future
|
|
|
|
def _handle(self, request):
|
|
"""handle a received sentence"""
|
|
|
|
if self._verbose:
|
|
LOGGER.debug(utils.get_log_buffer("-->", request))
|
|
|
|
#gets a query for analyzing the request
|
|
query = self._make_query()
|
|
|
|
retval = call_hooks("modbus.Server.before_handle_request", (self, request))
|
|
if retval:
|
|
request = retval
|
|
|
|
response = self._databank.handle_request(query, request)
|
|
retval = call_hooks("modbus.Server.after_handle_request", (self, response))
|
|
if retval:
|
|
response = retval
|
|
|
|
if response and self._verbose:
|
|
LOGGER.debug(utils.get_log_buffer("<--", response))
|
|
return response
|
|
|
|
|