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
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')
def validate(self, slave, fx, 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 is within range, False otherwise ''' logging.debug("In CbModbusDatastore.validate") if fx == ModbusFunctionCodes.ReadCoil or \ fx == ModbusFunctionCodes.WriteSingleCoil or \ fx == ModbusFunctionCodes.WriteMultipleCoils: return cbData.validate_coil_address(self.cbsystem, self.cbauth, slave, address, count) elif fx == ModbusFunctionCodes.ReadDiscreteInput: return cbData.validate_discrete_input_address(self.cbsystem, self.cbauth, slave, \ address, count) elif fx == ModbusFunctionCodes.ReadHoldingRegisters or \ fx == ModbusFunctionCodes.WriteSingleHoldingRegister or \ fx == ModbusFunctionCodes.WriteMultipleHoldingRegisters: return cbData.validate_holding_reg_address(self.cbsystem, self.cbauth, slave, \ address, count) elif fx == ModbusFunctionCodes.ReadInputRegisters: return cbData.validate_input_reg_address(self.cbsystem, self.cbauth, slave, \ address, count) else: raise ParameterException("Invalid function code received for \ CbModbusDatastore.validate request. Function code received = " + str(fx)) return True
def getValues(self, slave, fx, 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 ''' logging.debug("In CbModbusDatastore.getValues") if fx == ModbusFunctionCodes.ReadCoil or \ fx == ModbusFunctionCodes.WriteSingleCoil or \ fx == ModbusFunctionCodes.WriteMultipleCoils: return cbData.read_coils(self.cbsystem, self.cbauth, slave, address, count) elif fx == ModbusFunctionCodes.ReadDiscreteInput: return cbData.read_discrete_inputs(self.cbsystem, self.cbauth, slave, address, count) elif fx == ModbusFunctionCodes.ReadHoldingRegisters or \ fx == ModbusFunctionCodes.WriteSingleHoldingRegister or \ fx == ModbusFunctionCodes.WriteMultipleHoldingRegisters: return cbData.read_holding_registers(self.cbsystem, self.cbauth, slave, address, count) elif fx == ModbusFunctionCodes.ReadInputRegisters: return cbData.read_input_registers(self.cbsystem, self.cbauth, slave, address, count) else: raise ParameterException("Invalid function code received for \ CbModbusDatastore.getValues request. Function code received = " + str(fx))
def _write(self, address, value, **kwargs): """Perform the write, enforcing Defaults.Timeout around the entire transaction. Normally returns None, but may raise a ModbusException or a PlcOffline if there are communications problems. Use a supplied 'unit' ID, or the one specified/deduced at construction. """ self.client.timeout = True if not self.client.connect(): raise PlcOffline( "Modbus Write to PLC %s/%6d failed: Offline; Connect failure" % (self.description, address)) # Use address to deduce Holding Register or Coil (the only writable # entities); Statuses and Input Registers result in a pymodbus # ParameterException multi = hasattr(value, '__iter__') writer = None if 400001 <= address <= 465536: # 400001-465536: Holding Registers writer = (WriteMultipleRegistersRequest if multi or self.multi else WriteSingleRegisterRequest) address -= 400001 elif 40001 <= address <= 99999: # 40001-99999: Holding Registers writer = (WriteMultipleRegistersRequest if multi or self.multi else WriteSingleRegisterRequest) address -= 40001 elif 1 <= address <= 9999: # 1-9999: Coils writer = ( WriteMultipleCoilsRequest if multi # *don't* force multi else WriteSingleCoilRequest) address -= 1 else: # 100001-165536: Statuses (not writable) # 300001-365536: Input Registers (not writable) # 10001-19999: Statuses (not writable) # 30001-39999: Input Registers (not writable) pass if not writer: raise ParameterException("Invalid Modbus address for write: %d" % (address)) if writer is WriteMultipleRegistersRequest: # Overcome bug in 1.2.0/1.3.0 in handling single requests. Also reifies generators. value = list(value) if multi else [value] unit = kwargs.pop('unit', self.unit) result = self.client.execute( writer(address, value, unit=unit, **kwargs)) if isinstance(result, ExceptionResponse): raise ModbusException(str(result)) assert isinstance( result, ModbusResponse), "Unexpected non-ModbusResponse: %r" % result
def __implementation(method): ''' Returns the requested framer :method: The serial framer to instantiate :returns: The requested serial framer ''' method = method.lower() if method == 'rtu': return ModbusRtuFramer(ClientDecoder()) raise ParameterException("Invalid framer method requested")
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 ParameterException('slave index out of range')
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 ParameterException('slave index out of range')
def __setitem__(self, slave, context): ''' Wrapper used to access the slave context :param slave: slave The context to set :param context: The new context to set for this slave ''' if self.single: slave = 0x00 if 0xf7 >= slave >= 0x00: self.__slaves[slave] = context else: raise ParameterException('slave index out of range')
def __getitem__(self, slave): ''' Wrapper used to access the slave context :param slave: The slave context to get :returns: The requested slave context ''' if self.single: slave = 0x00 if slave in self.__slaves: return self.__slaves.get(slave) else: raise ParameterException( "slave does not exist, or is out of range")
def fromCoils(klass, coils, byteorder=Endian.Little): """ 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 = pack_bitstring(coils) return klass(payload, byteorder) raise ParameterException('Invalid collection of coils supplied')
def __init__(self, values): ''' Initializes the datastore Using the input values we create the default datastore value and the starting address :param values: Either a list or a dictionary of values ''' if isinstance(values, dict): self.values = values elif hasattr(values, '__iter__'): self.values = dict(enumerate(values)) else: raise ParameterException( "Values for datastore must be a list or dictionary") self.default_value = self.values.itervalues().next().__class__() self.address = self.values.iterkeys().next()
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 from_registers(registers, endian=Endian.Little): """ 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 endian: The endianess of the payload :returns: An initialized PayloadDecoder """ if isinstance(registers, list): # repack into flat binary payload = ''.join(pack('>H', x) for x in registers) return ModiconPayloadDecoder(payload, endian) raise ParameterException('Invalid collection of registers supplied')
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 """ 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')
def setValues(self, slave, fx, address, values): ''' Sets the requested values of the datastore :param address: The starting address :param values: The new values to be set ''' logging.debug("In CbModbusDatastore.setValues") if fx == ModbusFunctionCodes.WriteSingleCoil or \ fx == ModbusFunctionCodes.WriteMultipleCoils: cbData.write_coils(self.cbsystem, self.cbauth, slave, address, values) elif fx == ModbusFunctionCodes.WriteSingleHoldingRegister or \ fx == ModbusFunctionCodes.WriteMultipleHoldingRegisters: cbData.write_holding_registers(self.cbsystem, self.cbauth, slave, address, values) else: raise ParameterException("Invalid function code received for \ CbModbusDatastore.setValues request. Function code received = " + str(fx))
def __init__(self, context, register_type, values): """ Initializes the sparse datastore. Using the input values it creates the default datastore value and the starting address :param context.ClearBladeModbusProxySlaveContext context: the parent/context of the data block :param str register_type: select from hr, ir, di, co :param dict values: Either a dictionary or list of values """ super(CbModbusSparseDataBlock, self).__init__(values=values) self.context = context if register_type in REGISTER_TYPES: self.register_type = register_type else: raise ParameterException( "Register type must be one of: ".format(REGISTER_TYPES)) self.timestamps = {} for addr in self.values: self.timestamps[addr] = None
def __init__(self, context, register_type, address, values): """ Initializes the sequential datastore :param context.ClearBladeModbusProxySlaveContext context: the parent/context of the data block :param str register_type: select from hr, ir, di, co :param int address: the starting address for the sequential block :param iterable values: Either a dictionary or list of values """ super(CbModbusSequentialDataBlock, self).__init__(address=address, values=values) self.context = context if register_type in REGISTER_TYPES: self.register_type = register_type else: raise ParameterException( "Register type must be one of: ".format(REGISTER_TYPES)) self.timestamps = [] for i in range(0, len(self.values) - 1): self.timestamps.append(None)
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 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 read_collection_data(context, register_type, address, count, fill=0): """ Retrieve data from the specified collection. When retrieving sequential blocks if the ClearBlade collection is missing registers between the start and end, those will be filled (optionally) :param context.ClearBladeModbusProxySlaveContext context: The ClearBlade parent metadata to query against. :param str register_type: the type of register ('co', 'di', 'hr', 'ir') :param int address: The starting address :param int count: The number of values to retrieve :param int fill: automatically fills gaps in sequential register blocks with this value (or None) :returns: values, timestamps of the data read from the ClearBlade collection/proxy :rtype: list or dict (sequential or sparse) """ collection = context.cb_system.Collection( context.cb_auth, collectionName=context.cb_data_collection) query = Query() query.equalTo(COL_PROXY_IP_ADDRESS, context.ip_proxy) query.equalTo(COL_SLAVE_ID, context.slave_id) query.equalTo(COL_REG_TYPE, register_type) if count > 1: query.greaterThanEqualTo(COL_REG_ADDRESS, address) query.lessThan(COL_REG_ADDRESS, address + count) else: query.equalTo(COL_REG_ADDRESS, address) reg_list = sorted(collection.getItems(query), key=lambda k: k[COL_REG_ADDRESS]) if len(reg_list) != count: context.log.warning( "Got {} rows from ClearBlade, expecting {} registers".format( len(reg_list), count)) if context.sparse: values = {} timestamps = {} # Below commented code would fill in missing registers in a sparse data block (placeholder needs more thought) # for i in range(0, count-1): # curr_addr = reg_list[i][COL_REG_ADDRESS] # values[curr_addr] = reg_list[i][COL_REG_DATA] # if i+1 < count and i+1 < len(reg_list): # next_addr = curr_addr + 1 # if reg_list[i+1][COL_REG_ADDRESS] != next_addr and fill: # context.log.info("Filling {} register {} with value {}" # .format(register_type, next_addr, FILL_VALUE)) # values[next_addr] = FILL_VALUE for reg in reg_list: values[reg[COL_REG_ADDRESS]] = reg[COL_REG_DATA] timestamps[reg[COL_REG_ADDRESS]] = reg[COL_DATA_TIMESTAMP] else: values = [] timestamps = [] for addr in range(address, address + count): if reg_list[addr][COL_REG_ADDRESS] != addr: if fill is not None: if isinstance(fill, int): context.log.info( "Filling {} register {} with value {}".format( register_type, addr, fill)) reg_list.insert(addr, {COL_REG_DATA: fill}) else: raise ValueError( "Fill parameter must be integer or None") else: raise ParameterException( "ClearBlade Collection missing register {} from block [{}:{}]" .format(addr, address, address + count)) values.append(reg_list[addr][COL_REG_DATA]) timestamps.append(reg_list[addr][COL_DATA_TIMESTAMP]) return values, timestamps
def _read(self, address, count=1, **kwargs): """Perform the read, enforcing Defaults.Timeout around the entire transaction. Returns the result bit(s)/register(s), or raises an Exception; probably a ModbusException or a PlcOffline for communications errors, but could be some other type of Exception. Use a supplied 'unit' ID, or the one specified/deduced at construction. """ self.client.timeout = True if not self.client.connect(): raise PlcOffline( "Modbus Read of PLC %s/%6d failed: Offline; Connect failure" % (self.description, address)) # Use address to deduce Holding/Input Register or Coil/Status. reader = None xformed = address if 400001 <= address <= 465536: reader = ReadHoldingRegistersRequest xformed -= 400001 elif 300001 <= address <= 365536: reader = ReadInputRegistersRequest xformed -= 300001 elif 100001 <= address <= 165536: reader = ReadDiscreteInputsRequest xformed -= 100001 elif 40001 <= address <= 99999: reader = ReadHoldingRegistersRequest xformed -= 40001 elif 30001 <= address <= 39999: reader = ReadInputRegistersRequest xformed -= 30001 elif 10001 <= address <= 19999: reader = ReadDiscreteInputsRequest xformed -= 10001 elif 1 <= address <= 9999: reader = ReadCoilsRequest xformed -= 1 else: # Invalid address pass if not reader: raise ParameterException("Invalid Modbus address for read: %d" % (address)) unit = kwargs.pop('unit', self.unit) request = reader(xformed, count, unit=unit, **kwargs) log.debug("%s/%6d-%6d transformed to %s", self.description, address, address + count - 1, request) result = self.client.execute(request) if isinstance(result, ExceptionResponse): # The remote PLC returned a response indicating it encountered an # error processing the request. Convert it to raise a ModbusException. raise ModbusException(str(result)) assert isinstance( result, ModbusResponse), "Unexpected non-ModbusResponse: %r" % result # The result may contain .bits or .registers, 1 or more values values = result.bits if hasattr(result, 'bits') else result.registers return values if len(values) > 1 else values[0]