#!/usr/bin/env python3 """Socket Client Telescope (abstract) implementation To be used as a base class (interface) for any concrete socket client telescope class """ ''' UML class Diagram : can be displayed with PlantUML (plugin for Eclipse or for PyCharm) PlantUML: - How to install : https://projects.irap.omp.eu/projects/pyros/wiki/Project_Development#PlantUML - Eclipse plugin : http://plantuml.com/eclipse - class diagrams : http://plantuml.com/class-diagram - sequence diagrams : http://plantuml.com/sequence-diagram - state diagrams : http://plantuml.com/state-diagram - Use Case diagrams : http://plantuml.com/use-case-diagram - OLD Activity diagrams : http://plantuml.com/activity-diagram-legacy - NEW Activity diagrams : http://plantuml.com/activity-diagram-beta - Pre-processing (include...) : http://plantuml.com/preprocessing - GANTT diagrams : http://plantuml.com/gantt-diagram - REAL WORLD EXAMPLES !!! : https://real-world-plantuml.com/ - For Python: - https://github.com/SamuelMarks/python-plantuml - https://pythonhosted.org/plantuml/ UML diagrams theory : https://www.ibm.com/developerworks/rational/library/content/RationalEdge/sep04/bell/index.html @startuml version @enduml @startuml title __**DeviceController and ClientChannel classes diagram (1 - composition)**__ (ClientChannel is a COMPONENT of DeviceController) end title /' Channels '/ ClientChannel <|-- ClientSerial ClientChannel <|-- ClientSocket ClientChannel <|-- ClientUSB abstract class ClientChannel { my_channel # socket or serial or usb... {abstract} connect_to_server() {abstract} read() {abstract} put() put_read() {abstract} send_data() {abstract} receive_data() {abstract} close() } class ClientSocket { my_channel # (socket) -- connect_to_server() send_data() receive_data() close() } /' Abstract Devices Controllers '/ DeviceControllerAbstract o-- ClientChannel DeviceControllerAbstract <|-- TelescopeControllerAbstract DeviceControllerAbstract <|-- PLCControllerAbstract DeviceControllerAbstract <|-- CameraControllerAbstract abstract class DeviceControllerAbstract { my_channel # socket or serial or usb... _cmd = {start, stop, park...} class GenericResult -- {abstract} connect_to_device() {abstract} _format_data_to_send() {abstract} _unformat_received_data() available_commands() execute() execute_generic_cmd() execute_native_cmd() execute_native_cmd() send_data() receive_data() --- **Abstract GET/SET/DO commands :** {abstract} get_timezone(), set_timezone() {abstract} get_date(), set_date() {abstract} get_time(), set_time() {abstract} do_park() {abstract} do_start() {abstract} do_stop() {abstract} do_init() } abstract class TelescopeControllerAbstract { _cmd = {get_ra, get_dec, do_goto...} ---- **Abstract GET/SET/DO commands :** {abstract} get_ack() {abstract} get_ra(), set_ra() {abstract} get_dec(), set_dec() get_radec(), set_radec() {abstract} get_lat(), set_lat() {abstract} get_long(), set_long() {abstract} set_speed() {abstract} do_warm_start() {abstract} do_prec_refr() ---- **Generic MACRO commands:** do_init() do_goto() do_move() } abstract class PLCControllerAbstract { _cmd = {get_status, set_light...} } abstract class CameraControllerAbstract { _cmd = {set_pose, do_start_acq...} } /' Concrete Devices Controllers '/ TelescopeControllerAbstract <|-- TelescopeControllerMeade TelescopeControllerAbstract <|-- TelescopeControllerGemini TelescopeControllerAbstract <|-- TelescopeControllerColibri PLCControllerAbstract <|-- PLCControllerAK PLCControllerAbstract <|-- PLCControllerColibri CameraControllerAbstract <|-- CameraControllerVIS_AK CameraControllerAbstract <|-- CameraControllerCAGIRE CameraControllerAbstract <|-- CameraControllerDDRAGO class TelescopeControllerGemini { _cmd = {get_ra, get_dec, do_goto...} format_data_to_send() unformat_received_data() } TelescopeControllerMeade : _cmd = {get_ra, get_dec, do_goto...} @enduml ''' ''' @startuml title __**DeviceController and ClientChannel classes diagram (2 - multi-inheritance)**__ (TelescopeGemini heritates both from DeviceController and ClientChannel) end title /' Abstract Devices Controllers '/ DeviceControllerAbstract <|-- PLCControllerAbstract DeviceControllerAbstract <|-- CameraControllerAbstract DeviceControllerAbstract <|-- TelescopeControllerAbstract /' Concrete Devices Controllers '/ TelescopeControllerAbstract <|-- TelescopeControllerMeade TelescopeControllerAbstract <|-- TelescopeControllerGemini ClientSocket <|-- TelescopeControllerGemini TelescopeControllerAbstract <|-- TelescopeControllerColibri PLCControllerAbstract <|-- PLCControllerAK PLCControllerAbstract <|-- PLCControllerColibri CameraControllerAbstract <|-- CameraControllerVIS_AK CameraControllerAbstract <|-- CameraControllerCAGIRE CameraControllerAbstract <|-- CameraControllerDDRAGO /' Channels '/ ClientChannel <|-- ClientSocket ClientChannel <|-- ClientSerial ClientChannel <|-- ClientUSB @enduml ''' # Standard library imports #from enum import Enum import functools import logging import socket import sys import time # Third party imports # from sockets_tele/ sys.path.append("..") # from src_socket/client/ sys.path.append("../../..") import src.utils.celme as celme from devices_channel.client.logs import * # Local application imports #sys.path.append('../..') #from src.client.socket_client_abstract import UnknownCommandException, SocketClientAbstract ##from src_socket.client.socket_client_abstract import * ##from src_device.client.client_channel import * from devices_channel.client.client_channel_socket import ClientChannelSocket from devices_channel.client.client_channel_serial import ClientChannelSerial from devices_channel.client.client_channel_usb import ClientChannelUSB # Execute also "set" and "do" commands GET_ONLY=False # Execute only "get" commands #GET_ONLY=True # Default timeouts TIMEOUT_SEND = 10 TIMEOUT_RECEIVE = 10 ''' class c(Enum): # GET, SET DEC = 'DEC' RA = 'RA' RA_DEC = 'RA_DEC' # DO PARK = 'PARK' WARM_START = 'WARM_START' ''' # DECORATOR def generic_cmd(func): #def wrapper_generic_cmd(*args, **kwargs): @functools.wraps(func) def wrapper_generic_cmd(self, values_to_set=None): #print("func name is", func.__name__) return self.execute_generic_cmd(func.__name__, values_to_set) return wrapper_generic_cmd class GenericResult: ''' Usage: res = execute(command) print("result is", res) if res.ko: raise UnexpectedReturnCode() if res.ok: ... ''' # By default, bad result ok = True ko = False def __init__(self, native_result:str, ok=True): self.txt = native_result self.ok = ok self.ko = not ok def __str__(self): return self.txt ''' def __repr__(self): return self.txt def __get__(self, instance, owner): return self.b def __set__(self, instance, value): self.b = value ''' class UnexpectedCommandReturnCode(Exception): pass class TimeoutException(Exception): pass class UnknownCommandException(Exception): pass ''' def __init__(self,*args,**kwargs): super().__init__(self,*args,**kwargs) ''' #TODO: remove ClientChannelAbstract, and set instead a ClientChannel #class DeviceControllerAbstract(SocketClientAbstract): ##class DeviceControllerAbstract(ClientChannel): class DeviceControllerAbstract(): # ClientChannel used by the device controller (to be set during __init__ via set_client_channel()) my_channel = None # @abstract (to be overriden) _cmd_device_concrete = {} _cmd_device_abstract = {} _cmd = { # GET-SET commands: 'get_timezone': [], 'set_timezone': [], 'get_date': [], 'set_date': [], 'get_time': [], 'set_time': [], # DO commands: 'do_init': ['do_init'], 'do_park': [], } ##def __init__(self, device_host:str="localhost", device_port:int=11110, PROTOCOL:str="TCP", buffer_size=1024, DEBUG=False): def __init__(self, device_host:str="localhost", device_port:int=11110, PROTOCOL:str="SOCKET-TCP", buffer_size=1024, DEBUG=False): ''' :param device_host: server IP or hostname :param device_port: server port :param PROTOCOL: "SOCKET-TCP", "SOCKET-UDP", "SERIAL", or "USB" ''' ##super().__init__(device_host, device_port, PROTOCOL, buffer_size, DEBUG) set_logger(DEBUG) log_d("Logger configured") if PROTOCOL.startswith("SOCKET"): self.my_channel:ClientChannel = ClientChannelSocket(device_host, device_port, PROTOCOL, buffer_size, DEBUG) elif PROTOCOL == "SERIAL": self.my_channel:ClientChannel = ClientChannelSerial(device_host, device_port, buffer_size, DEBUG) elif PROTOCOL == "USB": self.my_channel:ClientChannel = ClientChannelUSB(device_host, device_port, buffer_size, DEBUG) else: raise Exception("Unknown Channel", PROTOCOL) # overwrite abstract _cmd dictionary with subclass native _cmd_native dictionary: #self._cmd = {**self._cmd, **self._cmd_native} self._cmd = {**self._cmd, **self._cmd_device_abstract, **self._cmd_device_concrete} # So that we can use this with the "with" statement (context manager) def __enter__(self): return self def __exit__(self, type, value, traceback): self.my_channel.__exit__(type, value, traceback) ''' def set_logger(self, DEBUG): self.my_channel.set_logger(DEBUG) ''' def _connect_to_device(self): self.my_channel._connect_to_server() #@override ClientChannel send_data def send_data(self, data:str): data_encapsulated = self.format_data_to_send(data) self.my_channel.send_data(data_encapsulated) ''' The chosen way to send data is this: # - :GD# b'00030000:GD#\x00' Another way to send data (which also works), is it better ? # - :GD# ###tsock.mysock.sendto(b'\x00\x00\x00\x01\x00\x00\x00\x00\x3A\x47\x44\x23\x00', (HOST, PORT)) # - :GR# ###tsock.mysock.sendto(b'\x00\x00\x00\x01\x00\x00\x00\x00\x3A\x47\x52\x23\x00', (HOST, PORT)) # - ACK 06 OK !!! : tsock.mysock.sendto(b'\x00\x00\x00\x01\x00\x00\x00\x00\x00\x06\x00\x00', (HOST, PORT)) Which one is the best method ? ''' #log_d("NATIVE Command to send is "+repr(data)) ##encapsulated_data = self.encapsulate_data_to_send(data) #print("before _send", encapsulated_data) #print("before _send", repr(encapsulated_data)) ##self._send_data(encapsulated_data) ##self.my_channel.send_data(encapsulated_data) #log_i(f'Sent: {encapsulated_data}') ''' def _send_data(self, data): self.my_channel._send_data(data) ''' #@override ClientChannel receive_data def receive_data(self)->str: ##data_received_bytes = self._receive_data() data_received = self.my_channel.receive_data() #log_d("Received (all data): {}".format(data_received)) #log_d("data in bytes: "+str(bytes(data_received, "utf-8"))) data = self.unformat_received_data(data_received) log_i("RECEIVED (useful data): {}".format(data)) return data ''' def _receive_data(self): return self.my_channel._receive_data() ''' # Encapsulate useful data to be ready for sending # By default, do nothing #@abstract def format_data_to_send(self, data:str): return self.encapsulate_data_to_send(data) #@deprecated def encapsulate_data_to_send(self, data:str): return data # Extract useful data from received raw data # By default, do nothing #@abstract def unformat_received_data(self, data:str): return self.uncap_received_data(data) #@deprecated def uncap_received_data(self, data:str): #return data_received.decode() return data ''' def encapsulate_data_to_send(self, command:str): return self.my_channel.encapsulate_data_to_send(command) def uncap_received_data(self, data_received:str): return self.my_channel.uncap_received_data(data_received) ''' def get_utc_date(self): return celme.Date("now").iso(0) #return celme.Date("now").ymdhms() def close(self): self.my_channel.close() def is_generic_cmd(self, raw_input_cmd:str): ''' is this a generic command ? :param raw_input_cmd: like 'get ra' or 'set ra 20:00:00' or 'set radec 20:00:00 90:00:00" or 'do park' or 'do_park'... :return either False or (cmd, [values]) with cmd like "get_ra" (using underscore '_' instead of space ' ') ''' #return cmd.startswith('get_') or cmd.startswith('set_') or cmd.startswith('do_') #cmds = ['get ', 'get_', 'set ', 'set_', 'do ', 'do_'] ''' seps = (" ", "_") #cmds = list(x+y for x in cmd for y in sep) for cmd in cmds: for sep in seps: generic_cmd = cmd+sep if raw_input_cmd.startswith(generic_cmd): # Is there value(s) passed ? if len(raw_input_cmd) > len(generic_cmd): values = raw_input_cmd[len(generic_cmd):] values = values.split(' ') # return cmd like "get_ra", [and values] return generic_cmd.replace(' ','_'), values return False, False ''' values_to_set = None cmds = ("get","set","do") raw_input_cmd = raw_input_cmd.strip() cmd_splitted = raw_input_cmd.split(' ') if len(cmd_splitted) == 1: return False,False # ex: "set_radec" generic_cmd = cmd_splitted[0] + '_' + cmd_splitted[1] # Check this generic command exists #if (generic_cmd not in self._cmd.keys()): return False,False if generic_cmd not in self._cmd: return False,False # Is there value(s) passed ? if len(cmd_splitted) > 2: values_to_set = cmd_splitted[2:] # ex: return "set_radec", ["20:00:00", "90:00:00"] return generic_cmd, values_to_set def execute_cmd(self, raw_input_cmd:str)->GenericResult: # GENERIC command generic_cmd, values = self.is_generic_cmd(raw_input_cmd) if generic_cmd is not False: #print("GENERIC COMMAND") return self.execute_generic_cmd(generic_cmd, values) else: ''' if cmd.startswith('get_'): #generic_cmd,_ = request[4:].split('(') generic_cmd = cmd[4:] if (generic_cmd not in self._cmd_getset.keys()) and (generic_cmd not in self._cmd_do.keys()): #eval(request) return self.get_radec() print("cmd is", generic_cmd) return self._get(generic_cmd) return ''' # NATIVE command #print("NATIVE COMMAND") res_native = self.execute_native_cmd(raw_input_cmd) return GenericResult(res_native) #def execute_native_cmd(self, request:str, awaited_res_if_ok=None)->GenericResult: def execute_native_cmd(self, native_cmd:str)->str: print("NATIVE Command to send is "+ repr(native_cmd)) #self.send_request(native_cmd) self.send_native_cmd(native_cmd) native_res = self.receive_data() return native_res ''' ok = True if not awaited_res_if_ok else (native_res == awaited_res_if_ok) return GenericResult(native_res, ok) ''' ''' def execute_native_cmd_OLD(self, request:str)->str: self.send_request(request) native_res = self.receive_data() return native_res ''' def execute_unformated_native_cmd(self, request:str)->str: request = self.formated_cmd(request) #return self.execute_native_cmd_OLD(request) return self.execute_native_cmd(request) def send_native_cmd(self, native_cmd:str)->str: return self.send_data(native_cmd) #@deprecated def send_request(self, request:str)->str: return self.send_native_cmd(request) def print_available_commands(self): print("\nAvailable commands are:") print("- GET commands:") print (list(cmd.replace('_',' ') for cmd in self._cmd.keys() if cmd.startswith('get_'))) print("- SET commands:") print (list(cmd.replace('_',' ') for cmd in self._cmd.keys() if cmd.startswith('set_'))) print("- DO commands:") print (list(cmd.replace('_',' ') for cmd in self._cmd.keys() if cmd.startswith('do_'))) def available_commands(self): return list(self._cmd.keys()) # @abstract def formated_cmd(self, cmd:str, value:str=None)->str: return cmd #def run_func(self, func, arg=None): def run_func(self, func, *args): #print("args", args) if args: return getattr(self, func)(*args) else: return getattr(self, func)() ''' TELESCOPE COMMANDS (abstract methods) ''' def execute_generic_cmd(self, generic_cmd:str, values_to_set:str=None)->str: ''' Execute a generic command :param generic_cmd: str like "get_ra" or "set_ra" or "do_park"... :param value: only for a "set_" cmd ''' #log_d("\n\nGENERIC Command to send is "+generic_cmd) print("\n\nGENERIC Command to send is "+generic_cmd) # Check if generic_param exists #if generic_cmd not in self._cmd.keys(): raise UnknownCommandException() # if this generic command has no corresponding native command, raise NotImplementedError native_cmd_infos = self._cmd[generic_cmd] if not native_cmd_infos: raise NotImplementedError # Get corresponding native command: native_cmd = native_cmd_infos[0] if not native_cmd: raise NotImplementedError # ex: native_cmd == "do_init", "get_radec" if native_cmd == generic_cmd: #print("cmd,val", native_cmd, values_to_set) #res:GenericResult = self.run_func(native_cmd, *values_to_set) if values_to_set: res = self.run_func(native_cmd, *values_to_set) #res = getattr(self, native_cmd)(values_to_set) else: res = self.run_func(native_cmd) #res = getattr(self, native_cmd)() #if res is None: res = 'ok' # res should be a GenericResult if not isinstance(res,GenericResult): raise Exception("Should be a GenericResult", res) return res # ex: native_cmd == "GR" else: native_cmd = self.formated_cmd(native_cmd,values_to_set) awaited_res_if_ok = None if len(native_cmd_infos) > 1: awaited_res_if_ok = native_cmd_infos[1] #native_res = self.execute_native_cmd(self.formated_cmd(native_cmd,value), awaited_res_ok) native_res = self.execute_native_cmd(native_cmd) ok = True if not awaited_res_if_ok else (native_res == awaited_res_if_ok) return GenericResult(native_res, ok) ''' **************************** **************************** GENERIC TELESCOPE COMMANDS (abstract methods) **************************** **************************** ''' ''' **************************** GENERIC GET & SET commands **************************** ''' @generic_cmd def get_timezone(self): pass #def get_timezone(self): return self.execute_generic_cmd('get_timezone') @generic_cmd def set_timezone(self, hh): pass #def set_timezone(self, hh): return self.execute_generic_cmd('set_timezone', hh) @generic_cmd def get_date(self): pass @generic_cmd def set_date(self, mmddyy): pass @generic_cmd def get_time(self): pass @generic_cmd def set_time(self, hhmmss): pass ''' **************************** GENERIC DO commands **************************** ''' # @abstract #def do_INIT(self): return self._do("INIT") ''' do_PARK() (p103) - STARTUP position = CWD - :hC# - position required for a Cold or Warm Start, pointing to the celestial pole of the given hemisphere (north or south), with the counterweight pointing downwards (CWD position). From L4, V1.0 up - HOME position parking => par defaut, c'est CWD, mais ca peut etre different - :hP# - defaults to the celestial pole visible at the given hemisphere (north or south) and can be set by the user ''' # @abstract def do_PARK(self): pass #def do_PARK(self): return self._do("PARK") # @abstract def do_start(self): pass def do_stop(self): pass # @abstract MACRO def do_init(self): return NotImplementedError # TODO: empecher de creer une instance de cette classe abstraite # Avec ABC ? ''' if __name__ == "__main__": #HOST, PORT = "localhost", 9999 #HOST, PORT = "localhost", 20001 HOST, PORT = "localhost", 11110 # Classic usage: #tsock = SocketClient_UDP_TCP(HOST, PORT, "UDP") # More elegant usage, using "with": with SocketClient_ABSTRACT(HOST, PORT, "UDP") as tsock: # 0) CONNECT to server (only for TCP, does nothing for UDP) tsock._connect_to_server() while True: # 1) SEND REQUEST data to server # saisie de la requête au clavier et suppression des espaces des 2 côtés data = input("REQUEST TO SERVER [ex: ':GD#' (Get Dec), ':GR#' (Get RA)']: ").strip() # test d'arrêt if data=="": break #data_to_send = bytes(data + "\n", "utf-8") tsock.send_data(data) #mysock.sendto("%s" % data, (HOST, PORT)) #print("Sent: {}".format(data)) # 2) RECEIVE REPLY data from server data_received = tsock.receive_data() #reponse, adr = mysock.recvfrom(buf) #print("Received: {}".format(data_received)) #print("Useful data received: {}".format(data_useful)) print('\n') #tsock.close() '''