Загрузка данных


import math
import re

class ddCNCPlannerX:
    def __init__(self, accel=150.0, min_speed=2.0, junction_deviation=0.02):
        self.accel = accel                     # Ускорение (мм/сек^2)
        self.min_speed = min_speed             # Стартовая скорость (мм/сек)
        self.junction_deviation = junction_deviation  # Жесткость станины (мм)

    def parse_and_plan(self, gcode_lines):
        """
        Главный метод управления пайплайном подготовки программы.
        Вход: массив текстовых строк G-кода.
        Выход: массив скомпилированных бинарных пакетов B:... для ESP32.
        """
        # 1. Шаг парсинга: превращаем текст в массив геометрических отрезков
        segments = self._parse_to_segments(gcode_lines)
        
        # 2. Шаг математики Look-Ahead: рассчитываем безопасные скорости сопряжения на углах
        segments = self._apply_look_ahead(segments)
        
        # 3. Шаг бинаризации: сборка пакетов по вашему новому протоколу
        packet_stream = []
        for seg in segments:
            # Формируем структуру: B:TYPE;LINE_NUM;X;Y;Z;F_MAX;V_START;V_END
            # Округляем до 3 знаков после запятой для высокой точности
            packet = (
                f"B:{seg['type']};{seg['line']};"
                f"{seg['x']:.3f};{seg['y']:.3f};{seg['z']:.3f};"
                f"{seg['f_min']:.3f};{seg['v_start']:.3f};{seg['v_end']:.3f}"
            )
            packet_stream.append(packet)
            
        return packet_stream

    def _parse_to_segments(self, gcode_lines):
        """
        БЛОК 1: Инициализация структуры данных и модального кэша траектории
        """
        segments = []  # Итоговый массив геометрических сегментов
        
        # Виртуальный начальный ноль траектории (база отсчета)
        curr_x = 0.0
        curr_y = 0.0
        curr_z = 0.0
        
        # Модальная скорость подачи по умолчанию (мм/мин)
        curr_f = 600.0 
        # ==========================================
        # БЛОК 2: Построчный цикл и очистка от мусора
        # ==========================================
        for idx, line in enumerate(gcode_lines):
            # 1. Удаляем пробелы по краям, бьем по комментарию ';' и берем левую часть
            line = line.strip().split(';')[0].upper()
            
            # 2. Если после очистки комментариев строка пустая — пропускаем её
            if not line:
                continue
            # ==========================================
            # БЛОК 3: Извлечение токенов через Regex
            # ==========================================
            # Выдергиваем типы команд (G0/G1 или M)
            g_match = re.search(r'G([0-1])', line)
            m_match = re.search(r'M([0-9]+)', line)
            
            # Выдергиваем числовые значения осей и подачи
            x_match = re.search(r'X([-+]?[0-9]*\.?[0-9]+)', line)
            y_match = re.search(r'Y([-+]?[0-9]*\.?[0-9]+)', line)
            z_match = re.search(r'Z([-+]?[0-9]*\.?[0-9]+)', line)
            f_match = re.search(r'F([0-9]*\.?[0-9]+)', line)
            # --------------------------------------------------------
            # БЛОК 4 (Пункты 1-3): Типизация, модальный кэш F и осей X/Y/Z
            # --------------------------------------------------------
            # Пункт 1: Привязка оригинального номера кадра и базовый тип
            line_num = idx
            seg_type = 1  # По умолчанию 1 = интерполированное движение (G0/G1)
            if m_match:
                seg_type = 3  # 3 = вспомогательная M-команда (СОЖ, шпиндель)
            
            # Пункт 2: Модальное обновление скорости подачи (перевод мм/мин -> мм/сек)
            if f_match:
                curr_f = float(f_match.group(1))
            f_min = curr_f / 60.0  # Ограничение скорости для Look-Ahead
            
            # Пункт 3: Защита от улета в 0. Автозаполнение пропущенных координат
            target_x = float(x_match.group(1)) if x_match else curr_x
            target_y = float(y_match.group(1)) if y_match else curr_y
            target_z = float(z_match.group(1)) if z_match else curr_z
            # --------------------------------------------------------
            # БЛОК 4 (Пункты 4-5): Расчет 3D-геометрии и упаковка
            # --------------------------------------------------------
            # Пункт 4: Расчет дельт перемещений по осям X, Y, Z
            dx = target_x - curr_x
            dy = target_y - curr_y
            dz = target_z - curr_z
            
            # Длина вектора перемещения в пространстве по теореме Пифагора
            length = math.sqrt(dx*dx + dy*dy + dz*dz)
            
            # Пункт 5: Формирование структуры сегмента и сохранение
            segment = {
                "type": seg_type,
                "line": line_num,
                "x": target_x,
                "y": target_y,
                "z": target_z,
                "dx": dx,
                "dy": dy,
                "dz": dz,
                "length": length,
                "f_min": f_min,          # Максимальный предел скорости кадра (мм/сек)
                "v_start": self.min_speed, # Стартовая скорость (заглушка для Look-Ahead)
                "v_end": self.min_speed    # Конечная скорость (заглушка для Look-Ahead)
            }
            segments.append(segment)
            
            # Сдвигаем кэш позиций — целевая точка становится текущей для следующего кадра
            curr_x = target_x
            curr_y = target_y
            curr_z = target_z
            
        return segments # Возвращаем полностью готовый массив геометрических отрезков

    def _apply_look_ahead(self, segments):
        """
        БЛОК 1: Инициализация граничных условий Look-Ahead планировщика
        """
        # Если сегментов нет или их слишком мало, математический расчет не требуется
        if len(segments) <= 1:
            return segments
            
        # Задаем граничные условия для краев программы ЧПУ
        # Первый кадр начинает движение со стартовой скорости
        segments[0]['v_start'] = self.min_speed
        
        # Последний кадр завершает программу полной остановкой
        segments[-1]['v_end'] = self.min_speed
        # ==========================================================
        # БЛОК 2: Попарный анализ углов излома (Junction Deviation)
        # ==========================================================
        for i in range(len(segments) - 1):
            seg = segments[i]
            next_seg = segments[i + 1]
            
            # Если хотя бы один из сегментов не движение (например, М-команда) — гасим скорость на стыке
            if seg["type"] != 1 or next_seg["type"] != 1:
                v_junction = self.min_speed
            else:
                # Проверка на нулевую длину векторов, чтобы избежать деления на ноль
                if seg["length"] < 1e-4 or next_seg["length"] < 1e-4:
                    v_junction = self.min_speed
                else:
                    # 1. Скалярное произведение векторов в 3D
                    dot_product = (seg["dx"] * next_seg["dx"] + 
                                   seg["dy"] * next_seg["dy"] + 
                                   seg["dz"] * next_seg["dz"])
                    
                    # 2. Находим косинус угла излома траектории
                    cos_theta = dot_product / (seg["length"] * next_seg["length"])
                    cos_theta = max(-1.0, min(1.0, cos_theta)) # Ограничение диапазона [-1, 1]
                    
                    # Если векторы сонаправлены (прямая линия, угол 0, косинус 1) — тормозить не нужно
                    if cos_theta > 0.999999:
                        v_junction = min(seg["f_min"], next_seg["f_min"])
                    else:
                        # 3. Вычисление синуса половинного угла по тригонометрическому тождеству
                        sin_half_theta = math.sqrt((1.0 - cos_theta) / 2.0)
                        
                        # 4. Расчет виртуального радиуса скругления угла по формуле Junction Deviation
                        radius = (self.junction_deviation * sin_half_theta) / (1.0 - sin_half_theta)
                        
                        # 5. Центростремительный лимит скорости прохождения угла по вашему ТЗ
                        v_junction = math.sqrt(self.accel * radius)
                        
                        # Ограничиваем скорость возможностями моторов текущего и следующего кадра
                        v_junction = min(v_junction, seg["f_min"], next_seg["f_min"])
                        v_junction = max(v_junction, self.min_speed)

            # Синхронизируем стык: конец текущего кадра равен началу следующего кадра
            seg["v_end"] = v_junction
            next_seg["v_start"] = v_junction
        # ==========================================================
        # БЛОК 3: Обратный проход Look-Ahead (Расчет волны торможения)
        # ==========================================================
        # Идем от предпоследнего сегмента назад до самого первого
        for i in reversed(range(len(segments) - 1)):
            seg = segments[i]
            next_seg = segments[i + 1]
            
            # Просчитываем торможение только для физических векторов перемещения
            if seg["type"] == 1:
                # На основе длины вектора и скорости на его конце вычисляем
                # максимально допустимую скорость на входе в этот кадр
                v_allowable = math.sqrt(seg["v_end"]**2 + 2.0 * self.accel * seg["length"])
                
                # Если скорость входа превышает физический лимит торможения — зажимаем её
                if seg["v_start"] > v_allowable:
                    seg["v_start"] = v_allowable
                    
                # Гарантируем, что скорость входа не прыгнет выше маршевого потолка F кадра
                seg["v_start"] = min(seg["v_start"], seg["f_min"])
            
            # Накатываем волну торможения назад: 
            # новое скорректированное начало следующего кадра становится концом текущего
            seg["v_end"] = next_seg["v_start"]
        # ==========================================================
        # БЛОК 4: Прямой проход Look-Ahead (Расчет волны разгона)
        # ==========================================================
        # Идем от самого первого сегмента вперед до конца программы
        for i in range(len(segments) - 1):
            seg = segments[i]
            next_seg = segments[i + 1]
            
            if seg["type"] == 1:
                # На основе стартовой скорости и длины вектора вычисляем
                # физически достижимую скорость на выходе из этого кадра
                v_allowable = math.sqrt(seg["v_start"]**2 + 2.0 * self.accel * seg["length"])
                
                # If скорость выхода превышает возможности разгона — зажимаем её
                if seg["v_end"] > v_allowable:
                    seg["v_end"] = v_allowable
                    
                # Гарантируем, что скорость выхода не превышает маршевый потолок F кадра
                seg["v_end"] = min(seg["v_end"], seg["f_min"])
            
            # Накатываем волну разгона вперед:
            # скорректированный конец текущего кадра становится началом следующего
            next_seg["v_start"] = seg["v_end"]
            
        return segments # Возвращаем полностью просчитанный массив траекторий