Compare commits

..

5 Commits

Author SHA1 Message Date
0b07c2a2f1 refactor(PdiList): 重构表单布局并优化字段组织
- 重新组织表单字段为更合理的分组结构
- 调整标签宽度和输入框样式
- 移除不必要的化学元素字段
- 更新表单验证规则
- 添加新的字段如钢卷状态、分卷模式等
2026-04-27 18:34:40 +08:00
d2d445eff1 fix(opc_service): 修复S7地址解析和修订号循环计数问题
修复S7地址解析中未处理的类型别名问题,并添加偏移量有效性检查
实现修订号的循环计数功能(0..999),并添加操作成功日志
2026-04-24 18:04:33 +08:00
8b15f78e78 feat(opc): 添加S7直写支持并更新字段映射格式
- 在OpcConfig模型中新增S7终端、机架和槽位配置
- 实现S7直写功能,替代原有的OPC写入方式
- 更新前端字段映射占位符为S7地址格式
- 修改焊接完成信号触发条件说明
- 添加S7 INT32读取功能用于数据版本控制
2026-04-24 16:58:58 +08:00
95ec77afae feat(opc): 添加计划写入触发功能及相关配置
后续的配置是追踪的点位配置和写入的点位配置已经做好持久化在页面上配置完保存重启OPC即可实现持久化
后续的代码修改:从哪里开始而不是从最小的钢卷号开始,因为对方数据库里面的计划有几百条,写入的时候写入哪个计划的钢卷信息给一级都是需要修改代码的,现在默认的都是第一个钢卷

添加写入计数器、来源和目标节点的配置,支持从计划表读取数据并写入到指定开卷机的OPC节点。包括:
1. 在models.py中添加相关字段
2. 在opc_service.py中实现写入触发逻辑
3. 在OpcConfig.vue中添加配置界面
4. 更新相关API接口以支持新配置
2026-04-13 16:09:48 +08:00
fc8b38d44d feat(追踪系统): 修改钢卷追踪逻辑为按钢卷号升序处理
- 前端调整位置数量从5个改为4个并更新提示信息
- 后端修改SQL查询按COILID升序获取钢卷
- 新增按钢卷号范围查询功能
- 实现追踪状态管理,支持追踪结束检测和恢复
- 优化信号处理逻辑,支持末卷重复信号处理
2026-04-13 14:57:44 +08:00
8 changed files with 823 additions and 205 deletions

View File

@@ -370,6 +370,13 @@ def get_opc_config():
"poll_interval": opc_service.poll_interval, "poll_interval": opc_service.poll_interval,
"signal1_node": opc_service.signal1_node, "signal1_node": opc_service.signal1_node,
"signal2_node": opc_service.signal2_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_s7_endpoint": opc_service.write_s7_endpoint,
"write_s7_rack": opc_service.write_s7_rack,
"write_s7_slot": opc_service.write_s7_slot,
"write_nodes": opc_service.write_nodes,
"running": opc_service.running, "running": opc_service.running,
"last_counter": opc_service.last_counter, "last_counter": opc_service.last_counter,
"last_update": opc_service.last_update, "last_update": opc_service.last_update,
@@ -385,6 +392,14 @@ async def save_opc_config(config: OpcConfig):
opc_service.poll_interval = config.poll_interval opc_service.poll_interval = config.poll_interval
opc_service.signal1_node = config.signal1_node opc_service.signal1_node = config.signal1_node
opc_service.signal2_node = config.signal2_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_s7_endpoint = config.write_s7_endpoint
opc_service.write_s7_rack = config.write_s7_rack
opc_service.write_s7_slot = config.write_s7_slot
opc_service.write_nodes = config.write_nodes
opc_service.write_counter_last = None
try: try:
opc_service.save_config() opc_service.save_config()
except Exception as e: except Exception as e:
@@ -402,6 +417,7 @@ def opc_status():
"last_update": opc_service.last_update, "last_update": opc_service.last_update,
"log": opc_service.event_log[-50:], "log": opc_service.event_log[-50:],
"track_state": opc_service.track_state, "track_state": opc_service.track_state,
"write_counter_last": opc_service.write_counter_last,
} }

View File

@@ -184,3 +184,10 @@ class OpcConfig(BaseModel):
trackmap_nodes: Dict[str, str] = {} trackmap_nodes: Dict[str, str] = {}
signal1_node: str = "" signal1_node: str = ""
signal2_node: str = "" signal2_node: str = ""
write_counter_node: str = ""
write_source_node: str = ""
write_target_node: str = ""
write_s7_endpoint: str = ""
write_s7_rack: int = 0
write_s7_slot: int = 1
write_nodes: Dict[str, Dict[str, str]] = {}

View File

@@ -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", "counter_node": "ns=2;s=PL.TRACKMAP.COUNTER",
"poll_interval": 2, "poll_interval": 2,
"trackmap_nodes": {}, "trackmap_nodes": {},
"signal1_node": "ns=2;s=PL.Signal.EntryCoil", "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": {}
}
} }

View File

