From 0e7b2e49585e3725736ae380cbd2dfb28fea5093 Mon Sep 17 00:00:00 2001 From: Paul Oliver Date: Thu, 29 Feb 2024 02:29:14 +0100 Subject: Added 'Common.py' module. [#27] Not exactly a 'tappable pipe', this simple communications module allows for IPC between individual Salis organisms and different simulations via UDP sockets. --- bin/modules/common.py | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 bin/modules/common.py (limited to 'bin/modules/common.py') diff --git a/bin/modules/common.py b/bin/modules/common.py new file mode 100644 index 0000000..25163d1 --- /dev/null +++ b/bin/modules/common.py @@ -0,0 +1,174 @@ +""" SALIS: Viewer/controller for the SALIS simulator. + +File: common.py +Author: Paul Oliver +Email: paul.t.oliver.design@gmail.com + +Network communications module for Salis simulator. This module allows for IPC +between individual Salis organisms and different simulations via UDP sockets. +""" + +import json +import os +import socket + +from ctypes import c_int, c_uint8, CFUNCTYPE + + +class Common: + SENDER_TYPE = CFUNCTYPE(c_int, c_uint8) + RECEIVER_TYPE = CFUNCTYPE(c_uint8) + + def __init__(self, sim, max_buffer_size=4096): + """ Initialize module with a default buffer size of 4 KiB. + """ + self.__sim = sim + self.__settings_path = self.__get_settings_path() + self.max_buffer_size = max_buffer_size + self.in_buffer = bytearray() + self.out_buffer = bytearray() + self.sources = [] + self.targets = [] + + # Use a global client socket for all output operations. + self.__client = self.__get_socket() + + def define_functors(self): + """ Define the C callbacks which we'll pass to the Salis simulator. + These simply push and pop instructions from the input and output + buffers whenever organisms call the SEND and RCVE instructions. + """ + def sender(inst): + if len(self.out_buffer) < self.max_buffer_size: + self.out_buffer.append(inst) + + def receiver(): + if len(self.in_buffer): + res = self.in_buffer[0] + self.in_buffer = self.in_buffer[1:] + return c_uint8(res) + else: + return c_uint8(0) + + self.__sender = self.SENDER_TYPE(sender) + self.__receiver = self.RECEIVER_TYPE(receiver) + self.__sim.lib.sal_comm_set_sender(self.__sender) + self.__sim.lib.sal_comm_set_receiver(self.__receiver) + + def add_source(self, address, port): + """ Create new input socket. + """ + sock = self.__get_server(address, port) + self.sources.append(sock) + + def add_target(self, address, port): + """ Create new output address/port tuple. We use global output socket + ('self.__client') for output operations. + """ + self.targets.append((address, port)) + + def remove_source(self, address, port): + """ Remove an input socket. + """ + source = (address, port) + self.sources = [s for s in self.sources if s.getsockname() != source] + + def remove_target(self, address, port): + """ Remove an output address/port pair. + """ + target = (address, port) + self.targets = [t for t in self.targets if t != target] + + def link_to_self(self, port): + """ Create input and output links to 'localhost'. + """ + self.add_source(socket.gethostbyname(socket.gethostname()), port) + self.add_target(socket.gethostbyname(socket.gethostname()), port) + + def cycle(self): + """ We push all data on the output buffer to all targets and clear it. + We withdraw incoming data from all source sockets and append it to the + input buffer. + """ + if len(self.out_buffer) and self.targets: + for target in self.targets: + self.__client.sendto(self.out_buffer, target) + + # Clear output buffer. + self.out_buffer = bytearray() + + # Receive data and store on input buffer. + if len(self.in_buffer) < self.max_buffer_size: + for source in self.sources: + try: + self.in_buffer += source.recv( + self.max_buffer_size - len(self.in_buffer) + ) + except socket.error: + pass + + def load_network_config(self, filename): + """ Load network configuration from a JSON file. + """ + with open(os.path.join(self.__settings_path, filename), "r") as f: + in_dict = json.load(f) + + self.max_buffer_size = in_dict["max_buffer_size"] + + for source in in_dict["sources"]: + self.add_source(*source) + + for target in in_dict["targets"]: + self.add_target(*target) + + for inst in in_dict["in_buffer"]: + self.in_buffer.append(self.__sim.handler.inst_dict[inst]) + + for inst in in_dict["out_buffer"]: + self.out_buffer.append(self.__sim.handler.inst_dict[inst]) + + def save_network_config(self, filename): + """ Save network configuration to a JSON file. + """ + out_dict = { + "max_buffer_size": self.max_buffer_size, + "in_buffer": "", + "out_buffer": "", + "sources": [s.getsockname() for s in self.sources], + "targets": self.targets, + } + + for byte in self.in_buffer: + out_dict["in_buffer"] += self.__sim.printer.inst_list[byte][1] + + for byte in self.out_buffer: + out_dict["out_buffer"] += self.__sim.printer.inst_list[byte][1] + + with open(os.path.join(self.__settings_path, filename), "w") as f: + json.dump(out_dict, f, indent="\t") + + + ############################### + # Private methods + ############################### + + def __get_settings_path(self): + """ Get path to network settings directory. + """ + self_path = os.path.dirname(__file__) + return os.path.join(self_path, "../network") + + def __get_socket(self): + """ Generate a non-blocking UDP socket. + """ + sock = socket.socket( + socket.AF_INET, socket.SOCK_DGRAM | socket.SOCK_NONBLOCK + ) + return sock + + def __get_server(self, address, port): + """ Generate a socket and bind to an address/port pair. + """ + serv_socket = self.__get_socket() + serv_socket.bind((address, port)) + return serv_socket -- cgit v1.2.1