# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019.
# Built in San Francisco.

# This software is distributed under a commercial license for personal,
# educational, corporate or any other use.
# The software as a whole or any parts of it is prohibited for distribution or
# use without obtaining a license from 3D Control Systems, Inc.

# All software licenses are subject to the 3DPrinterOS terms of use
# (available at https://www.3dprinteros.com/terms-and-conditions/),
# and privacy policy (available at https://www.3dprinteros.com/privacy-policy/)

import re
import time
import string
import logging
import threading
import collections

import log
from base_sender import BaseSender
from serial_connection import SerialConnection
from emulated_serial_connection import EmulatedSerialConnection


class Sender(BaseSender):

    OK_TIMEOUT = 300
    ERROR_REPORT_TIMEOUT = 3600
    CONNECTION_TIMEOUT = 6
    TEMP_REQUEST_PERIOD = 3
    PRINTER_INIT_WAIT = 3
    PRINT_JOIN_TIMEOUT = 5
    PAUSE_ON_ERROR_READING_PORT = 0.5
    RECONNECTION_RESET_SLEEP = 2
    CONNECTION_GREATINGS_READ_TIMEOUT = 20
    CONNECTION_RETRIES_PER_BAUDRATE = 2
    CONNECTION_LINE_READ_ATTEMPTS = 2
    CONNECTION_READ_ATTEMPTS = 10
    MAX_VALID_RESEND_RANGE = 128

    GET_TEMP_GCODE = 'M105'
    GET_POSITION_GCODE = 'M114'
    GET_FIRMWARE_VERSION_GCODE = "M115"
    BOARD_RESET_GCODE = 'M999'

    DEFAULT_PAUSE_AT_GCODES_LIST = ["M0", "M25", "M226", "M601", "@pause"]
    DEFAULT_RESUME_AT_GCODES_LIST = ["M24", "M602"]

    OK = 'ok'
    POSITIVE_ACKS = ['start', 'Grbl', 'grbl', 'echo'] + [OK]
    RESEND_REQUEST_PREFIXES = ['resend', 'rs']
    TEMPERATURE_ACKS_PREFIXES = ["T:", "B:", "T0:", "T1:", "T2:"]


    PAUSE_LIFT_HEIGHT = 5 # in mm
    PAUSE_EXTRUDE_LENGTH = 7 # in mm

    def __init__(self, parent, usb_info, profile, connection_class=SerialConnection, buffer_class=collections.deque):
        super().__init__(parent, usb_info, profile)
        if usb_info.get('EMU'):
            self.connection_class = EmulatedSerialConnection
        else:
            self.connection_class = connection_class
        self.define_regexps()
        self.logger = logging.getLogger(self.__class__.__name__)
        self.buffer_class = buffer_class
        self.operational_flag = False
        self.correct_baudrate = None
        self.heating_emulator = None
        self.connection = None
        self.read_thread = None
        self.print_thread = None
        self.was_in_relative_before_pause = False
        self.temperature_request_thread = None
        self.thread_start_lock = threading.Lock()
        self.ready_for_command = threading.Semaphore()
        self.pause_gcodes_list = profile.get("pause_at_gcodes", self.DEFAULT_PAUSE_AT_GCODES_LIST)
        self.resume_gcodes_list = profile.get("resume_at_gcodes", self.DEFAULT_RESUME_AT_GCODES_LIST)
        self.init() #TODO split init and constructor and rename init to something more suitable

    def init(self):
        if self.connect():
            self.last_line_sent = ''
            self.in_relative_pos_mode = False
            self.heating = False
            self.print_thread = None
            self.read_thread = threading.Thread(target=self.reading, name="Read thread")
            self.read_thread.start()
            time.sleep(self.PRINTER_INIT_WAIT)
            self.send_homing_gcodes()
        else:
            raise RuntimeError("Can't connection to printer at baudrates: " + str(self.profile['baudrate']))

    def connect(self):
        if not self.usb_info.get('COM'):
            raise RuntimeError("No serial port detected for serial printer")
        self.logger.info('Baudrates list for %s : %s' % (self.profile['name'], self.profile['baudrate']))
        for baudrate in self.profile['baudrate']:
            if self.correct_baudrate: #FIXME too ugly
                baudrate = self.correct_baudrate
            if self.connection:
                self.connection.close()
            self.logger.info("Connecting at baudrate %d" % baudrate)
            self.connection = self.connection_class(self.usb_info['COM'], baudrate)
            if self.connection.port:
                retries = 0
                while not self.stop_flag and retries <= self.CONNECTION_RETRIES_PER_BAUDRATE:
                    connection_result =  self.wait_for_online()
                    if connection_result:
                        self.correct_baudrate = baudrate
                        self.logger.info("Successful connection to %s: %s" % (self.profile['alias'], str(self.usb_info)))
                        return True
                    elif connection_result == None:
                        retries += 1
                    else:
                        retries = self.CONNECTION_RETRIES_PER_BAUDRATE + 1
                    if not self.profile.get("no_DTR"):
                        self.connection.reset()
                        time.sleep(self.RECONNECTION_RESET_SLEEP)
            self.logger.warning("Error connecting to printer at %d" % baudrate)
            self.connection.close()

    def wait_for_online(self):
        start_time = time.monotonic()
        while time.monotonic() < start_time + self.CONNECTION_TIMEOUT and not self.stop_flag:
            if self.connection.send(self.GET_TEMP_GCODE):
                return self.read_greetings()

    def read_greetings(self):
        line_read_attempts = 0
        start_time = time.monotonic()
        while not self.stop_flag and line_read_attempts < self.CONNECTION_LINE_READ_ATTEMPTS:
            line = ""
            read_attempts = 0
            while not self.stop_flag and read_attempts < self.CONNECTION_READ_ATTEMPTS and\
                            time.monotonic() < start_time + self.CONNECTION_GREATINGS_READ_TIMEOUT:
                symbol = self.connection.recv(1)
                read_attempts += 1
                if not symbol or symbol == "\n":
                    break
                elif not ord(symbol):
                    time.sleep(0.1)
                elif not symbol in string.printable:
                    return False
                else:
                    read_attempts = 0
                    line += symbol
            if self.BOARD_RESET_GCODE in line:
                self.logger.info("Printer's board requested reset. Resetting...")
                return False
            for ack in self.POSITIVE_ACKS:
                if line.startswith(ack):
                    self.logger.info("Printer is online")
                    return True
            for ack in self.TEMPERATURE_ACKS_PREFIXES:
                if line.startswith(ack):
                    self.logger.info("Printer is in blocking heating mode. Need to reset...")
                    return False
            line_read_attempts += 1

    def analyze_sent_line(self, line):
        self.last_line_sent = line
        if 'M109' in line or 'G10' in line:
            tool_match = re.match('.+T(\d+)', line)
            if tool_match:
                tool = int(tool_match.group(1)) + 1
            else:
                tool = 1
            temp_match = re.match('.+S(-?[\d\.]+)', line)
            if temp_match:
                if not tool >= len(self.target_temps):
                    self.target_temps[tool] = float(temp_match.group(1))
        elif 'M190' in line:
            temp_match = re.match('.+S(-?[\d\.]+)', line)
            if temp_match:
                self.target_temps[0] = float(temp_match.group(1))
        elif 'G90' in line:
            self.in_relative_pos_mode = False
        elif 'G91' in line:
            self.in_relative_pos_mode = True
        self.heating = 'M109' in line or 'M190' in line or 'M116' in line or 'M98' in line
        #  if self.heating:
        #      with self.pause_and_heating_duration_lock:
        #          self.heating_start_time = time.monotonic()
        #  else:
        #      with self.pause_and_heating_duration_lock:
        #          if self.heating_start_time:
        #              self.sum_pause_and_heating_duration += time.monotonic() - self.heating_start_time
        #              self.heating_start_time = None

    def send_now(self, line_or_lines_list):
        with self.thread_start_lock:
            if not self.print_thread or not self.print_thread.is_alive():
                self.print_thread = PrintThread(self, name="SendNowThread")
                self.add_lines_to_send_now_buffer(line_or_lines_list)
                self.print_thread.start()
            else:
                self.add_lines_to_send_now_buffer(line_or_lines_list)

    def add_lines_to_send_now_buffer(self, line_or_lines_list):
        if type(line_or_lines_list) == str:
            self.print_thread.send_now_buffer.append(line_or_lines_list)
        else:
            self.print_thread.send_now_buffer.extend(line_or_lines_list)

    def send_homing_gcodes(self):
        with self.thread_start_lock:
            if self.heating_emulator:
                self.heating_emulator.disable(False)
            self.logger.info("Starting homing thread")
            gcodes = []
            gcodes.extend(self.profile["end_gcodes"])
            gcodes.append(self.GET_FIRMWARE_VERSION_GCODE)
            self.print_thread = PrintThread(self, name="HomingThread")
            self.print_thread.send_now_buffer.extend(gcodes)
            self.print_thread.start()
            self.print_thread.join()
            self.start_temp_requesting()

    def define_regexps(self):
        self.temp_re = re.compile('.*T:(-?[\d\.]+) /(-?[\d\.]+) B:(-?[\d\.]+) /(-?[\d\.]+)')
        self.position_re = re.compile('.*X:(-?[\d\.]+).?Y:(-?[\d\.]+).?Z:(-?[\d\.]+).?E:(-?[\d\.]+).*')
        self.wait_tool_temp_re = re.compile('T:(-?[\d\.]+)')
        self.wait_platform_temp_re = re.compile('.+B:(-?[\d\.]+)')
        self.resend_number_re = re.compile("(\d+)")
        self.bed_re = re.compile('B:(-?[\d\.]+) /(-?[\d\.]+)')
        self.current_tool_re = re.compile('T:(-?[\d\.]+) /(-?[\d\.]+)')
        self.first_tool_re = re.compile('T0:(-?[\d\.]+) /(-?[\d\.]+)')
        self.second_tool_re = re.compile('T1:(-?[\d\.]+) /(-?[\d\.]+)')

    @log.log_exception
    def reading(self):
        last_ok_time = time.monotonic()
        last_error_report_time = 0
        while not self.stop_flag:
            line = self.connection.recv()
            if self.parse_printer_answers(line):
                last_ok_time = time.monotonic()
            elif not self.heating and last_ok_time + self.OK_TIMEOUT < time.monotonic():
                last_ok_time = time.monotonic()
                if self.heating_emulator and self.heating_emulator.enabled:
                    self.heating_emulator.check_temperatures()
                    continue
                if last_error_report_time + self.ERROR_REPORT_TIMEOUT < time.monotonic():
                    message = "Warning! Timeout while waiting for ok. Assuming printer's firmware malfunction."
                    self.parent.register_error(251, message, is_blocking=False)
                    last_error_report_time = time.monotonic()
                self.logger.warning("Attempting to recover printing process by stop to wait for ok")
                self.ready_for_command.release()

    def parse_printer_answers(self, line):
        #TODO prevent message splitting from causing problems(o\nk\n case)
        if line is None:  # None means error reading from printer, but "" is ok
            self.operational_flag = False
            time.sleep(self.PAUSE_ON_ERROR_READING_PORT)
        elif not line or "wait" in line:
            return # return is needed here for faster the parsing
        elif line.startswith(self.OK) or line[1:].startswith(self.OK):
            self.ready_for_command.release()
            self.operational_flag = True
            if not self.check_temperature_and_position(line):
                self.execute_callback(line, True)
            # M105 should not be affect anti-stuck code mechanism
            return self.last_line_sent != self.GET_TEMP_GCODE or not self.is_printing() or self.print_thread.paused
        elif line[0] in ("T", "B") or (len(line) > 1 and line[1] in ("T", "B")):
            self.parse_waiting_temperature_updates(line)
            self.operational_flag = True
        elif self.check_for_resend_requests(line):
            self.logger.debug(line)
            self.logger.info("Last_line: " + self.last_line_sent)
            self.operational_flag = True
            self.ready_for_command.release()
            self.execute_callback(line, False)
        elif 'error' in line or 'warning' in line or 'Error' in line or 'Warning' in line:
            self.logger.debug(line)
            if line.strip().endswith(self.OK):
                self.ready_for_command.release()
            if ("checksum" in line or
                "Checksum" in line or
                "expected line" in line or
                "Line Number is not Last Line Number+1" in line or
                "Format error" in line):
                self.logger.info("Last_line: " + self.last_line_sent)
            is_blocking = self.BOARD_RESET_GCODE in line or "disabling all for safety!" in line
            if is_blocking:
                self.operational_flag = False
            self.parent.register_error(201, "Printer: " + line, is_blocking=is_blocking)
            self.execute_callback(line, False)
        elif "firmware" in line.lower():
            self.logger.info("Firmware info: " + line)
            self.profile['firmware_info'] = line
        elif line.startswith('DEBUG'):
            self.logger.info(line)
        else:
            self.log_strange_acks(line)
            self.execute_callback(line, False)

    def log_strange_acks(self, line):
        self.logger.warning("Received: " + line.strip())

    def start_temp_requesting(self):
        self.stop_temp_requesting_flag = False
        self.temperature_request_thread = threading.Thread(target=self.temperature_requesting, name="TemperatureThread")
        self.temperature_request_thread.start()

    def stop_temp_requesting(self):
        self.stop_temp_requesting_flag = True
        if self.temperature_request_thread:
            self.temperature_request_thread.join()

    @log.log_exception
    def temperature_requesting(self):
        STEPS_NUMBER = 100
        sleep_step = self.TEMP_REQUEST_PERIOD / float(STEPS_NUMBER)
        while not self.stop_flag and not self.stop_temp_requesting_flag:
            # don't send M105 when we are in blocking heating or have ok callbacks
            if not self.heating: 
                self.send_now(self.GET_TEMP_GCODE)
            sleep_step_count = STEPS_NUMBER
            while not self.stop_flag and sleep_step_count:
                time.sleep(sleep_step)
                sleep_step_count -= 1

    def parse_temperature(self, line):
        self.logger.debug(line)
        match = self.temp_re.match(line)
        got_temps = False
        if match:
            tool_temp = float(match.group(1))
            tool_target_temp = float(match.group(2))
            platform_temp = float(match.group(3))
            platform_target_temp = float(match.group(4))
            self.temps = [platform_temp, tool_temp]
            self.target_temps = [platform_target_temp, tool_target_temp]
            if self.heating_emulator:
                self.heating_emulator.check_temperatures()
            got_temps = True
        else:
            temps = []
            target_temps = []
            bed_match = self.bed_re.search(line)
            first_tool_match = self.first_tool_re.search(line)
            second_tool_match = self.second_tool_re.search(line)
            try:
                if bed_match:
                    temps.append(float(bed_match.group(1)))
                    target_temps.append(float(bed_match.group(2)))
                    got_temps = True
                else:
                    temps.append(0.0)
                    target_temps.append(0.0)
                if first_tool_match:
                    temps.append(float(first_tool_match.group(1)))
                    target_temps.append(float(first_tool_match.group(2)))
                    got_temps = True
                else:
                    temps.append(0.0)
                    target_temps.append(0.0)
                if second_tool_match:
                    temps.append(float(second_tool_match.group(1)))
                    target_temps.append(float(second_tool_match.group(2)))
                    got_temps = True
            except Exception:
                self.logger.exception("Error parsing temperatures")
            self.temps = temps
            self.target_temps = target_temps
        return got_temps

    def parse_waiting_temperature_updates(self, line):
        match = self.wait_platform_temp_re.match(line)
        if match:
            self.temps[0] = float(match.group(1))
        match = self.wait_tool_temp_re.match(line)
        if match:
            self.temps[1] = float(match.group(1))

    def check_for_resend_requests(self, line):
        for prefix in self.RESEND_REQUEST_PREFIXES:
            if line.lower().startswith(prefix):
                match = self.resend_number_re.search(line)
                if match:
                    failed_line_number = int(match.group(1))
                    self.logger.info("Request to resend line N%d" % failed_line_number)
                    try:
                        if self.print_thread.lines_sent > failed_line_number + self.MAX_VALID_RESEND_RANGE:
                            message = "Printer request resent for too old line number. Assuming firmware reboot."
                            self.parent.register_error(261, message, is_blocking=True)
                        else:
                            self.print_thread.lines_sent = failed_line_number
                    except AttributeError: # print_thread could be deleted by other thread
                        pass
                    return True
                else:
                    self.logger.warning("Can't parse line number from resent request")

    def check_temperature_and_position(self, line):
        match = self.position_re.match(line)
        try:
            if match:
                self.position = [float(match.group(1)), float(match.group(2)), float(match.group(3)), float(match.group(4))]
        except ValueError:
            pass
        for prefix in self.TEMPERATURE_ACKS_PREFIXES:
            if prefix in line:
                return self.parse_temperature(line)

    def load_gcodes(self, gcodes):
        self.logger.info("Loading gcodes in ThreadedSender")
        with self.thread_start_lock:
            if (self.print_thread and self.print_thread.is_alive()):
                self.logger.info("Joining printing thread...")
                self.print_thread.join(timeout=self.PRINT_JOIN_TIMEOUT)
                if self.print_thread.is_alive():
                    self.parent.register_error(260, "Can't start print cause already printing", is_blocking=False)
                    return False
            self.print_thread = PrintThread(self)
            self.print_thread.load_gcodes(gcodes)
            self.print_thread.start()
            self.init_speed_calculation_thread()
            self.logger.info("Print thread started")

    def is_printing(self):
        return getattr(self, "print_thread", None) and self.print_thread.is_alive() and self.print_thread.buffer

    def is_paused(self):
        return getattr(self, "print_thread", None) and self.print_thread.is_alive() and self.print_thread.paused

    def is_operational(self):
        return self.operational_flag

    def pause(self, no_gcodes=False):
        if self.print_thread:
            if not self.print_thread.paused:
                if self.heating:
                    message = "Can't pause during heating."
                    self.parent.register_error(254, message, is_blocking=False)
                    return False
                self.print_thread.paused = True
                self.was_in_relative_before_pause = self.in_relative_pos_mode
                if not no_gcodes:
                    if not self.in_relative_pos_mode:
                        self.send_now("G91")
                    self.send_now("G1 Z+%d E-%d" % (self.PAUSE_LIFT_HEIGHT, self.PAUSE_EXTRUDE_LENGTH))
                #self.update_pause_time_and_duration()
                return True
        return False

    def unpause(self, no_gcodes=False):
        if self.print_thread:
            if self.print_thread.paused:
                if not no_gcodes:
                    self.send_now("G1 Z-%d E+%d" % (self.PAUSE_LIFT_HEIGHT, self.PAUSE_EXTRUDE_LENGTH))
                    if not self.was_in_relative_before_pause:
                        self.send_now("G90")
                self.print_thread.paused = False
                #self.update_unpause_time_and_duration()
                return True
        return False

    def cancel(self):
        if self.profile.get('no_DTR') and self.heating:
            message = "The printer is not supporting cancel in heating state."
            self.parent.register_error(257, message, is_blocking=True)
            return False
        else:
            self.stop_flag = True
            with self.thread_start_lock:
                 if self.print_thread and self.print_thread.is_alive():
                    self.print_thread.cancel()
                    self.print_thread.join()
            try:
                self.read_thread.join()
                self.temperature_request_thread.join()
                if getattr(self, 'speed_calculation_thread', None):
                    self.speed_calculation_thread.join(self.speed_calculation_thread.LOOP_TIME)
            except RuntimeError:
                pass
            self.reset()
            self.stop_flag = False
            try:
                self.init()
                self.logger.info("Successful cancel")
            except RuntimeError:
                self.parent.register_error(255, "Can't reconnect to printer after cancel.", is_blocking=True)
            return True

    def reset(self):
        if not self.connection:
            self.logger.warning("No connection to printer to reset")
            return False
        elif self.profile.get('no_DTR'):
            self.logger.warning("DTR reset is not supported for this printer type. Canceling using gcodes.")
            self.connection.send(self.BOARD_RESET_GCODE)
            time.sleep(self.RECONNECTION_RESET_SLEEP)
        else:
            self.connection.reset()
            self.logger.info("Successful reset")

    def get_percent(self):
        if self.print_thread:
            return self.print_thread.get_percent()
        else:
            return 0

    def get_current_line_number(self):
        try:
            lines_sent = self.print_thread.lines_sent
        except AttributeError:
            lines_sent = 0
        return lines_sent

    def unbuffered_gcodes(self, gcodes):
        self.logger.info("Gcodes to send now: " + str(gcodes))
        gcodes = self.preprocess_gcodes(gcodes)
        #gcodes.append(self.GET_POSITION_GCODE)
        self.send_now(gcodes)
        self.logger.info("Gcodes were sent to printer")

    #USE only to to fix stupid behaviour as after M28
    def release_ready_for_command(self):
        self.ready_for_command.release()

    def close(self):
        super().close()
        self.logger.debug("Joining reading thread...")
        try:
            self.read_thread.join(20)
        except RuntimeError:
            pass
        self.logger.debug("...done")
        with self.thread_start_lock:
            if getattr(self, "print_thread", None):
                self.logger.debug("Joining printing thread...")
                try:
                    self.print_thread.join(self.STOP_JOIN_TIMEOUT)
                except (AttributeError, RuntimeError):
                    self.logger.debug("...no running print thread to joint")
                else:
                    self.logger.debug("...done")
        if getattr(self, "temperature_request_thread", None):
            self.logger.debug("Joining temperature thread...")
            try:
                self.temperature_request_thread.join(self.STOP_JOIN_TIMEOUT)
            except (AttributeError, RuntimeError):
                self.logger.debug("...no running temperature thread to joint")
            else:
                self.logger.debug("...done")
        self.connection.close()
        self.logger.info("Threaded sender is closed")


