PyELSSA 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
PyELSSA/master.py ADDED
@@ -0,0 +1,87 @@
1
+ #
2
+ # -*- coding: utf-8 -*-
3
+ #=================================================================
4
+ # Created by: Jieming Ye
5
+ # Created on: Aug 2026
6
+ # Last Modified: Aug 2026
7
+ #=================================================================
8
+ # Copyright (c) 2026 [Jieming Ye]
9
+ #
10
+ # This Python source code is licensed under the
11
+ # Open Source Non-Commercial License (OSNCL) v1.0
12
+ # See LICENSE for details.
13
+ #=================================================================
14
+ """
15
+ Pre-requisite:
16
+ N/A
17
+ Used Input:
18
+ N/A
19
+ Expected Output:
20
+ Start GUI
21
+ Description:
22
+ This module is the package entry.
23
+ This module is the only module to be called from User End script directly.
24
+ It ensures GUI is only created in the main process while allowing subprocesses
25
+ to be spawned for other tasks.
26
+ """
27
+ #=================================================================
28
+ # VERSION CONTROL
29
+ # V1.0 - 2026.08.20 - Jieming Ye - Initial Version
30
+ #=================================================================
31
+
32
+ import multiprocessing
33
+ import sys
34
+
35
+ def is_main_process():
36
+ """Check if current process is the main process."""
37
+ return multiprocessing.current_process().name == 'MainProcess'
38
+
39
+ def initialize_gui():
40
+ """Initialize the GUI if license check passes."""
41
+ try:
42
+ # STAGE 1: license check
43
+ from PyELSSA import licensing
44
+ if not licensing.main():
45
+ input("Press any key to exit.....")
46
+ return False
47
+
48
+ # STAGE 2: Release message
49
+ from PyELSSA import release_update
50
+ if not release_update.main():
51
+ input("Press any key to exit.....")
52
+ return False
53
+
54
+ # STAGE 3: configuration check
55
+ from PyELSSA import configuration
56
+ if not configuration.main():
57
+ input("Press any key to exit.....")
58
+ return False
59
+
60
+ # STAGE 4: Launch the GUI
61
+ from PyELSSA.gui import gui_start
62
+ app = gui_start.SampleApp()
63
+ app.mainloop()
64
+ return True
65
+
66
+ except Exception as e:
67
+ print(f"FATAL ERROR: {e}")
68
+ input("Press any key to exit.....")
69
+ return False
70
+
71
+ def main():
72
+ """Main entry point for the application."""
73
+ if not is_main_process():
74
+ # If this is a subprocess, just return without creating GUI
75
+ # have this under this main as this master is usually called from another entering script
76
+ return
77
+
78
+ # Set up multiprocessing to work with frozen executables if needed
79
+ if getattr(sys, 'frozen', False):
80
+ multiprocessing.freeze_support()
81
+
82
+ print('Loading Application...')
83
+ # Initialize GUI in main process only
84
+ initialize_gui()
85
+
86
+ if __name__ == '__main__':
87
+ main()
@@ -0,0 +1,59 @@
1
+ #=================================================================
2
+ # Created by: Jieming Ye
3
+ # Created on: June 2026
4
+ # Last Modified: June 2026
5
+ #=================================================================
6
+ # Copyright (c) 2026 [Jieming Ye]
7
+ #
8
+ # This Python source code is licensed under the
9
+ # Open Source Non-Commercial License (OSNCL) v1.0
10
+ # See LICENSE for details.
11
+ #=================================================================
12
+ """
13
+ only run first time after user update the software
14
+
15
+ # migrate_old_settings()
16
+ # rebuild_cache()
17
+ # show_release_notes()
18
+
19
+ """
20
+ #=================================================================
21
+ # VERSION CONTROL
22
+ # V1.0 (Jieming Ye) - Initial Version
23
+ #=================================================================
24
+ # Set Information Variable
25
+ # N/A
26
+ #=================================================================
27
+
28
+ import os
29
+ import json
30
+ from datetime import date
31
+
32
+ from PyELSSA.shared_contents import SharedVariables, SharedMethods
33
+ from PyELSSA import configuration
34
+
35
+ def main():
36
+ # REQUIRE CHANGE FOR EACH VERSION
37
+ expiry_date = date(2026,8,21)
38
+
39
+ if date.today() <= expiry_date or configuration.version_updated():
40
+ SharedMethods.print_message(f"#############################################################\n","33")
41
+ SharedMethods.print_message(f"Welcome to the new version of ELSSA on Python tool\n","32")
42
+
43
+ # SharedMethods.print_message(f"New features in this version {SharedVariables.installed_version} include:", "32")
44
+ # SharedMethods.print_message(f"* XXXXXXXXXXXXXXX.", "32")
45
+
46
+ # SharedMethods.print_message("Fixed bugs and others include:", "32")
47
+ # SharedMethods.print_message("* Fixed an issue of ....... ", "32")
48
+
49
+ # SharedMethods.print_message(f"This version fixed multiple bugs reported by the user...", "32")
50
+ SharedMethods.print_message(f"\n#############################################################","33")
51
+
52
+ if configuration.version_updated():
53
+ # specific requirement for version update only
54
+ return True
55
+
56
+ return True
57
+
58
+ if __name__ == "__main__":
59
+ main()
@@ -0,0 +1,491 @@
1
+ #
2
+ # -*- coding: utf-8 -*-
3
+ #=================================================================
4
+ # Created by: Jieming Ye
5
+ # Created on: Aug 2026
6
+ # Last Modified: Aug 2026
7
+ #=================================================================
8
+ # Copyright (c) 2026 [Jieming Ye]
9
+ #
10
+ # This Python source code is licensed under the
11
+ # Open Source Non-Commercial License (OSNCL) v1.0
12
+ # See LICENSE for details.
13
+ #=================================================================
14
+ """
15
+ DataFilenames:
16
+ - Database filename lists
17
+
18
+ Common Variable:
19
+ - Global variable to be used among all scripts
20
+
21
+ Common Methods:
22
+ - Global methods to be used among all scripts
23
+
24
+ Internet Validation:
25
+ - For NR Internet configuration checking only
26
+
27
+ Tee:
28
+ - Consolo control
29
+
30
+ """
31
+ #=================================================================
32
+ # VERSION CONTROL
33
+ # V1.0 (Jieming Ye) - Initial Version
34
+ #
35
+ #
36
+ #=================================================================
37
+ # Set Information Variable
38
+ # N/A
39
+ #=================================================================
40
+
41
+
42
+ #import tkinter as tk
43
+ import os
44
+ import shutil
45
+ import importlib
46
+ import importlib.resources
47
+ import importlib.metadata
48
+ import csv
49
+ import subprocess
50
+ import sys
51
+ import tempfile
52
+ import requests
53
+ import platform
54
+ from datetime import datetime
55
+ from contextlib import contextmanager
56
+
57
+ from collections import Counter
58
+
59
+ class SharedVariables:
60
+ '''
61
+ This class will hold all shared varibles
62
+ '''
63
+ # this class will store all shared varibles
64
+ sim_variable = None # get when be called
65
+ elssa_version = 1 # default update at gui.gui_start.py
66
+ used_database_path = 2 # default update at gui.gui_start.py (would be string either database_path_user or databse_path_default)
67
+
68
+ # varible to be updated following version upgrade:
69
+ # Replace 'your_package_name' with the actual name of your package
70
+ package_name = 'PyELSSA'
71
+ support_name = 'support'
72
+ private_data_name = 'data'
73
+ package_path = importlib.resources.files(package_name)
74
+ elssa_core_path = os.path.join(package_path,"core")
75
+ try:
76
+ installed_version = importlib.metadata.version(package_name)
77
+ except:
78
+ installed_version = '0.0.0'
79
+ public_data_path = "https://github.com/NR-ESTractionPower/public_database"
80
+ raw_data_url_begin = "https://raw.githubusercontent.com/NR-ESTractionPower/public_database/refs/heads/main/"
81
+
82
+ lastupdate = 'Aug / 2026' # date of checking all links below
83
+ copyright = 'CopyRight @ 2026, All Rights Reserved.'
84
+ status_note = '(Beta) PyELSSA is under active development. User is expected to see big changes with this package.'
85
+
86
+ # bhtpbank_path = 'C:\\Users\\Public\\Documents\\VISION\\Resources\\bhtpbank'
87
+ database_path_user = 'C:\\Users\\Public\\Documents\\PyELSSA\\user_data' # This is user controlled library
88
+ database_path_default = 'C:\\Users\\Public\\Documents\\PyELSSA\\default_data' # This is forced default data library
89
+ configuration_path = 'C:\\Users\\Public\\Documents\\PyELSSA\\user_config' # This is configuration data library
90
+
91
+ contacts = "Email: 'traction.power@networkrail.co.uk'"
92
+ license_online = "https://raw.githubusercontent.com/NR-ESTractionPower/vo_addin/refs/heads/main/vision_oslo_extension_license.txt"
93
+ # support_online = "https://github.com/NR-ESTractionPower/vo_addin"
94
+ issue_online = "https://github.com/NR-ESTractionPower/ELSSA_python/issues"
95
+
96
+ license_file = os.path.join(configuration_path, "license.json")
97
+ config_file = os.path.join(configuration_path, "config.json")
98
+ current_path = None # get current path
99
+ admin_password = "passwordS3F3" # this is the Mac password
100
+
101
+ class DataFilenames:
102
+ '''
103
+ This class holds the database file name as in two lists.
104
+ These two list will be checked at the very beginning of application lauching
105
+ Any new dataset added in the future needs to be added to this comprehensive data list.
106
+ public_file list are files hold on the GitHub public repository
107
+ private_file list are files hold locally in data folder
108
+
109
+ This class also contains the default configuration settings
110
+ '''
111
+ public_file = {
112
+ 'tiploc_library': 'TestFile.txt'
113
+ }
114
+
115
+ private_file = {
116
+ 'private_file1': 'TestFile1.csv',
117
+ 'private_file2': 'TestFile2.csv'
118
+ }
119
+
120
+ default_config = {
121
+ 'package_version_number': SharedVariables.installed_version,
122
+ 'default_database_updated': False,
123
+ 'elssa_core_updated': False,
124
+ 'elssa_version': 1, # default to 1
125
+ 'database_location': 2 # default to user location
126
+ }
127
+
128
+ # Define a dictionary mapping import_option to module names
129
+ module_mapping = {
130
+ "cif_prepare.py": "cif_prepare",
131
+ "cif_duplicates.py": "cif_duplicates",
132
+ "model_check.py": "model_check",
133
+ "oslo_extraction.py": "oslo_extraction",
134
+ }
135
+
136
+ class SharedMethods:
137
+ '''
138
+ This class will hold all shared methods
139
+ '''
140
+ # check if the script running in debug mode or not
141
+ @staticmethod
142
+ def is_debug_mode():
143
+ # Checks if the debugger is attached by inspecting system flags
144
+ return (sys.gettrace() is not None or "debugpy" in sys.modules)
145
+
146
+ # context manager to change working directory and revert back
147
+ @contextmanager
148
+ def pushd(path):
149
+ prev = os.getcwd()
150
+ os.chdir(path)
151
+ try:
152
+ yield
153
+ finally:
154
+ os.chdir(prev)
155
+
156
+ # copy files from source folder to active entry
157
+ @staticmethod
158
+ def copy_example_files(filename):
159
+ distribution = importlib.resources.files(SharedVariables.package_name)
160
+ # Get the path to the package
161
+ #package_path = distribution.location + "\\" + SharedVariables.package_name
162
+ package_path = os.path.join(str(distribution), SharedVariables.support_name)
163
+
164
+ # Get the absolute path of the file in the package location
165
+ file_in_package = os.path.join(package_path, filename)
166
+ current_path = os.getcwd() # get current path
167
+
168
+ check_file = os.path.join(current_path, filename)
169
+
170
+ if os.path.exists(check_file):
171
+ print(f"File '{filename}' already exists in the current working directory. Skipping copy...")
172
+ else:
173
+ # Copy the file to the current working directory
174
+ shutil.copy(file_in_package,current_path)
175
+ print(f"File '{filename}' copied to the current working directory. Config as required...")
176
+
177
+ # check data library files, return full path if found
178
+ @staticmethod
179
+ def check_data_files(filename):
180
+ # Get the absolute path of the file in the package location
181
+ if not SharedVariables.used_database_path:
182
+ SharedVariables.used_database_path = SharedVariables.database_path_default
183
+ SharedMethods.print_message(f"WARNING: Database file path not set. Refer to Default. Contact support to report the issue...","33")
184
+ file_in_package = os.path.join(SharedVariables.used_database_path, filename)
185
+
186
+ if os.path.exists(file_in_package):
187
+ print(f"Data file '{filename}' exist in the Data library. Reading will be done.")
188
+ return file_in_package
189
+ else:
190
+ SharedMethods.print_message(f"WARNING: Data file '{filename}' does NOT exist in the Data library. Reading will be skipped.","33")
191
+ SharedMethods.print_message(f"ATTENTION: You can add data file '{filename}' to {SharedVariables.used_database_path} and report it to support.","33")
192
+ return False
193
+
194
+ # check if a file is in use or not
195
+ @staticmethod
196
+ def can_write_file(filename):
197
+ """Return True if the file can be opened for writing; False if Excel has it open."""
198
+ filepath = os.path.join(os.getcwd(), filename)
199
+ if not os.path.exists(filepath):
200
+ # File doesn't exist → safe to write
201
+ return True
202
+ try:
203
+ # Try opening in append+binary mode (does not read file)
204
+ with open(filepath, "r+b"):
205
+ pass
206
+ SharedMethods.print_message(f"WARNING: File {filename} could be overwritten at a later stage...", "33")
207
+ return True
208
+ except (OSError, PermissionError):
209
+ # File is locked by Excel or another process
210
+ return False
211
+
212
+ #check existing file
213
+ @staticmethod
214
+ def check_existing_file(filename,printing = True):
215
+ """
216
+ Check if a specific file exists.
217
+ Returns True if the file exists, False otherwise.
218
+ if printing is True, it will print message information.
219
+ """
220
+ print(f"Checking File {filename}...")
221
+
222
+ first = filename.split('.')[0]
223
+ if first == "":
224
+ SharedMethods.print_message("ERROR: Select the simulation or required file to continue...","31")
225
+ return False
226
+
227
+ current_path = os.getcwd() # get current path
228
+ file_path = os.path.join(current_path,filename) # join the file path
229
+ if not os.path.isfile(file_path): # if the oof file does not exist
230
+ if printing:
231
+ SharedMethods.print_message(f"ERROR: Required file {filename} does not exist. Checking required...","31")
232
+ return False
233
+ return True
234
+
235
+ # check the folder and file for summary
236
+ @staticmethod
237
+ def folder_file_check(subfolder,filename=None,required=True):
238
+ """
239
+ Check if a specific file exists in a given subfolder.
240
+ Returns True if the file exists, False otherwise.
241
+ if required is True, it will print an error message and exit if the file does not exist.
242
+ """
243
+ print(f"Checking File {filename} in {subfolder}...")
244
+ current_path = os.getcwd() # get current path
245
+
246
+ # Create the complete folder path
247
+ folder_path = os.path.join(current_path, subfolder)
248
+
249
+ # Check if the folder exists
250
+ if not os.path.exists(folder_path):
251
+ if required:
252
+ SharedMethods.print_message(f"ERROR: Required folder {subfolder} does not exist. Check your Input. Exiting...","31")
253
+ return False
254
+
255
+ if filename != None: #if filename is not None, check the file existance in the folder
256
+ # file path
257
+ file_path = os.path.join(folder_path,filename) # join the file path
258
+ # print(file_path)
259
+ if not os.path.isfile(file_path):
260
+ if required:
261
+ SharedMethods.print_message(f"ERROR: Required file {filename} does not exist at {subfolder}. Check your Input. Exiting...","31")
262
+ return False
263
+
264
+ return True
265
+
266
+ # copy the file to a subfolder / if not exist, create the subfolder
267
+ @staticmethod
268
+ def copy_file_to_subfolder(subfolder, filename, new_filename=None):
269
+ print(f"Copying File {filename} to {subfolder}...")
270
+ current_path = os.getcwd() # Get current path
271
+ folder_path = os.path.join(current_path, subfolder)
272
+
273
+ # Create the folder if it doesn't exist
274
+ if not os.path.exists(folder_path):
275
+ try:
276
+ os.makedirs(folder_path)
277
+ print(f"Folder '{subfolder}' created.")
278
+ except Exception as e:
279
+ SharedMethods.print_message(f"ERROR: Error creating folder {subfolder}: {e}. Check your Input...", "31")
280
+ return False
281
+
282
+ # Determine the target file name
283
+ target_filename = new_filename if new_filename else filename
284
+ file_path = os.path.join(folder_path, target_filename)
285
+
286
+ # Warn if the target file already exists
287
+ if os.path.isfile(file_path):
288
+ SharedMethods.print_message(f"WARNING: File {target_filename} already exists in {subfolder}. Overwriting...", "33")
289
+ os.remove(file_path)
290
+
291
+ # Copy the file
292
+ try:
293
+ shutil.copy(filename, file_path)
294
+ print(f"File '{filename}' copied to subfolder successfully'.")
295
+ except Exception as e:
296
+ SharedMethods.print_message(f"ERROR: Error copying file {filename} to {file_path}: {e}. Check your Input...", "31")
297
+ return False
298
+
299
+ return True
300
+
301
+ # open a file in support folder
302
+ @staticmethod
303
+ def open_support_file(filename):
304
+ distribution = importlib.resources.files(SharedVariables.package_name)
305
+ package_path = os.path.join(str(distribution), SharedVariables.support_name)
306
+ # Get the absolute path of the file in the package location
307
+ file_in_package = os.path.join(package_path, filename)
308
+
309
+ # create a temp directory and copy the file there
310
+ temp_dir = tempfile.gettempdir()
311
+ temp_file = os.path.join(temp_dir, filename)
312
+
313
+ try:
314
+ shutil.copy(file_in_package, temp_file) # copy the file to temp directory
315
+ subprocess.Popen(['start', '', temp_file], shell=True,close_fds=True)
316
+ except Exception as e:
317
+ SharedMethods.print_message(f"ERROR: Error opening file with default app: {e}","31")
318
+
319
+ return
320
+
321
+ # add unique key to a ditionary type
322
+ @staticmethod
323
+ def add_unique_key(dictionary, key, value):
324
+ original_key = key
325
+ counter = 1
326
+ while key in dictionary:
327
+ key = f"{original_key}_{counter}"
328
+ counter += 1
329
+ SharedMethods.print_message(f"WARNING: Name {original_key} already exists. Trying to rename to {key}...","33")
330
+ dictionary[key] = value
331
+ return dictionary
332
+
333
+ # delete all simulation result:
334
+ def clean_up_simulation_folder():
335
+ '''
336
+ This function deletes all files in the current working directory except those with specific suffixes.
337
+ '''
338
+ reserved_file_suffix = ['.vvw','.ocl','.vcn','extra.oslo','extra.bat.oslo','pdv.csv','.xlsx','xlsm']
339
+ # delete all files in current working directory if file name not ending as in the list
340
+ current_path = os.getcwd() # get current path
341
+ for filename in os.listdir(current_path):
342
+ if not any(filename.endswith(suffix) for suffix in reserved_file_suffix):
343
+ file_path = os.path.join(current_path, filename)
344
+ try:
345
+ if os.path.isfile(file_path) or os.path.islink(file_path):
346
+ os.remove(file_path) # remove the file or link
347
+ elif os.path.isdir(file_path):
348
+ shutil.rmtree(file_path) # remove the directory and its contents
349
+ print(f"INFO: File '{filename}' been deleted.")
350
+ except Exception as e:
351
+ SharedMethods.print_message(f"ERROR: Failed to delete {file_path}. Reason: {e}", "31")
352
+ print("INFO: Simulation Folder Clean Up Completed.")
353
+
354
+ # define the running in thread mechanism
355
+ def launch_new_thread_or_process(import_option,
356
+ text_input1, text_input2, option_input1, option_input2, time_input1, time_input2, add_text, add_input1, add_input2,config_input,
357
+ cwd=None):
358
+ '''
359
+ This function launches a new thread or process to run the specified module's main function.
360
+ '''
361
+
362
+ if cwd:
363
+ # Change the current working directory to the specified path
364
+ # this is compolsory for new process
365
+ os.chdir(cwd)
366
+
367
+ # Get the module name corresponding to import_option
368
+ module_name = DataFilenames.module_mapping.get(import_option)
369
+
370
+ # Import the module
371
+ if module_name:
372
+ fc = importlib.import_module(f"{SharedVariables.package_name}.{module_name}")
373
+ #from vision_oslo_extension import module_name as fc
374
+ else:
375
+ # Handle the case when import_option doesn't match any module
376
+ SharedMethods.print_message(f"ERROR: Invalid import_option: {import_option}", "31")
377
+
378
+ try:
379
+ print(f"\nLauching module in new thread (same console): {module_name}\n")
380
+ continue_process = fc.main(text_input1,text_input2,option_input1,option_input2,time_input1,time_input2,add_text,add_input1,add_input2,config_input)
381
+ if not continue_process:
382
+ # Do something if the process should not continue
383
+ # Print error message in red
384
+ SharedMethods.print_message("ERROR: Process terminated due to captured issue. "
385
+ "Please check the error history above or contact support. "
386
+ "You can continue using other options...", "31")
387
+ return False
388
+
389
+ else:
390
+ # Do something if the process should continue
391
+ # Print success message in green
392
+ SharedMethods.print_message("Action successfully completed. "
393
+ "Check monitor history above and result files in your folder.", "32")
394
+
395
+ except Exception as e:
396
+ SharedMethods.print_message(f"ERROR: UNEXPECTED! PLEASE REPORT BUG AND CONTACT SUPPORT... ", "31")
397
+ SharedMethods.print_message(f"ERROR: source code module - {import_option}: {e}","31")
398
+ return False
399
+
400
+ return True
401
+
402
+ def print_message(message: str, color_code: str):
403
+ '''
404
+ print out message in color in front end
405
+ color_code following ANSI color code
406
+ '''
407
+ color_start = f'\033[1;{color_code}m' # Start color
408
+ color_reset = '\033[1;0m' # Reset color
409
+ print(color_start + message + color_reset)
410
+
411
+ class InternetValidation:
412
+ '''
413
+ This class contains methods to validate internet connection and check for restricted networks.
414
+ '''
415
+ def internet_check(url):
416
+ '''Check if a handshake with internet can be established or not. To decide if an upgrade is possible or not'''
417
+
418
+ # check connected network to see if it is NR network or not
419
+ if InternetValidation.check_restricted_network():
420
+ SharedMethods.print_message(f"WARNING: Abort internet connection due to Network Rail private network detected.","33")
421
+ return False
422
+
423
+ try:
424
+ # Send a HEAD request using requests with a timeout of 5 seconds
425
+ response = requests.get(url, timeout=5)
426
+ return True
427
+
428
+ except requests.exceptions.Timeout:
429
+ SharedMethods.print_message("ERROR: Request timed out after 5 seconds. POOR INTERNET...", "31")
430
+ return False
431
+
432
+ except Exception as e:
433
+ SharedMethods.print_message(f"ERROR: Unexpected connection: {e}. Please contact the support...","31")
434
+ return False
435
+
436
+ def check_restricted_network():
437
+ '''Return TRUE if connected to the NR Office Environment'''
438
+ system = platform.system()
439
+ try:
440
+ if system == "Windows":
441
+ # WIFI Network Check
442
+ result = subprocess.run(["netsh", "wlan", "show", "interfaces"],
443
+ capture_output=True, text=True, check=True)
444
+ for line in result.stdout.splitlines():
445
+ if "SSID" in line and "BSSID" not in line:
446
+ ssid = line.split(":", 1)[1].strip()
447
+ if ssid == "14":
448
+ # print("NR CORP WIFI 14 DETECTED.")
449
+ return True
450
+ else:
451
+ break
452
+ # Ehternet Network Check
453
+ block = False
454
+ result = subprocess.run(["ipconfig", "/all"],
455
+ capture_output=True, text=True, check=True)
456
+ for line in result.stdout.splitlines():
457
+ if "adapter" in line.lower() and "Ethernet" in line:
458
+ block = True
459
+ if block:
460
+ # skip the check if the ethernet cable is not used
461
+ if "media disconnected" in line.lower():
462
+ # this means WIFI not 14 and Ethernet cable is not connected
463
+ return False
464
+ type = line.split(":", 1)[0].strip().lower()
465
+ if "default gateway" in type:
466
+ gatewayip = line.split(":", 1)[1].strip()
467
+ if gatewayip.startswith("10.176."): # This IP is only used for small private network
468
+ # print("NR CORP ETHERNET CONNECTION DETECTED.")
469
+ return True
470
+ else:
471
+ return False
472
+ else: # not ready for other operating system yet
473
+ return False
474
+ except Exception as e:
475
+ SharedMethods.print_message(f"ERROR: Unexpected error: {e}. Please contact the support...","31")
476
+ return False
477
+ return False
478
+
479
+ class Tee:
480
+ """Write to multiple streams (console + file)."""
481
+ def __init__(self, *streams):
482
+ self.streams = streams
483
+
484
+ def write(self, data):
485
+ for stream in self.streams:
486
+ stream.write(data)
487
+ stream.flush()
488
+
489
+ def flush(self):
490
+ for stream in self.streams:
491
+ stream.flush()