Compare commits
7 Commits
c609934156
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e41834e46 | |||
| 3ef502d737 | |||
| 0b07c2a2f1 | |||
| d2d445eff1 | |||
| 8b15f78e78 | |||
| 95ec77afae | |||
| fc8b38d44d |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -21,16 +21,11 @@ __pycache__/
|
||||
*$py.class
|
||||
|
||||
# 虚拟环境
|
||||
.env/
|
||||
.venv/
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# 环境变量文件 (包含敏感信息)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# 数据库文件 (SQLite 本地缓存)
|
||||
*.db
|
||||
|
||||
11
backend/.env
Normal file
11
backend/.env
Normal file
@@ -0,0 +1,11 @@
|
||||
# Oracle Database
|
||||
ORACLE_DSN=localhost:1521/orcl
|
||||
ORACLE_USER=pltm
|
||||
ORACLE_PASSWORD=pltm123
|
||||
|
||||
# OPC-UA Server
|
||||
OPC_URL=opc.tcp://127.0.0.1:49320
|
||||
OPC_COUNTER_NODE=ns=2;s=通道 1.PLCFur.LineMeasure-1.SystemCounter
|
||||
OPC_POLL_INTERVAL=2
|
||||
|
||||
|
||||
148
backend/main.py
148
backend/main.py
@@ -370,6 +370,13 @@ 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_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,
|
||||
"last_counter": opc_service.last_counter,
|
||||
"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.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_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:
|
||||
opc_service.save_config()
|
||||
except Exception as e:
|
||||
@@ -396,12 +411,36 @@ async def save_opc_config(config: OpcConfig):
|
||||
|
||||
@app.get("/api/opc/status")
|
||||
def opc_status():
|
||||
# 获取当前正在追踪的4个钢卷
|
||||
current_tracking_coils = []
|
||||
try:
|
||||
from sqlite_sync import sqlite_get_coil_track
|
||||
coils = sqlite_get_coil_track()
|
||||
current_tracking_coils = [
|
||||
{
|
||||
"coilid": coil.get("coilid", ""),
|
||||
"sequencenb": coil.get("sequencenb", 0),
|
||||
"rollprogramnb": coil.get("rollprogramnb", 0)
|
||||
}
|
||||
for coil in coils[:4]
|
||||
]
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get current tracking coils: {e}")
|
||||
|
||||
return {
|
||||
"running": opc_service.running,
|
||||
"last_counter": opc_service.last_counter,
|
||||
"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,
|
||||
"tracking_info": {
|
||||
"first_coilid": opc_service.first_coilid, # 这个保持不变
|
||||
"last_tracked_coilid": opc_service.last_tracked_coilid,
|
||||
"end_coilid": opc_service.end_coilid,
|
||||
"tracking_ended": opc_service.tracking_ended,
|
||||
"current_tracking_coils": current_tracking_coils # 当前正在追踪的4个钢卷
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -614,7 +653,6 @@ def get_coils_by_range(start: int = Query(1), end: int = Query(5)):
|
||||
def get_signal_config():
|
||||
"""获取信号节点配置"""
|
||||
return {
|
||||
"signal1_node": opc_service.signal1_node,
|
||||
"signal2_node": opc_service.signal2_node,
|
||||
}
|
||||
|
||||
@@ -622,7 +660,6 @@ def get_signal_config():
|
||||
@app.post("/api/opc/signals")
|
||||
async def save_signal_config(data: dict):
|
||||
"""保存信号节点配置"""
|
||||
opc_service.signal1_node = data.get("signal1_node", opc_service.signal1_node)
|
||||
opc_service.signal2_node = data.get("signal2_node", opc_service.signal2_node)
|
||||
try:
|
||||
opc_service.save_config()
|
||||
@@ -636,11 +673,7 @@ async def save_signal_config(data: dict):
|
||||
# 模拟信号接口 (用于测试)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.post("/api/track/simulate/signal1")
|
||||
async def simulate_signal1():
|
||||
"""模拟信号1触发 - 获取下5个钢卷"""
|
||||
await opc_service._handle_signal1()
|
||||
return {"message": "信号1已触发"}
|
||||
# 信号1模拟API已移除 - 信号1不再使用
|
||||
|
||||
|
||||
@app.post("/api/track/simulate/signal2")
|
||||
@@ -648,3 +681,104 @@ async def simulate_signal2():
|
||||
"""模拟信号2触发 - 更新追踪表"""
|
||||
await opc_service._handle_signal2()
|
||||
return {"message": "信号2已触发"}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Manual Starting Coil Configuration
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.post("/api/track/set-start-coil")
|
||||
async def set_start_coil(data: dict):
|
||||
"""设置手动起始钢卷ID用于追踪"""
|
||||
coilid = data.get("coilid")
|
||||
if not coilid:
|
||||
raise HTTPException(status_code=400, detail="coilid不能为空")
|
||||
|
||||
# 验证钢卷是否存在
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute("SELECT COUNT(*) FROM PLTM.PDI_PLTM WHERE COILID = :coilid", {"coilid": coilid})
|
||||
if cursor.fetchone()[0] == 0:
|
||||
raise HTTPException(status_code=404, detail=f"钢卷 {coilid} 不存在")
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 设置起始钢卷
|
||||
opc_service.set_manual_start_coil(coilid)
|
||||
|
||||
# 自动读取起始钢卷及后续3个钢卷到暂存表
|
||||
try:
|
||||
await opc_service._handle_signal1()
|
||||
except Exception as e:
|
||||
# 即使读取失败也不影响设置成功
|
||||
logger.warning(f"Failed to load initial coils after setting start coil: {e}")
|
||||
|
||||
return {"message": f"已设置起始钢卷: {coilid}"}
|
||||
|
||||
|
||||
@app.get("/api/track/available-coils")
|
||||
def get_available_coils(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=200),
|
||||
coilid: Optional[str] = None,
|
||||
sequencenb_min: Optional[int] = None,
|
||||
sequencenb_max: Optional[int] = None
|
||||
):
|
||||
"""获取可用的钢卷列表用于选择起始点(支持搜索和分页)"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
params = {}
|
||||
|
||||
if coilid:
|
||||
conditions.append("COILID LIKE :coilid")
|
||||
params["coilid"] = f"%{coilid}%"
|
||||
|
||||
if sequencenb_min is not None:
|
||||
conditions.append("SEQUENCENB >= :sequencenb_min")
|
||||
params["sequencenb_min"] = sequencenb_min
|
||||
|
||||
if sequencenb_max is not None:
|
||||
conditions.append("SEQUENCENB <= :sequencenb_max")
|
||||
params["sequencenb_max"] = sequencenb_max
|
||||
|
||||
where_clause = ""
|
||||
if conditions:
|
||||
where_clause = "WHERE " + " AND ".join(conditions)
|
||||
|
||||
# 查询总数
|
||||
count_sql = f"SELECT COUNT(*) FROM PLTM.PDI_PLTM {where_clause}"
|
||||
cursor.execute(count_sql, params)
|
||||
total = cursor.fetchone()[0]
|
||||
|
||||
# 分页查询
|
||||
offset = (page - 1) * page_size
|
||||
data_sql = f"""
|
||||
SELECT COILID, SEQUENCENB, ROLLPROGRAMNB, STEEL_GRADE,
|
||||
ENTRY_COIL_THICKNESS, ENTRY_COIL_WIDTH, ENTRY_COIL_WEIGHT
|
||||
FROM PLTM.PDI_PLTM
|
||||
{where_clause}
|
||||
ORDER BY COILID ASC
|
||||
"""
|
||||
cursor.execute(data_sql, params)
|
||||
|
||||
# 手动分页(因为Oracle的ROWNUM处理比较复杂)
|
||||
all_rows = cursor.fetchall()
|
||||
paginated_rows = all_rows[offset:offset + page_size]
|
||||
|
||||
columns = [col[0].lower() for col in cursor.description]
|
||||
rows = [dict(zip(columns, row)) for row in paginated_rows]
|
||||
|
||||
return {
|
||||
"data": rows,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
@@ -184,3 +184,10 @@ 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_s7_endpoint: str = ""
|
||||
write_s7_rack: int = 0
|
||||
write_s7_slot: int = 1
|
||||
write_nodes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
@@ -1,8 +1,39 @@
|
||||
{
|
||||
"opc_url": "opc.tcp://192.168.1.100:4840",
|
||||
"counter_node": "ns=2;s=PL.TRACKMAP.COUNTER",
|
||||
"opc_url": "opc.tcp://127.0.0.1:49320",
|
||||
"counter_node": "ns=2;s=通道 1.PLCFur.LineMeasure-1.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=通道 1.PLCFur.LineMeasure-1.WeldDone",
|
||||
"write_counter_node": "ns=2;s=通道 1.PLCFur.LineMeasure-1.WriteCounter",
|
||||
"write_source_node": "ns=2;s=通道 1.PLCFur.LineMeasure-1.Source",
|
||||
"write_target_node": "ns=2;s=通道 1.PLCFur.LineMeasure-1.Target",
|
||||
"write_s7_endpoint": "140.80.0.2:102",
|
||||
"write_s7_rack": 0,
|
||||
"write_s7_slot": 1,
|
||||
"write_nodes": {
|
||||
"1": {
|
||||
"setup_data_revision": "DB35501.DINT0",
|
||||
"coilid": "DB35501.DBB4",
|
||||
"entry_coil_weight": "DB35501.DBD26",
|
||||
"entry_of_coil_length": "DB35501.DBD30",
|
||||
"entry_coil_width": "DB35501.DBD34",
|
||||
"entry_coil_thickness": "DB35501.DBD38",
|
||||
"entry_of_coil_inner_diameter": "DB35501.DBD42",
|
||||
"entry_of_coil_outer_diameter": "DB35501.DBD46",
|
||||
"alloy_code": "DB35501.DBB50",
|
||||
"material": "DB35501.DBB56"
|
||||
},
|
||||
"2": {
|
||||
"setup_data_revision": "DB35501.DINT0",
|
||||
"coilid": "DB35501.DBB78",
|
||||
"entry_coil_weight": "DB35501.DBD100",
|
||||
"entry_of_coil_length": "DB35501.DBD104",
|
||||
"entry_coil_width": "DB35501.DBD108",
|
||||
"entry_coil_thickness": "DB35501.DBD112",
|
||||
"entry_of_coil_inner_diameter": "DB35501.DBD116",
|
||||
"entry_of_coil_outer_diameter": "DB35501.DBD120",
|
||||
"alloy_code": "DB35501.DBB124",
|
||||
"material": "DB35501.DBB130"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,21 @@ 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_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.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
|
||||
self.track_state: str = "WAIT_S1"
|
||||
self.last_counter_at_state_change: Optional[Any] = None
|
||||
@@ -79,6 +92,8 @@ class OpcService:
|
||||
self.event_log: List[str] = []
|
||||
self._stop_event = asyncio.Event()
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._need_fetch_next_batch: bool = False
|
||||
self._need_initial_load: bool = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _load_trackmap_nodes(self) -> Dict[str, str]:
|
||||
@@ -103,6 +118,13 @@ 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_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}")
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to load OPC config %s: %s", self.config_path, exc)
|
||||
@@ -116,6 +138,13 @@ 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_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)
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
@@ -166,6 +195,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()
|
||||
@@ -242,90 +272,467 @@ class OpcService:
|
||||
|
||||
counter_changed = current_counter != self.last_counter_at_state_change
|
||||
|
||||
# State machine for signal processing
|
||||
# Signal1自动化处理已禁用 - 只允许手动调用_handle_signal1函数
|
||||
# 信号1不再响应自动化触发,确保双向保护
|
||||
if self.track_state == "WAIT_S1":
|
||||
if signal1_value is not None and self.signal1_last is not None:
|
||||
if self.signal1_last == 0 and signal1_value == 1 and counter_changed:
|
||||
self._log(f"Signal1: Entry coil triggered (0->1) with counter change, state={self.track_state}")
|
||||
await self._handle_signal1()
|
||||
self.track_state = "WAIT_S2"
|
||||
self.last_counter_at_state_change = current_counter
|
||||
# 不再处理信号1的自动化触发
|
||||
# 信号1只能通过手动调用API或信号2内部逻辑触发
|
||||
pass
|
||||
|
||||
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")
|
||||
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")
|
||||
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 signal1_value is not None:
|
||||
self.signal1_last = signal1_value
|
||||
if signal2_value is not None:
|
||||
self.signal2_last = signal2_value
|
||||
|
||||
async def _handle_signal1(self):
|
||||
"""Handle signal1: fetch next 5 coils from PDI and save to SQLite temp table."""
|
||||
from sqlite_sync import (
|
||||
sqlite_get_max_sequencenb,
|
||||
sqlite_get_coils_by_sequencenb_range,
|
||||
sqlite_save_coils_to_track
|
||||
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(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
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def set_manual_start_coil(self, coilid: str):
|
||||
"""Set manual starting coil ID for tracking and load initial coils.
|
||||
|
||||
Args:
|
||||
coilid: The coil ID to start tracking from
|
||||
"""
|
||||
self.first_coilid = coilid
|
||||
self.last_tracked_coilid = None
|
||||
self.end_coilid = None
|
||||
self.tracking_ended = False
|
||||
self.signal1_coils = []
|
||||
self._log(f"Manual start coil set to: {coilid}")
|
||||
|
||||
# Clear any existing temp track data
|
||||
try:
|
||||
from sqlite_sync import sqlite_clear_coil_track
|
||||
sqlite_clear_coil_track()
|
||||
self._log("Cleared temp track data for manual start")
|
||||
except Exception as exc:
|
||||
self._log(f"Failed to clear temp track data: {exc}")
|
||||
|
||||
# 标记需要自动读取起始钢卷(由外部调用_handle_signal1)
|
||||
self._need_initial_load = True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
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():
|
||||
max_seq = sqlite_get_max_sequencenb()
|
||||
if max_seq is None or max_seq < 1:
|
||||
self._log("Signal1: No PDI data available")
|
||||
# 如果追踪已结束,检查是否有新钢卷需要恢复追踪
|
||||
self._log(f"Signal1: Called, first_coilid={repr(self.first_coilid)}, tracking_ended={self.tracking_ended}")
|
||||
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
|
||||
|
||||
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})")
|
||||
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
|
||||
|
||||
coils = sqlite_get_coils_by_sequencenb_range(start_seq, end_seq)
|
||||
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
|
||||
|
||||
self.signal1_coils = 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"]
|
||||
|
||||
# 详细日志:显示当前读取的4个钢卷信息
|
||||
coil_details = []
|
||||
for i, coil in enumerate(coils):
|
||||
coil_details.append(f"pos{i+1}:{coil['coilid']}(seq:{coil['sequencenb']})")
|
||||
|
||||
self._log(f"Signal1: Saved {len(coils)} coils [{', '.join(coil_details)}], next_start: {self.first_coilid}, last_tracked: {self.last_tracked_coilid}")
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, _do_fetch)
|
||||
|
||||
async def _handle_signal2(self):
|
||||
"""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
|
||||
|
||||
def _do_update():
|
||||
coils = sqlite_get_coil_track()
|
||||
if not coils:
|
||||
self._log("Signal2: No coils in temp track table")
|
||||
return
|
||||
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
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)
|
||||
for i in range(5):
|
||||
for i in range(4):
|
||||
target_position = i + 1
|
||||
coil_index = target_position - 1
|
||||
if coil_index >= 0 and coil_index < coil_count:
|
||||
@@ -357,8 +764,58 @@ class OpcService:
|
||||
INSERT INTO PLTM.CMPT_PL_TRACKMAP (POSITION, COILID, TOM)
|
||||
VALUES (:position, NULL, SYSDATE)
|
||||
""", {"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()
|
||||
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}")
|
||||
else:
|
||||
# 还有钢卷,标记需要自动读取下一批
|
||||
self._log(f"Signal2: Will auto-trigger Signal1 logic to fetch next batch of coils")
|
||||
self._need_fetch_next_batch = True
|
||||
except Exception as exc:
|
||||
self._log(f"Signal2: Check next coils failed: {exc}")
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
@@ -370,6 +827,15 @@ class OpcService:
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, _do_update)
|
||||
|
||||
# 如果标记需要读取下一批,则调用Signal1的逻辑
|
||||
if hasattr(self, '_need_fetch_next_batch') and self._need_fetch_next_batch:
|
||||
self._need_fetch_next_batch = False
|
||||
try:
|
||||
await self._handle_signal1()
|
||||
self._log("Signal2: Successfully fetched next batch of coils")
|
||||
except Exception as exc:
|
||||
self._log(f"Signal2: Failed to fetch next batch: {exc}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
async def _update_oracle(self, position: Any, data: Dict[str, Any]):
|
||||
"""Write fetched OPC values into PLTM.CMPT_PL_TRACKMAP."""
|
||||
@@ -540,6 +1006,14 @@ class OpcService:
|
||||
raise ValueError(f"{dt} data_length must be 8")
|
||||
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 data_length < 1:
|
||||
raise ValueError(f"{dt} data_length must be >= 1")
|
||||
@@ -552,7 +1026,7 @@ class OpcService:
|
||||
|
||||
raise ValueError(
|
||||
"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(
|
||||
@@ -681,6 +1155,27 @@ class OpcService:
|
||||
if area_part.startswith("DB"):
|
||||
area_part = area_part[2:]
|
||||
|
||||
# Normalize aliases:
|
||||
# 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:]
|
||||
|
||||
@@ -696,6 +1191,8 @@ class OpcService:
|
||||
else:
|
||||
if "." in offset_part:
|
||||
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)
|
||||
|
||||
if byte_offset < 0:
|
||||
@@ -754,6 +1251,13 @@ class OpcService:
|
||||
raise ValueError(f"{dt} data_length must be 8")
|
||||
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 isinstance(value, bytes):
|
||||
raw = value
|
||||
@@ -765,9 +1269,35 @@ class OpcService:
|
||||
|
||||
raise ValueError(
|
||||
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(
|
||||
self,
|
||||
endpoint: str,
|
||||
|
||||
@@ -337,7 +337,7 @@ def sqlite_get_coils_by_sequencenb_range(start_seq: int, end_seq: int) -> List[D
|
||||
SELECT COILID, SEQUENCENB, ROLLPROGRAMNB
|
||||
FROM PDI_PLTM
|
||||
WHERE SEQUENCENB >= ? AND SEQUENCENB <= ?
|
||||
ORDER BY SEQUENCENB DESC
|
||||
ORDER BY COILID ASC
|
||||
""", (start_seq, end_seq))
|
||||
rows = cursor.fetchall()
|
||||
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()
|
||||
|
||||
|
||||
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]]):
|
||||
sc = get_sqlite()
|
||||
try:
|
||||
sc.execute("DELETE FROM COIL_TRACK_TEMP")
|
||||
reversed_coils = list(reversed(coils))
|
||||
for i, coil in enumerate(reversed_coils):
|
||||
for i, coil in enumerate(coils):
|
||||
sc.execute("""
|
||||
INSERT INTO COIL_TRACK_TEMP (COILID, SEQUENCENB, ROLLPROGRAMNB, POSITION)
|
||||
VALUES (?, ?, ?, ?)
|
||||
|
||||
@@ -7,27 +7,61 @@
|
||||
<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>
|
||||
</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-input-number v-model="form.poll_interval" :min="1" :max="60" style="width:120px" />
|
||||
</el-form-item>
|
||||
</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-item label="信号1(入口钢卷)">
|
||||
<el-input v-model="form.signal1_node" placeholder="ns=2;s=PL.Signal.EntryCoil" style="width:360px" />
|
||||
<span class="hint">入口钢卷信号,0→1触发</span>
|
||||
<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="信号2(焊接完成)">
|
||||
<el-input v-model="form.signal2_node" placeholder="ns=2;s=PL.Signal.WeldDone" style="width:360px" />
|
||||
<span class="hint">焊接完成信号,0→1保持2秒触发</span>
|
||||
<span class="hint">焊接完成信号,0→1且计数器变化触发</span>
|
||||
</el-form-item>
|
||||
</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 style="font-size:12px;color:#888;margin-bottom:10px">
|
||||
Oracle列名 → OPC节点ID。必须包含 <b>position</b> 列。
|
||||
@@ -71,7 +105,32 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
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: [],
|
||||
status: { running: false, last_counter: null, last_update: null, log: [], track_state: '' },
|
||||
statusTimer: null
|
||||
@@ -93,6 +152,16 @@ export default {
|
||||
this.form.poll_interval = cfg.poll_interval
|
||||
this.form.signal1_node = cfg.signal1_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 }))
|
||||
} catch (e) { this.$message.error('加载失败: ' + e.message) }
|
||||
finally { this.loading = false }
|
||||
@@ -114,7 +183,7 @@ export default {
|
||||
}
|
||||
this.saving = true
|
||||
try {
|
||||
await opcApi.saveConfig({ ...this.form, trackmap_nodes })
|
||||
await opcApi.saveConfig({ ...this.form, trackmap_nodes, write_nodes: this.writeNodes })
|
||||
this.$message.success('配置已保存,OPC服务已重启')
|
||||
this.loadStatus()
|
||||
} catch (e) { this.$message.error('保存失败: ' + e.message) }
|
||||
@@ -134,6 +203,7 @@ export default {
|
||||
.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; }
|
||||
.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-line { font-size:11px; color:#555; line-height:1.7; border-bottom:1px solid #f0f0f0; }
|
||||
</style>
|
||||
@@ -72,156 +72,139 @@
|
||||
<div style="display:flex;gap:20px;">
|
||||
<!-- 左侧表单 -->
|
||||
<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-col :span="12"><el-form-item label="卷号" prop="coilid">
|
||||
<el-input v-model="form.coilid" :disabled="isEdit" />
|
||||
<el-col :span="12"><el-form-item label="批次编号" prop="rollprogramnb">
|
||||
<el-input-number v-model="form.rollprogramnb" :controls="false" style="width:200px" />
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="计划号">
|
||||
<el-input v-model="form.schedule_code" />
|
||||
<el-col :span="12"><el-form-item label="顺序" prop="sequencenb">
|
||||
<el-input-number v-model="form.sequencenb" :controls="false" style="width:200px" />
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="轧制程序号">
|
||||
<el-input-number v-model="form.rollprogramnb" :controls="false" style="width:100%" />
|
||||
<el-col :span="12"><el-form-item label="计划编号">
|
||||
<el-input v-model="form.schedule_code" style="width:200px" />
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="顺序号">
|
||||
<el-input-number v-model="form.sequencenb" :controls="false" style="width:100%" />
|
||||
<el-col :span="12"><el-form-item label="下道机组">
|
||||
<el-input v-model="form.next_process_code" 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:100%">
|
||||
</el-row>
|
||||
|
||||
<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-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-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-select>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="合同号">
|
||||
<el-input v-model="form.work_order_no" />
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="成品钢种">
|
||||
<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-col :span="12"><el-form-item label="分卷模式">
|
||||
<el-select v-model="form.slitting_mode" placeholder="请选择分卷模式" style="width:200px">
|
||||
<el-option label="不分卷" :value="0" />
|
||||
<el-option label="分卷" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="包装类型">
|
||||
<el-input v-model="form.packing_type_code" />
|
||||
</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-col :span="12"><el-form-item label="成品重量">
|
||||
<el-input-number v-model="form.exit_coil_weight" :precision="3" :controls="false" style="width:200px" />
|
||||
<span style="margin-left:8px;color:#666;">千克</span>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="厚度最大">
|
||||
<el-input-number v-model="form.entry_coil_thickness_max" :precision="3" :controls="false"
|
||||
style="width:100%" />
|
||||
<el-col :span="12"><el-form-item label="成品卷号">
|
||||
<el-input v-model="form.exit_coil_no" style="width:200px" />
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="厚度最小">
|
||||
<el-input-number v-model="form.entry_coil_thickness_min" :precision="3" :controls="false"
|
||||
style="width:100%" />
|
||||
<el-col :span="12"><el-form-item label="成品厚度">
|
||||
<el-input-number v-model="form.exit_coil_thickness" :precision="3" :controls="false" style="width:200px" />
|
||||
<span style="margin-left:8px;color:#666;">毫米</span>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="宽度(mm)">
|
||||
<el-input-number v-model="form.entry_coil_width" :precision="3" :controls="false"
|
||||
style="width:100%" />
|
||||
<el-col :span="12"><el-form-item label="成品宽度">
|
||||
<el-input-number v-model="form.exit_coil_width" :precision="3" :controls="false" style="width:200px" />
|
||||
<span style="margin-left:8px;color:#666;">毫米</span>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="宽度最大">
|
||||
<el-input-number v-model="form.entry_coil_width_max" :precision="3" :controls="false"
|
||||
style="width:100%" />
|
||||
<el-col :span="12"><el-form-item label="成品套筒">
|
||||
<el-input v-model="form.sleeve_code_of_cold_coil" placeholder="例如:无套筒" style="width:200px" />
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="宽度最小">
|
||||
<el-input-number v-model="form.entry_coil_width_min" :precision="3" :controls="false"
|
||||
style="width:100%" />
|
||||
<el-col :span="12"><el-form-item label="成品卷芯">
|
||||
<el-input-number v-model="form.coiler_diameter" :precision="3" :controls="false" style="width:200px" />
|
||||
<span style="margin-left:8px;color:#666;">毫米</span>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="重量(kg)">
|
||||
<el-input-number v-model="form.entry_coil_weight" :precision="3" :controls="false"
|
||||
style="width:100%" />
|
||||
<el-col :span="12"><el-form-item label="包装类型">
|
||||
<el-input v-model="form.packing_type_code" placeholder="例如:裸包A01" style="width:200px" />
|
||||
</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-form>
|
||||
@@ -258,20 +241,17 @@
|
||||
import { pdiApi, syncApi, gradeApi } from '../api/index'
|
||||
|
||||
const EMPTY = () => ({
|
||||
coilid: '', rollprogramnb: null, sequencenb: null,
|
||||
schedule_code: '', steel_grade: '', l2_grade: '',
|
||||
work_order_no: '', order_quality: '', sg_sign: '',
|
||||
packing_type_code: '', coiler_diameter: null, weight_mode: '',
|
||||
entry_coil_thickness: null, entry_coil_thickness_max: null, entry_coil_thickness_min: null,
|
||||
entry_coil_width: null, entry_coil_width_max: null, entry_coil_width_min: null,
|
||||
entry_coil_weight: null, entry_of_coil_length: null, entry_of_coil_outer_diameter: null,
|
||||
exit_coil_no: '', exit_coil_thickness: null, exit_coil_width: null, exit_coil_weight: null,
|
||||
order_thickness: null, order_width: null,
|
||||
uncoiler_tension: null, looper_tension_1: null, pl_tension: null,
|
||||
looper_tension_2: null, looper_tension_3: null,
|
||||
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
|
||||
rollprogramnb: null, sequencenb: null,
|
||||
status: 0, schedule_code: '', coilid: '', next_process_code: '', steel_grade: '', l2_grade: '',
|
||||
sg_sign: '', packing_type_code: '', coiler_diameter: null,
|
||||
entry_coil_thickness: null, entry_coil_width: null,
|
||||
entry_of_coil_inner_diameter: null, entry_of_coil_outer_diameter: null,
|
||||
entry_of_coil_length: null, entry_coil_weight: null, crown_average: null,
|
||||
order_thickness: null, order_width: null, material_yield_point: null,
|
||||
welding_code: '', trimming: 0, trimming_width: null,
|
||||
exit_coil_weight: null, exit_coil_no: '',
|
||||
exit_coil_thickness: null, exit_coil_width: null,
|
||||
sleeve_code_of_cold_coil: '', work_order_no: '', order_quality: ''
|
||||
})
|
||||
|
||||
export default {
|
||||
@@ -295,9 +275,11 @@ export default {
|
||||
autoRefreshTimer: null, // 定时器句柄
|
||||
autoRefreshCountdown: 0, // 倒计时秒数
|
||||
rules: {
|
||||
coilid: [
|
||||
{ required: true, message: '卷号不能为空', trigger: 'blur' },
|
||||
{ min: 12, max: 12, message: '卷号必须为12位', trigger: 'blur' }
|
||||
rollprogramnb: [
|
||||
{ required: true, message: '批次编号不能为空', 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'],
|
||||
@@ -555,5 +537,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<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>
|
||||
@@ -1,13 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 起始钢卷信息显示 -->
|
||||
<div class="panel" style="background:#f8f9fa;border:1px solid #e9ecef">
|
||||
<h3 style="margin:0 0 10px 0;color:#495057">起始钢卷信息</h3>
|
||||
<div v-if="startCoilInfo.coilid" style="display:flex;align-items:center;gap:20px">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="color:#dc3545;font-weight:bold;font-size:16px">起始钢卷:</span>
|
||||
<span style="color:#dc3545;font-weight:bold;font-size:16px">{{ startCoilInfo.coilid }}</span>
|
||||
<el-tag type="danger" size="small">顺序号: {{ startCoilInfo.sequencenb }}</el-tag>
|
||||
</div>
|
||||
<div style="color:#6c757d;font-size:14px">
|
||||
<span>设置时间: {{ startCoilInfo.set_time || '未知' }}</span>
|
||||
</div>
|
||||
<div style="flex:1"></div>
|
||||
<el-button size="small" type="warning" icon="el-icon-location" @click="openStartCoilDialog">重新设置</el-button>
|
||||
</div>
|
||||
<div v-else style="color:#6c757d;text-align:center;padding:20px">
|
||||
<span>尚未设置起始钢卷</span>
|
||||
<el-button size="small" type="primary" style="margin-left:10px" @click="openStartCoilDialog">设置起始钢卷</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<el-button size="small" @click="loadCoils">刷新</el-button>
|
||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="openAdd">新增钢卷</el-button>
|
||||
<el-button size="small" type="danger" icon="el-icon-delete" @click="clearAll">清空全部</el-button>
|
||||
<span style="flex:1"></span>
|
||||
<el-button size="small" type="warning" @click="triggerSignal1">模拟信号1(入口)</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 class="panel" style="padding:0;margin-top:10px">
|
||||
@@ -21,7 +40,7 @@
|
||||
<template slot-scope="{row}">
|
||||
<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="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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -40,7 +59,7 @@
|
||||
<el-input-number v-model="form.rollprogramnb" :controls="false" style="width:100%" />
|
||||
</el-form-item>
|
||||
<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>
|
||||
<div slot="footer">
|
||||
@@ -48,11 +67,80 @@
|
||||
<el-button type="primary" size="small" @click="doSave" :loading="saving">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 起始钢卷选择对话框 -->
|
||||
<el-dialog title="选择起始钢卷" :visible.sync="startCoilDialogVisible" width="1400px">
|
||||
<div style="margin-bottom:15px">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8">
|
||||
<el-input
|
||||
v-model="searchCoilid"
|
||||
placeholder="搜索钢卷号"
|
||||
clearable
|
||||
size="small"
|
||||
@clear="searchAvailableCoils"
|
||||
@keyup.enter.native="searchAvailableCoils"
|
||||
>
|
||||
<el-button slot="append" icon="el-icon-search" @click="searchAvailableCoils"></el-button>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-input
|
||||
v-model="searchSequencenb"
|
||||
placeholder="顺序号筛选"
|
||||
clearable
|
||||
size="small"
|
||||
@clear="searchAvailableCoils"
|
||||
@keyup.enter.native="searchAvailableCoils"
|
||||
>
|
||||
<el-button slot="append" icon="el-icon-search" @click="searchAvailableCoils"></el-button>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button size="small" style="width:100%" @click="searchAvailableCoils">刷新</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-table :data="availableCoils" stripe border size="small" v-loading="searchLoading" element-loading-text="搜索中..." style="width:100%" height="400" :empty-text="'暂无数据'">
|
||||
<el-table-column prop="sequencenb" label="顺序号" align="center" />
|
||||
<el-table-column prop="coilid" label="钢卷号" />
|
||||
<el-table-column prop="rollprogramnb" label="计划号" align="center" />
|
||||
<el-table-column prop="steel_grade" label="钢种" width="150" />
|
||||
<el-table-column prop="entry_coil_thickness" label="厚度(mm)" width="100" align="center" />
|
||||
<el-table-column prop="entry_coil_width" label="宽度(mm)" width="100" align="center" />
|
||||
<el-table-column prop="entry_coil_weight" label="重量(kg)" width="110" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" type="success">计划中</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-button type="primary" size="mini" @click="selectStartCoil(row)">选择</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top:15px;text-align:center">
|
||||
<el-pagination
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="searchParams.page"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="searchParams.page_size"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="searchTotal">
|
||||
</el-pagination>
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="startCoilDialogVisible = false">取消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { trackApi } from '../api/index'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'TrackCoil',
|
||||
@@ -74,6 +162,22 @@ export default {
|
||||
coilid: [
|
||||
{ required: true, message: '钢卷号不能为空', trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
// 起始钢卷选择相关
|
||||
startCoilDialogVisible: false,
|
||||
availableCoils: [],
|
||||
searchLoading: false,
|
||||
searchCoilid: '',
|
||||
searchSequencenb: '',
|
||||
searchParams: {
|
||||
page: 1,
|
||||
page_size: 20
|
||||
},
|
||||
searchTotal: 0,
|
||||
startCoilInfo: {
|
||||
coilid: '',
|
||||
sequencenb: 0,
|
||||
set_time: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -83,7 +187,18 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadStartCoilFromStorage()
|
||||
this.loadCoils()
|
||||
this.loadTrackStatus()
|
||||
// 定时刷新状态
|
||||
this.statusTimer = setInterval(() => {
|
||||
this.loadTrackStatus()
|
||||
}, 2000)
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.statusTimer) {
|
||||
clearInterval(this.statusTimer)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadCoils() {
|
||||
@@ -220,11 +335,152 @@ export default {
|
||||
async triggerSignal2() {
|
||||
try {
|
||||
await trackApi.simulateSignal2()
|
||||
this.$message.success('信号2已触发 - 已更新追踪表')
|
||||
this.$message.success('信号2已触发 - 已读取钢卷并更新追踪表')
|
||||
this.loadCoils()
|
||||
} catch (e) {
|
||||
this.$message.error(e.message)
|
||||
}
|
||||
},
|
||||
// 起始钢卷持久化存储方法
|
||||
loadStartCoilFromStorage() {
|
||||
try {
|
||||
const saved = localStorage.getItem('startCoilInfo')
|
||||
if (saved) {
|
||||
this.startCoilInfo = JSON.parse(saved)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load start coil from storage:', e)
|
||||
}
|
||||
},
|
||||
saveStartCoilToStorage() {
|
||||
try {
|
||||
localStorage.setItem('startCoilInfo', JSON.stringify(this.startCoilInfo))
|
||||
} catch (e) {
|
||||
console.error('Failed to save start coil to storage:', e)
|
||||
}
|
||||
},
|
||||
clearStartCoilFromStorage() {
|
||||
try {
|
||||
localStorage.removeItem('startCoilInfo')
|
||||
} catch (e) {
|
||||
console.error('Failed to clear start coil from storage:', e)
|
||||
}
|
||||
},
|
||||
// 起始钢卷选择相关方法
|
||||
async loadTrackStatus() {
|
||||
try {
|
||||
const res = await axios.get('/api/opc/status')
|
||||
const tracking_info = res.data.tracking_info || {}
|
||||
const first_coilid = tracking_info.first_coilid || ''
|
||||
|
||||
// 如果localStorage中有起始钢卷信息,优先使用localStorage的
|
||||
// 不再随后端状态变化而变化
|
||||
if (this.startCoilInfo.coilid) {
|
||||
// 有本地存储的起始钢卷,保持不变
|
||||
return
|
||||
}
|
||||
|
||||
// 只有在没有本地存储时,才检查后端是否有历史设置
|
||||
if (first_coilid) {
|
||||
try {
|
||||
const params = { page: 1, page_size: 1, coilid: first_coilid }
|
||||
const coilRes = await axios.get('/api/track/available-coils', { params })
|
||||
if (coilRes.data.data && coilRes.data.data.length > 0) {
|
||||
const coil = coilRes.data.data[0]
|
||||
this.startCoilInfo = {
|
||||
coilid: coil.coilid,
|
||||
sequencenb: coil.sequencenb,
|
||||
set_time: '历史设置' // 标记为历史设置,不是当前手动设置
|
||||
}
|
||||
// 保存到localStorage
|
||||
this.saveStartCoilToStorage()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get start coil details:', e)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load track status:', e)
|
||||
}
|
||||
},
|
||||
isStartCoil(coilid) {
|
||||
return this.startCoilInfo.coilid === coilid
|
||||
},
|
||||
openStartCoilDialog() {
|
||||
this.startCoilDialogVisible = true
|
||||
this.searchAvailableCoils()
|
||||
},
|
||||
clearStartCoil() {
|
||||
this.startCoilInfo = {
|
||||
coilid: '',
|
||||
sequencenb: 0,
|
||||
set_time: ''
|
||||
}
|
||||
this.clearStartCoilFromStorage()
|
||||
},
|
||||
async searchAvailableCoils() {
|
||||
this.searchLoading = true
|
||||
try {
|
||||
const params = {
|
||||
page: this.searchParams.page,
|
||||
page_size: this.searchParams.page_size,
|
||||
coilid: this.searchCoilid || undefined
|
||||
}
|
||||
|
||||
// 添加顺序号筛选
|
||||
if (this.searchSequencenb) {
|
||||
const seqNb = parseInt(this.searchSequencenb)
|
||||
if (!isNaN(seqNb)) {
|
||||
params.sequencenb_min = seqNb
|
||||
params.sequencenb_max = seqNb
|
||||
}
|
||||
}
|
||||
|
||||
const res = await axios.get('/api/track/available-coils', { params })
|
||||
this.availableCoils = res.data.data || []
|
||||
this.searchTotal = res.data.total || 0
|
||||
} catch (e) {
|
||||
this.$message.error('搜索钢卷失败: ' + e.message)
|
||||
} finally {
|
||||
this.searchLoading = false
|
||||
}
|
||||
},
|
||||
async selectStartCoil(row) {
|
||||
this.$confirm(`确认设置钢卷 [${row.coilid}] 作为追踪起始点?\n设置后将自动加载该钢卷及后续3个钢卷到追踪表。`, '设置起始钢卷', {
|
||||
confirmButtonText: '确认设置',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
// 设置起始钢卷
|
||||
const res = await axios.post('/api/track/set-start-coil', { coilid: row.coilid })
|
||||
this.$message.success(res.data.message)
|
||||
|
||||
// 更新起始钢卷信息并保存到localStorage
|
||||
this.startCoilInfo = {
|
||||
coilid: row.coilid,
|
||||
sequencenb: row.sequencenb,
|
||||
set_time: new Date().toLocaleString('zh-CN')
|
||||
}
|
||||
this.saveStartCoilToStorage()
|
||||
|
||||
// 重新加载钢卷列表
|
||||
this.loadCoils()
|
||||
|
||||
this.startCoilDialogVisible = false
|
||||
} catch (e) {
|
||||
this.$message.error(e.response?.data?.detail || e.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.searchParams.page_size = val
|
||||
this.searchParams.page = 1
|
||||
this.searchAvailableCoils()
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.searchParams.page = val
|
||||
this.searchAvailableCoils()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ set "BACKEND_DIR=backend"
|
||||
set "FRONTEND_DIR=frontend"
|
||||
:: =====================================================
|
||||
|
||||
:: 初始化 conda(关键:让脚本内部能使用 conda 命令)
|
||||
for /f "delims=" %%i in ('conda info --base') do set "CONDA_BASE=%%i"
|
||||
call "%CONDA_BASE%\shell\condabin\conda-hook.cmd"
|
||||
echo ✅ conda 环境初始化完成
|
||||
|
||||
echo.
|
||||
echo [1/4] 检查并创建 conda 环境...
|
||||
conda env list | findstr %CONDA_ENV%
|
||||
if %errorlevel% equ 0 (
|
||||
@@ -33,8 +39,11 @@ cd ..
|
||||
|
||||
echo.
|
||||
echo [4/4] 启动前后端...
|
||||
start "【后端】" cmd /k "chcp 65001 & call conda activate %CONDA_ENV% & cd /d ""%BACKEND_DIR%"" & uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
|
||||
:: 启动后端
|
||||
start "【后端】" cmd /k "chcp 65001 & for /f "delims=" %%i in ('conda info --base') do set "CONDA_BASE=%%i" & call "%%CONDA_BASE%%\shell\condabin\conda-hook.cmd" & call conda activate %CONDA_ENV% & cd /d ""%BACKEND_DIR%"" & uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
|
||||
timeout /t 2 /nobreak >nul
|
||||
|
||||
:: 启动前端
|
||||
start "【前端】" cmd /k "chcp 65001 & cd /d ""%FRONTEND_DIR%"" & npm run serve"
|
||||
|
||||
echo.
|
||||
|
||||
Reference in New Issue
Block a user