Skip to content

Commit 84edab2

Browse files
author
Mehmet Aksoy
committed
Improves serial communication and UI features
Enhances the serial communication by implementing line-by-line processing and fixing encoding issues. It introduces ANSI code conversion to HTML for rich text display in the UI. A night mode toggle is added to enhance user experience, along with line ending selection and worker thread management for robust serial port handling. Also stops worker thread when the main window is closed.
1 parent 50bb42d commit 84edab2

File tree

2 files changed

+156
-41
lines changed

2 files changed

+156
-41
lines changed

src/ui_main.py

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from sys import platform, exit, argv
1616
from glob import glob
1717
from src import action_ui
18+
import re
1819
# Runtime Type Checking
1920
PROGRAM_TYPE_DEBUG = True
2021
PROGRAM_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
87119
class 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

110146
class 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

422499
def 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())

ui/main_window.ui

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>1127</width>
10-
<height>633</height>
9+
<width>1026</width>
10+
<height>597</height>
1111
</rect>
1212
</property>
1313
<property name="sizePolicy">
@@ -35,7 +35,8 @@
3535
<string>AFCOM Client (A free COM port data transfer client)</string>
3636
</property>
3737
<property name="windowIcon">
38-
<iconset theme="applications-development"/>
38+
<iconset theme="applications-development">
39+
<normaloff>.</normaloff>.</iconset>
3940
</property>
4041
<property name="windowOpacity">
4142
<double>1.000000000000000</double>
@@ -56,7 +57,7 @@
5657
<number>1</number>
5758
</property>
5859
<property name="sizeConstraint">
59-
<enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
60+
<enum>QLayout::SetDefaultConstraint</enum>
6061
</property>
6162
<item>
6263
<widget class="QRadioButton" name="indicate_button">
@@ -95,27 +96,28 @@
9596
<string>Push Data</string>
9697
</property>
9798
<property name="icon">
98-
<iconset theme="mail-send"/>
99+
<iconset theme="mail-send">
100+
<normaloff>.</normaloff>.</iconset>
99101
</property>
100102
</widget>
101103
</item>
102104
</layout>
103105
</item>
104106
<item row="0" column="1">
105-
<layout class="QVBoxLayout" name="verticalLayout_config" stretch="2,0,0,0,0">
107+
<layout class="QVBoxLayout" name="verticalLayout_config" stretch="2,0,0,0,0,0">
106108
<property name="spacing">
107109
<number>6</number>
108110
</property>
109111
<property name="sizeConstraint">
110-
<enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
112+
<enum>QLayout::SetDefaultConstraint</enum>
111113
</property>
112114
<item>
113115
<layout class="QFormLayout" name="formLayout_config">
114116
<property name="labelAlignment">
115-
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
117+
<set>Qt::AlignLeading</set>
116118
</property>
117119
<property name="formAlignment">
118-
<set>Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter</set>
120+
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
119121
</property>
120122
<property name="horizontalSpacing">
121123
<number>4</number>
@@ -142,7 +144,8 @@
142144
<string/>
143145
</property>
144146
<property name="icon">
145-
<iconset theme="sync-synchronizing"/>
147+
<iconset theme="sync-synchronizing">
148+
<normaloff>.</normaloff>.</iconset>
146149
</property>
147150
</widget>
148151
</item>
@@ -435,8 +438,46 @@
435438
</item>
436439
</widget>
437440
</item>
441+
<item row="8" column="1">
442+
<widget class="QComboBox" name="lineEnd_comboBox">
443+
<item>
444+
<property name="text">
445+
<string>CRLF</string>
446+
</property>
447+
</item>
448+
<item>
449+
<property name="text">
450+
<string>LF</string>
451+
</property>
452+
</item>
453+
<item>
454+
<property name="text">
455+
<string>CR</string>
456+
</property>
457+
</item>
458+
<item>
459+
<property name="text">
460+
<string>None</string>
461+
</property>
462+
</item>
463+
</widget>
464+
</item>
465+
<item row="8" column="0">
466+
<widget class="QLabel" name="label">
467+
<property name="text">
468+
<string>Line Ending:</string>
469+
</property>
470+
</widget>
471+
</item>
438472
</layout>
439473
</item>
474+
<item>
475+
<widget class="QPushButton" name="nightMode_Button">
476+
<property name="text">
477+
<string>Night Mode</string>
478+
</property>
479+
</widget>
480+
</item>
440481
<item>
441482
<widget class="QLabel" name="label_12">
442483
<property name="text">
@@ -453,7 +494,7 @@
453494
</size>
454495
</property>
455496
<property name="frameShape">
456-
<enum>QFrame::Shape::NoFrame</enum>
497+
<enum>QFrame::NoFrame</enum>
457498
</property>
458499
</widget>
459500
</item>
@@ -465,6 +506,7 @@
465506
<font>
466507
<family>Arial</family>
467508
<pointsize>9</pointsize>
509+
<weight>50</weight>
468510
<bold>false</bold>
469511
</font>
470512
</property>
@@ -479,6 +521,7 @@
479521
<font>
480522
<family>Arial</family>
481523
<pointsize>9</pointsize>
524+
<weight>50</weight>
482525
<bold>false</bold>
483526
</font>
484527
</property>
@@ -508,6 +551,7 @@
508551
<property name="font">
509552
<font>
510553
<pointsize>9</pointsize>
554+
<weight>75</weight>
511555
<bold>true</bold>
512556
</font>
513557
</property>
@@ -532,13 +576,10 @@
532576
<bool>false</bool>
533577
</property>
534578
<property name="frameShape">
535-
<enum>QFrame::Shape::Box</enum>
536-
</property>
537-
<property name="frameShadow">
538-
<enum>QFrame::Shadow::Sunken</enum>
579+
<enum>QFrame::NoFrame</enum>
539580
</property>
540581
<property name="autoFormatting">
541-
<set>QTextEdit::AutoFormattingFlag::AutoNone</set>
582+
<set>QTextEdit::AutoNone</set>
542583
</property>
543584
<property name="readOnly">
544585
<bool>true</bool>
@@ -560,13 +601,10 @@
560601
</size>
561602
</property>
562603
<property name="inputMethodHints">
563-
<set>Qt::InputMethodHint::ImhNone</set>
604+
<set>Qt::ImhNone</set>
564605
</property>
565606
<property name="frameShape">
566-
<enum>QFrame::Shape::Box</enum>
567-
</property>
568-
<property name="frameShadow">
569-
<enum>QFrame::Shadow::Sunken</enum>
607+
<enum>QFrame::NoFrame</enum>
570608
</property>
571609
<property name="acceptRichText">
572610
<bool>false</bool>
@@ -585,8 +623,8 @@
585623
<rect>
586624
<x>0</x>
587625
<y>0</y>
588-
<width>1127</width>
589-
<height>33</height>
626+
<width>1026</width>
627+
<height>21</height>
590628
</rect>
591629
</property>
592630
<widget class="QMenu" name="menuFile">

0 commit comments

Comments
 (0)