结合VS2008 + .NET3.5 环境、加热台TCP通讯场景,逐段拆解代码、讲清作用、运行流程、调用方式,全程通俗,不讲晦涩术语。
整体前置认知
1 程序整体作用
这是一套 C# TCP 客户端代码 ,专门和 129.168.1.133:9000 加热台通讯:
- 建立网络连接 → 下发指令 → 接收设备返回数据 → 解析数据 → 断开连接
- 封装了设备手册里全部指令:读写功率/电流/电压、IO引脚控制、IIC读写、修改设备IP等
- 兼容老旧 VS2008,全程使用方法重载,不使用新版语法
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Text.RegularExpressions;
namespace VP_Communication.Communication.Heater
{
/// <summary>
/// SFP EVB加热台 TCP 全功能控制类
/// 适配 VS2008 + .NET 3.5
/// 完整覆盖Excel指令表所有功能
/// </summary>
public class SFP_EVB_Heater
{
private readonly object _lock = new object();
private Socket Client;
// 设备配置属性
public string DefaultIP { get; set; }
public int DefaultPort { get; set; }
public int DefaultTimeout { get; set; }
// 连接状态只读属性
public bool IsOpen
{
get
{
return Client != null && Client.Connected;
}
}
// 构造函数:初始化默认参数
public SFP_EVB_Heater()
{
DefaultIP = "129.168.1.133"; // 你的设备真实IP
DefaultPort = 9000;
DefaultTimeout = 5000; // 已调整为5秒,避免超时
Client = null;
}
#region 连接方法重载(适配VS2008,无默认参数)
public bool Open()
{
return Open(DefaultIP, DefaultPort, DefaultTimeout);
}
public bool Open(string ipAddress)
{
return Open(ipAddress, DefaultPort, DefaultTimeout);
}
public bool Open(string ipAddress, int port)
{
return Open(ipAddress, port, DefaultTimeout);
}
#endregion
/// <summary>
/// 核心连接方法
/// </summary>
public bool Open(string ipAddress, int port, int timeOut)
{
if (ipAddress == null)
ipAddress = DefaultIP;
if (port <= 0)
port = DefaultPort;
if (timeOut <= 0)
timeOut = DefaultTimeout;
lock (_lock)
{
if (IsOpen)
{
return true;
}
try
{
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Client.ReceiveTimeout = timeOut;
Client.SendTimeout = timeOut;
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
Client.Connect(endPoint);
Console.WriteLine("设备连接成功");
return true;
}
catch (Exception ex)
{
Console.WriteLine(string.Format("连接失败:{0}", ex.Message));
if (Client != null)
{
Client.Close();
Client = null;
}
return false;
}
}
}
/// <summary>
/// 断开设备连接
/// </summary>
public void Close()
{
lock (_lock)
{
if (IsOpen)
{
Client.Close();
Client = null;
Console.WriteLine("连接已断开");
}
}
}
#region 通用指令收发方法重载
public string SendCommand(string cmd)
{
return SendCommand(cmd, 300); // 已调整为300ms延迟,适配设备响应
}
#endregion
/// <summary>
/// 核心指令收发方法
/// </summary>
public string SendCommand(string cmd, int delay)
{
lock (_lock)
{
if (!IsOpen)
{
return null;
}
try
{
byte[] wbuf = Encoding.UTF8.GetBytes(cmd + "\r\n");
Client.Send(wbuf, SocketFlags.None);
Thread.Sleep(delay);
byte[] rbuf = new byte[1024];
int count = Client.Receive(rbuf, SocketFlags.None);
if (count > 0)
{
string res = Encoding.UTF8.GetString(rbuf, 0, count).Trim();
Console.WriteLine(string.Format("发送指令:{0}\n设备返回:{1}", cmd, res));
return res;
}
}
catch (Exception ex)
{
Console.WriteLine(string.Format("指令异常:{0}", ex.Message));
}
return null;
}
}
#region 一、IIC 读写功能(完整覆盖Excel指令)
#region IIC_Set 重载
public bool IIC_Set(int slot)
{
return IIC_Set(slot, "a0", "0", "5", "a1,9,1b,2c,3d");
}
public bool IIC_Set(int slot, string deviceAddr)
{
return IIC_Set(slot, deviceAddr, "0", "5", "a1,9,1b,2c,3d");
}
public bool IIC_Set(int slot, string deviceAddr, string regAddr)
{
return IIC_Set(slot, deviceAddr, regAddr, "5", "a1,9,1b,2c,3d");
}
public bool IIC_Set(int slot, string deviceAddr, string regAddr, string dataLength)
{
return IIC_Set(slot, deviceAddr, regAddr, dataLength, "a1,9,1b,2c,3d");
}
#endregion
/// <summary>
/// IIC 写入数据
/// </summary>
/// <param name="slot">槽位号</param>
/// <param name="deviceAddr">器件地址</param>
/// <param name="regAddr">寄存器地址</param>
/// <param name="dataLength">数据长度</param>
/// <param name="data">要写入的16进制数据,逗号分隔</param>
/// <returns>true=写入成功</returns>
public bool IIC_Set(int slot, string deviceAddr, string regAddr, string dataLength, string data)
{
string cmd = string.Format("IIC{0}:set {1},{2},{3},{4}", slot, deviceAddr, regAddr, dataLength, data);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains("iic set ok");
}
#region IIC_Get 重载
public string IIC_Get(int slot)
{
return IIC_Get(slot, "a0", "0", "9");
}
public string IIC_Get(int slot, string deviceAddr)
{
return IIC_Get(slot, deviceAddr, "0", "9");
}
public string IIC_Get(int slot, string deviceAddr, string regAddr)
{
return IIC_Get(slot, deviceAddr, regAddr, "9");
}
#endregion
/// <summary>
/// IIC 读取数据
/// </summary>
/// <param name="slot">槽位号</param>
/// <param name="deviceAddr">器件地址</param>
/// <param name="regAddr">寄存器地址</param>
/// <param name="dataLength">要读取的数据长度</param>
/// <returns>设备返回的完整数据</returns>
public string IIC_Get(int slot, string deviceAddr, string regAddr, string dataLength)
{
string cmd = string.Format("IIC{0}:get {1},{2},{3}", slot, deviceAddr, regAddr, dataLength);
return SendCommand(cmd);
}
#endregion
#region 二、EVB 功率/电流/电压 读写功能(完整覆盖Excel指令)
#region GetPower 重载
public string GetPower()
{
return GetPower(1);
}
#endregion
/// <summary>
/// 查询槽位功率
/// </summary>
/// <param name="slot">槽位号</param>
/// <returns>功率数值(单位mW)</returns>
public string GetPower(int slot)
{
string cmd = string.Format("evb{0}:getpower?", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res))
return null;
string numberOnly = Regex.Replace(res, @"[^0-9]", "");
return !string.IsNullOrEmpty(numberOnly) ? numberOnly : null;
}
#region GetCurrent 重载
public string GetCurrent()
{
return GetCurrent(1);
}
#endregion
/// <summary>
/// 查询槽位电流
/// </summary>
/// <param name="slot">槽位号</param>
/// <returns>电流数值(单位uA)</returns>
public string GetCurrent(int slot)
{
string cmd = string.Format("evb{0}:getcurrent?", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res))
return null;
string numberOnly = Regex.Replace(res, @"[^0-9]", "");
return !string.IsNullOrEmpty(numberOnly) ? numberOnly : null;
}
#region GetVoltage 重载
public string GetVoltage()
{
return GetVoltage(1);
}
#endregion
/// <summary>
/// 查询槽位电压
/// </summary>
/// <param name="slot">槽位号</param>
/// <returns>电压数值(单位mV)</returns>
public string GetVoltage(int slot)
{
string cmd = string.Format("evb{0}:getvoltage?", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res))
return null;
string numberOnly = Regex.Replace(res, @"[^0-9]", "");
return !string.IsNullOrEmpty(numberOnly) ? numberOnly : null;
}
#region SetVoltage 重载
public bool SetVoltage(int slot)
{
return SetVoltage(slot, 3.3);
}
#endregion
/// <summary>
/// 设置槽位电压(范围3.15~3.3V)
/// </summary>
/// <param name="slot">槽位号</param>
/// <param name="voltage">电压值(单位V,如3.3)</param>
/// <returns>true=设置成功</returns>
public bool SetVoltage(int slot, double voltage)
{
string cmd = string.Format("evb{0}:setvoltage {1}", slot, voltage);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains("set Voltage ok!");
}
#endregion
#region 三、IO 引脚控制功能(完整覆盖Excel指令)
#region 1. PowerEN 模块使能引脚
#region SetPowerEN 重载
public bool SetPowerEN(int slot)
{
return SetPowerEN(slot, 1);
}
#endregion
/// <summary>
/// 设置模块使能引脚
/// </summary>
/// <param name="slot">槽位号</param>
/// <param name="state">1=高电平使能,0=低电平禁用</param>
/// <returns>true=设置成功</returns>
public bool SetPowerEN(int slot, int state)
{
string cmd = string.Format("IO{0}:setPowerEN {1}", slot, state);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains(string.Format("POWER_EN:{0}", state));
}
#region GetPowerEN 重载
public string GetPowerEN()
{
return GetPowerEN(1);
}
#endregion
/// <summary>
/// 查询模块使能引脚状态
/// </summary>
/// <param name="slot">槽位号</param>
/// <returns>1=高电平,0=低电平</returns>
public string GetPowerEN(int slot)
{
string cmd = string.Format("IO{0}:getPowerEN", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res) || !res.Contains("POWER_EN:"))
return null;
return res.Split(':')[1].Trim();
}
#endregion
#region 2. TxDis 引脚
#region SetTxDis 重载
public bool SetTxDis(int slot)
{
return SetTxDis(slot, 0);
}
#endregion
/// <summary>
/// 设置TX_DIS引脚
/// </summary>
/// <param name="slot">槽位号</param>
/// <param name="state">1=高电平,0=低电平</param>
/// <returns>true=设置成功</returns>
public bool SetTxDis(int slot, int state)
{
string cmd = string.Format("IO{0}:setTxDis {1}", slot, state);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains(string.Format("TX_DIS:{0}", state));
}
#region GetTxDis 重载
public string GetTxDis()
{
return GetTxDis(1);
}
#endregion
/// <summary>
/// 查询TX_DIS引脚状态
/// </summary>
/// <param name="slot">槽位号</param>
/// <returns>1=高电平,0=低电平</returns>
public string GetTxDis(int slot)
{
string cmd = string.Format("IO{0}:getTxDis", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res) || !res.Contains("TX_DIS:"))
return null;
return res.Split(':')[1].Trim();
}
#endregion
#region 3. Rs0High 引脚
#region SetRs0High 重载
public bool SetRs0High(int slot)
{
return SetRs0High(slot, 1);
}
#endregion
/// <summary>
/// 设置RS0_HIGH引脚
/// </summary>
/// <param name="slot">槽位号</param>
/// <param name="state">1=高电平,0=低电平</param>
/// <returns>true=设置成功</returns>
public bool SetRs0High(int slot, int state)
{
string cmd = string.Format("IO{0}:setRs0High {1}", slot, state);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains(string.Format("RS0_HIGH:{0}", state));
}
#region GetRs0High 重载
public string GetRs0High()
{
return GetRs0High(1);
}
#endregion
/// <summary>
/// 查询RS0_HIGH引脚状态
/// </summary>
/// <param name="slot">槽位号</param>
/// <returns>1=高电平,0=低电平</returns>
public string GetRs0High(int slot)
{
string cmd = string.Format("IO{0}:getRs0High", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res) || !res.Contains("RS0_HIGH:"))
return null;
return res.Split(':')[1].Trim();
}
#endregion
#region 4. Rs1High 引脚
#region SetRs1High 重载
public bool SetRs1High(int slot)
{
return SetRs1High(slot, 1);
}
#endregion
/// <summary>
/// 设置RS1_HIGH引脚
/// </summary>
/// <param name="slot">槽位号</param>
/// <param name="state">1=高电平,0=低电平</param>
/// <returns>true=设置成功</returns>
public bool SetRs1High(int slot, int state)
{
string cmd = string.Format("IO{0}:setRs1High {1}", slot, state);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains(string.Format("RS1_HIGH:{0}", state));
}
#region GetRs1High 重载
public string GetRs1High()
{
return GetRs1High(1);
}
#endregion
/// <summary>
/// 查询RS1_HIGH引脚状态
/// </summary>
/// <param name="slot">槽位号</param>
/// <returns>1=高电平,0=低电平</returns>
public string GetRs1High(int slot)
{
string cmd = string.Format("IO{0}:getRs1High", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res) || !res.Contains("RS1_HIGH:"))
return null;
return res.Split(':')[1].Trim();
}
#endregion
#region 5. 其他只读引脚查询
#region GetTxFalu 重载
public string GetTxFalu()
{
return GetTxFalu(1);
}
#endregion
/// <summary>
/// 查询TX_FALU引脚状态
/// </summary>
/// <param name="slot">槽位号</param>
/// <returns>1=高电平,0=低电平</returns>
public string GetTxFalu(int slot)
{
string cmd = string.Format("IO{0}:getTxFalu", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res) || !res.Contains("TX_FALU:"))
return null;
return res.Split(':')[1].Trim();
}
#region GetRxLos 重载
public string GetRxLos()
{
return GetRxLos(1);
}
#endregion
/// <summary>
/// 查询RX_LOS引脚状态
/// </summary>
/// <param name="slot">槽位号</param>
/// <returns>1=高电平,0=低电平</returns>
public string GetRxLos(int slot)
{
string cmd = string.Format("IO{0}:getRxLos", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res) || !res.Contains("RX_LOS:"))
return null;
return res.Split(':')[1].Trim();
}
#region GetABS 重载
public string GetABS()
{
return GetABS(1);
}
#endregion
/// <summary>
/// 查询ABS引脚状态
/// </summary>
/// <param name="slot">槽位号</param>
/// <returns>1=高电平,0=低电平</returns>
public string GetABS(int slot)
{
string cmd = string.Format("IO{0}:getABS", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res) || !res.Contains("ABS:"))
return null;
return res.Split(':')[1].Trim();
}
#endregion
#endregion
#endregion
#region 四、系统功能(完整覆盖Excel指令)
/// <summary>
/// 设置设备IP地址
/// </summary>
/// <param name="newIp">新的IP地址,格式129.168.1.xxx</param>
/// <returns>true=设置成功</returns>
public bool SetDeviceIP(string newIp)
{
string cmd = string.Format("setip {0}", newIp);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains(string.Format("setip:{0}", newIp));
}
/// <summary>
/// 查询设备支持的所有命令
/// </summary>
/// <returns>设备返回的帮助信息</returns>
public string GetHelp()
{
return SendCommand("help");
}
#endregion
}
/// <summary>
/// 测试入口:完整功能演示
/// </summary>
class Program
{
static void Main(string[] args)
{
SFP_EVB_Heater heater = new SFP_EVB_Heater();
// 1. 连接设备
Console.WriteLine("===== 正在连接设备 =====");
if (!heater.Open())
{
Console.WriteLine("按任意键退出...");
Console.ReadKey();
return;
}
// 2. 基础信息查询演示
Console.WriteLine("\n===== 基础信息查询 =====");
string power = heater.GetPower();
string current = heater.GetCurrent();
string voltage = heater.GetVoltage();
if (!string.IsNullOrEmpty(power))
Console.WriteLine("当前功率:" + power + " mW");
if (!string.IsNullOrEmpty(current))
Console.WriteLine("当前电流:" + current + " uA");
if (!string.IsNullOrEmpty(voltage))
Console.WriteLine("当前电压:" + voltage + " mV");
// 3. 引脚状态查询演示
Console.WriteLine("\n===== 引脚状态查询 =====");
string powerEN = heater.GetPowerEN();
string txDis = heater.GetTxDis();
string rs0High = heater.GetRs0High();
if (!string.IsNullOrEmpty(powerEN))
Console.WriteLine("模块使能状态:" + powerEN);
if (!string.IsNullOrEmpty(txDis))
Console.WriteLine("TX_DIS状态:" + txDis);
if (!string.IsNullOrEmpty(rs0High))
Console.WriteLine("RS0_HIGH状态:" + rs0High);
// 4. 断开连接
Console.WriteLine("\n===== 测试完成 =====");
heater.Close();
Console.WriteLine("\n按任意键关闭窗口...");
Console.ReadKey();
}
}
}
2 整体运行顺序(先记大流程)
程序启动 → 进入Main入口
- 创建加热台控制对象
- 发起TCP连接
- 依次查询功率、电流、电压
- 依次查询多个硬件引脚状态
- 断开TCP连接
- 窗口停留,按按键退出
第一部分:头部引用区
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Text.RegularExpressions;
using:引入系统自带工具包,相当于"提前拿好要用的工具"
System:基础功能(控制台打印、异常报错、字符串处理)System.Net / System.Net.Sockets:TCP网络核心 ,提供Socket网络管道System.Text:字符串 ↔ 网络字节 互相转换(网络只能传二进制)System.Threading:线程功能,这里只用Thread.Sleep(暂停等待)System.Text.RegularExpressions:正则表达式,用来提取数字(解析功率/电流)
第二部分:命名空间
csharp
namespace VP_Communication.Communication.Heater
{
// 所有代码都写在这个大括号内
}
namespace:代码分类文件夹- 作用:把加热台相关代码统一归类,和你公司现有项目结构保持一致,避免代码重名。
第三部分:核心类 SFP_EVB_Heater
csharp
public class SFP_EVB_Heater
{
// 类内部所有成员
}
class:定义一个功能模板,你可以理解为「加热台遥控器」- 里面封装了:连接、发指令、解析数据、硬件控制所有功能
3.1 私有锁 & 网络管道
csharp
private readonly object _lock = new object();
private Socket Client;
private readonly object _lockprivate:仅当前类内部能用,外部访问不到_lock:程序锁,防止多个操作同时抢网络通道,造成通讯错乱- 小白:固定写法,不用修改,保留即可
private Socket ClientSocket:电脑和加热台之间的虚拟网线(网络管道)Client为空 = 网线未接通;有值 = 网线已创建
3.2 设备配置属性
csharp
public string DefaultIP { get; set; }
public int DefaultPort { get; set; }
public int DefaultTimeout { get; set; }
public:外部代码可以读取/修改- 三个属性用来存设备固定参数:
DefaultIP:设备IP地址(文本格式)DefaultPort:设备通讯端口(数字)DefaultTimeout:超时时间(单位:毫秒)
{ get; set; }:可读可写;VS2008 不支持直接赋值,统一在构造函数初始化
3.3 连接状态(只读属性)
csharp
public bool IsOpen
{
get
{
return Client != null && Client.Connected;
}
}
bool:布尔类型,只有true(是)/false(否)- 只有
get:只读,只能查询状态,不能手动修改 - 逻辑拆解:
Client != null:虚拟网线已创建Client.Connected:网线真正连上设备&&并且:两个条件同时成立 →true(已连接),否则未连接
3.4 构造函数(初始化参数)
csharp
public SFP_EVB_Heater()
{
DefaultIP = "129.168.1.133";
DefaultPort = 9000;
DefaultTimeout = 5000;
Client = null;
}
- 构造函数规则:和类名同名、没有返回值
- 执行时机 :代码
new SFP_EVB_Heater()创建遥控器时,自动第一时间执行 - 作用:给设备参数赋默认值
- IP = 加热台真实地址
- 端口 = 9000(设备固定端口)
- 超时 = 5000毫秒(5秒),5秒连不上/收不到数据就判定失败
Client = null:初始状态,虚拟网线为空
3.5 连接方法组(Open 重载)
知识点:VS2008 不支持「方法默认参数」,所以用方法重载(同名、不同参数),实现灵活调用。
csharp
#region 连接方法重载
public bool Open()
{
return Open(DefaultIP, DefaultPort, DefaultTimeout);
}
public bool Open(string ipAddress)
{
return Open(ipAddress, DefaultPort, DefaultTimeout);
}
public bool Open(string ipAddress, int port)
{
return Open(ipAddress, port, DefaultTimeout);
}
#endregion
#region / #endregion:代码折叠标记,只影响编辑器显示,不运行代码- 三个简易
Open方法,最终都会调用下面完整版Open :Open()不传参 → 使用默认IP/端口/超时Open("129.168.1.133")只传IP → 其余用默认Open("129.168.1.133",9000)传IP+端口 → 超时用默认
完整版 Open(核心连接逻辑)
csharp
public bool Open(string ipAddress, int port, int timeOut)
{
// 非法参数兜底
if (ipAddress == null)
ipAddress = DefaultIP;
if (port <= 0)
port = DefaultPort;
if (timeOut <= 0)
timeOut = DefaultTimeout;
lock (_lock) // 上锁,防止并发冲突
{
if (IsOpen) // 已经连接,直接返回成功
{
return true;
}
try // 尝试执行连接逻辑
{
// 创建TCP虚拟网线
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 设置收发超时
Client.ReceiveTimeout = timeOut;
Client.SendTimeout = timeOut;
// 把IP+端口打包成设备地址
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
// 真正发起网络连接
Client.Connect(endPoint);
Console.WriteLine("设备连接成功");
return true; // 连接成功返回true
}
catch (Exception ex) // 连接出错(断网/关机/IP错误)
{
Console.WriteLine(string.Format("连接失败:{0}", ex.Message));
if (Client != null)
{
Client.Close(); // 关闭无效网线
Client = null;
}
return false; // 连接失败返回false
}
}
}
逐句作用:
- 参数兜底:调用方传空/错误值,自动改用默认配置
lock:上锁,同一时间只允许一次连接操作if (IsOpen):避免重复连接try-catch:异常捕获,连接报错不会直接崩溃程序new Socket(...):固定写法,创建TCP网络管道IPEndPoint:拼接设备完整网络地址Client.Connect():核心联网动作- 成功/失败分别打印提示、返回对应结果
3.6 Close 断开连接
csharp
public void Close()
{
lock (_lock)
{
if (IsOpen) // 仅已连接时才执行断开
{
Client.Close(); // 关闭网络管道
Client = null; // 清空对象,释放资源
Console.WriteLine("连接已断开");
}
}
}
void:方法无返回值- 作用:用完设备,主动断开TCP连接,释放网络资源
3.7 通用指令发送(SendCommand 重载+核心)
所有和设备收发数据,全部走这个方法,是TCP通讯核心。
csharp
#region 通用指令收发方法重载
public string SendCommand(string cmd)
{
return SendCommand(cmd, 300); // 默认等待300毫秒
}
#endregion
- 只传指令、不传延迟,默认等待300ms(给设备处理时间)
完整版 SendCommand
csharp
public string SendCommand(string cmd, int delay)
{
lock (_lock)
{
if (!IsOpen) // 未连接设备,直接返回空
{
return null;
}
try
{
// 1. 字符串指令 → 转网络字节(网络只能传字节)
byte[] wbuf = Encoding.UTF8.GetBytes(cmd + "\r\n");
// 2. 发送字节数据到设备
Client.Send(wbuf, SocketFlags.None);
// 3. 程序暂停,等待设备响应
Thread.Sleep(delay);
// 4. 开辟内存空间,接收设备返回数据
byte[] rbuf = new byte[1024];
// 5. 接收设备传回的字节
int count = Client.Receive(rbuf, SocketFlags.None);
if (count > 0) // 收到有效数据
{
// 字节 → 转回可读字符串
string res = Encoding.UTF8.GetString(rbuf, 0, count).Trim();
Console.WriteLine(string.Format("发送指令:{0}\n设备返回:{1}", cmd, res));
return res; // 返回设备原始数据
}
}
catch (Exception ex)
{
Console.WriteLine(string.Format("指令异常:{0}", ex.Message));
}
return null; // 收发失败返回空
}
}
TCP固定四步(记下来,所有网络设备通用)
- 文字指令 → 转二进制字节
- 通过Socket发送字节
- 等待设备响应
- 接收字节 → 转回文字
补充:cmd + "\r\n" 是设备协议要求,指令末尾必须加换行,设备才能识别。
第四部分:业务功能方法(对应设备指令手册)
下面所有方法,都是基于上面 SendCommand 封装的业务功能,调用即可实现对应操作。
4.1 IIC 读写功能
IIC_Set 写入数据
csharp
#region IIC_Set 重载
public bool IIC_Set(int slot)
{
return IIC_Set(slot, "a0", "0", "5", "a1,9,1b,2c,3d");
}
public bool IIC_Set(int slot, string deviceAddr)
{
return IIC_Set(slot, deviceAddr, "0", "5", "a1,9,1b,2c,3d");
}
public bool IIC_Set(int slot, string deviceAddr, string regAddr)
{
return IIC_Set(slot, deviceAddr, regAddr, "5", "a1,9,1b,2c,3d");
}
#endregion
public bool IIC_Set(int slot, string deviceAddr, string regAddr, string dataLength, string data)
{
string cmd = string.Format("IIC{0}:set {1},{2},{3},{4}", slot, deviceAddr, regAddr, dataLength, data);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains("iic set ok");
}
slot:槽位号(1/2/3...)- 功能:向设备IIC总线写入数据
- 返回
true= 写入成功;false= 失败
IIC_Get 读取数据
csharp
#region IIC_Get 重载
public string IIC_Get(int slot)
{
return IIC_Get(slot, "a0", "0", "9");
}
public string IIC_Get(int slot, string deviceAddr)
{
return IIC_Get(slot, deviceAddr, "0", "9");
}
#endregion
public string IIC_Get(int slot, string deviceAddr, string regAddr, string dataLength)
{
string cmd = string.Format("IIC{0}:get {1},{2},{3}", slot, deviceAddr, regAddr, dataLength);
return SendCommand(cmd);
}
- 功能:读取设备IIC总线数据
- 返回值:设备返回的原始字符串
4.2 功率/电流/电压 读写(最常用)
GetPower 查询功率
csharp
#region GetPower 重载
public string GetPower()
{
return GetPower(1); // 默认1号槽位
}
#endregion
public string GetPower(int slot)
{
string cmd = string.Format("evb{0}:getpower?", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res))
return null;
// 正则:只保留数字,过滤文字、单位
string numberOnly = Regex.Replace(res, @"[^0-9]", "");
return !string.IsNullOrEmpty(numberOnly) ? numberOnly : null;
}
- 调用示例:
heater.GetPower()→ 查询1号槽功率 - 解析逻辑:自动剔除
Power : 42mW里的文字,只返回数字
GetCurrent 查询电流
逻辑和GetPower完全一致,只是指令不同:
csharp
public string GetCurrent(int slot)
{
string cmd = string.Format("evb{0}:getcurrent?", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res))
return null;
string numberOnly = Regex.Replace(res, @"[^0-9]", "");
return !string.IsNullOrEmpty(numberOnly) ? numberOnly : null;
}
GetVoltage 查询电压
同理:
csharp
public string GetVoltage(int slot)
{
string cmd = string.Format("evb{0}:getvoltage?", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res))
return null;
string numberOnly = Regex.Replace(res, @"[^0-9]", "");
return !string.IsNullOrEmpty(numberOnly) ? numberOnly : null;
}
SetVoltage 设置电压
csharp
public bool SetVoltage(int slot, double voltage)
{
string cmd = string.Format("evb{0}:setvoltage {1}", slot, voltage);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains("set Voltage ok!");
}
- 参数:
slot槽位,voltage电压值(范围3.15~3.3V) - 示例:
heater.SetVoltage(1,3.3)设置1号槽电压3.3V - 返回
true= 设置成功
4.3 IO 引脚控制(所有引脚统一规则)
以 SetPowerEN / GetPowerEN 举例,其余TxDis、Rs0High、Rs1High逻辑完全一样:
SetXXX:设置引脚状态(1高电平 / 0低电平),返回bool判断是否成功GetXXX:查询引脚状态,返回0/1字符串
示例:
csharp
// 设置模块使能 1号槽 高电平
public bool SetPowerEN(int slot, int state)
{
string cmd = string.Format("IO{0}:setPowerEN {1}", slot, state);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains(string.Format("POWER_EN:{0}", state));
}
// 查询模块使能状态
public string GetPowerEN(int slot)
{
string cmd = string.Format("IO{0}:getPowerEN", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res) || !res.Contains("POWER_EN:"))
return null;
return res.Split(':')[1].Trim(); // 按冒号分割,取出状态值
}
其余引脚(TxDis、Rs0High、Rs1High、TxFalu、RxLos、ABS)写法一模一样,只是指令名称不同。
4.4 系统功能
csharp
// 修改设备IP
public bool SetDeviceIP(string newIp)
{
string cmd = string.Format("setip {0}", newIp);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains(string.Format("setip:{0}", newIp));
}
// 查询设备所有支持指令
public string GetHelp()
{
return SendCommand("help");
}
第五部分:程序入口 Program + Main
整个程序的启动入口,程序运行从这里开始。
csharp
class Program
{
static void Main(string[] args)
{
// 1. 创建遥控器对象,自动执行构造函数初始化IP/端口
SFP_EVB_Heater heater = new SFP_EVB_Heater();
Console.WriteLine("===== 正在连接设备 =====");
// 2. 连接设备,连接失败直接退出
if (!heater.Open())
{
Console.WriteLine("按任意键退出...");
Console.ReadKey();
return;
}
// 3. 批量查询基础参数:功率、电流、电压
Console.WriteLine("\n===== 基础信息查询 =====");
string power = heater.GetPower();
string current = heater.GetCurrent();
string voltage = heater.GetVoltage();
// 打印结果
if (!string.IsNullOrEmpty(power))
Console.WriteLine("当前功率:" + power + " mW");
if (!string.IsNullOrEmpty(current))
Console.WriteLine("当前电流:" + current + " uA");
if (!string.IsNullOrEmpty(voltage))
Console.WriteLine("当前电压:" + voltage + " mV");
// 4. 批量查询引脚状态
Console.WriteLine("\n===== 引脚状态查询 =====");
string powerEN = heater.GetPowerEN();
string txDis = heater.GetTxDis();
string rs0High = heater.GetRs0High();
if (!string.IsNullOrEmpty(powerEN))
Console.WriteLine("模块使能状态:" + powerEN);
if (!string.IsNullOrEmpty(txDis))
Console.WriteLine("TX_DIS状态:" + txDis);
if (!string.IsNullOrEmpty(rs0High))
Console.WriteLine("RS0_HIGH状态:" + rs0High);
// 5. 断开连接
Console.WriteLine("\n===== 测试完成 =====");
heater.Close();
// 暂停窗口,防止一闪而过
Console.WriteLine("\n按任意键关闭窗口...");
Console.ReadKey();
}
}
Main 逐行运行流程(最终执行链路)
new SFP_EVB_Heater()→ 创建对象 + 自动初始化IP/端口/超时heater.Open()→ 发起TCP连接- 依次调用
GetPower/GetCurrent/GetVoltage→ 发指令、收数据、解析、打印 - 依次调用引脚查询方法 → 读取硬件状态
heater.Close()→ 断开网络Console.ReadKey()→ 窗口停留,按按键关闭
补充:小白常用调用示例(直接写在Main里就能用)
- 设置1号槽电压3.2V
csharp
bool ok = heater.SetVoltage(1, 3.2);
- 查询2号槽功率
csharp
string p = heater.GetPower(2);
- 关闭1号槽TX引脚
csharp
heater.SetTxDis(1,0);
核心总结(必记2点)
- TCP通讯固定套路(所有网口设备通用)
创建Socket → 连接IP+端口 → 文字转字节发送 → 接收字节转回文字 - 代码分层思想
SFP_EVB_Heater类:只管和硬件通讯、封装功能(底层能力)Main方法:只管调用功能、打印结果(上层测试逻辑)- 新增功能只需要仿照现有方法仿写即可。
下面逐行、逐语句、逐关键字 超详细讲解,包含语法含义、参数作用、执行结果、设计原因,重点对你举例的 string.Format 做拆解,全程贴合 VS2008 + .NET3.5 环境,小白也能完全看懂。
一、头部引用区
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Text.RegularExpressions;
using
语法含义:引入外部类库 。C# 很多现成功能不是自己写的,而是系统提供,using就是告诉编译器:我要使用下面这些工具库。using System;
引入最基础核心库:控制台打印Console、异常Exception、基础数据类型(字符串、数字)都在这里。using System.Net;
网络基础库:专门用来解析IP、组装IP+端口地址。using System.Net.Sockets;
TCP通讯核心库 :Socket(网络管道)就在这个库,没有它无法和加热台网络通信。using System.Text;
编码转换库:电脑发文字,网络只传二进制,这个库负责字符串 ↔ 字节数组互转。using System.Threading;
线程库:本代码只用到Thread.Sleep()(让程序暂停等待)。using System.Text.RegularExpressions;
正则表达式库:用来从设备返回的文本里单独提取数字 (比如从Power:42mW取出42)。
二、命名空间
csharp
namespace VP_Communication.Communication.Heater
{
namespace:命名空间,作用 = 虚拟文件夹VP_Communication.Communication.Heater:多级文件夹名称- 设计原因:车间项目代码多,把所有加热台相关代码统一归类,防止不同功能代码重名。
{:代码范围开始,大括号内所有内容都属于这个"文件夹"。
三、主类定义
csharp
/// <summary>
/// SFP EVB加热台 TCP 全功能控制类
/// 适配 VS2008 + .NET 3.5
/// 完整覆盖Excel指令表所有功能
/// </summary>
public class SFP_EVB_Heater
{
/// 注释:文档注释,只给人看,程序运行忽略,用来标注类的用途。public:访问修饰符 = 公开 ,外部代码(比如Main方法)可以访问这个类、创建对象。class:关键字,用来定义一个类(可以理解为功能模板/遥控器图纸)。SFP_EVB_Heater:类名,自定义名称,见名知意:加热台控制类。{:类的内容开始。
3.1 线程锁 & 网络管道变量
csharp
private readonly object _lock = new object();
private Socket Client;
第一行 private readonly object _lock = new object();
private:私有,仅当前类内部可以使用,外部代码访问不到。readonly:只读,变量一旦赋值,整个程序运行过程中不能再修改。object:C# 所有数据的根类型,这里专门用来做「锁对象」。_lock:自定义变量名,行业习惯用下划线开头表示内部变量。new object():在内存中创建一个全新的对象。- 整体作用:
防止多个指令同时抢占网络通道,导致通讯错乱。老旧设备/多指令场景标配写法,直接保留即可。
第二行 private Socket Client;
private:私有,类内部专用。Socket:网络管道类型,电脑和加热台之间的虚拟网线。Client:变量名,代表当前这根网线。- 含义:定义一根空网线,初始状态没有连接任何设备。
3.2 设备配置属性
csharp
public string DefaultIP { get; set; }
public int DefaultPort { get; set; }
public int DefaultTimeout { get; set; }
public:公开,外部可读可改。string:字符串类型,用来存放文本(IP地址是文本格式)。int:整数类型,存放纯数字(端口、超时都是数字)。DefaultIP:默认IP地址;DefaultPort:默认端口;DefaultTimeout:默认超时时间。{ get; set; }get:读取这个属性的值;set:给这个属性赋值;- 组合含义:可读、可写属性。
- 设计原因:
VS2008/.NET3.5 不支持public string DefaultIP = "xxx"这种直接赋值写法,所以先定义属性,统一在构造函数里赋值。
3.3 连接状态 只读属性
csharp
public bool IsOpen
{
get
{
return Client != null && Client.Connected;
}
}
bool:布尔类型,只有两个值:true(真)、false(假)。IsOpen:属性名,含义:是否已和设备建立连接。{ get { } }:只有get、没有set→ 只读属性
只能查询状态,代码不能强行修改连接状态。return:把后面计算的结果返回出去。Client != null:判断网络管道(网线)是否已经创建。&&:逻辑运算符 并且,左右两个条件必须同时成立。Client.Connected:Socket 自带属性,判断网线物理上是否连上设备。- 整句逻辑:
网线已创建 并且 网线已接通设备 → 返回true(已连接),否则false(未连接)。
3.4 构造函数(对象初始化)
csharp
public SFP_EVB_Heater()
{
DefaultIP = "129.168.1.133";
DefaultPort = 9000;
DefaultTimeout = 5000;
Client = null;
}
SFP_EVB_Heater():构造函数- 规则:函数名和类名完全一致 ,没有返回值;
- 执行时机:代码执行
new SFP_EVB_Heater()创建遥控器对象时,自动优先执行。
DefaultIP = "129.168.1.133":给IP属性赋值,加热台固定IP。DefaultPort = 9000:赋值通讯端口,设备固定端口。DefaultTimeout = 5000:超时时间,单位 毫秒 ,5000ms = 5秒。
含义:5秒内连不上设备、收不到数据,直接判定通讯失败。Client = null:初始化网络管道为空,代表初始状态网线未创建、未连接。
3.5 Open 方法重载(适配VS2008,无默认参数)
csharp
#region 连接方法重载(适配VS2008,无默认参数)
public bool Open()
{
return Open(DefaultIP, DefaultPort, DefaultTimeout);
}
public bool Open(string ipAddress)
{
return Open(ipAddress, DefaultPort, DefaultTimeout);
}
public bool Open(string ipAddress, int port)
{
return Open(ipAddress, port, DefaultTimeout);
}
#endregion
#region 名称/#endregion:代码折叠标记
仅影响 VS 编辑器显示(可以收起/展开代码),不改变程序运行逻辑。- 前置知识点:
VS2008 不支持Open(string ip="1.1.1.1")这种「方法默认参数」,所以用 方法重载 实现灵活调用。 public bool Open()- 公开方法,返回
bool布尔值(连接成功/失败); - 无参数版本:调用时不填任何内容。
- 公开方法,返回
return Open(DefaultIP, DefaultPort, DefaultTimeout);return:把后面方法的执行结果,当作当前方法的结果返回;- 含义:调用无参
Open时,自动使用默认IP、端口、超时 ,去调用完整版三参数Open。
public bool Open(string ipAddress)
只传入IP,端口、超时沿用默认值。public bool Open(string ipAddress, int port)
传入IP+端口,超时沿用默认值。- 设计目的:
调用灵活,想用默认参数就不传,想临时改IP/端口就单独传,不用改源代码。
3.6 完整版 Open 核心连接方法
csharp
public bool Open(string ipAddress, int port, int timeOut)
{
if (ipAddress == null)
ipAddress = DefaultIP;
if (port <= 0)
port = DefaultPort;
if (timeOut <= 0)
timeOut = DefaultTimeout;
public bool Open(参数1,参数2,参数3):完整版连接方法,接收3个外部传入参数。if (ipAddress == null):判断如果传入的IP是空值。ipAddress = DefaultIP:空值就替换成默认IP,参数容错,防止代码报错。if (port <= 0):端口不能是0或负数,非法值替换为默认端口。if (timeOut <= 0):超时非法,替换为默认超时。
csharp
lock (_lock)
{
lock (_lock):上锁。- 作用:同一时间只允许一段连接代码执行,防止多操作争抢网络。
csharp
if (IsOpen)
{
return true;
}
if (IsOpen):判断当前已经连上设备。return true:已经连接,直接返回成功,避免重复连接。
csharp
try
{
try:异常捕获块。- 含义:尝试执行大括号内的连接代码;一旦中间出错(网线断、设备关机、IP错误),自动跳转到
catch。
csharp
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Client.ReceiveTimeout = timeOut;
Client.SendTimeout = timeOut;
Client = new Socket(...):创建一根TCP虚拟网线。- 三个固定参数(TCP标准写法,不用修改):
AddressFamily.InterNetwork:使用 IPv4 地址(我们现在用的地址格式);SocketType.Stream:流式传输,TCP 专属特征;ProtocolType.Tcp:指定使用 TCP 协议。
Client.ReceiveTimeout = timeOut:设置接收数据超时时间。Client.SendTimeout = timeOut:设置发送数据超时时间。
csharp
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
IPAddress.Parse(ipAddress):把文本格式的IP (如129.168.1.133)转换成程序能识别的IP对象。IPEndPoint:组合类型,作用是 把 IP + 端口 打包成一个完整的设备网络地址。endPoint:自定义变量,存放打包后的设备地址。
csharp
Client.Connect(endPoint);
Client.Connect():核心联网动作,让虚拟网线和加热台设备建立TCP连接。
csharp
Console.WriteLine("设备连接成功");
return true;
- 连接无异常,控制台打印提示文字。
return true:方法返回true,代表连接成功。
csharp
}
catch (Exception ex)
{
catch (Exception ex):捕获try块中所有类型的错误。ex:错误信息对象,里面存放具体报错内容。
csharp
Console.WriteLine(string.Format("连接失败:{0}", ex.Message));
ex.Message:取出错误的文字描述。string.Format("连接失败:{0}", ex.Message):字符串格式化{0}:占位符,代表第一个参数的内容;- 执行结果:把
ex.Message替换{0},拼接成完整提示文本。
csharp
if (Client != null)
{
Client.Close();
Client = null;
}
return false;
}
}
}
- 如果网线已经创建,就执行
Client.Close()关闭网线。 Client = null:清空变量,释放内存资源。return false:返回false,代表连接失败。
3.7 Close 断开连接方法
csharp
public void Close()
{
lock (_lock)
{
if (IsOpen)
{
Client.Close();
Client = null;
Console.WriteLine("连接已断开");
}
}
}
public void Close():公开方法,void= 无返回值,功能:断开TCP连接。lock (_lock):上锁,保证操作安全。if (IsOpen):只有当前处于连接状态,才执行断开逻辑。Client.Close():关闭网络管道。Client = null:清空网线对象,释放资源。
3.8 SendCommand 重载(指令收发)
csharp
#region 通用指令收发方法重载
public string SendCommand(string cmd)
{
return SendCommand(cmd, 300);
}
#endregion
public string Send(string cmd):只接收指令文本,无延迟参数。return SendCommand(cmd, 300):调用完整版方法,默认延迟300毫秒(给设备反应时间)。
完整版 SendCommand(TCP收发核心)
csharp
public string SendCommand(string cmd, int delay)
{
lock (_lock)
{
if (!IsOpen)
{
return null;
}
- 入口参数:
cmd=要发送的指令文本;delay=等待延迟(毫秒)。 !IsOpen:!取反 = 未连接设备。return null:未连接直接返回空,不执行收发。
csharp
try
{
byte[] wbuf = Encoding.UTF8.GetBytes(cmd + "\r\n");
byte[]:字节数组,网络传输唯一标准格式。Encoding.UTF8.GetBytes():把字符串指令转为二进制字节。cmd + "\r\n":拼接换行符。
原因:设备协议强制要求,指令末尾必须加回车换行,设备才能识别指令。
csharp
Client.Send(wbuf, SocketFlags.None);
Client.Send():通过网络管道,把字节数据发送给加热台。SocketFlags.None:固定默认参数,无特殊发送规则。
csharp
Thread.Sleep(delay);
- 让程序暂停
delay毫秒,等待设备处理指令、准备返回数据。
csharp
byte[] rbuf = new byte[1024];
int count = Client.Receive(rbuf, SocketFlags.None);
byte[] rbuf = new byte[1024]:开辟一块1024字节的内存,作为接收缓冲区,存放设备回传数据。Client.Receive():接收设备发回的字节数据。count:整数,代表实际收到了多少个字节。
csharp
if (count > 0)
{
string res = Encoding.UTF8.GetString(rbuf, 0, count).Trim();
Console.WriteLine(string.Format("发送指令:{0}\n设备返回:{1}", cmd, res));
return res;
count > 0:判断收到了有效数据。Encoding.UTF8.GetString(rbuf, 0, count):把收到的字节 转回可读字符串 。0:从数组第0位开始读取;count:一共读取收到的有效字节数量。
.Trim():去掉字符串首尾的空格、换行。string.Format:格式化拼接字符串,打印收发日志。return res:把设备原始返回文本返回给调用方。
csharp
}
}
catch (Exception ex)
{
Console.WriteLine(string.Format("指令异常:{0}", ex.Message));
}
return null;
}
}
- 收发出错,打印异常信息。
- 最终返回
null,代表收发失败。
四、重点精讲:string.Format 语法(你指定的例句)
以这句为例:
csharp
string cmd = string.Format("IO{0}:getPowerEN", slot);
1. 整体作用
按照模板字符串 + 传入变量,自动拼接生成最终设备指令。
2. 逐段拆解
string cmd- 定义一个字符串变量
cmd,专门用来存放拼接完成的设备指令。
- 定义一个字符串变量
string.Format(模板字符串, 参数列表)- C# 字符串格式化方法,专门用来动态拼接文本。
- 第一个参数:
"IO{0}:getPowerEN"→ 模板{0}:占位符,编号从 0 开始;- 含义:这个位置会被后面第1个变量替换。
- 第二个参数:
slot→ 填充变量slot是槽位号(数字 1、2、3...)。
3. 执行过程 & 最终结果
假设调用时 slot = 1:
- 模板:
IO{0}:getPowerEN - 把
{0}替换为1 - 最终生成字符串 :
IO1:getPowerEN
4. 对应设备指令
生成的 IO1:getPowerEN 就是发给加热台的标准查询指令,和你Excel手册完全一致。
5. 拓展示例(举一反三)
string.Format("evb{0}:getpower?", 1)→ 生成evb1:getpower?string.Format("IO{0}:setPowerEN {1}", 1, 0)
两个占位符{0}{1},分别替换 1 和 0 → 生成IO1:setPowerEN 0
6. 为什么不直接写死字符串?
- 如果槽位固定为1:直接写
"IO1:getPowerEN"就行; - 但代码要支持 1号、2号、多槽位切换 ,所以用
string.Format动态拼接,一套代码适配所有槽位。
五、逐个业务方法精讲(基于 string.Format 统一逻辑)
5.1 IIC_Set / IIC_Get
csharp
public bool IIC_Set(int slot, string deviceAddr, string regAddr, string dataLength, string data)
{
string cmd = string.Format("IIC{0}:set {1},{2},{3},{4}", slot, deviceAddr, regAddr, dataLength, data);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains("iic set ok");
}
string.Format("IIC{0}:set {1},{2},{3},{4}", 五个参数){0}→ slot 槽位{1}→ deviceAddr 器件地址{2}→ regAddr 寄存器地址{3}→ dataLength 数据长度{4}→ data 写入数据
- 拼接后生成标准 IIC 写入指令,调用
SendCommand发送。 res.Contains("iic set ok"):判断设备返回是否包含成功关键字,返回true/false。
5.2 GetPower 查询功率
csharp
public string GetPower(int slot)
{
string cmd = string.Format("evb{0}:getpower?", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res))
return null;
string numberOnly = Regex.Replace(res, @"[^0-9]", "");
return !string.IsNullOrEmpty(numberOnly) ? numberOnly : null;
}
- 拼接指令
evb+槽位:getpower?。 Regex.Replace(res, @"[^0-9]", "")
正则表达式:把所有不是数字的字符全部替换为空 。
示例:Power : 42mW→ 过滤后只剩下42。
5.3 GetCurrent / GetVoltage
逻辑和 GetPower 完全一致:
string.Format拼接对应查询指令;- 调用
SendCommand收发; - 正则过滤,只保留数字。
5.4 SetVoltage 设置电压
csharp
public bool SetVoltage(int slot, double voltage)
{
string cmd = string.Format("evb{0}:setvoltage {1}", slot, voltage);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains("set Voltage ok!");
}
{0}=槽位,{1}=电压值;- 示例:
slot=1, voltage=3.3→ 生成evb1:setvoltage 3.3; - 判断返回文本是否包含成功标识。
5.5 IO 引脚系列(Get/Set 通用逻辑)
以 GetPowerEN 为例:
csharp
public string GetPowerEN(int slot)
{
string cmd = string.Format("IO{0}:getPowerEN", slot);
string res = SendCommand(cmd);
if (string.IsNullOrEmpty(res) || !res.Contains("POWER_EN:"))
return null;
return res.Split(':')[1].Trim();
}
string.Format拼接指令;res.Split(':'):字符串分割 ,以冒:为界限,把文本拆成数组。
示例:POWER_EN:1→ 拆分后数组["POWER_EN", "1"];[1]:取数组第2个元素(C# 数组从0开始编号);.Trim():去掉前后空格,最终取出状态值1。
所有
GetTxDis/GetRs0High等引脚查询,分割逻辑完全一样,仅指令名称不同。
5.6 SetDeviceIP 修改设备IP
csharp
public bool SetDeviceIP(string newIp)
{
string cmd = string.Format("setip {0}", newIp);
string res = SendCommand(cmd);
return !string.IsNullOrEmpty(res) && res.Contains(string.Format("setip:{0}", newIp));
}
- 拼接
setip + 新IP指令; - 判断设备是否返回设置成功的文本。
六、程序入口 Main 方法(整段逐句)
csharp
class Program
{
static void Main(string[] args)
{
class Program:程序入口类。static void Main(string[] args):C# 程序唯一启动入口
程序双击运行后,第一行执行这里 ;args接收启动参数,本代码未使用。
csharp
SFP_EVB_Heater heater = new SFP_EVB_Heater();
- 定义变量
heater; new SFP_EVB_Heater():创建加热台遥控器对象;- 创建瞬间,自动执行构造函数,初始化IP、端口、超时。
csharp
Console.WriteLine("===== 正在连接设备 =====");
if (!heater.Open())
{
Console.WriteLine("按任意键退出...");
Console.ReadKey();
return;
}
- 打印提示文字。
!heater.Open():调用连接方法,连接失败则进入分支。Console.ReadKey():暂停窗口,等待按键,防止窗口一闪而过。return:退出Main方法,程序结束。
csharp
string power = heater.GetPower();
string current = heater.GetCurrent();
string voltage = heater.GetVoltage();
依次调用查询方法,接收返回的数值。
csharp
if (!string.IsNullOrEmpty(power))
Console.WriteLine("当前功率:" + power + " mW");
!string.IsNullOrEmpty(power):判断返回值不为空(读取成功);+号:字符串拼接,把文本和数字拼在一起打印。
csharp
string powerEN = heater.GetPowerEN();
string txDis = heater.GetTxDis();
string rs0High = heater.GetRs0High();
调用引脚查询方法,读取硬件状态。
csharp
heater.Close();
Console.WriteLine("\n按任意键关闭窗口...");
Console.ReadKey();
}
}
- 调用
Close()断开连接。 - 暂停窗口,查看运行日志。
七、核心总结(小白必记)
string.Format核心用法
模板 +{编号}+ 变量 → 动态拼接字符串,本代码用来生成设备标准指令 。
{0}对应第一个变量,{1}对应第二个变量,依次类推。- TCP 固定四步
创建Socket → 连接IP+端口 → 文字转字节发送 → 接收字节转回文字。 - 代码分层
- 类内部方法:负责底层通讯、指令拼接、数据解析;
- Main 入口:负责调用功能、打印结果,只做业务测试。
- 重载方法作用
适配老旧 VS2008,实现"传参/不传参都能调用同一个功能"。
下面按你10个问题,逐个用通俗语言 + 可运行示例讲解,结合你现有代码、VS2008 环境,保证能看懂、能上机测试。
1、_lock 程序锁:作用 + 可运行示例
一、基础理解
lock 直译:排队锁 / 单人通道
- 同一时间,只允许一段代码被一个线程执行
- 防止多段代码同时抢同一个资源(这里就是网络通道),造成数据错乱、程序卡死
你当前场景:
如果程序同时发2条指令给加热台,网络通道会"打架",lock 让指令排队依次发送。
二、简易可运行示例(新建控制台项目,直接复制运行)
csharp
using System;
using System.Threading;
namespace LockDemo
{
class Program
{
// 定义锁对象(和你代码里的 _lock 一模一样)
private static readonly object _lockObj = new object();
// 模拟发送指令的方法
static void SendCmd(string name)
{
// 上锁:进入排队通道
lock (_lockObj)
{
Console.WriteLine("【" + name + "】开始执行");
Thread.Sleep(1000); // 模拟操作耗时
Console.WriteLine("【" + name + "】执行完毕\n");
}
}
static void Main(string[] args)
{
// 模拟两个任务同时调用发送指令(多线程争抢)
Thread t1 = new Thread(() => SendCmd("指令1"));
Thread t2 = new Thread(() => SendCmd("指令2"));
t1.Start();
t2.Start();
Console.ReadKey();
}
}
}
运行效果(加 lock):
【指令1】开始执行
【指令1】执行完毕
【指令2】开始执行
【指令2】执行完毕
👉 两个指令排队执行,不会重叠。
把 lock (_lockObj) 注释掉再运行(无锁):
【指令1】开始执行
【指令2】开始执行
【指令1】执行完毕
【指令2】执行完毕
👉 两个指令同时执行,争抢资源,对应你设备就会通讯乱码、超时。
总结
你代码里的 lock (_lock):
保证多条指令串行排队访问网络管道,是工业设备通讯的标准写法。
2、方法默认参数:是什么 + 用法 + VS2008 限制
1)什么是方法默认参数
调用方法时不传入某个参数,程序自动使用预先设置的默认值。
2)新版 C# 用法(VS2010+ 支持)
示例:
csharp
// port=9000 就是默认参数,不填就自动用 9000
public bool Open(string ipAddress, int port = 9000)
{
// 代码逻辑
}
// 调用两种写法
Open("129.168.1.133"); // 不填端口 → 自动用 9000
Open("129.168.1.133", 8080);// 手动指定端口
3)重点:你的 VS2008 + .NET3.5
不支持默认参数语法 ,所以项目里全部改用 方法重载 (多个同名方法)来实现相同效果,这也是你代码里一堆 Open() 重载的原因。
4、默认参数 和 重载 目标一致
都是为了:调用更灵活,少写重复代码。
3、代码折叠标记 #region / #endregion 作用,不写会怎样
1)作用
纯 编辑器美化标记 ,对程序运行、逻辑、编译没有任何影响。
- 可以把一大段代码「折叠/收起」,界面更整洁
- 用来给代码分区(比如"连接方法区""IO方法区"),方便查找
2)不写会怎么样
完全没事,程序正常编译、正常运行,只是代码平铺很长,不方便阅读。
3)补充规则
#region 分区名必须 和#endregion成对出现- 少一个就会报编译错误(你之前遇到的就是这个问题)
4、string.Format("连接失败:{0}", ex.Message) 为什么不能用 {1}?
1)规则
{数字} 叫占位符 ,数字从 0 开始,按顺序对应后面的参数:
{0}→ 对应Format后面 第1个参数{1}→ 对应 第2个参数{2}→ 对应 第3个参数,以此类推
2)你这句拆解
csharp
string.Format("连接失败:{0}", ex.Message)
- 模板字符串:
"连接失败:{0}" - 后面只有 1个参数 :
ex.Message - 所以只能用
{0}
3)举例演示(多参数)
csharp
// 2个参数 → 必须用 {0} {1}
string.Format("IP:{0} 端口:{1}", "129.168.1.133", 9000);
结果:IP:129.168.1.133 端口:9000
4)错误写法
csharp
// 后面只有1个参数,模板写 {1} → 编译报错
string.Format("连接失败:{1}", ex.Message);
结论:占位符数字 = 参数的顺序编号(从0开始)。
5、Encoding.UTF8.GetString(rbuf, 0, count).Trim() 逐段解释
整句作用:把设备发回来的二进制字节,转成正常文字,并清理多余空格
1)Encoding.UTF8.GetString(字节数组, 起始位置, 读取长度)
对应:GetString(rbuf, 0, count)
rbuf:接收数据的字节数组(存设备发回来的原始二进制)0:从数组第0个位置开始读(从头读取)count:Client.Receive返回的实际收到字节个数
为什么不直接写 rbuf 全长?
数组长度是固定1024,但设备可能只返回几十个字节,后面全是空数据,只读有效部分,避免乱码。
2).Trim() 方法
作用:删除字符串 头部、尾部 的空格、换行、制表符
示例:
原始字符串: Power : 42mW \r\n
执行 .Trim() 后:Power : 42mW
3)整句最终生成结果
把设备返回的二进制 → 干净、无多余空格的文本字符串。
6、IIC_Set(int slot, string deviceAddr, string regAddr, string dataLength, string data) 参数含义
结合你 Excel 设备指令手册,逐个对应:
csharp
public bool IIC_Set(int slot, string deviceAddr, string regAddr, string dataLength, string data)
int slot
槽位号:对应设备IIC1 / IIC2,取值 1、2、3......string deviceAddr
器件地址:硬件IIC设备地址(手册示例a0)string regAddr
寄存器地址:要操作的硬件寄存器地址(手册示例0)string dataLength
数据长度:本次要发送的数据位数(手册示例5)string data
要发送的十六进制数据,逗号分隔(手册示例a1,9,1b,2c,3d)
对应最终指令示例
调用:IIC_Set(2, "a0", "0", "5", "a1,9,1b,2c,3d")
内部拼接指令:IIC2:set a0,0,5,a1,9,1b,2c,3d
和你手册完全一致。
7、IsOpen 是设备自带的变量吗?
不是!完全是你代码自己定义的属性。
拆解:
csharp
public bool IsOpen
{
get
{
return Client != null && Client.Connected;
}
}
IsOpen:C# 代码里自定义只读属性- 逻辑:判断你代码中的
Socket Client(网络管道)是否已连接 - 和加热台硬件设备没有任何内置变量关联
只是代码用来「自查网络状态」的工具。
8、lock (_lock) 和 IsOpen 是同一个过程吗?
不是,是两个完全独立的逻辑,作用不同
-
lock (_lock)上锁作用:控制多段代码排队执行 ,防止并发争抢网络。
范围:
{}内部所有代码都受排队限制。 -
if (IsOpen)判断连接状态作用:检查当前网线有没有连上设备,避免重复连接、空指令。
执行顺序(你 Open 方法里)
进入 Open 方法
→ 先 lock 上锁(开始排队)
→ 再 if(IsOpen) 判断是否已连接
→ 再执行创建 Socket、连接设备
两个功能搭配使用,各司其职。
9、Regex.Replace(res, @"[^0-9]", "") 中 @ 和 ^ 含义
整句作用:把字符串里所有非数字字符全部删掉,只保留 0~9
1)@ 符号(C# 字符串前缀)
叫做 逐字字符串
作用:让字符串里的 \ 不再被当成转义字符。
正则表达式大量用到 \,所以正则字符串前面习惯性加 @。
示例对比:
- 不加
@:"[^\d]"要写双斜杠"[^\\d]"很麻烦 - 加
@:@"[^0-9"直接写原生正则,更直观
2)^ 正则符号(放在 [] 内部)
[^字符] = 取反
[0-9]:匹配 0~9 所有数字[^0-9]:匹配 所有不是数字的字符
整句逻辑
Regex.Replace(内容, 匹配规则, 替换为空白)
= 找到所有非数字 → 替换成空字符 → 最后只剩数字
示例:
输入:Power : 42mW
输出:42
10、res.Contains("set Voltage ok!") 含义
1)方法解释
字符串.Contains("子字符串")
作用:判断前面的大字符串里,是否包含指定的一段文字
- 包含 → 返回
true - 不包含 → 返回
false
2)结合你代码场景
csharp
res.Contains("set Voltage ok!")
res:设备返回的整行文本set Voltage ok!:设备约定的 设置成功标识
运行逻辑
设备正常返回:set Voltage ok! Voltage: 3360mV
→ Contains 判定为 true → 代表电压设置成功。
如果返回报错文本 → 判定为 false → 设置失败。
快速汇总(方便你记忆)
lock:排队锁,防止多指令抢网络- 默认参数:不传参用预设值,VS2008 不支持,改用重载
#region:仅编辑器折叠,不影响运行{0}:占位符按参数顺序编号,从0开始GetString(0,count):读取有效字节;Trim():清首尾空格- IIC 五个参数:槽位、器件地址、寄存器、数据长度、写入数据
IsOpen:代码自定义属性,不是设备变量lock控排队,IsOpen查连接状态,两个逻辑@禁用转义;[^0-9]匹配非数字Contains:判断字符串是否包含指定关键字(用来判断指令成败)