#!/usr/bin/env python3
"""
    File:    Tauno-Serial-Plotter.py
    Author:  Tauno Erik
    Started: 07.03.2020
    Edited:  10.01.2026
"""
import sys
import re
import os
import logging
import time
import serial
import serial.tools.list_ports
from PyQt6 import QtWidgets, QtCore, QtGui
from PyQt6.QtCore import Qt, QRunnable, QThreadPool
from PyQt6.QtWidgets import (QApplication, QHBoxLayout, QVBoxLayout,
                            QLabel, QWidget, QMessageBox)
import pyqtgraph as pg
import platform

VERSION = '1.20.4'
TIMESCALESIZE = 400  # = self.plot_timescale and self.plot_data_size

stop_port_scan = False # To kill port scan thread when sys.exit

# Set debuge level
logging.basicConfig(level=logging.DEBUG)
#logging.basicConfig(level=logging.CRITICAL)


# GUI Icons
if platform.system() == 'Windows' :
    icon_logo = "./icons/tauno-plotter.svg"
    icon_minus = "./icons/minus.svg"
    icon_plus = "./icons/plus.svg"
    icon_arrow_down = "./icons/arrow_down.svg"
    icon_about = "./icons/help-about-symbolic.svg"
    icon_clean = "./icons/larger-brush-symbolic.svg"
    icon_size = "./icons/ruler-end-horizontal-left-symbolic.svg"
else:
    icon_logo = '/usr/share/icons/hicolor/scalable/apps/tauno-plotter.svg'
    icon_minus = '/usr/share/icons/hicolor/scalable/apps/tauno-plotter-minus.svg'
    icon_plus = '/usr/share/icons/hicolor/scalable/apps/tauno-plotter-plus.svg'
    icon_arrow_down = '/usr/share/icons/hicolor/scalable/apps/tauno-plotter-arrow_down.svg'
    icon_about = '/usr/share/icons/hicolor/scalable/apps/tauno-plotter-help-about-symbolic.svg'
    icon_clean = '/usr/share/icons/hicolor/scalable/apps/tauno-plotter-larger-brush-symbolic.svg'
    icon_size = '/usr/share/icons/hicolor/scalable/apps/tauno-plotter-ruler-end-horizontal-left-symbolic.svg'

# GUI colours
colors =  {
    'oranz':"#FF6F00",
    'green':"#9CCC65",
    'dark' :"#263238",
    'hall' :"#B0BEC5",
	'black':"#212121"
}

# PLOT colors
plot_colors = [
    "#ba8310", # Kollane
    "#00BCD4", # syan
    "#3F51B5", # indigo
    "#E91E63", # pink
    "#FF9800", # orange
    "#9C27B0", # purple
    "#4CAF50", # green
    "#FFC107", # amber
    "#f44336", # red
    "#03A9F4", # light blue
    "#FFEB3B", # yellow
    "#CDDC39", # lime
    "#2196F3", # blue
    "#8BC34A", # light green
    "#009688"  # teal
    ]

# STYLING
FONTSIZE = '16px'
BORDER_RADIUS = '5px'

# Graph style
pg.setConfigOptions(antialias=True)
pg.setConfigOption('background', colors['dark'])
pg.setConfigOption('foreground', colors['hall'])

btn_icon_style = f"""
QPushButton{{
    color: {colors['black']};
    background-color: {colors['hall']};
    border: 1px solid {colors['dark']};
    border-radius: {BORDER_RADIUS};
    padding: 5px;
    margin-top: 0px;
    font: {FONTSIZE};
}}

QPushButton::hover{{
    background-color: {colors['green']};
    color: {colors['black']};
}}

QPushButton::pressed{{
    border: 1px solid {colors['oranz']};
    background-color: {colors['hall']};
}}"""

btn_icon_style_disabled = f"""
QPushButton{{
    color: {colors['black']};
    background-color: {colors['dark']};
    border: 1px solid {colors['dark']};
    padding: 5px;
    margin-top: 0px;
    font: {FONTSIZE};
}}
"""

