From 95ec77afae375691f1790e196bb25f07c23f8273 Mon Sep 17 00:00:00 2001
From: Joshi <3040996759@qq.com>
Date: Mon, 13 Apr 2026 16:09:48 +0800
Subject: [PATCH] =?UTF-8?q?feat(opc):=20=E6=B7=BB=E5=8A=A0=E8=AE=A1?=
=?UTF-8?q?=E5=88=92=E5=86=99=E5=85=A5=E8=A7=A6=E5=8F=91=E5=8A=9F=E8=83=BD?=
=?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE=20=E5=90=8E?=
=?UTF-8?q?=E7=BB=AD=E7=9A=84=E9=85=8D=E7=BD=AE=E6=98=AF=E8=BF=BD=E8=B8=AA?=
=?UTF-8?q?=E7=9A=84=E7=82=B9=E4=BD=8D=E9=85=8D=E7=BD=AE=E5=92=8C=E5=86=99?=
=?UTF-8?q?=E5=85=A5=E7=9A=84=E7=82=B9=E4=BD=8D=E9=85=8D=E7=BD=AE=E5=B7=B2?=
=?UTF-8?q?=E7=BB=8F=E5=81=9A=E5=A5=BD=E6=8C=81=E4=B9=85=E5=8C=96=E5=9C=A8?=
=?UTF-8?q?=E9=A1=B5=E9=9D=A2=E4=B8=8A=E9=85=8D=E7=BD=AE=E5=AE=8C=E4=BF=9D?=
=?UTF-8?q?=E5=AD=98=E9=87=8D=E5=90=AFOPC=E5=8D=B3=E5=8F=AF=E5=AE=9E?=
=?UTF-8?q?=E7=8E=B0=E6=8C=81=E4=B9=85=E5=8C=96=20=E5=90=8E=E7=BB=AD?=
=?UTF-8?q?=E7=9A=84=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9:=E4=BB=8E?=
=?UTF-8?q?=E5=93=AA=E9=87=8C=E5=BC=80=E5=A7=8B=E8=80=8C=E4=B8=8D=E6=98=AF?=
=?UTF-8?q?=E4=BB=8E=E6=9C=80=E5=B0=8F=E7=9A=84=E9=92=A2=E5=8D=B7=E5=8F=B7?=
=?UTF-8?q?=E5=BC=80=E5=A7=8B,=E5=9B=A0=E4=B8=BA=E5=AF=B9=E6=96=B9?=
=?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E9=87=8C=E9=9D=A2=E7=9A=84=E8=AE=A1?=
=?UTF-8?q?=E5=88=92=E6=9C=89=E5=87=A0=E7=99=BE=E6=9D=A1,=E5=86=99?=
=?UTF-8?q?=E5=85=A5=E7=9A=84=E6=97=B6=E5=80=99=E5=86=99=E5=85=A5=E5=93=AA?=
=?UTF-8?q?=E4=B8=AA=E8=AE=A1=E5=88=92=E7=9A=84=E9=92=A2=E5=8D=B7=E4=BF=A1?=
=?UTF-8?q?=E6=81=AF=E7=BB=99=E4=B8=80=E7=BA=A7=E9=83=BD=E6=98=AF=E9=9C=80?=
=?UTF-8?q?=E8=A6=81=E4=BF=AE=E6=94=B9=E4=BB=A3=E7=A0=81=E7=9A=84,?=
=?UTF-8?q?=E7=8E=B0=E5=9C=A8=E9=BB=98=E8=AE=A4=E7=9A=84=E9=83=BD=E6=98=AF?=
=?UTF-8?q?=E7=AC=AC=E4=B8=80=E4=B8=AA=E9=92=A2=E5=8D=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
添加写入计数器、来源和目标节点的配置,支持从计划表读取数据并写入到指定开卷机的OPC节点。包括:
1. 在models.py中添加相关字段
2. 在opc_service.py中实现写入触发逻辑
3. 在OpcConfig.vue中添加配置界面
4. 更新相关API接口以支持新配置
---
backend/main.py | 10 ++
backend/models.py | 4 +
backend/opc_config.json | 11 +-
backend/opc_service.py | 183 +++++++++++++++++++++++++++++--
frontend/src/views/OpcConfig.vue | 74 +++++++++++--
frontend/src/views/TrackCoil.vue | 2 +-
6 files changed, 263 insertions(+), 21 deletions(-)
diff --git a/backend/main.py b/backend/main.py
index 1aaa9bb..969dde9 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -370,6 +370,10 @@ def get_opc_config():
"poll_interval": opc_service.poll_interval,
"signal1_node": opc_service.signal1_node,
"signal2_node": opc_service.signal2_node,
+ "write_counter_node": opc_service.write_counter_node,
+ "write_source_node": opc_service.write_source_node,
+ "write_target_node": opc_service.write_target_node,
+ "write_nodes": opc_service.write_nodes,
"running": opc_service.running,
"last_counter": opc_service.last_counter,
"last_update": opc_service.last_update,
@@ -385,6 +389,11 @@ async def save_opc_config(config: OpcConfig):
opc_service.poll_interval = config.poll_interval
opc_service.signal1_node = config.signal1_node
opc_service.signal2_node = config.signal2_node
+ opc_service.write_counter_node = config.write_counter_node
+ opc_service.write_source_node = config.write_source_node
+ opc_service.write_target_node = config.write_target_node
+ opc_service.write_nodes = config.write_nodes
+ opc_service.write_counter_last = None
try:
opc_service.save_config()
except Exception as e:
@@ -402,6 +411,7 @@ def opc_status():
"last_update": opc_service.last_update,
"log": opc_service.event_log[-50:],
"track_state": opc_service.track_state,
+ "write_counter_last": opc_service.write_counter_last,
}
diff --git a/backend/models.py b/backend/models.py
index 5a74ca9..b3ad92e 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -184,3 +184,7 @@ class OpcConfig(BaseModel):
trackmap_nodes: Dict[str, str] = {}
signal1_node: str = ""
signal2_node: str = ""
+ write_counter_node: str = ""
+ write_source_node: str = ""
+ write_target_node: str = ""
+ write_nodes: Dict[str, Dict[str, str]] = {}
diff --git a/backend/opc_config.json b/backend/opc_config.json
index ec480d6..a8f1a4a 100644
--- a/backend/opc_config.json
+++ b/backend/opc_config.json
@@ -1,8 +1,15 @@
{
- "opc_url": "opc.tcp://192.168.1.100:4840",
+ "opc_url": "opc.tcp://127.0.0.1:49320",
"counter_node": "ns=2;s=PL.TRACKMAP.COUNTER",
"poll_interval": 2,
"trackmap_nodes": {},
"signal1_node": "ns=2;s=PL.Signal.EntryCoil",
- "signal2_node": "ns=2;s=PL.Signal.WeldDone"
+ "signal2_node": "ns=2;s=PL.Signal.WeldDone",
+ "write_counter_node": "",
+ "write_source_node": "",
+ "write_target_node": "",
+ "write_nodes": {
+ "1": {},
+ "2": {}
+ }
}
\ No newline at end of file
diff --git a/backend/opc_service.py b/backend/opc_service.py
index d172300..82cb851 100644
--- a/backend/opc_service.py
+++ b/backend/opc_service.py
@@ -63,6 +63,13 @@ class OpcService:
self.signal1_last: Optional[int] = None
self.signal2_last: Optional[int] = None
self.signal2_rise_time: Optional[datetime] = None
+ # 写入触发相关(三点位独立于追踪信号)
+ self.write_counter_node: str = os.getenv("OPC_WRITE_COUNTER_NODE", "")
+ self.write_source_node: str = os.getenv("OPC_WRITE_SOURCE_NODE", "")
+ self.write_target_node: str = os.getenv("OPC_WRITE_TARGET_NODE", "")
+ self.write_counter_last: Optional[Any] = None
+ # 写入字段映射(按目标开卷机 1/2 分组)
+ self.write_nodes: Dict[str, Dict[str, str]] = {}
self.signal1_coils: List[Dict[str, Any]] = []
self.first_coilid: str = ""
self.last_tracked_coilid: str = ""
@@ -106,6 +113,10 @@ class OpcService:
self.trackmap_nodes = cfg.get("trackmap_nodes", self.trackmap_nodes) or {}
self.signal1_node = cfg.get("signal1_node", self.signal1_node)
self.signal2_node = cfg.get("signal2_node", self.signal2_node)
+ self.write_counter_node = cfg.get("write_counter_node", self.write_counter_node)
+ self.write_source_node = cfg.get("write_source_node", self.write_source_node)
+ self.write_target_node = cfg.get("write_target_node", self.write_target_node)
+ self.write_nodes = cfg.get("write_nodes", self.write_nodes) or {}
self._log(f"Loaded OPC config from {self.config_path}")
except Exception as exc:
logger.warning("Failed to load OPC config %s: %s", self.config_path, exc)
@@ -119,6 +130,10 @@ class OpcService:
"trackmap_nodes": self.trackmap_nodes,
"signal1_node": self.signal1_node,
"signal2_node": self.signal2_node,
+ "write_counter_node": self.write_counter_node,
+ "write_source_node": self.write_source_node,
+ "write_target_node": self.write_target_node,
+ "write_nodes": self.write_nodes,
}
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
with open(self.config_path, "w", encoding="utf-8") as f:
@@ -169,6 +184,7 @@ class OpcService:
while not self._stop_event.is_set():
current_counter = await self._tick_with_counter(client)
await self._check_signals(client, current_counter)
+ await self._check_write_trigger(client)
await asyncio.sleep(self.poll_interval)
finally:
client.disconnect()
@@ -257,25 +273,170 @@ class OpcService:
elif self.track_state == "WAIT_S2":
if signal2_value is not None and self.signal2_last is not None:
if self.signal2_last == 0 and signal2_value == 1 and counter_changed:
- self.signal2_rise_time = datetime.now()
- self._log("Signal2: Weld done rising edge (0->1) with counter change")
+ self._log("Signal2: Weld done rising edge (0->1) with counter change, triggering update")
+ await self._handle_signal2()
+ self.track_state = "WAIT_S1"
+ self.signal2_rise_time = None
+ self.last_counter_at_state_change = current_counter
elif self.signal2_last == 1 and signal2_value == 0:
self.signal2_rise_time = None
- if signal2_value == 1 and self.signal2_rise_time and counter_changed:
- elapsed = (datetime.now() - self.signal2_rise_time).total_seconds()
- if elapsed >= 2.0:
- self._log(f"Signal2: Held {elapsed:.1f}s, triggering trackmap update")
- await self._handle_signal2()
- self.track_state = "WAIT_S1"
- self.signal2_rise_time = None
- self.last_counter_at_state_change = current_counter
-
if signal1_value is not None:
self.signal1_last = signal1_value
if signal2_value is not None:
self.signal2_last = signal2_value
+ async def _check_write_trigger(self, client):
+ """Check write trigger counter/source/target and write PDI head coil to OPC."""
+ if not (self.write_counter_node and self.write_source_node and self.write_target_node):
+ return
+
+ try:
+ write_counter = client.get_node(self.write_counter_node).get_value()
+ write_source = client.get_node(self.write_source_node).get_value()
+ write_target = client.get_node(self.write_target_node).get_value()
+ except Exception as exc:
+ self._log(f"Write trigger read failed: {exc}")
+ return
+
+ if self.write_counter_last is None:
+ self.write_counter_last = write_counter
+ return
+
+ if write_counter == self.write_counter_last:
+ return
+
+ old_counter = self.write_counter_last
+ self.write_counter_last = write_counter
+ self._log(
+ f"Write counter changed: {old_counter} -> {write_counter}, "
+ f"source={write_source}, target={write_target}"
+ )
+
+ try:
+ source_value = int(write_source)
+ target_value = int(write_target)
+ except (TypeError, ValueError):
+ self._log(f"Write trigger invalid source/target values: source={write_source}, target={write_target}")
+ return
+
+ if source_value != 100 or target_value not in (1, 2):
+ return
+
+ await self._write_entry_coil_to_uncoiler(client, target_value)
+
+ async def _write_entry_coil_to_uncoiler(self, client, target_uncoiler: int):
+ """Write the smallest COILID plan to target uncoiler OPC nodes."""
+
+ def _load_next_plan():
+ from database import get_connection
+
+ conn = get_connection()
+ cursor = conn.cursor()
+ try:
+ cursor.execute(
+ """
+ SELECT
+ COILID,
+ ENTRY_COIL_WEIGHT,
+ ENTRY_OF_COIL_LENGTH,
+ ENTRY_COIL_WIDTH,
+ ENTRY_COIL_THICKNESS,
+ ENTRY_OF_COIL_INNER_DIAMETER,
+ ENTRY_OF_COIL_OUTER_DIAMETER,
+ STEEL_GRADE
+ FROM PLTM.PDI_PLTM
+ ORDER BY COILID ASC
+ """
+ )
+ row = cursor.fetchone()
+ if not row:
+ return None
+ return {
+ "coilid": row[0],
+ "entry_coil_weight": row[1],
+ "entry_of_coil_length": row[2],
+ "entry_coil_width": row[3],
+ "entry_coil_thickness": row[4],
+ "entry_of_coil_inner_diameter": row[5],
+ "entry_of_coil_outer_diameter": row[6],
+ "alloy_code": row[7],
+ "material": row[7],
+ }
+ finally:
+ cursor.close()
+ conn.close()
+
+ loop = asyncio.get_event_loop()
+ plan = await loop.run_in_executor(None, _load_next_plan)
+ if not plan:
+ self._log("Write trigger matched but PDI has no plan data")
+ return
+
+ target_cfg = self.write_nodes.get(str(target_uncoiler), {}) or {}
+ if not target_cfg:
+ self._log(f"Write nodes for uncoiler {target_uncoiler} not configured")
+ return
+
+ # setup_data_revision: 每次写入 +1
+ revision_node = target_cfg.get("setup_data_revision")
+ if revision_node:
+ try:
+ rev_value = client.get_node(revision_node).get_value()
+ next_rev = int(rev_value or 0) + 1
+ await self._write_node_value_by_client(client, revision_node, next_rev, "Int32")
+ except Exception as exc:
+ self._log(f"Write setup_data_revision failed (U{target_uncoiler}): {exc}")
+
+ field_variant = {
+ "coilid": "String",
+ "entry_coil_weight": "Float",
+ "entry_of_coil_length": "Float",
+ "entry_coil_width": "Float",
+ "entry_coil_thickness": "Float",
+ "entry_of_coil_inner_diameter": "Float",
+ "entry_of_coil_outer_diameter": "Float",
+ "alloy_code": "String",
+ "material": "String",
+ }
+
+ for field, value in plan.items():
+ node_id = target_cfg.get(field)
+ if not node_id or value is None:
+ continue
+ try:
+ await self._write_node_value_by_client(
+ client,
+ node_id,
+ value,
+ field_variant.get(field),
+ )
+ except Exception as exc:
+ self._log(
+ f"Write field failed (U{target_uncoiler}, {field}, node={node_id}): {exc}"
+ )
+
+ self._log(
+ f"Wrote plan COILID={plan['coilid']} to uncoiler {target_uncoiler}"
+ )
+
+ async def _write_node_value_by_client(
+ self,
+ client,
+ node_id: str,
+ value: Any,
+ variant_type: Optional[str] = None,
+ ):
+ """Write OPC node value using an existing client connection."""
+ from opcua import ua # type: ignore
+
+ node = client.get_node(node_id)
+ vt = self._normalize_variant_type(variant_type)
+ if vt is None:
+ node.set_value(value)
+ else:
+ node.set_value(ua.DataValue(ua.Variant(value, vt)))
+
async def _handle_signal1(self):
"""Handle signal1: fetch next 4 coils from Oracle PDI table and save to SQLite temp table."""
from sqlite_sync import sqlite_save_coils_to_track, sqlite_clear_coil_track
diff --git a/frontend/src/views/OpcConfig.vue b/frontend/src/views/OpcConfig.vue
index ab9d135..061cc05 100644
--- a/frontend/src/views/OpcConfig.vue
+++ b/frontend/src/views/OpcConfig.vue
@@ -7,17 +7,17 @@