|
|
|
@ -2,6 +2,7 @@ import json
|
|
|
|
import traceback
|
|
|
|
import traceback
|
|
|
|
import os
|
|
|
|
import os
|
|
|
|
from pathlib import Path
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
import FreeCAD as App
|
|
|
|
import FreeCAD as App
|
|
|
|
import FreeCADGui as Gui
|
|
|
|
import FreeCADGui as Gui
|
|
|
|
@ -74,7 +75,12 @@ def _append_debug_log(message):
|
|
|
|
log_path = _debug_log_path()
|
|
|
|
log_path = _debug_log_path()
|
|
|
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
|
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
|
|
with open(log_path, "a", encoding="utf-8") as handle:
|
|
|
|
with open(log_path, "a", encoding="utf-8") as handle:
|
|
|
|
handle.write(message + "\n")
|
|
|
|
handle.write(
|
|
|
|
|
|
|
|
"[{0}] {1}\n".format(
|
|
|
|
|
|
|
|
datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
|
|
|
|
|
|
message,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
except Exception:
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
@ -97,7 +103,84 @@ def _get_main_window():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _show_info(title, message):
|
|
|
|
def _show_info(title, message):
|
|
|
|
QtWidgets.QMessageBox.information(_get_main_window(), title, message)
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
"_show_info requested: title={0}, message_length={1}".format(
|
|
|
|
|
|
|
|
title, len(message or "")
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
parent = _get_main_window()
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
if all(
|
|
|
|
|
|
|
|
hasattr(QtWidgets, attr)
|
|
|
|
|
|
|
|
for attr in (
|
|
|
|
|
|
|
|
"QDialog",
|
|
|
|
|
|
|
|
"QVBoxLayout",
|
|
|
|
|
|
|
|
"QLabel",
|
|
|
|
|
|
|
|
"QPlainTextEdit",
|
|
|
|
|
|
|
|
"QDialogButtonBox",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
):
|
|
|
|
|
|
|
|
dialog = QtWidgets.QDialog(parent)
|
|
|
|
|
|
|
|
dialog.setWindowTitle(title)
|
|
|
|
|
|
|
|
dialog.setModal(False)
|
|
|
|
|
|
|
|
dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
|
|
|
|
|
|
|
|
dialog.resize(980, 760)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
layout = QtWidgets.QVBoxLayout(dialog)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
title_label = QtWidgets.QLabel(title, dialog)
|
|
|
|
|
|
|
|
layout.addWidget(title_label)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
info_label = QtWidgets.QLabel("同步完成,详细信息如下。", dialog)
|
|
|
|
|
|
|
|
layout.addWidget(info_label)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
details_box = QtWidgets.QPlainTextEdit(dialog)
|
|
|
|
|
|
|
|
details_box.setReadOnly(True)
|
|
|
|
|
|
|
|
details_box.setPlainText(message or "")
|
|
|
|
|
|
|
|
details_box.setMinimumSize(920, 640)
|
|
|
|
|
|
|
|
layout.addWidget(details_box)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
button_box = QtWidgets.QDialogButtonBox(
|
|
|
|
|
|
|
|
QtWidgets.QDialogButtonBox.Ok,
|
|
|
|
|
|
|
|
parent=dialog,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
button_box.accepted.connect(dialog.accept)
|
|
|
|
|
|
|
|
layout.addWidget(button_box)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dialog.show()
|
|
|
|
|
|
|
|
dialog.raise_()
|
|
|
|
|
|
|
|
dialog.activateWindow()
|
|
|
|
|
|
|
|
_append_debug_log("_show_info displayed as resizable non-modal dialog")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dialog = QtWidgets.QMessageBox(parent)
|
|
|
|
|
|
|
|
dialog.setIcon(QtWidgets.QMessageBox.Information)
|
|
|
|
|
|
|
|
dialog.setWindowTitle(title)
|
|
|
|
|
|
|
|
dialog.setText(title)
|
|
|
|
|
|
|
|
dialog.setInformativeText("同步完成,详细信息见下方。")
|
|
|
|
|
|
|
|
dialog.setDetailedText(message or "")
|
|
|
|
|
|
|
|
dialog.setStandardButtons(QtWidgets.QMessageBox.Ok)
|
|
|
|
|
|
|
|
dialog.setModal(False)
|
|
|
|
|
|
|
|
dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
|
|
|
|
|
|
|
|
dialog.show()
|
|
|
|
|
|
|
|
dialog.raise_()
|
|
|
|
|
|
|
|
dialog.activateWindow()
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
details_box = dialog.findChild(QtWidgets.QTextEdit)
|
|
|
|
|
|
|
|
if details_box is not None:
|
|
|
|
|
|
|
|
details_box.setMinimumSize(860, 560)
|
|
|
|
|
|
|
|
dialog.resize(960, 720)
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
_append_debug_log("_show_info displayed as fallback message box")
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
|
|
_append_debug_log("_show_info failed: {0}".format(exc))
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] {0}\n{1}\n".format(title, message or "")
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _show_error(title, message):
|
|
|
|
def _show_error(title, message):
|
|
|
|
@ -131,6 +214,85 @@ def _has_tree_widget_parent(widget):
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _doc_name(doc):
|
|
|
|
|
|
|
|
if doc is None:
|
|
|
|
|
|
|
|
return "<none>"
|
|
|
|
|
|
|
|
return getattr(doc, "Name", "") or "<unnamed>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _doc_path(doc):
|
|
|
|
|
|
|
|
if doc is None:
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
return getattr(doc, "FileName", "") or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _doc_object_count(doc):
|
|
|
|
|
|
|
|
if doc is None:
|
|
|
|
|
|
|
|
return -1
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
return len(list(getattr(doc, "Objects", []) or []))
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
return -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _open_document_descriptions():
|
|
|
|
|
|
|
|
descriptions = []
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
documents = App.listDocuments()
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
|
|
return ["<failed to list documents: {0}>".format(exc)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for doc in documents.values():
|
|
|
|
|
|
|
|
descriptions.append(
|
|
|
|
|
|
|
|
"{0}|objects={1}|path={2}".format(
|
|
|
|
|
|
|
|
_doc_name(doc),
|
|
|
|
|
|
|
|
_doc_object_count(doc),
|
|
|
|
|
|
|
|
_doc_path(doc) or "<unsaved>",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return descriptions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _log_document_state(stage, doc=None, include_open_docs=False):
|
|
|
|
|
|
|
|
active_doc = None
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
active_doc = App.ActiveDocument
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
active_doc = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
target_doc = doc if doc is not None else active_doc
|
|
|
|
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
"{0}: active_doc={1}, active_path={2}, active_objects={3}, target_doc={4}, target_path={5}, target_objects={6}".format(
|
|
|
|
|
|
|
|
stage,
|
|
|
|
|
|
|
|
_doc_name(active_doc),
|
|
|
|
|
|
|
|
_doc_path(active_doc) or "<unsaved>",
|
|
|
|
|
|
|
|
_doc_object_count(active_doc),
|
|
|
|
|
|
|
|
_doc_name(target_doc),
|
|
|
|
|
|
|
|
_doc_path(target_doc) or "<unsaved>",
|
|
|
|
|
|
|
|
_doc_object_count(target_doc),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if include_open_docs:
|
|
|
|
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
"{0}: open_docs={1}".format(
|
|
|
|
|
|
|
|
stage,
|
|
|
|
|
|
|
|
"; ".join(_open_document_descriptions()) or "<none>",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if target_doc is not None:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
root = target_doc.getObject("QETExchangeDevices")
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
root = None
|
|
|
|
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
"{0}: target_root_group={1}, root_children={2}".format(
|
|
|
|
|
|
|
|
stage,
|
|
|
|
|
|
|
|
getattr(root, "Name", "") if root is not None else "<missing>",
|
|
|
|
|
|
|
|
len(list(getattr(root, "Group", []) or [])) if root is not None else 0,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _DeviceTreeDoubleClickFilter(QtCore.QObject):
|
|
|
|
class _DeviceTreeDoubleClickFilter(QtCore.QObject):
|
|
|
|
def eventFilter(self, watched, event):
|
|
|
|
def eventFilter(self, watched, event):
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
@ -312,13 +474,22 @@ def _require_string(payload, field_name):
|
|
|
|
return value.strip()
|
|
|
|
return value.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_instance_id(item):
|
|
|
|
def _normalize_instance_id(item, *field_names):
|
|
|
|
value = item.get("instance_id", "")
|
|
|
|
if not field_names:
|
|
|
|
|
|
|
|
field_names = ("instance_id",)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
value = ""
|
|
|
|
|
|
|
|
matched_field = field_names[0]
|
|
|
|
|
|
|
|
for field_name in field_names:
|
|
|
|
|
|
|
|
if field_name in item:
|
|
|
|
|
|
|
|
matched_field = field_name
|
|
|
|
|
|
|
|
value = item.get(field_name, "")
|
|
|
|
|
|
|
|
break
|
|
|
|
if value is None:
|
|
|
|
if value is None:
|
|
|
|
return ""
|
|
|
|
return ""
|
|
|
|
if not isinstance(value, str):
|
|
|
|
if not isinstance(value, str):
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
"Field 'instance_id' must be a string when present."
|
|
|
|
"Field '{0}' must be a string when present.".format(matched_field)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return value.strip()
|
|
|
|
return value.strip()
|
|
|
|
|
|
|
|
|
|
|
|
@ -334,7 +505,21 @@ def _normalize_devices(payload):
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
"Device entry #{0} must be an object.".format(index)
|
|
|
|
"Device entry #{0} must be an object.".format(index)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
element_uuid = _require_string(item, "element_uuid")
|
|
|
|
entry_label = "device entry #{0}".format(index)
|
|
|
|
|
|
|
|
if "instance_id" in item:
|
|
|
|
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
|
|
|
|
"Field 'instance_id' in {0} is no longer supported. Use 'device_instance_id'.".format(
|
|
|
|
|
|
|
|
entry_label
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if "element_uuid" in item:
|
|
|
|
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
|
|
|
|
"Field 'element_uuid' in {0} is no longer supported at device level. Put element_uuid in devices[].terminals[].".format(
|
|
|
|
|
|
|
|
entry_label
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
element_uuid = ""
|
|
|
|
|
|
|
|
device_instance_id = _require_string(item, "device_instance_id")
|
|
|
|
display_tag = item.get("display_tag", "")
|
|
|
|
display_tag = item.get("display_tag", "")
|
|
|
|
if display_tag and not isinstance(display_tag, str):
|
|
|
|
if display_tag and not isinstance(display_tag, str):
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
@ -351,6 +536,7 @@ def _normalize_devices(payload):
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
normalized_terminals = []
|
|
|
|
normalized_terminals = []
|
|
|
|
|
|
|
|
device_element_uuids = []
|
|
|
|
for terminal_index, terminal_item in enumerate(device_terminals):
|
|
|
|
for terminal_index, terminal_item in enumerate(device_terminals):
|
|
|
|
terminal_entry_label = "device entry #{0} terminal entry #{1}".format(
|
|
|
|
terminal_entry_label = "device entry #{0} terminal entry #{1}".format(
|
|
|
|
index, terminal_index
|
|
|
|
index, terminal_index
|
|
|
|
@ -359,26 +545,52 @@ def _normalize_devices(payload):
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
"{0} must be an object.".format(terminal_entry_label.capitalize())
|
|
|
|
"{0} must be an object.".format(terminal_entry_label.capitalize())
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
if "instance_id" in terminal_item or "device_instance_id" in terminal_item:
|
|
|
|
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
|
|
|
|
"{0} must not carry device instance fields. The parent device's device_instance_id is authoritative.".format(
|
|
|
|
|
|
|
|
terminal_entry_label.capitalize()
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
terminal_uuid = _require_string(terminal_item, "terminal_uuid")
|
|
|
|
terminal_uuid = _require_string(terminal_item, "terminal_uuid")
|
|
|
|
terminal_element_uuid = _optional_string(
|
|
|
|
terminal_element_uuid = _optional_string(
|
|
|
|
terminal_item, "element_uuid", terminal_entry_label
|
|
|
|
terminal_item, "element_uuid", terminal_entry_label
|
|
|
|
) or element_uuid
|
|
|
|
)
|
|
|
|
|
|
|
|
if not terminal_element_uuid:
|
|
|
|
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
|
|
|
|
"{0} is missing element_uuid.".format(
|
|
|
|
|
|
|
|
terminal_entry_label.capitalize()
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if terminal_element_uuid and terminal_element_uuid not in device_element_uuids:
|
|
|
|
|
|
|
|
device_element_uuids.append(terminal_element_uuid)
|
|
|
|
normalized_terminals.append(
|
|
|
|
normalized_terminals.append(
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"terminal_uuid": terminal_uuid,
|
|
|
|
"terminal_uuid": terminal_uuid,
|
|
|
|
"instance_id": _normalize_instance_id(terminal_item)
|
|
|
|
"instance_id": device_instance_id,
|
|
|
|
or _normalize_instance_id(item),
|
|
|
|
|
|
|
|
"element_uuid": terminal_element_uuid,
|
|
|
|
"element_uuid": terminal_element_uuid,
|
|
|
|
"terminal_display": _optional_string(
|
|
|
|
"terminal_display": _optional_string(
|
|
|
|
terminal_item, "terminal_display", terminal_entry_label
|
|
|
|
terminal_item, "terminal_display", terminal_entry_label
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
"slot_name_hint": _optional_string(
|
|
|
|
|
|
|
|
terminal_item, "slot_name_hint", terminal_entry_label
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"terminal_label": _optional_string(
|
|
|
|
|
|
|
|
terminal_item, "terminal_label", terminal_entry_label
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"terminal_instance_id": _optional_string(
|
|
|
|
|
|
|
|
terminal_item, "terminal_instance_id", terminal_entry_label
|
|
|
|
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not element_uuid and device_element_uuids:
|
|
|
|
|
|
|
|
element_uuid = device_element_uuids[0]
|
|
|
|
|
|
|
|
|
|
|
|
normalized.append(
|
|
|
|
normalized.append(
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"element_uuid": element_uuid,
|
|
|
|
"element_uuid": element_uuid,
|
|
|
|
"instance_id": _normalize_instance_id(item),
|
|
|
|
"element_uuids": list(device_element_uuids),
|
|
|
|
|
|
|
|
"instance_id": device_instance_id,
|
|
|
|
"display_tag": display_tag.strip() if isinstance(display_tag, str) else "",
|
|
|
|
"display_tag": display_tag.strip() if isinstance(display_tag, str) else "",
|
|
|
|
"terminals": normalized_terminals,
|
|
|
|
"terminals": normalized_terminals,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -390,10 +602,38 @@ def _normalize_terminals(devices):
|
|
|
|
normalized = []
|
|
|
|
normalized = []
|
|
|
|
for device in devices:
|
|
|
|
for device in devices:
|
|
|
|
for terminal in device.get("terminals", []) or []:
|
|
|
|
for terminal in device.get("terminals", []) or []:
|
|
|
|
normalized.append(dict(terminal))
|
|
|
|
entry = dict(terminal)
|
|
|
|
|
|
|
|
if not entry.get("instance_id"):
|
|
|
|
|
|
|
|
entry["instance_id"] = device.get("instance_id", "")
|
|
|
|
|
|
|
|
normalized.append(entry)
|
|
|
|
return normalized
|
|
|
|
return normalized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_top_level_terminals(payload):
|
|
|
|
|
|
|
|
if "terminals" in payload:
|
|
|
|
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
|
|
|
|
"Field 'terminals' at the JSON root is no longer supported. Use devices[].terminals[]."
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _merge_terminal_entries(*terminal_groups):
|
|
|
|
|
|
|
|
merged = []
|
|
|
|
|
|
|
|
seen = set()
|
|
|
|
|
|
|
|
for terminal_group in terminal_groups:
|
|
|
|
|
|
|
|
for item in terminal_group:
|
|
|
|
|
|
|
|
key = (
|
|
|
|
|
|
|
|
item.get("terminal_uuid", ""),
|
|
|
|
|
|
|
|
item.get("element_uuid", ""),
|
|
|
|
|
|
|
|
item.get("instance_id", ""),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if key in seen:
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
seen.add(key)
|
|
|
|
|
|
|
|
merged.append(item)
|
|
|
|
|
|
|
|
return merged
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _optional_string(item, field_name, entry_label):
|
|
|
|
def _optional_string(item, field_name, entry_label):
|
|
|
|
value = item.get(field_name, "")
|
|
|
|
value = item.get(field_name, "")
|
|
|
|
if value is None:
|
|
|
|
if value is None:
|
|
|
|
@ -482,7 +722,21 @@ def _normalize_device_models(payload):
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
"Device model entry #{0} must be an object.".format(index)
|
|
|
|
"Device model entry #{0} must be an object.".format(index)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
element_uuid = _require_string(item, "element_uuid")
|
|
|
|
entry_label = "device model entry #{0}".format(index)
|
|
|
|
|
|
|
|
if "instance_id" in item:
|
|
|
|
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
|
|
|
|
"Field 'instance_id' in {0} is no longer supported. Use 'device_instance_id'.".format(
|
|
|
|
|
|
|
|
entry_label
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if "element_uuid" in item:
|
|
|
|
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
|
|
|
|
"Field 'element_uuid' in {0} is no longer supported. Use 'device_instance_id'.".format(
|
|
|
|
|
|
|
|
entry_label
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
element_uuid = ""
|
|
|
|
|
|
|
|
instance_id = _require_string(item, "device_instance_id")
|
|
|
|
parts_3d = item.get("parts_3d", "")
|
|
|
|
parts_3d = item.get("parts_3d", "")
|
|
|
|
if parts_3d and not isinstance(parts_3d, str):
|
|
|
|
if parts_3d and not isinstance(parts_3d, str):
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
raise ExchangeValidationError(
|
|
|
|
@ -509,6 +763,7 @@ def _normalize_device_models(payload):
|
|
|
|
normalized.append(
|
|
|
|
normalized.append(
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"element_uuid": element_uuid,
|
|
|
|
"element_uuid": element_uuid,
|
|
|
|
|
|
|
|
"instance_id": instance_id,
|
|
|
|
"device_id": device_id,
|
|
|
|
"device_id": device_id,
|
|
|
|
"parts_3d": parts_3d.strip() if isinstance(parts_3d, str) else "",
|
|
|
|
"parts_3d": parts_3d.strip() if isinstance(parts_3d, str) else "",
|
|
|
|
"resolved_model_path": (
|
|
|
|
"resolved_model_path": (
|
|
|
|
@ -583,6 +838,11 @@ def load_exchange_payload(json_path):
|
|
|
|
|
|
|
|
|
|
|
|
normalized_devices = _normalize_devices(payload)
|
|
|
|
normalized_devices = _normalize_devices(payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
normalized_terminals = _merge_terminal_entries(
|
|
|
|
|
|
|
|
_normalize_terminals(normalized_devices),
|
|
|
|
|
|
|
|
_normalize_top_level_terminals(payload),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
normalized = {
|
|
|
|
normalized = {
|
|
|
|
"schema_version": schema_version.strip(),
|
|
|
|
"schema_version": schema_version.strip(),
|
|
|
|
"project_uuid": project_uuid,
|
|
|
|
"project_uuid": project_uuid,
|
|
|
|
@ -590,7 +850,7 @@ def load_exchange_payload(json_path):
|
|
|
|
"source": payload.get("source", {}),
|
|
|
|
"source": payload.get("source", {}),
|
|
|
|
"cabinet": _normalize_cabinet(payload),
|
|
|
|
"cabinet": _normalize_cabinet(payload),
|
|
|
|
"devices": normalized_devices,
|
|
|
|
"devices": normalized_devices,
|
|
|
|
"terminals": _normalize_terminals(normalized_devices),
|
|
|
|
"terminals": normalized_terminals,
|
|
|
|
"device_models": _normalize_device_models(payload),
|
|
|
|
"device_models": _normalize_device_models(payload),
|
|
|
|
"wires": _normalize_wires(payload),
|
|
|
|
"wires": _normalize_wires(payload),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -658,9 +918,15 @@ def _import_wiring_tasks(payload):
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_SCENE_FILE_NAME = "QETScene.FCStd"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _scene_path_from_exchange_context():
|
|
|
|
def _scene_path_from_exchange_context():
|
|
|
|
scene_path = os.environ.get(ENV_SCENE_PATH, "").strip()
|
|
|
|
scene_path = os.environ.get(ENV_SCENE_PATH, "").strip()
|
|
|
|
if scene_path:
|
|
|
|
if scene_path:
|
|
|
|
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
"_scene_path_from_exchange_context using env path: {0}".format(scene_path)
|
|
|
|
|
|
|
|
)
|
|
|
|
return scene_path
|
|
|
|
return scene_path
|
|
|
|
|
|
|
|
|
|
|
|
json_path = os.environ.get(ENV_JSON_PATH, "").strip()
|
|
|
|
json_path = os.environ.get(ENV_JSON_PATH, "").strip()
|
|
|
|
@ -668,18 +934,42 @@ def _scene_path_from_exchange_context():
|
|
|
|
return ""
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
exchange_dir = Path(json_path).parent
|
|
|
|
exchange_dir = Path(json_path).parent
|
|
|
|
for file_name in ("QETScene.FCStd", "scene.FCStd"):
|
|
|
|
candidate = exchange_dir / DEFAULT_SCENE_FILE_NAME
|
|
|
|
candidate = exchange_dir / file_name
|
|
|
|
if candidate.is_file():
|
|
|
|
if candidate.is_file():
|
|
|
|
os.environ[ENV_SCENE_PATH] = str(candidate)
|
|
|
|
resolved = str(candidate)
|
|
|
|
_append_debug_log(
|
|
|
|
os.environ[ENV_SCENE_PATH] = resolved
|
|
|
|
"QET_FREECAD_SCENE_FILE found: {0}".format(str(candidate))
|
|
|
|
_append_debug_log(
|
|
|
|
)
|
|
|
|
"QET_FREECAD_SCENE_FILE inferred from exchange directory: {0}".format(
|
|
|
|
return str(candidate)
|
|
|
|
resolved
|
|
|
|
|
|
|
|
)
|
|
|
|
# No existing scene file -> first time open
|
|
|
|
)
|
|
|
|
default_scene = str(exchange_dir / DEFAULT_SCENE_FILE_NAME)
|
|
|
|
return resolved
|
|
|
|
_append_debug_log(
|
|
|
|
return ""
|
|
|
|
"No existing scene file, first open mode: default_scene={0}".format(default_scene)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return default_scene
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_scene_first_open(scene_path):
|
|
|
|
|
|
|
|
"""Return True if this is the first time the 2D/3D exchange has run for this project.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Uses 3d_to_2d.json (always written by ExchangeWriteBack after every import)
|
|
|
|
|
|
|
|
rather than the .FCStd file, since the user might not have saved the scene.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
if not scene_path:
|
|
|
|
|
|
|
|
_append_debug_log("_is_scene_first_open: scene_path empty -> True")
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
writeback_path = Path(scene_path).parent / "3d_to_2d.json"
|
|
|
|
|
|
|
|
is_first_open = not writeback_path.is_file()
|
|
|
|
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
"_is_scene_first_open: scene_path={0}, writeback_path={1}, exists={2}, result={3}".format(
|
|
|
|
|
|
|
|
scene_path,
|
|
|
|
|
|
|
|
str(writeback_path),
|
|
|
|
|
|
|
|
writeback_path.is_file(),
|
|
|
|
|
|
|
|
is_first_open,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return is_first_open
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mark_stale_objects(payload):
|
|
|
|
def _mark_stale_objects(payload):
|
|
|
|
@ -687,11 +977,43 @@ def _mark_stale_objects(payload):
|
|
|
|
_append_debug_log("stale object sync skipped: StaleObjectSync module unavailable")
|
|
|
|
_append_debug_log("stale object sync skipped: StaleObjectSync module unavailable")
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Diagnostic: count device groups in doc and element_uuids in payload
|
|
|
|
|
|
|
|
doc = App.ActiveDocument
|
|
|
|
|
|
|
|
if doc is not None:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
doc_device_count = sum(
|
|
|
|
|
|
|
|
1 for _ in StaleObjectSync._iter_device_groups(doc)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
doc_device_count = -1
|
|
|
|
|
|
|
|
payload_device_count = len(payload.get("devices", []) or [])
|
|
|
|
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
"stale sync diagnostic: doc_device_groups={0}, payload_devices={1}".format(
|
|
|
|
|
|
|
|
doc_device_count, payload_device_count
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
# Log each payload device element_uuid for comparison
|
|
|
|
|
|
|
|
for item in (payload.get("devices", []) or [])[:10]:
|
|
|
|
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
" payload device: element_uuid={0}, instance_id={1}".format(
|
|
|
|
|
|
|
|
item.get("element_uuid", ""), item.get("instance_id", "")
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
return StaleObjectSync.mark_stale_objects_from_payload(
|
|
|
|
result = StaleObjectSync.mark_stale_objects_from_payload(
|
|
|
|
payload,
|
|
|
|
payload,
|
|
|
|
App.ActiveDocument,
|
|
|
|
doc,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
"stale sync result: active_devices={0}, stale_devices={1}, active_cabinets={2}, stale_cabinets={3}".format(
|
|
|
|
|
|
|
|
result.get("active_devices", 0),
|
|
|
|
|
|
|
|
result.get("stale_devices", 0),
|
|
|
|
|
|
|
|
result.get("active_cabinets", 0),
|
|
|
|
|
|
|
|
result.get("stale_cabinets", 0),
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
return result
|
|
|
|
except Exception as exc:
|
|
|
|
except Exception as exc:
|
|
|
|
_append_debug_log("stale object sync failed: {0}".format(exc))
|
|
|
|
_append_debug_log("stale object sync failed: {0}".format(exc))
|
|
|
|
_append_debug_log(traceback.format_exc())
|
|
|
|
_append_debug_log(traceback.format_exc())
|
|
|
|
@ -704,16 +1026,56 @@ def _summary_message(summary, import_report=None, terminal_report=None, writebac
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if import_report or stale_report:
|
|
|
|
if import_report or stale_report:
|
|
|
|
lines.extend(
|
|
|
|
lines.append("")
|
|
|
|
[
|
|
|
|
updated_device_details = import_report.get("updated_device_details", []) if import_report else []
|
|
|
|
"",
|
|
|
|
if summary.get("is_first_open"):
|
|
|
|
"同步结果:",
|
|
|
|
lines.extend(
|
|
|
|
"新增机柜:{0}".format(import_report.get("cabinet_added", 0) if import_report else 0),
|
|
|
|
[
|
|
|
|
"失效机柜:{0}".format(stale_report.get("stale_cabinets", 0) if stale_report else 0),
|
|
|
|
"同步模式:首次打开(全量导入)",
|
|
|
|
"新增设备:{0}".format(import_report.get("imported_devices", 0) if import_report else 0),
|
|
|
|
"新增机柜:{0}".format(import_report.get("cabinet_added", 0) if import_report else 0),
|
|
|
|
"失效设备:{0}".format(stale_report.get("stale_devices", 0) if stale_report else 0),
|
|
|
|
"新增设备:{0}".format(import_report.get("imported_devices", 0) if import_report else 0),
|
|
|
|
]
|
|
|
|
"更新设备:{0}".format(import_report.get("updated_devices", 0) if import_report else 0),
|
|
|
|
)
|
|
|
|
]
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
lines.extend(
|
|
|
|
|
|
|
|
[
|
|
|
|
|
|
|
|
"同步模式:再次打开(增量更新)",
|
|
|
|
|
|
|
|
"新增机柜:{0}".format(import_report.get("cabinet_added", 0) if import_report else 0),
|
|
|
|
|
|
|
|
"失效机柜:{0}".format(stale_report.get("stale_cabinets", 0) if stale_report else 0),
|
|
|
|
|
|
|
|
"新增设备:{0}".format(import_report.get("imported_devices", 0) if import_report else 0),
|
|
|
|
|
|
|
|
"更新设备:{0}".format(import_report.get("updated_devices", 0) if import_report else 0),
|
|
|
|
|
|
|
|
"失效设备:{0}".format(stale_report.get("stale_devices", 0) if stale_report else 0),
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if updated_device_details:
|
|
|
|
|
|
|
|
lines.append("修改设备:")
|
|
|
|
|
|
|
|
for item in updated_device_details[:5]:
|
|
|
|
|
|
|
|
change_types = " + ".join(item.get("change_types", []) or []) or "未知变化"
|
|
|
|
|
|
|
|
detail_bits = []
|
|
|
|
|
|
|
|
if "标注" in (item.get("change_types", []) or []):
|
|
|
|
|
|
|
|
previous_display_tag = item.get("previous_display_tag", "") or "<empty>"
|
|
|
|
|
|
|
|
current_display_tag = item.get("display_tag", "") or "<empty>"
|
|
|
|
|
|
|
|
detail_bits.append(
|
|
|
|
|
|
|
|
"标注 {0} -> {1}".format(previous_display_tag, current_display_tag)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if "端子" in (item.get("change_types", []) or []):
|
|
|
|
|
|
|
|
added_terms = item.get("added_terminal_uuids", []) or []
|
|
|
|
|
|
|
|
removed_terms = item.get("removed_terminal_uuids", []) or []
|
|
|
|
|
|
|
|
detail_bits.append("+{0}/-{1} 端子".format(len(added_terms), len(removed_terms)))
|
|
|
|
|
|
|
|
detail_suffix = ""
|
|
|
|
|
|
|
|
if detail_bits:
|
|
|
|
|
|
|
|
detail_suffix = " ({0})".format(", ".join(detail_bits))
|
|
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
|
|
"- {0} [{1}] -> {2}{3}".format(
|
|
|
|
|
|
|
|
item.get("label", "") or item.get("display_tag", "") or item.get("instance_id", ""),
|
|
|
|
|
|
|
|
item.get("instance_id", "") or "<empty>",
|
|
|
|
|
|
|
|
change_types,
|
|
|
|
|
|
|
|
detail_suffix,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if len(updated_device_details) > 5:
|
|
|
|
|
|
|
|
lines.append("- ... ({0} more)".format(len(updated_device_details) - 5))
|
|
|
|
|
|
|
|
|
|
|
|
lines.extend(
|
|
|
|
lines.extend(
|
|
|
|
[
|
|
|
|
[
|
|
|
|
@ -761,7 +1123,10 @@ def _summary_message(summary, import_report=None, terminal_report=None, writebac
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if summary["scene_path"]:
|
|
|
|
if summary["scene_path"]:
|
|
|
|
lines.append("Scene file: {0}".format(summary["scene_path"]))
|
|
|
|
if summary.get("is_first_open"):
|
|
|
|
|
|
|
|
lines.append("Scene file (new): {0}".format(summary["scene_path"]))
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
lines.append("Scene file (existing): {0}".format(summary["scene_path"]))
|
|
|
|
|
|
|
|
|
|
|
|
if import_report:
|
|
|
|
if import_report:
|
|
|
|
lines.extend(
|
|
|
|
lines.extend(
|
|
|
|
@ -775,8 +1140,53 @@ def _summary_message(summary, import_report=None, terminal_report=None, writebac
|
|
|
|
"Reused cabinets: {0}".format(import_report.get("cabinet_reused", 0)),
|
|
|
|
"Reused cabinets: {0}".format(import_report.get("cabinet_reused", 0)),
|
|
|
|
"Imported devices: {0}".format(import_report["imported_devices"]),
|
|
|
|
"Imported devices: {0}".format(import_report["imported_devices"]),
|
|
|
|
"Updated devices: {0}".format(import_report["updated_devices"]),
|
|
|
|
"Updated devices: {0}".format(import_report["updated_devices"]),
|
|
|
|
|
|
|
|
"Reused devices: {0}".format(import_report.get("reused_devices", 0)),
|
|
|
|
]
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
added_device_details = import_report.get("added_device_details", [])
|
|
|
|
|
|
|
|
if added_device_details:
|
|
|
|
|
|
|
|
lines.append("Added device details:")
|
|
|
|
|
|
|
|
for item in added_device_details[:10]:
|
|
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
|
|
"- {0} [{1}]".format(
|
|
|
|
|
|
|
|
item.get("label", "") or item.get("display_tag", "") or item.get("instance_id", ""),
|
|
|
|
|
|
|
|
item.get("instance_id", "") or "<empty>",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if len(added_device_details) > 10:
|
|
|
|
|
|
|
|
lines.append("- ... ({0} more)".format(len(added_device_details) - 10))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updated_device_details = import_report.get("updated_device_details", [])
|
|
|
|
|
|
|
|
if updated_device_details:
|
|
|
|
|
|
|
|
lines.append("Updated device details:")
|
|
|
|
|
|
|
|
for item in updated_device_details[:10]:
|
|
|
|
|
|
|
|
change_types = " + ".join(item.get("change_types", []) or []) or "未知变化"
|
|
|
|
|
|
|
|
terminal_bits = []
|
|
|
|
|
|
|
|
added_terms = item.get("added_terminal_uuids", []) or []
|
|
|
|
|
|
|
|
removed_terms = item.get("removed_terminal_uuids", []) or []
|
|
|
|
|
|
|
|
if "端子" in (item.get("change_types", []) or []):
|
|
|
|
|
|
|
|
terminal_bits.append("+{0}".format(len(added_terms)))
|
|
|
|
|
|
|
|
terminal_bits.append("-{0}".format(len(removed_terms)))
|
|
|
|
|
|
|
|
if "标注" in (item.get("change_types", []) or []):
|
|
|
|
|
|
|
|
previous_display_tag = item.get("previous_display_tag", "") or "<empty>"
|
|
|
|
|
|
|
|
current_display_tag = item.get("display_tag", "") or "<empty>"
|
|
|
|
|
|
|
|
terminal_bits.append(
|
|
|
|
|
|
|
|
"标注 {0} -> {1}".format(previous_display_tag, current_display_tag)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
detail_suffix = ""
|
|
|
|
|
|
|
|
if terminal_bits:
|
|
|
|
|
|
|
|
detail_suffix = " ({0})".format(", ".join(terminal_bits))
|
|
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
|
|
"- {0} [{1}] -> {2}{3}".format(
|
|
|
|
|
|
|
|
item.get("label", "") or item.get("display_tag", "") or item.get("instance_id", ""),
|
|
|
|
|
|
|
|
item.get("instance_id", "") or "<empty>",
|
|
|
|
|
|
|
|
change_types,
|
|
|
|
|
|
|
|
detail_suffix,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if len(updated_device_details) > 10:
|
|
|
|
|
|
|
|
lines.append("- ... ({0} more)".format(len(updated_device_details) - 10))
|
|
|
|
|
|
|
|
|
|
|
|
if import_report["imported_without_instance_id"]:
|
|
|
|
if import_report["imported_without_instance_id"]:
|
|
|
|
lines.append(
|
|
|
|
lines.append(
|
|
|
|
"Imported without instance_id yet: {0}".format(
|
|
|
|
"Imported without instance_id yet: {0}".format(
|
|
|
|
@ -877,6 +1287,18 @@ def _summary_message(summary, import_report=None, terminal_report=None, writebac
|
|
|
|
"Stale routed wires: {0}".format(stale_report.get("stale_routed_wires", 0)),
|
|
|
|
"Stale routed wires: {0}".format(stale_report.get("stale_routed_wires", 0)),
|
|
|
|
]
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
stale_device_details = stale_report.get("stale_device_details", [])
|
|
|
|
|
|
|
|
if stale_device_details:
|
|
|
|
|
|
|
|
lines.append("Stale device details:")
|
|
|
|
|
|
|
|
for item in stale_device_details[:10]:
|
|
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
|
|
"- {0} [{1}]".format(
|
|
|
|
|
|
|
|
item.get("label", "") or item.get("display_tag", "") or item.get("instance_id", ""),
|
|
|
|
|
|
|
|
item.get("instance_id", "") or "<empty>",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if len(stale_device_details) > 10:
|
|
|
|
|
|
|
|
lines.append("- ... ({0} more)".format(len(stale_device_details) - 10))
|
|
|
|
|
|
|
|
|
|
|
|
lines.append("")
|
|
|
|
lines.append("")
|
|
|
|
lines.append("This step validates the exchange payload and imports devices with valid resolved model paths.")
|
|
|
|
lines.append("This step validates the exchange payload and imports devices with valid resolved model paths.")
|
|
|
|
@ -888,6 +1310,10 @@ def _run_scheduled_device_import(attempt=0):
|
|
|
|
_append_debug_log(
|
|
|
|
_append_debug_log(
|
|
|
|
"scheduled device import invoked: attempt={0}".format(attempt)
|
|
|
|
"scheduled device import invoked: attempt={0}".format(attempt)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import state before gui readiness check",
|
|
|
|
|
|
|
|
include_open_docs=True,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not _is_gui_ready():
|
|
|
|
if not _is_gui_ready():
|
|
|
|
if attempt < IMPORT_READY_MAX_RETRIES:
|
|
|
|
if attempt < IMPORT_READY_MAX_RETRIES:
|
|
|
|
@ -916,8 +1342,17 @@ def _run_scheduled_device_import(attempt=0):
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
scene_path = _scene_path_from_exchange_context()
|
|
|
|
scene_path = _scene_path_from_exchange_context()
|
|
|
|
|
|
|
|
is_first_open = _is_scene_first_open(scene_path)
|
|
|
|
|
|
|
|
summary["scene_path"] = scene_path
|
|
|
|
|
|
|
|
summary["is_first_open"] = is_first_open
|
|
|
|
_append_debug_log(
|
|
|
|
_append_debug_log(
|
|
|
|
"scheduled device import starting with scene_path={0}".format(scene_path)
|
|
|
|
"scheduled device import: scene_path={0}, is_first_open={1}".format(
|
|
|
|
|
|
|
|
scene_path, is_first_open
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import before DeviceImport",
|
|
|
|
|
|
|
|
include_open_docs=True,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
import_report = DeviceImport.import_devices_from_payload(payload, scene_path)
|
|
|
|
import_report = DeviceImport.import_devices_from_payload(payload, scene_path)
|
|
|
|
@ -946,6 +1381,11 @@ def _run_scheduled_device_import(attempt=0):
|
|
|
|
import_report["skipped_import_error"],
|
|
|
|
import_report["skipped_import_error"],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import after DeviceImport",
|
|
|
|
|
|
|
|
App.ActiveDocument,
|
|
|
|
|
|
|
|
include_open_docs=True,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
"[FreeCADExchange] Loaded exchange payload from {0}\n".format(
|
|
|
|
"[FreeCADExchange] Loaded exchange payload from {0}\n".format(
|
|
|
|
@ -973,10 +1413,62 @@ def _run_scheduled_device_import(attempt=0):
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Sync banner ──
|
|
|
|
|
|
|
|
cabinet_added = import_report.get("cabinet_added", 0)
|
|
|
|
|
|
|
|
if is_first_open:
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] ========================================\n"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] 同步模式:首次打开(全量导入)\n"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] 新增机柜:{0}\n".format(cabinet_added)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] 新增设备:{0}\n".format(import_report["imported_devices"])
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] ========================================\n"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
"sync banner: first open, cabinets={0}, devices={1}".format(
|
|
|
|
|
|
|
|
cabinet_added, import_report["imported_devices"]
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] ========================================\n"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] 同步模式:再次打开(增量更新)\n"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] 新增机柜:{0}\n".format(cabinet_added)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] 新增设备:{0}\n".format(import_report["imported_devices"])
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] 更新设备:{0}\n".format(import_report["updated_devices"])
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
|
|
|
|
"[FreeCADExchange] ========================================\n"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
_append_debug_log(
|
|
|
|
|
|
|
|
"sync banner: reopen, cabinets={0}, imported={1}, updated={2}".format(
|
|
|
|
|
|
|
|
cabinet_added, import_report["imported_devices"], import_report["updated_devices"]
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if TerminalImport is None:
|
|
|
|
if TerminalImport is None:
|
|
|
|
_append_debug_log("terminal import skipped: TerminalImport module unavailable")
|
|
|
|
_append_debug_log("terminal import skipped: TerminalImport module unavailable")
|
|
|
|
terminal_report = _terminal_report_not_available()
|
|
|
|
terminal_report = _terminal_report_not_available()
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import before TerminalImport",
|
|
|
|
|
|
|
|
App.ActiveDocument,
|
|
|
|
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
terminal_report = TerminalImport.import_terminals_from_payload(payload, scene_path)
|
|
|
|
terminal_report = TerminalImport.import_terminals_from_payload(payload, scene_path)
|
|
|
|
except TerminalImport.TerminalImportError as exc:
|
|
|
|
except TerminalImport.TerminalImportError as exc:
|
|
|
|
@ -992,22 +1484,50 @@ def _run_scheduled_device_import(attempt=0):
|
|
|
|
"[FreeCADExchange] Failed to import terminals: {0}\n".format(exc)
|
|
|
|
"[FreeCADExchange] Failed to import terminals: {0}\n".format(exc)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import after TerminalImport",
|
|
|
|
|
|
|
|
App.ActiveDocument,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
setattr(App, STATE_TERMINAL_IMPORT_REPORT, terminal_report)
|
|
|
|
setattr(App, STATE_TERMINAL_IMPORT_REPORT, terminal_report)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import before wiring init",
|
|
|
|
|
|
|
|
App.ActiveDocument,
|
|
|
|
|
|
|
|
)
|
|
|
|
_initialize_wiring_scene(payload)
|
|
|
|
_initialize_wiring_scene(payload)
|
|
|
|
wiring_report = _import_wiring_tasks(payload)
|
|
|
|
wiring_report = _import_wiring_tasks(payload)
|
|
|
|
if wiring_report is not None:
|
|
|
|
if wiring_report is not None:
|
|
|
|
setattr(App, STATE_WIRING_IMPORT_REPORT, wiring_report)
|
|
|
|
setattr(App, STATE_WIRING_IMPORT_REPORT, wiring_report)
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import after wiring import",
|
|
|
|
|
|
|
|
App.ActiveDocument,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
stale_report = _mark_stale_objects(payload)
|
|
|
|
if is_first_open:
|
|
|
|
if stale_report is not None:
|
|
|
|
_append_debug_log("stale object sync skipped: first open (no prior 3D state to compare)")
|
|
|
|
setattr(App, STATE_STALE_SYNC_REPORT, stale_report)
|
|
|
|
stale_report = None
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import before stale sync",
|
|
|
|
|
|
|
|
App.ActiveDocument,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
stale_report = _mark_stale_objects(payload)
|
|
|
|
|
|
|
|
if stale_report is not None:
|
|
|
|
|
|
|
|
setattr(App, STATE_STALE_SYNC_REPORT, stale_report)
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import after stale sync",
|
|
|
|
|
|
|
|
App.ActiveDocument,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if ExchangeWriteBack is None:
|
|
|
|
if ExchangeWriteBack is None:
|
|
|
|
_append_debug_log("write-back skipped: ExchangeWriteBack module unavailable")
|
|
|
|
_append_debug_log("write-back skipped: ExchangeWriteBack module unavailable")
|
|
|
|
writeback_report = None
|
|
|
|
writeback_report = None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import before write-back",
|
|
|
|
|
|
|
|
App.ActiveDocument,
|
|
|
|
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
writeback_report = ExchangeWriteBack.write_back_document(
|
|
|
|
writeback_report = ExchangeWriteBack.write_back_document(
|
|
|
|
App.ActiveDocument, scene_path=scene_path, payload=payload
|
|
|
|
App.ActiveDocument, scene_path=scene_path, payload=payload
|
|
|
|
@ -1018,6 +1538,11 @@ def _run_scheduled_device_import(attempt=0):
|
|
|
|
writeback_report = None
|
|
|
|
writeback_report = None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
setattr(App, STATE_WRITEBACK_REPORT, writeback_report)
|
|
|
|
setattr(App, STATE_WRITEBACK_REPORT, writeback_report)
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import after write-back",
|
|
|
|
|
|
|
|
App.ActiveDocument,
|
|
|
|
|
|
|
|
include_open_docs=True,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
App.Console.PrintMessage(
|
|
|
|
"[FreeCADExchange] Imported terminals: {0}, updated: {1}, removed: {2}\n".format(
|
|
|
|
"[FreeCADExchange] Imported terminals: {0}, updated: {1}, removed: {2}\n".format(
|
|
|
|
@ -1032,12 +1557,18 @@ def _run_scheduled_device_import(attempt=0):
|
|
|
|
_summary_message(summary, import_report, terminal_report, writeback_report, wiring_report, stale_report),
|
|
|
|
_summary_message(summary, import_report, terminal_report, writeback_report, wiring_report, stale_report),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
_append_debug_log("summary dialog shown")
|
|
|
|
_append_debug_log("summary dialog shown")
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"scheduled device import completed",
|
|
|
|
|
|
|
|
App.ActiveDocument,
|
|
|
|
|
|
|
|
include_open_docs=True,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bootstrap_if_requested():
|
|
|
|
def bootstrap_if_requested():
|
|
|
|
if not getattr(App, STATE_FLAG, False):
|
|
|
|
if not getattr(App, STATE_FLAG, False):
|
|
|
|
_reset_debug_log()
|
|
|
|
_reset_debug_log()
|
|
|
|
_append_debug_log("bootstrap_if_requested entered")
|
|
|
|
_append_debug_log("bootstrap_if_requested entered")
|
|
|
|
|
|
|
|
_log_document_state("bootstrap_if_requested initial state", include_open_docs=True)
|
|
|
|
_install_tree_double_click_filter()
|
|
|
|
_install_tree_double_click_filter()
|
|
|
|
if getattr(App, STATE_FLAG, False):
|
|
|
|
if getattr(App, STATE_FLAG, False):
|
|
|
|
_append_debug_log("bootstrap_if_requested skipped: already bootstrapped")
|
|
|
|
_append_debug_log("bootstrap_if_requested skipped: already bootstrapped")
|
|
|
|
@ -1052,6 +1583,7 @@ def bootstrap_if_requested():
|
|
|
|
|
|
|
|
|
|
|
|
setattr(App, STATE_FLAG, True)
|
|
|
|
setattr(App, STATE_FLAG, True)
|
|
|
|
_append_debug_log("STATE_FLAG set")
|
|
|
|
_append_debug_log("STATE_FLAG set")
|
|
|
|
|
|
|
|
_log_document_state("bootstrap_if_requested after STATE_FLAG", include_open_docs=True)
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.isfile(json_path):
|
|
|
|
if not os.path.isfile(json_path):
|
|
|
|
_append_debug_log("exchange file missing: {0}".format(json_path))
|
|
|
|
_append_debug_log("exchange file missing: {0}".format(json_path))
|
|
|
|
@ -1097,3 +1629,7 @@ def bootstrap_if_requested():
|
|
|
|
QtCore.QTimer.singleShot(
|
|
|
|
QtCore.QTimer.singleShot(
|
|
|
|
IMPORT_READY_DELAY_MS, lambda: _run_scheduled_device_import(0)
|
|
|
|
IMPORT_READY_DELAY_MS, lambda: _run_scheduled_device_import(0)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
_log_document_state(
|
|
|
|
|
|
|
|
"bootstrap_if_requested after scheduling import",
|
|
|
|
|
|
|
|
include_open_docs=True,
|
|
|
|
|
|
|
|
)
|
|
|
|
|