Make_PartDry.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Data;
  5. using System.Drawing;
  6. using System.Net.Sockets;
  7. using System.Text;
  8. using System.Windows.Forms;
  9. using UAS_MES_NEW.DataOperate;
  10. using UAS_MES_NEW.Entity;
  11. using UAS_MES_NEW.PublicMethod;
  12. namespace UAS_MES_NEW.Make
  13. {
  14. public partial class Make_PartDry : Form
  15. {
  16. AutoSizeFormClass asc = new AutoSizeFormClass();
  17. DataHelper dh;
  18. DataTable dt;
  19. // ============ 零件烘烤批次相关 ============
  20. // 工序类型:烘烤页面固定写DRY,写入SB_STEPCODE用于和零件清洗区分
  21. const string StepCode = "DRY";
  22. // 当前批次号:结束烘烤后置空,下一个录入的SN会生成新的批次号
  23. string currentBatch = "";
  24. // ============ PLC温度采集相关(Modbus) ============
  25. // Modbus 连接对象(保持长连接,断线自动重连)
  26. TcpClient plcClient;
  27. NetworkStream plcStream;
  28. bool plcConnected = false;
  29. ushort transactionId = 0;
  30. readonly object readLock = new object();
  31. // 台达PLC数据寄存器:D寄存器对应Modbus保持寄存器(功能码0x03)
  32. // ETH-DVP-2P模块手册寄存器表:D0~4095 的Modbus地址 = 0x1000 + D编号
  33. // 如 D141 → 0x1000+141 = 0x108D;D4096以上是另一段(0x9000起),当前用不到
  34. const int D141_ADDR = 141;
  35. const int DVP_D_OFFSET = 0x1000;
  36. const int READ_COUNT = 2;
  37. // 温度记录表:每次采集往里插一条(1#舱、2#舱各一条)
  38. const string TempLogTable = "TEMPLOG";
  39. const string TempLogSeq = "TEMPLOG_SEQ";
  40. public Make_PartDry()
  41. {
  42. InitializeComponent();
  43. }
  44. private void Make_PartDry_Load(object sender, EventArgs e)
  45. {
  46. asc.controllInitializeSize(this);
  47. dh = SystemInf.dh;
  48. // PLC连接参数默认值(ETH-DVP-2P模块默认IP,端口固定502)
  49. ip.Text = "192.168.1.150";
  50. port.Value = 502;
  51. unitid.Value = 1;
  52. interval.Value = 2;
  53. decimals.Value = 1;
  54. SetStatus("未连接PLC,请录入SN或先连接设备");
  55. // 恢复未结束的批次(比如中途退出页面的情况)
  56. object b = dh.getFieldDataByCondition("SINGLEBOARD", "max(SB_BATCHCODE)", "SB_STEPCODE='" + StepCode + "' and SB_ENDTIME is null");
  57. currentBatch = b == null ? "" : b.ToString();
  58. if (currentBatch == "")
  59. {
  60. lblBatchCode.Text = "(暂无,录入SN时自动生成)";
  61. }
  62. else
  63. {
  64. lblBatchCode.Text = currentBatch;
  65. LoadBatchData(currentBatch);
  66. }
  67. sncode.Focus();
  68. }
  69. #region 零件烘烤:SN录入与批次管理
  70. /// <summary>
  71. /// 录入框回车:插入SN到SINGLEBOARD,本批次共用同一个批次号
  72. /// </summary>
  73. private void sncode_KeyDown(object sender, KeyEventArgs e)
  74. {
  75. if (e.KeyCode == Keys.Enter)
  76. {
  77. InsertSN();
  78. e.Handled = true;
  79. e.SuppressKeyPress = true;
  80. }
  81. }
  82. private void InsertSN()
  83. {
  84. string sn = sncode.Text.Trim();
  85. if (sn == "")
  86. {
  87. SetStatus("SN不能为空!");
  88. return;
  89. }
  90. try
  91. {
  92. // 没有进行中的批次时生成新批次号:PD+年月日时分秒
  93. if (currentBatch == "")
  94. {
  95. currentBatch = GenBatchCode();
  96. lblBatchCode.Text = currentBatch;
  97. }
  98. // 同一批次内SN不允许重复录入
  99. dt = (DataTable)dh.ExecuteSql("select SB_ID from SINGLEBOARD where SB_SNCODE='" + sn + "' and SB_BATCHCODE='" + currentBatch + "' and SB_STEPCODE='" + StepCode + "'", "select");
  100. if (dt.Rows.Count > 0)
  101. {
  102. SetStatus("SN【" + sn + "】在本批次已录入,请勿重复!");
  103. sncode.Clear();
  104. sncode.Focus();
  105. return;
  106. }
  107. dh.ExecuteSql("insert into SINGLEBOARD(SB_ID,SB_SNCODE,SB_INDATE,SB_STATUS,SB_STEPCODE,SB_BATCHCODE,SB_INMAN)values(SINGLEBOARD_SEQ.nextval,'" + sn + "',sysdate,'在录入','" + StepCode + "','" + currentBatch + "','" + User.UserName + "')", "insert");
  108. SetStatus("SN【" + sn + "】录入成功,批次号" + currentBatch);
  109. sncode.Clear();
  110. sncode.Focus();
  111. LoadBatchData(currentBatch);
  112. }
  113. catch (Exception ex)
  114. {
  115. SetStatus("录入失败:" + ex.Message);
  116. }
  117. }
  118. /// <summary>
  119. /// 生成批次号:PD+年月日时分秒,长度16在SB_BATCHCODE(20)范围内
  120. /// </summary>
  121. private string GenBatchCode()
  122. {
  123. return "PD" + DateTime.Now.ToString("yyyyMMddHHmmss");
  124. }
  125. /// <summary>
  126. /// 开始烘烤:给当前批次所有未开始的记录写开始时间
  127. /// </summary>
  128. private void btnStart_Click(object sender, EventArgs e)
  129. {
  130. if (currentBatch == "")
  131. {
  132. MessageBox.Show("当前没有进行中的批次,请先录入SN!");
  133. return;
  134. }
  135. try
  136. {
  137. dh.ExecuteSql("update SINGLEBOARD set SB_STARTTIME='" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "',SB_STATUS='烘烤中' where SB_BATCHCODE='" + currentBatch + "' and SB_STEPCODE='" + StepCode + "' and SB_STARTTIME is null", "update");
  138. SetStatus("批次【" + currentBatch + "】烘烤已开始");
  139. LoadBatchData(currentBatch);
  140. }
  141. catch (Exception ex)
  142. {
  143. SetStatus("开始烘烤失败:" + ex.Message);
  144. }
  145. }
  146. /// <summary>
  147. /// 结束烘烤:给当前批次所有未结束的记录写结束时间,之后录入的SN生成新批次
  148. /// </summary>
  149. private void btnEnd_Click(object sender, EventArgs e)
  150. {
  151. if (currentBatch == "")
  152. {
  153. MessageBox.Show("当前没有进行中的批次!");
  154. return;
  155. }
  156. try
  157. {
  158. dh.ExecuteSql("update SINGLEBOARD set SB_ENDTIME='" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "',SB_STATUS='已完成' where SB_BATCHCODE='" + currentBatch + "' and SB_STEPCODE='" + StepCode + "' and SB_ENDTIME is null", "update");
  159. SetStatus("批次【" + currentBatch + "】已结束,下一批录入SN时生成新批次号");
  160. // 保留显示刚结束批次的数据,批次号置空
  161. LoadBatchData(currentBatch);
  162. currentBatch = "";
  163. lblBatchCode.Text = "(暂无,录入SN时自动生成)";
  164. }
  165. catch (Exception ex)
  166. {
  167. SetStatus("结束烘烤失败:" + ex.Message);
  168. }
  169. }
  170. /// <summary>
  171. /// 按批次号查询
  172. /// </summary>
  173. private void btnQuery_Click(object sender, EventArgs e)
  174. {
  175. string b = txtQueryBatch.Text.Trim();
  176. if (b == "")
  177. {
  178. MessageBox.Show("请输入要查询的批次号!");
  179. return;
  180. }
  181. LoadBatchData(b);
  182. }
  183. /// <summary>
  184. /// 加载指定批次的数据显示在表格里
  185. /// </summary>
  186. private void LoadBatchData(string batch)
  187. {
  188. dt = (DataTable)dh.ExecuteSql("select SB_SNCODE \"SN\",SB_INDATE \"录入时间\",SB_STARTTIME \"开始烘烤时间\",SB_INMAN \"录入人\",SB_STATUS \"状态\" from SINGLEBOARD where SB_BATCHCODE='" + batch + "' and SB_STEPCODE='" + StepCode + "' order by SB_ID", "select");
  189. DGV.DataSource = dt;
  190. }
  191. #endregion
  192. #region PLC温度采集(Modbus)
  193. /// <summary>
  194. /// 连接串口转网口模块(ETH-DVP-2P,Modbus TCP)
  195. /// </summary>
  196. private void Connect_Click(object sender, EventArgs e)
  197. {
  198. try
  199. {
  200. ClosePlc();
  201. plcClient = new TcpClient();
  202. IAsyncResult ar = plcClient.BeginConnect(ip.Text.Trim(), (int)port.Value, null, null);
  203. if (!ar.AsyncWaitHandle.WaitOne(3000))
  204. {
  205. ClosePlc();
  206. SetStatus("PLC连接超时");
  207. MessageBox.Show("连接PLC超时,请检查IP和端口!");
  208. return;
  209. }
  210. plcClient.EndConnect(ar);
  211. plcClient.ReceiveTimeout = 5000;
  212. plcClient.SendTimeout = 5000;
  213. plcStream = plcClient.GetStream();
  214. plcConnected = true;
  215. OperatResult.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] 已连接 " + ip.Text.Trim() + ":" + port.Value + ",站号" + unitid.Value + "\n");
  216. SetStatus("已连接PLC " + ip.Text.Trim() + ":" + port.Value);
  217. timer1.Interval = (int)interval.Value * 1000;
  218. timer1.Start();
  219. // 连接成功后立即读一次
  220. ReadNow_Click(null, null);
  221. }
  222. catch (Exception ex)
  223. {
  224. ClosePlc();
  225. SetStatus("PLC连接失败:" + ex.Message);
  226. MessageBox.Show("连接失败:" + ex.Message);
  227. }
  228. }
  229. /// <summary>
  230. /// 断开PLC连接
  231. /// </summary>
  232. private void Disconnect_Click(object sender, EventArgs e)
  233. {
  234. timer1.Stop();
  235. ClosePlc();
  236. SetStatus("PLC已断开");
  237. OperatResult.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] 已断开连接\n");
  238. }
  239. /// <summary>
  240. /// 立即读取一次温度
  241. /// </summary>
  242. private void ReadNow_Click(object sender, EventArgs e)
  243. {
  244. if (!plcConnected)
  245. {
  246. SetStatus("未连接PLC");
  247. return;
  248. }
  249. try
  250. {
  251. // D寄存器地址要加台达DVP的0x1000偏移:D141→0x108D, D142→0x108E
  252. int[] values = ReadRegisters((ushort)(DVP_D_OFFSET + D141_ADDR), READ_COUNT);
  253. ShowValues(values);
  254. InsertTempLog(values);
  255. SetStatus("读取成功 " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
  256. }
  257. catch (Exception ex)
  258. {
  259. // 读取失败:断开连接,等待下一次重连
  260. timer1.Stop();
  261. ClosePlc();
  262. plcConnected = false;
  263. string mode = rdoTCP.Checked ? "ModbusTCP" : "ModbusRTU(透传)";
  264. SetStatus("读取失败:" + ex.Message);
  265. OperatResult.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] 读取失败(" + mode + "):" + ex.Message + "\n");
  266. if (ex.Message.Contains("超时") || ex.Message.Contains("没有正确答复") || ex.Message.Contains("连接已断开"))
  267. {
  268. OperatResult.AppendText("[提示] 如果RTU(透传)模式一直超时,请尝试切换到ModbusTCP模式;反之亦然。同时确认模块透传设置、PLC串口参数(9600/8/N/1)和站号。\n");
  269. }
  270. // 自动重连
  271. TryReconnect();
  272. }
  273. }
  274. /// <summary>
  275. /// 定时获取温度数据
  276. /// </summary>
  277. private void timer1_Tick(object sender, EventArgs e)
  278. {
  279. if (!plcConnected)
  280. {
  281. TryReconnect();
  282. return;
  283. }
  284. ReadNow_Click(null, null);
  285. }
  286. /// <summary>
  287. /// 尝试重新连接(不打断界面,仅更新状态)
  288. /// </summary>
  289. private void TryReconnect()
  290. {
  291. try
  292. {
  293. ClosePlc();
  294. plcClient = new TcpClient();
  295. IAsyncResult ar = plcClient.BeginConnect(ip.Text.Trim(), (int)port.Value, null, null);
  296. if (!ar.AsyncWaitHandle.WaitOne(3000))
  297. {
  298. ClosePlc();
  299. SetStatus("PLC重连超时,继续重试...");
  300. return;
  301. }
  302. plcClient.EndConnect(ar);
  303. plcClient.ReceiveTimeout = 5000;
  304. plcClient.SendTimeout = 5000;
  305. plcStream = plcClient.GetStream();
  306. plcConnected = true;
  307. SetStatus("PLC重连成功 " + DateTime.Now.ToString("HH:mm:ss"));
  308. OperatResult.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] 重连成功\n");
  309. }
  310. catch
  311. {
  312. ClosePlc();
  313. SetStatus("PLC重连失败,继续重试...");
  314. }
  315. }
  316. /// <summary>
  317. /// 把本次采集的温度写入TEMPLOG表,1#舱、2#舱各一条
  318. /// TL_ID=序列TEMPLOG_SEQ,TL_DATE=sysdate,TL_DEVICEID=舱号,TL_TP=换算后的温度
  319. /// </summary>
  320. private void InsertTempLog(int[] values)
  321. {
  322. if (values == null || values.Length < 2) return;
  323. try
  324. {
  325. int d = (int)decimals.Value;
  326. double v1 = values[0] / Math.Pow(10, d);
  327. double v2 = values[1] / Math.Pow(10, d);
  328. string v1s = v1.ToString("0.###");
  329. string v2s = v2.ToString("0.###");
  330. dh.ExecuteSql("insert into " + TempLogTable + "(TL_ID,TL_DATE,TL_DEVICEID,TL_TP)values(" + TempLogSeq + ".nextval,sysdate,'1#'," + v1s + ")", "insert");
  331. dh.ExecuteSql("insert into " + TempLogTable + "(TL_ID,TL_DATE,TL_DEVICEID,TL_TP)values(" + TempLogSeq + ".nextval,sysdate,'2#'," + v2s + ")", "insert");
  332. }
  333. catch (Exception ex)
  334. {
  335. // 插入失败不影响采集显示,只记录
  336. OperatResult.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] 写入" + TempLogTable + "失败:" + ex.Message + "\n");
  337. }
  338. }
  339. /// <summary>
  340. /// 显示温度值,并根据小数位设置换算(台达一般存实际值x10)
  341. /// </summary>
  342. private void ShowValues(int[] values)
  343. {
  344. if (values == null || values.Length < 2) return;
  345. int d = (int)decimals.Value;
  346. double v1 = values[0] / Math.Pow(10, d);
  347. double v2 = values[1] / Math.Pow(10, d);
  348. string fmt = d > 0 ? "F" + d : "F0";
  349. lblD141Value.Text = v1.ToString(fmt);
  350. lblD142Value.Text = v2.ToString(fmt);
  351. OperatResult.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] 1#舱(D141)=" + values[0] + " 2#舱(D142)=" + values[1] + "\n");
  352. }
  353. /// <summary>
  354. /// 读保持寄存器(功能码0x03),按界面选择的协议分发:
  355. /// ModbusRTU(串口透传):模块把TCP数据原样转发到串口,要发带CRC16的RTU裸帧
  356. /// ModbusTCP:模块/PLC本身是标准Modbus TCP服务端,发带MBAP头的帧
  357. /// </summary>
  358. private int[] ReadRegisters(ushort startAddr, ushort count)
  359. {
  360. if (rdoTCP.Checked)
  361. {
  362. return ReadTcpRegisters(startAddr, count);
  363. }
  364. return ReadRtuRegisters(startAddr, count);
  365. }
  366. /// <summary>
  367. /// Modbus RTU over TCP(串口透传模式):站号+功能码+数据+CRC16
  368. /// </summary>
  369. private int[] ReadRtuRegisters(ushort startAddr, ushort count)
  370. {
  371. lock (readLock)
  372. {
  373. if (!plcConnected || plcStream == null)
  374. {
  375. throw new Exception("未连接PLC");
  376. }
  377. byte unit = (byte)unitid.Value;
  378. byte[] req = new byte[8];
  379. req[0] = unit;
  380. // 功能码0x03 读保持寄存器
  381. req[1] = 0x03;
  382. req[2] = (byte)(startAddr >> 8);
  383. req[3] = (byte)(startAddr & 0xFF);
  384. req[4] = (byte)(count >> 8);
  385. req[5] = (byte)(count & 0xFF);
  386. ushort crc = CalcCRC16(req, 6);
  387. // Modbus RTU CRC低字节在前
  388. req[6] = (byte)(crc & 0xFF);
  389. req[7] = (byte)((crc >> 8) & 0xFF);
  390. plcStream.Write(req, 0, req.Length);
  391. // 响应:站号+功能码+字节数
  392. byte[] head = ReadBytes(plcStream, 3);
  393. if (head[0] != unit)
  394. {
  395. throw new Exception("响应站号不匹配(收到0x" + head[0].ToString("X2") + "),请检查站号设置");
  396. }
  397. if ((head[1] & 0x80) != 0)
  398. {
  399. byte[] rest = ReadBytes(plcStream, 3);
  400. throw new Exception("PLC返回异常码 0x" + rest[0].ToString("X2"));
  401. }
  402. int byteCount = head[2];
  403. byte[] rest2 = ReadBytes(plcStream, byteCount + 2);
  404. // CRC校验:响应最后2字节是CRC
  405. byte[] full = new byte[3 + byteCount + 2];
  406. Array.Copy(head, 0, full, 0, 3);
  407. Array.Copy(rest2, 0, full, 3, byteCount + 2);
  408. ushort calcCrc = CalcCRC16(full, full.Length - 2);
  409. ushort recvCrc = (ushort)(full[full.Length - 2] | (full[full.Length - 1] << 8));
  410. if (calcCrc != recvCrc)
  411. {
  412. throw new Exception("响应CRC校验失败,串口参数(波特率/校验位)可能与PLC不一致");
  413. }
  414. int[] values = new int[count];
  415. for (int i = 0; i < count; i++)
  416. {
  417. values[i] = (rest2[i * 2] << 8) | rest2[i * 2 + 1];
  418. }
  419. return values;
  420. }
  421. }
  422. /// <summary>
  423. /// Modbus TCP 读保持寄存器(功能码0x03),带MBAP头
  424. /// </summary>
  425. private int[] ReadTcpRegisters(ushort startAddr, ushort count)
  426. {
  427. lock (readLock)
  428. {
  429. if (!plcConnected || plcStream == null)
  430. {
  431. throw new Exception("未连接PLC");
  432. }
  433. transactionId++;
  434. byte[] req = new byte[12];
  435. // MBAP头:事务标识、协议标识0、长度6、单元标识符
  436. req[0] = (byte)(transactionId >> 8);
  437. req[1] = (byte)(transactionId & 0xFF);
  438. req[2] = 0x00;
  439. req[3] = 0x00;
  440. req[4] = 0x00;
  441. req[5] = 0x06;
  442. req[6] = (byte)unitid.Value;
  443. // 功能码0x03 读保持寄存器
  444. req[7] = 0x03;
  445. req[8] = (byte)(startAddr >> 8);
  446. req[9] = (byte)(startAddr & 0xFF);
  447. req[10] = (byte)(count >> 8);
  448. req[11] = (byte)(count & 0xFF);
  449. plcStream.Write(req, 0, req.Length);
  450. byte[] head = ReadBytes(plcStream, 9);
  451. if ((head[7] & 0x80) != 0)
  452. {
  453. byte[] ec = ReadBytes(plcStream, 1);
  454. throw new Exception("PLC返回异常码 0x" + (ec.Length > 0 ? ec[0] : 0).ToString("X2"));
  455. }
  456. int byteCount = head[8];
  457. byte[] data = ReadBytes(plcStream, byteCount);
  458. int[] values = new int[count];
  459. for (int i = 0; i < count; i++)
  460. {
  461. values[i] = (data[i * 2] << 8) | data[i * 2 + 1];
  462. }
  463. return values;
  464. }
  465. }
  466. /// <summary>
  467. /// Modbus RTU CRC16校验
  468. /// </summary>
  469. private ushort CalcCRC16(byte[] data, int length)
  470. {
  471. ushort crc = 0xFFFF;
  472. for (int i = 0; i < length; i++)
  473. {
  474. crc ^= data[i];
  475. for (int j = 0; j < 8; j++)
  476. {
  477. if ((crc & 0x0001) != 0)
  478. {
  479. crc = (ushort)((crc >> 1) ^ 0xA001);
  480. }
  481. else
  482. {
  483. crc = (ushort)(crc >> 1);
  484. }
  485. }
  486. }
  487. return crc;
  488. }
  489. /// <summary>
  490. /// 从流中读满指定长度,读不满视为连接异常
  491. /// </summary>
  492. private byte[] ReadBytes(NetworkStream stream, int length)
  493. {
  494. byte[] buffer = new byte[length];
  495. int read = 0;
  496. while (read < length)
  497. {
  498. int n = stream.Read(buffer, read, length - read);
  499. if (n <= 0)
  500. {
  501. throw new Exception("连接已断开");
  502. }
  503. read += n;
  504. }
  505. return buffer;
  506. }
  507. /// <summary>
  508. /// 关闭PLC连接
  509. /// </summary>
  510. private void ClosePlc()
  511. {
  512. try
  513. {
  514. if (plcStream != null)
  515. {
  516. plcStream.Close();
  517. plcStream = null;
  518. }
  519. if (plcClient != null)
  520. {
  521. plcClient.Close();
  522. plcClient = null;
  523. }
  524. }
  525. catch { }
  526. plcConnected = false;
  527. }
  528. #endregion
  529. private void SetStatus(string text)
  530. {
  531. lblStatus.Text = "状态:" + text;
  532. }
  533. private void Make_PartDry_FormClosing(object sender, FormClosingEventArgs e)
  534. {
  535. timer1.Stop();
  536. ClosePlc();
  537. }
  538. private void Make_PartDry_SizeChanged(object sender, EventArgs e)
  539. {
  540. asc.controlAutoSize(this);
  541. }
  542. }
  543. }