/
dAshkova
/
luna_CoDeSys
Обзор
Документация
Войти
/
dAshkova
/
luna_CoDeSys
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
client.py
961 строка
39 KB
dAshkova
Добавлена поддержка владельца (owner) для PROPERTY_ACCESSOR в сообщениях сборки
19 июл 2026, 02:58
19 июл 2026, 02:58
0461b58
Код
Авторство
О чём код?
# -*- coding: utf-8 -*- import socket import json import urllib2 import clr import time # Подключаем .NET компоненты для GUI clr.AddReference("System.Windows.Forms") clr.AddReference("System.Drawing") from System.Windows.Forms import Form, Label, ComboBox, ComboBoxStyle, Button, DialogResult from System.Drawing import Point, Size from System.Windows.Forms import Timer from System import AppDomain import parser from scriptengine import * # --- КОНФИГУРАЦИЯ --- UDP_PORT = 50005 HTTP_PORT = 8000 SYNC_INTERVAL = 2000 # Ключи для хранения в памяти процесса TIMER_KEY = "LUNA_SYNC_TIMER" IP_KEY = "LUNA_SERVER_IP" TOKEN_KEY = "LUNA_AUTH_TOKEN" # Severity маска: используем Enum SEVERITY_MASK = Severity.Error # | Severity.Warning COMPILER_CATEGORY = "97f48d64-a2a3-4856-b640-75c046e37ea9" class Journal: def Log(self, text, severity = Severity.Text, obj = None): system.write_message(severity, text, obj) def LogError(self, text, obj = None): self.Log(text, Severity.Error, obj) def LogWarning(self, text, obj = None): self.Log(text, Severity.Warning, obj) def LogInfo(self, text, obj = None): self.Log(text, Severity.Information, obj) def LogText(self, text, obj = None): self.Log(text, Severity.Text, obj) journal = Journal() # ========================================== # ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ СБОРКИ # ========================================== def _get_object_type_from_message(code_obj): """Получает тип объекта из сообщения через parser.CodesysTypes.get_type_name(). code_obj — объект кода, type — GUID типа объекта. """ try: if not code_obj: return "" guid = str(code_obj.type).lower() return parser.CodesysTypes.get_type_name(guid) except Exception: return "" def _parse_position_to_line_col(msg_obj, code_obj): """Преобразует Position в (line, column) используя position_text.""" try: position_text = msg_obj.position_text if hasattr(msg_obj, 'position_text') else None # Парсим position_text (там ПРАВИЛЬНЫЕ координаты) if position_text: try: text = str(position_text) import re # "Строка 9, Столбец 1" line_match = re.search(r'(?i)(line|zeile|строка)\s*(\d+)', text) col_match = re.search(r'(?i)(column|spalte|столбец)\s*(\d+)', text) if line_match: line = int(line_match.group(2)) col = int(col_match.group(2)) if col_match else 1 return line, col except Exception: pass # Фоллбэк: если нет position_text return -1, -1 except Exception: return -1, -1 def _get_error_line_text(code_obj, msg_obj): """Получает строку с ошибкой используя position_text. msg_obj.position_text содержит "Строка 9, Столбец 1 (Реализ.)" где 9 — это номер строки В IMPLEMENTATION (не в declaration)! """ try: if not code_obj: return "" # Парсим position_text чтобы понять где искать position_text = msg_obj.position_text if hasattr(msg_obj, 'position_text') else None if not position_text: return "" text_str = str(position_text) # Определяем секцию: (Реализ.) или (Декларация) is_implementation = 'реализ' in text_str.lower() or 'implement' in text_str.lower() is_declaration = 'деклар' in text_str.lower() or 'declaration' in text_str.lower() # Извлекаем номер строки import re line_match = re.search(r'(?i)(line|zeile|строка)\s*(\d+)', text_str) if not line_match: return "" target_line = int(line_match.group(2)) # Выбираем нужную секцию target_text = "" if is_implementation: if hasattr(code_obj, 'has_textual_implementation') and code_obj.has_textual_implementation: target_text = code_obj.textual_implementation.text or "" elif is_declaration: if hasattr(code_obj, 'has_textual_declaration') and code_obj.has_textual_declaration: target_text = code_obj.textual_declaration.text or "" else: # Если не указано — пробуем implementation по умолчанию if hasattr(code_obj, 'has_textual_implementation') and code_obj.has_textual_implementation: target_text = code_obj.textual_implementation.text or "" if not target_text: return "" # Извлекаем N-ю строку из текста lines = target_text.split('\n') # target_line начинается с 1, массив с 0 if 1 <= target_line <= len(lines): return lines[target_line - 1].rstrip() return "" except Exception as e: journal.LogWarning("Ошибка извлечения строки: {0}".format(e)) return "" def _get_object_parent(code_obj): """Получает имя родительского объекта через get_parent() или через путь.""" try: if not code_obj: return "" # Вариант 1: через метод get_parent() if hasattr(code_obj, 'get_parent'): try: parent = code_obj.get_parent() if parent: return parent.get_name() if hasattr(parent, 'get_name') else str(parent) except: pass # Вариант 2: через свойство parent (lowercase) if hasattr(code_obj, 'parent'): try: parent = code_obj.parent if parent: return parent.get_name() if hasattr(parent, 'get_name') else str(parent) except: pass # Вариант 3: через guid и поиск в дереве if hasattr(code_obj, 'guid'): try: obj_guid = code_obj.guid # Ищем объект в дереве проекта apps = get_all_applications() for app_info in apps: app_obj = app_info['object'] # Рекурсивно ищем объект по GUID found = _find_parent_by_guid(app_obj, obj_guid) if found: return found except: pass return "" except Exception as e: return "" def _get_object_owner(code_obj): """Получает владельца для PROPERTY_ACCESSOR через поиск по GUID.""" try: if not code_obj: return "" # У accessor'а нет get_parent(), используем только GUID if hasattr(code_obj, 'guid'): obj_guid = code_obj.guid apps = get_all_applications() for app_info in apps: app_obj = app_info['object'] # Ищем accessor в дереве и возвращаем владельца (на 2 уровня выше) owner = _find_owner_by_guid(app_obj, obj_guid) if owner: return owner return "" except Exception as e: journal.LogWarning("_get_object_owner error: {0}".format(e)) return "" def _find_owner_by_guid(parent_obj, target_guid, depth=0): """Рекурсивно ищет accessor и возвращает владельца (FB/Program).""" try: if depth > 15: return "" # Проверяем детей if hasattr(parent_obj, 'get_children'): for child in parent_obj.get_children(False): # Нашли accessor? if hasattr(child, 'guid') and str(child.guid).lower() == str(target_guid).lower(): # parent_obj — это свойство (FIFO) # Нужен ЕГО родитель (FB) return _get_object_parent(parent_obj) # Рекурсия вглубь result = _find_owner_by_guid(child, target_guid, depth + 1) if result: return result except: pass return "" def _find_parent_by_guid(parent_obj, target_guid): """Рекурсивно ищет объект с target_guid и возвращает имя его родителя.""" try: # Проверяем детей if hasattr(parent_obj, 'get_children'): for child in parent_obj.get_children(False): # Если нашли целевой объект — возвращаем имя родителя if hasattr(child, 'guid') and str(child.guid).lower() == str(target_guid).lower(): parent_name = parent_obj.get_name() if hasattr(parent_obj, 'get_name') else str(parent_obj) # Фильтруем технические папки if parent_name not in ['Methods', 'Properties', 'Actions', 'POUs', 'DUTs', 'GVLs']: return parent_name return "" # Рекурсивно ищем в детях result = _find_parent_by_guid(child, target_guid) if result: return result except: pass return "" # ========================================== # МЕХАНИЗМ СБОРКИ (BUILD/REBUILD/GENERATE CODE) # ========================================== def get_all_applications(): """Возвращает список всех application-объектов в проекте, у которых есть build/rebuild.""" apps = [] try: for node in projects.primary.get_children(): if str(node.type).lower() == parser.CodesysTypes.PLC_DEVICE: logic_nodes = node.find("Plc Logic", recursive=False) if logic_nodes: for logic_node in logic_nodes: for app in logic_node.get_children(): # Проверяем, является ли узел application (имеет метод build) if hasattr(app, 'build') and hasattr(app, 'is_application'): apps.append({ 'plc': node.get_name(), 'app': app.get_name(), 'object': app }) except Exception as e: journal.LogError("Ошибка получения списка приложений: " + str(e)) return apps def collect_build_messages(): """Собирает сообщения компиляции.""" messages = [] max_polls = 10 poll_interval = 0.5 for poll in range(max_polls): try: all_msgs = list(system.get_message_objects( category=COMPILER_CATEGORY, severities=SEVERITY_MASK )) real_count = 0 temp_messages = [] for msg in all_msgs: try: severity = int(msg.severity) text = str(msg.text) if msg.text else '' if not text: continue obj_ref = msg.object obj_name = '' obj_parent = '' obj_owner = '' if obj_ref: try: obj_name = obj_ref.get_name() except: obj_name = str(obj_ref) # Определяем тип узла category = _get_object_type_from_message(obj_ref) # Для PROPERTY_ACCESSOR нужен owner (на 2 уровня выше) if category == 'PROPERTY_ACCESSOR': obj_parent = _get_object_parent(obj_ref) # Имя свойства obj_owner = _get_object_owner(obj_ref) # FB/Program else: obj_parent = _get_object_parent(obj_ref) else: obj_name = '<project>' category = 'PROJECT' line, col = _parse_position_to_line_col(msg, obj_ref) line_text = '' if obj_ref: line_text = _get_error_line_text(obj_ref, msg) msg_category = '' if msg.prefix: try: msg_category = system.get_message_category_description(msg.prefix) except: msg_category = str(msg.prefix) position = int(msg.position) if msg.position >= 0 else 0 msg_data = { 'severity': severity, 'severity_name': _severity_to_name(severity), 'text': text, 'object': obj_name, 'parent': obj_parent, 'category': category, 'msg_category': msg_category, 'line': line, 'column': col, 'line_text': line_text, 'position': position } # Добавляем owner только для accessor'ов if obj_owner: msg_data['owner'] = obj_owner temp_messages.append(msg_data) real_count += 1 except Exception as e: journal.LogWarning("Ошибка парсинга сообщения: {0}".format(e)) continue if real_count > 0: messages = temp_messages journal.LogInfo("Собрано {0} сообщений компиляции".format(real_count)) break if poll < max_polls - 1: system.delay(int(poll_interval * 1000)) except Exception as e: journal.LogError("Ошибка: {0}".format(e)) import traceback journal.LogError(traceback.format_exc()) break return messages def _severity_to_name(sev): if sev & 1: return 'FatalError' if sev & 2: return 'Error' if sev & 4: return 'Warning' if sev & 8: return 'Information' if sev & 16: return 'Text' return 'Unknown' def clear_build_messages(): """Очищает сообщения компиляции.""" try: categories = system.get_message_categories(True) for cat in categories: system.clear_messages(cat) except Exception: pass def run_build(action='build'): """ Выполняет сборку всех приложений в проекте. """ apps = get_all_applications() if not apps: journal.LogError("Нет приложений для сборки!") return (False, [], []) clear_build_messages() results = [] overall_success = True for app_info in apps: plc_name = app_info['plc'] app_name = app_info['app'] app_obj = app_info['object'] app_result = {'plc': plc_name, 'app': app_name, 'action': action, 'success': False} journal.LogInfo("Сборка {0}/{1}: {2}".format(plc_name, app_name, action)) try: if action == 'build': app_obj.build() elif action == 'rebuild': app_obj.rebuild() elif action == 'generate_code': app_obj.generate_code() elif action == 'clean': app_obj.clean() else: journal.LogError("Неизвестное действие сборки: " + action) app_result['error'] = 'Неизвестное действие: ' + action results.append(app_result) continue app_result['success'] = True journal.LogInfo(" ✓ {0} для {1}/{2} завершён".format(action, plc_name, app_name)) except Exception as e: overall_success = False app_result['success'] = False app_result['error'] = str(e) journal.LogError(" ✗ Ошибка {0} для {1}/{2}: {3}".format(action, plc_name, app_name, str(e))) results.append(app_result) journal.LogInfo("Ожидание сообщений компиляции...") time.sleep(1) all_messages = collect_build_messages() # ВАЖНО: Если есть ошибки компиляции — overall_success = False has_errors = any(msg['severity'] == 2 for msg in all_messages) # 2 = Error if has_errors: overall_success = False journal.LogError(">>> СБОРКА ЗАВЕРШЕНА С ОШИБКАМИ <<<") else: journal.LogInfo(">>> СБОРКА {0} ЗАВЕРШЕНА УСПЕШНО <<<".format(action.upper())) return (overall_success, results, all_messages) def send_build_logs(current_ip, token, results, messages): """Отправляет логи сборки на сервер.""" try: log_payload = { 'type': 'build_log', 'success': all(r['success'] for r in results), 'results': results, 'messages': messages } url = "http://{0}:{1}/logs".format(current_ip, HTTP_PORT) payload_json = json.dumps(log_payload, ensure_ascii=False).encode('utf-8') req = urllib2.Request(url, payload_json) req.add_header("X-Auth-Token", token) req.add_header("Content-Type", "application/json; charset=utf-8") response = urllib2.urlopen(req, timeout=5.0) response.read() response.close() journal.LogInfo("Логи сборки отправлены на сервер ({0} сообщений)".format(len(messages))) return True except Exception as e: journal.LogError("Ошибка отправки логов сборки: " + str(e)) return False def get_secret_key(): """Получает имя проекта как SECRET_KEY""" try: project_path = projects.primary.path if project_path: import os file_name = os.path.splitext(os.path.basename(project_path))[0] journal.LogInfo("Используется имя проекта: {0}".format(file_name)) return file_name else: journal.LogError("КРИТИЧЕСКАЯ ОШИБКА: Путь к проекту пустой. Проект не открыт!") raise Exception("Проект не открыт") except AttributeError as e: journal.LogError("КРИТИЧЕСКАЯ ОШИБКА: Проект не загружен или не открыт") raise Exception("Проект не доступен: {0}".format(str(e))) except Exception as e: journal.LogError("КРИТИЧЕСКАЯ ОШИБКА получения имени проекта: {0}".format(str(e))) raise # --- GUI SELECTOR --- class ServerSelector(Form): def __init__(self, servers): self.Text = "Выбор luna сервера" self.Size = Size(350, 180) self.StartPosition = self.StartPosition.CenterScreen self.SelectedInfo = None # Вернет (name, ip) self.Journal = Journal() label = Label(Text="Выберите сервер для текущей сессии:", Location=Point(15, 15), Size=Size(300, 20)) self.combo = ComboBox() self.combo.Location = Point(15, 45) self.combo.Size = Size(300, 30) # Используем DropDownList (значение 2), но через явное указание типа self.combo.DropDownStyle = ComboBoxStyle.DropDownList for name, ip, token in servers: self.combo.Items.Add("{0} ({1})".format(name, ip)) self.combo.SelectedIndex = 0 btn = Button(Text="Подключиться", Location=Point(110, 90), Size=Size(120, 35)) btn.Click += self.on_click self.Controls.Add(label); self.Controls.Add(self.combo); self.Controls.Add(btn) def on_click(self, sender, e): # Извлекаем данные из оригинального списка серверов idx = self.combo.SelectedIndex self.SelectedInfo = self.Tag[idx] # Передаем данные через Tag self.DialogResult = DialogResult.OK self.Close() def get_all_local_ips(): local_ips = [] try: # Получаем имя хоста текущего ПК hostname = socket.gethostname() # Запрашиваем всю информацию об адресах, привязанных к этому имени addr_info = socket.getaddrinfo(hostname, None) for item in addr_info: ip = item[4][0] # Нам нужны только IPv4 (содержат точки) и не "петля" (127.0.0.1) if '.' in ip and not ip.startswith('127.'): if ip not in local_ips: local_ips.append(ip) except Exception as e: journal.LogError("Ошибка получения IP: " + str(e)) return local_ips def discover_all_servers(): """Сканирует все сети и возвращает список [(name, ip), ...]""" ips = get_all_local_ips() # Получаем SECRET_KEY динамически из имени проекта secret_key = get_secret_key() msg = "DISCOVERY_REQ:" + secret_key journal.LogInfo("Используется SECRET_KEY: {0}".format(secret_key)) found = [] for l_ip in ips: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.settimeout(0.5) try: sock.bind((l_ip, 0)) sock.sendto(msg.encode('utf-8'), ('255.255.255.255', UDP_PORT)) while True: raw_data, addr_info = sock.recvfrom(1024) srv_ip = addr_info[0] parts = raw_data.decode('utf-8', 'ignore').split('|') if len(parts) == 3: name, port, token = parts set_saved_token(token) journal.LogInfo("Получен новый токен сессии: " + token) found.append((name, srv_ip, token)) except socket.timeout: continue except Exception as e: journal.LogWarning("Ошибка на {0}: {1}".format(l_ip, e)) finally: sock.close() return found def get_paired_ip(): return AppDomain.CurrentDomain.GetData(IP_KEY) def set_paired_ip(ip): AppDomain.CurrentDomain.SetData(IP_KEY, ip) def get_saved_token(): return AppDomain.CurrentDomain.GetData(TOKEN_KEY) def set_saved_token(token): AppDomain.CurrentDomain.SetData(TOKEN_KEY, token) def detect_all_plc_apps(): """Возвращает список всех пар (plc_name, app_name, app_object)""" result = [] try: # 1. Секция Устройства: Ищем ПЛК и их внутренние Application for node in projects.primary.get_children(): if str(node.type).lower() == parser.CodesysTypes.PLC_DEVICE: plc_name = node.get_name() logic_nodes = node.find("Plc Logic", recursive=False) if logic_nodes: for logic_node in logic_nodes: for app in logic_node.get_children(): result.append((plc_name, app.get_name(), app)) # 2. Секция POU: Добавляем один раз весь корень проекта целиком! result.append(("POU", "Root", projects.primary)) return result except Exception as e: journal.LogError("Ошибка определения ПЛК/App: " + str(e)) return [] def do_sync_tick(sender, e): # Достаем IP из "кармана" процесса token = get_saved_token() current_ip = get_paired_ip() #journal.LogInfo("do_sync_tick. Текущий IP: " + str(current_ip)) if not token or not current_ip: # Пытаемся переподключиться sender.Interval = SYNC_INTERVAL * 20 new_found = discover_all_servers() if new_found and len(new_found) > 0: name, pure_ip, new_token = new_found[0] journal.LogInfo("Связь восстановлена с {0} ({1})".format(name, pure_ip)) set_paired_ip(pure_ip) set_saved_token(new_token) sender.Interval = SYNC_INTERVAL return sync_data = None try: # Проверка: тот ли это сервер url = "http://{0}:{1}/sync".format(current_ip, HTTP_PORT) req_body = json.dumps({"hash": " "}) req = urllib2.Request(url, data=req_body) req.add_header("X-Auth-Token", token) req.add_header("Content-Type", "application/json; charset=utf-8") response = urllib2.urlopen(req, timeout=0.5) sync_data = json.loads(response.read().decode('utf-8')) # Если запрос успешен, восстанавливаем нормальный интервал sender.Interval = SYNC_INTERVAL except urllib2.HTTPError as e: # Это ошибки уровня сервера (401, 404, 500) if e.code == 401: journal.LogWarning("Сессия истекла (401). Запрос нового токена...") # Только при 401 запрашиваем новый токен new_found = discover_all_servers() if new_found and len(new_found) > 0: name, pure_ip, new_token = new_found[0] # Проверяем, что это тот же сервер if pure_ip == current_ip: set_saved_token(new_token) journal.LogInfo("Токен обновлен для {0}".format(name)) sender.Interval = SYNC_INTERVAL return else: journal.LogWarning("IP сервера изменился. Требуется переподключение.") set_paired_ip(None) set_saved_token(None) else: journal.LogError("Не удалось получить новый токен") set_saved_token(None) sender.Interval = SYNC_INTERVAL * 20 else: journal.LogWarning("Ошибка HTTP сервера: {0}".format(e.code)) # Для других HTTP ошибок НЕ сбрасываем токен, только увеличиваем интервал sender.Interval = SYNC_INTERVAL * 5 except urllib2.URLError as ex: # Ошибки сети (таймаут, недоступность хоста) journal.LogWarning("Ошибка сети: {0}".format(str(ex.reason if hasattr(ex, 'reason') else ex))) # При сетевых ошибках НЕ сбрасываем IP и токен, только увеличиваем интервал sender.Interval = SYNC_INTERVAL * 10 except socket.timeout: journal.LogWarning("Таймаут соединения") sender.Interval = SYNC_INTERVAL * 10 except Exception as ex: journal.LogError("Неожиданная ошибка связи: {0}".format(str(ex))) sender.Interval = SYNC_INTERVAL * 10 # ========================================== # КОМАНДА BUILD — сборка проекта # ========================================== if sync_data and sync_data.get('action') == "build": build_action = sync_data.get('build_action', 'build') # build | rebuild | generate_code | clean journal.LogInfo(">>> КОМАНДА СБОРКИ: {0} <<<".format(build_action)) # Останавливаем таймер на время сборки sender.Stop() try: success, results, messages = run_build(build_action) # Отправляем логи на сервер send_build_logs(current_ip, token, results, messages) # Итоговый статус if success: journal.LogInfo(">>> СБОРКА {0} ЗАВЕРШЕНА УСПЕШНО <<<".format(build_action.upper())) else: error_count = sum(1 for m in messages if m['severity'] in (1, 2)) journal.LogWarning(">>> СБОРКА {0} ЗАВЕРШЕНА С {1} ОШИБКАМИ <<<".format( build_action.upper(), error_count )) except Exception as build_ex: journal.LogError("КРИТИЧЕСКАЯ ОШИБКА СБОРКИ: {0}".format(str(build_ex))) finally: sender.Start() return # После сборки пропускаем остальные обработчики # Обработка обычных изменений - синхронизация кода из внешнего редактора (не в режиме download) if sync_data and sync_data.get('changes') and sync_data.get('action') != "download": try: handler = parser.ASTParser(projects.primary, journal, projects) for node in sync_data['changes']: handler.sync_node(node) except Exception as parser_ex: # Ошибка в коде Python или API CODESYS journal.LogError("ОШИБКА ПАРСЕРА : " + str(parser_ex)) # Обработка download - загрузка всего кода из внешнего редактора (быстрая загрузка без пауз) if sync_data and sync_data.get('action') == "download": journal.LogInfo(">>> НАЧАЛО БЫСТРОЙ ЗАГРУЗКИ (download) <<<") # Временно останавливаем таймер для непрерывной загрузки sender.Stop() try: while True: # Запрашиваем данные try: url = "http://{0}:{1}/sync".format(current_ip, HTTP_PORT) req_body = json.dumps({"hash": " "}) req = urllib2.Request(url, data=req_body) req.add_header("X-Auth-Token", token) req.add_header("Content-Type", "application/json; charset=utf-8") response = urllib2.urlopen(req, timeout=1.0) download_data = json.loads(response.read().decode('utf-8')) # Проверяем статус и наличие changes has_changes = download_data.get('changes') and len(download_data.get('changes', [])) > 0 if has_changes: # Есть изменения - применяем handler = parser.ASTParser(projects.primary, journal, projects) for node in download_data['changes']: handler.sync_node(node) journal.LogInfo("Обработано изменений: {0}".format(len(download_data['changes']))) else: # Получили пустой массив changes - выходим из режима download journal.LogInfo(">>> БЫСТРАЯ ЗАГРУЗКА ЗАВЕРШЕНА (получен пустой changes) <<<") break except Exception as dl_ex: journal.LogError("Ошибка при download: {0}".format(str(dl_ex))) break finally: # Возобновляем таймер - возвращаемся к нормальному опросу sender.Start() journal.LogInfo("Возврат к нормальному режиму опроса") # Обработка upload - выгрузка кода из CodeSys if sync_data and sync_data.get('action') == "upload": # Получаем ВСЕ пары ПЛК/Приложение all_plc_apps = detect_all_plc_apps() if not all_plc_apps: journal.LogError("Не найдено ни одного ПЛК/Приложения для выгрузки") return total_count = 0 # Перебираем каждую пару ПЛК/Приложение for plc_name, app_name, app_obj in all_plc_apps: journal.LogInfo(">>> Обработка: ПЛК='{0}' | App='{1}'".format(plc_name, app_name)) exporter = parser.ASTParser(app_obj, journal, projects) node_map = exporter.get_project_map() # Взводим признак глобальной секции POU is_global_pou = (plc_name == "POU") journal.LogWarning("Выгрузка {0} объектов из {1}/{2}".format( len(node_map), plc_name, app_name )) for node_info in node_map: payload_dict = exporter.get_node_payload(node_info) # Наполняем контекст для сервера payload_dict["plc"] = plc_name payload_dict["app"] = app_name # Будет равен "Root" для POU, что защищает от пустого App payload_dict["is_global_pou"] = is_global_pou try: url = "http://{0}:{1}/upload_unit".format(current_ip, HTTP_PORT) payload_json = json.dumps(payload_dict, ensure_ascii=False).encode('utf-8') req = urllib2.Request(url, payload_json) req.add_header("X-Auth-Token", token) req.add_header("Content-Type", "application/json; charset=utf-8") response = urllib2.urlopen(req, timeout=2.0) response.read() response.close() total_count += 1 except urllib2.HTTPError as e: journal.LogError("HTTP {0} на {1}: {2}".format( e.code, node_info['path'], e.read() )) except urllib2.URLError as e: journal.LogError("Сеть недоступна для {0}: {1}".format( node_info['path'], e.reason )) except Exception as e: journal.LogError("Ошибка на {0}: {1}".format(node_info['path'], str(e))) journal.LogInfo("ПОЛНАЯ ВЫГРУЗКА ЗАВЕРШЕНА: {0} объектов из {1} ПЛК/Приложений".format( total_count, len(all_plc_apps) )) # Отправка завершающего сообщения try: completion_payload = {"type": "END_OK"} url = "http://{0}:{1}/upload_unit".format(current_ip, HTTP_PORT) payload_json = json.dumps(completion_payload, ensure_ascii=False).encode('utf-8') req = urllib2.Request(url, payload_json) req.add_header("X-Auth-Token", token) req.add_header("Content-Type", "application/json; charset=utf-8") response = urllib2.urlopen(req, timeout=2.0) response.read() response.close() except Exception as e: journal.LogError("Ошибка отправки завершающего сообщения: {0}".format(str(e))) def get_shared_timer(): return AppDomain.CurrentDomain.GetData(TIMER_KEY) def set_shared_timer(timer_obj): AppDomain.CurrentDomain.SetData(TIMER_KEY, timer_obj) def toggle_sync(): # Пытаемся получить уже работающий таймер existing_timer = get_shared_timer() if existing_timer is not None: existing_timer.Stop() set_shared_timer(None) set_paired_ip(None) set_saved_token(None) # ОБЯЗАТЕЛЬНО очищаем токен при остановке journal.LogWarning(">>> СИНХРОНИЗАЦИЯ ОСТАНОВЛЕНА <<<") return # 2. Поиск и Pairing try: journal.LogInfo("Поиск серверов для авторизации...") all_found = discover_all_servers() if not all_found: journal.LogError("Серверы не найдены!") return # Вызываем GUI selector = ServerSelector(all_found) selector.Tag = all_found if selector.ShowDialog() == DialogResult.OK: paired_server_name, paired_server_ip, paired_server_token = selector.SelectedInfo set_paired_ip(paired_server_ip) set_saved_token(paired_server_token) journal.LogInfo("Установлена связь (Pairing) с: " + paired_server_name + " | " + paired_server_ip) active_sync_timer = Timer() active_sync_timer.Interval = SYNC_INTERVAL active_sync_timer.Tick += do_sync_tick active_sync_timer.Start() set_shared_timer(active_sync_timer) journal.LogInfo("Синхронизация запущена и сохранена в AppDomain.") else: journal.LogText("Подключение отменено пользователем.") except Exception as e: journal.LogError("СИНХРОНИЗАЦИЯ ПРЕРВАНА: {0}".format(str(e))) return if __name__ == "__main__": toggle_sync() { "severity": 2, "severity_name": "Error", "text": "C0035: 'xVar' is not a valid variable name", "object": "FB_Motor", "category": "Compiler", "line": 2, "column": 4, "line_text": "xVar := 10;" }