feat(追踪系统): 添加手动设置起始钢卷功能

- 后端添加设置起始钢卷API和钢卷查询API
- 前端实现起始钢卷选择界面和状态管理
- 优化追踪逻辑自动加载后续钢卷
- 添加本地存储保持起始钢卷设置
This commit is contained in:
2026-04-30 11:45:42 +08:00
parent 0b07c2a2f1
commit 3ef502d737
3 changed files with 430 additions and 13 deletions

View File

@@ -411,6 +411,22 @@ 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,
@@ -418,6 +434,13 @@ def opc_status():
"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个钢卷
}
}
@@ -664,3 +687,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()

View File

@@ -92,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]:
@@ -569,6 +571,32 @@ class OpcService:
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
@@ -663,7 +691,12 @@ class OpcService:
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}")
# 详细日志显示当前读取的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)
@@ -760,7 +793,7 @@ class OpcService:
conn.commit()
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:
@@ -779,15 +812,10 @@ class OpcService:
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"
)
# 还有钢卷,标记需要自动读取下一批
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:
@@ -800,6 +828,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]):