封装SQL
csharp
cmd.Parameters.AddWithValue
给 SQL 里的参数赋值
就是把:
时间、压力、流量这些具体数值,塞进 SQL 语句里。
大白话:
把值传进去,填坑。
csharp
cmd.ExecuteNonQuery()
执行 SQL 语句
执行 建表 / 插入数据 / 更新数据。
大白话:
真正动手把数据存进数据库。
csharp
var close=connType.GetMethod("Close");
close.Invoke(conn, null);
通过反射拿到 Close 方法,然后调用它,把数据库连接关掉。
connType.GetMethod("Close")
找到数据库连接里的 关闭方法
close.Invoke(conn, null)
执行这个关闭方法,把连接关掉,释放资源
csharp
try
{
if(!File.Exists(_csvPath))
{
File.WriteAllText(_csvPath, "Timestamp,Pressure,Flow,TimeValue,Reg4Value\n",Encoding.UTF8);
}
var line= $"{DateTime.Now:O},{data.PressureValue},{data.Flow},{data.Time},{data.Reg4Value}\n";
File.AppendAllText(_csvPath, line, Encoding.UTF8);
}
核心
这是往 CSV 文件里存数据,没有文件就先创建表头,有就直接追加一行数据。
csharp
if (!File.Exists(_csvPath))
//判断 CSV 文件有没有,没有就新建一个
File.WriteAllText(...)
//写表头:时间、压力、流量、时间值、寄存器值
//就是第一行标题。
var line = $"...";
//拼一行数据:当前时间 + 压力 + 流量 + 设备数据
File.AppendAllText(...)
//把这一行追加写到文件里
//不会覆盖,只会往下加。
面试只背这一句(满分)
这段代码是将设备数据保存到 CSV 文件,没有文件就创建表头,有文件就直接追加数据。
完美!就这么简单!
csharp
_dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbFile);
拼接文件路径
封装通信类
private readonly object _lockObj = new object(); // 关键:串口锁
csharp
// 定义锁(全局唯一)
private readonly object _lockObj = new object();
public void Disconnect()
{
// 加锁:此时就算定时器线程正在读串口,也得等这个断连操作做完
lock(_lockObj)
{
_cts?.Cancel();
_serialPort?.Close(); // 关串口时,绝对不会有其他线程在读串口了
// ...其他释放操作
}
}
// 读数据的方法也要加锁
private async Task ReadSerialData()
{
lock(_lockObj)
{
// 读串口数据:此时断连操作也进不来,不会中途关串口
var data = _serialPort.ReadExisting();
}
}
csharp
public bool ConnectTcp(string host, int port)
{
//throw new NotImplementedException();
try
{
Disconnect();
_cts = new CancellationTokenSource();
//创建 CancellationTokenSource 用于控制后台异步数据读取任务的取消,保证断开连接时能安全停止任务。
_tcpClient = new TcpClient();
_tcpClient.Connect(host, port);
//创建 TCP 客户端,去连接设备。
LastError = null;
_master = ModbusIpMaster.CreateIp(_tcpClient);
//创建 Modbus 通讯工具,用来和设备收发数据。
_master.Transport.Retries = 1;
_master.Transport.WaitToRetryMilliseconds = 50;
//设置重试次数和超时时间,通讯更稳定。
_readTimer.Start();
return true;
}catch(Exception ex)
{
LastError = ex.ToString();
OnError?.Invoke($"TCP连接失败:{ex.Message}");
return false;
//连接失败 → 记录错误,发出错误通知,返回 false。
}
}
csharp
public bool Connect(string portName,int baudRate)
{
lock (_lockObj) // 加锁:防止多线程同时点"连接",避免串口冲突
{
// 1. 记录本次连接的串口名/波特率(方便后续重连用)
_lastPortName= portName;
_lastBaudRate= baudRate;
// 2. 先断开旧连接(核心!避免重复连接导致串口被占)
Disconnect();
// 3. 重置取消令牌(给新的读取任务用)
_cts=new CancellationTokenSource();
try
{
// 4. 调用你之前的"智能重试串口连接"方法,连不上直接返回false
if(!OpenSerial(portName, baudRate))
{
return false;
}
// 5. 创建Modbus RTU通讯对象(串口专用),配置重试参数
_modbusMaster = ModbusSerialMaster.CreateRtu(_serialPort);
_modbusMaster.Transport.Retries = 1; // 读失败重试1次
_modbusMaster.Transport.WaitToRetryMilliseconds = 50; // 重试间隔50毫秒
// 6. 启动后台读取任务(_readTask就是这里创建的)
StartReading();
// 7. 全部成功,返回true
return true;
}
catch (Exception ex)
{
// 8. 连接出错:记录错误、通知界面、AI分析故障
LastError= ex.ToString();
OnError?.Invoke($"连接串口发生异常:{ex.Message}");
_ = AnalyzeExceptionAsync(ex, "Connect");
// 9. 出错后兜底:断开所有可能的残留连接
Disconnect();
// 10. 返回false,告诉调用方连接失败
return false;
}
}
}
_ = AnalyzeExceptionAsync(ex, "Connect");
_ = 只是为了消除编译器警告,不影响功能
csharp
public void Disconnect()
{
//throw new NotImplementedException();
try
{
_readTimer?.Stop();
//停止自动读取数据的定时器
_cts?.Cancel();
//安全取消后台异步任务(就是你刚才问的取消令牌)
_master?.Dispose();
_master = null;
//释放 Modbus 通讯工具
if (_tcpClient != null)
{
try { _tcpClient.Close(); } catch { }
_tcpClient= null;
}
//关闭 TCP 连接,清空对象,彻底断开设备
}
catch { }
//出错也不崩溃,保证安全
}
csharp
private void StopTimer()
{
lock (_lockObj) // 加个锁:防止多线程乱操作,避免报错
{
_readTimer?.Stop(); // 停止定时器 → 不再自动触发读取
_cts?.Cancel(); // 取消令牌 → 让正在执行的异步读取立刻停止
}
}
csharp
public void Disconnect()
{
lock(_lockObj) // 加锁:防止多线程同时操作(比如一边断连、一边读数据),避免报错
{
try
{
_cts?.Cancel(); // 1. 取消令牌:让正在读数据的异步任务立刻停手
_modbusMaster?.Dispose(); // 2. 释放Modbus通讯对象
_modbusMaster= null; // 3. 清空对象,避免后续误操作
_serialPort?.Close(); // 4. 关闭串口连接(核心:断开和设备的物理连接)
_serialPort?.Dispose(); // 5. 释放串口资源(操作系统层面回收)
_serialPort= null; // 6. 清空串口对象
}
catch { } // 吞掉所有异常:断连时就算出错,也不影响程序运行
finally
{
// 7. 等后台读取任务结束(最多等50毫秒)
try { _readTask?.Wait(50); } catch { }
}
}
}
csharp
public void Disconnect()
{
lock (_lockObj) // 1. 上锁!同一时间只能断开,防止线程打架
{
try
{
// 2. 工长喊停工:工人、监工全部停止干活!
try { _cts?.Cancel(); } catch { }
// 3. 【重点优化!】等工人收尾200ms,等监工收尾200ms
// 给双方足够时间下班,不卡死、不残留
try { if (_readTask != null) _readTask.Wait(200); } catch { }
try { if (_heartbeatTask != null) _heartbeatTask.Wait(200); } catch { }
// 4. 销毁工长,释放资源 → 工长彻底消失
try { _cts?.Dispose(); } catch { }
_cts = null;
// 5. 销毁Modbus通信工具
try { _modbusMaster?.Dispose(); } catch { }
_modbusMaster = null;
// 6. 关闭串口 → 销毁串口 → 彻底释放硬件资源
try { _serialPort?.Close(); } catch { }
try { _serialPort?.Dispose(); } catch { }
_serialPort = null;
}
catch { } // 所有异常都捕获,工业软件绝不崩溃
}
finally
{
// 7. 最后兜底:工人、监工直接置空,彻底清场
_readTask = null;
_heartbeatTask = null;
}
}
_readTask
_readTask 是你程序里后台读取串口 / Modbus 数据的异步任务对象,简单说就是 "正在干活的读取线程",Wait(50) 就是等这个 "干活线程" 最多 50 毫秒,让它收尾完再彻底断连。
csharp
private bool OpenSerial(string portName, int baudRate)
{
//throw new NotImplementedException();
int attempt = 0;
Exception lastEx= null;
while (attempt < Math.Max(1, ReconnectMaxAttempts))
{
try
{
_serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
{
ReadTimeout = 1000,
WriteTimeout = 1000,
Handshake = Handshake.None
};
_serialPort.Open();
return true;
}
catch(Exception ex)
{
lastEx = ex;
attempt++;
int delay=ReconnectBaseDelayMs*(int)Math.Pow(2, attempt-1);
try { Thread.Sleep(Math.Min(delay, 30000)); } catch { }
}
}
if (lastEx != null) LastError = lastEx.ToString();
return false;
}
智能重试器
csharp
private bool OpenSerial(string portName, int baudRate)
{
int attempt = 0; // 记录重试次数(从0开始)
Exception lastEx= null; // 记录最后一次失败的错误原因
// 循环重试:最多试 ReconnectMaxAttempts 次(最少试1次)
while (attempt < Math.Max(1, ReconnectMaxAttempts))
{
try
{
// 1. 创建串口对象,配置参数(和设备匹配:无校验、8位数据位、1位停止位)
_serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
{
ReadTimeout = 1000, // 读串口超时1秒
WriteTimeout = 1000, // 写串口超时1秒
Handshake = Handshake.None // 无握手协议(串口通用配置)
};
// 2. 打开串口
_serialPort.Open();
// 3. 成功:直接返回true
return true;
}
catch(Exception ex)
{
// 失败:记录错误、重试次数+1
lastEx = ex;
attempt++;
// 核心:指数退避延迟(重试间隔越来越久,避免频繁重试占资源)
// 第1次重试等 ReconnectBaseDelayMs 毫秒,第2次等 2倍,第3次等4倍...
int delay=ReconnectBaseDelayMs*(int)Math.Pow(2, attempt-1);
// 延迟重试(最多等30秒,避免无限等)
try { Thread.Sleep(Math.Min(delay, 30000)); } catch { }
}
}
// 所有重试都失败:记录最后一次错误,返回false
if (lastEx != null) LastError = lastEx.ToString();
return false;
}
心跳检测
csharp
private void StartReading()
{
// 加锁保证线程安全,避免多线程同时启动任务
lock (_lockObj)
{
// 🔴 核心修复1:防重复启动(原逻辑写反,导致任务永远启动不了)
// 已有读取任务在运行 → 直接返回,不重复创建
if (_readTask != null && !_readTask.IsCompleted && !_readTask.IsCanceled && !_readTask.IsFaulted)
{
return;
}
// 🔴 核心修复2:先清理旧令牌/任务,再创建新的(避免残留取消信号)
// 取消旧的令牌(如果有)
_cts?.Cancel();
// 释放旧令牌资源
_cts?.Dispose();
// 创建新的取消令牌(给新任务用)
_cts = new CancellationTokenSource();
// 启动Modbus数据读取循环(后台线程,不卡主线程)
_readTask = Task.Run(() => ReadLoopAsync(_cts.Token));
// 🔴 优化:完善心跳任务启动逻辑(覆盖任务完成/异常场景)
// 开启心跳 + 心跳任务未运行 → 启动心跳循环
if (HeartbeatEnabled && (_heartbeatTask == null || _heartbeatTask.IsCompleted || _heartbeatTask.IsCanceled || _heartbeatTask.IsFaulted))
{
// 先取消旧心跳任务(如果有)
_heartbeatTask?.Wait(50); // 等旧任务收尾(最多50ms)
// 启动新的心跳循环
_heartbeatTask = Task.Run(() => HeartbeatLoopAsync(_cts.Token));
}
}
}
半导体协议:secs
SECS 是半导体设备的 "Modbus",但更复杂、更专业,专门解决半导体产线设备与 MES/CIM 系统的标准化通信问题。
一、核心定义与架构
SECS:Semiconductor Equipment Communication Standard(半导体设备通信标准),由SEMI制定
完整名称:SECS/GEM(Generic Equipment Model,通用设备模型),包含两大核心:
SECS-I/HSMS:传输层(串口 / 以太网),类似 Modbus 的 RTU/TCP
SECS-II:应用层(消息格式),定义数据如何组织
GEM:设备行为规范(状态机、事件报告、远程控制等),类似设备的 "操作手册
一句话:Modbus 是 "遥控器",SECS 是 "智能中控系统",专为半导体高精度、高自动化场景设计。
三、核心概念(面试必背)
SxFy 消息标识:
S=Stream(流号),F=Function(功能号),如S1F1(设备身份请求)、S1F14(身份响应)、S2F1(数据采集)
GEM 状态机:
通信状态:Not Communicating → Communicating → Selected
控制状态:Local → Remote → Online(只有Online Remote才能远程启停、下发配方)
事件报告机制:
设备主动上报关键状态(如报警、加工完成),上位机订阅即可接收,无需轮询