feat(opc): 添加计划写入触发功能及相关配置
后续的配置是追踪的点位配置和写入的点位配置已经做好持久化在页面上配置完保存重启OPC即可实现持久化 后续的代码修改:从哪里开始而不是从最小的钢卷号开始,因为对方数据库里面的计划有几百条,写入的时候写入哪个计划的钢卷信息给一级都是需要修改代码的,现在默认的都是第一个钢卷 添加写入计数器、来源和目标节点的配置,支持从计划表读取数据并写入到指定开卷机的OPC节点。包括: 1. 在models.py中添加相关字段 2. 在opc_service.py中实现写入触发逻辑 3. 在OpcConfig.vue中添加配置界面 4. 更新相关API接口以支持新配置
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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]] = {}
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user