1515from sys import platform , exit , argv
1616from glob import glob
1717from src import action_ui
18+ import re
1819# Runtime Type Checking
1920PROGRAM_TYPE_DEBUG = True
2021PROGRAM_TYPE_RELEASE = False
@@ -83,6 +84,37 @@ def get_serial_port():
8384 pass
8485 return result
8586
87+ def strip_ansi_codes (text ):
88+ ansi_escape = re .compile (r'\x1B\[[0-?]*[ -/]*[@-~]' )
89+ return ansi_escape .sub ('' , text )
90+
91+ def ansi_to_html (text ):
92+ # Improved ANSI to HTML conversion with support for bold and combined codes
93+ ansi_color_map = {
94+ '30' : 'black' , '31' : 'red' , '32' : 'green' , '33' : 'yellow' ,
95+ '34' : 'blue' , '35' : 'magenta' , '36' : 'cyan' , '37' : 'white' ,
96+ '90' : 'gray' , '91' : 'lightcoral' , '92' : 'lightgreen' , '93' : 'lightyellow' ,
97+ '94' : 'lightblue' , '95' : 'violet' , '96' : 'lightcyan' , '97' : 'white'
98+ }
99+ def ansi_repl (match ):
100+ codes = match .group (1 ).split (';' )
101+ styles = []
102+ for code in codes :
103+ if code == '1' :
104+ styles .append ('font-weight:bold;' )
105+ elif code in ansi_color_map :
106+ styles .append (f'color:{ ansi_color_map [code ]} ;' )
107+ if styles :
108+ return f'<span style="{ '' .join (styles )} ">'
109+ elif '0' in codes :
110+ return '</span>'
111+ return ''
112+ # Open/close spans for each ANSI code
113+ text = re .sub (r'\x1b\[([0-9;]+)m' , ansi_repl , text )
114+ # Remove any remaining ANSI codes
115+ text = re .sub (r'\x1b\[[0-9;]*[A-Za-z]' , '' , text )
116+ return text
117+
86118# MULTI-THREADING
87119class Worker (QObject ):
88120 """ Worker Thread """
@@ -94,18 +126,22 @@ def __init__(self):
94126 self .working = True
95127
96128 def work (self ):
97- """ Read data from serial port """
129+ """ Read data from serial port, emit full lines """
130+ buffer = ''
98131 while self .working :
99132 try :
100- char = SERIAL_DEVICE .read ()
101- h = char .decode ('utf-8' )
102- self .serial_data .emit (h )
133+ char = SERIAL_DEVICE .read ().decode ('utf-8' , errors = 'replace' )
134+ buffer += char
135+ if '\n ' in buffer :
136+ lines = buffer .split ('\n ' )
137+ for line in lines [:- 1 ]:
138+ self .serial_data .emit (line + '\n ' )
139+ buffer = lines [- 1 ]
103140 except SerialException as e :
104141 print (e )
105- # Emit last error message before die!
106142 self .serial_data .emit ("ERROR_SERIAL_EXCEPTION" )
107143 self .working = False
108- self .finished .emit ()
144+ self .finished .emit ()
109145
110146class MainWindow (QMainWindow ):
111147 """ Main Window """
@@ -173,6 +209,11 @@ def __init__(self):
173209 self .ui .actionHelp_2 .triggered .connect (action_ui .show_help_dialog )
174210 self .ui .actionPreferences .triggered .connect (action_ui .show_settings_dialog )
175211
212+ # Night mode button event
213+ self .night_mode_enabled = False
214+ if hasattr (self .ui , 'nightMode_Button' ):
215+ self .ui .nightMode_Button .clicked .connect (self .enable_night_mode )
216+
176217 '''
177218 def command1(self):
178219 """ Open the text input popup to save command for button 1 """
@@ -367,11 +408,7 @@ def on_stop_button_clicked(self):
367408 self .ui .status_label .setText ("DISCONNECTED!" )
368409 self .ui .status_label .setStyleSheet ('color: red' )
369410 self .enable_configuration (True )
370-
371- # Safely stop the worker thread
372- if hasattr (self , 'worker' ) and self .worker is not None :
373- self .worker .working = False
374-
411+ self .stop_worker_thread ()
375412 # Safely close the serial device
376413 global SERIAL_DEVICE
377414 try :
@@ -380,6 +417,15 @@ def on_stop_button_clicked(self):
380417 except Exception as e :
381418 print (f"Error closing serial port: { e } " )
382419
420+ def stop_worker_thread (self ):
421+ # Safely stop the worker thread and wait for it to finish
422+ if hasattr (self , 'worker' ) and self .worker is not None :
423+ self .worker .working = False
424+ if hasattr (self , 'thread' ) and self .thread is not None :
425+ if self .thread .isRunning ():
426+ self .thread .quit ()
427+ self .thread .wait (2000 ) # wait up to 2 seconds
428+
383429 def enable_configuration (self , state ):
384430 """ enable/disable the configuration """
385431 self .ui .timeout_comboBox .setEnabled (state )
@@ -389,40 +435,71 @@ def enable_configuration(self, state):
389435 self .ui .parity_comboBox .setEnabled (state )
390436 self .ui .bit_comboBox .setEnabled (state )
391437 self .ui .flow_comboBox .setEnabled (state )
438+ self .ui .lineEnd_comboBox .setEnabled (state )
392439
393440 def read_data_from_thread (self , serial_data ):
394441 """ Write the result to the text edit box"""
395- # self.ui.data_textEdit.append("{}".format(i))
396442 if "ERROR_SERIAL_EXCEPTION" in serial_data :
397443 self .print_message_on_screen (
398444 "Serial Port Exception! Please check the serial port"
399445 " Possibly it is not connected or the port is not available!" )
400446 self .on_stop_button_clicked ()
401447 else :
402- serial_data = serial_data .replace ('\n ' , '' )
403- self .ui .data_textEdit .insertPlainText ("{}" .format (serial_data ))
448+ # Replace CRLF and LF with <br> for HTML display
449+ html_data = ansi_to_html (serial_data .replace ('\r \n ' , '<br>' ).replace ('\n ' , '<br>' ))
450+ self .ui .data_textEdit .insertHtml (html_data )
404451 self .ui .data_textEdit .verticalScrollBar ().setValue (
405452 self .ui .data_textEdit .verticalScrollBar ().maximum ())
406453
407454 def on_send_data_button_clicked (self ):
408455 """ Send data to serial port """
409456 if is_serial_port_established :
410457 self .ui .indicate_button .setChecked (True )
411- mytext = self .ui .send_data_text .toPlainText ().encode ('utf-8' )
458+ # Clear end of line, spaces, and tabs from end of the text
459+ mytext = self .ui .send_data_text .toPlainText ().strip ().encode ('utf-8' )
460+ # Get line ending from combo box
461+ line_end = self .ui .lineEnd_comboBox .currentText ()
462+ if line_end == "CR" :
463+ line_ending = b'\r '
464+ elif line_end == "LF" :
465+ line_ending = b'\n '
466+ elif line_end == "CRLF" :
467+ line_ending = b'\r \n '
468+ else :
469+ line_ending = b''
412470 SERIAL_DEVICE .flushOutput () # Flush output buffer
413- SERIAL_DEVICE .write (mytext + b' \r \n ' )
471+ SERIAL_DEVICE .write (mytext + line_ending )
414472
415473 # Uncheck after 5 seconds without blocking the UI
416474 QTimer .singleShot (500 , lambda : self .ui .indicate_button .setChecked (False ))
417475 else :
418476 self .print_message_on_screen (
419477 "Serial Port is not established yet! Please establish the serial port first!" )
420-
478+
479+ def enable_night_mode (self ):
480+ """Toggle night/day mode for data_textEdit"""
481+ if not hasattr (self , 'night_mode_enabled' ):
482+ self .night_mode_enabled = False
483+ if not self .night_mode_enabled :
484+ self .ui .data_textEdit .setStyleSheet ("background-color: black;" )
485+ if hasattr (self .ui .nightMode_Button , 'setText' ):
486+ self .ui .nightMode_Button .setText ("Day Mode" )
487+ self .night_mode_enabled = True
488+ else :
489+ self .ui .data_textEdit .setStyleSheet ("" )
490+ if hasattr (self .ui .nightMode_Button , 'setText' ):
491+ self .ui .nightMode_Button .setText ("Night Mode" )
492+ self .night_mode_enabled = False
493+
494+ def closeEvent (self , event ):
495+ # Properly stop the worker and thread before closing
496+ self .stop_worker_thread ()
497+ event .accept ()
421498
422499def start_ui_design ():
423500 """ Start the UI Design """
424501 app = QApplication (argv ) # Create an instance
425502 window_object = MainWindow () # Create an instance of our class
426503 if PROGRAM_TYPE_RELEASE :
427504 window_object .show ()
428- exit (app .exec ())
505+ exit (app .exec ())
0 commit comments