btn_style = f"""
QPushButton{{
    color: {colors['black']};
    background-color: {colors['hall']};
    border: 1px solid {colors['black']};
    border-radius: {BORDER_RADIUS};
    padding: 5px;
    margin-top: 0px;
    font: {FONTSIZE};
}}

QPushButton::hover{{
    background-color: {colors['green']};
    color: {colors['black']};
}}

QPushButton::pressed{{
    border: 1px solid {colors['oranz']};
    background-color: {colors['hall']};
}}"""

btn_style_disabled = f"""
QPushButton{{
    color: {colors['black']};
    background-color: {colors['dark']};
    border: 1px solid {colors['black']};
    padding: 5px;
    margin-top: 0px;
    font: {FONTSIZE};
}}
"""

label_style = f"""
QLabel{{
    color: {colors['hall']};
    font: {FONTSIZE};
    margin-top: 0px;
}}
"""

Qinfo_text_style = f"""
QLabel{{
    color: {colors['hall']};
    font: {FONTSIZE};
    margin-top: 0px;
}}
"""


dropdown_style = f"""
QComboBox:editable, QComboBox{{
    background-color: {colors['hall']};
    color: {colors['black']};
    border: 1px solid {colors['black']};
    border-radius: {BORDER_RADIUS};
    padding: 5px 25p 5px 5px;
    font: {FONTSIZE};
}}

QComboBox::hover{{
    background-color: {colors['green']};
    color: {colors['black']}; /* tekst*/
}}

QComboBox:editable:on, QComboBox:on {{ /* shift the text when the popup opens */
    padding-left: 10px;
    background-color: {colors['green']};
    color: {colors['dark']};
}}

QComboBox::drop-down {{ /* shift the text when the popup opens */
    background-color: {colors['dark']}; /* noole tagune */
    color: {colors['green']};
    width: 24px;
}}

QComboBox::down-arrow {{
    background-color: {colors['dark']};/* nool */
    image: url({icon_arrow_down});
    width: 24px;
    height: 24px;
}}
"""

dropdown_style_disabled = f"""
QComboBox:editable, QComboBox{{
    background-color: {colors['dark']};
    color: {colors['black']};
    border: 1px solid {colors['black']};
    border-radius: {BORDER_RADIUS};
    padding: 5px 25p 5px 5px;
    font: {FONTSIZE};
}}

QComboBox::drop-down {{ /* shift the text when the popup opens */
    background-color: {colors['dark']}; /* noole tagune */
    color: {colors['green']};
    width: 24px;
}}

QComboBox::down-arrow {{
    background-color: {colors['dark']};/* nool */
    image: url({icon_arrow_down});
    width: 24px;
    height: 24px;
}}
"""


QDoubleSpinBox_style = f"""
QDoubleSpinBox{{
    background-color: {colors['hall']};
    color: {colors['black']};
    border: 1px solid {colors['black']};
    border-radius: {BORDER_RADIUS};
    padding: 0px; 
    font: {FONTSIZE};
}}

QDoubleSpinBox::hover{{
    background-color: {colors['green']};
    color: {colors['black']}; /* tekst*/
}}

QDoubleSpinBox::up-button{{
    subcontrol-origin: border;
    subcontrol-position: top right;
    background-color: {colors['dark']};
    border: 1px solid {colors['black']};
    border-top-right-radius: {BORDER_RADIUS};
    border-bottom-right-radius: {BORDER_RADIUS};
    width: 25px;
    height:25px;
    margin:0px;
    /*padding-bottom: 1px;*/
}}

QDoubleSpinBox::down-button{{
    subcontrol-origin: border;
    background-color: {colors['dark']};
    subcontrol-position: top left;
    border: 1px solid {colors['black']};
    border-top-left-radius: {BORDER_RADIUS};
    border-bottom-left-radius: {BORDER_RADIUS};
    width: 25px;
    height:25px;
    /*padding-bottom: 1px;*/
}}

QDoubleSpinBox::up-arrow {{
    image: url({icon_plus});
    width: 25px;
    height: 25px;
}}

QDoubleSpinBox::down-arrow {{
    image: url({icon_minus});
    width: 25px;
    height: 25px;
}}

"""

