diff --git a/.gitignore b/.gitignore index 420512a..ff4b9b9 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ var/ # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest -*.spec \ No newline at end of file +*.spec +*.exe \ No newline at end of file diff --git a/Readme.md b/Readme.md index 7ef7f03..91ad5f3 100644 --- a/Readme.md +++ b/Readme.md @@ -52,7 +52,7 @@ Pydatalink is an application whose functions are similar to RxTools' Datalink, b The second major feature of this version is that the code is entirely open source, unlike the Datalink code. The aim of making the project available as open source is to allow the community to contribute to the development of the tool in order to integrate new functionalities.
- +

@@ -84,28 +84,19 @@ Once you've installed python, all you have to do is download the source code and ### Using git clone ``` -git clone https://github.com/septentrio-gnss/DataLink.git -cd DataLink +git clone https://github.com/septentrio-gnss/Septentrio-PyDataLink.git +cd Septentrio-PyDataLink ``` ### using GitHub - First click on **code**.
- Then click on **dowload Zip** -### (Optional) Create a Virtual environement -This will allow you to create a contained workspace where every python package will be installed -``` -python -m venv venv -source venv/bin/activate -``` -### Install Python packages -``` -pip install -r requirements.txt -``` -### Run pyDatalink -By default pyDatalink run as a Graphical interface +## Build the project +to build and generate the executable file , run the folowing command ``` -python pyDatalinkApp.py +python build.py ``` +After the build is successfully completed , a executable file will be generated
diff --git a/build.py b/build.py new file mode 100644 index 0000000..218d077 --- /dev/null +++ b/build.py @@ -0,0 +1,61 @@ +import os +import subprocess +import sys +import shutil +import venv +from src.constants import * + +# Define paths and script name +script_name = MAINSCRIPTPATH +icon_path = os.path.join(DATAFILESPATH, 'pyDatalink_icon.ico') +output_directory = PROJECTPATH +requirements_file = 'requirements.txt' +spec_file = APPNAME + ".spec" +venv_dir = 'venv' + + + +print("Create a virtual environment") +venv.create(venv_dir, with_pip=True) + +print("Activate virtual environment") +activate_script = os.path.join(venv_dir, 'Scripts', 'activate') if sys.platform == 'win32' else os.path.join(venv_dir, 'bin', 'activate') + +print("Install required python packages") +subprocess.run([sys.executable, '-m', 'pip', 'install', '-r', requirements_file]) + +print("Create the executable") +pyinstaller_command = [ + 'pyinstaller', + '--name=' + APPNAME, + '--onefile', + '--icon=' + icon_path, + '--distpath=' + output_directory, + '--clean', + '--noconfirm', + "--noconsole", + "--add-data=" + DATAFILESPATH +";data", + script_name +] + +status = subprocess.run(pyinstaller_command) + +if os.path.exists('build'): + shutil.rmtree('build') +if os.path.exists(spec_file): + os.remove(spec_file) + +if sys.platform == 'win32': + deactivate_script = os.path.join(venv_dir, 'Scripts', 'deactivate.bat') +else: + deactivate_script = os.path.join(venv_dir, 'bin', 'deactivate') + + +subprocess.run(deactivate_script, shell=True) + + +shutil.rmtree(venv_dir) +if status.returncode == 0 : + print("Build completed successfully!") +else : + print("Error while building the project") diff --git a/dev/README.md b/dev/README.md index 0987b01..9a86f1f 100644 --- a/dev/README.md +++ b/dev/README.md @@ -39,7 +39,7 @@ yes , The very purpose of this project is to remake a version of pydatalink with # What is pyDatalink
- +

@@ -164,8 +164,8 @@ Once you've installed python, all you have to do is download the source code and ### Using git clone ``` -git clone https://github.com/septentrio-gnss/DataLink.git -cd DataLink +git clone https://github.com/septentrio-gnss/Septentrio-PyDataLink.git +cd Septentrio-PyDataLink ``` ### using GitHub - First click on **code**.
@@ -184,7 +184,7 @@ pip install -r requirements.txt By default pyDatalink run as a Graphical interface ``` -python pyDatalinkApp.py +python pyDatalink.py ``` # User Manual diff --git a/dev/Todo b/dev/Todo deleted file mode 100644 index 75d93eb..0000000 --- a/dev/Todo +++ /dev/null @@ -1,5 +0,0 @@ -General - -Graphical Interface - -- remember last tab in configuration window \ No newline at end of file diff --git a/dev/doc_sources/pyDatalink.PNG b/dev/doc_sources/pyDatalink.PNG index 86f58f2..c4cd001 100644 Binary files a/dev/doc_sources/pyDatalink.PNG and b/dev/doc_sources/pyDatalink.PNG differ diff --git a/doc_sources/pyDatalink.PNG b/doc_sources/pyDatalink.PNG index 86f58f2..c4cd001 100644 Binary files a/doc_sources/pyDatalink.PNG and b/doc_sources/pyDatalink.PNG differ diff --git a/pyDatalink.py b/pyDatalink.py new file mode 100644 index 0000000..4107c3f --- /dev/null +++ b/pyDatalink.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# ############################################################################### +# +# Copyright (c) 2024, Septentrio +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import sys +import os +import argparse +from src.constants import DATAPATH , CONFIGPATH , LOGFILESPATH , DEFAULTCONFIGFILE , MAXFILENUMBER +from src.StreamConfig.App import App , ConfigurationType +try : + from PySide6.QtWidgets import QApplication + from src.UserInterfaces.GraphicalUserInterface import GraphicalUserInterface + GUI = True +except Exception as e: + GUI = False + raise e + +try : + from src.UserInterfaces.TerminalUserInterface import TerminalUserInterface + TUI = True +except NotImplementedError : + TUI = False + +from src.UserInterfaces.CommandLineInterface import CommandLineInterface + +def clean_log_folder(): + files = [os.path.join(LOGFILESPATH, f) for f in os.listdir(LOGFILESPATH) if os.path.isfile(os.path.join(LOGFILESPATH, f))] + while len(files) > MAXFILENUMBER: + oldest_file = min(files, key=os.path.getctime) + os.remove(oldest_file) + files = [os.path.join(LOGFILESPATH, f) for f in os.listdir(LOGFILESPATH) if os.path.isfile(os.path.join(LOGFILESPATH, f))] + + + +def check_data_folder(): + if os.path.exists(DATAPATH) is not True : + os.mkdir(DATAPATH) + if os.path.exists( CONFIGPATH ) is not True: + os.mkdir(CONFIGPATH ) + if os.path.exists( LOGFILESPATH ) is not True: + os.mkdir(LOGFILESPATH ) + else : + clean_log_folder() + +class DatalinkApp: + """Main class for Datalink application + """ + + def __init__(self , config_args) -> None: + self.config_args = config_args + self.app : App = None + self.user_interface = None + self.show_data_port = None + if self.config_args.Mode == "CMD" : + if self.config_args.Streams is None : + print("Error : you need to specify the streams to configure\n") + return + else : + if self.config_args.ShowData is not None: + try : + show = int(self.config_args.ShowData) + except ValueError as exc: + print(f"Error : streams stream_id \"{self.config_args.ShowData}\" is not correct , please enter a valid ID !") + raise exc + + self.app = App(max_stream=len(self.config_args.Streams),stream_settings_list=self.config_args.Streams , configuration_type= ConfigurationType.CMDLINE) + if self.config_args.ShowData is not None : + if show <= len(self.app.stream_list): + self.show_data_port=self.app.stream_list[show - 1] + self.show_data_port.toggle_all_data_visibility() + + else : + if os.path.exists(self.config_args.ConfigPath): + self.app = App(configuration_type= ConfigurationType.FILE,config_file=self.config_args.ConfigPath, debug_logging=True) + elif os.path.exists(DEFAULTCONFIGFILE) : + self.app = App(configuration_type= ConfigurationType.FILE , config_file=DEFAULTCONFIGFILE , debug_logging=True) + else : + self.app = App(debug_logging=True) + + def start(self) -> None : + if self.config_args.Mode == "TUI": + self.datalink__terminal_start() + elif self.config_args.Mode == "GUI": + self.datalink_graphical_start() + elif self.config_args.Mode == "CMD": + self.datalink_cmdline_start() + + def datalink__terminal_start(self): + """Start Datalink as a Graphical User interface + """ + if os.name == "posix" and TUI: + self.user_interface = TerminalUserInterface(self.app) + sys.exit(self.user_interface.MainMenu()) + elif not TUI: + print("simple-Term-menu is required to run in TUI mode \nInstall Simple-Term-Menu : pip install simple-term-menu\nOr run the App in a Different mode (-m GUI or -m CMD)") + else : + print("Sorry the terminal version of Data link is only available on Unix distro") + + def datalink_graphical_start(self): + """Start Datalink as a Graphical User interface + """ + if(GUI): + self.user_interface = QApplication() + gallery = GraphicalUserInterface(self.app) + gallery.show() + sys.exit(self.user_interface.exec()) + else : + print("PySide6 is required to run in GUI mode \nInstall pyside : pip install PySide6 \nOr run the App in a Different mode (-m CMD or -m TUI)") + + def datalink_cmdline_start(self): + """Start Datalink as a command line interface + """ + self.user_interface = CommandLineInterface(self.app , show_data_id= self.show_data_port) + sys.exit(self.user_interface.run()) + +if __name__ == "__main__": + check_data_folder() + + parser = argparse.ArgumentParser(prog="PyDatalink" ,description='') + parser.add_argument('--Mode','-m', choices=['TUI', 'GUI', 'CMD'], default='GUI', + help="Start %(prog)s with a specific interface (DEFAULT : GUI)") + parser.add_argument('--ConfigPath','-c', action='store', default= DEFAULTCONFIGFILE , + help='Path to the config file ( This Option won\'t be use when in CMD mode ) ') + + parser.add_argument('--Streams' ,'-s', nargs='*' , action='store' , + help="List of streams to configure , the size of this list is configure by --nbPorts\n ,this parameter is only used when in CMD mode \n ") + parser.add_argument('--ShowData', "-d" ,nargs="?", action="store", + help="Lisf of streams stream_id, will print every input and output data from the streams\n ,this parameter is only used when in CMD mode ") + + DatalinkApp(config_args=parser.parse_args()).start() + diff --git a/pyDatalinkApp.py b/pyDatalinkApp.py deleted file mode 100644 index 4f5b62c..0000000 --- a/pyDatalinkApp.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python -# ############################################################################### -# -# Copyright (c) 2024, Septentrio -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# 3. Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import threading -import sys -from src.StreamConfig import * -from src import constants -try : - from PySide6.QtWidgets import QApplication - from src.UserInterfaces import GraphicalUserInterface - GUI = True -except : - GUI = False -import os -import shutil -import argparse -import time -try : - from src.UserInterfaces import TerminalUserInterface - TUI = True -except : - TUI = False -from src.UserInterfaces import CommandLineInterface - - - -def checkDataFolder(): - if os.path.exists(constants.DATAPATH) is not True : - os.mkdir(constants.DATAPATH) - if os.path.exists( constants.CONFIGPATH ) is not True: - os.mkdir(constants.CONFIGPATH ) - if os.path.exists( constants.PROJECTPATH+ "/conf/pydatalink.conf") and not os.path.exists(constants.DEFAULTCONFIGFILE): - shutil.copy(constants.PROJECTPATH+ "/conf/pydatalink.conf" ,constants.DEFAULTCONFIGFILE ) - if os.path.exists( constants.LOGFILESPATH ) is not True: - os.mkdir(constants.LOGFILESPATH ) - - - -class DatalinkApp: - - - def __init__(self) -> None: - self.Streams = None - self.userInterface = None - self.showDataPort = None - if args.Mode == "CMD" : - if args.Streams is None : - print("Error : you need to specify the streams to configure\n") - return - else : - if args.ShowData is not None: - try : - show = int(args.ShowData) - except Exception as e : - print(f"Error : streams id \"{args.ShowData}\" is not correct , please enter a valid ID !") - return - - self.Streams = Streams(maxStream=len(args.Streams),streamSettingsList=args.Streams) - if args.ShowData is not None : - if show <= len(self.Streams.StreamList): - self.showDataPort=self.Streams.StreamList[show - 1] - self.showDataPort.ToggleAllDataVisibility() - - else : - if os.path.exists(args.ConfigPath): - self.Streams = Streams(configFile=args.ConfigPath) - elif os.path.exists(constants.DEFAULTCONFIGFILE) : - self.Streams = Streams(configFile=constants.DEFAULTCONFIGFILE) - else : - self.Streams = Streams() - self.Start() - - def Start(self) -> None : - if args.Mode == "TUI": - self.DatalinkInTerminalStart() - elif args.Mode == "GUI": - self.DatalinkInGuiStart() - elif args.Mode == "CMD": - self.DatalinkInCmdStart() - - - def DatalinkInTerminalStart(self): - if os.name == "posix" and TUI: - self.userInterface = TerminalUserInterface(self.Streams) - sys.exit(self.userInterface.MainMenu()) - - elif not TUI: - print(f"simple-Term-menu is required to run in TUI mode \nInstall Simple-Term-Menu : pip install simple-term-menu\nOr run the App in a Different mode (-m GUI or -m CMD)") - else : - print(f"Sorry the terminal version of Data link is only available on Unix distro") - - def DatalinkInGuiStart(self): - if(GUI): - self.userInterface = QApplication() - gallery = GraphicalUserInterface.GraphicalUserInterface(self.Streams) - gallery.show() - sys.exit(self.userInterface.exec()) - else : - print(f"PySide6 is required to run in GUI mode \nInstall pyside : pip install PySide6 \nOr run the App in a Different mode (-m CMD or -m TUI)") - - def DatalinkInCmdStart(self): - self.userInterface = CommandLineInterface.CommandLineInterface(self.Streams , showdataId = self.showDataPort) - sys.exit(self.userInterface.run()) - - - -if __name__ == "__main__": - checkDataFolder() - MinPorts = 1 - MaxPorts = 6 - - parser = argparse.ArgumentParser(prog="PyDatalink" ,description='') - parser.add_argument('--Mode','-m', choices=['TUI', 'GUI', 'CMD'], default='GUI', - help="Start %(prog)s with a specific interface (DEFAULT : GUI)") - parser.add_argument('--ConfigPath','-c', action='store', default= constants.DEFAULTCONFIGFILE , - help='Path to the config file ( This Option won\'t be use when in CMD mode ) ') - - parser.add_argument('--Streams' ,'-s', nargs='*' , action='store' , - help="List of streams to configure , the size of this list is configure by --nbPorts\n ,this parameter is only used when in CMD mode \n ") - parser.add_argument('--ShowData', "-d" ,nargs="?", action="store", - help="Lisf of streams id, will print every input and output data from the streams\n ,this parameter is only used when in CMD mode ") - - - args = parser.parse_args() - App = DatalinkApp() - - - - \ No newline at end of file diff --git a/src/Configuration/CommandLineConfiguration.py b/src/Configuration/CommandLineConfiguration.py new file mode 100644 index 0000000..1a341da --- /dev/null +++ b/src/Configuration/CommandLineConfiguration.py @@ -0,0 +1,240 @@ +# ############################################################################### +# +# Copyright (c) 2024, Septentrio +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from src.StreamSettings import TcpSettings , UdpSettings +from src.StreamSettings.SerialSettings import SerialSettings , BaudRate, ByteSize, Parity, StopBits +from ..NTRIP import NtripClient, NtripSettings +from ..StreamConfig.Stream import StreamType , Stream + +class CommandLineConfigurationException(Exception): + """ + Exception class for CommandLine Configuration + """ + def __init__(self, message, error_code = None): + super().__init__(message) + self.error_code = error_code + +class IncorrectStreamException(CommandLineConfigurationException): + """Raised when the given stream type is not supported or incorect + """ +class ConfigurationException(CommandLineConfigurationException): + """Raised when the configuration failed + """ +class PortLinkedException(CommandLineConfigurationException): + """Raised when adding link to a stream failed + """ + +class BeginStreamException(CommandLineConfigurationException): + """Raised when the startup of the stream failed + """ + +class MissingParameterException(CommandLineConfigurationException) : + """Raised when a parameter is missing """ + +class InccorectParameterException(CommandLineConfigurationException) : + """Raised when stream settings are incorrect """ + +def command_line_config(stream : Stream, command_line : str): + """ + Configure a Stream with a single line of configuration + + Args: + command_line_config (str): Configuration line + + """ + port_to_link = [] + try : + stream_type :str = command_line.split("://")[0] + config: str = command_line.split("://")[1] + if '#' in config : + port_to_link = config.split("#")[1].split(",") + config = config.split("#")[0] + if stream_type.lower() == "serial": + config_serial_stream(stream , command_config=config) + elif stream_type.lower() == "tcpcli": + config_tcp_stream(stream ,is_server=False , command_config=config) + elif stream_type.lower() == "tcpsrv": + config_tcp_stream(stream ,is_server=True, command_config=config) + elif stream_type.lower() == "udp": + config_udp_stream(stream ,command_config=config) + elif stream_type.lower() == "udpspe": + config_udp_stream(stream ,specific_host=True, command_config= config) + elif stream_type.lower() == "ntrip": + config_ntrip_stream(stream ,config) + else : + raise IncorrectStreamException("Stream type not found or incorrect") + except Exception as e : + raise ConfigurationException(f"Config line is incorrect : {e}") from e + + if len(port_to_link) > 0 : + for i in port_to_link: + try : + link = int(i) + if link != stream.stream_id: + stream.linked_ports.append(link) + except (TypeError, ValueError) as e : + raise PortLinkedException(e) from e + try: + stream.connect(stream.stream_type) + except Exception as e: + raise BeginStreamException(e) from e + +def config_ntrip_stream(stream : Stream ,command_config : str ): + """ + Init a NTRIP Client with a configuration line + + Args: + command_config (str): configuration line + """ + credentials = command_config.split("@")[0].split(":") + if len(credentials) != 2 : + raise MissingParameterException("Missing a credential paremeter !") + + host = command_config.split("@")[1].split(":") + if len(host) != 2: + raise MissingParameterException("Missing a host paremeter !") + + mountpoint = command_config.split("@")[1].split("/") + if len(mountpoint) != 2: + raise MissingParameterException("Missing a MountPoint paremeter !") + + try : + settings = NtripSettings(host = host[0], port = int(host[1].split("/")[0]), + auth= (True if len(credentials[0]) > 0 and len(credentials[1]) > 0 else False), + username= credentials[0],password= credentials[1], + mountpoint=mountpoint[1]) + stream.ntrip_client = NtripClient(settings) + stream.stream_type = StreamType.NTRIP + except Exception as e : + raise InccorectParameterException(f"Parameters for a NTRIP Client stream are incorrect : \n{e}") from e + +def config_udp_stream(stream : Stream,specific_host : bool = False, command_config : str = None): + """ + Init a UDP stream with a configuration line + Args: + specific_host (bool, optional): if True the UDP Stream will stream only to a specific host name. Defaults to False. + command_config (str, optional): Configuration line. Defaults to None. + + Raises: + Exception: too few or too much parameter + Exception: Given parameter incorrect + Exception: Given parameter incorrect (Specific host) + """ + if specific_host : + config = command_config.split(":")[0] + if len(config) != 2 : + if len(config) > 2 : + raise MissingParameterException("Too much parameters for a UDP Stream") + else : + raise MissingParameterException("Not enough parameters for a UDP Stream") + else : + try: + stream.udp_settings = UdpSettings.UdpSettings(host=config[0], port=int(config[1]), specific_host=specific_host) + stream.stream_type = StreamType.UDP + except Exception as e : + raise InccorectParameterException(f"Parameters for a UDP stream are incorrect : \n{e}") from e + else : + try: + stream.udp_settings = UdpSettings.UdpSettings(port=int(command_config)) + stream.stream_type = StreamType.UDP + except Exception as e : + raise InccorectParameterException(f"Parameter for a UDP stream are incorrect : \n{e}") from e + +def config_tcp_stream(stream : Stream,is_server : bool = False ,command_config : str= None) -> None: + """ + Init a TCP stream with a configuration line + Args: + is_server (bool, optional): If True the TCP stream will be configure as a Server. Defaults to False. + command_config (str, optional): Configuration line . Defaults to None. + + Raises: + Exception: too few or too much parameter + Exception: Given parameter incorrect (Server) + Exception: Given parameter incorrect (Client) + """ + if is_server : + config = command_config.split(":") + if len(config) != 2 : + if len(config) > 2 : + raise MissingParameterException("Too much parameters for a TCP Stream") + else : + raise MissingParameterException("Not enough parameters for a TCP Stream") + try : + stream.tcp_settings = TcpSettings.TcpSettings(host=config[0] , port=int(config[1]), stream_mode= TcpSettings.StreamMode.SERVER) + except Exception as e : + raise InccorectParameterException(f"Parameters for a TCP Server stream are incorrect : \n{e}") from e + else : + try : + stream.tcp_settings = TcpSettings.TcpSettings(port=int(command_config) , stream_mode= TcpSettings.StreamMode.CLIENT) + except Exception as e : + raise InccorectParameterException(f"Parameters for a TCP Client stream are incorrect : \n{e}") from e + stream.stream_type = StreamType.TCP + +def config_serial_stream(stream : Stream,command_config :str) -> None: + """ + Init a Serial stream with a configuration line + exemple : + Args: + command_config (str): Configuration line + + Raises: + Exception: If too few argument for a proper configuration + Exception: If given argument are Incorrect + """ + config = command_config.split(":") + if len(config) != 6 : + if len(config) > 6 : + raise MissingParameterException("Too much parameters for a Serial Stream") + else: + raise MissingParameterException("Not enough parameters for a Serial Stream") + + port : str = config[0] + try : + baudrate : BaudRate = BaudRate(config[1]) + except : + baudrate : BaudRate = BaudRate.eBAUD115200 + try : + parity : Parity = Parity(config[2]) + except : + parity : Parity = Parity.PARITY_NONE + try : + stopbits : StopBits = StopBits(config[3]) + except : + stopbits : StopBits = StopBits.STOPBITS_ONE + try : + bytesize : ByteSize= ByteSize(config[4]) + except : + bytesize : ByteSize = ByteSize.EIGHTBITS + + rtscts : bool = True if config[5] == "1" else False + try : + stream.serial_settings = SerialSettings(port = port , baudrate=baudrate , parity=parity , stopbits=stopbits,bytesize=bytesize , rtscts=rtscts) + stream.stream_type = StreamType.Serial + except Exception as e : + raise InccorectParameterException(f"Parameters for a Serial stream are incorrect : \n{e}") from e \ No newline at end of file diff --git a/src/Configuration/FileConfiguration.py b/src/Configuration/FileConfiguration.py new file mode 100644 index 0000000..bc368e5 --- /dev/null +++ b/src/Configuration/FileConfiguration.py @@ -0,0 +1,261 @@ +# ############################################################################### +# +# Copyright (c) 2024, Septentrio +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import base64 +import configparser +from ..NTRIP import NtripSettings , NtripClient +from ..StreamSettings.TcpSettings import StreamMode , TcpSettings +from ..StreamSettings.UdpSettings import DataFlow , UdpSettings +from ..StreamSettings.SerialSettings import ByteSize, Parity, BaudRate, StopBits , SerialSettings +from ..StreamConfig.Stream import StreamType , Stream +from ..StreamConfig.Preferences import Preferences + + +class FileConfigurationException(Exception): + """ + Exception class for File Configuration + """ + def __init__(self, message, error_code = None): + super().__init__(message) + self.error_code = error_code + +class PreferencesMissingException(FileConfigurationException): + """Raised when the preference parameter is none + """ +class StreamMissingException(FileConfigurationException): + """Raised when the stream parameter is none + """ + +def conf_file_preference(preference : Preferences ,conf_file : configparser.SectionProxy) : + """ + Configure preferences value with data form a config file + """ + if preference is None : + raise PreferencesMissingException("the preference can't be empty") + try : + preference.max_streams = int(conf_file.get("numberOfPortPanels")) + except (TypeError, ValueError): + pass + for connectid in range(preference.max_streams): + try : + preference.connect[connectid] = ( True if conf_file.get(f"connect{connectid}").lower() == "true" else False ) + except (TypeError, ValueError) : + pass + try : + preference.line_termination = str(conf_file.get("linetermination")).replace("\\n","\n").replace("\\r","\r") + except (TypeError, ValueError) : + pass + try: + if len(conf_file.get("configname")) != 0 : + preference.config_name = str(conf_file.get("configname")) + except (TypeError, ValueError): + pass + +def conf_file_config(stream : Stream ,conf_file : configparser.SectionProxy) : + """ + Init a stream and it's settings with a configuration file + + Args: + conf_file (configparser.SectionProxy): section of a stream in a configuration file + """ + if stream is None : + raise StreamMissingException('need a stream to be configured') + + stream.serial_settings = conf_file_serial(conf_file, stream.debug_logging) + stream.tcp_settings= conf_file_tcp(conf_file , stream.debug_logging) + stream.udp_settings = conf_file_udp(conf_file , stream.debug_logging ) + stream.ntrip_client = conf_file_ntrip_client(conf_file , stream.debug_logging ) + try : + + stream.stream_type = StreamType(int(conf_file.get("connectionType"))) + except (TypeError, ValueError): + stream.stream_type = StreamType.NONE + links = str(conf_file.get("linksChecked")).replace("[","").replace(" ","").replace("]","").split(",") + for link in links: + if link != '': + stream.linked_ports.append(int(link)) + stream.startup_script = conf_file.get("startupscriptfile") + try : + stream.send_startup_script = True if str(conf_file.get("startupscript")).lower() == "true" else False + except (TypeError, ValueError): + stream.send_startup_script = False + stream.close_script = conf_file.get("closescriptfile") + try : + stream.send_close_script = True if str(conf_file.get("closescript")).lower() == "true" else False + except (TypeError, ValueError): + stream.send_close_script = False + stream.logging_file = conf_file.get("logfile") + if stream.logging_file != "": + stream.logging = True + +def conf_file_serial(conf_file :configparser.SectionProxy, debug_logging :bool): + """ + Init Serial settings of the current stream with value from a configuration file. + If no value in configuration file , default value will be use + + Args: + conf_file (configparser.SectionProxy): configuration file + + Returns: + SerialSetting: return a new SerialSettings + """ + port : str = conf_file.get('Serial.Port') + try : + baudrate : BaudRate = BaudRate(conf_file.get('Serial.BaudRate')) + except (TypeError, ValueError): + baudrate : BaudRate = BaudRate.eBAUD115200 + try : + parity : Parity = Parity(conf_file.get('Serial.Parity')) + except (TypeError, ValueError) : + parity : Parity = Parity.PARITY_NONE + try : + stopbits : StopBits = StopBits(conf_file.get('Serial.StopBits') ) + except (TypeError, ValueError): + stopbits : StopBits = StopBits.STOPBITS_ONE + try : + bytesize : ByteSize= ByteSize(conf_file.get('Serial.ByteSize')) + except (TypeError, ValueError): + bytesize : ByteSize = ByteSize.EIGHTBITS + try : + rtscts : bool = True if conf_file.get('Serial.RtcCts').lower() == "true" else False + except (TypeError, ValueError): + rtscts = False + return SerialSettings(port = port , baudrate=baudrate , parity=parity , + stopbits=stopbits,bytesize=bytesize , + rtscts=rtscts,debug_logging=debug_logging) + +def conf_file_tcp(conf_file : configparser.SectionProxy, debug_logging : bool): + """ + Init TCP settings of the current stream with value from a configuration file. + If no value in configuration file , default value will be use + + Args: + conf_file (configparser.SectionProxy): configuration file + + Returns: + TcpSettings: return a new TcpSettings + """ + host : str = conf_file.get('hostName') + try : + port : int = int(conf_file.get('portNumber')) + except (TypeError, ValueError) : + port = 28784 + + if bool(True if str(conf_file.get('TCPserver')).lower() == "true" else False ) is True : + host = '' + stream_mode = StreamMode.SERVER + else: + stream_mode = StreamMode.CLIENT + + return TcpSettings(host= host , port= port , + stream_mode = stream_mode , debug_logging=debug_logging) + +def conf_file_udp(conf_file : configparser.SectionProxy, debug_logging : bool): + """ + Init UDP settings of the current stream with value from a configuration file. + If no value in configuration file , default value will be use + + Args: + conf_file (configparser.SectionProxy): configuration file + + Returns: + UdpSettings: return a new udpsettings + """ + host : str = conf_file.get('hostNameUDP') + try : + port : int = int(conf_file.get('portNumberUDP')) + except (TypeError, ValueError): + port = 28784 + try: + dataflow : DataFlow = DataFlow(conf_file.get('dataDirUDP')) + except (TypeError, ValueError): + dataflow : DataFlow = DataFlow.BOTH + try : + specific_host : bool = True if conf_file.get('specificIpUDP').lower() == "true" else False + except (TypeError, ValueError): + specific_host = False + + return UdpSettings(host=host,port=port , dataflow=dataflow, + specific_host=specific_host , debug_logging = debug_logging ) + +def conf_file_ntrip_client(conf_file : configparser.SectionProxy , debug_logging : bool): + """ + Init a Ntrip client with value from a configuration file. + If no value in configuration file , default value will be use + + Args: + conf_file (configparser.SectionProxy): configuration file + + Returns: + NtripClient: return a new ntrip + """ + host : str = conf_file.get('NTRIP.hostname') + try : + port : int = int(conf_file.get('NTRIP.portnumber')) + except (TypeError, ValueError): + port = 2101 + mountpoint : str = conf_file.get('NTRIP.mountPoint') + try : + auth : bool = True if conf_file.get('NTRIP.authenticationenabled').lower() == "true" else False + except (TypeError, ValueError): + auth = False + username : str = conf_file.get('NTRIP.user') + try : + password : str = base64.b64decode((conf_file.get('NTRIP.password')).encode()).decode() + except (TypeError, ValueError): + password : str = "" + try : + tls : bool = True if conf_file.get('NTRIP.TLS').lower() == "true" else False + except (TypeError, ValueError): + tls = False + try : + fixed_pos : bool = True if conf_file.get('NTRIP.fixedpositionset').lower() == "true" else False + except (TypeError, ValueError): + fixed_pos = False + + try : + latitude : float = float(conf_file.get('NTRIP.fixedlatitude')) + except (TypeError, ValueError): + latitude = 00.000000000 + try : + longitude : float = float(conf_file.get('NTRIP.fixedlongitude')) + except (TypeError, ValueError): + longitude = 000.000000000 + try : + height : int = int(conf_file.get('NTRIP.fixedheight')) + except (TypeError, ValueError) : + height = 0 + + settings = NtripSettings(host=host , port=port , auth=auth , + username=username , password= password, + mountpoint=mountpoint, tls= tls ,fixed_pos=fixed_pos , + latitude = latitude ,longitude = longitude , + height = height , debug_logging= debug_logging ) + + return NtripClient( ntrip_settings= settings ,debug_logging=debug_logging ) diff --git a/src/Configuration/SaveConfiguration.py b/src/Configuration/SaveConfiguration.py new file mode 100644 index 0000000..e5a0d7d --- /dev/null +++ b/src/Configuration/SaveConfiguration.py @@ -0,0 +1,133 @@ +# ############################################################################### +# +# Copyright (c) 2024, Septentrio +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import base64 +import os +import configparser +from src import constants +from ..StreamConfig.Preferences import Preferences +from ..StreamConfig.Stream import Stream + +class SaveConfigurationException(Exception): + """ + Exception class for Save Configuration + """ + def __init__(self, message, error_code = None): + super().__init__(message) + self.error_code = error_code + +def create_conf_file(app): + """ + Create the default configuration file with the current values + """ + conf_file_name = constants.DEFAULTCONFIGFILE + # try : + # conf_file = open(conf_file_name, "w",encoding='utf-8') + # except Exception as e : + # raise SaveConfigurationException(e) from e + # Add content to the file + config = configparser.ConfigParser() + file = config.read(constants.DEFAULTCONFIGFILE) + + for stream in app.stream_list : + section_name = "Port"+str(stream.stream_id) + # config.add_section(section_name) + config.set(section_name,"linksChecked" , str(stream.linked_ports)) + config.set(section_name,"startup_script" ,str(stream.send_startup_script )) + config.set(section_name,"startupScriptFile" , stream.startup_script) + config.set(section_name,"close_script",str(stream.send_close_script)) + config.set(section_name,"closeScriptFile",stream.close_script) + config.set(section_name,"logfile",str(stream.logging_file)) + save_tcp_config(stream ,section_name , config) + save_udp_config(stream ,section_name,config) + config.set(section_name,"connectionType",str(stream.stream_type.value)) + save_serial_config(stream ,section_name , config) + save_ntrip_config(stream , section_name , config) + + save_preferences_config(app.preferences, "Preferences" , config) + with open(constants.DEFAULTCONFIGFILE, 'w' , encoding="utf-8") as configfile: + config.write(configfile) + +# Saving config File +def save_udp_config(stream : Stream,section_name : str,save_config_file:configparser.ConfigParser): + """ + Add current udp settings values in the config_file + """ + save_config_file.set(section_name,"hostNameUDP", stream.udp_settings.host ) + save_config_file.set(section_name,"portNumberUDP",str(stream.udp_settings.port)) + save_config_file.set(section_name,"specificIpUDP",str(stream.udp_settings.specific_host)) + save_config_file.set(section_name,"dataDirUDP",str(stream.udp_settings.dataflow.value)) + +def save_tcp_config(stream : Stream,section_name : str,save_config_file:configparser.ConfigParser): + """ + Add current tcp settings values in the config_file + """ + save_config_file.set(section_name,"hostName",stream.tcp_settings.host) + save_config_file.set(section_name,"portNumber",str(stream.tcp_settings.port)) + save_config_file.set(section_name,"TCPserver",str(stream.tcp_settings.is_server())) + +def save_serial_config(stream:Stream,section_name : str,save_config_file:configparser.ConfigParser): + """ + Add current serial settings values in the config_file + """ + + save_config_file.set(section_name,"Serial.Port", str(stream.serial_settings.port)) + save_config_file.set(section_name,"Serial.BaudRate",str(stream.serial_settings.baudrate.value)) + save_config_file.set(section_name,"Serial.Parity",str(stream.serial_settings.parity.value)) + save_config_file.set(section_name,"Serial.StopBits",str(stream.serial_settings.stopbits.value)) + save_config_file.set(section_name,"Serial.ByteSize",str(stream.serial_settings.bytesize.value)) + save_config_file.set(section_name,"Serial.RtcCts",str(stream.serial_settings.rtscts)) + +def save_ntrip_config(stream : Stream,section_name:str,save_config_file:configparser.ConfigParser): + """ + Add current ntrip settings values in the config_file + """ + save_config_file.set(section_name,"NTRIP.hostname",str(stream.ntrip_client.ntrip_settings.host)) + save_config_file.set(section_name,"NTRIP.portnumber",str(stream.ntrip_client.ntrip_settings.port)) + save_config_file.set(section_name,"NTRIP.mountPoint",str(stream.ntrip_client.ntrip_settings.mountpoint)) + save_config_file.set(section_name,"NTRIP.authenticationenabled", str(stream.ntrip_client.ntrip_settings.auth)) + save_config_file.set(section_name,"NTRIP.user",str(stream.ntrip_client.ntrip_settings.username)) + save_config_file.set(section_name,"NTRIP.password",str(base64.b64encode((stream.ntrip_client.ntrip_settings.password).encode()).decode())) + save_config_file.set(section_name,"NTRIP.fixedpositionset",str(stream.ntrip_client.ntrip_settings.fixed_pos)) + save_config_file.set(section_name,"NTRIP.fixedLatitude",str(stream.ntrip_client.ntrip_settings.latitude)) + save_config_file.set(section_name,"NTRIP.fixedLongitude",str(stream.ntrip_client.ntrip_settings.longitude)) + save_config_file.set(section_name,"NTRIP.fixedHeight",str(stream.ntrip_client.ntrip_settings.height)) + save_config_file.set(section_name,"NTRIP.TLS", str(stream.ntrip_client.ntrip_settings.tls)) + + + +def save_preferences_config(preferences : Preferences , section_name : str,save_config_file : configparser.ConfigParser): + """ + Add current preference values in the config_file + """ + save_config_file.set(section_name , "config_name" ,str(preferences.config_name)) + save_config_file.set(section_name,"numberOfPortPanels",str(preferences.max_streams)) + save_config_file.set(section_name,"line_termination",preferences.line_termination.replace("\n","\\n").replace("\r","\\r")) + for connect , index in zip(preferences.connect,range(len(preferences.connect))): + connect_string = "connect" + str(index) + save_config_file.set(section_name,connect_string,str(connect)) diff --git a/src/Configuration/__init__.py b/src/Configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/Data Files/Github_icon.png b/src/Data Files/Github_icon.png new file mode 100644 index 0000000..3347961 Binary files /dev/null and b/src/Data Files/Github_icon.png differ diff --git a/src/Data Files/pyDatalink_Logo.png b/src/Data Files/pyDatalink_Logo.png new file mode 100644 index 0000000..c0cf183 Binary files /dev/null and b/src/Data Files/pyDatalink_Logo.png differ diff --git a/src/NTRIP/NtripClient.py b/src/NTRIP/NtripClient.py index 22d1f27..a5c6416 100644 --- a/src/NTRIP/NtripClient.py +++ b/src/NTRIP/NtripClient.py @@ -1,21 +1,21 @@ # ############################################################################### -# +# # Copyright (c) 2024, Septentrio -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: -# +# # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. -# +# # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# +# # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. -# +# # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -30,103 +30,138 @@ import base64 import datetime import math -from .NtripSourceTable import NtripSourceTable -from .NtripSettings import NtripSettings , ConnectFailedError import logging +from .NtripSourceTable import NtripSourceTable +from .NtripSettings import NtripSettings, NtripSettingsException +from ..constants import DEFAULTLOGFILELOGGER RAD2DEGREES = 180.0 / 3.141592653589793 -class SendRequestError(Exception): - pass -class ReceiveRequestError(Exception): - pass -class SourceTableRequestError(Exception): - pass -class ConnectRequestError(Exception): - pass -class ClosingError(Exception): - pass +class NtripClientError(Exception): + """Raised when a error with the NTRIP Client + """ + def __init__(self, message, error_code=None): + super().__init__(message) + self.error_code = error_code + +class SendRequestError(NtripClientError): + """Error while sending message to NTRIP Caster + """ + +class ReceiveRequestError(NtripClientError): + """Error while receiving a message from NTRIP Caster + """ + +class SourceTableRequestError(NtripClientError): + """ + Error while requesting the source table from NTRIP Caster + """ +class ConnectRequestError(NtripClientError): + """Error while establishing a connection with NTRIP Caster + """ + +class ClosingError(NtripClientError): + """Error while closing the connection with NTRIP Caster + """ + + class NtripClient: - - def __init__(self, host : str = "" , port : int = 2101 , auth : bool = False, username : str = "" , password : str = "" , mountpoint :str = "",tls :bool =False , - fixedPos : bool = False , latitude : str = "00.000000000" , longitude : str = "000.000000000" , height : int = 0 , logFile : logging = None) -> None: - - self.ntripSettings : NtripSettings = NtripSettings(host,port,auth,username,password,mountpoint,tls,fixedPos,latitude,longitude,height,logFile) + """Class for a ntrip client + """ + + def __init__(self, ntrip_settings : NtripSettings = NtripSettings(), debug_logging : bool = False)->None: + + self.ntrip_settings : NtripSettings = ntrip_settings self.socket = None self.connected : bool = False - self.fixedPosGga : str = None - self.logFile : logging = logFile - + self.fixed_pos_gga : str + if debug_logging : + self.log_file : logging.Logger = DEFAULTLOGFILELOGGER + else : + self.log_file = None def connect(self): - self.logFile.debug("Create NTRIP Client socket") - self.socket = self.ntripSettings.connect() - self.connected = True - self.logFile.info("NTRIP Client socket Created") + """Try to start the ntrip connection + """ try: - self._connectRequest() - except Exception as e: + if self.log_file is not None : + self.log_file.debug("Create NTRIP Client socket") + self.socket = self.ntrip_settings.connect() + self.connected = True + if self.log_file is not None : + self.log_file.info("NTRIP Client socket Created") + self._connect_request() + except NtripSettingsException as e: self.connected = False raise e - def sendNMEA(self, nmeaMessage): - if self.logFile is not None : - self.logFile.debug("Sending NMEA Message : %s", nmeaMessage) - if self.ntripSettings.ntripVersion == 2 : - request = "GET /"+ self.ntripSettings.mountpoint +" HTTP/1.1\r\n" - else : - request = "GET /"+ self.ntripSettings.mountpoint +" HTTP/1.0\r\n" - request += "Host: " + self.ntripSettings.host + "\r\n" + def send_nmea(self, nmea_message): + """Sending nmea message to the ntrip caster + """ + if self.log_file is not None : + self.log_file.debug("Sending NMEA Message : %s", nmea_message) + + if self.ntrip_settings.ntrip_version == 2 : + request = "GET /"+ self.ntrip_settings.mountpoint +" HTTP/1.1\r\n" + else : + request = "GET /"+ self.ntrip_settings.mountpoint +" HTTP/1.0\r\n" + request += "Host: " + self.ntrip_settings.host + "\r\n" request += "User-Agent: NTRIP Python Client\r\n" - if self.ntripSettings.ntripVersion == 2 : + if self.ntrip_settings.ntrip_version == 2 : request += "Ntrip-Version: Ntrip/2.0\r\n" - else : + else : request += "Ntrip-Version: Ntrip/1.0\r\n" - if self.ntripSettings.auth : - request += "Authorization: Basic " + base64.b64encode((self.ntripSettings.username + ":" + self.ntripSettings.password).encode()).decode() + "\r\n" - if self.ntripSettings.ntripVersion ==2 : - request += "Ntrip-GGA: " + nmeaMessage + "\r\n" + if self.ntrip_settings.auth : + request += "Authorization: Basic " + base64.b64encode((self.ntrip_settings.username + ":" + self.ntrip_settings.password).encode()).decode() + "\r\n" + if self.ntrip_settings.ntrip_version ==2 : + request += "Ntrip-GGA: " + nmea_message + "\r\n" request += "Connection: close\r\n\r\n" else : - request += nmeaMessage + "\r\n" - - self._sendRequest(request) - - def getSourceTable(self): - if self.logFile is not None : - self.logFile.debug("Getting source table from NTRIP Caster %s" ,self.ntripSettings.host ) - tempConnection = None + request += nmea_message + "\r\n" + try : + self._send_request(request) + except NtripClientError as e : + raise SendRequestError(e) from e + + def get_source_table(self): + """retrive source table from a ntrip caster , + if the ntripclient not yet connect , + it will open a temporary connection + """ + if self.log_file is not None : + self.log_file.debug("Getting source table from NTRIP Caster %s",self.ntrip_settings.host) + temp_connection = None if not self.connected: try : - self.socket = self.ntripSettings.connect() - except Exception as e: - self.logFile.error("Failed to connect to NTRIP caster %s : %s" , self.ntripSettings.host,e) - raise SourceTableRequestError(e) - tempConnection = True - + self.socket = self.ntrip_settings.connect() + except Exception as e: + self.log_file.error("Failed to connect to NTRIP caster %s : %s",self.ntrip_settings.host,e) + raise SourceTableRequestError(e) from e + temp_connection = True + request = "GET / HTTP/1.1\r\n" - request += "Host: " + self.ntripSettings.host + "\r\n" + request += "Host: " + self.ntrip_settings.host + "\r\n" request += "User-Agent: NTRIP pydatalink Client\r\n" request += "Ntrip-Version: Ntrip/2.0\r\n" - if self.ntripSettings.auth : - request+="Authorization: Basic " + base64.b64encode((self.ntripSettings.username + ":" + self.ntripSettings.password).encode()).decode() + "\r\n" + if self.ntrip_settings.auth : + request+="Authorization: Basic " + base64.b64encode((self.ntrip_settings.username + ":" + self.ntrip_settings.password).encode()).decode() + "\r\n" request += "Connection: close\r\n\r\n" - + try : - self._sendRequest(request) + self._send_request(request) except SendRequestError as e : - if self.logFile is not None : - self.logFile.error("Failed to send Header : %s" ,e) - raise SourceTableRequestError("Failed to send header") + if self.log_file is not None : + self.log_file.error("Failed to send Header : %s" ,e) + raise SourceTableRequestError("Failed to send header") from e try : - response : str = self._receiveResponse() + response : str = self._receive_response() except ReceiveRequestError as e : - if self.logFile is not None : - self.logFile.error("Failed to read source table : %s" ,e) - raise SourceTableRequestError("Failed to read source table") + if self.log_file is not None : + self.log_file.error("Failed to read source table : %s" ,e) + raise SourceTableRequestError("Failed to read source table") from e if "200" in response : - if self.logFile is not None : - self.logFile.info("parsing source table ") + if self.log_file is not None : + self.log_file.info("parsing source table ") # Parse the response to extract the resource table response : str= response.split("\r\n\r\n")[1] sources = response.split("STR;") @@ -136,64 +171,63 @@ def getSourceTable(self): for source in sources : newsourcetable = source.split(";") source_table.append(NtripSourceTable(newsourcetable[0],newsourcetable[1],newsourcetable[2],newsourcetable[3])) - if tempConnection is not None: + if temp_connection is not None: self.close() - if self.logFile is not None : - self.logFile.debug("Number of mountpoints available from %s : %s " , self.ntripSettings.host , str(len(source_table))) + if self.log_file is not None : + self.log_file.debug("Number of mountpoints available from %s : %s " , self.ntrip_settings.host , str(len(source_table))) return source_table else : - if self.logFile is not None : - self.logFile.error("The return value is inccorect") - self.logFile.debug("NTRIP Caster response : %s",response) + if self.log_file is not None : + self.log_file.error("The return value is inccorect") + self.log_file.debug("NTRIP Caster response : %s",response) raise SourceTableRequestError("Error in returned source table") - - - def _connectRequest(self): - - if self.ntripSettings.ntripVersion == 2 : - request = "GET /"+ self.ntripSettings.mountpoint +" HTTP/1.1\r\n" - else : - request = "GET /"+ self.ntripSettings.mountpoint +" HTTP/1.0\r\n" - request += f"Host: {self.ntripSettings.host}\r\n" + + def _connect_request(self): + + if self.ntrip_settings.ntrip_version == 2 : + request = "GET /"+ self.ntrip_settings.mountpoint +" HTTP/1.1\r\n" + else : + request = "GET /"+ self.ntrip_settings.mountpoint +" HTTP/1.0\r\n" + request += f"Host: {self.ntrip_settings.host}\r\n" request += "User-Agent: NTRIP pydatalink Client\r\n" - if self.ntripSettings.ntripVersion == 2 : + if self.ntrip_settings.ntrip_version == 2 : request += "Ntrip-Version: Ntrip/2.0\r\n" - else : + else : request += "Ntrip-Version: Ntrip/1.0\r\n" - if self.ntripSettings.auth : - request += "Authorization: Basic " + base64.b64encode((self.ntripSettings.username + ":" + self.ntripSettings.password).encode()).decode() + "\r\n" - if self.ntripSettings.ntripVersion ==2 : + if self.ntrip_settings.auth : + request += "Authorization: Basic " + base64.b64encode((self.ntrip_settings.username + ":" + self.ntrip_settings.password).encode()).decode() + "\r\n" + if self.ntrip_settings.ntrip_version ==2 : request += "Connection: close\r\n\r\n" - + try : - self._sendRequest(request) - except SendRequestError as e : - if self.logFile is not None : - self.logFile.error("Failed to send request : %s",e) - raise SendRequestError(e) + self._send_request(request) + except SendRequestError as e : + if self.log_file is not None : + self.log_file.error("Failed to send request : %s",e) + raise SendRequestError("Failed to send request") from e try : - response = self._receiveResponse() - self.logFile.debug("return value from the request : %s", response) - - except ReceiveRequestError as e: - if self.logFile is not None : - self.logFile.debug("return value from the request : %s", response) - self.logFile.error("Failed to catch response : %s",e) - raise ReceiveRequestError(e) - if "HTTP/1.1 40" in response or "HTTP/1.0 40" in response : - error = response.split("\r\n\r\n")[1] - if self.logFile is not None : - self.logFile.error("Client error : %s",error.replace("\n","").replace("\r","")) - raise ConnectRequestError(error) - - - def _sendRequest(self, request : str): + response = self._receive_response() + self.log_file.debug("return value from the request : %s", response) + + except ReceiveRequestError as e: + if self.log_file is not None : + self.log_file.debug("return value from the request : %s", response) + self.log_file.error("Failed to catch response : %s",e) + raise ReceiveRequestError("Failed to catch receive a response") from e + + if "HTTP/1.1 40" in response or "HTTP/1.0 40" in response : + error = response.split("\r\n\r\n")[0].split("\r\n")[0].replace("HTTP/1.1","").replace("HTTP/1.0","") + if self.log_file is not None : + self.log_file.error("Client error : %s",error.replace("\n","").replace("\r","")) + raise ConnectRequestError("Caster Response :" + error) + + def _send_request(self, request : str): try: self.socket.sendall(request.encode()) except Exception as e: - raise SendRequestError(e) - - def _receiveResponse(self): + raise SendRequestError(e) from e + + def _receive_response(self): response = "" while True: try : @@ -203,49 +237,55 @@ def _receiveResponse(self): response += data.decode(encoding='ISO-8859-1') if "\r\n\r\n" in response and "sourcetable" not in response : break - except Exception as e : - raise ReceiveRequestError(e) + except Exception as e : + raise ReceiveRequestError(e) from e return response - - def _createGgaString(self): - + + def create_gga_string(self): + """Generate GGA String with fixed position + """ result = "" - timeString = datetime.datetime.now().strftime("%H%M%S") + ".00" - minutes,degrees = math.modf(self.ntripSettings.latitude) + time_string = datetime.datetime.now().strftime("%H%M%S") + ".00" + minutes,degrees = math.modf(self.ntrip_settings.latitude) minutes *= 60 - latString = "{:02d}{:08.5f},{}" .format(int(abs(degrees)), abs(minutes), "N" if self.ntripSettings.latitude > 0 else "S") - minutes,degrees = math.modf(self.ntripSettings.longitude) + lat_string = f"{int(abs(degrees)):02d}{abs(minutes):08.5f},{'N' if self.ntrip_settings.latitude > 0 else 'S'}" + minutes,degrees = math.modf(self.ntrip_settings.longitude) minutes *= 60 - lonString = "{:03d}{:08.5f},{}".format(int(abs(degrees)), abs(minutes), "E" if self.ntripSettings.longitude > 0 else "W") - heightString = "{:0.2f},M,0.00,M".format(self.ntripSettings.height) - result = "$GPGGA,{},{},{},1,08,0.75,{},,*".format(timeString, latString, lonString, heightString) + lon_string = f"{int(abs(degrees)):03d}{abs(minutes):08.5f},{'E' if self.ntrip_settings.longitude > 0 else 'W'}" + height_string = f"{self.ntrip_settings.height:0.2f},M,0.00,M" + result = f"$GPGGA,{time_string},{lat_string},{lon_string},1,08,0.75,{height_string},,*" - checkSum = 0 + checksum = 0 for i in range(1, len(result) - 1): - checkSum ^= ord(result[i]) + checksum ^= ord(result[i]) - result += "{:02X}\r\n".format(checkSum) - self.fixedPosGga = result - - if self.logFile is not None : - self.logFile.debug("New GGA String : %s " , result.replace("\n","").replace("\r","")) + result += f"{checksum:02X}\r\n" + self.fixed_pos_gga = result + + if self.log_file is not None : + self.log_file.debug("New GGA String : %s " , result.replace("\n","").replace("\r","")) return result def close(self): + """Close current ntrip connection + """ try: self.socket.close() self.connected = False except Exception as e: - raise ClosingError(e) - - def set_Settings_Host(self, host): - self.ntripSettings.setHost(host) - if self.logFile is not None: - self.logFile.info("New NTRIP Host Name : %s" , host) - try : - self.ntripSettings.sourceTable = self.getSourceTable() - except Exception as e : - if self.logFile is not None: - self.logFile.error("Failed to get Source Table : %s" , e ) - raise e + raise ClosingError("Error while clossing the socket") from e + + def set_settings_host(self, host): + """ + Set a new ntrip caster hostname and retrieve the new source table + """ + self.ntrip_settings.set_host(host) + if self.log_file is not None: + self.log_file.info("New NTRIP Host Name : %s" , host) + try : + self.ntrip_settings.source_table = self.get_source_table() + except NtripClientError as e : + if self.log_file is not None: + self.log_file.error("Failed to get Source Table : %s" , e ) + raise SourceTableRequestError("Error while getting the new source table") from e diff --git a/src/NTRIP/NtripSettings.py b/src/NTRIP/NtripSettings.py index 3442bb2..bf155c5 100644 --- a/src/NTRIP/NtripSettings.py +++ b/src/NTRIP/NtripSettings.py @@ -1,21 +1,21 @@ # ############################################################################### -# +# # Copyright (c) 2024, Septentrio -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: -# +# # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. -# +# # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# +# # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. -# +# # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -27,196 +27,211 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import base64 -import configparser import socket -from .NtripSourceTable import NtripSourceTable import ssl import logging +from .NtripSourceTable import NtripSourceTable +from ..constants import DEFAULTLOGFILELOGGER - -class InvalidHostnameError(Exception): - def __init__(self, message = "Invalid Hostname", errors = "NTRIPSettings 1"): +class NtripSettingsException(Exception): + """ + Exception class for ntrip settings + """ + def __init__(self, message, error_code = None): super().__init__(message) - self.errors = errors + self.error_code = error_code -class FailedHandshakeError(Exception): - def __init__(self, message = "TLS connection Failed", errors = "NTRIPSettings 2"): - super().__init__(message) - self.errors = errors +class InvalidHostnameError(NtripSettingsException): + """Raised when invalid host name is given + """ -class ConnectFailedError(Exception): - def __init__(self, message = "Connection to server Failed", errors = "NTRIPSettings 3"): - super().__init__(message) - self.errors = errors +class FailedHandshakeError(NtripSettingsException): + """Raised when TLS Handshake failed with server + """ -class NtripSettings: +class ConnectFailedError(NtripSettingsException): + """Raised when creating a socket failed + """ + +class NtripSettings: """ Represents the TCP settings for a Stream. """ - def __init__(self , host : str = "" , port : int = 2101 ,auth : bool = False, username : str = "" , password : str = "", - mountpoint : str = "" , tls : bool = False , fixedPos : bool = False , latitude : str = "00.000000000" , longitude : str = "000.000000000" , height : int = 0 , logFile : logging = None) -> None: + def __init__(self , host : str = "" , port : int = 2101 , + auth : bool = False, username : str = "" , password : str = "", + mountpoint : str = "" , tls : bool = False , fixed_pos : bool = False , + latitude : float = 00.00 , longitude : float = 0.000 , height : int = 0 , + debug_logging : bool = False) -> None: """ Initializes a new instance of the NTRIPSettings class. """ self.host : str = host self.port : int = port self.mountpoint : str = mountpoint - + self.auth : bool = auth - + self.username: str = username self.password :str = password - self.tls : bool = tls + self.tls : bool = tls self.cert : str = "" - - self.ntripVersion : int = 2 + + self.ntrip_version : int = 2 # Fixed Position GGA - self.fixedPos : bool = fixedPos - self.latitude : float = float(latitude) - self.longitude : float = float(longitude) + self.fixed_pos : bool = fixed_pos + self.latitude : float = latitude + self.longitude : float = longitude self.height : int = height - - self.sourceTable : list[NtripSourceTable] = None - + + self.source_table : list[NtripSourceTable] = [] + # Support Log - self.logFile : logging = logFile - - def connect(self): - if len(self.host) == 0: - if self.logFile is not None : - self.logFile.error("Invalid Hostname : hostname empty") - raise InvalidHostnameError() - if self.tls : - ntripsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - if self.cert != "": - context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - context.load_verify_locations(self.cert) - else : - context = ssl.create_default_context() - wrappedSocket = context.wrap_socket(ntripsocket, server_hostname=self.host) - wrappedSocket.settimeout(0.5) - try: - wrappedSocket.connect((self.host, self.port)) - return wrappedSocket - except TimeoutError as e: - if self.logFile is not None : - self.logFile.error("Error during the handshake for TLS connection : %s" , e) - raise FailedHandshakeError() - except Exception as e : - if self.logFile is not None : - self.logFile.error("Failed to open TLS socket : %s", e) - raise e - else : - try: - ntripsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ntripsocket.connect((self.host, self.port)) - return ntripsocket - except Exception as e: - if self.logFile is not None : - self.logFile.error("Failed to open socket : %s", e ) - raise ConnectFailedError(e) + if debug_logging : + self.log_file : logging.Logger = DEFAULTLOGFILELOGGER + else : + self.log_file = None + def connect(self): + """Create the communication socket + """ + if len(self.host) == 0: + if self.log_file is not None : + self.log_file.error("Invalid Hostname : hostname empty") + raise InvalidHostnameError("Invalid Host Name or Host name Empty") + if self.tls : + ntrip_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if self.cert != "": + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.load_verify_locations(self.cert) + else : + context = ssl.create_default_context() + wrapped_socket = context.wrap_socket(ntrip_socket, server_hostname=self.host) + wrapped_socket.settimeout(0.5) + try: + wrapped_socket.connect((self.host, self.port)) + return wrapped_socket + except TimeoutError as e: + if self.log_file is not None : + self.log_file.error("Error during the handshake for TLS connection : %s" , e) + raise FailedHandshakeError("Error during the handshake") from e + except Exception as e : + if self.log_file is not None : + self.log_file.error("Failed to open TLS socket : %s", e) + raise ConnectFailedError("Failed to open TLS socket") from e + else : + try: + ntrip_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ntrip_socket.connect((self.host, self.port)) + return ntrip_socket + except Exception as e: + if self.log_file is not None : + self.log_file.error("Failed to open socket : %s", e ) + raise ConnectFailedError("Failed to open communication socket") from e - def setHost(self, NewHost : str): + + def set_host(self, new_host : str): """ Sets the host IP address. Args: - NewHost (str): The new host IP address. + new_host (str): The new host IP address. """ - self.host = NewHost + self.host = new_host - def setPort(self, NewPort : int): + def set_port(self, new_port : int): """ Sets the port number. Args: - NewPort (int): The new port number. + new_port (int): The new port number. """ - self.port = NewPort + self.port = new_port - def setMountpoint(self, NewMountpoint : str): + def set_mountpoint(self, new_mountpoint : str): """ Sets the mountpoint. Args: - NewMountpoint (str): The new mountpoint. + new_mountpoint (str): The new mountpoint. """ - self.mountpoint = NewMountpoint - - def setAuth(self, Newauth : bool): + self.mountpoint = new_mountpoint + + def set_auth(self, new_auth : bool): """ Sets the authentication. Args: - Newauth (bool): The new authentication. + new_auth (bool): The new authentication. """ - self.auth = Newauth - - def setUsername(self, NewUsername : str): + self.auth = new_auth + + def set_username(self, new_username : str): """ Sets the username. Args: - NewUsername (str): The new username. + new_username (str): The new username. """ - self.username = NewUsername - - def setPassword(self, NewPassword : str): + self.username = new_username + + def set_password(self, new_password : str): """ Sets the password. Args: - NewPassword (str): The new password. + new_password (str): The new password. """ - self.password = NewPassword - - def setFixedPos(self, NewFixedPos : bool): + self.password = new_password + + def set_fixed_pos(self, new_fixed_pos : bool): """ Sets the fixed position. Args: - NewFixedPos (bool): The new fixed position. + new_fixed_pos (bool): The new fixed position. """ - self.fixedPos = NewFixedPos + self.fixed_pos = new_fixed_pos - def setLatitude(self, NewLatitude : str): + def set_latitude(self, new_latitude : str): """ Sets the latitude. Args: - NewLatitude (str): The new latitude. + new_latitude (str): The new latitude. """ - if "S" in NewLatitude : - NewLatitude = "-" + NewLatitude.split("S ")[1] + if "S" in new_latitude : + new_latitude = "-" + new_latitude.split("S ")[1] else : - NewLatitude = NewLatitude.split("N ")[1] - self.latitude = float(NewLatitude) - - def setLongitude(self, NewLongitude : str): + new_latitude = new_latitude.split("N ")[1] + self.latitude = float(new_latitude) + + def set_longitude(self, new_longitude : str): """ Sets the longitude. Args: - NewLongitude (str): The new longitude. + new_longitude (str): The new longitude. """ - if "W" in NewLongitude : - NewLongitude = "-" + NewLongitude.split("W ")[1] + if "W" in new_longitude : + new_longitude = "-" + new_longitude.split("W ")[1] else : - NewLongitude = NewLongitude.split("E ")[1] - self.longitude = float(NewLongitude) - - def setTls(self , newTls): - self.tls = newTls - - def setCert(self,newcert): - self.cert = newcert - - def getLatitude(self) -> str: + new_longitude = new_longitude.split("E ")[1] + self.longitude = float(new_longitude) + + def set_tls(self , new_tls : bool): + """ Set the use of tls + """ + self.tls = new_tls + + def set_cert(self,new_cert): + """Set the path to the certificate + """ + self.cert = new_cert + + def get_latitude(self) -> str: """ Gets the latitude. @@ -227,8 +242,8 @@ def getLatitude(self) -> str: return "S {:012.9f}".format(self.latitude) else : return "N {:012.9f}".format(self.latitude) - - def getLongitude(self) -> str : + + def get_longitude(self) -> str : """ Gets the longitude. @@ -239,27 +254,16 @@ def getLongitude(self) -> str : return "W {:013.9f}".format(self.longitude) else : return "E {:013.9f}".format(self.longitude) - - def setHeight(self, NewHeight : int): + + def set_height(self, new_height : int): """ Sets the height. Args: - NewHeight (int): The new height. + new_height (int): The new height. + """ + self.height = new_height + def to_string(self) ->str : + """Return settings as a single string """ - self.height = NewHeight - def toString(self) ->str : return f"Host : {self.host} \n Port : {self.port} \n Username : {self.username} \n Password : {self.password} \n Mountpoint : {self.mountpoint} \n" - - def SaveConfig(self , sectionName : str,SaveConfigFile : configparser.ConfigParser): - SaveConfigFile.set(sectionName,"NTRIP.hostname",str(self.host)) - SaveConfigFile.set(sectionName,"NTRIP.portnumber",str(self.port)) - SaveConfigFile.set(sectionName,"NTRIP.mountPoint",str(self.mountpoint)) - SaveConfigFile.set(sectionName,"NTRIP.authenticationenabled", str(self.auth)) - SaveConfigFile.set(sectionName,"NTRIP.user",str(self.username)) - SaveConfigFile.set(sectionName,"NTRIP.password",str(base64.b64encode((self.password).encode()).decode())) - SaveConfigFile.set(sectionName,"NTRIP.fixedpositionset",str(self.fixedPos)) - SaveConfigFile.set(sectionName,"NTRIP.fixedLatitude",str(self.latitude)) - SaveConfigFile.set(sectionName,"NTRIP.fixedLongitude",str(self.longitude)) - SaveConfigFile.set(sectionName,"NTRIP.fixedHeight",str(self.height)) - SaveConfigFile.set(sectionName,"NTRIP.TLS", str(self.tls)) \ No newline at end of file diff --git a/src/NTRIP/NtripSourceTable.py b/src/NTRIP/NtripSourceTable.py index f38a643..727ed1f 100644 --- a/src/NTRIP/NtripSourceTable.py +++ b/src/NTRIP/NtripSourceTable.py @@ -1,21 +1,21 @@ # ############################################################################### -# +# # Copyright (c) 2024, Septentrio -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: -# +# # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. -# +# # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# +# # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. -# +# # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -46,24 +46,24 @@ class Fee(Enum) : N = "No user fee" Y = "Usage is charged" class NtripSourceTable : - - def __init__(self,mountpoint : str = None ,identifier : str = None , format :str = None , formatDetail : str = None ) -> None: - + + def __init__(self,mountpoint : str = None ,identifier : str = None , ntrip_format :str = None , format_detail : str = None ) -> None: + self.mountpoint : str = mountpoint self.identifier :str = identifier - self.format : str = format - self.formatDetail : str = formatDetail + self.ntrip_format : str = ntrip_format + self.format_detail : str = format_detail self.carrier : Carrier - self.navSystem : str - self.network :str - self.country :str + self.nav_system : str + self.network :str + self.country :str self.latitude : float self.longitude : float self.nmea :str - self.solution : Solution + self.solution : Solution self.generator : str - self.comprEncryp :str - self.authentification : Authentication - self.fee : Fee - self.bitrate :str - self.misc :str \ No newline at end of file + self.compr_encryp :str + self.authentification : Authentication + self.fee : Fee + self.bitrate :str + self.misc :str diff --git a/src/StreamConfig/App.py b/src/StreamConfig/App.py new file mode 100644 index 0000000..53cbc55 --- /dev/null +++ b/src/StreamConfig/App.py @@ -0,0 +1,160 @@ +# ############################################################################### +# +# Copyright (c) 2024, Septentrio +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import configparser +from enum import Enum +import queue + +from ..NTRIP.NtripClient import NtripClientError +from .Preferences import Preferences +from .Stream import Stream +from ..Configuration import SaveConfiguration , CommandLineConfiguration , FileConfiguration + + +class AppException(Exception): + """ + Exception class for App + """ + def __init__(self, message, error_code = None): + super().__init__(message) + self.error_code = error_code + +class ConfigurationFileEmpty(AppException): + """Raised when the configuration file is empty + """ + +class InvalidStreamTypeException(AppException): + """Raised when the given stream type is not supported + """ +class ConfigurationType(Enum): + """Type of configuration that need to be executed + """ + FILE = 0 + CMDLINE = 1 + DEFAULT = None + + +class App : + """ + Class which initialise every Streams. + Depending on the input it can be configure via a configuration file or via a configuration list + + """ + + def __init__(self,max_stream : int = 6, config_file :str = "" , + stream_settings_list : list[str] = None , + configuration_type : ConfigurationType = ConfigurationType.DEFAULT , + debug_logging : bool = False): + + self.preferences : Preferences = Preferences() + self.max_stream :int = max_stream + self.stream_settings_list : list[str] = stream_settings_list + self.config_file :str = config_file + self.stream_list : list[Stream] = [] + self.linked_data : list[queue.Queue] = [] + self.debug_logging : bool = debug_logging + self.configuration_type : ConfigurationType = configuration_type + + for i in range (self.max_stream) : + self.linked_data.append(queue.Queue()) + + for i in range(self.max_stream): + new_port = Stream(i ,self.linked_data , debug_logging=debug_logging ) + self.stream_list.append(new_port) + + self.preferences = Preferences(self.max_stream) + + if configuration_type != ConfigurationType.DEFAULT: + self.configure_app( configuration_type) + + def configure_app(self , config_type : ConfigurationType ): + """ Configure app according to the configuration type selected + """ + if config_type.value == ConfigurationType.FILE.value : + config = configparser.ConfigParser() + read_value = config.read(self.config_file) + if len(config.sections()) == 0 : + raise ConfigurationFileEmpty("Configuration file is empty") + else : + try : + new_max_stream = int(config.get("Preferences","numberOfPortPanels")) + if self.max_stream > new_max_stream and new_max_stream > 0 : + diff = self.max_stream - new_max_stream + for i in range(diff) : + self.stream_list.pop(self.max_stream - i - 1) + self.max_stream = new_max_stream + finally : + next_stream_id = 0 + for key in config.sections(): + if "Preferences" in key: + FileConfiguration.conf_file_preference(self.preferences, config[key] ) + if "Port" in key : + if next_stream_id < self.max_stream : + FileConfiguration.conf_file_config(self.stream_list[next_stream_id],config[key]) + next_stream_id +=1 + for port_id, value in enumerate(self.preferences.connect): + if port_id < self.max_stream: + self.stream_list[port_id].set_line_termination(self.preferences.line_termination) + if value : + try : + self.stream_list[port_id].connect(self.stream_list[port_id].stream_type) + except (NtripClientError) as e : + self.stream_list[port_id].startup_error =f"Ntrip stream couldn't start properly : \n {e}" + except Exception as e: + self.stream_list[port_id].startup_error =f"Stream couldn't start properly : \n {e}" + + else : + iterator = 0 + for stream in self.stream_settings_list : + stream_type = stream.split("://")[0] + if stream_type.lower() in ["udp","udpspe","tcpcli","tcpsrv","serial","ntrip"]: + try : + CommandLineConfiguration.command_line_config(self.stream_list[iterator],stream) + iterator += 1 + except CommandLineConfiguration.CommandLineConfigurationException as e : + print(f"Could not open {stream_type} : {e}") + self.close_all() + break + else : + raise InvalidStreamTypeException(f" {stream_type} is not a valid stream type") + + + def close_all(self): + """ + Close every Stream that are still connected + """ + if ((self.configuration_type.value == ConfigurationType.FILE.value) or + (self.configuration_type.value == ConfigurationType.DEFAULT.value)) : + + SaveConfiguration.create_conf_file(self) + for port in self.stream_list: + if port.is_connected() : + port.disconnect() + self.linked_data.clear() + self.stream_list.clear() diff --git a/src/StreamConfig/PortConfig.py b/src/StreamConfig/PortConfig.py deleted file mode 100644 index 21c8bce..0000000 --- a/src/StreamConfig/PortConfig.py +++ /dev/null @@ -1,1355 +0,0 @@ -# ############################################################################### -# -# Copyright (c) 2024, Septentrio -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# 3. Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - - -import base64 -import copy -from datetime import datetime -from enum import Enum -import os -import socket -import time -from serial import Serial -import threading -import configparser -import queue -import logging -from ..NTRIP import * -from .UdpSettings import * -from .SerialSettings import * -from .TcpSettings import * -from .Preferences import * -from ..constants import * - -class StreamType(Enum): - Serial = 0 - TCP = 1 - UDP = 2 - NTRIP = 3 - NONE = None - -def Task_UpdateLinkedPort(updateLinkedPort : queue.Queue , LinkedPort : list[int] ): - for _ in range(updateLinkedPort.qsize()): - port = updateLinkedPort.get() - if port in LinkedPort: - LinkedPort.remove(port) - else: - LinkedPort.append(port) - return LinkedPort -def Task_SendCommand(linkedData : queue.Queue , Stream , ShowData : bool = False , udpsendaddress : str = None - , DataToShow : queue.Queue = None , logger : logging.Logger = None , lineTermination : str = "\r\n"): - try : - for _ in range(linkedData.qsize()): - outputData : str = linkedData.get() - if type(Stream) == Serial : - Stream.write((outputData+"\n").encode(encoding='ISO-8859-1')) - elif type(Stream) == socket.socket and udpsendaddress is None: - Stream.sendall((outputData + lineTermination).encode(encoding='ISO-8859-1')) - elif type(Stream) == socket.socket: - Stream.sendto((outputData + lineTermination).encode(encoding='ISO-8859-1'), udpsendaddress) - elif type(Stream) == NtripClient : - if "GPGGA" in outputData: - Stream.sendNMEA(outputData) - else : continue - if ShowData and len(outputData) != 0 : - DataToShow.put(outputData) - if logger is not None: - logger.log(0,outputData) - return len(outputData) - except Exception as e : - raise e - - -class PortConfig: - """ - Represents a port/stream for data streaming. - - """ - - def __init__(self, id: int = 0, linkedData: list[queue.Queue] = None ,confFile : configparser.SectionProxy = None - , commandLineConfig : str = None , logFile : logging.Logger = None ): - - self.id = id - self.linkedPort: list[int] = [] - self.connected: bool = False - self.currentTask = None - self.StreamType: StreamType = StreamType.NONE - self.Stream : Serial | socket.socket | NtripClient = None - self.lineTermination :str = "\r\n" - self.dataTransferInput : float = 0.0 - self.dataTransferOutput :float = 0.0 - - #Support Log File - self.logFile : logging.Logger = logFile - - # logging file - self.logging : bool = False - self.loggingFile : str = "" - self.logger : logging = None - # Startup and close script - self.startupScript : str = "" - self.closeScript : str = "" - self.sendStartupScript : bool = False - self.sendCloseScript : bool = False - - # Event for data visibility and stop - - self.ShowInputData = threading.Event() - self.ShowOutputData = threading.Event() - self.StopEvent = threading.Event() - - # Queue for data link between ports - - self.linkedData = linkedData - self.updateLinkedPort: queue.Queue = queue.Queue() - self.DataToShow: queue.Queue = queue.Queue() - - # Thread for data read/link - - self.dataLinkStreamThread: threading.Thread = None - - # Stream settings - if confFile is not None: - self.ConfFileConfig(confFile) - elif commandLineConfig is not None : - self.CommandLineConfig(commandLineConfig) - else: - self.serialSettings = SerialSettings(logFile = self.logFile ) - self.tcpSettings = TcpSettings(logFile = self.logFile ) - self.udpSettings = UdpSettings(logFile = self.logFile ) - self.ntripClient = NtripClient(logFile= self.logFile ) - - - - - - def Connect(self, Streamtype : StreamType = None): - """ - Connects the port using the specified Stream type. - - Args: - Streamtype: The type of Stream. - - Returns: - int: 0 if the Stream fails, otherwise None. - """ - if self.logFile is not None : - self.logFile.info("Connecting Stream %s " , self.id) - if Streamtype is None : - Streamtype = self.StreamType - if self.logFile is not None : - self.logFile.info("Stream %s : start new %s Stream" , self.id , Streamtype.name) - if self.connected is not True: - if Streamtype == StreamType.Serial: - if self.serialSettings.port != "": - try: - self.Stream = self.serialSettings.Connect() - self.connected = True - task = self.DatalinkSerialTask - if self.logFile is not None : - self.logFile.info("Stream %s : Stream openned succesfully " , self.id) - except Exception as e: - if self.logFile is not None : - self.logFile.error("Stream %s : Failed to connect to the serial port : %s" , self.id,e) - self.Stream = None - self.connected = False - raise e - else: - if self.logFile is not None : - self.logFile.error("Stream %s : Failed to open Serial stream : Serial port hasn't been configured yet" , self.id) - raise Exception("Connection Error","Serial port hasn't been configured yet") - elif Streamtype == StreamType.TCP: - if self.logFile is not None : - self.logFile.debug("Stream %s : Create TCP %s with host : %s and port : %s" , self.id,self.StreamType.name,self.tcpSettings.host , self.tcpSettings.port) - if self.tcpSettings is not None: - try: - socket.gethostbyname(self.tcpSettings.host) - self.Stream = self.tcpSettings.connect() - self.connected = True - if self.tcpSettings.StreamMode == StreamMode.SERVER: - task = self.DatalinkTCPServerTask - else : - task = self.DatalinkTCPClientTask - if self.logFile is not None : - self.logFile.info("Stream %s : Stream openned succesfully " , self.id) - except Exception as e : - self.Stream = None - self.connected = False - if self.logFile is not None : - self.logFile.error("Stream %s : Failed to open TCP stream: %s" , self.id,e) - raise e - else: - if self.logFile is not None : - self.logFile.error("Stream %s : Failed to open TCP stream : TCP settings not set " , self.id) - raise Exception("Connection Error","tcp settings are empty !") - elif Streamtype == StreamType.UDP: - if self.udpSettings is not None: - try: - socket.gethostbyname(self.tcpSettings.host) - self.Stream = self.udpSettings.connect() - if self.logFile is not None : - self.logFile.info("Stream %s : Stream openned succesfully " , self.id) - except Exception as e: - self.Stream = None - self.connected = False - if self.logFile is not None : - self.logFile.error("Stream %s : Failed to open TCP stream: %s" , self.id,e) - raise e - else: - if self.logFile is not None : - self.logFile.error("Stream %s : Failed to open UDP stream : UDP settings not set " , self.id) - raise Exception("Connection Error","udp settings are empty!") - elif Streamtype == StreamType.NTRIP: - if self.ntripClient is not None: - try: - if len(self.ntripClient.ntripSettings.host.replace(" ","")) != 0 : - socket.gethostbyname(self.ntripClient.ntripSettings.host) - self.ntripClient.connect() - self.Stream = self.ntripClient - self.connected = True - task = self.DatalinkNTRIPTask - if self.ntripClient.ntripSettings.fixedPos: - self.ntripClient._createGgaString() - - if self.logFile is not None : - self.logFile.info("Stream %s : Stream openned succesfully " , self.id) - else: - if self.logFile is not None : - self.logFile.error("Stream %s : Failed to open NTRIP stream : NTRIP Host Name not set " , self.id) - raise Exception("Connection Error","NTRIP host is not set !") - except Exception as e: - if self.logFile is not None : - self.logFile.error("Stream %s : Failed to open NTRIP stream: %s" , self.id,e) - self.Stream = None - self.connected = False - raise e - else: - if self.logFile is not None : - self.logFile.error("Stream %s : Failed to open NTRIP stream : NTRIP settings not set " , self.id) - raise Exception("Connection Error","ntrip client is not set !") - else: - if self.logFile is not None : - self.logFile.error("Stream %s : Invalid Stream Type " , self.id) - raise Exception("Connection Error","Invalid Stream type!") - if self.connected is True: - try: - if self.logFile is not None : - self.logFile.debug("Stream %s : start final configuration " , self.id) - self.StopEvent.clear() - if self.logging : - self.logger = logging.getLogger(f"Stream{self.id}Log") - self.logger.basicConfig(filename=self.loggingFile , format="") - if self.logFile is not None : - self.logFile.debug("Stream %s : init loggin file : %s" , self.id,self.loggingFile) - if self.sendStartupScript: - if self.logFile is not None : - self.logFile.debug("Stream %s : init startup script file : %s" , self.id,self.startupScript) - self._clearQueue(self.linkedData[self.id]) - self.sendScript(self.linkedData[self.id], True) - self.dataLinkStreamThread = threading.Thread(target=task,args=(self.Stream, self.linkedData, self.updateLinkedPort,self.DataToShow)) - if self.logFile is not None : - self.logFile.debug("Stream %s : Starting Thread " , self.id) - self.dataLinkStreamThread.start() - self.currentTask = task - if len(self.linkedPort) != 0: - if self.logFile is not None : - self.logFile.error("Stream %s : update linked Port : %s" , self.id ,str(self.linkedPort) ) - for link in self.linkedPort: - self.updateLinkedPort.put(link) - if self.logFile is not None : - self.logFile.error("Stream %s : final configuration finished " , self.id ) - except Exception as e: - if self.logFile is not None : - self.logFile.info("Stream %s : Failed during final configuration : %s" , self.id ,e ) - self.connected = False - raise e - else : - if self.logFile is not None : - self.logFile.error("Stream %s : Stream was already connected",self.id) - - def Disconnect(self): - """ - Disconnects the port if is connected. - """ - if self.Stream is not None: - if self.logFile is not None : - self.logFile.info("Stream %s : Disconnecting stream",self.id) - try: - if self.sendCloseScript: - self.sendScript(self.linkedData[self.id], False) - self.StopEvent.set() - self.currentTask = None - self.dataLinkStreamThread.join() - if self.logFile is not None : - self.logFile.debug("Stream %s : wait for Thread to stop",self.id) - self.Stream.close() - self.connected = False - self.dataTransferInput = 0.0 - self.dataTransferOutput = 0.0 - if self.logFile is not None : - self.logFile.info("Stream %s : Disconnected",self.id) - except Exception as e: - if self.logFile is not None : - self.logFile.error("Stream %s : Failed to disconnect stream : %s",self.id,e) - raise e - - def UpdatelinkedPort(self, link : int): - """ - Updates the linked port with the specified link. - - Args: - link: The link to update. - - """ - if link in self.linkedPort: - if self.logFile is not None : - self.logFile.info("Stream %s : remove link : %s",self.id,link) - self.linkedPort.remove(link) - else: - if self.logFile is not None : - self.logFile.info("Stream %s : add link : %s",self.id,link) - self.linkedPort.append(link) - if self.connected : - self.updateLinkedPort.put(link) - - def toString(self): - """ - Return current running stream settings class as a string - Returns: - classToString(str , None): Return the class as a string - """ - if self.StreamType == StreamType.Serial: - return self.serialSettings.toString() - elif self.StreamType == StreamType.TCP: - return self.tcpSettings.toString() - elif self.StreamType == StreamType.UDP: - return self.udpSettings.toString() - elif self.StreamType == StreamType.NTRIP: - return self.ntripClient.ntripSettings.toString() - else: - return None - - def sendScript(self, queue : queue.Queue , startup : bool): - """ - ouput every command found in a script file - Args: - queue (queue.Queue): output queue - startup (bool): if startup script then True , else False - - Raises: - Exception: Error while opening script file - """ - - try : - if startup : - if self.logFile is not None : - self.logFile.info("Stream %s : send Startup script command",self.id) - if self.startupScript != "": - if self.logFile is not None : - self.logFile.debug("Stream %s : open Startup script file",self.id) - OpenScript = open(self.startupScript,"r") - if OpenScript is not None: - if self.logFile is not None : - self.logFile.debug("Stream %s : file not empty , send command to thread",self.id) - for line in OpenScript: - queue.put(line) - else : - if self.logFile is not None : - self.logFile.info("Stream %s : send closeup script command",self.id) - if self.closeScript != "": - if self.logFile is not None : - self.logFile.debug("Stream %s : open closeup script file",self.id) - OpenScript = open(self.closeScript,"r") - if OpenScript is not None: - if self.logFile is not None : - self.logFile.debug("Stream %s : file not empty , send command to thread",self.id) - for line in OpenScript: - queue.put(line) - - except Exception as e: - if self.logFile is not None : - self.logFile.error("Stream %s : failed to send script command to thread : %s",self.id,e) - raise Exception("Error : couldn't open the script file " + e) - - def sendCommand(self, CMD :str): - """ - send a command to ouput - """ - self.linkedData[self.id].put(CMD) - # Getter & Setter - - def ToggleInputDataVisibility(self): - """ - Toggles the visibility of input data. - """ - self.ShowInputData.clear() if self.ShowInputData.is_set() is True else self.ShowInputData.set() - - def ToggleOutputDataVisibility(self): - """ - Toggles the visibility of output data. - """ - self.ShowOutputData.clear() if self.ShowOutputData.is_set() is True else self.ShowOutputData.set() - - def ToggleAllDataVisibility(self): - """ - Toggle the data visibility for both output and input - """ - self.ToggleInputDataVisibility() - self.ToggleOutputDataVisibility() - - def setLogging(self): - """ - Toggle the logging event - """ - if self.logging: - self.logging = False - else : - self.logging = True - - def setStartupScript(self): - """ - Toggle the send script on startup event - """ - if self.sendStartupScript : - self.sendStartupScript = False - else : - self.sendStartupScript = True - - def setCloseScript(self): - """ - Toggle the send script on closeup event - """ - if self.sendCloseScript : - self.sendCloseScript = False - else : - self.sendCloseScript = True - - def setCloseScriptPath(self , newpath :str ): - """ - set the path to the closeup script file - - Args: - newpath (str): the new path to the closeup script - - Raises: - Exception: File not found - """ - if os.path.exists(newpath) : - self.closeScript = newpath - else : - if self.logFile is not None : - self.logFile.error("Stream %s : Closeup file not found : %s",self.id,newpath) - raise Exception("File not found !") - - def setStartupScriptPath(self,newpath :str ): - """ - set the path to the startup script file - Args: - newpath (str): the new path to the startup script - - Raises: - Exception: File not Found - """ - if os.path.exists(newpath) : - self.startupScript = newpath - else : - if self.logFile is not None : - self.logFile.error("Stream %s : Startup file not found : %s",self.id,newpath) - raise Exception("File not found !") - - def setLoggingFileName(self,newFileName : str): - """ - Set the logging file name use when logging is True - - Args: - newFileName (str): the new file name of the logging file - """ - if os.path.exists(newFileName): - self.loggingFile = newFileName - else : - if self.logFile is not None : - self.logFile.error("Stream %s : Logging file Path not found : %s",self.id,newFileName) - raise Exception("Path not found !") - - - def setStreamType(self,newStreamType : StreamType): - """ - Change the stream type of the current stream - - Args: - newStreamType (StreamType): the new stream type - """ - self.StreamType = newStreamType - - def setLineTermination(self,newLineTermination :str): - """ - set new line of termination - - Args: - newLineTermination (str): the new line termination - """ - self.lineTermination = newLineTermination - - def isConnected(self): - """ - return the current state of the stream - - Returns: - status(bool): return True if stream is connected , otherwise False - """ - if self.dataLinkStreamThread is None : - return False - if self.dataLinkStreamThread.is_alive() : - self.connected = True - else : - self.connected = False - return self.connected - - - - # Thread task Methods - - def DatalinkSerialTask(self, serial: Serial, linkedData: list[queue.Queue], updateLinkedPort: queue.Queue - , dataToShow : queue.Queue ): - """ - The task for data link Stream using serial communication. - - Args: - serial: The serial Stream object. - linkedData: A list of linked data queues. - updateLinkedPort: The queue for updating linked ports. - - Returns: - int: 0 if the task completes, otherwise None. - """ - LinkedPort = [] - datarate = 0 - tempInputTranfert = 0 - tempOutputTransfert = 0 - # Send startup command - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : Task Started " ) - self.logFile.info(f"Stream {self.id} : sending startup script" ) - try: - if not linkedData[self.id].empty(): - Task_SendCommand(linkedData[self.id],serial,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e : - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} : Start script couldn't finish : {e} " ) - self._ExceptionDisconnect() - raise Exception("Error : Start script couldn't finish " + e) - currentTime = datetime.datetime.now() - #Main loop - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : sending startup script finished " ) - self.logFile.info(f"Stream {self.id} : start main loop" ) - while self.StopEvent.is_set() is not True: - now = datetime.datetime.now() - if (now-currentTime).total_seconds() >= 1 : - currentTime = now - self.dataTransferInput = round(tempInputTranfert/1000,1) - self.dataTransferOutput = round(tempOutputTransfert/1000,1) - tempInputTranfert = 0 - tempOutputTransfert = 0 - try: - if serial is not None: - if serial.is_open: - inputData = serial.readline().decode(encoding='ISO-8859-1') - - tempInputTranfert += len(inputData) - if self.ShowInputData.is_set() and len(inputData) != 0: - dataToShow.put(inputData) - if self.logging: - self.logger.log(0,inputData) - if LinkedPort is not None: - for portid in LinkedPort: - portQueue: queue.Queue = linkedData[portid] - portQueue.put(inputData) - if not linkedData[self.id].empty(): - tempOutputTransfert += Task_SendCommand(linkedData[self.id],serial,self.ShowOutputData.is_set(), DataToShow=dataToShow,logger=self.logger,lineTermination=self.lineTermination ) - except Exception as e: - self._ExceptionDisconnect() - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} {self.StreamType} has been disconnected, error: {e}") - raise Exception(f"Stream {self.id} {self.StreamType} has been disconnected, error: {e}") - #Update current linked Streams list - if not updateLinkedPort.empty(): - Task_UpdateLinkedPort(updateLinkedPort , LinkedPort) - #Send closeup commands - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : main loop ended " ) - self.logFile.info(f"Stream {self.id} : sending closing script" ) - try : - if not linkedData[self.id].empty(): - Task_SendCommand(linkedData[self.id],serial,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e : - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} : closing script couldn't finish : {e} " ) - raise Exception("Error : closing script couldn't finish " + e) - return 0 - - def DatalinkTCPServerTask(self, tcp: socket.socket, linkedData: list[queue.Queue], updateLinkedPort: queue.Queue - , dataToShow : queue.Queue): - """ - The task for data link Stream using TCP communication. - - """ - LinkedPort: list[int] = [] - tcp.settimeout(0.1) - tcp.setblocking(0) - tempInputTranfert = 0 - tempOutputTransfert = 0 - # Wait for a client to connect to the server - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : Task Started " ) - self.logFile.info(f"Stream {self.id} : waiting for client to connect " ) - while True: - try: - tcp.listen() - conn, address = tcp.accept() - conn.settimeout(0.1) - break - except Exception as e : - if self.StopEvent.is_set(): - return 0 - # Send startup command - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : sending startup script" ) - try: - if not linkedData[self.id].empty(): - Task_SendCommand(linkedData[self.id],conn,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e : - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} : Start script couldn't finish : {e} " ) - self._ExceptionDisconnect() - raise Exception("Error : Start script couldn't finish " + e) - currentTime = datetime.datetime.now() - #Main loop - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : sending startup script finished " ) - self.logFile.info(f"Stream {self.id} : start main loop" ) - while self.StopEvent.is_set() is not True: - now = datetime.datetime.now() - if (now-currentTime).total_seconds() >= 1 : - currentTime = now - self.dataTransferInput = round(tempInputTranfert/1000,1) - self.dataTransferOutput = round(tempOutputTransfert/1000,1) - tempInputTranfert = 0 - tempOutputTransfert = 0 - try: - #If a client is connected - if conn is not None: - #Read input data - try: - inputData = conn.recv(4096).decode(encoding='ISO-8859-1') - tempInputTranfert += len(inputData) - if len(inputData) == 0: - conn = None - except socket.timeout: - inputData = "" - # Print data if show data input - if self.ShowInputData.is_set() and len(inputData) != 0: - dataToShow.put(inputData) - if self.logging: - self.logger.log(0,inputData) - # Send input data to linked Streams - if LinkedPort is not None and len(inputData) != 0: - for portid in LinkedPort: - portQueue: queue.Queue = linkedData[portid] - portQueue.put(inputData) - - #Send output data comming from other streams and print data if showdata is set - if not linkedData[self.id].empty(): - tempOutputTransfert+= Task_SendCommand(linkedData[self.id] , conn , self.ShowOutputData.is_set(), DataToShow=dataToShow,logger=self.logger,lineTermination=self.lineTermination) - else : - time.sleep(1) - #Wait for a new Client if the current one has disconnect - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : Client disconnected {e}") - while True: - try: - - tcp.listen() - conn, address = tcp.accept() - conn.settimeout(0.1) - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : new Client Connected {e}") - break - except Exception as e: - if self.StopEvent.is_set(): - return 0 - except Exception as e: - self._ExceptionDisconnect() - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} {self.StreamType} has been disconnected, error: {e}") - raise Exception(f"Stream {self.id} {self.StreamType} has been disconnected, error: {e}") - #Update current linked Streams list - if not updateLinkedPort.empty(): - LinkedPort = Task_UpdateLinkedPort(updateLinkedPort , LinkedPort) - #Send closeup commands - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : main loop ended " ) - self.logFile.info(f"Stream {self.id} : sending closing script" ) - try : - if not linkedData[self.id].empty(): - Task_SendCommand(linkedData[self.id],conn,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e : - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} : closing script couldn't finish : {e} " ) - raise Exception("Error : closing script couldn't finish " + e) - return 0 - - def DatalinkTCPClientTask(self, tcp: socket.socket, linkedData: list[queue.Queue], updateLinkedPort: queue.Queue - , dataToShow : queue.Queue): - """ - The task for data link Stream using TCP communication. - - """ - LinkedPort: list[int] = [] - tcp.settimeout(0.1) - conn = 1 - tempInputTranfert = 0 - tempOutputTransfert = 0 - #Send startup command - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : Task Started " ) - self.logFile.info(f"Stream {self.id} : sending startup script" ) - try: - if not linkedData[self.id].empty(): - Task_SendCommand(linkedData[self.id],tcp,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e : - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} : Start script couldn't finish : {e} " ) - self._ExceptionDisconnect() - raise Exception("Error : Start script couldn't finish " + e) - currentTime = datetime.datetime.now() - #Main loop - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : sending startup script finished " ) - self.logFile.info(f"Stream {self.id} : start main loop" ) - while self.StopEvent.is_set() is not True: - now = datetime.datetime.now() - if (now-currentTime).total_seconds() >= 1 : - currentTime = now - self.dataTransferInput = round(tempInputTranfert/1000,1) - self.dataTransferOutput = round(tempOutputTransfert/1000,1) - tempInputTranfert = 0 - tempOutputTransfert = 0 - try: - #While Connection still up - if conn is not None : - #Read input data - try: - inputData = tcp.recv(4096).decode(encoding='ISO-8859-1') - tempInputTranfert += len(inputData) - if len(inputData) == 0: - conn = None - except socket.timeout: - inputData = "" - except ConnectionResetError : - conn = None - except BrokenPipeError : - conn = None - # Print if show data - if self.ShowInputData.is_set() and len(inputData) != 0: - dataToShow.put(inputData) - if self.logging: - self.logger.log(0,inputData) - # Send data to linked stream - if LinkedPort is not None and len(inputData) != 0: - for portid in LinkedPort: - portQueue: queue.Queue = linkedData[portid] - portQueue.put(inputData) - # Output data comming from other streams - if not linkedData[self.id].empty(): - tempOutputTransfert+= Task_SendCommand(linkedData[self.id],tcp,self.ShowOutputData.is_set(),DataToShow=dataToShow,logger=self.logger,lineTermination=self.lineTermination) - else : - # If connection Lost try to reconnect to the server - while True: - try: - tcp = self.tcpSettings.connect() - conn = 1 - break - except Exception as e: - if self.StopEvent.is_set(): - return 0 - - #If there is any probleme , disconnect everything and kill thread - except Exception as e: - self._ExceptionDisconnect() - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} {self.StreamType} has been disconnected, error: {e}") - raise Exception(f"Stream {self.id} {self.StreamType} has been disconnected, error: {e}") - - # Update Current linked Stream list - if not updateLinkedPort.empty(): - Task_UpdateLinkedPort(updateLinkedPort,LinkedPort) - # Send Closeup command - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : main loop ended " ) - self.logFile.info(f"Stream {self.id} : sending closing script" ) - try : - Task_SendCommand(linkedData[self.id],tcp,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e : - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} : closing script couldn't finish : {e} " ) - raise Exception("Error : closing script couldn't finish " + e) - return 0 - - def DatalinkUDPTask(self, udp: socket.socket, linkedData: list[queue.Queue], updateLinkedPort: queue.Queue - , dataToShow : queue.Queue): - """ - Task for data link Stream using UDP communication. - """ - LinkedPort = [] - tempInputTranfert = 0 - tempOutputTransfert = 0 - if self.udpSettings.specificHost is True: - sendaddress = (self.udpSettings.host, self.udpSettings.port) - else: - sendaddress = ('', self.udpSettings.port) - #Send Startup command - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : Task Started " ) - self.logFile.info(f"Stream {self.id} : sending startup script" ) - try: - if not linkedData[self.id].empty(): - if sendaddress is not None: - Task_SendCommand(linkedData[self.id] , Stream=udp , udpsendaddress=sendaddress,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e : - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} : Start script couldn't finish : {e} " ) - self._ExceptionDisconnect() - raise Exception("Error : Start script couldn't finish " + e) - currentTime = datetime.datetime.now() - #Main loop - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : sending startup script finished " ) - self.logFile.info(f"Stream {self.id} : start main loop" ) - while self.StopEvent.is_set() is not True: - now = datetime.datetime.now() - if (now-currentTime).total_seconds() >= 1 : - currentTime = now - self.dataTransferInput = round(tempInputTranfert/1000,1) - self.dataTransferOutput = round(tempOutputTransfert/1000,1) - tempInputTranfert = 0 - tempOutputTransfert = 0 - try: - #Continue is Stream is still up - if udp is not None: - if self.udpSettings.DataFlow in (1, 2): - bytesAddressPair = udp.recvfrom(4096) - inputData = bytesAddressPair[0].decode(encoding='ISO-8859-1') - tempInputTranfert += len(inputData) - - if self.ShowInputData.is_set() and len(inputData) != 0: - dataToShow.put(inputData) - if self.logging: - self.logger.log(0,inputData) - - if LinkedPort is not None and len(inputData) != 0: - for port in LinkedPort: - linkedData[port].put(inputData) - - if self.udpSettings.DataFlow in (0, 2): - if not linkedData[self.id].empty(): - if sendaddress is not None: - tempOutputTransfert+= Task_SendCommand(linkedData[self.id] , udp , self.ShowOutputData.is_set() , sendaddress, dataToShow,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e: - self._ExceptionDisconnect() - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} {self.StreamType} has been disconnected, error: {e}") - raise Exception(f"Stream {self.id} {self.StreamType.name} has been disconnected, error: {e}") - #Update linked Streams list - if not updateLinkedPort.empty(): - Task_UpdateLinkedPort(updateLinkedPort,LinkedPort) - #Send closeup Commands - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : main loop ended " ) - self.logFile.info(f"Stream {self.id} : sending closing script" ) - try : - if not linkedData[self.id].empty(): - if sendaddress is not None: - Task_SendCommand(linkedData[self.id] ,Stream= udp , udpsendaddress=sendaddress,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e : - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} : closing script couldn't finish : {e} " ) - raise Exception("Error : closing script couldn't finish " + e) - return 0 - - def DatalinkNTRIPTask(self, ntrip: NtripClient, linkedData: list[queue.Queue], updateLinkedPort: queue.Queue - , dataToShow : queue.Queue): - """ - Process the NTRIP data received from the NTRIP client and send correction to other the linked streams . - - Args: - ntrip (NtripClient): The NTRIP client object. - linkedData (list[queue.Queue]): list of queues for sending or reading data to/from other streams . - updateLinkedPort (queue.Queue): The queue for updating the linked ports. - """ - LinkedPort: list[int] = [] - ntrip.socket.settimeout(0.1) - tempInputTranfert = 0 - tempOutputTransfert = 0 - # Send startup command - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : Task Started " ) - self.logFile.info(f"Stream {self.id} : sending startup script" ) - try: - if self.ntripClient.ntripSettings.fixedPos : - linkedData[self.id].put(self.ntripClient.fixedPosGga) - if not linkedData[self.id].empty(): - Task_SendCommand(linkedData[self.id],ntrip ,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e : - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} : Start script couldn't finish : {e} " ) - self._ExceptionDisconnect() - raise Exception("Error : Start script couldn't finish " + e) - currentTime = datetime.datetime.now() - #Main loop - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : sending startup script finished " ) - self.logFile.info(f"Stream {self.id} : start main loop" ) - while self.StopEvent.is_set() is not True: - now = datetime.datetime.now() - if (now-currentTime).total_seconds() >= 1 : - currentTime = now - self.dataTransferInput = round(tempInputTranfert/1000,1) - self.dataTransferOutput = round(tempOutputTransfert/1000,1) - tempInputTranfert = 0 - tempOutputTransfert = 0 - try: - if ntrip is not None: - #Read input data - try: - inputData = ntrip.socket.recv(4096).decode(encoding='ISO-8859-1') - tempInputTranfert += len(inputData) - except socket.timeout: - inputData = "" - # Print data if show data input - if self.ShowInputData.is_set() and len(inputData) != 0: - dataToShow.put(inputData) - if self.logging: - self.logger.log(0,inputData) - - # Send input data to linked Streams - if LinkedPort is not None and len(inputData) != 0: - for portid in LinkedPort: - portQueue: queue.Queue = linkedData[portid] - portQueue.put(inputData) - #Send output data comming from other streams and print data if showdata is set - if not linkedData[self.id].empty(): - tempOutputTransfert+= Task_SendCommand(linkedData[self.id],ntrip ,self.ShowOutputData.is_set(),DataToShow=dataToShow,logger=self.logger,lineTermination=self.lineTermination) - - except Exception as e: - self._ExceptionDisconnect() - raise Exception(f"Stream {self.id} {self.StreamType} has been disconnected, error: {e}") - - #Update current linked Streams list - if not updateLinkedPort.empty(): - Task_UpdateLinkedPort(updateLinkedPort,LinkedPort) - if self.logFile is not None : - self.logFile.info(f"Stream {self.id} : main loop ended " ) - self.logFile.info(f"Stream {self.id} : sending closing script" ) - #Send closeup commands - try : - if not linkedData[self.id].empty(): - Task_SendCommand(linkedData[self.id],ntrip,logger=self.logger,lineTermination=self.lineTermination) - except Exception as e : - if self.logFile is not None : - self.logFile.error(f"Stream {self.id} : closing script couldn't finish : {e} " ) - raise Exception("Error : closing script couldn't finish " + e) - return 0 - - # Command line Configuration Functions - - def CommandLineConfig(self,commandLineConfig : str): - """ - Configure a Stream with a single line of configuration - - Args: - commandLineConfig (str): Configuration line - - """ - PortToLink = [] - try : - streamType :str = commandLineConfig.split("://")[0] - config: str = commandLineConfig.split("://")[1] - if '#' in config : - PortToLink = config.split("#")[1].split(",") - config = config.split("#")[0] - if streamType.lower() == "serial": - self.ConfigSerialStream(commandConfig=config) - elif streamType.lower() == "tcpcli": - self.ConfigTCPStream(isServer=False , commandConfig=config) - elif streamType.lower() == "tcpsrv": - self.ConfigTCPStream(isServer=True, commandConfig=config) - elif streamType.lower() == "udp": - self.ConfigUDPStream(commandConfig=config) - elif streamType.lower() == "udpspe": - self.ConfigUDPStream(specificHost=True, commandConfig= config) - elif streamType.lower() == "ntrip": - self.ConfigNTRIPStream(config) - - except Exception as e : - raise Exception(f"config line is not correct ! , {e}") - - if len(PortToLink) > 0 : - for i in PortToLink: - try : - link = int(i) - if link != self.id: - self.linkedPort.append(link) - except : - pass - try: - self.Connect(self.StreamType) - except Exception as e: - raise e - - def ConfigNTRIPStream(self,CommandConfig : str ): - """ - Init a NTRIP Client with a configuration line - - Args: - CommandConfig (str): configuration line - """ - credentials = CommandConfig.split("@")[0].split(":") - if len(credentials) != 2 : - raise Exception("Missing a credential paremeter !") - - host = CommandConfig.split("@")[1].split(":") - if len(host) != 2: - raise Exception("Missing a host paremeter !") - - mountpoint = CommandConfig.split("@")[1].split("/") - if len(mountpoint) != 2: - raise Exception("Missing a MountPoint paremeter !") - - try : - self.ntripClient = NtripClient(host = host[0], port = int(host[1].split("/")[0]), auth= (True if len(credentials[0]) > 0 and len(credentials[1]) > 0 else False) ,username= credentials[0],password= credentials[1],mountpoint=mountpoint[1]) - self.StreamType = StreamType.NTRIP - except Exception as e : - raise Exception(f"Error : given parameters for a NTRIP Client stream are incorrect : \n{e}") - - def ConfigUDPStream(self,specificHost : bool = False, commandConfig : str = None): - """ - Init a UDP stream with a configuration line - Args: - specificHost (bool, optional): if True the UDP Stream will stream only to a specific host name. Defaults to False. - commandConfig (str, optional): Configuration line. Defaults to None. - - Raises: - Exception: too few or too much parameter - Exception: Given parameter incorrect - Exception: Given parameter incorrect (Specific host) - """ - if specificHost : - config = commandConfig.split(":")[0] - if len(config) != 2 : - raise Exception("Error : too few or too much parameter") - else : - try: - self.udpSettings = UdpSettings(host=config[0], port=int(config[1]), specificHost=specificHost) - self.StreamType = StreamType.UDP - except Exception as e : - raise Exception("Error : given parameter for a UDP stream are incorrect : \n{e}") - else : - try: - self.udpSettings = UdpSettings(port=int(commandConfig)) - self.StreamType = StreamType.UDP - except Exception as e : - raise Exception("Error : given parameter for a UDP stream are incorrect : \n{e}") - - def ConfigTCPStream(self,isServer : bool = False ,commandConfig : str = None) -> None: - """ - Init a TCP stream with a configuration line - Args: - isServer (bool, optional): If True the TCP stream will be configure as a Server. Defaults to False. - commandConfig (str, optional): Configuration line . Defaults to None. - - Raises: - Exception: too few or too much parameter - Exception: Given parameter incorrect (Server) - Exception: Given parameter incorrect (Client) - """ - if isServer : - config = commandConfig.split(":") - if len(config) != 2 : - raise Exception("Error : too few or too much parameter") - try : - self.tcpSettings = TcpSettings(host=config[0] , port=config[1], streamMode= StreamMode.SERVER) - except Exception as e : - raise Exception("Error : given parameters for a TCP Server stream are incorrect : \n{e}") - else : - try : - self.tcpSettings = TcpSettings(port=commandConfig , streamMode= StreamMode.CLIENT) - except Exception as e : - raise Exception("Error : given parameters for a TCP Client stream are incorrect : \n{e}") - self.StreamType = StreamType.TCP - - def ConfigSerialStream(self,commandConfig :str) -> None: - """ - Init a Serial stream with a configuration line - exemple : - Args: - commandConfig (str): Configuration line - - Raises: - Exception: If too few argument for a proper configuration - Exception: If given argument are Incorrect - """ - config = commandConfig.split(":") - if len(config) != 6 : - raise Exception("Error : too few or too much parameter") - port : str = config[0] - try : - baudrate : BaudRate = BaudRate(config[1]) - except : - baudrate : BaudRate = BaudRate.eBAUD115200 - try : - parity : Parity = Parity(config[2]) - except : - parity : Parity = Parity.PARITY_NONE - try : - stopbits : StopBits = StopBits(config[3]) - except : - stopbits : StopBits = StopBits.STOPBITS_ONE - try : - bytesize : ByteSize= ByteSize(config[4]) - except : - bytesize : ByteSize = ByteSize.EIGHTBITS - - rtscts : bool = True if config[5] == "1" else False - try : - self.serialSettings = SerialSettings(Port= port , baudrate=baudrate , parity=parity , stopbits=stopbits,bytesize=bytesize , Rtscts=rtscts) - self.StreamType = StreamType.Serial - except Exception as e : - raise Exception("Error : given parameters for a Serial stream are incorrect : \n{e}") - - # .conf file Configuration Functions - - def ConfFileConfig(self,confFile : configparser.SectionProxy) : - """ - Init a stream and it's settings with a configuration file - - Args: - confFile (configparser.SectionProxy): section of a stream in a configuration file - """ - self.serialSettings: SerialSettings = ConfFileSerial(confFile , self.logFile ) - self.tcpSettings: TcpSettings = ConfFileTCP(confFile, self.logFile ) - - self.udpSettings : UdpSettings = ConfFileUDP(confFile , self.logFile ) - self.ntripClient : NtripClient = ConfFileNTRIPClient(confFile , self.logFile ) - try : - self.StreamType : StreamType = StreamType(int(confFile.get("connectionType"))) - except : - self.StreamType : StreamType = StreamType.NONE - links = confFile.get("linksChecked").replace("[","").replace(" ","").replace("]","").split(",") - for link in links: - if link != '': - self.linkedPort.append(int(link)) - self.startupScript = confFile.get("startupscriptfile") - try : - self.sendStartupScript = True if confFile.get("startupscript").lower() == "true" else False - except : - self.sendStartupScript = False - self.closeScript = confFile.get("closescriptfile") - try : - self.sendCloseScript = True if confFile.get("closescript").lower() == "true" else False - except : - self.sendCloseScript = False - self.loggingFile = confFile.get("logfile") - if self.loggingFile != "": - self.logging = True - - # Private Methods - - def _ExceptionDisconnect(self): - """ - Disconnects the port if is connected in case of a exception caused in the task Thread - """ - if self.logFile is not None : - self.logFile.info("Stream %s : Disconnecting stream due to error in task") - self.StopEvent.set() - self.currentTask = None - try : - self.Stream.close() - finally: - self.connected = False - self.dataTransferInput = 0.0 - self.dataTransferOutput = 0.0 - - def _clearQueue(self, queue : queue.Queue): - """ - clear the queue passed as argument - Args: - queue (queue.Queue): queue to empty - """ - while not queue.empty(): - queue.get() - -def ConfFileSerial(confFile :configparser.SectionProxy, logFile : logging.Logger): - """ - Init Serial settings of the current stream with value from a configuration file. - If no value in configuration file , default value will be use - - Args: - confFile (configparser.SectionProxy): configuration file - - Returns: - SerialSetting: return a new SerialSettings - """ - port : str = confFile.get('Serial.Port') - try : - baudrate : BaudRate = BaudRate(confFile.get('Serial.BaudRate')) - except : - baudrate : BaudRate = BaudRate.eBAUD115200 - try : - parity : Parity = Parity(confFile.get('Serial.Parity')) - except : - parity : Parity = Parity.PARITY_NONE - try : - stopbits : StopBits = StopBits(confFile.get('Serial.StopBits') ) - except : - stopbits : StopBits = StopBits.STOPBITS_ONE - try : - bytesize : ByteSize= ByteSize(confFile.get('Serial.ByteSize')) - except : - bytesize : ByteSize = ByteSize.EIGHTBITS - try : - rtscts : bool = True if confFile.get('Serial.RtcCts').lower() == "true" else False - except : - rtscts = False - return SerialSettings(Port= port , baudrate=baudrate , parity=parity , stopbits=stopbits,bytesize=bytesize , Rtscts=rtscts, logFile=logFile) - -def ConfFileTCP(confFile : configparser.SectionProxy, logFile : logging.Logger): - """ - Init TCP settings of the current stream with value from a configuration file. - If no value in configuration file , default value will be use - - Args: - confFile (configparser.SectionProxy): configuration file - - Returns: - TcpSettings: return a new TcpSettings - """ - host : str = confFile.get('hostName') - try : - port : int = int(confFile.get('portNumber')) - except : - port = 28784 - - if bool(True if str(confFile.get('TCPserver')).lower() == "true" else False ) is True : - host = '' - streamMode : StreamMode = StreamMode.SERVER - else: - streamMode : StreamMode = StreamMode.CLIENT - - return TcpSettings(host= host , port= port ,streamMode=streamMode,logFile=logFile) - -def ConfFileUDP(confFile : configparser.SectionProxy, logFile : logging.Logger): - """ - Init UDP settings of the current stream with value from a configuration file. - If no value in configuration file , default value will be use - - Args: - confFile (configparser.SectionProxy): configuration file - - Returns: - UdpSettings: return a new udpsettings - """ - host : str = confFile.get('hostNameUDP') - try : - port : int = int(confFile.get('portNumberUDP')) - except : - port = 28784 - try: - dataFlow : DataFlow = DataFlow(confFile.get('dataDirUDP')) - except : - dataFlow : DataFlow = DataFlow.Both - try : - specificHost : bool = True if confFile.get('specificIpUDP').lower() == "true" else False - except : - specificHost = False - - return UdpSettings(host=host,port=port , dataflow=dataFlow,specificHost=specificHost , logFile= logFile) - -def ConfFileNTRIPClient(confFile : configparser.SectionProxy , logFile : logging.Logger): - """ - Init a Ntrip client with value from a configuration file. - If no value in configuration file , default value will be use - - Args: - confFile (configparser.SectionProxy): configuration file - - Returns: - NtripClient: return a new ntrip - """ - host : str = confFile.get('NTRIP.hostname') - try : - port : int = int(confFile.get('NTRIP.portnumber')) - except: - port = 2101 - mountpoint : str = confFile.get('NTRIP.mountPoint') - try : - auth : bool = True if confFile.get('NTRIP.authenticationenabled').lower() == "true" else False - except : - auth = False - username : str = confFile.get('NTRIP.user') - try : - password : str = base64.b64decode((confFile.get('NTRIP.password')).encode()).decode() - except : - password : str = "" - try : - tls : bool = True if confFile.get('NTRIP.TLS').lower() == "true" else False - except : - tls = False - try : - fixedPos : bool = True if confFile.get('NTRIP.fixedpositionset').lower() == "true" else False - except : - fixedPos = False - - try : - latitude : float = float(confFile.get('NTRIP.fixedlatitude')) - except : - latitude = 00.000000000 - try : - longitude : float = float(confFile.get('NTRIP.fixedlongitude')) - except : - longitude = 000.000000000 - try : - height : int = int(confFile.get('NTRIP.fixedheight')) - except : - height = 0 - - - return NtripClient(host=host , port=port , auth=auth , username=username , password= password , mountpoint=mountpoint - , tls= tls ,fixedPos=fixedPos , latitude = latitude , longitude = longitude , height = height , logFile= logFile ) diff --git a/src/StreamConfig/Preferences.py b/src/StreamConfig/Preferences.py index a7b29fe..67024e1 100644 --- a/src/StreamConfig/Preferences.py +++ b/src/StreamConfig/Preferences.py @@ -1,21 +1,21 @@ # ############################################################################### -# +# # Copyright (c) 2024, Septentrio -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: -# +# # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. -# +# # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# +# # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. -# +# # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -27,94 +27,54 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import configparser -import os +class Preferences : + """ + Preferences class : save every preferences and options done previously + """ -""" -Preferences class : save every preferences and options done previously -""" -class Preferences : + def __init__(self,max_streams : int = 2, config_name : str = "Datalink_Config" , + line_termination :str = "\r\n") -> None: + self.max_streams = max_streams + self.connect : list[bool] = [] + self.config_name : str = config_name + for _ in range(max_streams) : + self.connect.append(False) + self.line_termination : str = line_termination - def __init__(self,maxStreams : int = 2, configName : str = "Datalink_Config" ,lineTermination :str = "\r\n" - , ConfigFile : configparser.SectionProxy = None) -> None: - if ConfigFile is None: - self.maxStreams = maxStreams - self.Connect : list[bool] = [] - self.configName : str = configName - for i in range(maxStreams) : - self.Connect.append(False) - self.lineTermination : str = lineTermination - else : - - try : - self.maxStreams : int = int(ConfigFile.get("numberOfPortPanels")) - except : - self.maxStreams = maxStreams - self.Connect : list[bool] = [] - for connectid in range(maxStreams): - try : - self.Connect.append( True if ConfigFile.get(f"connect{connectid}").lower() == "true" else False ) - except : - self.Connect.append(False) - try : - self.lineTermination :str = ConfigFile.get("lineTermination").replace("\\n","\n").replace("\\r","\r") - except : - self.lineTermination : str = "\n\r" - - try: - if len(ConfigFile.get("configName")) != 0 : - self.configName : str = str(ConfigFile.get("configName")) - else : - self.configName : str = configName - except : - self.configName : str = configName - - def setMaxStream(self,newMaxStream : int ): + def set_max_stream(self,new_max_stream : int ): """ Set number of stream that can be configure Args: - newMaxStream (int): the new number of stream + new_max_stream (int): the new number of stream """ - self.maxStreams = newMaxStream - - def setConfigName(self , newName : str): + self.max_streams = new_max_stream + + def set_config_name(self , new_name : str): """ Set the current configuration file name Args: - newName (str): the new file name + new_name (str): the new file name """ - self.configName = newName - - def setLineTermination(self,newLineTermination :str): + self.config_name = new_name + + def set_line_termination(self,new_line_termination :str): """ Set the Termination line when showing data and sending data Args: - newLineTermination (str): the new termination line + new_line_termination (str): the new termination line """ - self.lineTermination = newLineTermination - - def getLineTermination(self): + self.line_termination = new_line_termination + + def get_line_termination(self): """ Convert the termination line to readable string Returns: termination Line : readable termination line """ - return self.lineTermination.replace("\n","\\n").replace("\r","\\r") - - def SaveConfig(self , sectionName : str,SaveConfigFile : configparser.ConfigParser): - """ - Add current class values in the configFile - Args: - sectionName (str): name of the current section - SaveConfigFile (configparser.ConfigParser): configparser of the configuration file - """ - SaveConfigFile.set(sectionName , "configName" ,str(self.configName)) - SaveConfigFile.set(sectionName,"numberOfPortPanels",str(self.maxStreams)) - SaveConfigFile.set(sectionName,"lineTermination",self.lineTermination.replace("\n","\\n").replace("\r","\\r")) - for connect , index in zip(self.Connect,range(len(self.Connect))): - connectString = "connect" + str(index) - SaveConfigFile.set(sectionName,connectString,str(connect)) + return self.line_termination.replace("\n","\\n").replace("\r","\\r") + + diff --git a/src/StreamConfig/Stream.py b/src/StreamConfig/Stream.py new file mode 100644 index 0000000..2e39a4b --- /dev/null +++ b/src/StreamConfig/Stream.py @@ -0,0 +1,1112 @@ +# ############################################################################### +# +# Copyright (c) 2024, Septentrio +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from enum import Enum +from io import TextIOWrapper +import os +import socket +import threading +import queue +import logging +from datetime import datetime +import time +from serial import Serial, SerialException + +from ..NTRIP.NtripSettings import NtripSettingsException +from ..StreamSettings.UdpSettings import UDPSettingsException, UdpSettings +from ..StreamSettings.SerialSettings import SerialSettings, SerialSettingsException +from ..StreamSettings.TcpSettings import StreamMode, TCPSettingsException , TcpSettings +from ..NTRIP.NtripClient import NtripClient , NtripClientError +from ..constants import DEFAULTLOGFILELOGGER + +class StreamException(Exception): + """ + Exception class for Stream class + """ + def __init__(self, message, error_code = None): + super().__init__(message) + self.error_code = error_code +class MissingSettingsException(StreamException): + """Raised when the settings used to conect a stream is missing + """ +class OpenConnectionError(StreamException): + """Raised when openning connection failed + """ +class InvalidStreamTypeException(StreamException): + """Raised when the given stream type is not supported + """ +class DisconnectError(StreamException): + """Raised when the disconnection failed + """ +class ScriptFileException(StreamException): + """Raised when a issue occur with a script file + """ +class LogFileException(StreamException): + """Raised when a issue occur with the log file""" +class TaskException(StreamException): + """Raised when a issue occur in a Task function + """ +class StreamThreadException(StreamException): + """Raised when a issue occur with the stream thread + """ + +class StreamType(Enum): + """Possible communication type + """ + Serial = 0 + TCP = 1 + UDP = 2 + NTRIP = 3 + NONE = None + +class Stream: + """ + Represents a port/stream for data streaming. + + """ + + def __init__(self, stream_id : int = 0, linked_data: list[queue.Queue] = None , + debug_logging : bool = False ): + + self.stream_id = stream_id + self.linked_ports: list[int] = [] + self.connected: bool = False + self.current_task = None + self.stream_type: StreamType = StreamType.NONE + self.stream : Serial | socket.socket | NtripClient | None = None + self.line_termination :str = "\r\n" + self.data_transfer_input : float = 0.0 + self.data_transfer_output :float = 0.0 + self.debug_logging : bool = debug_logging + self.startup_error ="" + #Support Log File + + if debug_logging : + self.log_file : logging.Logger = DEFAULTLOGFILELOGGER + else : + self.log_file = None + + # logging file + + self.logging : bool = False + self.logging_file : str = "" + self.logger : TextIOWrapper = None + + # Startup and close script + + self.startup_script : str = "" + self.close_script : str = "" + self.send_startup_script : bool = False + self.send_close_script : bool = False + + # Event for data visibility and stop + + self.show_incoming_data = threading.Event() + self.show_outgoing_data = threading.Event() + self.stop_event = threading.Event() + + # Queue for data link between ports + + self.linked_data = linked_data + self.update_linked_ports_queue: queue.Queue = queue.Queue() + self.data_to_show: queue.Queue = queue.Queue() + + # Thread for data read/link + + self.datalink_stream_thread: threading.Thread = None + + # Init all Settings + self.serial_settings = SerialSettings(debug_logging = debug_logging) + self.tcp_settings = TcpSettings(debug_logging = debug_logging) + self.udp_settings = UdpSettings(debug_logging = debug_logging) + self.ntrip_client = NtripClient() + + def connect(self, stream_type : StreamType = None): + """ + Connects the port using the specified Stream type. + + Args: + stream_type: The type of Stream. + + Returns: + int: 0 if the Stream fails, otherwise None. + """ + if self.log_file is not None : + self.log_file.info("Connecting Stream %s " , self.stream_id) + if stream_type is None : + stream_type = self.stream_type + if self.log_file is not None : + self.log_file.info("Stream %s : start new %s Stream" , self.stream_id , stream_type.name) + if self.connected is True: + if self.log_file is not None : + self.log_file.error("Stream %s : Stream was already connected",self.stream_id) + else : + if stream_type == StreamType.Serial: + if self.serial_settings.port == "" or self.serial_settings is None: + if self.log_file is not None : + self.log_file.error("Stream %s : Failed to open Serial stream : Serial port hasn't been configured yet" , self.stream_id) + raise MissingSettingsException("Serial settings hasn't been given yet") + else: + try: + self.stream = self.serial_settings.connect() + self.connected = True + task = self.datalink_serial_task + if self.log_file is not None : + self.log_file.info("Stream %s : Stream openned successfully " , self.stream_id) + except SerialSettingsException as e: + if self.log_file is not None : + self.log_file.error("Stream %s : Failed to connect to the serial port : %s" , self.stream_id,e) + self.stream = None + self.connected = False + raise OpenConnectionError(e) from e + + elif stream_type == StreamType.TCP: + if self.log_file is not None : + self.log_file.debug("Stream %s : Create TCP %s with host : %s and port : %s" , self.stream_id,self.stream_type.name,self.tcp_settings.host , self.tcp_settings.port) + if self.tcp_settings is None: + if self.log_file is not None : + self.log_file.error("Stream %s : Failed to open TCP stream : TCP settings not set " , self.stream_id) + raise MissingSettingsException("tcp settings are empty !") + else: + try: + socket.gethostbyname(self.tcp_settings.host) + self.stream = self.tcp_settings.connect() + self.connected = True + if self.tcp_settings.stream_mode == StreamMode.SERVER: + task = self.datalink_tcp_server_task + else : + task = self.datalink_tcp_client_task + if self.log_file is not None : + self.log_file.info("Stream %s : Stream openned successfully " , self.stream_id) + except TCPSettingsException as e : + self.stream = None + self.connected = False + if self.log_file is not None : + self.log_file.error("Stream %s : Failed to open TCP stream: %s",self.stream_id,e) + raise OpenConnectionError(e) from e + + elif stream_type == StreamType.UDP: + if self.udp_settings is None: + if self.log_file is not None : + self.log_file.error("Stream %s : Failed to open UDP stream : UDP settings not set ", self.stream_id) + raise MissingSettingsException("udp settings are empty!") + else: + try: + self.connected = True + socket.gethostbyname(self.tcp_settings.host) + self.stream = self.udp_settings.connect() + task = self.datalink_udp_task + if self.log_file is not None : + self.log_file.info("Stream %s : Stream openned successfully " , self.stream_id) + except UDPSettingsException as e: + self.stream = None + self.connected = False + if self.log_file is not None : + self.log_file.error("Stream %s : Failed to open TCP stream: %s" , self.stream_id,e) + raise OpenConnectionError(e) from e + + elif stream_type == StreamType.NTRIP: + if self.ntrip_client is None or len(self.ntrip_client.ntrip_settings.host.replace(" ","")) == 0 : + if self.log_file is not None : + self.log_file.error("Stream %s : Failed to open NTRIP stream : Incorect Settings " , self.stream_id) + raise MissingSettingsException("ntrip client is not set !") + else: + try: + socket.gethostbyname(self.ntrip_client.ntrip_settings.host) + self.ntrip_client.connect() + self.stream = self.ntrip_client + self.connected = True + task = self.datalink_ntrip_task + if self.ntrip_client.ntrip_settings.fixed_pos: + self.ntrip_client.create_gga_string() + + if self.log_file is not None : + self.log_file.info("Stream %s : Stream openned successfully " , self.stream_id) + except (NtripClientError,NtripSettingsException) as e: + self.stream = None + self.connected = False + if self.log_file is not None : + self.log_file.error("Stream %s : Failed to open NTRIP stream: %s" , self.stream_id,e) + raise OpenConnectionError(f"Failed to open NTRIP Stream : {e}") from e + elif stream_type == StreamType.NONE : + if self.log_file is not None : + self.log_file.error("Stream %s : no configuration yet " , self.stream_id) + raise InvalidStreamTypeException(" No configuration selected ") + else: + if self.log_file is not None : + self.log_file.error("Stream %s : Invalid Stream Type " , self.stream_id) + raise InvalidStreamTypeException(f" {stream_type.name} is not a valid Stream type !") + if self.connected is True: + try: + if self.log_file is not None : + self.log_file.debug("Stream %s : start final configuration " , self.stream_id) + + self.stop_event.clear() + + if self.logging : + self.logger = open(self.logging_file,"w",encoding="utf-8") + + if self.log_file is not None : + self.log_file.debug("Stream %s : init loggin file : %s" , self.stream_id,self.logging_file) + + if self.send_startup_script: + + if self.log_file is not None : + self.log_file.debug("Stream %s : init startup script file : %s" , self.stream_id,self.startup_script) + + self._clear_queue(self.linked_data[self.stream_id]) + self.send_script(self.linked_data[self.stream_id], True) + self.datalink_stream_thread = threading.Thread(target=task,args=(self.stream, self.linked_data, self.update_linked_ports_queue,self.data_to_show)) + + if self.log_file is not None : + self.log_file.debug("Stream %s : Starting Thread " , self.stream_id) + + self.datalink_stream_thread.start() + self.current_task = task + + if len(self.linked_ports) != 0: + + if self.log_file is not None : + self.log_file.info("Stream %s : update linked Port : %s" , self.stream_id ,str(self.linked_ports) ) + + for link in self.linked_ports: + self.update_linked_ports_queue.put(link) + + if self.log_file is not None : + self.log_file.info("Stream %s : final configuration finished " , self.stream_id ) + + except Exception as e: + if self.log_file is not None : + self.log_file.error("Stream %s : Failed during final configuration : %s" , self.stream_id ,e ) + self.connected = False + raise StreamThreadException(e) from e + + def disconnect(self): + """ + Disconnects the port if is connected. + """ + if self.stream is not None: + if self.log_file is not None : + self.log_file.info("Stream %s : Disconnecting stream",self.stream_id) + try: + if self.send_close_script: + self.send_script(self.linked_data[self.stream_id], False) + self.stop_event.set() + self.current_task = None + self.datalink_stream_thread.join() + if self.log_file is not None : + self.log_file.debug("Stream %s : wait for Thread to stop",self.stream_id) + self.stream.close() + self.connected = False + self.data_transfer_input = 0.0 + self.data_transfer_output = 0.0 + if self.log_file is not None : + self.log_file.info("Stream %s : Disconnected",self.stream_id) + except Exception as e: + if self.log_file is not None : + self.log_file.error("Stream %s : Failed to disconnect stream : %s",self.stream_id,e) + raise DisconnectError(e) from e + + def update_linked_ports(self, link : int): + """ + Updates the linked port with the specified link. + + Args: + link: The link to update. + + """ + if link in self.linked_ports: + if self.log_file is not None : + self.log_file.info("Stream %s : remove link : %s",self.stream_id,link) + self.linked_ports.remove(link) + else: + if self.log_file is not None : + self.log_file.info("Stream %s : add link : %s",self.stream_id,link) + self.linked_ports.append(link) + if self.connected : + self.update_linked_ports_queue.put(link) + + def to_string(self): + """ + Return current running stream settings class as a string + Returns: + classToString(str , None): Return the class as a string + """ + if self.stream_type == StreamType.Serial: + return self.serial_settings.to_string() + elif self.stream_type == StreamType.TCP: + return self.tcp_settings.to_string() + elif self.stream_type == StreamType.UDP: + return self.udp_settings.to_string() + elif self.stream_type == StreamType.NTRIP: + return self.ntrip_client.ntrip_settings.to_string() + else: + return "" + + def send_script(self, output_queue : queue.Queue , startup : bool): + """ + ouput every command found in a script file + Args: + queue (queue.Queue): output queue + startup (bool): if startup script then True , else False + + Raises: + : Error while opening script file + """ + try : + if startup : + if self.log_file is not None : + self.log_file.info("Stream %s : send Startup script command",self.stream_id) + if self.startup_script != "": + if self.log_file is not None : + self.log_file.debug("Stream %s : open Startup script file",self.stream_id) + open_script = open(self.startup_script,"r",encoding="utf-8") + if open_script is not None: + + if self.log_file is not None : + self.log_file.debug("Stream %s : file not empty , send command to thread",self.stream_id) + + for line in open_script: + output_queue.put(line) + else : + if self.log_file is not None : + self.log_file.info("Stream %s : send closeup script command",self.stream_id) + + if self.close_script != "": + + if self.log_file is not None : + self.log_file.debug("Stream %s : open closeup script file",self.stream_id) + + open_script = open(self.close_script,"r",encoding="utf-8") + if open_script is not None: + + if self.log_file is not None : + self.log_file.debug("Stream %s : file not empty , send command to thread",self.stream_id) + + for line in open_script: + output_queue.put(line) + + except Exception as e: + if self.log_file is not None : + self.log_file.error("Stream %s : failed to send script command to thread : %s",self.stream_id,e) + raise ScriptFileException("Couldn't open the script file " + e) from e + + def send_command(self, command :str): + """ + send a command to ouput + """ + self.linked_data[self.stream_id].put(command) + # Getter & Setter + + def toggle_incomming_data_visibility(self): + """ + Toggles the visibility of input data. + """ + return self.show_incoming_data.clear() if self.show_incoming_data.is_set() is True else self.show_incoming_data.set() + + def toggle_outgoing_data_visibility(self): + """ + Toggles the visibility of output data. + """ + return self.show_outgoing_data.clear() if self.show_outgoing_data.is_set() is True else self.show_outgoing_data.set() + + def toggle_all_data_visibility(self): + """ + Toggle the data visibility for both output and input + """ + self.toggle_incomming_data_visibility() + self.toggle_outgoing_data_visibility() + + def set_logging(self): + """ + Toggle the logging event + """ + if self.logging: + self.logging = False + else : + self.logging = True + + def set_startup_script(self): + """ + Toggle the send script on startup event + """ + if self.send_startup_script : + self.send_startup_script = False + else : + self.send_startup_script = True + + def set_close_script(self): + """ + Toggle the send script on closeup event + """ + if self.send_close_script : + self.send_close_script = False + else : + self.send_close_script = True + + def set_close_script_path(self , new_path :str ): + """ + set the path to the closeup script file + + Args: + new_path (str): the new path to the closeup script + + Raises: + Exception: File not found + """ + if os.path.exists(new_path) : + self.close_script = new_path + else : + if self.log_file is not None : + self.log_file.error("Stream %s : Closeup file not found : %s",self.stream_id,new_path) + raise ScriptFileException("File not found ") + + def set_startup_script_path(self,new_path :str ): + """ + set the path to the startup script file + Args: + new_path (str): the new path to the startup script + + Raises: + ScriptFileException: File not Found + """ + if os.path.exists(new_path) : + self.startup_script = new_path + else : + if self.log_file is not None : + self.log_file.error("Stream %s : Startup file not found : %s",self.stream_id,new_path) + raise ScriptFileException("File not found ") + + def set_logging_file_name(self,new_file_name : str): + """ + Set the logging file name use when logging is True + + Args: + new_file_name (str): the new file name of the logging file + """ + if os.path.exists(new_file_name): + self.logging_file = new_file_name + else : + if self.log_file is not None : + self.log_file.error("Stream %s : Logging file Path not found : %s",self.stream_id,new_file_name) + raise LogFileException("Path not found") + + def set_stream_type(self,new_stream_type : StreamType): + """ + Change the stream type of the current stream + + Args: + new_stream_type (StreamType): the new stream type + """ + self.stream_type = new_stream_type + + def set_line_termination(self,new_line_termination :str): + """ + set new line of termination + + Args: + new_line_termination (str): the new line termination + """ + self.line_termination = new_line_termination + + def is_connected(self): + """ + return the current state of the stream + + Returns: + status(bool): return True if stream is connected , otherwise False + """ + if self.datalink_stream_thread is None : + return False + if self.datalink_stream_thread.is_alive() : + self.connected = True + else : + self.connected = False + return self.connected + + def _clear_queue(self, queue_to_empty : queue.Queue): + """ + clear the queue passed as argument + Args: + queue (queue.Queue): queue to empty + """ + while not queue_to_empty.empty(): + queue_to_empty.get() + + def _exception_disconnect(self): + """ + Disconnects the port if is connected in case of a exception caused in the task Thread + """ + self.stop_event.set() + self.current_task = None + try : + if self.stream is not None: + self.stream.close() + finally: + self.connected = False + self.data_transfer_input = 0.0 + self.data_transfer_output = 0.0 + + + + # Thread task Methods + + def datalink_serial_task(self, serial: Serial, linked_data: list[queue.Queue], update_linked_ports_queue: queue.Queue + , data_to_show : queue.Queue ): + """ + The task for data link Stream using serial communication. + + Args: + serial: The serial Stream object. + linkedData: A list of linked data queues. + update_linked_ports_queue: The queue for updating linked ports. + + Returns: + int: 0 if the task completes, otherwise None. + """ + linked_ports = [] + temp_incoming_tranfert = 0 + temp_outgoing_tranfert = 0 + # Send startup command + if self.log_file is not None : + self.log_file.info("Stream %i : Task Started " , self.stream_id ) + self.log_file.info("Stream %i : sending startup script" , self.stream_id ) + try: + if not self.linked_data[self.stream_id].empty(): + task_send_command(self.linked_data[self.stream_id],serial,logger=self.logger,line_termination=self.line_termination) + except TaskException as e : + if self.log_file is not None : + self.log_file.error("Stream %i : Start script couldn't finish : %e ", self.stream_id , e ) + self._exception_disconnect() + raise ScriptFileException(f"Start script couldn't finish {e}") from e + current_time = datetime.now() + #Main loop + if self.log_file is not None : + self.log_file.info("Stream %i : sending startup script finished ", self.stream_id ) + self.log_file.info("Stream %i : start main loop",self.stream_id ) + while self.stop_event.is_set() is not True: + temp_incoming_tranfert ,temp_outgoing_tranfert, current_time = self.task_data_transfer_rate( current_time , temp_incoming_tranfert , temp_outgoing_tranfert) + try: + if serial is not None: + if serial.is_open: + incoming_data = serial.readline().decode(encoding='ISO-8859-1') + temp_incoming_tranfert += len(incoming_data) + if self.show_incoming_data.is_set() and len(incoming_data) != 0: + data_to_show.put(incoming_data) + if self.logging: + self.logger.write(incoming_data) + if linked_ports is not None: + for portid in linked_ports: + port_queue: queue.Queue = linked_data[portid] + port_queue.put(incoming_data) + if not linked_data[self.stream_id].empty(): + temp_outgoing_tranfert += task_send_command(linked_data[self.stream_id],serial,self.show_outgoing_data.is_set(), data_to_show=data_to_show,logger=self.logger,line_termination=self.line_termination ) + except Exception as e: + self._exception_disconnect() + if self.log_file is not None : + self.log_file.error("Stream %i %s has been disconnected, error: %e",self.stream_id , self.stream_type , e ) + raise StreamThreadException(f"Stream {self.stream_id} {self.stream_type} has been disconnected, error: {e}") from e + #Update current linked Streams list + if not update_linked_ports_queue.empty(): + task_update_linked_port(update_linked_ports_queue, linked_ports) + #Send closeup commands + if self.log_file is not None : + self.log_file.info("Stream %i : main loop ended ",self.stream_id ) + self.log_file.info("Stream %i : sending closing script" , self.stream_id ) + try : + if not self.linked_data[self.stream_id].empty(): + task_send_command(linked_data[self.stream_id],serial,logger=self.logger,line_termination=self.line_termination) + except TaskException as e : + if self.log_file is not None : + self.log_file.error("Stream %i : closing script couldn't finish : %e " , self.stream_id , e) + raise ScriptFileException(f"Closeup script couldn't finish {e}") from e + return 0 + + def datalink_tcp_server_task(self, tcp: socket.socket, linked_data: list[queue.Queue], update_linked_ports_queue: queue.Queue + , data_to_show : queue.Queue): + """ + The task for data link Stream using TCP communication. + + """ + linked_ports: list[int] = [] + tcp.settimeout(0.1) + tcp.setblocking(0) + temp_incoming_tranfert = 0 + temp_outgoing_tranfert = 0 + # Wait for a client to connect to the server + if self.log_file is not None : + self.log_file.info("Stream %i : Task Started " , self.stream_id ) + self.log_file.info("Stream %i : waiting for client to connect " , self.stream_id) + while True: + try: + tcp.listen() + conn, _ = tcp.accept() + conn.settimeout(0.1) + break + except (TimeoutError , BlockingIOError) : + if self.stop_event.is_set(): + return self._exception_disconnect() + # Send startup command + if self.log_file is not None : + self.log_file.info("Stream %i : sending startup script" , self.stream_id ) + try: + if not self.linked_data[self.stream_id].empty(): + task_send_command(self.linked_data[self.stream_id],conn,logger=self.logger,line_termination=self.line_termination) + except TaskException as e : + if self.log_file is not None : + self.log_file.error("Stream %i : Start script couldn't finish : %e ", self.stream_id , e ) + self._exception_disconnect() + raise ScriptFileException(f"Start script couldn't finish {e}") from e + current_time = datetime.now() + #Main loop + if self.log_file is not None : + self.log_file.info("Stream %i : sending startup script finished ", self.stream_id ) + self.log_file.info("Stream %i : start main loop",self.stream_id ) + while self.stop_event.is_set() is not True: + temp_incoming_tranfert ,temp_outgoing_tranfert, current_time = task_data_transfer_rate(self , current_time , temp_incoming_tranfert , temp_outgoing_tranfert) + try: + #If a client is connected + if conn is not None: + #Read input data + try: + incoming_data = conn.recv(4096).decode(encoding='ISO-8859-1') + temp_incoming_tranfert += len(incoming_data) + if len(incoming_data) == 0: + conn = None + except socket.timeout: + incoming_data = "" + # Print data if show data input + if self.show_incoming_data.is_set() and len(incoming_data) != 0: + data_to_show.put(incoming_data) + if self.logging: + self.logger.write(incoming_data) + # Send input data to linked Streams + if linked_ports is not None and len(incoming_data) != 0: + for portid in linked_ports: + linked_data[portid].put(incoming_data) + + #Send output data comming from other streams and print data if showdata is set + if not linked_data[self.stream_id].empty(): + temp_outgoing_tranfert+= task_send_command(linked_data[self.stream_id] , conn , self.show_outgoing_data.is_set(), data_to_show=data_to_show,logger=self.logger,line_termination=self.line_termination) + else : + time.sleep(1) + #Wait for a new Client if the current one has disconnect + if self.log_file is not None : + self.log_file.info(f"Stream {self.stream_id} : Client disconnected {e}") + while True: + try: + + tcp.listen() + conn, address = tcp.accept() + conn.settimeout(0.1) + if self.log_file is not None : + self.log_file.info(f"Stream {self.stream_id} : new Client Connected {e}") + break + except Exception as e : + if self.stop_event.is_set(): + raise e + except Exception as e: + self._exception_disconnect() + if self.log_file is not None : + self.log_file.error("Stream %i %s has been disconnected, error: %e",self.stream_id , self.stream_type , e ) + raise StreamThreadException(f"Stream {self.stream_id} {self.stream_type} has been disconnected, error: {e}") from e + #Update current linked Streams list + if not update_linked_ports_queue.empty(): + linked_ports = task_update_linked_port(update_linked_ports_queue , linked_ports) + #Send closeup commands + if self.log_file is not None : + self.log_file.info("Stream %i : main loop ended ",self.stream_id ) + self.log_file.info("Stream %i : sending closing script" , self.stream_id ) + try : + if not self.linked_data[self.stream_id].empty(): + task_send_command(self.linked_data[self.stream_id],conn,logger=self.logger,line_termination=self.line_termination) + except TaskException as e : + if self.log_file is not None : + self.log_file.error("Stream %i : closing script couldn't finish : %e " , self.stream_id , e) + raise ScriptFileException(f"Closeup script couldn't finish {e}") from e + return 0 + + def datalink_tcp_client_task(self, tcp: socket.socket, linked_data: list[queue.Queue], update_linked_ports_queue: queue.Queue + , data_to_show : queue.Queue): + """ + The task for data link Stream using TCP communication. + + """ + linked_ports: list[int] = [] + tcp.settimeout(0.1) + conn = 1 + temp_incoming_tranfert = 0 + temp_outgoing_tranfert = 0 + #Send startup command + if self.log_file is not None : + self.log_file.info("Stream %i : Task Started " , self.stream_id ) + self.log_file.info("Stream %i : sending startup script" , self.stream_id ) + try: + if not self.linked_data[self.stream_id].empty(): + task_send_command(self.linked_data[self.stream_id],tcp,logger=self.logger,line_termination=self.line_termination) + except TaskException as e : + if self.log_file is not None : + self.log_file.error("Stream %i : Start script couldn't finish : %e ", self.stream_id , e ) + self._exception_disconnect() + raise ScriptFileException(f"Start script couldn't finish {e}") from e + current_time = datetime.now() + #Main loop + if self.log_file is not None : + self.log_file.info("Stream %i : sending startup script finished ", self.stream_id ) + self.log_file.info("Stream %i : start main loop",self.stream_id ) + while self.stop_event.is_set() is not True: + temp_incoming_tranfert ,temp_outgoing_tranfert,current_time = task_data_transfer_rate(self , current_time , temp_incoming_tranfert , temp_outgoing_tranfert) + try: + #While Connection still up + if conn is not None : + #Read input data + try: + incoming_data = tcp.recv(4096).decode(encoding='ISO-8859-1') + temp_incoming_tranfert += len(incoming_data) + if len(incoming_data) == 0: + conn = None + except socket.timeout: + incoming_data = "" + except ConnectionResetError : + conn = None + except BrokenPipeError : + conn = None + # Print if show data + if self.show_incoming_data.is_set() and len(incoming_data) != 0: + data_to_show.put(incoming_data) + if self.logging: + self.logger.write(incoming_data) + # Send data to linked stream + if linked_ports is not None and len(incoming_data) != 0: + for portid in linked_ports: + linked_data[portid].put(incoming_data) + # Output data comming from other streams + if not linked_data[self.stream_id].empty(): + returnedValue = task_send_command(linked_data[self.stream_id],tcp,self.show_outgoing_data.is_set(),data_to_show=data_to_show,logger=self.logger,line_termination=self.line_termination) + if returnedValue is not None : + temp_outgoing_tranfert += returnedValue + else : + # If connection Lost try to reconnect to the server + while True: + try: + tcp = self.tcp_settings.connect() + conn = 1 + break + except TimeoutError: + if self.stop_event.is_set(): + return 0 + + #If there is any probleme , disconnect everything and kill thread + except Exception as e: + self._exception_disconnect() + if self.log_file is not None : + self.log_file.error("Stream %i %s has been disconnected, error: %e",self.stream_id , self.stream_type , e ) + raise StreamThreadException(f"Stream {self.stream_id} {self.stream_type} has been disconnected, error: {e}") from e + + # Update Current linked Stream list + if not self.update_linked_ports_queue.empty(): + task_update_linked_port(self.update_linked_ports,linked_ports) + # Send Closeup command + if self.log_file is not None : + self.log_file.info("Stream %i : main loop ended ",self.stream_id ) + self.log_file.info("Stream %i : sending closing script" , self.stream_id ) + try : + task_send_command(self.linked_data[self.stream_id],tcp,logger=self.logger,line_termination=self.line_termination) + except TaskException as e : + if self.log_file is not None : + self.log_file.error("Stream %i : closing script couldn't finish : %e " , self.stream_id , e) + raise ScriptFileException(f"Start script couldn't finish {e}") from e + return 0 + + def datalink_udp_task(self, udp: socket.socket, linked_data: list[queue.Queue], update_linked_ports_queue: queue.Queue + , data_to_show : queue.Queue ): + """ + Task for data link Stream using UDP communication. + """ + linked_ports = [] + temp_incoming_tranfert = 0 + temp_outgoing_tranfert = 0 + bytes_address_pair = None + udp.settimeout(0.1) + if self.udp_settings.specific_host is True: + sendaddress = (self.udp_settings.host, self.udp_settings.port) + else: + sendaddress = ('localhost', self.udp_settings.port) + #Send Startup command + if self.log_file is not None : + self.log_file.info("Stream %i : Task Started " , self.stream_id ) + self.log_file.info("Stream %i : sending startup script" , self.stream_id ) + try: + if not self.linked_data[self.stream_id].empty(): + if sendaddress is not None: + task_send_command(self.linked_data[self.stream_id] , stream=udp , udp_send_address=sendaddress,logger = self.logger,line_termination = self.line_termination) + except TaskException as e : + if self.log_file is not None : + self.log_file.error("Stream %i : Start script couldn't finish : %e ", self.stream_id , e ) + self._exception_disconnect() + raise ScriptFileException(f"Start script couldn't finish {e}") from e + current_time = datetime.now() + #Main loop + if self.log_file is not None : + self.log_file.info("Stream %i : sending startup script finished ", self.stream_id ) + self.log_file.info("Stream %i : start main loop",self.stream_id ) + while self.stop_event.is_set() is not True: + temp_incoming_tranfert ,temp_outgoing_tranfert,current_time = task_data_transfer_rate(self , current_time , temp_incoming_tranfert , temp_outgoing_tranfert) + try: + #Continue is Stream is still up + if udp is not None: + if self.udp_settings.dataflow.value in (1, 2): + try : + bytes_address_pair = udp.recvfrom(4096) + incoming_data = bytes_address_pair[0].decode(encoding='ISO-8859-1') + temp_incoming_tranfert += len(incoming_data) + except socket.timeout : + incoming_data = "" + if self.show_incoming_data.is_set() and len(incoming_data) != 0: + data_to_show.put(incoming_data) + if self.logging: + self.logger.write(incoming_data) + + if linked_ports is not None and len(incoming_data) != 0: + for port in linked_ports: + linked_data[port].put(incoming_data) + + if self.udp_settings.dataflow.value in (0, 2): + if not linked_data[self.stream_id].empty(): + if bytes_address_pair is not None: + send_to_addresse = (bytes_address_pair[1],self.udp_settings.port) + else : + send_to_addresse = sendaddress + returned_value = task_send_command(self.linked_data[self.stream_id] , stream=self.stream , show_data=self.show_outgoing_data.is_set() , udp_send_address=send_to_addresse , data_to_show=self.data_to_show,logger=self.logger,line_termination=self.line_termination) + if returned_value is not None : + temp_outgoing_tranfert += returned_value + except Exception as e: + self._exception_disconnect() + if self.log_file is not None : + self.log_file.error("Stream %i %s has been disconnected, error: %e",self.stream_id , self.stream_type , e ) + raise StreamThreadException(f"Stream {self.stream_id} {self.stream_type} has been disconnected, error: {e}") from e + #Update linked Streams list + if not self.update_linked_ports_queue.empty(): + task_update_linked_port(update_linked_ports_queue,linked_ports) + #Send closeup Commands + if self.log_file is not None : + self.log_file.info("Stream %i : main loop ended ",self.stream_id ) + self.log_file.info("Stream %i : sending closing script" , self.stream_id ) + try : + if not self.linked_data[self.stream_id].empty(): + if sendaddress is not None: + task_send_command(self.linked_data[self.stream_id] ,stream= udp , udp_send_address=sendaddress,logger=self.logger,line_termination=self.line_termination) + except TaskException as e : + if self.log_file is not None : + self.log_file.error("Stream %i : closing script couldn't finish : %e " , self.stream_id , e) + raise ScriptFileException(f"Start script couldn't finish {e}") from e + return 0 + + def datalink_ntrip_task(self, ntrip : NtripClient, linked_data: list[queue.Queue], update_linked_ports_queue: queue.Queue + , data_to_show : queue.Queue ): + """ + Process the NTRIP data received from the NTRIP client and send correction to other the linked streams . + + Args: + ntrip (NtripClient): The NTRIP client object. + linked_data (list[queue.Queue]): list of queues for sending or reading data to/from other streams . + update_linked_ports (queue.Queue): The queue for updating the linked ports. + """ + linked_ports: list[int] = [] + ntrip.socket.settimeout(0.1) + temp_incoming_tranfert = 0 + temp_outgoing_tranfert = 0 + # Send startup command + if self.log_file is not None : + self.log_file.info("Stream %i : Task Started " , self.stream_id ) + self.log_file.info("Stream %i : sending startup script" , self.stream_id ) + try: + if self.ntrip_client.ntrip_settings.fixed_pos : + self.linked_data[self.stream_id].put(self.ntrip_client.fixed_pos_gga) + if not self.linked_data[self.stream_id].empty(): + task_send_command(self.linked_data[self.stream_id],ntrip ,logger=self.logger,line_termination=self.line_termination) + except TaskException as e : + if self.log_file is not None : + self.log_file.error("Stream %i : Start script couldn't finish : %e ", self.stream_id , e ) + self._exception_disconnect() + raise ScriptFileException(f"Start script couldn't finish {e}") from e + current_time = datetime.now() + #Main loop + if self.log_file is not None : + self.log_file.info("Stream %i : sending startup script finished ", self.stream_id ) + self.log_file.info("Stream %i : start main loop",self.stream_id ) + while self.stop_event.is_set() is not True: + temp_incoming_tranfert ,temp_outgoing_tranfert,current_time = task_data_transfer_rate(self , current_time , temp_incoming_tranfert , temp_outgoing_tranfert) + try: + if ntrip is not None: + #Read input data + try: + incoming_data = ntrip.socket.recv(4096).decode(encoding='ISO-8859-1') + temp_incoming_tranfert += len(incoming_data) + except socket.timeout: + incoming_data = "" + # Print data if show data input + if self.show_incoming_data.is_set() and len(incoming_data) != 0: + data_to_show.put(incoming_data) + if self.logging: + self.logger.write(incoming_data) + + # Send input data to linked Streams + if linked_ports is not None and len(incoming_data) != 0: + for portid in linked_ports: + portQueue: queue.Queue = linked_data[portid] + portQueue.put(incoming_data) + #Send output data comming from other streams and print data if showdata is set + if not linked_data[self.stream_id].empty(): + returnedValue = task_send_command(linked_data[self.stream_id],ntrip ,self.show_outgoing_data.is_set(),data_to_show=data_to_show,logger=self.logger,line_termination=self.line_termination) + if returnedValue is not None : + temp_outgoing_tranfert += returnedValue + except Exception as e: + self._exception_disconnect() + if self.log_file is not None : + self.log_file.error("Stream %i %s has been disconnected, error: %e",self.stream_id , self.stream_type , e ) + raise StreamThreadException(f"Stream {self.stream_id} {self.stream_type} has been disconnected, error: {e}") from e + #Update current linked Streams list + if not self.update_linked_ports_queue.empty(): + task_update_linked_port(update_linked_ports_queue,linked_ports) + if self.log_file is not None : + self.log_file.info("Stream %i : main loop ended ",self.stream_id ) + self.log_file.info("Stream %i : sending closing script" , self.stream_id ) + #Send closeup commands + try : + if not self.linked_data[self.stream_id].empty(): + + task_send_command(self.linked_data[self.stream_id],ntrip, + logger=self.logger,line_termination=self.line_termination) + + except TaskException as e : + if self.log_file is not None : + self.log_file.error("Stream %i : closing script couldn't finish : %e " , self.stream_id , e) + raise ScriptFileException(f"Start script couldn't finish {e}") from e + return 0 + + # Command line Configuration Functions + + + def _ExceptionDisconnect(self): + """ + Disconnects the port if is connected in case of a exception caused in the task Thread + """ + if self.log_file is not None : + self.log_file.info("Stream %s : Disconnecting stream due to error in task") + self.stop_event.set() + self.current_task = None + try : + self.stream.close() + finally: + self.connected = False + self.data_transfer_input = 0.0 + self.data_transfer_output = 0.0 + + def _clearQueue(self, queue : queue.Queue): + """ + clear the queue passed as argument + Args: + queue (queue.Queue): queue to empty + """ + while not queue.empty(): + queue.get() + + def task_data_transfer_rate(self , current_time , temp_incoming_tranfert , temp_outgoing_tranfert) : + """ + Calculate current data rate from incoming and outgoing stream + """ + now = datetime.now() + if (now-current_time).total_seconds() >= 1 : + current_time = now + self.data_transfer_input = round(temp_incoming_tranfert/1000,1) + self.data_transfer_output = round(temp_outgoing_tranfert/1000,1) + return 0 , 0 , current_time + else : + return temp_incoming_tranfert , temp_outgoing_tranfert ,current_time +##################################################################################### +# STREAM TASK +##################################################################################### + +def task_update_linked_port(update_linked_ports_queue : queue.Queue , linked_ports : list[int] ): + """ + update the list of port to which we have to share incoming data + """ + for _ in range(update_linked_ports_queue.qsize()): + port = update_linked_ports_queue.get() + if port in linked_ports: + linked_ports.remove(port) + else: + linked_ports.append(port) + return linked_ports + +def task_send_command(linked_data : queue.Queue , stream : Serial | socket.socket | NtripClient, show_data : bool = False , + udp_send_address = None , data_to_show : queue.Queue = None , + logger : TextIOWrapper = None , line_termination : str = "\r\n"): + """ + output data from data queue + """ + try : + for _ in range(linked_data.qsize()): + outgoing_data : str = linked_data.get() + if isinstance(stream , Serial): + stream.write((outgoing_data+line_termination).encode(encoding='ISO-8859-1')) + elif isinstance(stream, socket.socket) and udp_send_address is None: + stream.sendall(outgoing_data.encode(encoding='ISO-8859-1')+ line_termination) + elif isinstance(stream, socket.socket) : + stream.sendto(outgoing_data.encode(encoding='ISO-8859-1') + line_termination, udp_send_address) + elif isinstance(stream, NtripClient): + if "GGA" in outgoing_data: + stream.send_nmea(outgoing_data) + else : continue + if show_data and len(outgoing_data) != 0 : + data_to_show.put(outgoing_data) + try : + if logger is not None: + logger.write(outgoing_data) + except : pass + return len(outgoing_data) + except (NtripClientError, socket.gaierror, SerialException) as e : + raise TaskException(e) from e + + +def task_data_transfer_rate(stream :Stream , current_time , temp_incoming_tranfert , temp_outgoing_tranfert) : + """ + Calculate current data rate from incoming and outgoing stream + """ + now = datetime.now() + if (now-current_time).total_seconds() >= 1 : + current_time = now + stream.data_transfer_input = round(temp_incoming_tranfert/1000,1) + stream.data_transfer_output = round(temp_outgoing_tranfert/1000,1) + return 0 , 0 , current_time + else : + return temp_incoming_tranfert , temp_outgoing_tranfert ,current_time + \ No newline at end of file diff --git a/src/StreamConfig/Streams.py b/src/StreamConfig/Streams.py deleted file mode 100644 index 78ce26b..0000000 --- a/src/StreamConfig/Streams.py +++ /dev/null @@ -1,164 +0,0 @@ -# ############################################################################### -# -# Copyright (c) 2024, Septentrio -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# 3. Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import base64 -import queue -import configparser -import os -import serial -from .Preferences import Preferences -from .PortConfig import PortConfig -from src import constants -import logging -""" - Class which initialise every Port/Stream. - Depending on the input it can be configure via a configuration file or via a configuration list - -""" -class Streams : - - def __init__(self,maxStream : int = 6, configFile :str = None , streamSettingsList : list[str] = None): - - self.preferences : Preferences = Preferences() - self.maxStream = maxStream - self.configFile = configFile - self.streamSettingsList = streamSettingsList - self.StreamList : list[PortConfig] = [] - self.linkedData : list[queue.Queue] = [] - self.logFile : logging.Logger = logging.getLogger("PyDatalink") - logging.basicConfig(filename= constants.DEFAULTLOGFILE, encoding='utf-8', level=logging.DEBUG, format='[%(asctime)s] %(levelname)s : %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') - #Init Communication Queues - for i in range (self.maxStream) : - self.linkedData.append(queue.Queue()) - - if self.streamSettingsList is None: - - #Init Communication Ports with default Value - if configFile is None : - for i in range(maxStream): - if configFile is None : - newPort = PortConfig(i ,self.linkedData , logFile=self.logFile ) - self.StreamList.append(newPort) - self.preferences = Preferences(maxStream) - - #init Communication Ports with a ConfigFile - else : - config = configparser.ConfigParser() - readValue = config.read(configFile) - if len(readValue) != 0 : - try : - self.maxStream = int(config.get("Preferences","numberOfPortPanels")) - except : - self.maxStream = maxStream - nbPreConfig = 0 - for key in config.sections(): - if "Preferences" in key: - preferenceKey = config[key] - self.preferences = Preferences(self.maxStream , ConfigFile= preferenceKey) - - if "Port" in key and nbPreConfig < self.maxStream : - portKey = config[key] - newPort = PortConfig(nbPreConfig ,self.linkedData,portKey, logFile=self.logFile) - self.StreamList.append(newPort) - nbPreConfig += 1 - if nbPreConfig < self.maxStream : - for i in range(self.maxStream - nbPreConfig): - newPort = PortConfig(i + nbPreConfig ,self.linkedData , logFile=self.logFile ) - self.StreamList.append(newPort) - for portId in range(len(self.preferences.Connect)): - self.StreamList[portId].setLineTermination(self.preferences.lineTermination) - if self.preferences.Connect[portId] : - try : - self.StreamList[portId].Connect(self.StreamList[portId].StreamType) - except: - print(f"Stream {portId} couldn't start properly") - else : - raise Exception("Init Error","The given file is empty") - else : - - iterator = 0 - for stream in self.streamSettingsList : - StreamType = stream.split("://")[0] - if StreamType.lower() in ["udp","udpspe","tcpcli","tcpsrv","serial","ntrip"]: - try : - newPort = PortConfig(iterator,self.linkedData,commandLineConfig=stream , logFile= self.logFile) - self.StreamList.append(newPort) - iterator += 1 - except Exception as e : - print(f"Could not open {StreamType} : {e}") - self.CloseAll() - break - else : - raise ValueError(f" {StreamType} is not a valid stream type") - - - def CloseAll(self): - """ - Close every Stream that are still connected - """ - if self.streamSettingsList is None : - self.createConfFile() - for port in self.StreamList: - port.Disconnect() - self.linkedData.clear() - self.StreamList.clear() - - def createConfFile(self): - """ - Create the default configuration file with the current values - """ - confFileName = constants.CONFIGPATH + "/temp" - confFile = open(confFileName, "w") - - # Add content to the file - Config = configparser.ConfigParser() - for stream in self.StreamList : - sectionName = "Port"+str(stream.id) - Config.add_section(sectionName) - Config.set(sectionName,"linksChecked" , str(stream.linkedPort)) - Config.set(sectionName,"startupScript" ,str(stream.sendStartupScript )) - Config.set(sectionName,"startupScriptFile" , stream.startupScript) - Config.set(sectionName,"closeScript",str(stream.sendCloseScript)) - Config.set(sectionName,"closeScriptFile",stream.closeScript) - Config.set(sectionName,"logFile",stream.loggingFile) - stream.tcpSettings.SaveConfig(sectionName , Config) - stream.udpSettings.SaveConfig(sectionName,Config) - Config.set(sectionName,"connectionType",str(stream.StreamType.value)) - stream.serialSettings.SaveConfig(sectionName , Config) - stream.ntripClient.ntripSettings.SaveConfig(sectionName , Config) - - Config.add_section("Preferences") - self.preferences.SaveConfig("Preferences",Config) - Config.write(confFile) - confFile.close() - if os.path.exists(constants.DEFAULTCONFIGFILE) : - os.remove(constants.DEFAULTCONFIGFILE) - os.rename(confFileName , constants.DEFAULTCONFIGFILE) - - diff --git a/src/StreamConfig/__init__.py b/src/StreamConfig/__init__.py index 080b5b5..e69de29 100644 --- a/src/StreamConfig/__init__.py +++ b/src/StreamConfig/__init__.py @@ -1,6 +0,0 @@ -from .PortConfig import PortConfig , StreamType -from .Preferences import Preferences -from .Streams import Streams -from .UdpSettings import UdpSettings , DataFlow -from .TcpSettings import TcpSettings, StreamMode -from .SerialSettings import SerialSettings , BaudRate , Parity , StopBits , ByteSize \ No newline at end of file diff --git a/src/StreamConfig/SerialSettings.py b/src/StreamSettings/SerialSettings.py similarity index 56% rename from src/StreamConfig/SerialSettings.py rename to src/StreamSettings/SerialSettings.py index fcb1a1e..08be025 100644 --- a/src/StreamConfig/SerialSettings.py +++ b/src/StreamSettings/SerialSettings.py @@ -1,21 +1,21 @@ # ############################################################################### -# +# # Copyright (c) 2024, Septentrio -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: -# +# # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. -# +# # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# +# # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. -# +# # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -27,13 +27,30 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import configparser from enum import Enum import logging from serial import Serial import serial.tools.list_ports +from serial.serialutil import SerialException + +from src.constants import DEFAULTLOGFILELOGGER + +class SerialSettingsException(Exception): + """ + Exception class for Serial settings + """ + def __init__(self, message, error_code = None): + super().__init__(message) + self.error_code = error_code +class PortInUseException(SerialSettingsException): + """Raised when the given serial port is already in use + """ +class PortNotFound(SerialSettingsException): + """Raised when the port is not found + """ class BaudRate(Enum): + """ Baudrate of a Serial port """ eBAUD300 = "300" eBAUD600 = "600" eBAUD1200 = "1200" @@ -58,6 +75,7 @@ class BaudRate(Enum): eBAUD4000000 = "4000000" class Parity(Enum): + """ Parity of a Serial port """ PARITY_NONE = 'N' PARITY_EVEN = 'E' PARITY_ODD = 'O' @@ -65,11 +83,13 @@ class Parity(Enum): PARITY_SPACE= 'S' class StopBits(Enum): + """ StopBits of a Serial port """ STOPBITS_ONE = 1 STOPBITS_ONE_POINT_FIVE= 1.5 STOPBITS_TWO = 2 class ByteSize(Enum): + """ Bytesize of a Serial port """ FIVEBITS = 5 SIXBITS = 6 SEVENBITS = 7 @@ -82,25 +102,29 @@ class SerialSettings: """ - def __init__(self, Port: str = "", baudrate: BaudRate = BaudRate.eBAUD115200, parity: Parity = Parity.PARITY_NONE, - stopbits: StopBits = StopBits.STOPBITS_ONE, bytesize: ByteSize = ByteSize.EIGHTBITS, - Rtscts: bool = False , logFile : logging = None): + def __init__(self, port: str = "", baudrate: BaudRate = BaudRate.eBAUD115200, + parity: Parity = Parity.PARITY_NONE,stopbits: StopBits = StopBits.STOPBITS_ONE, + bytesize: ByteSize = ByteSize.EIGHTBITS, + rtscts: bool = False , debug_logging : bool = False): """ Initializes a new instance of the SerialSettings class. """ - self.port :str = Port + self.port :str = port self.baudrate : BaudRate = baudrate self.parity : Parity = parity self.stopbits : StopBits = stopbits self.bytesize : ByteSize = bytesize - self.rtscts : bool= Rtscts - - # Support Logging file - self.logFile : logging = logFile + self.rtscts : bool= rtscts + # Support Logging file + if debug_logging : + self.log_file : logging.Logger = DEFAULTLOGFILELOGGER + else : + self.log_file = None # type: ignore - def GetAvailablePort(self)-> list : + + def get_available_port(self)-> list : """ Gets the list of available ports. @@ -108,14 +132,14 @@ def GetAvailablePort(self)-> list : list: The list of available ports. """ ports = serial.tools.list_ports.comports() - availablePort = [] - for port, desc, hwid in sorted(ports): - availablePort.append([port, desc]) - if self.logFile is not None : - self.logFile.debug("%s serial port detected" , str(len(availablePort))) - return availablePort + available_port = [] + for port, desc, _ in sorted(ports): + available_port.append([port, desc]) + if self.log_file is not None : + self.log_file.debug("%s serial port detected" , str(len(available_port))) + return available_port - def Connect(self) -> Serial | None : + def connect(self) -> Serial | None : """ Connects to the serial port. @@ -123,22 +147,29 @@ def Connect(self) -> Serial | None : Serial: The serial port object. """ try: - newSerial = Serial(None, baudrate=self.baudrate.value, bytesize=self.bytesize.value, - parity=self.parity.value, stopbits=self.stopbits.value, rtscts=self.rtscts, timeout=0.01,exclusive=True) - newSerial.port = self.port - newSerial.open() - return newSerial - except Exception as e: - if e.args[0] == 11 : - if self.logFile is not None : - self.logFile.error("Port %s is already in use" ,self.port ) - raise Exception ("PortError","Port already in use") + new_serial = Serial(None, baudrate=int(self.baudrate.value), + bytesize=self.bytesize.value,parity=self.parity.value, + stopbits=self.stopbits.value, rtscts=self.rtscts, + timeout=0.01,exclusive=True) + new_serial.port = self.port + new_serial.open() + return new_serial + except SerialException as e: + if e.args[0] == 11 or "PermissionError" in e.args[0]: + if self.log_file is not None : + self.log_file.error("Port %s is already in use" ,self.port ) + raise PortInUseException("Port already in use") from e + elif "FileNotFoundError" in e.args[0]: + if self.log_file is not None : + self.log_file.error("Port %s not found " ,self.port ) + raise PortNotFound("Port unreachable or not available") from e else: - self.logFile.error("Failed to open serial stream with port %s", self.port) - self.logFile.error("%s", e) - raise(e) + if self.log_file is not None : + self.log_file.error("Failed to open serial stream with port %s", self.port) + self.log_file.error("%s", e) + raise SerialSettingsException(e) from e - def setPort(self, newport : str): + def set_port(self, newport : str): """ Sets the port name. @@ -165,34 +196,34 @@ def set_parity(self, newparity : Parity): """ self.parity = newparity - def set_stopbits(self, newStopBits : StopBits): + def set_stopbits(self, new_stop_bits : StopBits): """ Sets the stop bits setting. Args: - newStopBits (StopBits): The new stop bits setting. + new_stop_bits (StopBits): The new stop bits setting. """ - self.stopbits = newStopBits + self.stopbits = new_stop_bits - def set_bytesize(self, newbytesize : ByteSize): + def set_bytesize(self, new_bytesize : ByteSize): """ Sets the byte size setting. Args: - newbytesize (ByteSize): The new byte size setting. + new_bytesize (ByteSize): The new byte size setting. """ - self.bytesize = newbytesize + self.bytesize = new_bytesize - def set_rtscts(self, newrtccts : bool): + def set_rtscts(self, new_rtccts : bool): """ Sets the RTS/CTS flow control setting. Args: - newrtccts (bool): The new RTS/CTS flow control setting. + new_rtccts (bool): The new RTS/CTS flow control setting. """ - self.rtscts = newrtccts + self.rtscts = new_rtccts - def toString(self) -> str : + def to_string(self) -> str : """ Return current class as a string @@ -200,19 +231,4 @@ def toString(self) -> str : str: class as string """ parity = self.parity.name.replace("PARITY_","") - return f"Port : {self.port} \n BaudRate :{self.baudrate.value} \n Parity : {parity} \n StopBits : {self.stopbits.value} \n ByteSize : {self.bytesize.value} \n rtscts : {self.rtscts}" - - def SaveConfig(self , sectionName : str,SaveConfigFile : configparser.ConfigParser): - """ - Add current class values in the configFile - Args: - sectionName (str): name of the current section - SaveConfigFile (configparser.ConfigParser): configparser of the configuration file - """ - - SaveConfigFile.set(sectionName,"Serial.Port", str(self.port)) - SaveConfigFile.set(sectionName,"Serial.BaudRate",str(self.baudrate.value)) - SaveConfigFile.set(sectionName,"Serial.Parity",str(self.parity.value)) - SaveConfigFile.set(sectionName,"Serial.StopBits",str(self.stopbits.value)) - SaveConfigFile.set(sectionName,"Serial.ByteSize",str(self.bytesize.value)) - SaveConfigFile.set(sectionName,"Serial.RtcCts",str(self.rtscts)) \ No newline at end of file + return f"Port : {self.port} \n BaudRate :{self.baudrate.value} \n Parity : {parity} \n StopBits : {self.stopbits.value} \n ByteSize : {self.bytesize.value} \n rtscts : {self.rtscts}" \ No newline at end of file diff --git a/src/StreamConfig/TcpSettings.py b/src/StreamSettings/TcpSettings.py similarity index 61% rename from src/StreamConfig/TcpSettings.py rename to src/StreamSettings/TcpSettings.py index e720803..b8b9632 100644 --- a/src/StreamConfig/TcpSettings.py +++ b/src/StreamSettings/TcpSettings.py @@ -1,21 +1,21 @@ # ############################################################################### -# +# # Copyright (c) 2024, Septentrio -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: -# +# # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. -# +# # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# +# # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. -# +# # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -27,31 +27,49 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import configparser -from enum import Enum import socket import logging +from enum import Enum +from ..constants import DEFAULTLOGFILELOGGER + + +class TCPSettingsException(Exception): + """ + Exception class for tcp settings + """ + def __init__(self, message, error_code = None): + super().__init__(message) + self.error_code = error_code class StreamMode(Enum): + """ Stream mode of the TCP port + """ CLIENT = "Client" SERVER = "Server" -class TcpSettings: +class TcpSettings: """ Represents the TCP settings for a stream. """ - def __init__(self , host : str = "localhost" , port : int =28784 , streamMode : StreamMode = StreamMode.CLIENT , logFile : logging = None) -> None: - self.host : str = host - self.port : int = port - self.StreamMode : StreamMode = streamMode - if self.StreamMode == StreamMode.SERVER : - self.host = '' - self.logFile : logging = logFile - + def __init__(self , host : str = "localhost" , port : int =28784 , + stream_mode : StreamMode = StreamMode.CLIENT , + debug_logging : bool =False) -> None: + + self.host : str = host + self.port : int = port + self.stream_mode : StreamMode = stream_mode + if self.stream_mode == StreamMode.SERVER : + self.host = '' + + if debug_logging : + self.log_file : logging.Logger = DEFAULTLOGFILELOGGER + else : + self.log_file = None # type: ignore + def connect(self) -> socket.socket: @@ -64,84 +82,72 @@ def connect(self) -> socket.socket: Raises: socket.error: If there is an error while connecting. """ - try : + try : newsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) newsocket.settimeout(5) except socket.error as e : - if self.logFile is not None : - self.logFile.error("Failed to start socket : %s" , e) - raise e - match self.StreamMode : + if self.log_file is not None : + self.log_file.error("Failed to start socket : %s" , e) + raise TCPSettingsException(e) from e + match self.stream_mode : case StreamMode.CLIENT : - try : + try : newsocket.connect((self.host, self.port)) return newsocket except socket.error as e : - if self.logFile is not None : - self.logFile.error("Failed to open the Client socket ( host : %s and port : %s) : %s",self.host , self.port , e) - raise e + if self.log_file is not None : + self.log_file.error("Failed to open the Client socket ( host : %s and port : %s) : %s",self.host , self.port , e) + raise TCPSettingsException(e) from e case StreamMode.SERVER : - try : + try : newsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) newsocket.bind(('', self.port)) return newsocket except socket.error as e : - if self.logFile is not None : - self.logFile.error("Failed to open the Server socket : %s" , e) - raise e + if self.log_file is not None : + self.log_file.error("Failed to open the Server socket : %s" , e) + raise TCPSettingsException(e) from e - def setHost(self, NewHost : str): + def set_host(self, new_host : str): """ set the Host name of the TCP Stream Args: - NewHost (str): the new Host name + new_host (str): the new Host name """ - self.host = NewHost + self.host = new_host - def setPort(self, NewPort : int): + def set_port(self, new_port : int): """ set the Port of the TCP Stream Args: - NewPort (int): The new Port + new_port (int): The new Port """ - self.port = NewPort + self.port = new_port - def set_StreamMode(self, NewMode : StreamMode): + def set_stream_mode(self, new_mode : StreamMode): """ set the stream mode of the tcp stream. Args: - NewMode (StreamMode): The new stream mode. + new_mode (StreamMode): The new stream mode. """ - self.StreamMode = NewMode - - def isServer(self): + self.stream_mode = new_mode + + def is_server(self): """ Return current stream mode of the stream Returns: Bool : True if the current Stream is in Server mode , False otherwise """ - return True if self.StreamMode == StreamMode.SERVER else False - - def toString(self) -> str : + return True if self.stream_mode == StreamMode.SERVER else False + + def to_string(self) -> str : """ Return current class as a string Returns: str: class as string """ - return f" Host : {self.host} \n Port :{self.port} \n StreamMode : {self.StreamMode.value}" - - - def SaveConfig(self , sectionName : str,SaveConfigFile : configparser.ConfigParser): - """ - Add current class values in the configFile - Args: - sectionName (str): name of the current section - SaveConfigFile (configparser.ConfigParser): configparser of the configuration file - """ - SaveConfigFile.set(sectionName,"hostName",self.host) - SaveConfigFile.set(sectionName,"portNumber",str(self.port)) - SaveConfigFile.set(sectionName,"TCPserver",str(self.isServer())) \ No newline at end of file + return f" Host : {self.host} \n Port :{self.port} \n StreamMode : {self.stream_mode.value}" \ No newline at end of file diff --git a/src/StreamConfig/UdpSettings.py b/src/StreamSettings/UdpSettings.py similarity index 65% rename from src/StreamConfig/UdpSettings.py rename to src/StreamSettings/UdpSettings.py index 7c08d78..4d2d60e 100644 --- a/src/StreamConfig/UdpSettings.py +++ b/src/StreamSettings/UdpSettings.py @@ -1,21 +1,21 @@ # ############################################################################### -# +# # Copyright (c) 2024, Septentrio -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: -# +# # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. -# +# # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# +# # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. -# +# # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -27,10 +27,18 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import configparser -from enum import Enum import socket import logging +from enum import Enum +from ..constants import DEFAULTLOGFILELOGGER + +class UDPSettingsException(Exception): + """ + Exception class for udp settings + """ + def __init__(self, message, error_code = None): + super().__init__(message) + self.error_code = error_code class DataFlow(Enum): """ @@ -38,11 +46,10 @@ class DataFlow(Enum): """ OnlyTransmit = 0 OnlyListen = 1 - Both = 2 - + BOTH = 2 -class UdpSettings: +class UdpSettings: """ Represents the UDP settings for a Stream. @@ -51,10 +58,10 @@ class UdpSettings: port (int): The port number to connect to. Default is 28784. DataFlow (DataFlow): The data flow mode. Default is DataFlow.Both. socket (socket): The socket object used for the Stream. - specificHost (str): The specific host IP address to bind to. + specific_host (str): The specific host IP address to bind to. """ - def __init__(self , host : str = "localhost", port : int = 28784 , dataflow : DataFlow = DataFlow.Both , specificHost : bool = False , logFile : logging =None) -> None: + def __init__(self , host : str = "localhost", port : int = 28784 , dataflow : DataFlow = DataFlow.BOTH , specific_host : bool = False , debug_logging : bool =None) -> None: """ Initializes a new instance of the UDPSettings class. @@ -64,10 +71,12 @@ def __init__(self , host : str = "localhost", port : int = 28784 , dataflow : D """ self.host : str = host self.port : int = port - self.DataFlow : DataFlow = dataflow - self.specificHost : bool = specificHost - self.logFile : logging = logFile - + self.dataflow : DataFlow = dataflow + self.specific_host : bool = specific_host + if debug_logging : + self.log_file : logging.Logger = DEFAULTLOGFILELOGGER + else : + self.log_file = None def connect(self) -> socket.socket: @@ -80,68 +89,57 @@ def connect(self) -> socket.socket: Raises: socket.error: If there is an error while connecting. """ - try : + try : newsocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) newsocket.settimeout(5) except socket.error as e : - if self.logFile is not None : - self.logFile.error("Failed to create socket : %s" , e) - raise e - try : - if self.specificHost is False: + if self.log_file is not None : + self.log_file.error("Failed to create socket : %s" , e) + raise UDPSettingsException(e) from e + try : + if self.specific_host is False: newsocket.bind(('', self.port)) return newsocket except socket.error as e : - if self.logFile is not None : - self.logFile.error("Failed to create UDP server : %s" ,e) - raise e + if self.log_file is not None : + self.log_file.error("Failed to create UDP server : %s" ,e) + raise UDPSettingsException(e) from e - def setHost(self, NewHost : str): + def set_host(self, new_host : str): """ Sets the host IP address. Args: - NewHost (str): The new host IP address. + new_host (str): The new host IP address. """ - self.host = NewHost + self.host = new_host - def setPort(self, NewPort : int): + def set_port(self, new_port : int): """ Sets the port number. Args: - NewPort (int): The new port number. + new_port (int): The new port number. """ - self.port = NewPort - - def set_DataFlow(self, NewDataFlow : DataFlow): + self.port = new_port + + def set_dataflow(self, new_dataflow : DataFlow): """ Sets the Data flow. Args: - NewDataFlow (DataFlow): The new Data Flow. - """ - self.DataFlow = NewDataFlow - - - def toString(self) -> str : + new_Dataflow (DataFlow): The new Data Flow. + """ + self.dataflow = new_dataflow + + def to_string(self) -> str : """ Return current class as a string Returns: str: class as string """ - return f" Host : {self.host} \n Port : {self.port} \n SpecificHost : {self.specificHost} \n DataFlow : {self.DataFlow.name}\n" + return f" Host : {self.host} \n Port : {self.port} \n SpecificHost : {self.specific_host} \n DataFlow : {self.dataflow.name}\n" - def SaveConfig(self , sectionName : str,SaveConfigFile : configparser.ConfigParser): - """ - Add current class values in the configFile - Args: - sectionName (str): name of the current section - SaveConfigFile (configparser.ConfigParser): configparser of the configuration file - """ - SaveConfigFile.set(sectionName,"hostNameUDP", self.host ) - SaveConfigFile.set(sectionName,"portNumberUDP",str(self.port)) - SaveConfigFile.set(sectionName,"specificIpUDP",str(self.specificHost)) - SaveConfigFile.set(sectionName,"dataDirUDP",str(self.DataFlow.value)) \ No newline at end of file + \ No newline at end of file diff --git a/src/StreamSettings/__init__.py b/src/StreamSettings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/UserInterfaces/CommandLineInterface.py b/src/UserInterfaces/CommandLineInterface.py index 7242644..f5dbb8f 100644 --- a/src/UserInterfaces/CommandLineInterface.py +++ b/src/UserInterfaces/CommandLineInterface.py @@ -30,53 +30,59 @@ import threading import time -from ..StreamConfig import * -class CommandLineInterface: - def __init__(self,streams : Streams , showdataId : int = None) -> None: - self.Streams = streams - self.showdataid = showdataId +from src.StreamConfig import App +from src.StreamConfig.Stream import Stream - def print(self, message): - print(message) +class CommandLineInterface: + def __init__(self,app : App , show_data_id : int = None) -> None: + self.app = app + self.show_data_id = show_data_id def run(self): - if len(self.Streams.StreamList) == 0 : + """Start the thread + """ + if len(self.app.stream_list) == 0 : return 0 - else : - if self.showdataid is not None: + else : + if self.show_data_id is not None: target = self._showDataTask + show_data_port = self.app.stream_list[self.show_data_id] else : target = self._showDataTransfert - self.showDataPort = self.Streams - - stopShowDataEvent = threading.Event() - stopShowDataEvent.clear() - showDataThread = threading.Thread(target=target , args=(stopShowDataEvent , self.showDataPort,)) - showDataThread.start() + show_data_port = self.app + + stop_show_data_event = threading.Event() + stop_show_data_event.clear() + show_data_thread = threading.Thread(target=target ,args=(stop_show_data_event , show_data_port,)) + show_data_thread.start() input("Press Enter to close the program \n") - stopShowDataEvent.set() - showDataThread.join() - self.Streams.CloseAll() - - - def _showDataTask(self,stopShowDataEvent, selectedPort : PortConfig): - while stopShowDataEvent.is_set() is False: - if selectedPort.DataToShow.empty() is False : - print(selectedPort.DataToShow.get()) + stop_show_data_event.set() + show_data_thread.join() + self.app.close_all() + + + def _showDataTask(self,stop_show_data_event, selected_port : Stream): + """Show all the data of a specific stream + """ + while stop_show_data_event.is_set() is False: + if selected_port.data_to_show.empty() is False : + print(selected_port.data_to_show.get()) return 0 - - def _showDataTransfert(self,stopShowDataEvent, stream : Streams): + + def _showDataTransfert(self,stop_show_data_event, app : App): + """Show the data transfert rate on all the configured stream + """ speed = "\r" - while stopShowDataEvent.is_set() is False: + while stop_show_data_event.is_set() is False: time.sleep(1) speed = "\r" - for port in stream.StreamList: - speed += "Port "+ str(port.id) +" : in "+ str(port.dataTransferInput) + " kBps ; out "+ str(port.dataTransferOutput) + " kBps " - print(speed, end="\r") + for port in app.stream_list: + speed += "Port "+ str(port.stream_id) +" : in "+ str(port.data_transfer_input) + " kBps ; out "+ str(port.data_transfer_output) + " kBps " + print(speed, end="\r") return 0 - - + + diff --git a/src/UserInterfaces/GraphicalUserInterface.py b/src/UserInterfaces/GraphicalUserInterface.py index 84df840..6417143 100644 --- a/src/UserInterfaces/GraphicalUserInterface.py +++ b/src/UserInterfaces/GraphicalUserInterface.py @@ -1,953 +1,1228 @@ -import threading -from ..StreamConfig import * -from ..constants import * +# ############################################################################### +# +# Copyright (c) 2024, Septentrio +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import math import copy -import PySide6.QtAsyncio as QtAsyncio -from PySide6.QtCore import QSize, Qt , QRegularExpression , QThread , Signal -from PySide6.QtGui import QIntValidator , QRegularExpressionValidator,QTextCursor,QAction,QIcon -from PySide6.QtWidgets import (QMainWindow, QApplication, QCheckBox, QComboBox, - QCommandLinkButton, QDateTimeEdit, QDial, - QDialog, QDialogButtonBox, QFileSystemModel, - QGridLayout, QGroupBox, QHBoxLayout, QLabel, - QLineEdit, QListView, QMenu, QPlainTextEdit, - QProgressBar, QPushButton, QRadioButton, - QScrollBar, QSizePolicy, QSlider, QSpinBox, - QStyleFactory, QTableWidget, QTabWidget, - QTextBrowser, QTextEdit, QToolBox, QToolButton, - QTreeView, QVBoxLayout, QWidget, QInputDialog,QFileDialog) - - -def PairWidgets(widget1, widget2 ) -> QHBoxLayout: +from ..StreamConfig.Stream import * +from ..StreamConfig.App import * +from ..StreamSettings import SerialSettings ,TcpSettings , UdpSettings +from ..constants import * + +from PySide6.QtCore import QObject, Qt , QRegularExpression , QUrl, QThread , Signal +from PySide6.QtGui import QRegularExpressionValidator,QTextCursor,QAction,QIcon,QDesktopServices , QPixmap +from PySide6.QtWidgets import (QMainWindow, QApplication, QCheckBox, QComboBox, QFrame, + QDialog, QDialogButtonBox,QGridLayout, QGroupBox, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QRadioButton, QMessageBox, + QSpinBox,QTabWidget, QTextEdit,QVBoxLayout, QWidget,QFileDialog) + + +def pair_h_widgets( *widgets : QWidget ) -> QHBoxLayout: + """Add every widgets to a horizontal layout + Returns: + QHBoxLayout: _description_ + """ result = QHBoxLayout() - result.addWidget(widget1) - result.addWidget(widget2) + for widget in widgets : + result.addWidget(widget) result.setAlignment(Qt.AlignmentFlag.AlignTop) return result +def pair_v_widgets(*widgets : QWidget) ->QVBoxLayout: + """Add every widgets to a horizontal layout -def TrioWidgets(widget1,widget2,widget3) -> QHBoxLayout: + Returns: + QHBoxLayout: _description_ + """ result = QHBoxLayout() - result.addWidget(widget1) - result.addWidget(widget2) - result.addWidget(widget3) + for widget in widgets : + result.addWidget(widget) result.setAlignment(Qt.AlignmentFlag.AlignTop) return result - - - - class GraphicalUserInterface(QMainWindow): - - def __init__(self, streams : Streams) -> None: + """Graphical interface for datalink + """ + + def __init__(self, app : App) -> None: super().__init__() - self.setFixedSize(950,750) - self.setWindowIcon(QIcon(DATAFILESPATH + 'pyDatalink_icon.png')) - self.streams : Streams = streams - self.streamsWidget : list[ConnectionCard] = [] - self.setWindowTitle(APPNAME) - # Menu bar - menuBar = self.menuBar() + self.setWindowIcon(QIcon(os.path.join(DATAFILESPATH , 'pyDatalink_icon.png'))) + self.app : App = app + self.streams_widget : list[ConnectionCard] = [] + self.setWindowTitle(APPNAME) + + # Menu bar + menu_bar = self.menuBar() # Create a menu - fileMenu = menuBar.addMenu('File') - - # IDEA - TO DO - #confMenu = menuBar.addMenu('Config') + file_menu = menu_bar.addMenu('File') + + # Preferences action + preference_action = QAction("Preferences",self) + preference_action.setShortcut('Ctrl+P') + preference_action.triggered.connect(lambda p : self.open_preference_interface()) + + # exit action + exit_Action = QAction('Exit', self) + exit_Action.setShortcut('Ctrl+Q') + exit_Action.triggered.connect(lambda : self.close()) + + file_menu.addAction(preference_action) + file_menu.addSeparator() + file_menu.addAction(exit_Action) + + # IDEA - TO DO + #confMenu = menu_bar.addMenu('Config') #Save current Config #Load config #Change config - - # Create Preferences - preferenceAction = QAction("Preferences",self) - preferenceAction.setShortcut('Ctrl+P') - preferenceAction.triggered.connect(lambda p : self.openPreferenceInterface()) - - # Create exit action - exitAction = QAction('Exit', self) - exitAction.setShortcut('Ctrl+Q') - exitAction.triggered.connect(self.close) - - fileMenu.addAction(preferenceAction) - fileMenu.addSeparator() - fileMenu.addAction(exitAction) - - + + # github page + github_page_action = QAction(QIcon(os.path.join(DATAFILESPATH ,"Github_icon.png")),"GitHub Repository", self) + github_page_action.triggered.connect(lambda : self.open_link()) + + # About action + about_action = QAction(QIcon(os.path.join(DATAFILESPATH , 'pyDatalink_icon.png')),"About",self) + about_action.triggered.connect(lambda p : self.open_about_dialog()) + + #Help Menu + help_menu = menu_bar.addMenu("Help") + help_menu.addAction(github_page_action) + help_menu.addSeparator() + help_menu.addAction(about_action) + #Main Window layout - mainLayout = QGridLayout() - connectionCardLayout = QVBoxLayout() - connectionCardLineLayout = QHBoxLayout() - for i in range(self.streams.maxStream): - newCard = ConnectionCard(i,self.streams.StreamList[i],self.streams.maxStream) - self.streamsWidget.append(newCard) - connectionCardLineLayout.addWidget(newCard.getWidget()) - if connectionCardLineLayout.count() == 3 : - connectionCardLayout.addLayout(connectionCardLineLayout) - connectionCardLineLayout = QHBoxLayout() - - if connectionCardLineLayout.count() < 3 : - connectionCardLayout.addLayout(connectionCardLineLayout) - - mainLayout.addLayout(connectionCardLayout,0,0) + main_layout = QGridLayout() + connection_card_layout = QVBoxLayout() + connection_card_line_layout = QHBoxLayout() + number_card_per_line = math.ceil(self.app.max_stream / 2) + number_line = math.ceil(self.app.max_stream /number_card_per_line) + for i in range(self.app.max_stream): + new_card = ConnectionCard(i,self.app.stream_list[i],self.app.max_stream) + self.streams_widget.append(new_card) + connection_card_line_layout.addWidget(new_card.get_card_widget()) + if connection_card_line_layout.count() == number_card_per_line : + connection_card_layout.addLayout(connection_card_line_layout) + connection_card_line_layout = QHBoxLayout() + connection_card_line_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + + if connection_card_line_layout.count() < number_card_per_line : + connection_card_layout.addLayout(connection_card_line_layout) + window_height = (350 * number_line) + 50 + window_width = (300 * number_card_per_line) + 20 + self.setFixedSize(window_width,window_height) + + main_layout.addLayout(connection_card_layout,0,0) + main_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + main_layout.setAlignment(Qt.AlignmentFlag.AlignTop) widget = QWidget() - widget.setLayout(mainLayout) + widget.setLayout(main_layout) self.setCentralWidget(widget) - + self.timer_id = self.startTimer(30) - + def timerEvent(self, event): - for widget in self.streamsWidget: - widget.refreshCards() - - - def openPreferenceInterface(self): - configureDialog = PreferencesInterface(self.streams.preferences) - configureDialog.exec_() - + for widget in self.streams_widget: + widget.refresh_cards() + + def open_preference_interface(self): + """open the setting page to configure preferences + """ + configure_dialog = PreferencesInterface(self.app.preferences) + configure_dialog.exec_() + + def open_about_dialog(self): + """Open the about dialog + """ + about_dialog = AboutDialog() + about_dialog.exec_() + + def open_link(self): + """open github repository in a web browser + """ + url = QUrl("https://github.com/septentrio-gnss/Septentrio-PyDataLink") + if not QDesktopServices.openUrl(url): + QMessageBox.warning(self, "Error", "Couldn't open the web page") + def closeEvent(self,event): - self.streams.CloseAll() + self.app.close_all() QApplication.quit() -class ConnectionCard : - def __init__(self,id : int ,stream : PortConfig , maxStreams : int ) -> None: +class ConnectionCard : + """Widget of a stream in the main page + """ + def __init__(self,stream_id : int ,stream : Stream , max_streams : int ) -> None: self.stream = stream - self.id = id - self.maxStreams = maxStreams - self.connectionCardWidget = self.ConnectionCard() - - - - def ConnectionCard(self): - result = QGroupBox(f"Connection {self.id}") - + self.stream_id = stream_id + self.max_streams = max_streams + self.connection_card_widget = self.connection_card() + self.connect_thread = None + self.worker = None + self.show_data_dialog = None + self.previous_tab = 0 + + def connection_card(self): + """create the card widget + """ + result = QGroupBox(f"Connection {self.stream_id}") + result.setFixedSize(300,350) #Top button - self.connectButton = QPushButton("Connect") - - + self.connect_button = QPushButton("Connect") + #Add slot : connect the selected stream - self.configureButton = QPushButton("Configure") - #add slot : create a new window to configure stream - - topButtonLayout = QHBoxLayout() - topButtonLayout.addWidget(self.connectButton) - topButtonLayout.addWidget(self.configureButton) - + self.configure_button = QPushButton("Configure") + # overviewText - self.currentConfigOverview = QLabel() - self.currentConfigOverview.setText(f"Current configuration :\n {self.stream.toString()}") - self.currentConfigOverview.setAlignment(Qt.AlignmentFlag.AlignJustify) - self.currentConfigOverview.setFixedHeight(150) - - # Status + self.current_config_overview = QLabel() + self.current_config_overview.setText(f"Current configuration :\n {self.stream.to_string()}") + self.current_config_overview.setAlignment(Qt.AlignmentFlag.AlignJustify) + self.current_config_overview.setFixedHeight(150) + + # Status self.status= QLabel() - self.status.setAlignment(Qt.AlignHCenter) - - #Data Transfert - self.currentDataTransfert = QLabel() - self.currentDataTransfert.setText(f"ingoing : {self.stream.dataTransferInput} kBps | outgoing : {self.stream.dataTransferOutput} kBps") - self.currentDataTransfert.setAlignment(Qt.AlignmentFlag.AlignCenter) - + self.status.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.status.setFixedSize(280,30) + + #Data Transfert + self.current_data_transfert = QLabel() + self.current_data_transfert.setText(f"In: {self.stream.data_transfer_input} kBps | Out: {self.stream.data_transfer_output} kBps") + self.current_data_transfert.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.current_data_transfert.setStyleSheet("QWidget { border: 2px solid grey; }") + self.current_data_transfert.setFixedSize(160,24) + #showdata - self.showDataButton = QPushButton("Show Data") + self.show_data_button = QPushButton("Show Data") + data_layout = pair_h_widgets(self.show_data_button , self.current_data_transfert) + button_layout = pair_h_widgets(self.connect_button , self.configure_button) + top_layout = QVBoxLayout() + top_layout.addLayout(button_layout) + top_layout.addLayout(data_layout) + top_layout.addWidget(self.status) + + #Separator + separator : list[QFrame] = [] + for _ in range(2) : + new_separator = QFrame() + new_separator.setFrameShape(QFrame.HLine) + new_separator.setFrameShadow(QFrame.Sunken) + new_separator.setLineWidth(1) + separator.append(new_separator) # Final Layout - cardLayout = QVBoxLayout(result) - cardLayout.addLayout(topButtonLayout) - cardLayout.addWidget(self.currentConfigOverview) - cardLayout.addWidget(self.status) - cardLayout.addWidget(self.currentDataTransfert) - cardLayout.addLayout(self.linkLayout()) - cardLayout.addWidget(self.showDataButton) - - #Init in case of Startup Connect - if self.stream.isConnected(): - self.connectButton.setText("Disonnect") - self.configureButton.setDisabled(True) - self.status.setText("Connected") + card_layout = QVBoxLayout(result) + card_layout.addLayout(top_layout) + card_layout.addWidget(separator[0]) + card_layout.addWidget(self.current_config_overview) + card_layout.addWidget(separator[1]) + card_layout.addWidget(self.link_layout()) + + #Init in case of Startup connect + if self.stream.is_connected(): + self.connect_button.setText("Disonnect") + self.configure_button.setDisabled(True) + self.status.setText("CONNECTED") + self.status.setStyleSheet("QLabel {color : #32a852; font-weight: bold;}") + else : + self.current_data_transfert.setStyleSheet("QWidget { border: 2px solid grey; }") + if self.stream.startup_error != "": + self.status.setText("ERROR ON STARTUP CONNECT !") + self.status.setStyleSheet("QLabel { color: #c42323;} QToolTip {font-weight: bold;}") + self.status.setToolTip(self.stream.startup_error) # SIGNALS - - self.configureButton.pressed.connect(lambda : self.openConfigureInterface(self.stream)) - self.connectButton.pressed.connect(lambda : self.connectStream()) - self.showDataButton.pressed.connect(lambda : self.showData()) - + + self.configure_button.pressed.connect(lambda : self.open_configure_interface(self.stream)) + self.connect_button.pressed.connect(self.connect_stream) + self.show_data_button.pressed.connect(self.show_data) return result - - def linkLayout(self): - linkLayout = QHBoxLayout() - linkLayout.addWidget(QLabel("Links : ")) - for a in range(self.maxStreams): - newCheckBox = QCheckBox(str(a)) - if self.id == a : - newCheckBox.setDisabled(True) - if a in self.stream.linkedPort : - newCheckBox.setChecked(True) - newCheckBox.stateChanged.connect(lambda state,x=a : self.toggleLinkedPort(x)) - - linkLayout.addWidget(newCheckBox) - return linkLayout - def toggleLinkedPort(self , linkindex): - self.stream.UpdatelinkedPort(linkindex) - - def openConfigureInterface(self,stream : PortConfig = None): - configureDialog = ConfigureInterface(stream) - configureDialog.accepted.connect(lambda : self.refreshCards()) - configureDialog.exec_() - - def refreshCards(self): - self.currentConfigOverview.setText(f"Current configuration :\n {self.stream.toString()}") - self.currentDataTransfert.setText(f"ingoing : {self.stream.dataTransferInput} kBps | outgoing : {self.stream.dataTransferOutput} kBps") - if not self.stream.connected and self.connectButton.text() =="Disconnect" : - self.connectButton.setText("Connect") - self.configureButton.setDisabled(False) + + def link_layout(self): + """create the link check box for a connection card + """ + link_widget = QWidget() + + link_layout = QHBoxLayout(link_widget) + link_layout.addWidget(QLabel("Links : ")) + for a in range(self.max_streams): + new_check_box = QCheckBox(str(a)) + if self.stream_id == a : + new_check_box.setDisabled(True) + if a in self.stream.linked_ports : + new_check_box.setChecked(True) + new_check_box.stateChanged.connect(lambda state,x=a : self.toggle_linked_port(x)) + + link_layout.addWidget(new_check_box) + return link_widget + + def toggle_linked_port(self , linkindex): + """update linked ports of stream when checkbox is check or uncheck + """ + self.stream.update_linked_ports(linkindex) + + def open_configure_interface(self,stream : Stream): + """Open configuration dialog to configure current stream + """ + configure_dialog = ConfigureInterface(stream , self.previous_tab) + configure_dialog.accepted.connect(self.refresh_cards) + configure_dialog.save_previous_tab.connect(self.save_previous_tab) + configure_dialog.save_config.connect(self.save_config) + configure_dialog.exec_() + + def save_previous_tab(self, tab): + self.previous_tab = tab + def save_config(self,stream): + self.stream = stream + + def refresh_cards(self): + """Refresh sumarry value when a setting has changed + """ + self.current_config_overview.setText(f"Current configuration : {self.stream.stream_type.name}\n{self.stream.to_string()}") + self.current_data_transfert.setText(f"In: {self.stream.data_transfer_input} kBps | Out: {self.stream.data_transfer_output} kBps") + if not self.stream.connected and self.connect_button.text() =="Disconnect" : + self.connect_button.setText("Connect") + self.configure_button.setDisabled(False) self.status.setText("") - - - def connectStream(self): - if not self.stream.connected: - try : - self.stream.Connect() - self.configureButton.setDisabled(True) - self.status.setText("Connected") - self.connectButton.setText("Disconnect") - except Exception as e : - self.status.setText("Couldn't connect") - if len(e.args) > 2 : - self.status.setToolTip(str(e.args[1])) - else: - self.status.setToolTip(str(e.args[0])) - else : - try : - self.stream.Disconnect() - self.configureButton.setDisabled(False) - self.status.setText("") - self.connectButton.setText("Connect") - except Exception as e : - self.status.setText(f"Couldn't Disconnect") - self.status.setToolTip(str(e.args[1])) - - def showData(self): - if self.stream.ShowInputData.is_set() or self.stream.ShowOutputData.is_set(): - self.showDataButton.setText("Show Data") - self.showdatadialog.closeDialog() + + def connect_stream(self): + """Connect or disconnect the stream + """ + self.connect_button.setDisabled(True) + self.connect_thread = QThread() + self.worker = StreamConnectWorker(self) + self.worker.moveToThread(self.connect_thread) + self.connect_thread.started.connect(self.worker.run) + self.worker.finished.connect(self.connect_thread.quit) + self.worker.finished.connect(self.worker.deleteLater) + self.connect_thread.finished.connect(self.connect_thread.deleteLater) + self.connect_thread.finished.connect(self.cleanup) + self.worker.finished.connect(lambda : self.connect_button.setDisabled(False) ) + + self.connect_thread.start() + + def cleanup(self): + """Reset thread and worker values + """ + self.connect_thread = None + self.worker = None + + def show_data(self): + """open the dialog to visualize the incoming or outgoing data + """ + if self.stream.show_incoming_data.is_set() or self.stream.show_outgoing_data.is_set(): + self.show_data_button.setText("Show Data") + self.show_data_dialog.close_dialog() + self.show_data_dialog = None else: - self.showDataButton.setText("Hide Data") - self.showdatadialog = ShowDataInterface(self.stream) - self.showdatadialog.finished.connect(lambda : self.showDataButton.setText("Show Data")) - self.showdatadialog.show() - - - def getWidget(self): - return self.connectionCardWidget - - + self.show_data_dialog = ShowDataInterface(self.stream) + self.show_data_dialog.finished.connect(lambda : self.show_data_button.setText("Show Data")) + self.show_data_button.setText("Hide Data") + self.show_data_dialog.show() + + def get_card_widget(self): + """return a new connection card widget + """ + return self.connection_card_widget + class ConfigureInterface(QDialog) : - def __init__(self , stream : PortConfig = None) -> None: + """Configuration dialog + """ + save_previous_tab = Signal(int) + save_config = Signal(Stream) + def __init__(self , stream : Stream , previous_tab : int = 0 ) -> None: super().__init__() - self.setWindowTitle(f"Configure Connection {stream.id}") - self.setWindowIcon(QIcon(DATAFILESPATH + 'pyDatalink_icon.png')) + self.setWindowTitle(f"Configure Connection {stream.stream_id}") + self.setWindowIcon(QIcon(os.path.join(DATAFILESPATH , 'pyDatalink_icon.png'))) self.stream = stream - self.streamsave = copy.copy(stream) + self.stream_save = copy.copy(stream) self.setModal(True) self.setFixedSize(350,580) - configureLayout = QVBoxLayout(self) - - serialMenu = self.serialMenu() - tcpMenu = self.tcpMenu() - udpMenu = self.udpMenu() - ntripMenu = self.ntripMenu() - - configTabs = QTabWidget() - configTabs.addTab(self.generalMenu(), "General") - index = configTabs.addTab(serialMenu , "Serial") - configTabs.addTab(tcpMenu, "TCP") - configTabs.addTab(udpMenu, "UDP") - configTabs.addTab(ntripMenu, "NTRIP") - - if len(self.stream.serialSettings.GetAvailablePort()) == 0 : - configTabs.setTabEnabled(index,False) + configure_layout = QVBoxLayout(self) + self.previous_tab = previous_tab + self.update_thread = None + self.worker = None + + serial_menu = self.serial_menu() + tcp_menu = self.tcp_menu() + udp_menu = self.udp_menu() + ntrip_menu = self.ntrip_menu() - configureLayout.addWidget(configTabs) - configureLayout.addLayout(self.bottomButtonLayout()) - - def generalMenu(self): - result = QGroupBox(f"General Settings") + self.config_tabs = QTabWidget() + self.config_tabs.addTab(self.general_menu(), "General") + index = self.config_tabs.addTab(serial_menu , "Serial") + self.config_tabs.addTab(tcp_menu, "TCP") + self.config_tabs.addTab(udp_menu, "UDP") + self.config_tabs.addTab(ntrip_menu, "NTRIP") + + if len(self.stream.serial_settings.get_available_port()) == 0 : + self.config_tabs.setTabEnabled(index,False) + if previous_tab == index : + previous_tab = 0 + self.config_tabs.setCurrentIndex(int(previous_tab)) + + configure_layout.addWidget(self.config_tabs) + configure_layout.addLayout(self.bottom_button_layout()) + + def general_menu(self): + """General menu tab + """ + result = QGroupBox("General Settings") + # Select Connection - streamTypeBox = QGroupBox("Select the connection type") - streamTypeBoxLayout = QHBoxLayout(streamTypeBox) - streamTypeList = QComboBox() + stream_type_box = QGroupBox("Select the connection type") + stream_type_box_layout = QHBoxLayout(stream_type_box) + stream_type_list = QComboBox() for type in StreamType : - streamTypeList.addItem(type.name, type) - if self.stream.serialSettings.baudrate is not None : - index = streamTypeList.findData(self.stream.StreamType) - else : - index = streamTypeList.findData(StreamType.NONE) - streamTypeList.setCurrentIndex(index) - streamTypeBoxLayout.addWidget(streamTypeList) - streamTypeBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - # Connect/Close Script - scriptBox = QGroupBox("Scripts") - scriptBoxLayout = QVBoxLayout(scriptBox) - - openScriptCheckBox = QCheckBox() - openScriptCheckBox.setChecked(self.stream.sendStartupScript) - openScriptLabel = QLabel("Connect Script : ") - openScriptEdit = QLineEdit() - openScriptEdit.setDisabled(not self.stream.sendStartupScript) - openScriptEdit.setText(self.stream.startupScript) - scriptBoxLayout.addLayout(TrioWidgets(openScriptCheckBox,openScriptLabel,openScriptEdit)) - - closeScriptCheckBox = QCheckBox() - closeScriptCheckBox.setChecked(self.stream.sendCloseScript) - closeScriptLabel = QLabel("Close Script : ") - closeScriptEdit = QLineEdit() - closeScriptEdit.setDisabled(not self.stream.sendCloseScript) - closeScriptEdit.setText(self.stream.closeScript) - scriptBoxLayout.addLayout(TrioWidgets(closeScriptCheckBox,closeScriptLabel,closeScriptEdit)) - - # Logging file - logBox = QGroupBox("Logging") - logBoxLayout = QVBoxLayout(logBox) - - logCheckBox = QCheckBox() - closeScriptCheckBox.setChecked(self.stream.logging) - logLabel = QLabel("Log File : ") - logEdit = QLineEdit() - logEdit.setDisabled(not self.stream.logging) - logEdit.setText(self.stream.loggingFile) - logBoxLayout.addLayout(TrioWidgets(logCheckBox,logLabel,logEdit)) - - # Final Layout - resultLayout = QVBoxLayout(result) - resultLayout.addWidget(streamTypeBox) - resultLayout.addWidget(scriptBox) - resultLayout.addWidget(logBox) - resultLayout.setAlignment(Qt.AlignmentFlag.AlignTop) + stream_type_list.addItem(type.name, type) + if self.stream.serial_settings.baudrate is not None : + index = stream_type_list.findData(self.stream.stream_type) + else : + index = stream_type_list.findData(StreamType.NONE) + stream_type_list.setCurrentIndex(index) + stream_type_box_layout.addWidget(stream_type_list) + stream_type_box_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + # connect/Close Script + script_box = QGroupBox("Scripts") + script_box_layout = QVBoxLayout(script_box) + + open_script_check_box = QCheckBox() + open_script_check_box.setChecked(self.stream.send_startup_script) + open_script_label = QLabel("connect Script : ") + open_script_edit = QLineEdit() + open_script_edit.setDisabled(not self.stream.send_startup_script) + open_script_edit.setText(self.stream.startup_script) + script_box_layout.addLayout(pair_h_widgets(open_script_check_box,open_script_label,open_script_edit)) + + close_script_check_box = QCheckBox() + close_script_check_box.setChecked(self.stream.send_close_script) + close_script_label = QLabel("Close Script : ") + close_script_edit = QLineEdit() + close_script_edit.setDisabled(not self.stream.send_close_script) + close_script_edit.setText(self.stream.close_script) + script_box_layout.addLayout(pair_h_widgets(close_script_check_box,close_script_label,close_script_edit)) + + # Logging file + log_box = QGroupBox("Logging") + log_box_layout = QVBoxLayout(log_box) + + log_check_box = QCheckBox() + log_check_box.setChecked(self.stream.logging) + log_label = QLabel("Log File : ") + log_edit = QLineEdit() + log_edit.setDisabled(not self.stream.logging) + log_edit.setText(self.stream.logging_file) + log_box_layout.addLayout(pair_h_widgets(log_check_box,log_label,log_edit)) + + # Final Layout + result_layout = QVBoxLayout(result) + result_layout.addWidget(stream_type_box) + result_layout.addWidget(script_box) + result_layout.addWidget(log_box) + result_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # SIGNALS - - streamTypeList.currentIndexChanged.connect(lambda : self.stream.setStreamType(streamTypeList.currentData())) - openScriptCheckBox.checkStateChanged.connect(lambda : self.openScriptFile(openScriptEdit,openScriptCheckBox , True)) - closeScriptCheckBox.checkStateChanged.connect(lambda : self.openScriptFile(closeScriptEdit,closeScriptCheckBox , False)) - logCheckBox.checkStateChanged.connect(lambda :self) - + stream_type_list.currentIndexChanged.connect(lambda : self.stream.set_stream_type(stream_type_list.currentData())) + open_script_check_box.checkStateChanged.connect(lambda : self.open_script_file(open_script_edit,open_script_check_box , True)) + close_script_check_box.checkStateChanged.connect(lambda : self.open_script_file(close_script_edit,close_script_check_box , False)) + log_check_box.checkStateChanged.connect(lambda : self.select_log_file(log_edit ,log_check_box )) return result - - - def serialMenu(self): - result = QGroupBox(f"Serial connection") - + + def serial_menu(self): + """Serial tab + """ + result = QGroupBox("Serial connection") #Available Port - availablePortList = QComboBox() - ports = self.stream.serialSettings.GetAvailablePort() - if len(ports) > 0 : - for port in ports : - availablePortList.addItem(port[0].replace("/dev/ttyACM","COM") + " - " + port[1].split("-")[0],port[0]) - availablePortList.setCurrentIndex(-1) - else : - availablePortList.addItem("no com port detected") - availablePortList.setDisabled(True) - if self.stream.serialSettings.port != "": - index = availablePortList.findData(self.stream.serialSettings.port) - availablePortList.setCurrentIndex(index) - availablePortLabel = QLabel("Port COM:") - availablePortLabel.setBuddy(availablePortList) - + available_port_list = QComboBox() + ports = self.stream.serial_settings.get_available_port() + if len(ports) > 0 : + for port in ports : + available_port_list.addItem(port[0].replace("/dev/ttyACM","COM") + " - " + port[1].split("-")[0],port[0]) + available_port_list.setCurrentIndex(-1) + else : + available_port_list.addItem("no com port detected") + available_port_list.setDisabled(True) + if self.stream.serial_settings.port != "": + index = available_port_list.findData(self.stream.serial_settings.port) + available_port_list.setCurrentIndex(index) + available_port_label = QLabel("Port COM:") + available_port_label.setBuddy(available_port_list) + #Baudrate - baudRateList = QComboBox() - for baudrate in BaudRate : - baudRateList.addItem(baudrate.value, baudrate) - if self.stream.serialSettings.baudrate is not None : - index = baudRateList.findData(self.stream.serialSettings.baudrate) - baudRateList.setCurrentIndex(index) - baudRateListLabel = QLabel("Baudrate :") - baudRateListLabel.setBuddy(baudRateList) - + baudrate_list = QComboBox() + for baudrate in SerialSettings.BaudRate : + baudrate_list.addItem(baudrate.value, baudrate) + if self.stream.serial_settings.baudrate is not None : + index = baudrate_list.findData(self.stream.serial_settings.baudrate) + baudrate_list.setCurrentIndex(index) + baudrate_list_label = QLabel("Baudrate :") + baudrate_list_label.setBuddy(baudrate_list) + #Parity - parityList = QComboBox() - for parity in Parity : - parityList.addItem( parity.value + " - " + parity.name.replace("PARITY_","") , parity ) - if self.stream.serialSettings.parity is not None : - index = parityList.findData(self.stream.serialSettings.parity) - parityList.setCurrentIndex(index) - parityListLabel = QLabel("Parity :") - parityListLabel.setBuddy(parityList) - + parity_list = QComboBox() + for parity in SerialSettings.Parity : + parity_list.addItem( parity.value + " - " + parity.name.replace("PARITY_","") , parity ) + if self.stream.serial_settings.parity is not None : + index = parity_list.findData(self.stream.serial_settings.parity) + parity_list.setCurrentIndex(index) + parity_list_label = QLabel("Parity :") + parity_list_label.setBuddy(parity_list) + #Stop bits - stopBitsList = QComboBox() - for stopbits in StopBits : - stopBitsList.addItem(str(stopbits.value),stopbits) - if self.stream.serialSettings.stopbits is not None : - index = stopBitsList.findData(self.stream.serialSettings.stopbits) - stopBitsList.setCurrentIndex(index) - stopBitsListLabel = QLabel("StopBits :") - stopBitsListLabel.setBuddy(stopBitsList) - + stop_bits_list = QComboBox() + for stopbits in SerialSettings.StopBits : + stop_bits_list.addItem(str(stopbits.value),stopbits) + if self.stream.serial_settings.stopbits is not None : + index = stop_bits_list.findData(self.stream.serial_settings.stopbits) + stop_bits_list.setCurrentIndex(index) + stop_bits_list_label = QLabel("StopBits :") + stop_bits_list_label.setBuddy(stop_bits_list) + #ByteSize - byteSizeList = QComboBox() - for bytesize in ByteSize : - byteSizeList.addItem(str(bytesize.value) , bytesize) - index = byteSizeList.findData(self.stream.serialSettings.bytesize) - byteSizeList.setCurrentIndex(index) - byteSizeListLabel = QLabel("Bytesize :") - byteSizeListLabel.setBuddy(byteSizeList) - + byte_size_list = QComboBox() + for bytesize in SerialSettings.ByteSize : + byte_size_list.addItem(str(bytesize.value) , bytesize) + index = byte_size_list.findData(self.stream.serial_settings.bytesize) + byte_size_list.setCurrentIndex(index) + byte_size_list_label = QLabel("Bytesize :") + byte_size_list_label.setBuddy(byte_size_list) + #RTscts - rtsctsList = QComboBox() - rtsctsList.addItem("True", True) - rtsctsList.addItem("False", False) - index = rtsctsList.findData(self.stream.serialSettings.rtscts) - rtsctsList.setCurrentIndex(index) - rtsctsListLabel = QLabel("Rts-Cts :") - rtsctsListLabel.setBuddy(rtsctsList) - - + rtscts_list = QComboBox() + rtscts_list.addItem("True", True) + rtscts_list.addItem("False", False) + index = rtscts_list.findData(self.stream.serial_settings.rtscts) + rtscts_list.setCurrentIndex(index) + rtscts_list_label = QLabel("Rts-Cts :") + rtscts_list_label.setBuddy(rtscts_list) + # Final Layout - widgetLayout = QVBoxLayout(result) - widgetLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - widgetLayout.addLayout(PairWidgets(availablePortLabel , availablePortList)) - widgetLayout.addLayout(PairWidgets(baudRateListLabel,baudRateList)) - widgetLayout.addLayout(PairWidgets(parityListLabel,parityList)) - widgetLayout.addLayout(PairWidgets(stopBitsListLabel,stopBitsList)) - widgetLayout.addLayout(PairWidgets(byteSizeListLabel ,byteSizeList )) - widgetLayout.addLayout(PairWidgets(rtsctsListLabel,rtsctsList)) - + widget_layout = QVBoxLayout(result) + widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + widget_layout.addLayout(pair_h_widgets(available_port_label , available_port_list)) + widget_layout.addLayout(pair_h_widgets(baudrate_list_label,baudrate_list)) + widget_layout.addLayout(pair_h_widgets(parity_list_label,parity_list)) + widget_layout.addLayout(pair_h_widgets(stop_bits_list_label,stop_bits_list)) + widget_layout.addLayout(pair_h_widgets(byte_size_list_label ,byte_size_list )) + widget_layout.addLayout(pair_h_widgets(rtscts_list_label,rtscts_list)) + # SIGNAL - availablePortList.currentIndexChanged.connect(lambda : self.stream.serialSettings.setPort(availablePortList.currentData())) - parityList.currentIndexChanged.connect(lambda : self.stream.serialSettings.set_parity(parityList.currentData())) - baudRateList.currentIndexChanged.connect(lambda : self.stream.serialSettings.set_baudrate(baudRateList.currentData())) - stopBitsList.currentIndexChanged.connect(lambda : self.stream.serialSettings.set_stopbits(stopBitsList.currentData())) - byteSizeList.currentIndexChanged.connect(lambda : self.stream.serialSettings.set_bytesize(byteSizeList.currentData())) - rtsctsList.currentIndexChanged.connect(lambda : self.stream.serialSettings.set_rtscts(rtsctsList.currentData())) + available_port_list.currentIndexChanged.connect(lambda : self.stream.serial_settings.set_port(available_port_list.currentData())) + parity_list.currentIndexChanged.connect(lambda : self.stream.serial_settings.set_parity(parity_list.currentData())) + baudrate_list.currentIndexChanged.connect(lambda : self.stream.serial_settings.set_baudrate(baudrate_list.currentData())) + stop_bits_list.currentIndexChanged.connect(lambda : self.stream.serial_settings.set_stopbits(stop_bits_list.currentData())) + byte_size_list.currentIndexChanged.connect(lambda : self.stream.serial_settings.set_bytesize(byte_size_list.currentData())) + rtscts_list.currentIndexChanged.connect(lambda : self.stream.serial_settings.set_rtscts(rtscts_list.currentData())) return result - - def tcpMenu(self): - - result = QWidget() - resultLayout = QVBoxLayout(result) - + + def tcp_menu(self): + """TCP config tab + """ + result = QWidget() + result_layout = QVBoxLayout(result) + # Connection mode Box - connectionModeBox = QGroupBox("Connection Mode") - clientMode = QRadioButton("TCP/IP Client") - serverMode = QRadioButton("TCP/IP Server") - if self.stream.tcpSettings.StreamMode == StreamMode.CLIENT: - clientMode.setChecked(True) + connection_mode_box = QGroupBox("Connection Mode") + client_mode = QRadioButton("TCP/IP Client") + server_mode = QRadioButton("TCP/IP Server") + if self.stream.tcp_settings.stream_mode == StreamMode.CLIENT: + client_mode.setChecked(True) else: - serverMode.setChecked(True) - - connectionModeLayout = QHBoxLayout(connectionModeBox) - connectionModeLayout.addWidget(clientMode) - connectionModeLayout.addWidget(serverMode) - connectionModeLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - + server_mode.setChecked(True) + + connection_mode_layout = QHBoxLayout(connection_mode_box) + connection_mode_layout.addWidget(client_mode) + connection_mode_layout.addWidget(server_mode) + connection_mode_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + # Host name Box - hostNameBox = QGroupBox("Host-Name or IP-Address") - - hostName = QLineEdit() - hostName.setText(self.stream.tcpSettings.host) - - hostNameLayout = QHBoxLayout(hostNameBox) - hostNameLayout.addWidget(hostName) - hostNameLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - serverMode.toggled.connect(hostName.setDisabled) - - # Port Box - PortBox = QGroupBox("Port number") - - Port = QSpinBox() - Port.setMaximumWidth(100) - Port.setMaximum(65535) - Port.setValue(self.stream.tcpSettings.port) - - PortLayout = QHBoxLayout(PortBox) - PortLayout.addWidget(Port) - PortLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - + host_name_box = QGroupBox("Host-Name or IP-Address") + + host_name = QLineEdit() + host_name.setText(self.stream.tcp_settings.host) + + host_name_layout = QHBoxLayout(host_name_box) + host_name_layout.addWidget(host_name) + host_name_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + server_mode.toggled.connect(host_name.setDisabled) + + # Port Box + port_box = QGroupBox("Port number") + + port = QSpinBox() + port.setMaximumWidth(100) + port.setMaximum(65535) + port.setValue(self.stream.tcp_settings.port) + + port_layout = QHBoxLayout(port_box) + port_layout.addWidget(port) + port_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + # Final Layout - - resultLayout.addWidget(connectionModeBox) - resultLayout.addWidget(hostNameBox) - resultLayout.addWidget(PortBox) - resultLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - + + result_layout.addWidget(connection_mode_box) + result_layout.addWidget(host_name_box) + result_layout.addWidget(port_box) + result_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + # SIGNALS - clientMode.clicked.connect(lambda : self.stream.tcpSettings.set_StreamMode(StreamMode.CLIENT)) - serverMode.clicked.connect(lambda : self.stream.tcpSettings.set_StreamMode(StreamMode.SERVER)) - hostName.editingFinished.connect(lambda : self.stream.tcpSettings.setHost(hostName.text())) - Port.editingFinished.connect(lambda : self.stream.tcpSettings.setPort(Port.value())) - + client_mode.clicked.connect(lambda : self.stream.tcp_settings.set_stream_mode(StreamMode.CLIENT)) + server_mode.clicked.connect(lambda : self.stream.tcp_settings.set_stream_mode(StreamMode.SERVER)) + host_name.editingFinished.connect(lambda : self.stream.tcp_settings.set_host(host_name.text())) + port.editingFinished.connect(lambda : self.stream.tcp_settings.set_port(port.value())) + return result - - def udpMenu(self): - result = QWidget() - resultLayout = QVBoxLayout(result) - + + def udp_menu(self): + """udp configure tab + """ + result = QWidget() + result_layout = QVBoxLayout(result) + #Host name box - hostNameBox = QGroupBox("Host-Name or IP-Address") - - specificHost = QCheckBox("Listen/Transmit to a specific Host Name or IP Address") - hostName = QLineEdit() - hostName.setText(self.stream.udpSettings.host) - hostName.setDisabled(True) - - hostNameLayout = QVBoxLayout(hostNameBox) - hostNameLayout.addWidget(specificHost) - hostNameLayout.addWidget(hostName) - hostNameLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - + host_name_box = QGroupBox("Host-Name or IP-Address") + + specific_host = QCheckBox("Listen/Transmit to a specific Host Name or IP Address") + host_name = QLineEdit() + host_name.setText(self.stream.udp_settings.host) + host_name.setDisabled(True) + + host_name_layout = QVBoxLayout(host_name_box) + host_name_layout.addWidget(specific_host) + host_name_layout.addWidget(host_name) + host_name_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + # Port Box - PortBox = QGroupBox("Port number") - - Port = QSpinBox() - Port.setMaximumWidth(100) - Port.setMaximum(65535) - Port.setValue(self.stream.udpSettings.port) - - PortLayout = QHBoxLayout(PortBox) - PortLayout.addWidget(Port) - PortLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - + port_box = QGroupBox("Port number") + + port = QSpinBox() + port.setMaximumWidth(100) + port.setMaximum(65535) + port.setValue(self.stream.udp_settings.port) + + port_layout = QHBoxLayout(port_box) + port_layout.addWidget(port) + port_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + # Data flow Box - dataFlowBox = QGroupBox("Data Flow") - - dataFlowBoxLayout = QHBoxLayout(dataFlowBox) - dataflowList = QComboBox() - for dataflow in DataFlow : - dataflowList.addItem( dataflow.name , dataflow ) - if self.stream.udpSettings.DataFlow is not None : - index = dataflowList.findData(self.stream.udpSettings.DataFlow) - dataflowList.setCurrentIndex(index) - dataFlowBoxLayout.addWidget(dataflowList) - - # Final Layout - resultLayout.addWidget(hostNameBox) - resultLayout.addWidget(PortBox) - resultLayout.addWidget(dataFlowBox) - resultLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - + data_flow_box = QGroupBox("Data Flow") + + data_flow_box_layout = QHBoxLayout(data_flow_box) + data_flow_list = QComboBox() + for dataflow in UdpSettings.DataFlow : + data_flow_list.addItem( dataflow.name , dataflow ) + if self.stream.udp_settings.dataflow is not None : + index = data_flow_list.findData(self.stream.udp_settings.dataflow) + data_flow_list.setCurrentIndex(index) + data_flow_box_layout.addWidget(data_flow_list) + + # Final Layout + result_layout.addWidget(host_name_box) + result_layout.addWidget(port_box) + result_layout.addWidget(data_flow_box) + result_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + # SIGNALS - specificHost.toggled.connect(lambda : hostName.setDisabled( not specificHost.isChecked())) - - hostName.editingFinished.connect(lambda : self.stream.udpSettings.setHost(hostName.text())) - Port.editingFinished.connect(lambda : self.stream.udpSettings.setPort(Port.text())) - dataflowList.currentIndexChanged.connect(lambda : self.stream.udpSettings.set_DataFlow(dataflowList.currentData())) - + specific_host.toggled.connect(lambda : host_name.setDisabled( not specific_host.isChecked())) + host_name.editingFinished.connect(lambda : self.stream.udp_settings.set_host(host_name.text())) + port.editingFinished.connect(lambda : self.stream.udp_settings.set_port(int(port.text()))) + data_flow_list.currentIndexChanged.connect(lambda : self.stream.udp_settings.set_dataflow(data_flow_list.currentData())) + return result - def ntripMenu(self): - result = QWidget() - resultLayout = QVBoxLayout(result) - + def ntrip_menu(self): + """ntrip config tab + """ + result = QWidget() + result_layout = QVBoxLayout(result) + # Host name Box - - ntripCasterBox = QGroupBox("Ntrip Caster") - ntripCasterLayout = QVBoxLayout(ntripCasterBox) - - self.ntriphostName = QLineEdit() - self.ntriphostName.setText(self.stream.ntripClient.ntripSettings.host) - hostNameLabel= QLabel("Host : ") - hostNameLabel.setBuddy(self.ntriphostName) - - hostNameLayout = QHBoxLayout() - hostNameLayout.addWidget(hostNameLabel) - hostNameLayout.addWidget(self.ntriphostName) - hostNameLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - ntripCasterLayout.addLayout(hostNameLayout) - + + ntrip_caster_box = QGroupBox("Ntrip Caster") + ntrip_caster_layout = QVBoxLayout(ntrip_caster_box) + + self.ntrip_host_name = QLineEdit() + self.ntrip_host_name.setText(self.stream.ntrip_client.ntrip_settings.host) + host_name_label= QLabel("Host : ") + host_name_label.setBuddy(self.ntrip_host_name) + + host_name_layout = QHBoxLayout() + host_name_layout.addWidget(host_name_label) + host_name_layout.addWidget(self.ntrip_host_name) + host_name_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + ntrip_caster_layout.addLayout(host_name_layout) + # Port Box - Port = QSpinBox() - Port.setMaximumWidth(100) - Port.setMaximum(65535) - Port.setValue(self.stream.ntripClient.ntripSettings.port) - - PortLabel = QLabel("Port : ") - PortLabel.setBuddy(Port) - - PortLayout = QHBoxLayout() - PortLayout.addWidget(PortLabel) - PortLayout.addWidget(Port) - PortLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - PortLayout.setAlignment(Qt.AlignmentFlag.AlignLeft) - ntripCasterLayout.addLayout(PortLayout) - - - # Stream - MountPoint Box - mountPointBox = QGroupBox("Stream") - mountPointBoxLayout = QVBoxLayout(mountPointBox) - self.mountPointList = QComboBox() - self.mountPointList.setPlaceholderText("List unavailable") + port = QSpinBox() + port.setMaximumWidth(100) + port.setMaximum(65535) + port.setValue(self.stream.ntrip_client.ntrip_settings.port) - mountPointBoxLayout.addWidget(self.mountPointList) + port_label = QLabel("Port : ") + port_label.setBuddy(port) + + port_layout = QHBoxLayout() + port_layout.addWidget(port_label) + port_layout.addWidget(port) + port_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + port_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + ntrip_caster_layout.addLayout(port_layout) + + # Stream - MountPoint Box + mount_point_box = QGroupBox("Stream") + mount_point_box_layout = QVBoxLayout(mount_point_box) + self.mountpoint_list = QComboBox() + self.mountpoint_list.setPlaceholderText("List unavailable") + self.task_get_new_source_table() + mount_point_box_layout.addWidget(self.mountpoint_list) # TLS - tlsBox = QGroupBox("TLS") - tlsBox.setCheckable(True) - tlsBox.setChecked(self.stream.ntripClient.ntripSettings.tls) - - tlsBoxLayout = QVBoxLayout(tlsBox) - + tls_box = QGroupBox("TLS") + tls_box.setCheckable(True) + tls_box.setChecked(self.stream.ntrip_client.ntrip_settings.tls) + + tls_box_layout = QVBoxLayout(tls_box) + self.cert = QLineEdit() - self.cert.setText(self.stream.ntripClient.ntripSettings.cert) - certLabel= QLabel("Certificate : ") - certLabel.setBuddy(self.cert) - certSelectFile = QPushButton("...") - certSelectFile.setToolTip("Only .cer, .crt, .pem or .key Files are allowed") - tlsBoxLayout.addLayout(TrioWidgets(certLabel,self.cert,certSelectFile)) - + self.cert.setText(self.stream.ntrip_client.ntrip_settings.cert) + cert_label= QLabel("Certificate : ") + cert_label.setBuddy(self.cert) + cert_select_file = QPushButton("...") + cert_select_file.setToolTip("Only .cer, .crt, .pem or .key Files are allowed") + tls_box_layout.addLayout(pair_h_widgets(cert_label,self.cert,cert_select_file)) + # Authentification Box - authBox = QGroupBox("Authentification") - authBox.setCheckable(True) - authBox.setChecked(self.stream.ntripClient.ntripSettings.auth) - - authBoxLayout = QVBoxLayout(authBox) - + auth_box = QGroupBox("Authentification") + auth_box.setCheckable(True) + auth_box.setChecked(self.stream.ntrip_client.ntrip_settings.auth) + + auth_box_layout = QVBoxLayout(auth_box) + user = QLineEdit() - user.setText(self.stream.ntripClient.ntripSettings.username) - userLabel= QLabel("User : ") - userLabel.setBuddy(user) - authBoxLayout.addLayout(PairWidgets(userLabel,user)) - + user.setText(self.stream.ntrip_client.ntrip_settings.username) + user_label= QLabel("User : ") + user_label.setBuddy(user) + auth_box_layout.addLayout(pair_h_widgets(user_label,user)) + password = QLineEdit() - password.setText(self.stream.ntripClient.ntripSettings.password) - passwordLabel= QLabel("Password : ") - passwordLabel.setBuddy(password) - authBoxLayout.addLayout(PairWidgets(passwordLabel , password)) - + password.setText(self.stream.ntrip_client.ntrip_settings.password) + password_label= QLabel("Password : ") + password_label.setBuddy(password) + auth_box_layout.addLayout(pair_h_widgets(password_label , password)) + # FIXED POSITION Box - - fixedPositionBox = QGroupBox("Fixed position for GGA ") - fixedPositionBox.setCheckable(True) - fixedPositionBox.setChecked(self.stream.ntripClient.ntripSettings.fixedPos) - - fixedPositionBoxLayout = QVBoxLayout(fixedPositionBox) - + + fixed_position_box = QGroupBox("Fixed position for GGA ") + fixed_position_box.setCheckable(True) + fixed_position_box.setChecked(self.stream.ntrip_client.ntrip_settings.fixed_pos) + + fixed_position_box_layout = QVBoxLayout(fixed_position_box) + latitude = QLineEdit() latitude.setInputMask("!N 99.999999999") input_validator = QRegularExpressionValidator(QRegularExpression("[NS] [0-9]{2}.[0-9]{9}")) latitude.setValidator(input_validator) - latitude.setText(self.stream.ntripClient.ntripSettings.getLatitude()) - latitudeLabel= QLabel("Latitude : ") - latitudeLabel.setBuddy(latitude) - fixedPositionBoxLayout.addLayout(PairWidgets(latitudeLabel,latitude)) - - longitude = QLineEdit() + latitude.setText(self.stream.ntrip_client.ntrip_settings.get_latitude()) + latitude_label= QLabel("Latitude : ") + latitude_label.setBuddy(latitude) + fixed_position_box_layout.addLayout(pair_h_widgets(latitude_label,latitude)) + + longitude = QLineEdit() longitude.setInputMask("!E 999.999999999") input_validator = QRegularExpressionValidator(QRegularExpression("[EW] [0-9]{3}.[0-9]{9}")) longitude.setValidator(input_validator) - longitude.setText(self.stream.ntripClient.ntripSettings.getLongitude()) - longitudeLabel= QLabel("Longitude : ") - longitudeLabel.setBuddy(longitude) - fixedPositionBoxLayout.addLayout(PairWidgets(longitudeLabel,longitude)) - + longitude.setText(self.stream.ntrip_client.ntrip_settings.get_longitude()) + longitude_label= QLabel("Longitude : ") + longitude_label.setBuddy(longitude) + fixed_position_box_layout.addLayout(pair_h_widgets(longitude_label,longitude)) + height = QSpinBox() height.setMaximum(100000) height.setMaximumWidth(100) - height.setValue(self.stream.ntripClient.ntripSettings.height) - heightLabel = QLabel("Height") - heightLabel.setBuddy(height) - fixedPositionBoxLayout.addLayout(PairWidgets(heightLabel,height)) - + height.setValue(self.stream.ntrip_client.ntrip_settings.height) + height_label = QLabel("Height") + height_label.setBuddy(height) + fixed_position_box_layout.addLayout(pair_h_widgets(height_label,height)) # Final Layout - resultLayout.addWidget(ntripCasterBox) - resultLayout.addWidget(mountPointBox) - resultLayout.addWidget(tlsBox) - resultLayout.addWidget(authBox) - resultLayout.addWidget(fixedPositionBox) - resultLayout.setAlignment(Qt.AlignmentFlag.AlignTop) - + result_layout.addWidget(ntrip_caster_box) + result_layout.addWidget(mount_point_box) + result_layout.addWidget(tls_box) + result_layout.addWidget(auth_box) + result_layout.addWidget(fixed_position_box) + result_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + # SIGNALS - self.ntriphostName.editingFinished.connect(lambda : self.updateMountpointList()) - self.ntriphostName.editingFinished.emit() - - Port.valueChanged.connect(lambda : self.stream.ntripClient.ntripSettings.setPort(Port.value())) - - self.mountPointList.currentIndexChanged.connect(lambda : self.stream.ntripClient.ntripSettings.setMountpoint(self.mountPointList.currentData())) - - authBox.toggled.connect(lambda : self.stream.ntripClient.ntripSettings.setAuth(authBox.isChecked())) - user.editingFinished.connect(lambda : self.stream.ntripClient.ntripSettings.setUsername(user.text())) - password.editingFinished.connect(lambda : self.stream.ntripClient.ntripSettings.setPassword(password.text())) - fixedPositionBox.toggled.connect(lambda : self.stream.ntripClient.ntripSettings.setFixedPos(fixedPositionBox.isChecked())) - latitude.editingFinished.connect(lambda : self.stream.ntripClient.ntripSettings.setLatitude(latitude.text())) - longitude.editingFinished.connect(lambda : self.stream.ntripClient.ntripSettings.setLongitude(longitude.text())) - height.valueChanged.connect(lambda x : self.stream.ntripClient.ntripSettings.setHeight(height.value())) - certSelectFile.pressed.connect(lambda : self.selectCertFile() ) - self.cert.editingFinished.connect(lambda : self.stream.ntripClient.ntripSettings.setCert(self.cert.text())) - tlsBox.toggled.connect(lambda : self.stream.ntripClient.ntripSettings.setTls(tlsBox.isChecked())) + self.ntrip_host_name.editingFinished.connect(lambda: self.update_mountpoint_list()) + self.ntrip_host_name.editingFinished.emit() + + port.valueChanged.connect(lambda : self.stream.ntrip_client.ntrip_settings.set_port(port.value())) + + self.mountpoint_list.currentIndexChanged.connect(lambda : self.stream.ntrip_client.ntrip_settings.set_mountpoint(self.mountpoint_list.currentData())) + + auth_box.toggled.connect(lambda : self.stream.ntrip_client.ntrip_settings.set_auth(auth_box.isChecked())) + user.editingFinished.connect(lambda : self.stream.ntrip_client.ntrip_settings.set_username(user.text())) + password.editingFinished.connect(lambda : self.stream.ntrip_client.ntrip_settings.set_password(password.text())) + fixed_position_box.toggled.connect(lambda : self.stream.ntrip_client.ntrip_settings.set_fixed_pos(fixed_position_box.isChecked())) + latitude.editingFinished.connect(lambda : self.stream.ntrip_client.ntrip_settings.set_latitude(latitude.text())) + longitude.editingFinished.connect(lambda : self.stream.ntrip_client.ntrip_settings.set_longitude(longitude.text())) + height.valueChanged.connect(lambda x : self.stream.ntrip_client.ntrip_settings.set_height(height.value())) + cert_select_file.pressed.connect(self.select_cert_file ) + self.cert.editingFinished.connect(lambda : self.stream.ntrip_client.ntrip_settings.set_cert(self.cert.text())) + tls_box.toggled.connect(lambda : self.stream.ntrip_client.ntrip_settings.set_tls(tls_box.isChecked())) return result - def updateMountpointList(self): - self.mountPointList.clear() - self.mountPointList.setPlaceholderText("Waiting for source table ...") - self.updatethread = ConfigurationThread(self) - self.updatethread.finished.connect(self.taskGetNewSourceTable) - self.updatethread.finished.connect(self.updatethread.deleteLater) - self.updatethread.start() - - def taskGetNewSourceTable(self): - if len(self.ntriphostName.text()) > 0: - if self.stream.ntripClient.ntripSettings.sourceTable is not None: - if len(self.stream.ntripClient.ntripSettings.sourceTable) != 0: - for source in self.stream.ntripClient.ntripSettings.sourceTable: - self.mountPointList.addItem(source.mountpoint, source.mountpoint) - if self.stream.ntripClient.ntripSettings.mountpoint is not None: - index = self.mountPointList.findData(self.stream.ntripClient.ntripSettings.mountpoint) - else : - index : int = 3 - self.mountPointList.setPlaceholderText("") - self.mountPointList.setCurrentIndex(index) - else : - self.mountPointList.setPlaceholderText("List unavailable") - else: - self.mountPointList.setPlaceholderText("List unavailable") - - def bottomButtonLayout(self): + def update_mountpoint_list(self): + """update the list of available mountpoint selectable. + """ + self.mountpoint_list.clear() + self.mountpoint_list.setPlaceholderText("Waiting for source table ...") + self.update_thread = QThread() + self.worker = SourceTableWorker(self) + self.worker.moveToThread(self.update_thread) + self.update_thread.started.connect(self.worker.run) + self.worker.finished.connect(self.update_thread.quit) + self.worker.finished.connect(self.worker.deleteLater) + self.update_thread.finished.connect(self.update_thread.deleteLater) + self.update_thread.finished.connect(self.cleanup) + self.worker.finished.connect(self.task_get_new_source_table) + + self.update_thread.start() + + def cleanup(self): + self.update_thread = None + self.worker = None + + def task_get_new_source_table(self): + """ get new source table from the new ntrip caster + """ + if len(self.ntrip_host_name.text()) < 1 : + self.mountpoint_list.setPlaceholderText("List unavailable") + else : + try : + if self.stream.ntrip_client.ntrip_settings.source_table is None : + self.mountpoint_list.setPlaceholderText("List unavailable") + elif len(self.stream.ntrip_client.ntrip_settings.source_table) != 0: + currentindex : int = 3 + for source, index in zip(self.stream.ntrip_client.ntrip_settings.source_table , range(len(self.stream.ntrip_client.ntrip_settings.source_table))): + self.mountpoint_list.addItem(source.mountpoint, source.mountpoint) + if self.stream.ntrip_client.ntrip_settings.mountpoint is not None and self.stream.ntrip_client.ntrip_settings.mountpoint == source.mountpoint: + currentindex = index + + self.mountpoint_list.setPlaceholderText("") + self.mountpoint_list.setCurrentIndex(currentindex) + else : + self.mountpoint_list.setPlaceholderText("List unavailable") + except NtripClientError : + self.mountpoint_list.setPlaceholderText("List unavailable") + + def bottom_button_layout(self): + """Create the ok and cancel button of the dialog + """ + #Valid Button : confirm all modification - validButton = QPushButton("OK") - validButton.pressed.connect(self.confirm) - + valid_button = QPushButton("OK") + valid_button.pressed.connect(self.confirm) + #return : close the dialog with no modification - - returnButton = QPushButton("Cancel") - returnButton.pressed.connect(self.cancel) - - bottomButtonLayout = QHBoxLayout() - bottomButtonLayout.addWidget(validButton) - bottomButtonLayout.addWidget(returnButton) - - return bottomButtonLayout + + return_button = QPushButton("Cancel") + return_button.pressed.connect(self.cancel) + + bottom_button_layout = QHBoxLayout() + bottom_button_layout.addWidget(valid_button) + bottom_button_layout.addWidget(return_button) + + return bottom_button_layout + def confirm(self): - if self.stream.ntripClient.ntripSettings.fixedPos : - self.stream.ntripClient._createGgaString() + """function called when ok button is pressed + """ + self.save_previous_tab.emit(self.config_tabs.currentIndex()) + if self.stream.ntrip_client.ntrip_settings.fixed_pos : + self.stream.ntrip_client.create_gga_string() + self.save_config.emit(self.stream) self.accept() - + def cancel(self): - self.stream = self.streamsave + """function called when cancel button is pressed + """ + self.save_config.emit(self.stream_save) self.reject() - - def openScriptFile(self, inputWidget : QLineEdit, checkbox: QCheckBox, connectScript : bool = None ): - if checkbox.isChecked(): - fileName = QFileDialog.getOpenFileName(self,"Select Script") - if fileName[0] != '' and fileName[1] != '': - try : - if connectScript: - self.stream.setStartupScriptPath(fileName[0]) - self.stream.setStartupScript() + + def open_script_file(self, input_widget : QLineEdit, checkbox: QCheckBox, connect_script : bool = None ): + """open a dialog to get the path to the script file + """ + if checkbox.isChecked(): + file_name = QFileDialog.getOpenFileName(self,"Select Script") + if file_name[0] != '' and file_name[1] != '': + try : + if connect_script: + self.stream.set_startup_script_path(file_name[0]) + self.stream.set_startup_script() else: - self.stream.setCloseScriptPath(fileName[0]) - self.stream.setCloseScript() - inputWidget.setDisabled(False) - inputWidget.setText(fileName[0]) - except : + self.stream.set_close_script_path(file_name[0]) + self.stream.set_close_script() + input_widget.setDisabled(False) + input_widget.setText(file_name[0]) + except StreamException : + checkbox.click() + input_widget.setDisabled(True) + else: + checkbox.click() + else : + input_widget.setDisabled(True) + + def select_log_file(self , logedit : QLineEdit , checkbox : QCheckBox): + """open the dialog to get the path to the log file + """ + if checkbox.isChecked(): + file_name = QFileDialog.getSaveFileName(self,"Select log file ") + if file_name[0] != '' and file_name[1] != '': + try : + self.stream.set_logging_file_name(file_name[0]) + self.stream.set_logging() + logedit.setDisabled(False) + logedit.setText(file_name[0]) + except StreamException : checkbox.click() - inputWidget.setDisabled(True) - else: + logedit.setDisabled(True) + else: checkbox.click() else : - inputWidget.setDisabled(True) - - def selectCertFile(self): - fileName = QFileDialog.getOpenFileName(self,"Select certificat") - if fileName[0] != '' and fileName[1] != '': - if fileName[1] in [ ".cer", ".crt", ".pem" ,".key"] : - self.stream.ntripClient.ntripSettings.setCert(fileName[0]) - self.cert.setText(fileName[0]) + logedit.setDisabled(True) + + def select_cert_file(self): + """open a dialog to get the path to the certification file + """ + file_name = QFileDialog.getOpenFileName(self,"Select certificat") + if file_name[0] != '' and file_name[1] != '': + if file_name[1] in [ ".cer", ".crt", ".pem" ,".key"] : + self.stream.ntrip_client.ntrip_settings.set_cert(file_name[0]) + self.cert.setText(file_name[0]) + class ShowDataInterface(QDialog): - def __init__(self,stream : PortConfig) -> None: + def __init__(self,stream : Stream) -> None: super().__init__() self.stream = stream + self.timer_id = self.startTimer(10) self.setMinimumSize(350,300) self.setBaseSize(350,300) - self.setWindowIcon(QIcon(DATAFILESPATH + 'pyDatalink_icon.png')) - self.setWindowTitle("Data Link Connection " + str(stream.id)) - configureLayout = QVBoxLayout(self) - self.showDataOutput = QTextEdit() - self.showDataOutput.setLineWrapColumnOrWidth(1000) - self.showDataOutput.setLineWrapMode(QTextEdit.LineWrapMode.FixedPixelWidth) - self.showDataOutput.setReadOnly(True) + self.setWindowIcon(QIcon(os.path.join(DATAFILESPATH , 'pyDatalink_icon.png'))) + self.setWindowTitle("Data Link Connection " + str(stream.stream_id)) + configure_layout = QVBoxLayout(self) + self.show_data_output = QTextEdit() + self.show_data_output.setLineWrapColumnOrWidth(1000) + self.show_data_output.setLineWrapMode(QTextEdit.LineWrapMode.FixedPixelWidth) + self.show_data_output.setReadOnly(True) self.freeze = False - self.stream.ShowInputData.set() - self.stream.ShowOutputData.set() - - self.sendCommandEdit = QLineEdit() - self.sendCommandEdit.returnPressed.connect(lambda : self.sendCommand(self.sendCommandEdit.text())) - showData = QComboBox() - showData.addItem("All Data") - showData.addItem("Only incomming Data") - showData.addItem("Only outgoing Data") - showData.setCurrentIndex(0) - showData.currentIndexChanged.connect(lambda test : self.changeDataVisibility(test)) - - configureLayout.addWidget(self.showDataOutput) - configureLayout.addWidget(self.sendCommandEdit) - configureLayout.addWidget(showData) - configureLayout.addLayout(self.bottomButton()) - - self.timer_id = self.startTimer(10) - + self.stream.show_incoming_data.set() + self.stream.show_outgoing_data.set() + + self.send_command_edit = QLineEdit() + self.send_command_edit.returnPressed.connect(lambda : self.send_command(self.send_command_edit.text())) + show_data = QComboBox() + show_data.addItem("All Data") + show_data.addItem("Only incomming Data") + show_data.addItem("Only outgoing Data") + show_data.setCurrentIndex(0) + show_data.currentIndexChanged.connect(lambda e : self.change_data_visibility(e)) + + configure_layout.addWidget(self.show_data_output) + configure_layout.addWidget(self.send_command_edit) + configure_layout.addWidget(show_data) + configure_layout.addLayout(self.bottom_button()) + def timerEvent(self, event): - if self.stream.DataToShow.empty() is False : - value = self.stream.DataToShow.get() - if not self.freeze : - self.showDataOutput.insertPlainText( value + "\n" ) - self.showDataOutput.moveCursor(QTextCursor.End) - self.showDataOutput.horizontalScrollBar().setValue(self.showDataOutput.horizontalScrollBar().minimum()) - - - def bottomButton(self): - + if self.stream.data_to_show.empty() is False : + value = self.stream.data_to_show.get() + if not self.freeze : + self.show_data_output.insertPlainText( value + "\n" ) + self.show_data_output.moveCursor(QTextCursor.End) + self.show_data_output.horizontalScrollBar().setValue(self.show_data_output.horizontalScrollBar().minimum()) + + def bottom_button(self): + """ configuration of the bottom button of the show data dialog + """ + #Valid Button : confirm all modification - - clearButton = QPushButton("Clear") - clearButton.pressed.connect(lambda : self.showDataOutput.clear()) - clearButton.setDefault(False) - clearButton.setAutoDefault(False) - freezeButton = QPushButton("Freeze") - freezeButton.setDefault(False) - freezeButton.setAutoDefault(False) - freezeButton.pressed.connect(lambda : self.freezeDataFlow()) - validButton = QPushButton("Close") - validButton.setDefault(False) - validButton.setAutoDefault(False) - validButton.pressed.connect(lambda : self.closeDialog()) - + clear_button = QPushButton("Clear") + clear_button.pressed.connect(self.show_data_output.clear) + clear_button.setDefault(False) + clear_button.setAutoDefault(False) + self.freeze_button = QPushButton("Freeze") + self.freeze_button.setDefault(False) + self.freeze_button.setAutoDefault(False) + self.freeze_button.pressed.connect(self.freeze_data_flow) + valid_button = QPushButton("Close") + valid_button.setDefault(False) + valid_button.setAutoDefault(False) + valid_button.pressed.connect(self.close_dialog) + #return : close the dialog with no modification - - bottomButtonLayout = QHBoxLayout() - bottomButtonLayout.addWidget(clearButton) - bottomButtonLayout.addWidget(freezeButton) - bottomButtonLayout.addWidget(validButton) - - return bottomButtonLayout - def closeDialog(self): - self.stream.ShowInputData.clear() - self.stream.ShowOutputData.clear() + + bottom_button_layout = QHBoxLayout() + bottom_button_layout.addWidget(clear_button) + bottom_button_layout.addWidget(self.freeze_button) + bottom_button_layout.addWidget(valid_button) + + return bottom_button_layout + def close_dialog(self): + """function called when clossing the dialog + """ + self.stream.show_incoming_data.clear() + self.stream.show_outgoing_data.clear() self.killTimer(self.timer_id) self.reject() - + def closeEvent(self, event): - self.stream.ShowInputData.clear() - self.stream.ShowOutputData.clear() + """function called when clossing the dialog + """ + self.stream.show_incoming_data.clear() + self.stream.show_outgoing_data.clear() self.killTimer(self.timer_id) self.reject() - - def freezeDataFlow(self): + + def freeze_data_flow(self): + """fonction that freeze the data flow + """ self.freeze = not self.freeze - - def changeDataVisibility(self,index): - if index == 0 : - self.allDataVisibility() + + def change_data_visibility(self,index): + """modfy data visibility + """ + if index == 0 : + self.all_data_visibility() elif index == 1: - self.incomingDataVisibility() + self.incoming_data_visibility() elif index == 2 : - self.outgoingDataVisibility() - - def allDataVisibility(self): - if not self.stream.ShowInputData.is_set() : - self.stream.ShowInputData.set() - if not self.stream.ShowOutputData.is_set() : - self.stream.ShowOutputData.set() - - def incomingDataVisibility(self): - if not self.stream.ShowInputData.is_set() : - self.stream.ShowInputData.set() - if self.stream.ShowOutputData.is_set() : - self.stream.ShowOutputData.clear() - - def outgoingDataVisibility(self): - if self.stream.ShowInputData.is_set() : - self.stream.ShowInputData.clear() - if not self.stream.ShowOutputData.is_set() : - self.stream.ShowOutputData.set() - - def sendCommand(self,CMD): - self.stream.sendCommand(CMD) - self.sendCommandEdit.setText("") + self.outgoing_data_visibility() + + def all_data_visibility(self): + """show all the data + """ + if not self.stream.show_incoming_data.is_set() : + self.stream.show_incoming_data.set() + if not self.stream.show_outgoing_data.is_set() : + self.stream.show_outgoing_data.set() + + def incoming_data_visibility(self): + """Show only the incoming data + """ + if not self.stream.show_incoming_data.is_set() : + self.stream.show_incoming_data.set() + if self.stream.show_outgoing_data.is_set() : + self.stream.show_outgoing_data.clear() + + def outgoing_data_visibility(self): + """Show only the outgoing data + """ + if self.stream.show_incoming_data.is_set() : + self.stream.show_incoming_data.clear() + if not self.stream.show_outgoing_data.is_set() : + self.stream.show_outgoing_data.set() + def send_command(self,command): + """send a command to the connected device + """ + self.stream.send_command(command) + self.send_command_edit.setText("") + class PreferencesInterface(QDialog): - + """class for the preference dialog + """ + def __init__(self,preference : Preferences) -> None: super().__init__() - + self.preference = preference - preferenceLayout = QVBoxLayout(self) - - #Configuration File - generalBox = QGroupBox("General") - generalLayout = QVBoxLayout(generalBox) - configNameLabel = QLabel("Current Config Name") - configNameInput = QLineEdit() - configNameInput.setText(preference.configName) - generalLayout.addLayout(PairWidgets(configNameLabel , configNameInput)) - + preference_layout = QVBoxLayout(self) + self.setWindowIcon(QIcon(os.path.join(DATAFILESPATH , 'pyDatalink_icon.png'))) + self.setWindowTitle("Preferences") + + #Configuration File + general_box = QGroupBox("General") + general_layout = QVBoxLayout(general_box) + config_name_label = QLabel("Current Config Name") + config_name_input = QLineEdit() + config_name_input.setText(preference.config_name) + general_layout.addLayout(pair_h_widgets(config_name_label , config_name_input)) + # line Termination # List of 3 : \n \r \r\n - lineTerminationLabel = QLabel("Line Termination") - lineTerminationComboBox = QComboBox() - lineTerminationComboBox.addItem("","\n") - lineTerminationComboBox.addItem("","\r") - lineTerminationComboBox.addItem("","\n\r") - generalLayout.addLayout(PairWidgets(lineTerminationLabel,lineTerminationComboBox)) - + line_termination_label = QLabel("Line Termination") + line_termination_combobox = QComboBox() + line_termination_combobox.addItem("","\n") + line_termination_combobox.addItem("","\r") + line_termination_combobox.addItem("","\n\r") + general_layout.addLayout(pair_h_widgets(line_termination_label,line_termination_combobox)) + #Number of streams - maxStreamLabel = QLabel("Number of Port Panels") - maxStreamsInput = QSpinBox() - maxStreamsInput.setMaximumWidth(50) - maxStreamsInput.setMaximum(6) - maxStreamsInput.setMinimum(1) - maxStreamsInput.setValue(preference.maxStreams) - generalLayout.addLayout(PairWidgets(maxStreamLabel,maxStreamsInput)) - - # Connect list + max_stream_label = QLabel("Number of Port Panels") + max_streams_input = QSpinBox() + max_streams_input.setMaximumWidth(50) + max_streams_input.setMaximum(6) + max_streams_input.setMinimum(1) + max_streams_input.setValue(preference.max_streams) + general_layout.addLayout(pair_h_widgets(max_stream_label,max_streams_input)) + + # connect list + + startup_connect_box = QGroupBox("connect at Startup") + startup_connect_layout = QVBoxLayout(startup_connect_box) + startup_connect_label = QLabel("Select the ports that should auto connect at startup") + startup_connect_layout.addWidget(startup_connect_label) + startup_connect_layout.addLayout(self.startup_connect_layout()) - startupConnectBox = QGroupBox("Connect at Startup") - startupConnectLayout = QVBoxLayout(startupConnectBox) - startupConnectLabel = QLabel("Select the ports that should auto connect at startup") - startupConnectLayout.addWidget(startupConnectLabel) - startupConnectLayout.addLayout(self.startupConnectLayout()) - + # Final Layout - - preferenceLayout.addWidget(generalBox) - preferenceLayout.addWidget(startupConnectBox) - + + preference_layout.addWidget(general_box) + preference_layout.addWidget(startup_connect_box) + preference_layout.addLayout(self.bottom_button_layout()) + #SIGNALS - # self.preference.setMaxStream(newvalue) - maxStreamsInput.valueChanged.connect(lambda : self.preference.setMaxStream(maxStreamsInput.value())) - lineTerminationComboBox.currentIndexChanged.connect(lambda : self.preference.setLineTermination(lineTerminationComboBox.currentData())) - configNameInput.editingFinished.connect(lambda : self.preference.setConfigName(configNameInput.text())) - - - - def startupConnectLayout(self): - startupConnectFinalLayout = QVBoxLayout() - for portId in range(6): - newCheckBox = QCheckBox(f"Connect {portId}") - if self.preference.Connect[portId] : - newCheckBox.setChecked(True) - newCheckBox.stateChanged.connect(lambda state,x=portId : self.toggleStartupConnection(x)) - startupConnectFinalLayout.addWidget(newCheckBox) - return startupConnectFinalLayout - - def toggleStartupConnection(self, portid): - self.preference.Connect[portid] = not self.preference.Connect[portid] - - - -class ConfigurationThread(QThread): + # self.preference.set_max_stream(newvalue) + max_streams_input.valueChanged.connect(lambda : self.preference.set_max_stream(max_streams_input.value())) + line_termination_combobox.currentIndexChanged.connect(lambda : self.preference.set_line_termination(line_termination_combobox.currentData())) + config_name_input.editingFinished.connect(lambda : self.preference.set_config_name(config_name_input.text())) + + def bottom_button_layout(self): + """Create the ok and cancel button of the dialog + """ + + #Valid Button : confirm all modification + valid_button = QPushButton("OK") + valid_button.pressed.connect(self.accept) + + + bottom_button_layout = QHBoxLayout() + bottom_button_layout.addWidget(valid_button) + + return bottom_button_layout + + def startup_connect_layout(self): + """ + Create connect on startup layout + """ + startup_connect_final_layout = QVBoxLayout() + for port_id in range(6): + new_check_box = QCheckBox(f"connect {port_id}") + if self.preference.connect[port_id] : + new_check_box.setChecked(True) + new_check_box.stateChanged.connect(lambda state,x=port_id : self.toggle_startup_connection(x)) + startup_connect_final_layout.addWidget(new_check_box) + return startup_connect_final_layout + + def toggle_startup_connection(self, portid): + """toggle the startup connect + """ + self.preference.connect[portid] = not self.preference.connect[portid] + +class AboutDialog(QDialog): + def __init__(self): + super().__init__() + + self.setWindowIcon(QIcon(os.path.join(DATAFILESPATH , 'pyDatalink_icon.png'))) + self.setWindowTitle("About PyDatalink") + button = QDialogButtonBox.Ok + + self.button_box = QDialogButtonBox(button) + self.button_box.accepted.connect(self.accept) + # Add Version + # add Link to github repo + # add Description + # Add Warning for still in testing + self.dialoglayout = QVBoxLayout() + + about_logo_label = QLabel() + logo = QPixmap(os.path.join(DATAFILESPATH ,"pyDatalink_Logo.png")) + about_logo_label.setPixmap(logo) + about_logo_label.setFixedSize(200,100) + about_logo_label.setScaledContents(True) + about_logo_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) + + about_label = QLabel() + about_label.setText(" PyDatalink \n Version 1.0a\n 2024 , Septentrio") + about_label.setStyleSheet(" font-weight: bold;") + about_label.setScaledContents(True) + + self.dialoglayout.addLayout(pair_h_widgets(about_logo_label,about_label)) + self.dialoglayout.addWidget(self.button_box) + self.dialoglayout.setAlignment(Qt.AlignmentFlag.AlignHCenter) + self.setLayout(self.dialoglayout) + +class SourceTableWorker(QObject): + """worker for getting source table thread + """ + finished = Signal() + + def __init__(self, configure_interface : ConfigureInterface): + super().__init__() + self.configure_interface = configure_interface + + def run(self): + try : + self.configure_interface.stream.ntrip_client.set_settings_host(self.configure_interface.ntrip_host_name.text()) + except NtripClientError: + self.configure_interface.stream.ntrip_client.ntrip_settings.source_table = None + self.finished.emit() + +class StreamConnectWorker(QObject): + """Worker for stream connect thread + """ finished = Signal() - def __init__(self, configureInterface : ConfigureInterface): + def __init__(self, connection_card : ConnectionCard): super().__init__() - self.configuratInterface = configureInterface + self.connection_card = connection_card def run(self): - try : - self.configuratInterface.stream.ntripClient.set_Settings_Host(self.configuratInterface.ntriphostName.text()) - except : - self.configuratInterface.stream.ntripClient.ntripSettings.sourceTable = None - self.finished.emit() \ No newline at end of file + if not self.connection_card.stream.connected: + try : + self.connection_card.stream.connect() + self.connection_card.configure_button.setDisabled(True) + self.connection_card.status.setText("CONNECTED") + self.connection_card.status.setStyleSheet("QLabel { color: #32a852;}") + self.connection_card.connect_button.setText("Disconnect") + except StreamException as e : + self.connection_card.status.setText("ERROR DURING CONNECTION") + self.connection_card.status.setToolTip(str(e)) + self.connection_card.status.setStyleSheet("QLabel { color: #c42323;}") + else : + try : + self.connection_card.stream.disconnect() + self.connection_card.configure_button.setDisabled(False) + self.connection_card.status.setText("") + self.connection_card.connect_button.setText("Connect") + except StreamException as e : + self.connection_card.status.setText("Couldn't disconnect") + self.connection_card.status.setToolTip(str(e)) + self.finished.emit() diff --git a/src/UserInterfaces/TerminalUserInterface.py b/src/UserInterfaces/TerminalUserInterface.py index a386771..79b1c30 100644 --- a/src/UserInterfaces/TerminalUserInterface.py +++ b/src/UserInterfaces/TerminalUserInterface.py @@ -1,21 +1,21 @@ # ############################################################################### # # Copyright (c) 2024, Septentrio -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: -# +# # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. -# +# # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# +# # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. -# +# # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -31,678 +31,729 @@ import threading import socket import sys -import time -try : - from simple_term_menu import TerminalMenu -except: - print("WARNING : You are running pyDataLink on a system that doesn't support TUI interface !") -from ..StreamConfig import DataFlow , StreamMode , Streams , StreamType , PortConfig , BaudRate , Parity , StopBits , ByteSize , SerialSettings +from src.StreamSettings.SerialSettings import BaudRate , Parity ,ByteSize , StopBits +from src.StreamSettings.TcpSettings import StreamMode +from src.StreamSettings.UdpSettings import DataFlow +from ..StreamConfig.Stream import LogFileException, ScriptFileException, Stream, StreamException, StreamType +from ..StreamConfig.App import App +try : + from simple_term_menu import TerminalMenu +except NotImplementedError as e : + print("WARNING : You are running pyDataLink on a system that doesn't support TUI interface !") class TerminalUserInterface : - - # Menu Items - mainMenuItems = ["[1] - Configure Stream" , "[2] - Connect / Disconnect","[3] - ShowData" ,"[4] - Link","[5] - Preferences","[q] - Exit"] - connectDisconectItems=["[1] - Connect","[2] - Disconnect" ,"[q] - Back to stream selection"] - showDataMenuItems=["[1] - show all data","[2] - Show Input data" , "[3] - Show Output data","[q] - Back to stream selection"] - - - PortListMenuItems = ["[q] - Back to main menu"] - - configureMenuSubmenuItems = ["[1] - Stream Config","[2] - Connect Script", "[3] - Close Script" ,"[4] - Logging" , "[q] - Back to stream selection"] - - configureStreamTypeMenuItems=[] - - SerialSettingsBaudRateItems =[] - SerialSettingsParityItems =[] - SerialSettingsStopBitsItems =[] - SerialSettingsBytesizeItems = [] - - TCPSettingsStreamModeItems = [] - - UDPSettingsDataFlowItems = [] - - - def __init__(self , Streams : Streams ) -> None: - self.Streams = Streams - for port , i in zip(Streams.StreamList , range(len(Streams.StreamList))): - self.PortListMenuItems.insert(i,"[%d] - Stream %d - %s %s" %(i ,i ,( "Connected" if port.connected else "Disonnected") , ("" if port.StreamType is None else str(port.StreamType).replace("StreamType.","- ")))) - self._CreateMenus() - - self.showDataThread : threading.Thread = None - self.stopShowDataEvent = threading.Event() - - def _showDataTask(self, selectedPort : PortConfig): - while self.stopShowDataEvent.is_set() is False: - if selectedPort.DataToShow.empty() is False : - print(selectedPort.DataToShow.get()) + + # Menu Items + main_menu_items = ["[1] - Configure Stream" , "[2] - connect / disconnect", + "[3] - ShowData" ,"[4] - Link","[5] - Preferences","[q] - Exit"] + + connect_disconect_items=["[1] - connect","[2] - disconnect" ,"[q] - Back to stream selection"] + show_data_menu_items=["[1] - show all data","[2] - Show Input data" , + "[3] - Show Output data","[q] - Back to stream selection"] + + port_list_menu_items = ["[q] - Back to main menu"] + + configure_menu_submenu_items = ["[1] - Stream Config","[2] - connect Script", + "[3] - Close Script" ,"[4] - Logging" , "[q] - Back to stream selection"] + + configure_stream_type_menu_items=[] + + serialsettings_baudrate_items =[] + serialsettings_parity_items =[] + serialsettings_stopbits_items =[] + serialsettings_bytesize_items = [] + + tcpsettings_stream_mode_items = [] + + udpsettings_dataflow_items = [] + + + def __init__(self ,app : App ) -> None: + self.app : App = app + for port , i in zip(app.stream_list , range(len(app.stream_list))): + self.port_list_menu_items.insert(i,f"[{i}] - Stream {i} - {"Connected" if port.connected else "Disonnected"} {"" if port.stream_type is None else str(port.stream_type).replace("StreamType.","- ")}") + self._create_menus() + + self.show_data_thread : threading.Thread = None + self.stop_show_data_event = threading.Event() + + def _show_data_task(self, selected_port : Stream): + while self.stop_show_data_event.is_set() is False: + if selected_port.data_to_show.empty() is False : + print(selected_port.data_to_show.get()) return 0 - - def _refreshMenuItems(self) : - for port , i in zip(self.Streams.StreamList , range(len(self.Streams.StreamList))): - self.PortListMenuItems[i] = "[%d] - Stream %d - %s %s" %(i ,i ,( "Connected" if port.connected else "Disonnected") , ("" if port.StreamType is None else str(port.StreamType).replace("StreamType.","- "))) - + def _refresh_menu_items(self) : + + for port , i in zip(self.app.stream_list , range(len(self.app.stream_list))): + self.port_list_menu_items[i] = f"[{i}] - Stream {i} - {"Connected" if port.connected else "Disonnected"} {"" if port.stream_type is None else str(port.stream_type).replace("StreamType.","- ")}" - def _CreateMenus(self): + + def _create_menus(self): #BaudRate Menu for baudrate in BaudRate : - self.SerialSettingsBaudRateItems.append(baudrate.value) - self.SerialSettingsBaudRateItems.append("[q] - Back") + self.serialsettings_baudrate_items.append(baudrate.value) + self.serialsettings_baudrate_items.append("[q] - Back") #Parity Menu for parity in Parity : - self.SerialSettingsParityItems.append(str(parity).replace("Parity.PARITY_","")) - self.SerialSettingsParityItems.append("[q] - Back") + self.serialsettings_parity_items.append(str(parity).replace("Parity.PARITY_","")) + self.serialsettings_parity_items.append("[q] - Back") #StopBits Menu for stopbits in StopBits : - self.SerialSettingsStopBitsItems.append(str(stopbits).replace("StopBits.STOPBITS_","")) - self.SerialSettingsStopBitsItems.append("[q] - Back") + self.serialsettings_stopbits_items.append(str(stopbits).replace("StopBits.STOPBITS_","")) + self.serialsettings_stopbits_items.append("[q] - Back") #Bytesize Menu for bytesize in ByteSize : - self.SerialSettingsBytesizeItems.append(str(bytesize).replace("ByteSize.","")) - self.SerialSettingsBytesizeItems.append("[q] - Back") + self.serialsettings_bytesize_items.append(str(bytesize).replace("ByteSize.","")) + self.serialsettings_bytesize_items.append("[q] - Back") #StreamMode Menu for mode in StreamMode : - self.TCPSettingsStreamModeItems.append(str(mode).replace("StreamMode.","")) - self.TCPSettingsStreamModeItems.append("[q] - Back") + self.tcpsettings_stream_mode_items.append(str(mode).replace("StreamMode.","")) + self.tcpsettings_stream_mode_items.append("[q] - Back") #StreamType - iterator = 1 - for type in StreamType : - if type.value is not None: - self.configureStreamTypeMenuItems.append("[%d] - %s" %(iterator, str(type).replace("StreamType.",""))) + iterator = 1 + for stream_type in StreamType : + if stream_type.value is not None: + self.configure_stream_type_menu_items.append(f"[{iterator}] - {str(stream_type).replace("StreamType.","")}" ) iterator+=1 - self.configureStreamTypeMenuItems.append("[q] - Back") + self.configure_stream_type_menu_items.append("[q] - Back") # DataFlow for flow in DataFlow : - self.UDPSettingsDataFlowItems.append("%s" %(str(flow).replace("DataFlow.",""))) - self.UDPSettingsDataFlowItems.append("[q] - Back") - - + self.udpsettings_dataflow_items.append(str(flow).replace("DataFlow.","")) + self.udpsettings_dataflow_items.append("[q] - Back") - # Main menu - def MainMenu(self) : - - terminalMenu = TerminalMenu(self.mainMenuItems ,clear_screen=True, title="PyDatalink\n you are using pyDatalink App in Terminal UI mode \n") - menuEntryIndex = terminalMenu.show() - match menuEntryIndex: - case 0 : self.ConfigureMenu() - case 1 : self.Connect_menu() - case 2 : self.ShowData_menu() - case 3 : self.LinkPort_menu() - case 4 : self.Preferences_menu() - case _ : - self.Streams.CloseAll() + def main_menu(self) : + """Main menu of TUI + """ + terminal_menu = TerminalMenu(self.main_menu_items ,clear_screen=True, + title="PyDatalink\n you are using pyDatalink App in Terminal UI mode \n") + menu_entry_index = terminal_menu.show() + match menu_entry_index: + case 0 : self.configure_menu() + case 1 : self.connect_menu() + case 2 : self.showdata_menu() + case 3 : self.link_port_menu() + case 4 : self.preferences_menu() + case _ : + self.app.close_all() sys.exit() - - def ConfigureMenu(self): - terminalMenu = TerminalMenu(self.PortListMenuItems ,clear_screen=False, title="Configuration Menu : \n Change Streams configs \n") - ConfigureMenuEntryIndex = terminalMenu.show() - if ConfigureMenuEntryIndex is None : return self.MainMenu() - if ConfigureMenuEntryIndex >= self.Streams.preferences.maxStreams : - return self.MainMenu() + + def configure_menu(self): + """Configuration menu + """ + terminal_menu = TerminalMenu(self.port_list_menu_items ,clear_screen=False, + title="Configuration Menu : \n Change Streams configs \n") + + configure_menu_entry_index = terminal_menu.show() + if configure_menu_entry_index is None or configure_menu_entry_index >= self.app.preferences.max_streams : + return self.main_menu() else: - selectedPort : PortConfig = self.Streams.StreamList[ConfigureMenuEntryIndex] - if selectedPort.connected : + selected_port : Stream = self.app.stream_list[configure_menu_entry_index] + if selected_port.is_connected : print("This port is currently connected , Disonnect before configuration ! \n") - return self.ConfigureMenu() + return self.configure_menu() else: - return self.ConfigureMenu_SubMenu(selectedPort) - - - - - def Preferences_menu(self): - - - - preference_menu_Items : list = ["[q] - Back"] - - preference_menu_Items.insert(0,"[1] - Configuration File Name - %s" %(self.Streams.preferences.configName)) - preference_menu_Items.insert(1,"[2] - Line Termination - %s" %(str(self.Streams.preferences.getLineTermination()))) - preference_menu_Items.insert(2,"[3] - Max streams- %s" %(self.Streams.preferences.maxStreams)) - preference_menu_Items.insert(3,"[4] - Startup Connect") - - def preferences_configurationFileName(): - print(f"Current file name : {self.Streams.preferences.configName}") + return self.configure_menu_submenu(selected_port) + + def preferences_menu(self): + """Preferences menu + """ + preference_menu_items : list = ["[q] - Back"] + preference_menu_items.insert(0,f"[1] - Configuration File Name - {self.app.preferences.config_name}") + preference_menu_items.insert(1,f"[2] - Line Termination - {str(self.app.preferences.get_line_termination())}") + preference_menu_items.insert(2,f"[3] - Max streams- {self.app.preferences.max_streams}") + preference_menu_items.insert(3,"[4] - Startup connect") + + def preferences_configuration_filename(): + print(f"Current file name : {self.app.preferences.config_name}") print("Enter a new name for the file") newname = input() - self.Streams.preferences.configName= newname - return self.Preferences_menu() - - def preferences_lineTermination(): - print(f"Current line termination {self.Streams.preferences.getLineTermination()}") - terminalMenu = TerminalMenu(["[1] - \\n" , "[2] - \\r" , "[3] - \\r\\n" , "[q] - Back"],clear_screen=False, title="Preferences Menu : Line Termination\n") - lineTerminationMenuEntryIndex = terminalMenu.show() - match lineTerminationMenuEntryIndex : - case 0 : self.Streams.preferences.lineTermination = "\n" - case 1 : self.Streams.preferences.lineTermination = "\r" - case 2 : self.Streams.preferences.lineTermination = "\r\n" - case _ : return self.Preferences_menu() - return self.Preferences_menu() - def preferences_maxStreams(): - print(f"Current max number of stream : {self.Streams.preferences.maxStreams}") - maxStreamList = ["[1] - 1","[2] - 2","[3] - 3","[4] - 4","[5] - 5","[6] - 6","[q] - Back"] - terminalMenu = TerminalMenu(maxStreamList,clear_screen=False, title="Preferences Menu : Max number of stream\n") - maxStreamMenuEntryIndex = terminalMenu.show() - if maxStreamMenuEntryIndex < len(maxStreamList) - 1: - self.Streams.preferences.maxStreams = maxStreamMenuEntryIndex + 1 - return self.Preferences_menu() - - def preferences_StartupConnect(): + self.app.preferences.config_name= newname + return self.preferences_menu() + + def preferences_line_termination(): + print(f"Current line termination {self.app.preferences.get_line_termination()}") + terminal_menu = TerminalMenu(["[1] - \\n" , "[2] - \\r" , "[3] - \\r\\n" , "[q] - Back"], + clear_screen=False, title="Preferences Menu : Line Termination\n") + line_termination_menu_entry_index = terminal_menu.show() + match line_termination_menu_entry_index : + case 0 : self.app.preferences.line_termination = "\n" + case 1 : self.app.preferences.line_termination = "\r" + case 2 : self.app.preferences.line_termination = "\r\n" + case _ : return self.preferences_menu() + return self.preferences_menu() + def preferences_max_streams(): + print(f"Current max number of stream : {self.app.preferences.max_streams}") + max_stream_list = ["[1] - 1","[2] - 2","[3] - 3", + "[4] - 4","[5] - 5","[6] - 6","[q] - Back"] + terminal_menu = TerminalMenu(max_stream_list,clear_screen=False, + title="Preferences Menu : Max number of stream\n") + max_stream_menu_entry_index = terminal_menu.show() + if max_stream_menu_entry_index < len(max_stream_list) - 1: + self.app.preferences.max_streams = max_stream_menu_entry_index + 1 + return self.preferences_menu() + + def preferences_startup_connect(): iterator = 0 - PreferencesStartupconnectMenuItems = [] - for startupConnect in self.Streams.preferences.Connect : - PreferencesStartupconnectMenuItems.append("[%d] - Stream %d - %s" %(iterator,iterator,("True" if startupConnect else "False"))) + preferences_startup_connect_menu_items = [] + for startup_connect in self.app.preferences.connect : + preferences_startup_connect_menu_items.append(f"[{iterator}] - Stream {iterator} - {"True" if startup_connect else "False"}" ) iterator +=1 - PreferencesStartupconnectMenuItems.append("[q] - Back") - terminalMenu = TerminalMenu(PreferencesStartupconnectMenuItems ,clear_screen=False, title="Preferences Menu : Startup Connect\n") - startupConnectMenuEntryIndex = terminalMenu.show() - if startupConnectMenuEntryIndex <= len(self.Streams.preferences.Connect) - 1: - self.Streams.preferences.Connect[startupConnectMenuEntryIndex] = not self.Streams.preferences.Connect[startupConnectMenuEntryIndex] - return preferences_StartupConnect() - return self.Preferences_menu() - - terminalMenu = TerminalMenu(preference_menu_Items, clear_screen=False , title =" Preferences Menu") - preferenceMenuEntryIndex = terminalMenu.show() - match preferenceMenuEntryIndex : - case 0 : return preferences_configurationFileName() - case 1 : return preferences_lineTermination() - case 2 : return preferences_maxStreams() - case 3 : return preferences_StartupConnect() - case _ : return self.MainMenu() - - - - def Connect_menu(self) : - - def connect_menu_select_StreamType (selectedPort : PortConfig): - terminalMenu = TerminalMenu(self.connectDisconectItems ,clear_screen=False, title=" Connect Menu : Stream %i %s %s\n to change the stream type , the stream need to be disconnected\n" %(selectedPort.id,"Connected" if selectedPort.connected else "Disconnected",self.getSettingsTitle(selectedPort) if selectedPort.connected else "") ) - ConfigureMenuEntryIndex = terminalMenu.show() - match ConfigureMenuEntryIndex : - case 0 : - if selectedPort.connected : - print(f"Stream {selectedPort.id} is already connected !") - return self.Connect_menu() - else : - return Connect(selectedPort) - case 1 : return Disconnect(selectedPort) - case _ : return self.Connect_menu() - - def Disconnect(selectedPort : PortConfig): - if selectedPort.connected : - selectedPort.Disconnect() - return self.Connect_menu() - - def Connect(selectedPort : PortConfig): - terminalMenu = TerminalMenu(self.configureStreamTypeMenuItems ,clear_screen=False, title=" Connect Menu : Stream %i %s \n Choose wich type of stream you want\n" %(selectedPort.id,"Connected" if selectedPort.connected else "Disconnected" )) - ConfigureMenuEntryIndex = terminalMenu.show() - - if ConfigureMenuEntryIndex is None : return self.MainMenu() - if ConfigureMenuEntryIndex < len(self.configureStreamTypeMenuItems) - 1 : - try : - selectedPort.Connect(StreamType(ConfigureMenuEntryIndex)) - except Exception as e: - print(f"Connection failed ! : {e}") - return self.Connect_menu() - - self._refreshMenuItems() - terminalMenu = TerminalMenu(self.PortListMenuItems , title="Connect Menu : \n Choose which stream you want to enable or disable\n" ) - ConfigureMenuEntryIndex =terminalMenu.show() - if ConfigureMenuEntryIndex is None : return self.MainMenu() - if ConfigureMenuEntryIndex < self.Streams.preferences.maxStreams : - selectedPort : PortConfig = self.Streams.StreamList[ConfigureMenuEntryIndex] - return connect_menu_select_StreamType(selectedPort) - return self.MainMenu() - - def ShowData_menu(self): - terminalMenu = TerminalMenu(self.PortListMenuItems ,clear_screen=False, title="Show Data Menu : \n Select a stream \n" ,) - showdataMenuEntryIndex = terminalMenu.show() - if showdataMenuEntryIndex is None : return self.MainMenu() - if showdataMenuEntryIndex < self.Streams.preferences.maxStreams : - selectedPort : PortConfig = self.Streams.StreamList[showdataMenuEntryIndex] - settings_title = self.getSettingsTitle(selectedPort) - terminalMenu = TerminalMenu(self.showDataMenuItems ,title =f"Show Data Menu : Stream {selectedPort.id}" +settings_title ) - showdataMenuEntryIndex = terminalMenu.show() - - if showdataMenuEntryIndex is None : - return self.MainMenu() - if showdataMenuEntryIndex >= len(self.showDataMenuItems) - 1 : - return self.ShowData_menu() + preferences_startup_connect_menu_items.append("[q] - Back") + terminal_menu = TerminalMenu(preferences_startup_connect_menu_items ,clear_screen=False, + title="Preferences Menu : Startup connect\n") + startup_connect_menu_entry_index = terminal_menu.show() + if startup_connect_menu_entry_index <= len(self.app.preferences.connect) - 1: + self.app.preferences.connect[startup_connect_menu_entry_index] = not self.app.preferences.connect[startup_connect_menu_entry_index] + return preferences_startup_connect() + return self.preferences_menu() + + terminal_menu = TerminalMenu(preference_menu_items, clear_screen=False , + title =" Preferences Menu") + preference_menu_entry_index = terminal_menu.show() + match preference_menu_entry_index : + case 0 : return preferences_configuration_filename() + case 1 : return preferences_line_termination() + case 2 : return preferences_max_streams() + case 3 : return preferences_startup_connect() + case _ : return self.main_menu() + + + + def connect_menu(self) : + """Connect menu , allow you to connect + """ + + def connect_menu_select_stream_type (selected_port : Stream): + terminal_menu = TerminalMenu(self.connect_disconect_items ,clear_screen=False, + title=f" connect Menu : Stream {selected_port.stream_id} {"Connected" if selected_port.connected else "Disconnected"} {self.get_settings_title(selected_port) if selected_port.connected else ""}\n to change the stream type , the stream need to be disconnected\n") + configure_menu_entry_index = terminal_menu.show() + match configure_menu_entry_index : + case 0 : + if selected_port.connected : + print(f"Stream {selected_port.stream_id} is already connected !") + return self.connect_menu() + else : + return connect(selected_port) + case 1 : return disconnect(selected_port) + case _ : return self.connect_menu() + + def disconnect(selected_port : Stream): + if selected_port.connected : + selected_port.disconnect() + return self.connect_menu() + + def connect(selected_port : Stream): + terminal_menu = TerminalMenu(self.configure_stream_type_menu_items ,clear_screen=False, + title=f"connect Menu : Stream {selected_port.stream_id} {"Connected" if selected_port.connected else "Disconnected"} \n Choose wich type of stream you want\n" ) + configure_menu_entry_index = terminal_menu.show() + + if configure_menu_entry_index is None : + return self.main_menu() + if configure_menu_entry_index < len(self.configure_stream_type_menu_items) - 1 : + try : + selected_port.connect(StreamType(configure_menu_entry_index)) + except StreamException as exc: + print(f"Connection failed ! : {exc}") + return self.connect_menu() + + self._refresh_menu_items() + terminal_menu = TerminalMenu(self.port_list_menu_items , + title="connect Menu : \n Choose which stream you want to enable or disable\n" ) + configure_menu_entry_index =terminal_menu.show() + if configure_menu_entry_index is None : + return self.main_menu() + if configure_menu_entry_index < self.app.preferences.max_streams : + selected_port : Stream = self.app.stream_list[configure_menu_entry_index] + return connect_menu_select_stream_type(selected_port) + return self.main_menu() + + def showdata_menu(self): + """Show data menu + """ + terminal_menu = TerminalMenu(self.port_list_menu_items ,clear_screen=False, + title="Show Data Menu : \n Select a stream \n" ,) + showdata_menu_entry_index = terminal_menu.show() + if showdata_menu_entry_index is None : + return self.main_menu() + if showdata_menu_entry_index < self.app.preferences.max_streams : + selected_port : Stream = self.app.stream_list[showdata_menu_entry_index] + terminal_menu = TerminalMenu(self.show_data_menu_items , + title =f"Show Data Menu : Stream {selected_port.stream_id} {self.get_settings_title(selected_port)}") + showdata_menu_entry_index = terminal_menu.show() + + if showdata_menu_entry_index is None : + return self.main_menu() + if showdata_menu_entry_index >= len(self.show_data_menu_items) - 1 : + return self.showdata_menu() else : - if selectedPort.connected is True : - match showdataMenuEntryIndex : - case 0 : - selectedPort.ToggleInputDataVisibility() - selectedPort.ToggleOutputDataVisibility() + if selected_port.connected is True : + match showdata_menu_entry_index : + case 0 : + selected_port.toggle_incomming_data_visibility() + selected_port.toggle_outgoing_data_visibility() case 1 : - selectedPort.ToggleInputDataVisibility() + selected_port.toggle_incomming_data_visibility() case 2 : - selectedPort.ToggleOutputDataVisibility() - self.stopShowDataEvent.clear() - self.showDataThread = threading.Thread(target=self._showDataTask , args=(selectedPort,)) - self.showDataThread.start() + selected_port.toggle_outgoing_data_visibility() + self.stop_show_data_event.clear() + self.show_data_thread = threading.Thread(target=self._show_data_task , args=(selected_port,)) + self.show_data_thread.start() input() print("Stop showing data") - self.stopShowDataEvent.set() - self.showDataThread.join() - - match showdataMenuEntryIndex : - case 0 : - selectedPort.ToggleInputDataVisibility() - selectedPort.ToggleOutputDataVisibility() + self.stop_show_data_event.set() + self.show_data_thread.join() + + match showdata_menu_entry_index : + case 0 : + selected_port.toggle_incomming_data_visibility() + selected_port.toggle_outgoing_data_visibility() case 1 : - selectedPort.ToggleInputDataVisibility() + selected_port.toggle_incomming_data_visibility() case 2 : - selectedPort.ToggleOutputDataVisibility() - return self.ShowData_menu() - else : + selected_port.toggle_outgoing_data_visibility() + return self.showdata_menu() + else : print("Error : Selected Stream is not connected\n ") - return self.ShowData_menu() - else : - return self.MainMenu() - - def LinkPort_menu(self): - terminalMenu = TerminalMenu(self.PortListMenuItems ,clear_screen=False, title="Link Menu : \n Link output data to a Stream\n" ,) - LinkPortMenuEntryIndex = terminalMenu.show() - if LinkPortMenuEntryIndex is None : return self.MainMenu() - if LinkPortMenuEntryIndex < self.Streams.preferences.maxStreams : - selectedPort = self.Streams.StreamList[LinkPortMenuEntryIndex] - self.LinkPort_link_menu(selectedPort) - return self.MainMenu() - - def LinkPort_link_menu(self , selectedPort : PortConfig): - - def GetAvailableLinkList(selectedPort : PortConfig): - availableLink = [] - for port in self.Streams.StreamList : - if port is not selectedPort: - availableLink.append("[%d] - Stream %d %s" %(port.id,port.id,(" Linked " if port.id in selectedPort.linkedPort else ""))) - else : - availableLink.append("[%d] - Stream %d ( Port can't link itself )" %(port.id,port.id)) - availableLink.append("[q] - Back") - return availableLink - - availableStream = GetAvailableLinkList(selectedPort) - terminalMenu = TerminalMenu( availableStream,clear_screen=False, title="chose Stream for output data\n" ,) - LinkPortMenuEntryIndex = terminalMenu.show() - if LinkPortMenuEntryIndex < len(availableStream)-1 : - if LinkPortMenuEntryIndex is not selectedPort.id : - selectedPort.UpdatelinkedPort(LinkPortMenuEntryIndex) + return self.showdata_menu() + else : + return self.main_menu() + + def link_port_menu(self): + """Link port menu + """ + terminal_menu = TerminalMenu(self.port_list_menu_items ,clear_screen=False, + title="Link Menu : \n Link output data to a Stream\n" ,) + link_port_menu_entry_index = terminal_menu.show() + if link_port_menu_entry_index is None : + return self.main_menu() + if link_port_menu_entry_index < self.app.preferences.max_streams : + selected_port = self.app.stream_list[link_port_menu_entry_index] + self.link_port_link_menu(selected_port) + return self.main_menu() + + def link_port_link_menu(self , selected_port : Stream): + """List of availabel stream to link + """ + + def get_available_link_list(selected_port : Stream): + available_link = [] + for port in self.app.stream_list : + if port is not selected_port: + available_link.append(f"[{port.stream_id}] - Stream {port.stream_id} {" Linked " if port.stream_id in selected_port.linked_ports else ""}") else : - print("not possible !") - return self.LinkPort_link_menu(selectedPort) - return self.LinkPort_menu() - - def ConfigureMenu_SubMenu(self , selectedPort : PortConfig): - terminalMenu = TerminalMenu(self.configureMenuSubmenuItems , clear_screen= False , title=f"Configuration Menu : \n Change Streams {selectedPort.id} configs \n") - ConfigureMenuEntryIndex = terminalMenu.show() - match ConfigureMenuEntryIndex : - case 0 : self.Configure_Stream_StreamType_menu(selectedPort) - case 1 : self.Configure_Stream_Script_menu(selectedPort,True) - case 2 : self.Configure_Stream_Script_menu(selectedPort,False) - case 3 : self.Configure_Stream_Logging_menu(selectedPort) - case _ : return self.ConfigureMenu() - - def Configure_Stream_StreamType_menu(self,selectedPort : PortConfig): - def Configure_Stream_TCP_menu(): - TCPSettings_menu_Items : list = ["[q] - Back"] - - TCPSettings_menu_Items.insert(0,"[1] - Host - %s" %(selectedPort.tcpSettings.host)) - TCPSettings_menu_Items.insert(1,"[2] - Port - %s" %(selectedPort.tcpSettings.port)) - TCPSettings_menu_Items.insert(2,"[3] - Stream Mode - %s" %(selectedPort.tcpSettings.StreamMode.value)) - - TCP_title = f"Configuration Menu : Stream {selectedPort.id} \n Current Configuration : \n Host : {selectedPort.tcpSettings.host} \n Port : {selectedPort.tcpSettings.port}\n StreamMode : {selectedPort.tcpSettings.StreamMode.value}\n " - - def Configure_TCP_Host_menu(): - print(TCP_title) + available_link.append(f"[{port.stream_id}] - Stream {port.stream_id} ( Port can't link itself )") + available_link.append("[q] - Back") + return available_link + + available_stream = get_available_link_list(selected_port) + terminal_menu = TerminalMenu( available_stream,clear_screen=False, + title="chose Stream for output data\n" ,) + link_port_menu_entry_index = terminal_menu.show() + if link_port_menu_entry_index < len(available_stream)-1 : + if link_port_menu_entry_index is not selected_port.stream_id : + selected_port.update_linked_ports(link_port_menu_entry_index) + else : + print("not possible !") + return self.link_port_link_menu(selected_port) + return self.link_port_menu() + + def configure_menu_submenu(self , selected_port : Stream): + """Menu to configure a specific stream + """ + terminal_menu = TerminalMenu(self.configure_menu_submenu_items , clear_screen= False , + title=f"Configuration Menu : \n Change Streams {selected_port.stream_id} configs \n") + configure_menu_entry_index = terminal_menu.show() + match configure_menu_entry_index : + case 0 : self.configure_stream_stream_type_menu(selected_port) + case 1 : self.configure_stream_script_menu(selected_port,True) + case 2 : self.configure_stream_script_menu(selected_port,False) + case 3 : self.configure_stream_logging_menu(selected_port) + case _ : return self.configure_menu() + + def configure_stream_stream_type_menu(self,selected_port : Stream): + """Menu to configure a Stream + """ + def configure_stream_tcp_menu(): + tcp_settings_menu_items : list = ["[q] - Back"] + + tcp_settings_menu_items.insert(0,f"[1] - Host - {selected_port.tcp_settings.host}") + tcp_settings_menu_items.insert(1,f"[2] - Port - {selected_port.tcp_settings.port}") + tcp_settings_menu_items.insert(2,f"[3] - Stream Mode - {selected_port.tcp_settings.stream_mode.value}") + + tcp_title = f"Configuration Menu : Stream {selected_port.stream_id} \n Current Configuration : \n Host : {selected_port.tcp_settings.host} \n Port : {selected_port.tcp_settings.port}\n StreamMode : {selected_port.tcp_settings.stream_mode.value}\n " + + def configure_tcp_host_menu(): + print(tcp_title) print("Enter a valid hostname or IP address") print("note : if in server mode , host will be 127.0.0.1") newhost = input() - try : - if len(newhost) !=0 : + try : + if len(newhost) !=0 : socket.gethostbyname(newhost) - selectedPort.tcpSettings.setHost(newhost) - except Exception as e : + selected_port.tcp_settings.set_host(newhost) + except socket.gaierror: print("Invalid host !") - return Configure_Stream_TCP_menu() - - def Configure_TCP_Port_menu(): - print(TCP_title) + return configure_stream_tcp_menu() + + def configure_tcp_port_menu(): + print(tcp_title) print("Enter a valid port ") - try : + try : newport = int(input()) - selectedPort.tcpSettings.setPort( newport) - except Exception as e : + selected_port.tcp_settings.set_port( newport) + except ValueError: print("Invalid port !") - return Configure_Stream_TCP_menu() - - - def Configure_TCP_StreamMode_menu(): - terminalMenu= TerminalMenu( self.TCPSettingsStreamModeItems,clear_screen=False, title=TCP_title +"Stream mode Configuration\n" ) - menuEntryIndex = terminalMenu.show() - if menuEntryIndex < len(self.TCPSettingsStreamModeItems) - 1: - selectedPort.tcpSettings.set_StreamMode(StreamMode[self.TCPSettingsStreamModeItems[menuEntryIndex]]) - return Configure_Stream_TCP_menu() - - terminalMenu= TerminalMenu(TCPSettings_menu_Items ,clear_screen=False, title="Configuration Menu : Stream %d \n TCP Configuration Menu" %(selectedPort.id) ) - configure_Stream_TCPMenuEntryIndex = terminalMenu.show() - match configure_Stream_TCPMenuEntryIndex: - case 0 : return Configure_TCP_Host_menu() - case 1 : return Configure_TCP_Port_menu() - case 2 : return Configure_TCP_StreamMode_menu() - case _ : return self.Configure_Stream_StreamType_menu(selectedPort) - - def Configure_Stream_Serial_menu(): - - SerialSettings_menu_Items : list = ["[q] - Back"] - - SerialSettings_menu_Items.insert(0,"[1] - Port - %s" %("None" if selectedPort.serialSettings.port is None else selectedPort.serialSettings.port)) - SerialSettings_menu_Items.insert(1,"[2] - BaudRate - %s" %(selectedPort.serialSettings.baudrate.value)) - SerialSettings_menu_Items.insert(2,"[3] - stopBits - %s" %(selectedPort.serialSettings.stopbits.value)) - SerialSettings_menu_Items.insert(3,"[4] - Parity - %s" %(selectedPort.serialSettings.parity.value)) - SerialSettings_menu_Items.insert(4,"[5] - Bytesize - %s" %(selectedPort.serialSettings.bytesize.value)) - SerialSettings_menu_Items.insert(5,"[6] - Rtc-cts - %s" %(selectedPort.serialSettings.rtscts)) + return configure_stream_tcp_menu() + + def configure_tcp_stream_mode_menu(): + terminal_menu= TerminalMenu( self.tcpsettings_stream_mode_items,clear_screen=False, + title=tcp_title +"Stream mode Configuration\n" ) + menu_entry_index = terminal_menu.show() + if menu_entry_index < len(self.tcpsettings_stream_mode_items) - 1: + selected_port.tcp_settings.set_stream_mode(StreamMode[self.tcpsettings_stream_mode_items[menu_entry_index]]) + return configure_stream_tcp_menu() + + terminal_menu= TerminalMenu(tcp_settings_menu_items ,clear_screen=False, + title=f"Configuration Menu : Stream {selected_port.stream_id} \n TCP Configuration Menu") + configure_stream_tpc_menu_entry_index = terminal_menu.show() + match configure_stream_tpc_menu_entry_index: + case 0 : return configure_tcp_host_menu() + case 1 : return configure_tcp_port_menu() + case 2 : return configure_tcp_stream_mode_menu() + case _ : return self.configure_stream_stream_type_menu(selected_port) + + def configure_stream_serial_menu(): + + serial_settings_menu_items : list = ["[q] - Back"] + + serial_settings_menu_items.insert(0,f"[1] - Port - {"None" if selected_port.serial_settings.port is None else selected_port.serial_settings.port}") + serial_settings_menu_items.insert(1,f"[2] - BaudRate - {selected_port.serial_settings.baudrate.value}") + serial_settings_menu_items.insert(2,f"[3] - stopBits - {selected_port.serial_settings.stopbits.value}") + serial_settings_menu_items.insert(3,f"[4] - Parity - {selected_port.serial_settings.parity.value}") + serial_settings_menu_items.insert(4,f"[5] - Bytesize - {selected_port.serial_settings.bytesize.value}") + serial_settings_menu_items.insert(5,f"[6] - Rtc-cts - {selected_port.serial_settings.rtscts}") - Serial_title = f"Configuration Menu : Stream {selectedPort.id} \n Current Configuration : \n Port : {selectedPort.serialSettings.port} \n BaudRate : {selectedPort.serialSettings.baudrate.value}\n StopBits : {selectedPort.serialSettings.stopbits.value}\n Parity : {selectedPort.serialSettings.parity.value}\n Bytesize : {selectedPort.serialSettings.bytesize.value}\n Rtc-cts : {selectedPort.serialSettings.rtscts}\n" + serial_title = f"Configuration Menu : Stream {selected_port.stream_id} \n Current Configuration : \n Port : {selected_port.serial_settings.port} \n BaudRate : {selected_port.serial_settings.baudrate.value}\n StopBits : {selected_port.serial_settings.stopbits.value}\n Parity : {selected_port.serial_settings.parity.value}\n Bytesize : {selected_port.serial_settings.bytesize.value}\n Rtc-cts : {selected_port.serial_settings.rtscts}\n" - def Configure_Serial_Port_menu(): - AvailableStreams = SerialSettings.GetAvailablePort() - AvailableStreams_temps =[] + def configure_serial_port_menu(): + available_streams = selected_port.serial_settings.get_available_port() + available_streams_temps =[] found = False - for AvailablePort in AvailableStreams : - - for port in self.Streams.StreamList: - if port.serialSettings is not None: - if port.serialSettings.port is not None: - if AvailablePort[0] in port.serialSettings.port : + for available_port in available_streams : + + for port in self.app.stream_list: + if port.serial_settings is not None: + if port.serial_settings.port is not None: + if available_port[0] in port.serial_settings.port : found = True - if found == False: - AvailableStreams_temps.append(AvailablePort) - found = False - - iterator = 0 - ConfigureMenu_port_items = ["[d] - Disconnect","[q] - Back"] - if AvailableStreams_temps is not None: - for AvailablePort in AvailableStreams_temps : - ConfigureMenu_port_items.insert(iterator,"[%d] %s - %s" %(iterator + 1,AvailablePort[0],AvailablePort[1])) + if found is False: + available_streams_temps.append(available_port) + found = False + + iterator = 0 + configure_menu_port_items = ["[d] - disconnect","[q] - Back"] + if available_streams_temps is not None: + for available_port in available_streams_temps : + configure_menu_port_items.insert(iterator,f"[{iterator + 1}] {available_port[0]} - {available_port[1]}") iterator +=1 - terminalMenu= TerminalMenu(ConfigureMenu_port_items ,clear_screen=False, title=Serial_title + "Configure Stream's port \n") - menuEntryIndex = terminalMenu.show() + terminal_menu= TerminalMenu(configure_menu_port_items ,clear_screen=False, + title=serial_title + "Configure Stream's port \n") + menu_entry_index = terminal_menu.show() - if menuEntryIndex >= iterator : - if menuEntryIndex == iterator: - selectedPort.serialSettings.setPort("") - return Configure_Stream_Serial_menu() + if menu_entry_index >= iterator : + if menu_entry_index == iterator: + selected_port.serial_settings.set_port("") + return configure_stream_serial_menu() else: - selectedPort.serialSettings.setPort(AvailableStreams_temps[menuEntryIndex][0]) - return Configure_Stream_Serial_menu() - - - def Configure_Serial_BaudRate_menu(): - terminalMenu= TerminalMenu( self.SerialSettingsBaudRateItems,clear_screen=False, title=Serial_title +"Configure Stream's Baudrate \n") - menuEntryIndex = terminalMenu.show() - if menuEntryIndex < len(self.SerialSettingsBaudRateItems) - 1: - selectedPort.serialSettings.set_baudrate(BaudRate( self.SerialSettingsBaudRateItems[menuEntryIndex])) - return Configure_Stream_Serial_menu() - - def Configure_Serial_Parity_menu(): - terminalMenu= TerminalMenu( self.SerialSettingsParityItems,clear_screen=False, title=Serial_title +"Configure Stream's Parity \n") - menuEntryIndex = terminalMenu.show() - if menuEntryIndex < len(self.SerialSettingsParityItems) - 1: - selectedPort.serialSettings.set_parity(Parity["PARITY_"+self.SerialSettingsParityItems[menuEntryIndex]]) - return Configure_Stream_Serial_menu() - - def Configure_Serial_StopBits_menu(): - terminalMenu= TerminalMenu( self.SerialSettingsStopBitsItems,clear_screen=False, title=Serial_title+"Configure Stream's StopBits \n") - menuEntryIndex = terminalMenu.show() - if menuEntryIndex < len(self.SerialSettingsStopBitsItems) - 1: - selectedPort.serialSettings.set_stopbits(StopBits["STOPBITS_" + self.SerialSettingsStopBitsItems[menuEntryIndex]]) - return Configure_Stream_Serial_menu() - - def Configure_Serial_Bytesize_menu(): - terminalMenu= TerminalMenu( self.SerialSettingsBytesizeItems,clear_screen=False, title=Serial_title+"Configure Stream's StopBits \n") - menuEntryIndex = terminalMenu.show() - if menuEntryIndex < len(self.SerialSettingsBytesizeItems) - 1: - selectedPort.serialSettings.set_bytesize(ByteSize[self.SerialSettingsBytesizeItems[menuEntryIndex]]) - return Configure_Stream_Serial_menu() - - def Configure_Serial_RTSCTS_menu(): - if selectedPort.serialSettings.rtscts is True : - selectedPort.serialSettings.set_rtscts(False) + selected_port.serial_settings.set_port(available_streams_temps[menu_entry_index][0]) + return configure_stream_serial_menu() + + + def configure_serial_baudrate_menu(): + terminal_menu= TerminalMenu( self.serialsettings_baudrate_items,clear_screen=False, + title=f"{serial_title} Configure Stream's Baudrate \n") + menu_entry_index = terminal_menu.show() + if menu_entry_index < len(self.serialsettings_baudrate_items) - 1: + selected_port.serial_settings.set_baudrate(BaudRate( self.serialsettings_baudrate_items[menu_entry_index])) + return configure_stream_serial_menu() + + def configure_serial_parity_menu(): + terminal_menu= TerminalMenu( self.serialsettings_parity_items,clear_screen=False, + title=f"{serial_title} Configure Stream's Parity \n") + menu_entry_index = terminal_menu.show() + if menu_entry_index < len(self.serialsettings_parity_items) - 1: + selected_port.serial_settings.set_parity(Parity["PARITY_"+self.serialsettings_parity_items[menu_entry_index]]) + return configure_stream_serial_menu() + + def configure_serial_stopbits_menu(): + terminal_menu= TerminalMenu( self.serialsettings_stopbits_items,clear_screen=False, + title=f"{serial_title} Configure Stream's StopBits \n") + menu_entry_index = terminal_menu.show() + if menu_entry_index < len(self.serialsettings_stopbits_items) - 1: + selected_port.serial_settings.set_stopbits(StopBits["STOPBITS_" + self.serialsettings_stopbits_items[menu_entry_index]]) + return configure_stream_serial_menu() + + def configure_serial_bytesize_menu(): + terminal_menu= TerminalMenu( self.serialsettings_bytesize_items,clear_screen=False, + title=f"{serial_title} Configure Stream's StopBits \n") + menu_entry_index = terminal_menu.show() + if menu_entry_index < len(self.serialsettings_bytesize_items) - 1: + selected_port.serial_settings.set_bytesize(ByteSize[self.serialsettings_bytesize_items[menu_entry_index]]) + return configure_stream_serial_menu() + + def configure_serial_rtscts_menu(): + if selected_port.serial_settings.rtscts is True : + selected_port.serial_settings.set_rtscts(False) else: - selectedPort.serialSettings.set_rtscts(True) - return Configure_Stream_Serial_menu() - - terminalMenu= TerminalMenu(SerialSettings_menu_Items ,clear_screen=False, title="Configuration Menu : Stream %d \n Serial Configuration Menu" %(selectedPort.id) ) - configure_Stream_SerialMenuEntryIndex = terminalMenu.show() - match configure_Stream_SerialMenuEntryIndex: - case 0 : return Configure_Serial_Port_menu() - case 1 : return Configure_Serial_BaudRate_menu() - case 2 : return Configure_Serial_StopBits_menu() - case 3 : return Configure_Serial_Parity_menu() - case 4 : return Configure_Serial_Bytesize_menu() - case 5 : return Configure_Serial_RTSCTS_menu() - case _ : return self.Configure_Stream_StreamType_menu(selectedPort) - - def Configure_Stream_UDP_menu(): - UDPSettings_menu_Items : list =[] - if selectedPort.udpSettings.specificHost: - UDPSettings_menu_Items.insert(0,"[1] - Specific Host - %s" %(selectedPort.udpSettings.host)) - - UDPSettings_menu_Items.insert(1,"[2] - Port - %s" %(selectedPort.udpSettings.port)) - UDPSettings_menu_Items.insert(2,"[3] - Stream to specific Host - %s" %( selectedPort.udpSettings.specificHost )) - UDPSettings_menu_Items.insert(3,"[4] - Data Flow Mode - %s"%(str(selectedPort.udpSettings.DataFlow).replace("DataFlow.","")) ) - UDPSettings_menu_Items.append("[q] - Back") - UDP_title = f"Configuration Menu : Stream {selectedPort.id} \n Current Configuration : \n Host : {selectedPort.udpSettings.host}\n Port : {selectedPort.udpSettings.port}\n Specific Host : {selectedPort.udpSettings.specificHost}\n Data Flow Mode : {str(selectedPort.udpSettings.DataFlow).replace('DataFlow.','')}\n" - - def Configure_UDP_Host_menu(): - print(UDP_title) + selected_port.serial_settings.set_rtscts(True) + return configure_stream_serial_menu() + + terminal_menu= TerminalMenu(serial_settings_menu_items ,clear_screen=False, + title=f"Configuration Menu : Stream {selected_port.stream_id} \n Serial Configuration Menu" ) + configure_stream_serial_menu_entry_index = terminal_menu.show() + match configure_stream_serial_menu_entry_index: + case 0 : return configure_serial_port_menu() + case 1 : return configure_serial_baudrate_menu() + case 2 : return configure_serial_stopbits_menu() + case 3 : return configure_serial_parity_menu() + case 4 : return configure_serial_bytesize_menu() + case 5 : return configure_serial_rtscts_menu() + case _ : return self.configure_stream_stream_type_menu(selected_port) + + def configure_stream_udp_menu(): + udp_settings_menu_items : list =[] + if selected_port.udp_settings.specific_host: + udp_settings_menu_items.insert(0,f"[1] - Specific Host - {selected_port.udp_settings.host}") + + udp_settings_menu_items.insert(1,f"[2] - Port - {selected_port.udp_settings.port}") + udp_settings_menu_items.insert(2,f"[3] - Stream to specific Host - {selected_port.udp_settings.specific_host}") + udp_settings_menu_items.insert(3,f"[4] - Data Flow Mode - {str(selected_port.udp_settings.dataflow).replace("DataFlow.","")}") + udp_settings_menu_items.append("[q] - Back") + udp_title = f"Configuration Menu : Stream {selected_port.stream_id} \n Current Configuration : \n Host : {selected_port.udp_settings.host}\n Port : {selected_port.udp_settings.port}\n Specific Host : {selected_port.udp_settings.specific_host}\n Data Flow Mode : {str(selected_port.udp_settings.DataFlow).replace('DataFlow.','')}\n" + + def configure_udp_host_menu(): + print(udp_title) print("Enter a valid hostname or Ip address") newhost = input() try : if len(newhost) !=0: socket.gethostbyname(newhost) - selectedPort.tcpSettings.setHost(newhost) - except Exception as e : + selected_port.tcp_settings.set_host(newhost) + except socket.gaierror: print("Invalid hostname or IP address !") - return Configure_Stream_UDP_menu() - - def Configure_UDP_Port_menu(): - print(UDP_title) + return configure_stream_udp_menu() + + def configure_udp_port_menu(): + print(udp_title) print("Enter a valid port ") - try : + try : newport = int(input()) if newport is not None: - selectedPort.tcpSettings.setPort( newport) - except Exception as e : + selected_port.tcp_settings.set_port( newport) + except ValueError: print("Invalid port !") - return Configure_Stream_UDP_menu() - - def Configure_UDP_SpecificHost_menu(): - selectedPort.udpSettings.specificHost = False if selectedPort.udpSettings.specificHost else True - return Configure_Stream_UDP_menu() - - def Configure_UDP_DataFlow_menu(): - terminalMenu= TerminalMenu( self.UDPSettingsDataFlowItems,clear_screen=False, title=UDP_title + "Configure Stream's Dataflow\n") - menuEntryIndex = terminalMenu.show() - if menuEntryIndex < len(self.UDPSettingsDataFlowItems) - 1: - selectedPort.udpSettings.DataFlow = DataFlow[self.UDPSettingsDataFlowItems[menuEntryIndex]] - return Configure_Stream_UDP_menu() - - terminalMenu= TerminalMenu(UDPSettings_menu_Items ,clear_screen=False, title="Configuration Menu : Stream %d \n UDP Configuration Menu" %(selectedPort.id) ) - configure_Stream_UDPMenuEntryIndex = terminalMenu.show() - if selectedPort.udpSettings.specificHost is False : configure_Stream_UDPMenuEntryIndex += 1 - - match configure_Stream_UDPMenuEntryIndex: - case 0 : return Configure_UDP_Host_menu() if selectedPort.udpSettings.specificHost else Configure_Stream_UDP_menu() - case 1 : return Configure_UDP_Port_menu() - case 2 : return Configure_UDP_SpecificHost_menu() - case 3 : return Configure_UDP_DataFlow_menu() - case _ : return self.Configure_Stream_StreamType_menu(selectedPort) - - def Configure_Stream_NTRIP_menu(): - NTRIPSettings_menu_Items : list =[] - NTRIPSettings_menu_Items.insert(0,"[1] - Host - %s" %(selectedPort.ntripClient.ntripSettings.host)) - NTRIPSettings_menu_Items.insert(1,"[2] - Port - %s" %(selectedPort.ntripClient.ntripSettings.port)) - NTRIPSettings_menu_Items.insert(2,"[3] - Mountpoint - %s" %(selectedPort.ntripClient.ntripSettings.mountpoint)) - NTRIPSettings_menu_Items.insert(3,"[4] - Authentification - %s" %("Enabled" if selectedPort.ntripClient.ntripSettings.auth else "Disabled")) - NTRIPSettings_menu_Items.insert(4,"[5] - Username - %s" %(selectedPort.ntripClient.ntripSettings.username)) - NTRIPSettings_menu_Items.insert(5,"[6] - Password - %s" %(selectedPort.ntripClient.ntripSettings.password)) - NTRIPSettings_menu_Items.append("[q] - Back") - NTRIP_title = f"Configuration Menu : Stream {selectedPort.id} \n Current Configuration : \n Host : {selectedPort.ntripClient.ntripSettings.host}\n Port : {selectedPort.ntripClient.ntripSettings.port}\n Username : {selectedPort.ntripClient.ntripSettings.username}\n Password : {selectedPort.ntripClient.ntripSettings.password}\n Mountpoint : {selectedPort.ntripClient.ntripSettings.mountpoint}\n" - - def Configure_NTRIP_Host_menu(): - print(NTRIP_title) + return configure_stream_udp_menu() + + def configure_udp_specific_host_menu(): + selected_port.udp_settings.specific_host = False if selected_port.udp_settings.specific_host else True + return configure_stream_udp_menu() + + def configure_udp_dataflow_menu(): + terminal_menu= TerminalMenu( self.udpsettings_dataflow_items,clear_screen=False, title=udp_title + "Configure Stream's Dataflow\n") + menu_entry_index = terminal_menu.show() + if menu_entry_index < len(self.udpsettings_dataflow_items) - 1: + selected_port.udp_settings.dataflow = DataFlow[self.udpsettings_dataflow_items[menu_entry_index]] + return configure_stream_udp_menu() + + terminal_menu= TerminalMenu(udp_settings_menu_items ,clear_screen=False, + title=f"Configuration Menu : Stream {selected_port.stream_id} \n UDP Configuration Menu") + configure_stream_udp_menu_entry_index = terminal_menu.show() + if selected_port.udp_settings.specific_host is False : + configure_stream_udp_menu_entry_index += 1 + + match configure_stream_udp_menu_entry_index: + case 0 : return configure_udp_host_menu() if selected_port.udp_settings.specific_host else configure_stream_udp_menu() + case 1 : return configure_udp_port_menu() + case 2 : return configure_udp_specific_host_menu() + case 3 : return configure_udp_dataflow_menu() + case _ : return self.configure_stream_stream_type_menu(selected_port) + + def configure_stream_ntrip_menu(): + ntrip_settings_menu_items : list =[] + ntrip_settings_menu_items.insert(0,f"[1] - Host - {selected_port.ntrip_client.ntrip_settings.host}") + ntrip_settings_menu_items.insert(1,f"[2] - Port - {selected_port.ntrip_client.ntrip_settings.port}") + ntrip_settings_menu_items.insert(2,f"[3] - Mountpoint - {selected_port.ntrip_client.ntrip_settings.mountpoint}") + ntrip_settings_menu_items.insert(3,f"[4] - Authentification - {"Enabled" if selected_port.ntrip_client.ntrip_settings.auth else "Disabled"}") + ntrip_settings_menu_items.insert(4,f"[5] - Username - {selected_port.ntrip_client.ntrip_settings.username}") + ntrip_settings_menu_items.insert(5,f"[6] - Password - {selected_port.ntrip_client.ntrip_settings.password}") + ntrip_settings_menu_items.append("[q] - Back") + ntrip_title = f"Configuration Menu : Stream {selected_port.stream_id} \n Current Configuration : \n Host : {selected_port.ntrip_client.ntrip_settings.host}\n Port : {selected_port.ntrip_client.ntrip_settings.port}\n Username : {selected_port.ntrip_client.ntrip_settings.username}\n Password : {selected_port.ntrip_client.ntrip_settings.password}\n Mountpoint : {selected_port.ntrip_client.ntrip_settings.mountpoint}\n" + + def configure_ntrip_host_menu(): + print(ntrip_title) print("Enter a valid hostname or Ip address") newhost = input() - try : + try : if len(newhost) !=0: socket.gethostbyname(newhost) - selectedPort.ntripClient.set_Settings_Host(newhost) - except Exception as e : + selected_port.ntrip_client.set_settings_host(newhost) + except socket.gaierror: print("Invalid hostname or IP address !") - return Configure_Stream_NTRIP_menu() - - def Configure_NTRIP_Port_menu(): - print(NTRIP_title) + return configure_stream_ntrip_menu() + + def configure_ntrip_port_menu(): + print(ntrip_title) print("Enter a valid port ") try : newport = int(input()) if newport is not None: - selectedPort.ntripClient.ntripSettings.setPort( newport) - except Exception as e : + selected_port.ntrip_client.ntrip_settings.set_port( newport) + except ValueError: print("Invalid port !") - return Configure_Stream_NTRIP_menu() - - def Configure_NTRIP_Mountpoint_menu(): - print(NTRIP_title) - if selectedPort.ntripClient.ntripSettings.host is not None and selectedPort.ntripClient.ntripSettings.sourceTable is not None: - listofMountPoint = [] - for sourceTable in selectedPort.ntripClient.ntripSettings.sourceTable: - listofMountPoint.append(sourceTable.mountpoint) - listofMountPoint.append("[q] - Back") - terminalMenu = TerminalMenu(listofMountPoint ,clear_screen=False, title="Configuration Menu : Stream %d \n NTRIP Configuration Menu \n Choose a mountpoint \n" %(selectedPort.id) ) - mountPointIndex = terminalMenu.show() - if mountPointIndex < len(listofMountPoint) - 1 : - selectedPort.ntripClient.ntripSettings.setMountpoint(listofMountPoint[mountPointIndex]) - return Configure_Stream_NTRIP_menu() + return configure_stream_ntrip_menu() + + def configure_ntrip_mountpoint_menu(): + print(ntrip_title) + if selected_port.ntrip_client.ntrip_settings.host is not None and selected_port.ntrip_client.ntrip_settings.source_table is not None: + list_of_mountpoint = [] + for source_table in selected_port.ntrip_client.ntrip_settings.source_table: + list_of_mountpoint.append(source_table.mountpoint) + list_of_mountpoint.append("[q] - Back") + terminal_menu = TerminalMenu(list_of_mountpoint ,clear_screen=False, + title=f"Configuration Menu : Stream {selected_port.stream_id} \n NTRIP Configuration Menu \n Choose a mountpoint \n" ) + mountpoint_index = terminal_menu.show() + if mountpoint_index < len(list_of_mountpoint) - 1 : + selected_port.ntrip_client.ntrip_settings.set_mountpoint(list_of_mountpoint[mountpoint_index]) + return configure_stream_ntrip_menu() - def Configure_NTRIP_Username_menu(): - print(NTRIP_title) + def configure_ntrip_username_menu(): + print(ntrip_title) print("Enter a valid username ") - newusername = input() - if len(newusername) !=0: - selectedPort.ntripClient.ntripSettings.setUsername(newusername) - return Configure_Stream_NTRIP_menu() + new_username = input() + if len(new_username) !=0: + selected_port.ntrip_client.ntrip_settings.set_username(new_username) + return configure_stream_ntrip_menu() - def Configure_NTRIP_Password_menu(): - print(NTRIP_title) + def configure_ntrip_password_menu(): + print(ntrip_title) print("Enter a valid password ") - newpassword = input() - if len(newpassword) !=0: - selectedPort.ntripClient.ntripSettings.setPassword(newpassword) - return Configure_Stream_NTRIP_menu() + new_password = input() + if len(new_password) !=0: + selected_port.ntrip_client.ntrip_settings.set_password(new_password) + return configure_stream_ntrip_menu() - def Configure_NTRIP_Auth_menu(): - selectedPort.ntripClient.ntripSettings.auth = False if selectedPort.ntripClient.ntripSettings.auth else True - return Configure_Stream_NTRIP_menu() - - terminalMenu= TerminalMenu(NTRIPSettings_menu_Items ,clear_screen=False, title="Configuration Menu : Stream %d \n NTRIP Configuration Menu" %(selectedPort.id) ) - configure_Stream_UDPMenuEntryIndex = terminalMenu.show() - match configure_Stream_UDPMenuEntryIndex: - case 0 : return Configure_NTRIP_Host_menu() - case 1 : return Configure_NTRIP_Port_menu() - case 2 : return Configure_NTRIP_Mountpoint_menu() - case 3 : return Configure_NTRIP_Auth_menu() - case 4 : return Configure_NTRIP_Username_menu() - case 5 : return Configure_NTRIP_Password_menu() - case _ : return self.Configure_Stream_StreamType_menu(selectedPort) - - - terminalMenu= TerminalMenu(self.configureStreamTypeMenuItems,clear_screen=False , title="Configuration Menu : Stream %d \n select which type of stream you want to configure \n" %(selectedPort.id) ) - configure_StreamMenuEntryIndex = terminalMenu.show() - match configure_StreamMenuEntryIndex: - case 0 : return Configure_Stream_Serial_menu() - case 1 : return Configure_Stream_TCP_menu() - case 2 : return Configure_Stream_UDP_menu() - case 3 : return Configure_Stream_NTRIP_menu() - case _ : return self.ConfigureMenu_SubMenu(selectedPort) - - def Configure_Stream_Script_menu(self, selectedPort : PortConfig , startup : bool): - if startup : - configureScriptMenuItems=[f"[1] - Startup Script - {selectedPort.sendStartupScript}",f"[2] - Script file - {selectedPort.startupScript}","[q] - Back"] + def configure_ntrip_auth_menu(): + selected_port.ntrip_client.ntrip_settings.auth = False if selected_port.ntrip_client.ntrip_settings.auth else True + return configure_stream_ntrip_menu() + + terminal_menu= TerminalMenu(ntrip_settings_menu_items ,clear_screen=False, + title=f"Configuration Menu : Stream {selected_port.stream_id} \n NTRIP Configuration Menu") + configure_stream_udp_menu_entry_index = terminal_menu.show() + match configure_stream_udp_menu_entry_index: + case 0 : return configure_ntrip_host_menu() + case 1 : return configure_ntrip_port_menu() + case 2 : return configure_ntrip_mountpoint_menu() + case 3 : return configure_ntrip_auth_menu() + case 4 : return configure_ntrip_username_menu() + case 5 : return configure_ntrip_password_menu() + case _ : return self.configure_stream_stream_type_menu(selected_port) + + + terminal_menu= TerminalMenu(self.configure_stream_type_menu_items,clear_screen=False, + title=f"Configuration Menu : Stream {selected_port.stream_id} \n select which type of stream you want to configure \n") + configure_stream_menu_entry_index = terminal_menu.show() + match configure_stream_menu_entry_index: + case 0 : return configure_stream_serial_menu() + case 1 : return configure_stream_tcp_menu() + case 2 : return configure_stream_udp_menu() + case 3 : return configure_stream_ntrip_menu() + case _ : return self.configure_menu_submenu(selected_port) + + def configure_stream_script_menu(self, selected_port : Stream , startup : bool): + """add closeup or startup script menu + """ + if startup : + configure_script_menu_items=[f"[1] - Startup Script - {selected_port.send_startup_script}", + f"[2] - Script file - {selected_port.startup_script}","[q] - Back"] else : - configureScriptMenuItems=[f"[1] - Closeup Script - {selectedPort.sendCloseScript}",f"[2] - Script file - {selectedPort.closeScript}","[q] - Back"] - def Configure_Script(): - if startup : - selectedPort.setStartupScript() - else : - selectedPort.setCloseScript() - return self.Configure_Stream_Script_menu(selectedPort , startup) - - def Configure_ScriptFile_menu(): - print("Enter the path to the Script file") - newpath = input() - try : - if len(newpath) !=0 : - if startup : - selectedPort.setStartupScriptPath(newpath) - else : - selectedPort.setCloseScriptPath(newpath) - except Exception as e : - print(f"Invalid path ! , {e}") - return self.Configure_Stream_Script_menu(selectedPort , startup) - - terminalMenu = TerminalMenu(configureScriptMenuItems,clear_screen=False, title="") - choice_index = terminalMenu.show() - match choice_index : - case 0 : return Configure_Script() - case 1 : return Configure_ScriptFile_menu() - case _ : return self.ConfigureMenu() - - def Configure_Stream_Logging_menu(self,selectedPort : PortConfig ): - - configureLoggingMenuItems=[f"[1] - Logging - {str(selectedPort.logging)}",f"[2] - Logging Filename - {selectedPort.loggingFile}","[q] - Back"] - - def Configure_logging(): - selectedPort.setLogging() - return self.Configure_Stream_Logging_menu(selectedPort) - def Configure_Logging_FileName_menu(): - print("Enter the path to the Script file") - newpath = input() - try : - if len(newpath) !=0 : - selectedPort.setLoggingFileName(newpath) - except Exception as e : - print(f"Invalid path ! , {e}") - return self.Configure_Stream_Logging_menu(selectedPort) - - terminalMenu = TerminalMenu(configureLoggingMenuItems, title="") - choice_index = terminalMenu.show() - match choice_index : - case 0 : return Configure_logging() - case 1 : return Configure_Logging_FileName_menu() - case _ : return self.ConfigureMenu() - - - - - def getSettingsTitle(self, selectedPort : PortConfig): - currentSettings = selectedPort.toString() - if currentSettings is None : + configure_script_menu_items=[f"[1] - Closeup Script - {selected_port.send_close_script}", + f"[2] - Script file - {selected_port.close_script}", + "[q] - Back"] + def configure_script(): + if startup : + selected_port.set_startup_script() + else : + selected_port.set_close_script() + return self.configure_stream_script_menu(selected_port , startup) + + def configure_script_file_menu(): + print("Enter the path to the Script file") + new_path = input() + try : + if len(new_path) !=0 : + if startup : + selected_port.set_startup_script_path(new_path) + else : + selected_port.set_close_script_path(new_path) + except ScriptFileException as exc : + print(f"Invalid path ! , {exc}") + return self.configure_stream_script_menu(selected_port , startup) + + terminal_menu = TerminalMenu(configure_script_menu_items,clear_screen=False, title="") + choice_index = terminal_menu.show() + match choice_index : + case 0 : return configure_script() + case 1 : return configure_script_file_menu() + case _ : return self.configure_menu() + + def configure_stream_logging_menu(self,selected_port : Stream ): + """Add logging file for logging + """ + + configure_logging_menu_items=[f"[1] - Logging - {str(selected_port.logging)}", + f"[2] - Logging Filename - {selected_port.logging_file}", + "[q] - Back"] + + def configure_logging(): + selected_port.set_logging() + return self.configure_stream_logging_menu(selected_port) + def configure_logging_file_name_menu(): + print("Enter the path to the Script file") + new_path = input() + try : + if len(new_path) !=0 : + selected_port.set_logging_file_name(new_path) + except LogFileException as exc : + print(f"Invalid path ! , {exc}") + return self.configure_stream_logging_menu(selected_port) + + terminal_menu = TerminalMenu(configure_logging_menu_items, title="") + choice_index = terminal_menu.show() + match choice_index : + case 0 : return configure_logging() + case 1 : return configure_logging_file_name_menu() + case _ : return self.configure_menu() + + def get_settings_title(self, selected_port : Stream): + """Return the current configuration of the stream + """ + current_settings = selected_port.to_string() + if current_settings is None : return "\n No settings" - else : - return f"\n Current Settings : \n {currentSettings} \n" \ No newline at end of file + else : + return f"\n Current Settings : \n {current_settings} \n" diff --git a/src/UserInterfaces/__init__.py b/src/UserInterfaces/__init__.py index 2bc5d5f..e69de29 100644 --- a/src/UserInterfaces/__init__.py +++ b/src/UserInterfaces/__init__.py @@ -1 +0,0 @@ -from .TerminalUserInterface import TerminalUserInterface \ No newline at end of file diff --git a/src/constants.py b/src/constants.py index 6eb766d..8be218c 100644 --- a/src/constants.py +++ b/src/constants.py @@ -26,19 +26,29 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os +import logging +import os import datetime - +import sys + +if getattr(sys, 'frozen', False): + PROJECTPATH = sys._MEIPASS + DATAFILESPATH = os.path.join(PROJECTPATH, "data" ) +else: + PROJECTPATH = os.path.abspath(os.path.dirname(__file__)).replace("\\src","") #Path to the project folder + DATAFILESPATH = os.path.join(PROJECTPATH , "src" , "Data Files" ) + APPNAME = "PyDataLink" -PROJECTPATH = os.path.abspath(os.path.dirname(__file__)) #Path to the project folder -MAINSCRIPTPATH = PROJECTPATH + "pyDatalink.py" -DATAFILESPATH = PROJECTPATH + "/Data Files/" -DATAPATH = os.path.expanduser("~") + "/.septentrio" # Path to the PyDataLink Data Folder -CONFIGPATH = DATAPATH + "/confs" # Path to the Configuration folder -LOGFILESPATH = DATAPATH + "/logs" # Path to the Logs folder -DEFAULTCONFIGFILE = CONFIGPATH + "/pydatalink.conf" # Path to the default configuration file +MAINSCRIPTPATH = os.path.join(PROJECTPATH , "pyDatalink.py") +MAXFILENUMBER = 20 +DATAPATH = os.path.join(os.path.expanduser("~") , ".septentrio") # Path to the PyDataLink Data Folder +CONFIGPATH = os.path.join(DATAPATH , "confs" ) # Path to the Configuration folder +LOGFILESPATH = os.path.join(DATAPATH ,"logs") # Path to the Logs folder +DEFAULTCONFIGFILE = os.path.join(CONFIGPATH ,"pydatalink.conf") # Path to the default configuration file +# Create logging file for the app now = datetime.datetime.now() -filename = now.strftime("pyDatalink_%Y-%m-%d_%H-%M-%S.log") - -DEFAULTLOGFILE = LOGFILESPATH + "/" + filename # Path to the log file \ No newline at end of file +filename = now.strftime("pyDatalink_%Y-%m-%d_%H-%M.log") +DEFAULTLOGFILEPATH = LOGFILESPATH + "\\" + filename +logging.basicConfig(filename= DEFAULTLOGFILEPATH, encoding='utf-8', level=logging.DEBUG, format='[%(asctime)s] %(levelname)s : %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') +DEFAULTLOGFILELOGGER = logging.getLogger("PyDatalink") diff --git a/user/README.md b/user/README.md index 6bb448c..d1c600a 100644 --- a/user/README.md +++ b/user/README.md @@ -43,6 +43,9 @@ This guide explains how to use pyDatalink software. It comes with 3 types of int - Terminal Interface These interfaces are used to configure connections, launch them and, if necessary, link them together. +
+ +
# Installation As pyDatalink app is entirely developed with python, you must first install python and all its dependencies. ## Install Python @@ -68,8 +71,8 @@ Once you've installed python, all you have to do is download the source code and ### Using git clone ``` -git clone https://github.com/septentrio-gnss/DataLink.git -cd DataLink +git clone https://github.com/septentrio-gnss/Septentrio-PyDataLink.git +cd Septentrio-PyDataLink ``` ### Using GitHub - First click on **code**.
@@ -86,7 +89,7 @@ pip install -r requirements.txt ``` # Graphical Interface
- +

The graphical interface is the interface that allows you to perform the most tasks. It gives you access to the full range of pydatalink functions. @@ -105,11 +108,11 @@ The graphical interface is the interface that allows you to perform the most tas Use the following command to use pyDatalink in Graphical Interface mode ``` -python pyDatalinkApp.py -m GUI +python pyDatalink.py -m GUI ``` or ``` -python pyDatalinkApp.py +python pyDatalink.py ``` # Command Line Interface
@@ -158,10 +161,15 @@ ntrip://[user]:[pwd]@[adrr]:[port]/[mountpoint]#[linkport] The details of the different values for the configuration for a NTRIP connection are available further down in the document : [NTRIP Settings](#ntrip-settings) ### Example In this exemple we create 2 serial stream that are inter connected +#### Unix ``` -python DatalinkApp.py --Mode CMD --Streams serial:///dev/ttyACM0:115200:n:1:8:0#1 serial:///dev/ttyACM1:115200:n:1:8:0#0 +python pyDatalink.py --Mode CMD --Streams serial:///dev/ttyACM0:115200:n:1:8:0#1 serial:///dev/ttyACM1:115200:n:1:8:0#0 ``` +#### Windows +``` +python pyDatalink.py --Mode CMD --Streams serial://COM7:115200:n:1:8:0#1 serial://COM6:115200:n:1:8:0#0 +``` # Terminal Interface
@@ -172,13 +180,13 @@ The project propose a menu-based TUI, providing a semi-graphical way to configur To start the Data link in TUI mode ``` -python pyDatalinkApp.py -m TUI +python pyDatalink.py -m TUI ``` On this menu you can find the following items : * [Configuration](#Configure) -* [Connect / Disconnect](#Connect-Disconnect) +* [connect / disconnect](#connect-disconnect) * [ShowData](#Show-Data) * [Link](#Link) * [Preferences](#Preferences) @@ -224,7 +232,7 @@ The Configure menu allow you to choose the type of communication you want to use | MountPoint | *Contact your ntrip service provider* | - | Mountpoint of the ntrip server , depend on the provider | | -## Connect Disconnect +## connect disconnect In this section, you can start or stop a connection. To connect or disconnect, simply select a connection then select with type of stream oyu want to use and press `enter`. If the connection fails, a message will be displayed above the menu indicating the issue that needs to be fixed before retrying. If the connection is successful, the status next to the connection name will switch between "Connected" and "Disconnected". diff --git a/user/doc_sources/pyDatalink.PNG b/user/doc_sources/pyDatalink.PNG index 86f58f2..c4cd001 100644 Binary files a/user/doc_sources/pyDatalink.PNG and b/user/doc_sources/pyDatalink.PNG differ diff --git a/user/doc_sources/pyDatalink_GUI.PNG b/user/doc_sources/pyDatalink_GUI.PNG new file mode 100644 index 0000000..f8851a9 Binary files /dev/null and b/user/doc_sources/pyDatalink_GUI.PNG differ