Ejemplo n.º 1
0
def init_extarct():
    global trainer
    global logger
    cfg = ConfigParser()
    configuration_path = Path(__file__).resolve(
        strict=True).parent / 'configs' / 'extract_eval.conf'
    cfg.read(configuration_path)
    logger = Logger(cfg)
    logger.info(f'Configuration parsed: {cfg.sections()}')
    trainer = SpanTrainer(cfg, logger)
Ejemplo n.º 2
0
class SimulinkInterface:
    def __init__(self, config_path, send_port=5000):
        self.controller_ps = []
        self.config = self.read_config(config_path)
        self.selector = selectors.DefaultSelector()
        self.publish_queue = PublishQueue()
        self.udp_send_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.udp_send_socket.bind(('', send_port))
        self.logger = Logger('InterfaceLogger',
                             '../logger/logs/interface_log.txt')
        # Used for sim time oracle which is currently disabled
        # self.time_oracle = None

    def read_config(self, config_path):
        """
            Read and parse the .yaml configuration file.
        :param config_path: the path to the specific yaml file
        :return:
        """
        with open(config_path, 'r') as stream:
            try:
                config_yaml = yaml.safe_load(stream)
            except yaml.YAMLError as exc:
                print(exc)
                exit(1)
        # if not config_yaml['settings']:
        #     print("Failed to read config... no settings in file")
        #     exit(1)
        # if not config_yaml['settings']['send_port']:
        #     print("Failed to read config... no send_port in settings")
        #     exit(1)
        return config_yaml

    def create_plcs(self):
        """
            Creates PLC(s) based on the .yaml configuration file.
            Each PLC spawns several "worker" processes which listen for incoming simulink data
                through a predefined publish / subscribe mechanism.
            The register_workers function creates the server sockets for each worker, and registers them to
            the main selector. These workers also subscribe to receive the data coming to their "listening port"
            by registering themselves with the publish queue.
        :return:
        """
        for plc_name, plc_config in self.config.items():
            if plc_name == 'time_oracle':
                continue
            controller = LogicController(plc_name, plc_config)
            controller.register_workers(self.selector, self.publish_queue)
            controller_ps = multiprocessing.Process(
                target=controller.start_plc, daemon=True, name=plc_name)
            self.controller_ps.append(controller_ps)

    # def init_time_oracle(self):
    #     """
    #     Read simtime oracle source for more information, tries to resolve the difference between real
    #         and simulated time using a pseudo-NTP approach. You can work on this if you'd like to centralize the timer
    #         to the interface instead of using a virtual PLC (oracle PLC) to manage simulation timestamps.
    #     :return:
    #     """
    #     timer_conf = self.config['time_oracle']
    #     time_oracle = simtimeoracle.SimulationTimeOracle(timer_conf['receive_port'], timer_conf['respond_port'])
    #     return time_oracle, multiprocessing.Process(target=time_oracle.start, name='Time Oracle', daemon=True)

    # def _accept_connection(self, sock: socket.socket):
    #     """
    #         !!!! NO LONGER USED IN UDP Implementation !!!!
    #         Upon receiving a new connection from simulink register this connection to our selector and
    #         it's respective data.
    #     :param sock:
    #     :return:
    #     """
    #     conn, addr = sock.accept()
    #     print("New connection from {}".format(addr))
    #     conn.setblocking(False)
    #     self.selector.register(conn, selectors.EVENT_READ, {"connection_type": "client_connection",
    #                                                         "channel": addr[1]})

    def _read_and_publish(self, connection: socket, channel: str):
        """
            Reads data from simulink.
            |----------------------|
            |--- 64 bit timestamp -|
            |--- 64 bit reading ---|
            |----------------------|
        :param connection: the connection from a simulink block
        :param channel: the channel to publish this data to on the publish queue
        """
        data = connection.recv(16)  # Should be ready
        if data:
            sim_time, reading = struct.unpack(">dd", data)
            sim_time = int(sim_time)
            self.publish_queue.publish((sim_time, reading), channel)
        else:
            print('closing', connection)
            self.selector.unregister(connection)
            connection.close()

    def _send_response(self, read_pipe, host: str, port: int):
        """
            Reads from the worker pipe and forwards the data to the respective simulink block
            based on the host and port specified.
        :param read_pipe: a pipe connecting the worker thread to the main simulink selector
        :param host: ip / hostname to send data
        :param port: port number that the host is listening on
        :return:
        """
        response_data = os.read(read_pipe, 128)
        self.logger.info("Sending response {} to {}:{}".format(
            binascii.hexlify(response_data), host, port))
        self.udp_send_socket.sendto(response_data, (host, port))

    def service_connection(self, key):
        """
            Based on the information in the key['connection_type'] route take the correct action.
            For server_sockets read the data and publish to the queue.
            For responses read the appropriate data from the response pipe and forward to simulink.
        :param key: The key associated with the file object registered in the selector
        :return:
        """
        connection = key.fileobj
        connection_type = key.data['connection_type']
        if connection_type == 'server_socket':
            channel = key.data['channel']
            self._read_and_publish(connection, channel)
        if connection_type == 'response':
            read_pipe = key.fileobj
            # The address to respond to should be registered along with the pipe object
            host, port = key.data['respond_to']
            self._send_response(read_pipe, host, port)

    def start_server(self):
        """
            Set up the virtual PLC(s) and their respective worker processes / threads.
            Initialize the time oracle.
            Once setup, start the PLC(s) to begin listening for data.
            Then start the selector loop, waiting for new data and servicing incoming responses.
        :return:
        """
        # Time oracle stuff is now manage in PLC sensors
        # self.time_oracle, time_oracle_ps = self.init_time_oracle()
        # time_oracle_ps.start()

        self.create_plcs()
        for plc in self.controller_ps:
            self.logger.info('Starting controller: {}'.format(plc))
            plc.start()

        while True:
            events = self.selector.select()
            for key, mask in events:
                self.service_connection(key)
