diff --git a/globals.py b/globals.py index fa3334a..46e8024 100644 --- a/globals.py +++ b/globals.py @@ -47,3 +47,7 @@ default_ports = { "xy_port": "COM1", # Default serial port where PSU for X- and Y-Axes is connected "z_port": "COM2", # Default serial port where PSU for Z-Axis is connected } + +# Configuration for socket interface +SOCKET_PORT = 6677 +SOCKET_MAX_CONNECTIONS = 5 \ No newline at end of file diff --git a/main.py b/main.py index 2e8fb42..40275c5 100644 --- a/main.py +++ b/main.py @@ -13,6 +13,7 @@ import User_Interface as ui import globals as g import config_handling as config import csv_logging as log +from socket_control import SocketInterfaceThread def program_end(): # called on exception or when user closes application @@ -55,7 +56,7 @@ try: # start normal operations g.app = HelmholtzGUI() # initialize user interface g.exitFlag = False # tell all functions that the user interface is now running - g.app.state('zoomed') # open UI in maximized window + # g.app.state('zoomed') # open UI in maximized window # g.app.StatusDisplay.continuous_label_update(g.app, 1000) # initiate regular Status Display updates (ms) # ToDo: label update is very slow, commented out to save performance but should be implemented # ToDo!: csv thread + continuous label update seems to exceed capacity of PSU communication @@ -65,6 +66,10 @@ try: # start normal operations ui_print("\nStarting setup...") # do setup again, so it is printed in the UI console ToDo: do it only once func.setup_all() # initiate communication with devices and initialize all major program objects + # Create TCP/Socket listener + socket_controller = SocketInterfaceThread() + socket_controller.start() + g.app.protocol("WM_DELETE_WINDOW", program_end) # call program_end function if user closes the application g.app.mainloop() # start main program loop diff --git a/socket_control.py b/socket_control.py new file mode 100644 index 0000000..24630c2 --- /dev/null +++ b/socket_control.py @@ -0,0 +1,146 @@ +import globals as g +from User_Interface import ui_print +import cage_func as cage_controls + +from threading import Thread +import socket +import numpy as np + +# --- Definition of TCP interface --- +# +# Clients should by default initialize a TCP connection to port 6677 +# The commands shown must be terminated with a single \n (newline) char +# Commands may be split across multiple packets. +# Before useful commands can be sent, declare_api_version must be called. +# +# A description of the TCP api (safety limits are always enforced): +# +# set_raw_field [X comp.] [Y comp.] [Z comp.] +# Returns: 0 or 1 for success +# Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it. +# The field units are Tesla +# This causes an additional field of the given strength to be generated, without regard for the pre-existing +# geomagnetic/external fields. +# +# set_compensated_field [X comp.] [Y comp.] [Z comp.] +# Returns: 0 or 1 for success +# Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it. +# The field units are Tesla +# This causes a field of exactly the given magnitude to be generated by compensating external factors such as the +# geomagnetic field. +# +# set_coil_currents [X comp.] [Y comp.] [Z comp.] +# Returns: 0 or 1 for success +# Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it. +# The field units are Ampere +# This establishes the requested current in the individual coils. +# +# get_api_version +# Returns: a string uniquely identifying each API version. +# This function can be called before declare_api_version. +# Please dont put +# +# declare_api_version [version] +# Returns: 0 or 1 (terminated with newline) +# Declare the api version the client application was programmed for. It must be compatible with the current +# API version. This prevents unexpected behaviour by forcing programmers to specify which API they are expecting. +# This function must be called before sending HW commands. + + +SOCKET_INTERFACE_API_VERSION = "1" + + +class ClientConnectionThread(Thread): + def __init__(self, client_socket, address): + Thread.__init__(self) + self.client_socket = client_socket + self.client_address = address + + self.api_compat = False # Indicates whether the client has a compatible API version + + def run(self): + msg = '' + while True: + raw_msg = self.client_socket.recv(2048).decode() + for char in raw_msg: + if char == '\n': + msg = msg.rstrip() # Some systems will try to send \r characters... looking at you windows O_O + try: + response = self.handle_msg(msg) + except Exception as e: + ui_print("An error occurred while processing a client message") + ui_print("Msg: {}".format(msg)) + ui_print(e) + response = "err" + self.client_socket.sendall((response + '\n').encode('utf-8')) + msg = '' + else: + msg += char + + def handle_msg(self, message): + """ Executes command logic and returns string response (for client). """ + tokens = message.split(" ") + if tokens[0] == "get_api_version": + return SOCKET_INTERFACE_API_VERSION + elif tokens[0] == "declare_api_version": + if tokens[1] == SOCKET_INTERFACE_API_VERSION: + self.api_compat = True + return "1" + else: + ui_print("Declared socket API version ({}) is incompatible with current version ({})!".format(tokens[1], SOCKET_INTERFACE_API_VERSION)) + return "0" + else: + # api_compat indicates we have checked the api version and are ready to accept commands + if self.api_compat: + if tokens[0] == "set_raw_field": + x = float(tokens[1]) + y = float(tokens[2]) + z = float(tokens[3]) + field_vec = np.array([x, y, z], dtype=np.float32) + # uncompensated + cage_controls.set_field_simple(field_vec) + return "1" + elif tokens[0] == "set_compensated_field": + x = float(tokens[1]) + y = float(tokens[2]) + z = float(tokens[3]) + field_vec = np.array([x, y, z], dtype=np.float32) + # compensated + cage_controls.set_field(field_vec) + return "1" + elif tokens[0] == "set_coil_currents": + x = float(tokens[1]) + y = float(tokens[2]) + z = float(tokens[3]) + current_vec = np.array([x, y, z], dtype=np.float32) + cage_controls.set_current_vec(current_vec) + return "1" + else: + # The message given is unknown. The programmer probably did not intend for this, so display an error + # even if is not inherently problematic. + raise Exception("The command '{}' is unknown".format(tokens[0])) + else: + raise Exception("The command '{}' may not be called before 'declare_api_version'".format(tokens[0])) + + +class SocketInterfaceThread(Thread): + def __init__(self): + Thread.__init__(self) + self.server_socket = None + + # Can throw exception, which should be passed on to the instantiator of this class + self.configure_tcp_port() + + def run(self): + while True: + (client_socket, address) = self.server_socket.accept() + new_thread = ClientConnectionThread(client_socket, address) + new_thread.start() + ui_print("Accepted connection from {}".format(address)) + + def configure_tcp_port(self): + # Creates and configures the listening port + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.bind(('', g.SOCKET_PORT)) + self.server_socket.listen(5) # Limit to max. 5 simultaneous connections + ui_print("Listening for TCP connections on port {}".format(g.SOCKET_PORT))