# 1. Subclass QRunnable
# https://realpython.com/python-pyqt-qthread/
# https://www.learnpyqt.com/tutorials/multithreading-pyqt-applications-qthreadpool/
class ForeverWorker(QRunnable):
    """
    It put function to run forever on background.
    I use it to scan the avaible serial ports.
    """
    def __init__(self, fn):
        super(ForeverWorker, self).__init__()
        self.fn = fn
        self.is_working = True

    def __del__(self):
        self.is_working = False #True
        #self.wait()

    def run(self):
        """ Forever running task """
        while self.is_working:
            logging.debug("ForeverWorker.Run while loop")
            self.fn()
            time.sleep(10) # seconds
            if stop_port_scan:
               self.is_working = False



class Plot(pg.GraphicsLayoutWidget):
    """ Plot definition """
    def __init__(self, nr_plot_lines='1', labels=["sensor1"]):
        super(Plot,self).__init__(parent=None)

        self.nr_plot_lines = nr_plot_lines
        self.data_labels =labels

        if self.nr_plot_lines is None:
            logging.debug("nr_plot_lines is None!")

        logging.debug("Init Plot class. With %i plot lines.", self.nr_plot_lines)

        # Create plot
        self.serialplot = self.addPlot()
        self.serialplot.setLabel('left', 'Data')
        self.serialplot.setLabel('bottom', 'Time')
        self.serialplot.showGrid(x=True, y=True)
        self.serialplot.addLegend()

        # Place to hold data
        self.x_axis = [0]  # Time
        # generate list of lists, incoming data
        self.y_axis = [[0] for i in range(nr_plot_lines)] # Datas

        # List of all data lines
        self.data_lines = []

        for i in range(self.nr_plot_lines):
            if i >= len(plot_colors):
                # If we have more data than colors
                color_i = i - len(plot_colors)
            else:
                color_i = i

            pen = pg.mkPen(color=(plot_colors[color_i]), width=3)

            brush = pg.mkBrush(color=(plot_colors[color_i]))
            # Quick fix:
            # https://github.com/taunoe/tauno-serial-plotter/issues/71#issuecomment-1769499968
            if len(self.data_labels) == len(self.y_axis):
                line = self.serialplot.plot(x=self.x_axis, y=self.y_axis[i], name=self.data_labels[i],
                                       pen=pen, symbol='o', symbolBrush=brush, symbolSize=3)
            else:
                line = self.serialplot.plot(x=self.x_axis, y=self.y_axis[i],
                                       pen=pen, symbol='o', symbolBrush=brush, symbolSize=3)
            self.data_lines.append(line)
# END of class Plot ------------------------------------------------------