@@ -63,8 +63,21 @@ class OpcService:
self.signal1_last: Optional[int] = None self.signal1_last: Optional[int] = None
self.signal2_last: Optional[int] = None self.signal2_last: Optional[int] = None
self.signal2_rise_time: Optional[datetime] = 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_s7_endpoint: str = os.getenv("OPC_WRITE_S7_ENDPOINT", "")
self.write_s7_rack: int = int(os.getenv("OPC_WRITE_S7_RACK", "0"))
self.write_s7_slot: int = int(os.getenv("OPC_WRITE_S7_SLOT", "1"))
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.signal1_coils: List[Dict[str, Any]] = []
self.current_seq_start: int = 1 self.first_coilid: str = ""
self.last_tracked_coilid: str = ""
self.end_coilid: str = ""
self.tracking_ended: bool = False
# 状态机: WAIT_S1=等待信号1, WAIT_S2=等待信号2 # 状态机: WAIT_S1=等待信号1, WAIT_S2=等待信号2
self.track_state: str = "WAIT_S1" self.track_state: str = "WAIT_S1"
self.last_counter_at_state_change: Optional[Any] = None self.last_counter_at_state_change: Optional[Any] = None
@@ -103,6 +116,13 @@ class OpcService:
self.trackmap_nodes = cfg.get("trackmap_nodes", self.trackmap_nodes) or {} self.trackmap_nodes = cfg.get("trackmap_nodes", self.trackmap_nodes) or {}
self.signal1_node = cfg.get("signal1_node", self.signal1_node) self.signal1_node = cfg.get("signal1_node", self.signal1_node)
self.signal2_node = cfg.get("signal2_node", self.signal2_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_s7_endpoint = cfg.get("write_s7_endpoint", self.write_s7_endpoint)
self.write_s7_rack = int(cfg.get("write_s7_rack", self.write_s7_rack))
self.write_s7_slot = int(cfg.get("write_s7_slot", self.write_s7_slot))
self.write_nodes = cfg.get("write_nodes", self.write_nodes) or {}
self._log(f"Loaded OPC config from {self.config_path}") self._log(f"Loaded OPC config from {self.config_path}")
except Exception as exc: except Exception as exc:
logger.warning("Failed to load OPC config %s: %s", self.config_path, exc) logger.warning("Failed to load OPC config %s: %s", self.config_path, exc)
@@ -116,6 +136,13 @@ class OpcService:
"trackmap_nodes": self.trackmap_nodes, "trackmap_nodes": self.trackmap_nodes,
"signal1_node": self.signal1_node, "signal1_node": self.signal1_node,
"signal2_node": self.signal2_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_s7_endpoint": self.write_s7_endpoint,
"write_s7_rack": self.write_s7_rack,
"write_s7_slot": self.write_s7_slot,
"write_nodes": self.write_nodes,
} }
os.makedirs(os.path.dirname(self.config_path), exist_ok=True) os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
with open(self.config_path, "w", encoding="utf-8") as f: with open(self.config_path, "w", encoding="utf-8") as f:
@@ -166,6 +193,7 @@ class OpcService:
while not self._stop_event.is_set(): while not self._stop_event.is_set():
current_counter = await self._tick_with_counter(client) current_counter = await self._tick_with_counter(client)
await self._check_signals(client, current_counter) await self._check_signals(client, current_counter)
await self._check_write_trigger(client)
await asyncio.sleep(self.poll_interval) await asyncio.sleep(self.poll_interval)
finally: finally:
client.disconnect() client.disconnect()
@@ -254,78 +282,426 @@ class OpcService:
elif self.track_state == "WAIT_S2": elif self.track_state == "WAIT_S2":
if signal2_value is not None and self.signal2_last is not None: 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: 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, triggering update")
self._log("Signal2: Weld done rising edge (0->1) with counter change") 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: elif self.signal2_last == 1 and signal2_value == 0:
self.signal2_rise_time = None 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: if signal1_value is not None:
self.signal1_last = signal1_value self.signal1_last = signal1_value
if signal2_value is not None: if signal2_value is not None:
self.signal2_last = signal2_value self.signal2_last = signal2_value
async def _handle_signal1(self): async def _check_write_trigger(self, client):
"""Handle signal1: fetch next 5 coils from PDI and save to SQLite temp table.""" """Check write trigger counter/source/target and write PDI head coil to OPC."""
from sqlite_sync import ( if not (self.write_counter_node and self.write_source_node and self.write_target_node):
sqlite_get_max_sequencenb, return
sqlite_get_coils_by_sequencenb_range,
sqlite_save_coils_to_track 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(target_value)
async def _write_entry_coil_to_uncoiler(self, target_uncoiler: int):
"""Write the smallest COILID plan to target uncoiler via S7."""
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
if not self.write_s7_endpoint:
self._log("Write S7 endpoint is empty, skip write")
return
field_meta = {
"setup_data_revision": {"data_type": "INT32", "data_length": 4},
"coilid": {"data_type": "S7STRING", "data_length": 20},
"entry_coil_weight": {"data_type": "FLOAT", "data_length": 4},
"entry_of_coil_length": {"data_type": "FLOAT", "data_length": 4},
"entry_coil_width": {"data_type": "FLOAT", "data_length": 4},
"entry_coil_thickness": {"data_type": "FLOAT", "data_length": 4},
"entry_of_coil_inner_diameter": {"data_type": "FLOAT", "data_length": 4},
"entry_of_coil_outer_diameter": {"data_type": "FLOAT", "data_length": 4},
"alloy_code": {"data_type": "S7STRING", "data_length": 4},
"material": {"data_type": "S7STRING", "data_length": 20},
}
revision_address = target_cfg.get("setup_data_revision")
if revision_address:
try:
curr_rev = self._read_s7_int32(
endpoint=self.write_s7_endpoint,
address=revision_address,
rack=self.write_s7_rack,
slot=self.write_s7_slot,
)
# 循环计数0..999达到999后回到0
next_rev = 0 if curr_rev >= 999 else curr_rev + 1
await self.write_s7_value(
endpoint=self.write_s7_endpoint,
address=revision_address,
data_length=4,
data_type="INT32",
value=next_rev,
rack=self.write_s7_rack,
slot=self.write_s7_slot,
)
self._log(
f"Write setup_data_revision success (U{target_uncoiler}): "
f"{curr_rev} -> {next_rev}, address={revision_address}"
)
except Exception as exc:
self._log(f"Write setup_data_revision failed (U{target_uncoiler}): {exc}")
for field, value in {"coilid": plan["coilid"], **plan}.items():
if field == "setup_data_revision":
continue
address = target_cfg.get(field)
if not address or value is None:
continue
meta = field_meta.get(field)
if not meta:
continue
try:
await self.write_s7_value(
endpoint=self.write_s7_endpoint,
address=address,
data_length=meta["data_length"],
data_type=meta["data_type"],
value=value,
rack=self.write_s7_rack,
slot=self.write_s7_slot,
)
except Exception as exc:
self._log(
f"Write field failed (U{target_uncoiler}, {field}, address={address}): {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)
# 1) Try direct write first (some OPC servers do their own coercion better)
if variant_type is None:
try:
node.set_value(value)
return
except Exception:
pass
vt = self._normalize_variant_type(variant_type)
if vt is None:
try:
vt = node.get_data_type_as_variant_type()
except Exception:
vt = None
if vt is None:
node.set_value(value)
return
# 2) Coerce by OPC variant type
try:
write_value = self._coerce_value_for_variant(value, vt)
node.set_value(ua.DataValue(ua.Variant(write_value, vt)))
return
except Exception:
pass
# 3) Coerce by current value runtime shape (list/bytes/str/int...)
current_val = node.get_value()
write_value = self._coerce_value_by_current_value(value, current_val)
node.set_value(write_value)
def _coerce_value_for_variant(self, value: Any, vt):
"""Coerce python value to match OPC ua.VariantType."""
vt_name = getattr(vt, "name", str(vt))
if vt_name in ("Boolean",):
return bool(value)
if vt_name in ("SByte", "Byte", "Int16", "UInt16", "Int32", "UInt32", "Int64", "UInt64"):
if value is None or value == "":
return 0
if isinstance(value, str):
raw = value.strip()
if raw == "":
return 0
if raw.lstrip("-").isdigit():
return int(raw)
# Non-numeric text to numeric scalar is invalid; let caller try another strategy
raise ValueError(f"Cannot coerce non-numeric text '{value}' to {vt_name}")
return int(value)
if vt_name in ("Float", "Double"):
if value is None or value == "":
return 0.0
return float(value)
if vt_name in ("String",):
return "" if value is None else str(value)
if vt_name in ("ByteString",):
if value is None:
return b""
if isinstance(value, bytes):
return value
return str(value).encode("utf-8")
return value
def _coerce_value_by_current_value(self, value: Any, current_val: Any):
"""Coerce value according to current node value runtime structure."""
if isinstance(current_val, str):
return "" if value is None else str(value)
if isinstance(current_val, (bytes, bytearray)):
raw = b"" if value is None else str(value).encode("utf-8")
length = len(current_val)
if length > 0:
raw = raw[:length].ljust(length, b"\x00")
return raw if isinstance(current_val, bytes) else bytearray(raw)
if isinstance(current_val, (list, tuple)) and current_val:
# Common PLC string representation: array of bytes/ints
if all(isinstance(x, int) for x in current_val):
arr_len = len(current_val)
raw = b"" if value is None else str(value).encode("ascii", errors="ignore")
raw = raw[:arr_len].ljust(arr_len, b"\x00")
return [int(b) for b in raw]
if isinstance(current_val, bool):
return bool(value)
if isinstance(current_val, int):
if value is None or value == "":
return 0
if isinstance(value, str) and not value.strip().lstrip("-").isdigit():
raise ValueError(f"Cannot write non-numeric '{value}' to int node")
return int(value)
if isinstance(current_val, float):
if value is None or value == "":
return 0.0
return float(value)
return value
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
def _do_fetch(): def _do_fetch():
max_seq = sqlite_get_max_sequencenb() # 如果追踪已结束,检查是否有新钢卷需要恢复追踪
if max_seq is None or max_seq < 1: self._log(f"Signal1: Called, first_coilid={repr(self.first_coilid)}, tracking_ended={self.tracking_ended}")
self._log("Signal1: No PDI data available") if self.tracking_ended:
try:
from database import get_connection
conn = get_connection()
cursor = conn.cursor()
try:
cursor.execute("SELECT COILID FROM PLTM.PDI_PLTM ORDER BY COILID ASC")
all_coil_ids = [r[0] for r in cursor.fetchall()]
# 使用 end_coilid 判断是否有新钢卷
check_coilid = self.end_coilid or self.last_tracked_coilid
if all_coil_ids and check_coilid:
has_new = any(cid > check_coilid for cid in all_coil_ids)
if has_new:
self._log("Signal1: New coils detected, resuming tracking")
self.tracking_ended = False
# 从最后一个追踪的钢卷之后继续
self.first_coilid = check_coilid
else:
self._log("Signal1: Tracking ended, no new coils, skip")
return
elif all_coil_ids and not check_coilid:
# 没有结束点记录但有计划,按首卷重新启动追踪
self._log("Signal1: Tracking ended without checkpoint, restarting from first plan")
self.tracking_ended = False
self.first_coilid = ""
else:
# 没有计划,保持结束状态
self._log("Signal1: Tracking ended, PDI empty, skip")
return
finally:
cursor.close()
conn.close()
except Exception as exc:
self._log(f"Signal1: Check new coils failed: {exc}")
return
try:
from database import get_connection
conn = get_connection()
cursor = conn.cursor()
try:
if self.first_coilid:
cursor.execute("""
SELECT COILID, SEQUENCENB, ROLLPROGRAMNB
FROM PLTM.PDI_PLTM
WHERE COILID >= :start_coilid
ORDER BY COILID ASC
""", {"start_coilid": self.first_coilid})
else:
cursor.execute("""
SELECT COILID, SEQUENCENB, ROLLPROGRAMNB
FROM PLTM.PDI_PLTM
ORDER BY COILID ASC
""")
rows = cursor.fetchmany(4)
coils = [{"coilid": r[0], "sequencenb": r[1], "rollprogramnb": r[2]} for r in rows]
finally:
cursor.close()
conn.close()
except Exception as exc:
self._log(f"Signal1: Failed to fetch from Oracle: {exc}")
return return
start_seq = self.current_seq_start
end_seq = min(start_seq + 4, max_seq)
if end_seq < start_seq:
self._log(f"Signal1: Insufficient PDI data (max_seq={max_seq}, start={start_seq})")
return
coils = sqlite_get_coils_by_sequencenb_range(start_seq, end_seq)
if len(coils) == 0: if len(coils) == 0:
self._log(f"Signal1: No coils found") self._log("Signal1: No more coils in PDI, ending tracking")
sqlite_clear_coil_track()
self.tracking_ended = True
return
if len(coils) == 1 and coils[0]["coilid"] == self.first_coilid:
self._log("Signal1: Only the same coil exists, saving it and setting for next query")
self.signal1_coils = coils
sqlite_save_coils_to_track(coils)
# 保持 first_coilid 不变,下次查询会从同一个位置继续
# Signal2 触发后,检查是否还有更多钢卷
return return
self.signal1_coils = coils self.signal1_coils = coils
sqlite_save_coils_to_track(coils) sqlite_save_coils_to_track(coils)
self._log(f"Signal1: Saved {len(coils)} coils (seq {start_seq}-{end_seq}) to temp table") if len(coils) >= 2:
self.first_coilid = coils[1]["coilid"]
else:
self.first_coilid = coils[0]["coilid"]
self.current_seq_start = start_seq + 1 self.last_tracked_coilid = coils[0]["coilid"]
self._log(f"Signal1: Saved {len(coils)} coils, next_start: {self.first_coilid}, last_tracked: {self.last_tracked_coilid}")
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
await loop.run_in_executor(None, _do_fetch) await loop.run_in_executor(None, _do_fetch)
async def _handle_signal2(self): async def _handle_signal2(self):
"""Handle signal2: update CMPT_PL_TRACKMAP with temp table data.""" """Handle signal2: update CMPT_PL_TRACKMAP with temp table data."""
from sqlite_sync import sqlite_get_coil_track from sqlite_sync import sqlite_clear_coil_track, sqlite_get_coil_track
from database import get_connection from database import get_connection
def _do_update(): def _do_update():
coils = sqlite_get_coil_track() coils = sqlite_get_coil_track()
if not coils:
self._log("Signal2: No coils in temp track table")
return
try: try:
conn = get_connection() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
try: try:
if not coils:
self._log("Signal2: No coils in temp track table, clearing all positions")
for pos in range(1, 11):
cursor.execute("SELECT COUNT(*) FROM PLTM.CMPT_PL_TRACKMAP WHERE POSITION = :pos", {"pos": pos})
if cursor.fetchone()[0] > 0:
cursor.execute("UPDATE PLTM.CMPT_PL_TRACKMAP SET COILID = NULL, TOM = SYSDATE WHERE POSITION = :pos", {"pos": pos})
conn.commit()
# 标记追踪结束
if self.signal1_coils:
self.end_coilid = self.signal1_coils[0]["coilid"]
self.tracking_ended = True
# 清空 first_coilid下次 Signal1 触发时从头查询
self.first_coilid = ""
self._log(f"Signal2: Tracking ended (no coils), end_coilid={self.end_coilid}")
return
cursor.execute("SELECT COILID FROM PLTM.CMPT_PL_TRACKMAP WHERE POSITION = 1")
prev_pos1_coil = cursor.fetchone()
prev_pos1_coilid = prev_pos1_coil[0] if prev_pos1_coil else None
coil_count = len(coils) coil_count = len(coils)
for i in range(5): for i in range(4):
target_position = i + 1 target_position = i + 1
coil_index = target_position - 1 coil_index = target_position - 1
if coil_index >= 0 and coil_index < coil_count: if coil_index >= 0 and coil_index < coil_count:
@@ -357,8 +733,63 @@ class OpcService:
INSERT INTO PLTM.CMPT_PL_TRACKMAP (POSITION, COILID, TOM) INSERT INTO PLTM.CMPT_PL_TRACKMAP (POSITION, COILID, TOM)
VALUES (:position, NULL, SYSDATE) VALUES (:position, NULL, SYSDATE)
""", {"position": target_position}) """, {"position": target_position})
for pos in range(5, 11):
cursor.execute("SELECT COUNT(*) FROM PLTM.CMPT_PL_TRACKMAP WHERE POSITION = :pos", {"pos": pos})
if cursor.fetchone()[0] > 0:
cursor.execute("UPDATE PLTM.CMPT_PL_TRACKMAP SET COILID = NULL, TOM = SYSDATE WHERE POSITION = :pos", {"pos": pos})
new_pos1_coil = coils[0]["coilid"] if coils else None
pos1_unchanged = prev_pos1_coilid and new_pos1_coil and prev_pos1_coilid == new_pos1_coil
if pos1_unchanged:
# 只在“末卷(单卷)重复信号”时清空,避免 6-7 阶段提前进入清空/重写循环
if coil_count == 1:
for pos in range(1, 11):
cursor.execute("SELECT COUNT(*) FROM PLTM.CMPT_PL_TRACKMAP WHERE POSITION = :pos", {"pos": pos})
if cursor.fetchone()[0] > 0:
cursor.execute("UPDATE PLTM.CMPT_PL_TRACKMAP SET COILID = NULL, TOM = SYSDATE WHERE POSITION = :pos", {"pos": pos})
sqlite_clear_coil_track()
self.end_coilid = coils[0]["coilid"]
self.last_tracked_coilid = self.end_coilid
self.tracking_ended = True
self.first_coilid = ""
self._log("Signal2: Final single coil repeated, cleared all positions and ended tracking")
else:
self._log("Signal2: Coil at position 1 unchanged but not final single-coil stage, keep tracking")
conn.commit() conn.commit()
self._log(f"Signal2: Updated 5 positions (coils: {coil_count})") self._log(f"Signal2: Updated 4 positions (coils: {coil_count})")
# 检查是否还有更多钢卷可以追踪
self._log(f"Signal2: Checking remaining coils, first_coilid={repr(self.first_coilid)}")
if self.first_coilid:
try:
cursor.execute("""
SELECT COILID FROM PLTM.PDI_PLTM
WHERE COILID >= :start_coilid
ORDER BY COILID ASC
""", {"start_coilid": self.first_coilid})
all_remaining = cursor.fetchall()
remaining_count = len(all_remaining)
self._log(f"Signal2: Check remaining after {self.first_coilid}: count={remaining_count}, coils={[r[0] for r in all_remaining]}")
if remaining_count == 0:
# 没有更多钢卷了,标记追踪结束
self.end_coilid = self.first_coilid
self.last_tracked_coilid = self.first_coilid
self.tracking_ended = True
self._log(f"Signal2: No more coils, tracking ended, end_coilid={self.end_coilid}")
elif remaining_count >= 2:
# 有2个以上钢卷保持 first_coilid 不变,让 Signal1 处理
self._log(f"Signal2: >=2 coils remain, first_coilid unchanged, waiting for Signal1")
else:
# 只有一个钢卷时先展示为“7(1个)”,下一次重复信号再清空到 NULL
self._log(
f"Signal2: One coil remains ({all_remaining[0][0]}), "
"waiting one more cycle before final clear"
)
except Exception as exc:
self._log(f"Signal2: Check next coils failed: {exc}")
finally: finally:
cursor.close() cursor.close()
conn.close() conn.close()
@@ -540,6 +971,14 @@ class OpcService:
raise ValueError(f"{dt} data_length must be 8") raise ValueError(f"{dt} data_length must be 8")
return struct.pack(">d", float(value)) return struct.pack(">d", float(value))
if dt == "S7STRING":
if data_length < 1 or data_length > 254:
raise ValueError("S7STRING data_length must be 1..254")
text = "" if value is None else str(value)
raw = text.encode("ascii", errors="ignore")[:data_length]
# Siemens STRING layout: [max_len][cur_len][chars...]
return bytes([data_length, len(raw)]) + raw.ljust(data_length, b"\x00")
if dt in ("STRING", "BYTES"): if dt in ("STRING", "BYTES"):
if data_length < 1: if data_length < 1:
raise ValueError(f"{dt} data_length must be >= 1") raise ValueError(f"{dt} data_length must be >= 1")
@@ -552,7 +991,7 @@ class OpcService:
raise ValueError( raise ValueError(
"Unsupported S7 data_type. " "Unsupported S7 data_type. "
"Example: BOOL/BYTE/INT16/UINT16/INT32/UINT32/REAL/LREAL/STRING" "Example: BOOL/BYTE/INT16/UINT16/INT32/UINT32/REAL/LREAL/STRING/S7STRING"
) )
async def write_s7_value( async def write_s7_value(
@@ -681,8 +1120,29 @@ class OpcService:
if area_part.startswith("DB"): if area_part.startswith("DB"):
area_part = area_part[2:] area_part = area_part[2:]
area_code = area_part[:1] # Normalize aliases:
offset_part = area_part[1:] # X0.0 / B2 / W4 / D8 / DINT0 / INT4 / WORD6 / REAL8 / BYTE10
if area_part.startswith("DINT"):
area_code = "D"
offset_part = area_part[4:]
elif area_part.startswith("DWORD"):
area_code = "D"
offset_part = area_part[5:]
elif area_part.startswith("REAL"):
area_code = "D"
offset_part = area_part[4:]
elif area_part.startswith("INT"):
area_code = "W"
offset_part = area_part[3:]
elif area_part.startswith("WORD"):
area_code = "W"
offset_part = area_part[4:]
elif area_part.startswith("BYTE"):
area_code = "B"
offset_part = area_part[4:]
else:
area_code = area_part[:1]
offset_part = area_part[1:]
bit_index = None bit_index = None
if area_code == "X": if area_code == "X":
@@ -696,6 +1156,8 @@ class OpcService:
else: else:
if "." in offset_part: if "." in offset_part:
raise ValueError(f"Only X area may include bit index: {address}") raise ValueError(f"Only X area may include bit index: {address}")
if not offset_part or not offset_part.lstrip("-").isdigit():
raise ValueError(f"Invalid S7 byte offset in address: {address}")
byte_offset = int(offset_part) byte_offset = int(offset_part)
if byte_offset < 0: if byte_offset < 0:
@@ -754,6 +1216,13 @@ class OpcService:
raise ValueError(f"{dt} data_length must be 8") raise ValueError(f"{dt} data_length must be 8")
return struct.pack(">d", float(value)) return struct.pack(">d", float(value))
if dt == "S7STRING":
if data_length < 1 or data_length > 254:
raise ValueError("S7STRING data_length must be 1..254")
text = "" if value is None else str(value)
raw = text.encode("ascii", errors="ignore")[:data_length]
return bytes([data_length, len(raw)]) + raw.ljust(data_length, b"\x00")
if dt in ("STRING", "BYTES"): if dt in ("STRING", "BYTES"):
if isinstance(value, bytes): if isinstance(value, bytes):
raw = value raw = value
@@ -765,9 +1234,35 @@ class OpcService:
raise ValueError( raise ValueError(
f"Unsupported data_type: {data_type}. " f"Unsupported data_type: {data_type}. "
"Supported: BOOL/BYTE/INT8/UINT16/WORD/INT16/UINT32/DWORD/INT32/DINT/REAL/FLOAT/LREAL/DOUBLE/STRING/BYTES" "Supported: BOOL/BYTE/INT8/UINT16/WORD/INT16/UINT32/DWORD/INT32/DINT/REAL/FLOAT/LREAL/DOUBLE/STRING/S7STRING/BYTES"
) )
def _read_s7_int32(self, endpoint: str, address: str, rack: int = 0, slot: int = 1) -> int:
"""Read a 4-byte signed int from S7 DB address."""
try:
import snap7 # type: ignore
except ImportError as exc:
raise RuntimeError("python-snap7 package not installed") from exc
ip, tcp_port = self._parse_s7_endpoint(endpoint)
db_number, area_code, byte_offset, bit_index = self._parse_s7_address(address)
if area_code not in ("D",):
raise ValueError(f"INT32 read expects D/DBD address, got: {address}")
client = snap7.client.Client()
try:
client.set_connection_type(3)
client.connect(ip, int(rack), int(slot), int(tcp_port))
if not client.get_connected():
raise RuntimeError(f"S7 connect failed: {endpoint}")
raw = client.db_read(db_number, byte_offset, 4)
return int.from_bytes(raw, byteorder="big", signed=True)
finally:
try:
client.disconnect()
except Exception:
pass
async def write_s7_value( async def write_s7_value(
self, self,
endpoint: str, endpoint: str,

View File

@@ -337,7 +337,7 @@ def sqlite_get_coils_by_sequencenb_range(start_seq: int, end_seq: int) -> List[D
SELECT COILID, SEQUENCENB, ROLLPROGRAMNB SELECT COILID, SEQUENCENB, ROLLPROGRAMNB
FROM PDI_PLTM FROM PDI_PLTM
WHERE SEQUENCENB >= ? AND SEQUENCENB <= ? WHERE SEQUENCENB >= ? AND SEQUENCENB <= ?
ORDER BY SEQUENCENB DESC ORDER BY COILID ASC
""", (start_seq, end_seq)) """, (start_seq, end_seq))
rows = cursor.fetchall() rows = cursor.fetchall()
return [{"coilid": r[0], "sequencenb": r[1], "rollprogramnb": r[2]} for r in rows] return [{"coilid": r[0], "sequencenb": r[1], "rollprogramnb": r[2]} for r in rows]
@@ -345,12 +345,42 @@ def sqlite_get_coils_by_sequencenb_range(start_seq: int, end_seq: int) -> List[D
sc.close() sc.close()
def sqlite_get_coils_by_coilid(start_coilid: str, count: int = 4) -> List[Dict[str, Any]]:
sc = get_sqlite()
try:
cursor = sc.execute("""
SELECT COILID, SEQUENCENB, ROLLPROGRAMNB
FROM PDI_PLTM
WHERE COILID >= ?
ORDER BY COILID ASC
LIMIT ?
""", (start_coilid, count))
rows = cursor.fetchall()
return [{"coilid": r[0], "sequencenb": r[1], "rollprogramnb": r[2]} for r in rows]
finally:
sc.close()
def sqlite_get_first_coils(count: int = 4) -> List[Dict[str, Any]]:
sc = get_sqlite()
try:
cursor = sc.execute("""
SELECT COILID, SEQUENCENB, ROLLPROGRAMNB
FROM PDI_PLTM
ORDER BY COILID ASC
LIMIT ?
""", (count,))
rows = cursor.fetchall()
return [{"coilid": r[0], "sequencenb": r[1], "rollprogramnb": r[2]} for r in rows]
finally:
sc.close()
def sqlite_save_coils_to_track(coils: List[Dict[str, Any]]): def sqlite_save_coils_to_track(coils: List[Dict[str, Any]]):
sc = get_sqlite() sc = get_sqlite()
try: try:
sc.execute("DELETE FROM COIL_TRACK_TEMP") sc.execute("DELETE FROM COIL_TRACK_TEMP")
reversed_coils = list(reversed(coils)) for i, coil in enumerate(coils):
for i, coil in enumerate(reversed_coils):
sc.execute(""" sc.execute("""
INSERT INTO COIL_TRACK_TEMP (COILID, SEQUENCENB, ROLLPROGRAMNB, POSITION) INSERT INTO COIL_TRACK_TEMP (COILID, SEQUENCENB, ROLLPROGRAMNB, POSITION)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)

View File

@@ -7,27 +7,65 @@
<el-input v-model="form.opc_url" placeholder="opc.tcp://192.168.1.100:4840" style="width:360px" /> <el-input v-model="form.opc_url" placeholder="opc.tcp://192.168.1.100:4840" style="width:360px" />
<span class="hint">opc.tcp://IP:PORT</span> <span class="hint">opc.tcp://IP:PORT</span>
</el-form-item> </el-form-item>
<el-form-item label="计数器节点 ID">
<el-input v-model="form.counter_node" placeholder="ns=2;s=PL.TRACKMAP.COUNTER" style="width:360px" />
<span class="hint">节点值变化时触发采集</span>
</el-form-item>
<el-form-item label="轮询间隔(秒)"> <el-form-item label="轮询间隔(秒)">
<el-input-number v-model="form.poll_interval" :min="1" :max="60" style="width:120px" /> <el-input-number v-model="form.poll_interval" :min="1" :max="60" style="width:120px" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="panel-title" style="margin-top:16px">跟踪信号节点配置</div> <div class="panel-title" style="margin-top:16px">追踪节点配置</div>
<el-form :model="form" label-width="155px" size="small"> <el-form :model="form" label-width="155px" size="small">
<el-form-item label="追踪计数器节点">
<el-input v-model="form.counter_node" placeholder="ns=2;s=PL.TRACKMAP.COUNTER" style="width:360px" />
<span class="hint">追踪流程计数器节点值变化时触发采集</span>
</el-form-item>
<el-form-item label="信号1(入口钢卷)"> <el-form-item label="信号1(入口钢卷)">
<el-input v-model="form.signal1_node" placeholder="ns=2;s=PL.Signal.EntryCoil" style="width:360px" /> <el-input v-model="form.signal1_node" placeholder="ns=2;s=PL.Signal.EntryCoil" style="width:360px" />
<span class="hint">入口钢卷信号01触发</span> <span class="hint">入口钢卷信号01触发</span>
</el-form-item> </el-form-item>
<el-form-item label="信号2(焊接完成)"> <el-form-item label="信号2(焊接完成)">
<el-input v-model="form.signal2_node" placeholder="ns=2;s=PL.Signal.WeldDone" style="width:360px" /> <el-input v-model="form.signal2_node" placeholder="ns=2;s=PL.Signal.WeldDone" style="width:360px" />
<span class="hint">焊接完成信号01保持2秒触发</span> <span class="hint">焊接完成信号01且计数器变化触发</span>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="panel-title" style="margin-top:16px">计划写入触发节点配置</div>
<el-form :model="form" label-width="155px" size="small">
<el-form-item label="写入计数器节点">
<el-input v-model="form.write_counter_node" placeholder="ns=2;s=PL.Material.Counter" style="width:360px" />
<span class="hint">计数器变化触发写入流程与追踪计数器分开</span>
</el-form-item>
<el-form-item label="起始位置节点">
<el-input v-model="form.write_source_node" placeholder="ns=2;s=PL.Material.Source" style="width:360px" />
<span class="hint">读到100表示来源为计划表</span>
</el-form-item>
<el-form-item label="目标位置节点">
<el-input v-model="form.write_target_node" placeholder="ns=2;s=PL.Material.Target" style="width:360px" />
<span class="hint">1=一号开卷机, 2=二号开卷机</span>
</el-form-item>
<el-form-item label="S7 终端">
<el-input v-model="form.write_s7_endpoint" placeholder="192.168.0.10:102" style="width:360px" />
<span class="hint">计划写入采用 S7 直写格式 IP:PORT</span>
</el-form-item>
<el-form-item label="S7 Rack/Slot">
<el-input-number v-model="form.write_s7_rack" :min="0" :max="7" style="width:120px" />
<el-input-number v-model="form.write_s7_slot" :min="0" :max="31" style="width:120px;margin-left:8px" />
</el-form-item>
</el-form>
<div class="panel-title" style="margin-top:16px">一号开卷机字段地址映射(S7)</div>
<div v-for="field in writeFieldDefs" :key="'u1-' + field.key" class="node-row">
<div class="field-label">{{ field.label }}</div>
<span class="arrow"></span>
<el-input v-model="writeNodes['1'][field.key]" :placeholder="field.placeholder" size="small" style="width:320px" />
</div>
<div class="panel-title" style="margin-top:16px">二号开卷机字段地址映射(S7)</div>
<div v-for="field in writeFieldDefs" :key="'u2-' + field.key" class="node-row">
<div class="field-label">{{ field.label }}</div>
<span class="arrow"></span>
<el-input v-model="writeNodes['2'][field.key]" :placeholder="field.placeholder" size="small" style="width:320px" />
</div>
<div class="panel-title" style="margin-top:16px">跟踪图节点映射</div> <div class="panel-title" style="margin-top:16px">跟踪图节点映射</div>
<div style="font-size:12px;color:#888;margin-bottom:10px"> <div style="font-size:12px;color:#888;margin-bottom:10px">
Oracle列名 OPC节点ID必须包含 <b>position</b> Oracle列名 OPC节点ID必须包含 <b>position</b>
@@ -71,7 +109,32 @@ export default {
data() { data() {
return { return {
loading: false, saving: false, loading: false, saving: false,
form: { opc_url: '', counter_node: '', poll_interval: 2, signal1_node: '', signal2_node: '' }, form: {
opc_url: '',
counter_node: '',
poll_interval: 2,
signal1_node: '',
signal2_node: '',
write_counter_node: '',
write_source_node: '',
write_target_node: '',
write_s7_endpoint: '',
write_s7_rack: 0,
write_s7_slot: 1
},
writeFieldDefs: [
{ key: 'setup_data_revision', label: '设置数据修改', placeholder: 'DB35501.DINT0' },
{ key: 'coilid', label: '钢卷ID', placeholder: 'DB35501.Byte4 (A20)' },
{ key: 'entry_coil_weight', label: '入口钢卷重量', placeholder: 'DB35501.DBD26' },
{ key: 'entry_of_coil_length', label: '长度', placeholder: 'DB35501.DBD30' },
{ key: 'entry_coil_width', label: '宽度', placeholder: 'DB35501.DBD34' },
{ key: 'entry_coil_thickness', label: '厚度', placeholder: 'DB35501.DBD38' },
{ key: 'entry_of_coil_inner_diameter', label: '钢卷内径', placeholder: 'DB35501.DBD42' },
{ key: 'entry_of_coil_outer_diameter', label: '钢卷外径', placeholder: 'DB35501.DBD46' },
{ key: 'alloy_code', label: '合金代码', placeholder: 'DB35501.Byte50 (A4)' },
{ key: 'material', label: '材质', placeholder: 'DB35501.Byte54 (A20)' }
],
writeNodes: { '1': {}, '2': {} },
nodeList: [], nodeList: [],
status: { running: false, last_counter: null, last_update: null, log: [], track_state: '' }, status: { running: false, last_counter: null, last_update: null, log: [], track_state: '' },
statusTimer: null statusTimer: null
@@ -93,6 +156,16 @@ export default {
this.form.poll_interval = cfg.poll_interval this.form.poll_interval = cfg.poll_interval
this.form.signal1_node = cfg.signal1_node || '' this.form.signal1_node = cfg.signal1_node || ''
this.form.signal2_node = cfg.signal2_node || '' this.form.signal2_node = cfg.signal2_node || ''
this.form.write_counter_node = cfg.write_counter_node || ''
this.form.write_source_node = cfg.write_source_node || ''
this.form.write_target_node = cfg.write_target_node || ''
this.form.write_s7_endpoint = cfg.write_s7_endpoint || ''
this.form.write_s7_rack = cfg.write_s7_rack ?? 0
this.form.write_s7_slot = cfg.write_s7_slot ?? 1
this.writeNodes = {
'1': { ...(cfg.write_nodes?.['1'] || {}) },
'2': { ...(cfg.write_nodes?.['2'] || {}) }
}
this.nodeList = Object.entries(cfg.trackmap_nodes || {}).map(([col, node]) => ({ col, node })) this.nodeList = Object.entries(cfg.trackmap_nodes || {}).map(([col, node]) => ({ col, node }))
} catch (e) { this.$message.error('加载失败: ' + e.message) } } catch (e) { this.$message.error('加载失败: ' + e.message) }
finally { this.loading = false } finally { this.loading = false }
@@ -114,7 +187,7 @@ export default {
} }
this.saving = true this.saving = true
try { try {
await opcApi.saveConfig({ ...this.form, trackmap_nodes }) await opcApi.saveConfig({ ...this.form, trackmap_nodes, write_nodes: this.writeNodes })
this.$message.success('配置已保存OPC服务已重启') this.$message.success('配置已保存OPC服务已重启')
this.loadStatus() this.loadStatus()
} catch (e) { this.$message.error('保存失败: ' + e.message) } } catch (e) { this.$message.error('保存失败: ' + e.message) }
@@ -134,6 +207,7 @@ export default {
.hint { font-size:11px; color:#999; margin-left:10px; } .hint { font-size:11px; color:#999; margin-left:10px; }
.node-row { display:flex; align-items:center; gap:8px; margin-bottom:7px; padding:5px 8px; background:#fafafa; border:1px solid #eee; border-radius:2px; } .node-row { display:flex; align-items:center; gap:8px; margin-bottom:7px; padding:5px 8px; background:#fafafa; border:1px solid #eee; border-radius:2px; }
.arrow { font-size:14px; color:#888; font-weight:600; } .arrow { font-size:14px; color:#888; font-weight:600; }
.field-label { width: 180px; color:#666; font-size:12px; }
.log-box { background:#fafafa; border:1px solid #e8e8e8; border-radius:2px; padding:8px 12px; height:180px; overflow-y:auto; font-family:monospace; } .log-box { background:#fafafa; border:1px solid #e8e8e8; border-radius:2px; padding:8px 12px; height:180px; overflow-y:auto; font-family:monospace; }
.log-line { font-size:11px; color:#555; line-height:1.7; border-bottom:1px solid #f0f0f0; } .log-line { font-size:11px; color:#555; line-height:1.7; border-bottom:1px solid #f0f0f0; }
</style> </style>

View File

@@ -72,156 +72,139 @@
<div style="display:flex;gap:20px;"> <div style="display:flex;gap:20px;">
<!-- 左侧表单 --> <!-- 左侧表单 -->
<div style="flex:1;"> <div style="flex:1;">
<el-form :model="form" :rules="rules" ref="pdiForm" label-width="100px" size="small"> <el-form :model="form" :rules="rules" ref="pdiForm" label-width="120px" size="small">
<div class="section-title">基本信息</div> <div class="section-title">基本数据</div>
<el-row :gutter="14"> <el-row :gutter="14">
<el-col :span="12"><el-form-item label="号" prop="coilid"> <el-col :span="12"><el-form-item label="批次编号" prop="rollprogramnb">
<el-input v-model="form.coilid" :disabled="isEdit" /> <el-input-number v-model="form.rollprogramnb" :controls="false" style="width:200px" />
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="12"><el-form-item label="计划号"> <el-col :span="12"><el-form-item label="顺序" prop="sequencenb">
<el-input v-model="form.schedule_code" /> <el-input-number v-model="form.sequencenb" :controls="false" style="width:200px" />
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="12"><el-form-item label="轧制程序号"> <el-col :span="12"><el-form-item label="计划编号">
<el-input-number v-model="form.rollprogramnb" :controls="false" style="width:100%" /> <el-input v-model="form.schedule_code" style="width:200px" />
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="12"><el-form-item label="顺序号"> <el-col :span="12"><el-form-item label="下道机组">
<el-input-number v-model="form.sequencenb" :controls="false" style="width:100%" /> <el-input v-model="form.next_process_code" style="width:200px" />
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="12"><el-form-item label="来料钢种"> </el-row>
<el-select v-model="form.steel_grade" placeholder="请选择来料钢种" clearable filterable style="width:100%">
<div class="section-title">来料数据基本信息</div>
<el-row :gutter="14">
<!-- 左列来料物理参数 -->
<el-col :span="12">
<el-form-item label="来料卷号">
<el-input v-model="form.coilid" style="width:200px" />
</el-form-item>
<el-form-item label="来料厚度[mm]">
<el-input-number v-model="form.entry_coil_thickness" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
<el-form-item label="来料宽度[mm]">
<el-input-number v-model="form.entry_coil_width" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
<el-form-item label="来料内径[mm]">
<el-input-number v-model="form.entry_of_coil_inner_diameter" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
<el-form-item label="来料外径[mm]">
<el-input-number v-model="form.entry_of_coil_outer_diameter" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
<el-form-item label="来料长度[mm]">
<el-input-number v-model="form.entry_of_coil_length" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
<el-form-item label="来料重量[kg]">
<el-input-number v-model="form.entry_coil_weight" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
<el-form-item label="来料凸度">
<el-input-number v-model="form.crown_average" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
</el-col>
<!-- 右列钢种和订单信息 -->
<el-col :span="12">
<el-form-item label="来料钢种">
<el-select v-model="form.steel_grade" placeholder="请选择来料钢种" clearable filterable style="width:200px">
<el-option v-for="g in entryGrades" :key="g" :label="g" :value="g" /> <el-option v-for="g in entryGrades" :key="g" :label="g" :value="g" />
</el-select> </el-select>
</el-form-item></el-col> </el-form-item>
<el-form-item label="钢卷状态">
<el-select v-model="form.status" placeholder="请选择钢卷状态" style="width:200px">
<el-option label="计划" :value="0" />
<el-option label="鞍座" :value="1" />
<el-option label="开卷" :value="2" />
<el-option label="完成" :value="3" />
<el-option label="焊接" :value="4" />
<el-option label="待轧" :value="5" />
<el-option label="轧制" :value="6" />
<el-option label="拒绝" :value="9" />
</el-select>
</el-form-item>
<el-form-item label="成品钢种">
<el-select v-model="form.sg_sign" placeholder="请选择成品钢种" clearable filterable style="width:200px">
<el-option v-for="g in productGrades" :key="g" :label="g" :value="g" />
</el-select>
</el-form-item>
<el-form-item label="订单厚度[mm]">
<el-input-number v-model="form.order_thickness" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
<el-form-item label="订单宽度[mm]">
<el-input-number v-model="form.order_width" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
<el-form-item label="屈服强度[MPa]">
<el-input-number v-model="form.material_yield_point" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
<el-form-item label="焊接代码">
<el-input v-model="form.welding_code" style="width:200px" />
</el-form-item>
<el-form-item label="是否切边">
<el-select v-model="form.trimming" placeholder="请选择" style="width:200px">
<el-option label="切边" :value="1" />
<el-option label="不切边" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="切边后宽度[mm]">
<el-input-number v-model="form.trimming_width" :precision="3" :controls="false" style="width:200px" />
</el-form-item>
</el-col>
</el-row>
<div class="section-title">主数据</div>
<el-row :gutter="14">
<el-col :span="12"><el-form-item label="二级模型钢种"> <el-col :span="12"><el-form-item label="二级模型钢种">
<el-select v-model="form.l2_grade" placeholder="请选择二级模型钢种" clearable filterable style="width:100%"> <el-select v-model="form.l2_grade" placeholder="请选择二级模型钢种" clearable filterable style="width:200px">
<el-option v-for="g in l2ModelGrades" :key="g" :label="g" :value="g" /> <el-option v-for="g in l2ModelGrades" :key="g" :label="g" :value="g" />
</el-select> </el-select>
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="12"><el-form-item label="合同号"> <el-col :span="12"><el-form-item label="分卷模式">
<el-input v-model="form.work_order_no" /> <el-select v-model="form.slitting_mode" placeholder="请选择分卷模式" style="width:200px">
</el-form-item></el-col> <el-option label="不分卷" :value="0" />
<el-col :span="12"><el-form-item label="成品钢种"> <el-option label="分卷" :value="1" />
<el-select v-model="form.sg_sign" placeholder="请选择成品钢种" clearable filterable style="width:100%">
<el-option v-for="g in productGrades" :key="g" :label="g" :value="g" />
</el-select> </el-select>
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="12"> <el-col :span="12"><el-form-item label="成品重量">
<el-form-item label="包装类型"> <el-input-number v-model="form.exit_coil_weight" :precision="3" :controls="false" style="width:200px" />
<el-input v-model="form.packing_type_code" /> <span style="margin-left:8px;color:#666;">千克</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="卷筒直径">
<el-input-number v-model="form.coiler_diameter" :controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="重量模式">
<el-input v-model="form.weight_mode" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="订单质量">
<el-input v-model="form.order_quality" />
</el-form-item>
</el-col>
</el-row>
<div class="section-title">入口参数</div>
<el-row :gutter="14">
<el-col :span="8"><el-form-item label="厚度(mm)">
<el-input-number v-model="form.entry_coil_thickness" :precision="3" :controls="false"
style="width:100%" />
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="8"><el-form-item label="厚度最大"> <el-col :span="12"><el-form-item label="成品卷号">
<el-input-number v-model="form.entry_coil_thickness_max" :precision="3" :controls="false" <el-input v-model="form.exit_coil_no" style="width:200px" />
style="width:100%" />
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="8"><el-form-item label="厚度最小"> <el-col :span="12"><el-form-item label="成品厚度">
<el-input-number v-model="form.entry_coil_thickness_min" :precision="3" :controls="false" <el-input-number v-model="form.exit_coil_thickness" :precision="3" :controls="false" style="width:200px" />
style="width:100%" /> <span style="margin-left:8px;color:#666;">毫米</span>
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="8"><el-form-item label="宽度(mm)"> <el-col :span="12"><el-form-item label="成品宽度">
<el-input-number v-model="form.entry_coil_width" :precision="3" :controls="false" <el-input-number v-model="form.exit_coil_width" :precision="3" :controls="false" style="width:200px" />
style="width:100%" /> <span style="margin-left:8px;color:#666;">毫米</span>
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="8"><el-form-item label="宽度最大"> <el-col :span="12"><el-form-item label="成品套筒">
<el-input-number v-model="form.entry_coil_width_max" :precision="3" :controls="false" <el-input v-model="form.sleeve_code_of_cold_coil" placeholder="例如:无套筒" style="width:200px" />
style="width:100%" />
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="8"><el-form-item label="宽度最小"> <el-col :span="12"><el-form-item label="成品卷芯">
<el-input-number v-model="form.entry_coil_width_min" :precision="3" :controls="false" <el-input-number v-model="form.coiler_diameter" :precision="3" :controls="false" style="width:200px" />
style="width:100%" /> <span style="margin-left:8px;color:#666;">毫米</span>
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="8"><el-form-item label="重量(kg)"> <el-col :span="12"><el-form-item label="包装类型">
<el-input-number v-model="form.entry_coil_weight" :precision="3" :controls="false" <el-input v-model="form.packing_type_code" placeholder="例如裸包A01" style="width:200px" />
style="width:100%" />
</el-form-item></el-col> </el-form-item></el-col>
<el-col :span="8"><el-form-item label="长度(m)">
<el-input-number v-model="form.entry_of_coil_length" :precision="3" :controls="false"
style="width:100%" />
</el-form-item></el-col>
<el-col :span="8"><el-form-item label="外径(mm)">
<el-input-number v-model="form.entry_of_coil_outer_diameter" :precision="3" :controls="false"
style="width:100%" />
</el-form-item></el-col>
</el-row>
<div class="section-title">出口 / 订单参数</div>
<el-row :gutter="14">
<el-col :span="8"><el-form-item label="出口卷号">
<el-input v-model="form.exit_coil_no" />
</el-form-item></el-col>
<el-col :span="8"><el-form-item label="出口厚度(mm)">
<el-input-number v-model="form.exit_coil_thickness" :precision="3" :controls="false"
style="width:100%" />
</el-form-item></el-col>
<el-col :span="8"><el-form-item label="出口宽度(mm)">
<el-input-number v-model="form.exit_coil_width" :precision="3" :controls="false" style="width:100%" />
</el-form-item></el-col>
<el-col :span="8"><el-form-item label="出口重量(kg)">
<el-input-number v-model="form.exit_coil_weight" :precision="3" :controls="false"
style="width:100%" />
</el-form-item></el-col>
<el-col :span="8"><el-form-item label="订单厚度(mm)">
<el-input-number v-model="form.order_thickness" :precision="3" :controls="false" style="width:100%" />
</el-form-item></el-col>
<el-col :span="8"><el-form-item label="订单宽度(mm)">
<el-input-number v-model="form.order_width" :precision="3" :controls="false" style="width:100%" />
</el-form-item></el-col>
</el-row>
<div class="section-title">张力参数</div>
<el-row :gutter="14">
<el-col :span="8"><el-form-item label="开卷张力(kN)">
<el-input-number v-model="form.uncoiler_tension" :precision="3" :controls="false"
style="width:100%" />
</el-form-item></el-col>
<el-col :span="8"><el-form-item label="活套1张力(kN)">
<el-input-number v-model="form.looper_tension_1" :precision="3" :controls="false"
style="width:100%" />
</el-form-item></el-col>
<el-col :span="8"><el-form-item label="平整张力(kN)">
<el-input-number v-model="form.pl_tension" :precision="3" :controls="false" style="width:100%" />
</el-form-item></el-col>
<el-col :span="8"><el-form-item label="活套2张力(kN)">
<el-input-number v-model="form.looper_tension_2" :precision="3" :controls="false"
style="width:100%" />
</el-form-item></el-col>
<el-col :span="8"><el-form-item label="活套3张力(kN)">
<el-input-number v-model="form.looper_tension_3" :precision="3" :controls="false"
style="width:100%" />
</el-form-item></el-col>
</el-row>
<div class="section-title">化学成分 (%)</div>
<el-row :gutter="14">
<el-col :span="6" v-for="el in chemElements" :key="el">
<el-form-item :label="el.toUpperCase()" label-width="60px">
<el-input-number v-model="form[el]" :precision="3" :controls="false" style="width:100%" />
</el-form-item>
</el-col>
</el-row> </el-row>
</el-form> </el-form>
@@ -258,20 +241,17 @@
import { pdiApi, syncApi, gradeApi } from '../api/index' import { pdiApi, syncApi, gradeApi } from '../api/index'
const EMPTY = () => ({ const EMPTY = () => ({
coilid: '', rollprogramnb: null, sequencenb: null, rollprogramnb: null, sequencenb: null,
schedule_code: '', steel_grade: '', l2_grade: '', status: 0, schedule_code: '', coilid: '', next_process_code: '', steel_grade: '', l2_grade: '',
work_order_no: '', order_quality: '', sg_sign: '', sg_sign: '', packing_type_code: '', coiler_diameter: null,
packing_type_code: '', coiler_diameter: null, weight_mode: '', entry_coil_thickness: null, entry_coil_width: null,
entry_coil_thickness: null, entry_coil_thickness_max: null, entry_coil_thickness_min: null, entry_of_coil_inner_diameter: null, entry_of_coil_outer_diameter: null,
entry_coil_width: null, entry_coil_width_max: null, entry_coil_width_min: null, entry_of_coil_length: null, entry_coil_weight: null, crown_average: null,
entry_coil_weight: null, entry_of_coil_length: null, entry_of_coil_outer_diameter: null, order_thickness: null, order_width: null, material_yield_point: null,
exit_coil_no: '', exit_coil_thickness: null, exit_coil_width: null, exit_coil_weight: null, welding_code: '', trimming: 0, trimming_width: null,
order_thickness: null, order_width: null, exit_coil_weight: null, exit_coil_no: '',
uncoiler_tension: null, looper_tension_1: null, pl_tension: null, exit_coil_thickness: null, exit_coil_width: null,
looper_tension_2: null, looper_tension_3: null, sleeve_code_of_cold_coil: '', work_order_no: '', order_quality: ''
c: null, si: null, mn: null, p: null, s: null, cu: null,
ni: null, cr: null, mo: null, v: null, ti: null, sol_al: null,
nb: null, n: null, b: null, fe: null
}) })
export default { export default {
@@ -295,9 +275,11 @@ export default {
autoRefreshTimer: null, // 定时器句柄 autoRefreshTimer: null, // 定时器句柄
autoRefreshCountdown: 0, // 倒计时秒数 autoRefreshCountdown: 0, // 倒计时秒数
rules: { rules: {
coilid: [ rollprogramnb: [
{ required: true, message: '号不能为空', trigger: 'blur' }, { required: true, message: '批次编号不能为空', trigger: 'blur' }
{ min: 12, max: 12, message: '卷号必须为12位', trigger: 'blur' } ],
sequencenb: [
{ required: true, message: '顺序不能为空', trigger: 'blur' }
] ]
}, },
chemElements: ['c', 'si', 'mn', 'p', 's', 'cu', 'ni', 'cr', 'mo', 'v', 'ti', 'sol_al', 'nb', 'n', 'b', 'fe'], chemElements: ['c', 'si', 'mn', 'p', 's', 'cu', 'ni', 'cr', 'mo', 'v', 'ti', 'sol_al', 'nb', 'n', 'b', 'fe'],
@@ -555,5 +537,12 @@ export default {
</script> </script>
<style> <style>
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin: 20px 0 15px 0;
padding-bottom: 8px;
border-bottom: 2px solid #409EFF;
}
</style> </style>

View File

@@ -7,7 +7,7 @@
<span style="flex:1"></span> <span style="flex:1"></span>
<el-button size="small" type="warning" @click="triggerSignal1">模拟信号1(入口)</el-button> <el-button size="small" type="warning" @click="triggerSignal1">模拟信号1(入口)</el-button>
<el-button size="small" type="success" @click="triggerSignal2">模拟信号2(焊接完成)</el-button> <el-button size="small" type="success" @click="triggerSignal2">模拟信号2(焊接完成)</el-button>
<span style="font-size:12px;color:#666">提示: Position 1对应产线入口(顺序号1), Position 5对应产线出口(顺序号5)</span> <span style="font-size:12px;color:#666">1.出口 2.酸洗 3.入口活套 4.开卷机 提示:(最小的先进产线)</span>
</div> </div>
<div class="panel" style="padding:0;margin-top:10px"> <div class="panel" style="padding:0;margin-top:10px">
@@ -21,7 +21,7 @@
<template slot-scope="{row}"> <template slot-scope="{row}">
<el-button type="text" size="mini" @click.stop="openEdit(row)">编辑</el-button> <el-button type="text" size="mini" @click.stop="openEdit(row)">编辑</el-button>
<el-button type="text" size="mini" style="color:#e67e22" @click.stop="moveUp(row)" :disabled="row.position === 1">上移</el-button> <el-button type="text" size="mini" style="color:#e67e22" @click.stop="moveUp(row)" :disabled="row.position === 1">上移</el-button>
<el-button type="text" size="mini" style="color:#e67e22" @click.stop="moveDown(row)" :disabled="row.position === 5">下移</el-button> <el-button type="text" size="mini" style="color:#e67e22" @click.stop="moveDown(row)" :disabled="row.position === 4">下移</el-button>
<el-button type="text" size="mini" style="color:#c0392b" @click.stop="doDelete(row)">删除</el-button> <el-button type="text" size="mini" style="color:#c0392b" @click.stop="doDelete(row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -40,7 +40,7 @@
<el-input-number v-model="form.rollprogramnb" :controls="false" style="width:100%" /> <el-input-number v-model="form.rollprogramnb" :controls="false" style="width:100%" />
</el-form-item> </el-form-item>
<el-form-item label="位置" prop="position"> <el-form-item label="位置" prop="position">
<el-input-number v-model="form.position" :min="1" :max="5" :controls="false" style="width:100%" /> <el-input-number v-model="form.position" :min="1" :max="4" :controls="false" style="width:100%" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer"> <div slot="footer">