Ejemplo n.º 3
0
class ModbusReceiver:
    def __init__(self,
                 port,
                 localhost=True,
                 device_function_codes=None,
                 socket_type=socket.SOCK_STREAM,
                 failures={}):
        self.port = port
        self.localhost = localhost
        self.logger = Logger('ServerLogger-{}'.format(port),
                             '../logger/logs/server_log.txt',
                             prefix='Server {}'.format(port))
        self.stop = threading.Event()
        self.done = threading.Event()
        self.device_function_codes = device_function_codes
        self._current_connection = None
        self.socket_type = socket_type
        self.failures = failures
        self.lock = threading.RLock()

    '''
        Dispatches packet data for decoding based on it's function code.
        ModbusDecoder handles decoding of the packets and returns a Dict containing
        appropriate data. Invalid function codes lead to an invalid_function_code message which
        is also created by the modbus decoder.
    '''

    def _dissect_packet(self, packet_data) -> Dict:
        function_code = packet_data[0]
        # Check that the device supports this function code
        if self.device_function_codes:
            if function_code not in self.device_function_codes:
                return modbusdecoder.invalid_function_code(packet_data)
        switch = {
            1: modbusdecoder.read_coils,
            2: modbusdecoder.read_discrete_inputs,
            3: modbusdecoder.read_holding_registers,
            4: modbusdecoder.read_input_registers,
            5: modbusdecoder.write_single_coil,
            6: modbusdecoder.write_single_holding_register,
            15: modbusdecoder.write_multiple_coils,
            16: modbusdecoder.write_multiple_holding_registers
        }
        function = switch.get(function_code,
                              modbusdecoder.invalid_function_code)
        return function(packet_data)

    def _start_server_tcp(self, request_handler: Callable) -> None:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            if self.localhost:
                s.bind(('localhost', self.port))
            else:
                s.bind((socket.gethostname(), self.port))
            s.listen(5)
            self.logger.info('Server started {}:{}'.format(
                socket.gethostname(), self.port))
            while not self.stop.is_set():
                if self.failures.get('disconnected', False):
                    sleep(1)
                    continue
                self._current_connection, address = s.accept()
                self.logger.info('New connection accepted {}'.format(
                    self._current_connection.getpeername()))
                with self._current_connection:
                    while not self.stop.is_set():
                        try:
                            buffer = self._current_connection.recv(7)
                            if buffer == b'' or len(buffer) <= 0:
                                self.logger.debug(
                                    'Initial read was empty, peer connection was likely closed'
                                )
                                break
                            header = buffer
                            self.logger.debug(
                                'MB:{} Header DATA like: {}'.format(
                                    self.port, header))
                            # Modbus length is in bytes 4 & 5 of the header according to spec (pg 25)
                            # https://www.prosoft-technology.com/kb/assets/intro_modbustcp.pdf
                            header = modbusdecoder.dissect_header(header)
                            length = header['length']
                            if length == 0:
                                self.logger.debug(
                                    'A length 0 header was read, closing connection'
                                )
                                break
                            data = self._current_connection.recv(length - 1)
                            StatisticsCollector.increment_packets_received()
                            response_start = time.time()
                            is_error, dissection = self._dissect_packet(data)
                            if is_error:
                                self.logger.debug(
                                    'MB:{} Header appears like: {}'.format(
                                        self.port, header))
                                self.logger.debug('MB:{} Request: {}'.format(
                                    self.port, hexlify(buffer + data)))
                                self.logger.debug(
                                    'MB:{} An error was found in the modbus request {}'
                                    .format(self.port, hexlify(dissection)))
                                self._current_connection.sendall(dissection)
                                response_stop = time.time()
                                StatisticsCollector.increment_error_packets_sent(
                                )
                                StatisticsCollector.increment_responses_sent()
                                StatisticsCollector.increment_avg_response(
                                    response_stop - response_start)
                                continue
                            else:
                                dissection['type'] = 'request'
                                header['function_code'] = data[0]
                                response = request_handler({
                                    'header': header,
                                    'body': dissection
                                })
                                self.logger.debug(
                                    'MB:{} Header: {} Body:{}'.format(
                                        self.port, header, dissection))
                                self.logger.debug('MB:{} Request: {}'.format(
                                    self.port, hexlify(buffer + data)))
                                self.logger.debug(
                                    'MB:{} Responding: {}'.format(
                                        self.port, hexlify(response)))
                                # add failures to the receiver
                                if not self.simulate_failures():
                                    continue
                                self._current_connection.sendall(response)
                                response_stop = time.time()
                                StatisticsCollector.increment_responses_sent()
                                StatisticsCollector.increment_avg_response(
                                    response_stop - response_start)
                        except IOError as e:
                            self.logger.warning(
                                'An IO error occurred when reading the socket {}'
                                .format(e))
                            self.logger.debug('Closing connection')
                            self._current_connection.close()
                            StatisticsCollector.increment_socket_errors()
                            break
            self.done.set()

    def _start_server_udp(self, request_handler: Callable) -> None:
        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            if self.localhost:
                s.bind(('localhost', self.port))
                self.logger.info('Starting UDP server at localhost:{}'.format(
                    self.port))
            else:
                s.bind((socket.gethostname(), self.port))
                self.logger.debug('Starting UDP server at {}:{}'.format(
                    socket.gethostname(), self.port))
            while not self.stop.is_set():
                try:
                    if self.failures.get('disconnected', False):
                        sleep(1)
                        continue
                    buffer, address = s.recvfrom(256)
                    self.logger.debug(
                        'Message received from: {}'.format(address))
                    StatisticsCollector.increment_packets_received()
                    response_start = time.time()
                    if buffer == b'' or len(buffer) <= 0:
                        self.logger.debug(
                            'Initial read was empty, peer connection was likely closed'
                        )
                        continue
                    header = buffer[:7]
                    header = modbusdecoder.dissect_header(header)
                    length = header['length']
                    if length == 0:
                        self.logger.debug('Length 0 message received')
                        continue
                    data = buffer[7:7 + length - 1]
                    is_error, dissection = self._dissect_packet(data)
                    if is_error:
                        self.logger.debug(
                            'An error was found in the modbus request {}'.
                            format(dissection))
                        self.logger.debug(
                            'Header appears like: {}'.format(header))
                        self.logger.debug('Buffer: {}'.format(hexlify(buffer)))
                        s.sendto(dissection, address)
                        response_stop = time.time()
                        StatisticsCollector.increment_avg_response(
                            response_stop - response_start)
                        StatisticsCollector.increment_error_packets_sent()
                        StatisticsCollector.increment_responses_sent()
                        continue
                    else:
                        dissection['type'] = 'request'
                        header['function_code'] = data[0]
                        response = request_handler({
                            'header': header,
                            'body': dissection
                        })
                        # add failures to the receiver
                        if not self.simulate_failures():
                            continue
                        s.sendto(response, address)
                        response_stop = time.time()
                        StatisticsCollector.increment_avg_response(
                            response_stop - response_start)
                        StatisticsCollector.increment_responses_sent()
                        self.logger.debug('MB:{} Request: {}'.format(
                            self.port, hexlify(buffer[:7 + length])))
                        self.logger.debug('MB:{} Header: {} Body:{}'.format(
                            self.port, header, dissection))
                        self.logger.debug('MB:{} Responding: {}'.format(
                            self.port, hexlify(response)))
                except IOError as e:
                    self.logger.warning(
                        'An IO error occurred with the socket {}'.format(e))
                    StatisticsCollector.increment_socket_errors()
                    continue
        self.done.set()

    # Return False to not respond
    def simulate_failures(self):
        with self.lock:
            if self.failures.get('stop-responding', False):
                self.logger.info('MB:{} Simulating no-response'.format(
                    self.port))
                return False
            elif self.failures.get('flake-response'):
                val = random.choice([1, 2, 3])
                if val == 1:
                    upper_bound = self.failures['flake-response']
                    sleep_time = random.randint(0, upper_bound) * 0.01
                    self.logger.info(
                        'MB:{} Simulating flake-response "delayed" {}ms'.
                        format(self.port, sleep_time))
                    time.sleep(sleep_time)
                elif val == 2:
                    self.logger.info(
                        'MB:{} Simulating flake-response "no-response"'.format(
                            self.port))
                    return False
            elif self.failures.get('delay-response', False):
                upper_bound = self.failures['delay-response']
                sleep_time = random.randint(0, upper_bound) * 0.01
                self.logger.info('MB:{} Simulating delay-response {}ms'.format(
                    self.port, sleep_time))
                time.sleep(sleep_time)
            return True

    def set_failures(self, failures):
        with self.lock:
            self.failures = failures

    '''
        Starts the Modbus server and listens for packets over a TCP/IP connection. By default it will bind to
        localhost at a port specified in the constructor. Upon receiving a modbus message it will decode the header
        and send the function code and data to the _dissect_packet function for further processing. Error packets
        lead to an immediate response with an error code, while valid requests are sent back to the request handler.
    '''

    def start_server(self, request_handler: Callable) -> None:
        if self.socket_type == socket.SOCK_STREAM:
            self._start_server_tcp(request_handler)
        else:
            self._start_server_udp(request_handler)

    '''
        Breaks the server out of it's blocking accept or recv calls and sets the stop flag.
        In order to do this the method uses a 'dummy' connection to break the blocking call.
        It then sends a 'dummy' message that will lead to the method dropping the request and exiting.
        **NOTE**: This is especially useful when the server is being run on it's own thread.
    '''

    def stop_server(self) -> None:
        self.logger.info('Stopping server now')
        self.stop.set()
        if self._current_connection:
            self._current_connection.close()
        sleep(.5)
        # In order to stop the server we have to interrupt
        # The blocking socket.accept()
        # We create a connection that sends a header for a 0 length
        # Packet
        if not self.done.is_set() and self.socket_type == socket.SOCK_STREAM:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                if self.localhost:
                    s.connect(('localhost', self.port))
                else:
                    s.connect((socket.gethostname(), self.port))
                s.sendall(b'\x00\x01\x00\x00\x00\x00\x00')
                s.close()