class Controls(QWidget):
    """
    Define controls and menus design.
    """
    def __init__(self, parent=None):
        super(Controls, self).__init__(parent=parent)

        # Plot time scale == data visible area size
        self.plot_timescale = TIMESCALESIZE # default
        self.plot_timescale_min = 50
        self.plot_timescale_max = 1000

        self.top_menu_row = QHBoxLayout(self)
        self.top_menu_row.setAlignment(Qt.AlignmentFlag.AlignTop)

        # Menu width:
        self.control_width = 550

        # Top Menu
        self.menu_top = QHBoxLayout()#QVBoxLayout()
        self.menu_top.setAlignment(Qt.AlignmentFlag.AlignLeft)

        # Label Baud
        self.baud_label = QLabel(self)
        self.menu_top.addWidget(self.baud_label)
        self.baud_label.setText("Baud:")
        self.baud_label.setStyleSheet(label_style)
        # Select Baud
        self.select_baud = QtWidgets.QComboBox(parent=self)
        self.menu_top.addWidget(self.select_baud)
        self.select_baud.setStyleSheet(dropdown_style)
        self.select_baud.setFixedWidth(100)
        self.select_baud.setEditable(True)
        self.select_baud.setInsertPolicy(QtWidgets.QComboBox.InsertPolicy.NoInsert)
        

        # Label Port
        self.device_label = QLabel(self)
        self.menu_top.addWidget(self.device_label)
        self.device_label.setText("Port:")
        self.device_label.setStyleSheet(label_style)
        # Select Port
        self.select_port = QtWidgets.QComboBox(parent=self)
        self.menu_top.addWidget(self.select_port)
        self.select_port.setStyleSheet(dropdown_style)
        self.select_port.setFixedWidth(150)

        # Button Connect
        self.connect = QtWidgets.QPushButton('Connect', parent=self)
        self.menu_top.addWidget(self.connect)
        self.connect.setFixedWidth(100)
        self.connect.setStyleSheet(btn_style)

        # Info text
        #self.info_text = QLabel(self)
        #self.menu_top.addWidget(self.info_text)
        #self.info_text.setText("\nTo export data\nright-click on the plot.")
        #self.info_text.setStyleSheet(Qinfo_text_style)

        self.top_menu_row.addLayout(self.menu_top)
        # Top menu ends


        # Bottom menu
        self.menu_left = QHBoxLayout()#QVBoxLayout()
        self.menu_left.setAlignment(Qt.AlignmentFlag.AlignRight)

        # Select Time scale size
        ## Time scale txt
        self.time_scale_txt = QLabel(self)
        self.menu_left.addWidget(self.time_scale_txt)
        #self.time_scale_txt.set(QtGui.QIcon(icon_size))
        self.time_scale_txt.setText("Size:")
        self.time_scale_txt.setStyleSheet(label_style)
        ## SpinBox
        self.time_scale_spin = QtWidgets.QDoubleSpinBox()
        self.time_scale_spin.setSingleStep(1)
        self.time_scale_spin.setDecimals(0)
        self.time_scale_spin.setFixedWidth(120)
        self.time_scale_spin.setMaximum(self.plot_timescale_max)
        self.time_scale_spin.setMinimum(self.plot_timescale_min)
        self.time_scale_spin.setValue(self.plot_timescale)
        self.menu_left.addWidget(self.time_scale_spin)
        self.time_scale_spin.setStyleSheet(QDoubleSpinBox_style)


        self.btn_clear = QtWidgets.QPushButton()
        self.btn_clear.setIcon(QtGui.QIcon(icon_clean))
        self.btn_clear.setIconSize(QtCore.QSize(13,16))
        self.btn_clear.setFixedWidth(30)
        self.btn_clear.setStyleSheet(btn_icon_style_disabled)
        self.btn_clear.setEnabled(False)

        self.menu_left.addWidget(self.btn_clear)

        # Button: About
        self.about = QtWidgets.QPushButton(
            icon=QtGui.QIcon(icon_about),
            text='',
            parent=self)
        self.menu_left.addWidget(self.about)
        self.about.setFixedWidth(30)
        self.about.setStyleSheet(btn_icon_style)

        self.top_menu_row.addLayout(self.menu_left)

    def update_timescale(self, new_value):
        """ Assign new value. """
        self.plot_timescale = new_value

    def resizeEvent(self, event):
        """ If we resize main window. """
        super(Controls, self).resizeEvent(event)


# END of class Controls ----------------------------------------