class PrintThread(threading.Thread):

    RESUME_WAITING_STEP = 0.1
    OK_WAITING_STEP = 0.1
    GO_TO_LINE_N_GCODE = "M110"

    def __init__(self, sender, name=None):
        self.sender = sender
        self.lines_sent = 0 # needed for multithreading purposes(lines_sent count be read before load gcodes)
        self.paused = False
        self.buffer = None
        self.send_now_buffer = collections.deque()
        if not name:
            name = self.__class__.__name__
        self.logger = logging.getLogger(name)
        super(PrintThread, self).__init__(name=name)

    def calc_checksum(self, command):
        checksum = 0
        for char in command:
            checksum ^= ord(char)
        return checksum

    def load_gcodes(self, gcodes):
        self.logger.info("Starting gcodes loading...")
        if type(gcodes) == str or type(gcodes) == bytes:
            gcodes = self.sender.preprocess_gcodes(gcodes)
        self.total_gcodes = len(gcodes)
        self.sender.total_gcodes = len(gcodes)
        self.lines_sent = 0
        gcodes.insert(0, self.GO_TO_LINE_N_GCODE)
        self.buffer = self.sender.buffer_class(gcodes)
        self.logger.info("...done loading gcodes")

    def add_line_number_and_checksum(self, line):
        command = "N%d %s" % (self.lines_sent, line)
        command = command + "*" + str(self.calc_checksum(command))
        return command

    def send(self, line, add_checksum=True):
        line = line.split(";")[0].strip() # remove any comments
        if not line:
            line = self.sender.GET_TEMP_GCODE # we cant just skip lines - that will cause N100 G0... \nN102 like error
        if add_checksum:
            line = self.add_line_number_and_checksum(line)
            # we need to do this before sending to avoid bugs when resent request change line number before +1
        if self.sender.connection.send(line):
            if add_checksum:
                self.lines_sent += 1
            self.sender.analyze_sent_line(line)
            return True
        else:
            self.sender.parent.register_error(250, "Unable to write to serial port", is_blocking=False)
            return False

    @log.log_exception
    def run(self):
        while not self.sender.stop_flag and self.sender.connection.port:
            while not self.sender.ready_for_command.acquire(True, self.OK_WAITING_STEP):
                if self.sender.stop_flag:
                    self.sender.ready_for_command.release()
                    return
            if self.send_now_buffer:
                line = self.send_now_buffer.popleft()
                self.send(line, add_checksum=False)
            elif self.paused or (self.sender.heating_emulator and self.sender.heating_emulator.enabled):
                time.sleep(self.RESUME_WAITING_STEP)
                self.sender.ready_for_command.release()
            else:
                try:
                    line = self.buffer[self.lines_sent]
                    if self.sender.heating_emulator:
                        line2 = self.sender.heating_emulator.start(line)
                        if line2:
                            line = line2
                except (IndexError, TypeError):
                    #self.logger.info(threading.currentThread().getName() + " has finished.")
                    self.sender.ready_for_command.release()
                    if self.buffer:
                        self.logger.info("Print finished on %d/%d." % (self.lines_sent, self.total_gcodes))
                        self.buffer = None
                        self.sender.set_estimated_print_time(0)
                    return
                else:
                    for gcode in self.sender.pause_gcodes_list:
                        if self.line_contains_gcode(line, gcode):
                            self.sender.parent.register_error(601, f"Pause by gcode {line}", is_blocking=False, is_info=True)
                            line = ""
                            self.sender.pause()
                            break
                    for gcode in self.sender.resume_gcodes_list:
                        if self.line_contains_gcode(line, gcode):
                            line = ""
                            self.sender.parent.register_error(602, f"Resume by gcode {line}", is_blocking=False, is_info=True)
                            self.sender.unpause()
                            break
                    if not self.send(line):
                        self.sender.ready_for_command.release()

    def pause(self):
        self.paused = True

    def unpause(self):
        self.paused = False

    def cancel(self):
        self.buffer = None

    def get_percent(self):
        if not self.lines_sent:
            return 0
        else:
            return round(self.lines_sent / self.total_gcodes * 100, 2)

    def line_contains_gcode(self, line, gcode):
        return line.endswith(gcode) or\
            gcode + " " in line or\
            gcode + ";" in line