class LogicController:
    def __init__(self, name: str, conf: Dict):
        self.plc_name = name
        self.conf = conf
        self.modbus_port = conf['modbus_port']
        self.worker_processes = {}
        self.setup_complete = False
        self.logger = Logger("PLCLogger",
                             "../logger/logs/plc_log.txt",
                             prefix="[{}]".format(self.plc_name))
        self.clock = PLCClock()
        self.register_map = {}

    def __str__(self):
        return "{}:\n{}".format(self.plc_name, self.conf)

    def start_plc(self, modbus_port=None):
        if self.setup_complete:
            self.start_workers()
            self.start_modbus_server(modbus_port)
        else:
            self.logger.warning(
                "PLC has not been initialized, rejecting start up")

    def register_workers(self, selector, publish_queue):
        workers_conf = self.conf['workers']
        for worker_name, attr in workers_conf.items():
            # Invoke the factory to create a new worker
            attr['name'] = worker_name
            worker, response_pipe_r = WorkerFactory.create_new_worker(attr)
            if worker is None:
                continue
            # Add the clock to the workers attributes
            attr['clock'] = self.clock
            # If this worker intends to respond to simulink then
            # Link up it's pipe to the main selector
            if response_pipe_r:
                respond_to = (attr['respond_to']['host'],
                              attr['respond_to']['port'])
                selector.register(response_pipe_r, selectors.EVENT_READ, {
                    "connection_type": "response",
                    "respond_to": respond_to
                })
            # If this worker intends to listen from simulink data then it should give a port
            # A server socket will be set up for this port in the main selector
            # Data destined to this port will be parsed, packaged, and then sent to listening worker processes
            # using the publish_queue
            port = 0
            if attr.get('port', None):
                port = attr['port']
                serverfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                serverfd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                serverfd.bind(('', attr['port']))
                selector.register(serverfd, selectors.EVENT_READ, {
                    "connection_type": "server_socket",
                    "channel": attr['port']
                })
                # Unsure whether creation of thread or starting a thread attaches it to the parent ps
                # If there are performance issues in the simulink interface you can investigate this
            channel = attr.get('channel', port)
            p = threading.Thread(target=worker.run,
                                 args=(publish_queue.register(channel), ))
            self.worker_processes[worker_name] = {
                "process": p,
                "attributes": attr,
                "worker": worker,
            }
            self.register_map[int(attr['register'])] = worker
            self.logger.info("Setting up worker '{}'".format(worker_name))
        self.setup_complete = True

    def start_workers(self):
        for worker_name, info in self.worker_processes.items():
            self.logger.info("Starting up worker '{}'".format(worker_name))
            info['process'].start()

    def start_modbus_server(self, port=None):
        if port is None:
            port = self.conf['modbus_port']

        ENDIANNESS = 'BIG'

        def handle_request(request):
            request_header = request['header']
            request_body = request['body']
            self.logger.debug(
                "Servicing modbus request {}".format(request_header))
            start_register = request_body['address']
            if request_header[
                    'function_code'] == FunctionCodes.WRITE_SINGLE_HOLDING_REGISTER:
                setting = request_body['value']
                worker = self.register_map.get(start_register, None)
                if worker:
                    if hasattr(worker, 'set_reading'):
                        worker.set_reading(setting)
                        self.logger.info(
                            "Setting new pressure reading to {} at {}".format(
                                setting, worker.attributes['name']))
                return modbusencoder.respond_write_registers(
                    request_header, 0, 1, endianness=ENDIANNESS)
            else:
                readings = []
                register_count = request_body['count']
                for current_reg in range(start_register, register_count, 2):
                    worker = self.register_map.get(current_reg, None)
                    if worker:
                        self.logger.info('Retrieving data from {}'.format(
                            worker.attributes['name']))
                        readings.append((worker.get_reading(), 'FLOAT32'))
                self.logger.info(
                    "Responding to request with {}".format(readings))
                return modbusencoder.respond_read_registers(
                    request_header, readings, endianness=ENDIANNESS)

        DEVICE_FUNCTION_CODES = [3, 4, 6, 16]
        modbus_receiver = ModbusReceiver(
            port,
            device_function_codes=DEVICE_FUNCTION_CODES,
            socket_type=socket.SOCK_DGRAM)
        self.logger.info("Starting modbus server for PLC on {}".format(
            self.modbus_port))
        modbus_receiver.start_server(handle_request)