class MainWindow(QWidget):
    """
    Define MainWindow
    """
    def __init__(self, app, parent=None):
        super(MainWindow, self).__init__(parent=parent)

        self.app = app
        self.plot_exist = False
        self.is_fullscreen = False

        self.labels = ["label"]
        self.ports = [''] # list of avablie devices
        self.selected_port = self.ports[0] # '/dev/ttyACM0'
        self.baudrates = [
                          '150', # 0
                          '200', # 1
                          '300', # 2
                          '600', # 3
                         '1200', # 4
                         '1800', # 5
                         '2400', # 6
                         '4800', # 7
                         '9600', # 8
                        '19200',
                        '28800',
                        '38400',
                        '57600',
                        '74880',
                        '76800',
                       '115200',
                       '230400',
                       '250000',
                       '460800',
                       '500000',
                       '576000']
        self.default_baud_index = 8
        self.selected_baudrate = self.baudrates[self.default_baud_index] # default selected baud rate
        logging.debug("self.selected_baudrate =")
        logging.debug(self.selected_baudrate)

        self.max_tryes = 75 # how_many_lines()
        self.number_of_lines = 0
        self.error_counter = 0
        self.plot_data_size = TIMESCALESIZE #?
        self.is_button_connected = False

        self.init_ui()
        self.center_mainwindow()
        self.horizontal_layout = QVBoxLayout(self) #QHBoxLayout(self)

        self.init_timer()
        self.ser = serial.Serial()

        # Controlls
        self.controls = Controls(parent=self)
        self.horizontal_layout.addWidget(self.controls)

        self.init_baudrates()   # Baud Rates on dropdown menu

        # TODO: How to exit thread when mainwindow is closed??
        self.threadpool = QThreadPool()
        self.thread_find_ports()
        #self.find_ports() # while threads disabled!

        # Controll selct and button calls
        self.controls.select_port.currentIndexChanged.connect(self.selected_port_changed)
        self.controls.select_baud.currentIndexChanged.connect(self.selected_baud_changed)
        self.controls.time_scale_spin.valueChanged.connect(self.time_scale_changed)
        self.controls.connect.pressed.connect(self.connect_stop)
        self.controls.btn_clear.pressed.connect(self.clear_data)
        self.controls.about.pressed.connect(self.about)

        # Init About window
        self.aboutbox = QMessageBox()



    def thread_find_ports(self):
        """ Runs on background forewer. """
        # Pass the function to execute
        worker = ForeverWorker(self.find_ports)
        # Execute
        self.threadpool.start(worker)

    def init_timer(self):
        self.timer = QtCore.QTimer()
        self.timer.setInterval(10)
        self.timer.start()

    def init_ui(self):
        self.setStyleSheet(f"MainWindow {{ background-color: {colors['dark']}; }}")
        self.setWindowTitle("Tauno Serial Plotter")
        self.setWindowIcon(QtGui.QIcon(icon_logo))
        self.setMinimumSize(900,550)

    def center_mainwindow(self):
        """ Center window on startup. """
        qr = self.frameGeometry()

        #cp = QDesktopWidget().availableGeometry().center() #PyQt5
        screen = QApplication.primaryScreen()
        rect = screen.availableGeometry()
        cp = rect.center()

        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def find_ports(self):
        """
        Find avaible ports/devices and add to self.ports
        """
        logging.debug("self.plot_exist %s", self.plot_exist)
        logging.debug("self.is_button_connected %s", self.is_button_connected)

        before_selected_port = self.selected_port
        logging.debug("before_selected_port %s", before_selected_port)

        try:
            if not self.plot_exist or not self.is_button_connected:
            # Kui plot on olemas siis me ei skänni!
                self.ports.clear() # clear the devices list
                self.controls.select_port.clear() # clear dropdown menu

                logging.debug("self.ports: %s", len(self.ports))
                ports = list(serial.tools.list_ports.comports())
                logging.debug("find_ports: %s", len(ports))

                for port in ports:
                    #print(port[0]) # /dev/ttyACM0
                    #print(port[1]) # USB2.0-Serial
                    #print(port[2]) # USB VID:PID=2341:0043
                                    # SER=9563430343235150C281
                                    # LOCATION=1-1.4.4:1.0
                    self.ports.append(port[0]) # add devices to list

                # add devices to dropdown menu
                self.controls.select_port.addItems(self.ports)

                if len(ports) > 0:
                    # Et valitud port ei muutuks
                    if before_selected_port in self.ports:
                        index = self.ports.index(before_selected_port)
                    else:
                        index = 0
                    self.controls.select_port.setCurrentIndex(index)
                    self.selected_port = self.ports[index]
        except IOError:
            logging.error("find_ports IOError")

    def init_baudrates(self):
        self.controls.select_baud.addItems(self.baudrates)
        self.controls.select_baud.setCurrentIndex(self.default_baud_index)

    def selected_port_changed(self, i):
        self.selected_port = self.ports[i]
        logging.info("Main selected port changed:")
        logging.info(self.selected_port)

    def selected_baud_changed(self, i):
        self.selected_baudrate = self.baudrates[i]
        logging.info("Main selected baud index changed:")
        logging.info(self.selected_baudrate)
        self.equal_x_and_y()
        self.open_serial()

    def equal_x_and_y(self):
        if self.plot_exist:
            logging.debug("\t eqaul_x_and_y !!!")
            try:
                logging.debug("equal_x_and_y try")
                for i in range(self.number_of_lines):
                    if len(self.plot.x_axis) > len(self.plot.y_axis[i]):
                        logging.debug("\t x on suurem kui y[%s]", i)

                        while len(self.plot.x_axis) > len(self.plot.y_axis[i]):
                            # Remove the first element on list
                            self.plot.x_axis = self.plot.x_axis[1:]
                    if len(self.plot.y_axis[i]) > len(self.plot.x_axis):
                        logging.debug("\t y[%s] on suurem kui x_axis", i)

                        while len(self.plot.y_axis[i]) > len(self.plot.x_axis):
                            # Remove the first element on list
                            self.plot.y_axis[i] = self.plot.y_axis[i][1:]
            
            except Exception as ex:
                logging.debug(ex)
                self.error_counter += 1
                self.error_status()

            except SystemExit:  
                logging.debug(sys.exc_info())


    def time_scale_changed(self):
        logging.debug("Timescale changed!")
        new_value = int(self.controls.time_scale_spin.value())
        old_value = self.plot_data_size

        self.controls.update_timescale(new_value)
        #print("New Timescale value = {}".format(self.controls.plot_timescale))

        self.update_data_size(new_value)
        #print("New data size value = {}".format(self.plot_data_size))

        if new_value < old_value:
            if self.plot_exist:
                real_size = len(self.plot.x_axis)
                difference = real_size - new_value
                # shrink data size
                if difference > 1:
                    del self.plot.x_axis[0:difference]
                    for i in range(self.number_of_lines):
                        del self.plot.y_axis[i][0:difference]


    def update_data_size(self, new_value):
        self.plot_data_size = new_value

    def connect_stop(self):
        """ Connected or Pause button press? """
        if not self.is_button_connected:
            self.is_button_connected = True
            logging.debug('--> Connect Button.')
            self.connect()
            # Enable Clear Data button
            self.controls.btn_clear.setEnabled(True)
            self.controls.btn_clear.setStyleSheet(btn_icon_style)
        else:
            self.is_button_connected = False
            logging.debug('--> Pause Button.')
            self.disconnect()
            # Diable Clear Data button
            self.controls.btn_clear.setEnabled(False)
            self.controls.btn_clear.setStyleSheet(btn_icon_style_disabled)


    def connect(self):
        """ When we press button Connect. """
        # Change button txt
        self.controls.connect.setText('Pause')
        # Disable button
        self.controls.select_port.setEnabled(False)
        self.controls.select_baud.setEnabled(False)
        # Change button style
        self.controls.select_port.setStyleSheet(dropdown_style_disabled)
        self.controls.select_baud.setStyleSheet(dropdown_style_disabled)

        if not self.plot_exist:
            logging.debug("connect: create plot")
            self.number_of_lines = self.how_many_lines()

            if self.number_of_lines is not None:
                # Init plot
                self.plot = Plot(self.number_of_lines, self.labels)
                self.horizontal_layout.addWidget(self.plot)
                self.open_serial()
                self.plot_exist = True
                self.timer.timeout.connect(self.read_serial_data)
            else:
                logging.debug("connect: None!")
        else:
            #self.equal_x_and_y()
            self.open_serial()

    def disconnect(self):
        """ When we press Pause button """
        self.close_serial()
        #self.clear_data() # ??
        # Change button txt
        self.controls.connect.setText('Resume')
        # Enable buttons
        self.controls.select_port.setEnabled(True)
        self.controls.select_baud.setEnabled(True)
        # Change button style
        self.controls.select_port.setStyleSheet(dropdown_style)
        self.controls.select_baud.setStyleSheet(dropdown_style)

    def clear_data(self):
        """ Button clear data """
        logging.debug('--> Clear data Button.')
        # delete existing data
        size = len(self.plot.x_axis)
        logging.debug("x_axis: %s", size)
        del self.plot.x_axis[0:(size-1)]
        for i in range(self.number_of_lines):
            del self.plot.y_axis[i][0:(size-1)]

    def about(self):
        """ Button About """
        logging.debug('--> About Button.')
        self.aboutbox.setWindowTitle("About")
        self.aboutbox.setWindowIcon(QtGui.QIcon(icon_logo))
        self.aboutbox.setText("<center></center><b>Tauno Serial Plotter</b><br/><br/>\
            More info: <a href ='https://github.com/taunoe/tauno-serial-plotter'>\
            github.com/taunoe/tauno-serial-plotter</a><br/><br/>\
            Version {}<br/><br/>\
            Tauno Erik<br/><br/>\
            2021-2026".format(VERSION))
        self.aboutbox.exec()

    def get_numbers(self, string):
        """
        Function to extract all the numbers from the given string
        https://www.regular-expressions.info/floatingpoint.html
        """
        numbers = re.findall(r'[-+]?[0-9]*\.?[0-9]+', string)
        return numbers
    
    def get_labels(self, string):
        """
        Function to extract all the labels from the given string
        """
        labels = re.findall(r'[-+]?[a-zA-Z]*\.?[a-zA-Z]+', string)
        return labels

    def add_numbers(self, i, number, plot_data_size):
        """
        y-axis
        """
        # If list is full
        if len(self.plot.y_axis[i]) > plot_data_size:
            # Remove the first element on list
            self.plot.y_axis[i] = self.plot.y_axis[i][1:]
        # Before adding newone
        self.plot.y_axis[i].append(float(number))

    def add_time(self, plot_data_size):
        """
        x-axis
        """
        # If list is full
        if len(self.plot.x_axis) > plot_data_size:
            # Remove the first element on list
            self.plot.x_axis = self.plot.x_axis[1:]
        # Add a new value 1 higher than the last to end
        self.plot.x_axis.append(self.plot.x_axis[-1] + 1)

    def open_serial(self):
        try:
            logging.debug("0 Open serial: %s %s", self.selected_port, self.selected_baudrate)
            if self.ser.is_open:
                self.ser.close()
            self.ser = serial.Serial(self.selected_port, int(self.selected_baudrate), timeout=1)
            self.ser.reset_input_buffer()## 09.02.2022
            logging.debug("1 Open serial: %s %s", self.ser.name, self.ser.baudrate)
        except IOError:
            logging.error("open_serial IOError")

    def close_serial(self):
        """ Close serial connection. """
        logging.debug("Close serial.")
        self.ser.close()

    def error_status(self):
        """
        If we have to many errors close serial connection.
        Example:
            self.error_counter += 1
            self.error_status()
        """
        if self.error_counter > 9:
            self.close_serial()
            self.error_counter = 0

    """
    Read serial data from serial port.
    """
    def read_serial_data(self):

        if self.ser.is_open:
            # Check if there is data in the input buffer
            if self.ser.in_waiting > 0:
                try:
                    incoming_data = self.ser.readline().decode('utf8')

                    if incoming_data:
                        logging.info("read_serial_dat: Incoming data: %s", incoming_data)

                        numbers = self.get_numbers(incoming_data)
                        logging.debug("numbers: %s", len(numbers))

                        # mitu data punkti tuleb sisse?
                        while len(numbers) > len(self.plot.y_axis):
                            self.plot.y_axis.append([0])

                        for count, value in enumerate(numbers):
                            self.add_numbers(count, value, self.plot_data_size)
                            logging.debug("value: %s", value)

                        self.add_time(self.plot_data_size) # x axis

                        for i in range(self.number_of_lines):
                            logging.debug("for loop %s", i)
                            logging.debug("plot.x_axis %s", self.plot.x_axis)
                            logging.debug("plot.y_axis[i] %s", self.plot.y_axis[i])
                            if len(self.plot.x_axis) > len(self.plot.y_axis[i]):
                                # At beginning append 0.0
                                self.plot.y_axis[i].insert(0, 0.0)
                            self.plot.data_lines[i].setData(self.plot.x_axis, self.plot.y_axis[i])
                except Exception as ex:
                    logging.debug(ex)
                    logging.debug("Error read_serial_data!!!")
                    self.error_counter += 1
                    self.error_status()
                    self.equal_x_and_y()
                
                except SystemExit:  
                    logging.debug(sys.exc_info())


    def how_many_lines(self):
        """
            Return number of different incoming data lines.
            Example: data:454something45t=454-\n == 3
        """
        logging.debug("How_many_lines?")
        self.open_serial()
        if self.ser.is_open:
            try:
                # This may be half of data
                # [:-2] removes the new-line chars.
                broken_data = self.ser.readline()#[:-2].decode('ascii')
                logging.debug("try broken_data %s", broken_data)
                # Full data is between two \n chars
                incoming_data = self.ser.readline().decode('utf8')
                
                logging.debug("try incoming_data %s", incoming_data)
                
                i = 0
                while not incoming_data:
                    input = self.ser.readline().decode('utf8')
                    for c in input:
                        if c.isdigit():
                            incoming_data = input

                    #incoming_data = self.ser.readline().decode('utf8') #readline()[:-1]
                    if i > self.max_tryes:
                        break
                    logging.debug("i = %s", i)
                    logging.debug("while not incoming_data %s", incoming_data)
                    i = i+1
                    

                if incoming_data:
                    logging.debug("if Incoming data %s", incoming_data)
                    numbers = self.get_numbers(incoming_data)
                    logging.debug("Found: %s lines", len(numbers))

                    labels = self.get_labels(incoming_data)
                    logging.debug("Found: %s labels", len(labels))
                    for i, label in enumerate(labels):
                        if i == 0: 
                            self.labels[i] = label
                        else:
                           self.labels.append(label)
                        logging.debug(i)
                        logging.debug(label)

                    return len(numbers)
                

            except Exception as ex:
                logging.debug(ex)
                logging.debug("Error how_many_lines")
                self.ser.close()
                
            except SystemExit:  
                logging.debug(sys.exc_info())


    def keyPressEvent(self, event):
        """
            Detect keypress and runs function
        """
        if event.key() == 32: # Space
            logging.debug("Space")
        elif event.key() == 16777219: # Backspace
            logging.debug("Backspace")
        elif event.key() == 16777274: # F11
            self.key_f11()
        elif event.key() == 16777216: # Esc
            self.key_esc()
        else:
            logging.debug("Unknown keypress: %s, %s", event.key(),event.text())

    def key_f11(self):
        """ F11 of/off fullscreen"""
        if not self.is_fullscreen:
            self.showFullScreen()
            self.is_fullscreen = True
        else:
            self.showNormal()
            self.is_fullscreen = False

    def key_esc(self):
        """ Window is on fullscreen and user presses ESC """
        if self.is_fullscreen:
            self.showNormal()
            self.is_fullscreen = False

    #def mouseClickEvent(self, event):
        #print("clicked")

# END of class MainWindow --------------------------------------------

if __name__ == '__main__':

    if hasattr(Qt, 'AA_EnableHighDpiScaling'):
        QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
    if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
        QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
    if hasattr(Qt, 'AA_Use96Dpi'):
        QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_Use96Dpi, True)


    app = QApplication(sys.argv)
    window = MainWindow(app)
    window.show()

    try:
        exit_code = app.exec()
        print(exit_code)
        sys.exit(exit_code)
    except SystemExit:
        stop_port_scan = True # Kill port scan thread
        print(SystemExit)
