一、C# WinForm + 基恩士扫码枪 + 串口通信 + SQLite + Log4Net
1.1 技术架构概览
| 模块 | 技术选型 | 作用 |
|---|---|---|
| UI框架 | WinForms (.NET Framework 4.7+) | 传统工业界面,快速开发 |
| 硬件通信 | SerialPort + 基恩士SDK | 扫码枪数据采集 |
| 数据存储 | SQLite + System.Data.SQLite | 轻量级本地数据库 |
| 日志系统 | Log4Net | 运行日志与故障追踪 |
| 数据导出 | NPOI/EPPlus | Excel报表生成 |
1.2 基恩士扫码枪SDK集成
基恩士(Keyence)扫码枪通常提供两种集成方式:
方式A:USB HID键盘模式(免开发)
// 窗体级别键盘钩子捕获扫码数据
public partial class ScanForm : Form
{
private StringBuilder _scanBuffer = new StringBuilder();
private DateTime _lastKeyTime;
private const int SCAN_THRESHOLD_MS = 50; // 扫码枪输入间隔<50ms
public ScanForm()
{
InitializeComponent();
this.KeyPreview = true; // 捕获全局键盘事件
}
protected override void OnKeyPress(KeyPressEventArgs e)
{
var now = DateTime.Now;
// 判断是否为扫码枪快速输入(人工输入间隔>100ms)
if ((now - _lastKeyTime).TotalMilliseconds > 100 && _scanBuffer.Length > 0)
{
_scanBuffer.Clear();
}
_lastKeyTime = now;
if (e.KeyChar == (char)Keys.Enter)
{
string barcode = _scanBuffer.ToString().Trim();
_scanBuffer.Clear();
ProcessBarcode(barcode); // 处理条码
}
else
{
_scanBuffer.Append(e.KeyChar);
}
base.OnKeyPress(e);
}
}
方式B:SerialPort串口通信模式(推荐工业场景)
using System.IO.Ports;
public class KeyenceScanner
{
private SerialPort _serialPort;
private ILog _logger = LogManager.GetLogger(typeof(KeyenceScanner));
public event Action<string> OnBarcodeScanned;
public bool Connect(string portName, int baudRate = 9600)
{
try
{
_serialPort = new SerialPort
{
PortName = portName,
BaudRate = baudRate,
DataBits = 8,
StopBits = StopBits.One,
Parity = Parity.None,
ReadTimeout = 500,
WriteTimeout = 500
};
_serialPort.DataReceived += SerialPort_DataReceived;
_serialPort.Open();
_logger.Info($"扫码枪已连接: {portName}");
return true;
}
catch (Exception ex)
{
_logger.Error($"扫码枪连接失败: {ex.Message}");
return false;
}
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
string data = _serialPort.ReadExisting().Trim();
if (!string.IsNullOrEmpty(data))
{
// 跨线程更新UI
OnBarcodeScanned?.Invoke(data);
_logger.Debug($"扫描到条码: {data}");
}
}
catch (Exception ex)
{
_logger.Error($"数据接收异常: {ex.Message}");
}
}
// 发送指令控制扫码枪(需参考基恩士指令集)
public void TriggerScan()
{
// 示例:发送触发扫描指令
_serialPort.WriteLine("LON"); // 开启激光
Thread.Sleep(100);
_serialPort.WriteLine("LOFF"); // 关闭激光
}
}
1.3 SQLite数据库操作封装
using System.Data.SQLite;
public class SQLiteHelper : IDisposable
{
private SQLiteConnection _connection;
private readonly string _connectionString;
private static readonly object _lock = new object();
public SQLiteHelper(string dbPath)
{
_connectionString = $"Data Source={dbPath};Version=3;Pooling=True;Max Pool Size=100;";
InitializeDatabase();
}
private void InitializeDatabase()
{
if (!File.Exists(_connectionString.Split('=')[1].Split(';')[0]))
{
SQLiteConnection.CreateFile(_connectionString.Split('=')[1].Split(';')[0]);
}
_connection = new SQLiteConnection(_connectionString);
_connection.Open();
// 创建生产数据表
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS ProductionData (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Barcode TEXT NOT NULL,
ScanTime DATETIME DEFAULT CURRENT_TIMESTAMP,
Result TEXT,
Operator TEXT
)");
}
public void InsertScanRecord(string barcode, string result, string operatorName)
{
lock (_lock) // 多线程安全
{
using (var cmd = new SQLiteCommand(
"INSERT INTO ProductionData (Barcode, Result, Operator) VALUES (@barcode, @result, @operator)",
_connection))
{
cmd.Parameters.AddWithValue("@barcode", barcode);
cmd.Parameters.AddWithValue("@result", result);
cmd.Parameters.AddWithValue("@operator", operatorName);
cmd.ExecuteNonQuery();
}
}
}
public DataTable QueryByDateRange(DateTime start, DateTime end)
{
using (var cmd = new SQLiteCommand(
"SELECT * FROM ProductionData WHERE ScanTime BETWEEN @start AND @end ORDER BY ScanTime DESC",
_connection))
{
cmd.Parameters.AddWithValue("@start", start);
cmd.Parameters.AddWithValue("@end", end);
var adapter = new SQLiteDataAdapter(cmd);
var dt = new DataTable();
adapter.Fill(dt);
return dt;
}
}
public void Dispose() => _connection?.Dispose();
}
1.4 Log4Net配置(工业级)
<!-- log4net.config -->
<log4net>
<!-- 调试日志:按日期滚动 -->
<appender name="DebugAppender" type="log4net.Appender.RollingFileAppender">
<file value="Logs\Debug\" />
<datePattern value="yyyy-MM-dd'.log'" />
<appendToFile value="true" />
<rollingStyle value="Date" />
<staticLogFileName value="false" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<!-- 错误日志:按大小滚动,保留10个备份 -->
<appender name="ErrorAppender" type="log4net.Appender.RollingFileAppender">
<file value="Logs\Error\error.log" />
<appendToFile value="true" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="10" />
<maximumFileSize value="5MB" />
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="ERROR" />
<levelMax value="FATAL" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline%exception" />
</layout>
</appender>
<!-- 扫码数据专用日志 -->
<appender name="ScanAppender" type="log4net.Appender.RollingFileAppender">
<file value="Logs\Scan\scan.log" />
<datePattern value="yyyy-MM-dd'.log'" />
<appendToFile value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date,%message%newline" />
</layout>
</appender>
<logger name="ScanLogger" additivity="false">
<level value="INFO" />
<appender-ref ref="ScanAppender" />
</logger>
<root>
<level value="DEBUG" />
<appender-ref ref="DebugAppender" />
<appender-ref ref="ErrorAppender" />
</root>
</log4net>
二、C# WinForm + 西门子S7-1200 PLC + Modbus TCP + LabVIEW辅助
2.1 通信协议选择对比
| 协议 | 适用场景 | 性能 | 复杂度 |
|---|---|---|---|
| S7 Protocol | 纯西门子环境 | 高(原生优化) | 中 |
| Modbus TCP | 多品牌混合 | 中(通用性强) | 低 |
| Profinet | 实时性要求极高 | 极高 | 高 |
2.2 Modbus TCP通信实现(NModbus4)
using Modbus.Device; // NModbus4 NuGet包
public class ModbusPLCClient
{
private TcpClient _tcpClient;
private IModbusMaster _master;
private readonly string _ip;
private readonly int _port;
private readonly byte _slaveId;
private Timer _heartbeatTimer;
public bool IsConnected => _tcpClient?.Connected ?? false;
public event Action<bool> OnConnectionStatusChanged;
public ModbusPLCClient(string ip, int port = 502, byte slaveId = 1)
{
_ip = ip;
_port = port;
_slaveId = slaveId;
}
public async Task<bool> ConnectAsync()
{
try
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(_ip, _port);
_master = ModbusIpMaster.CreateIp(_tcpClient);
StartHeartbeat();
OnConnectionStatusChanged?.Invoke(true);
return true;
}
catch (Exception ex)
{
Log.Error($"PLC连接失败: {ex.Message}");
return false;
}
}
// 读取保持寄存器(对应PLC的DB块映射)
public ushort[] ReadHoldingRegisters(ushort startAddress, ushort count)
{
if (!IsConnected) throw new InvalidOperationException("PLC未连接");
return _master.ReadHoldingRegisters(_slaveId, startAddress, count);
}
// 写入线圈(控制输出点)
public void WriteCoil(ushort coilAddress, bool value)
{
if (!IsConnected) throw new InvalidOperationException("PLC未连接");
_master.WriteSingleCoil(_slaveId, coilAddress, value);
}
// 心跳检测:每3秒读取一次系统状态字
private void StartHeartbeat()
{
_heartbeatTimer = new Timer(async _ =>
{
try
{
await Task.Run(() => ReadHoldingRegisters(0, 1));
}
catch
{
OnConnectionStatusChanged?.Invoke(false);
await ReconnectAsync();
}
}, null, 3000, 3000);
}
private async Task ReconnectAsync()
{
// 指数退避重连策略
int delay = 1000;
while (!IsConnected)
{
await Task.Delay(delay);
if (await ConnectAsync()) break;
delay = Math.Min(delay * 2, 30000); // 最大30秒
}
}
}
2.3 LabVIEW辅助调试技巧
在开发阶段,使用LabVIEW进行协议验证:
-
Modbus TCP测试:使用LabVIEW的Modbus库快速验证寄存器地址映射
-
数据监控:通过LabVIEW前面板实时观察PLC数据变化
-
报文分析:利用LabVIEW捕获TCP报文,对比C#程序发送的报文格式
LabVIEW与C#混合调试流程:
TIA Portal配置PLC → LabVIEW验证通信 → C#实现业务逻辑 → 对比数据一致性
2.4 Excel导出组件(NPOI)
using NPOI.XSSF.UserModel;
using System.Data;
public class ExcelExporter
{
public void ExportProductionData(DataTable data, string filePath)
{
IWorkbook workbook = new XSSFWorkbook();
ISheet sheet = workbook.CreateSheet("生产数据");
// 创建表头
IRow headerRow = sheet.CreateRow(0);
for (int i = 0; i < data.Columns.Count; i++)
{
headerRow.CreateCell(i).SetCellValue(data.Columns[i].ColumnName);
}
// 填充数据
for (int i = 0; i < data.Rows.Count; i++)
{
IRow row = sheet.CreateRow(i + 1);
for (int j = 0; j < data.Columns.Count; j++)
{
row.CreateCell(j).SetCellValue(data.Rows[i][j].ToString());
}
}
// 自动列宽
for (int i = 0; i < data.Columns.Count; i++)
{
sheet.AutoSizeColumn(i);
}
using (var fs = new FileStream(filePath, FileMode.Create))
{
workbook.Write(fs);
}
}
}
三、C# WPF + MVVMLight + Modbus TCP + HTTPS + S7NetPlus
3.1 MVVMLight框架核心架构
View (XAML)
↓ DataContext
ViewModel (继承ViewModelBase)
↓ 命令(ICommand) + 属性(INotifyPropertyChanged)
Model (数据实体)
↓
Service (PLC通信服务/HTTP服务)
3.2 完整ViewModel示例
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using S7.Net; // S7NetPlus库
public class MainViewModel : ViewModelBase
{
private readonly PlcService _plcService;
private readonly HttpService _httpService;
// 绑定属性
private bool _isRunning;
public bool IsRunning
{
get => _isRunning;
set => Set(ref _isRunning, value);
}
private float _temperature;
public float Temperature
{
get => _temperature;
set => Set(ref _temperature, value);
}
// 命令
public RelayCommand StartCommand { get; private set; }
public RelayCommand StopCommand { get; private set; }
public RelayCommand UploadDataCommand { get; private set; }
public MainViewModel()
{
_plcService = new PlcService("192.168.0.1", CpuType.S71200);
_httpService = new HttpService("https://api.factory.com");
InitializeCommands();
StartPolling();
}
private void InitializeCommands()
{
StartCommand = new RelayCommand(async () =>
{
await _plcService.WriteBit("DB1.DBX0.0", true); // 启动设备
IsRunning = true;
});
StopCommand = new RelayCommand(async () =>
{
await _plcService.WriteBit("DB1.DBX0.0", false); // 停止设备
IsRunning = false;
});
UploadDataCommand = new RelayCommand(async () =>
{
var data = new { Temperature = Temperature, Timestamp = DateTime.Now };
await _httpService.PostAsync("/api/production", data);
});
}
// 定时轮询PLC数据
private void StartPolling()
{
Task.Run(async () =>
{
while (true)
{
try
{
Temperature = await _plcService.ReadReal("DB1.DBD2");
await Task.Delay(500); // 500ms刷新周期
}
catch (Exception ex)
{
// 记录日志,继续轮询
Log.Error(ex);
}
}
});
}
}
3.3 S7NetPlus高级封装
using S7.Net;
public class PlcService
{
private Plc _plc;
private readonly string _ip;
private readonly CpuType _cpuType;
private readonly object _lockObj = new object();
public bool IsConnected => _plc?.IsConnected ?? false;
public PlcService(string ip, CpuType cpuType)
{
_ip = ip;
_cpuType = cpuType;
}
public async Task<bool> ConnectAsync()
{
try
{
_plc = new Plc(_cpuType, _ip, 0, 1); // Rack=0, Slot=1(S7-1200)
await Task.Run(() => _plc.Open());
return true;
}
catch (Exception ex)
{
Log.Error($"PLC连接失败: {ex.Message}");
return false;
}
}
// 读取Real类型(浮点数)
public async Task<float> ReadReal(string address)
{
lock (_lockObj) // 线程安全
{
var result = _plc.Read(address);
return Convert.ToSingle(result);
}
}
// 读取Int类型
public async Task<short> ReadInt(string address)
{
lock (_lockObj)
{
return (short)_plc.Read(address);
}
}
// 写入布尔值(如DB1.DBX0.0)
public async Task WriteBit(string address, bool value)
{
lock (_lockObj)
{
_plc.WriteBit(address, value);
}
}
// 批量读取优化(减少通信次数)
public async Task<byte[]> ReadBytes(int dbNumber, int startByte, int count)
{
lock (_lockObj)
{
return _plc.ReadBytes(DataType.DataBlock, dbNumber, startByte, count);
}
}
}
3.4 HTTPS通信服务
using System.Net.Http;
using System.Net.Http.Json;
public class HttpService
{
private readonly HttpClient _httpClient;
public HttpService(string baseUrl)
{
_httpClient = new HttpClient
{
BaseAddress = new Uri(baseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
// 配置HTTPS证书验证(开发环境可忽略)
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
};
}
public async Task<T> GetAsync<T>(string url)
{
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>();
}
public async Task PostAsync<T>(string url, T data)
{
var response = await _httpClient.PostAsJsonAsync(url, data);
response.EnsureSuccessStatusCode();
}
}
四、C# .NET Core + MySQL + Web API
4.1 技术架构(工业物联网方向)
┌─────────────────────────────────────────┐
│ ASP.NET Core Web API │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Controllers│ │ Services │ │ Repos │ │
│ └──────────┘ └──────────┘ └─────────┘ │
│ ┌──────────┐ ┌──────────┐ │
│ │ JWT Auth │ │ EF Core │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ MySQL Database │
│ (生产数据/设备状态/用户权限) │
└─────────────────────────────────────────┘
4.2 完整Web API项目结构
IndustrialApi/
├── Controllers/
│ ├── ProductionController.cs # 生产数据API
│ └── DeviceController.cs # 设备管理API
├── Services/
│ ├── IPlcCommunicationService.cs
│ └── PlcCommunicationService.cs
├── Repositories/
│ ├── IProductionRepository.cs
│ └── ProductionRepository.cs
├── Models/
│ ├── Entities/ # 数据库实体
│ └── DTOs/ # 数据传输对象
├── Infrastructure/
│ ├── DatabaseContext.cs # DbContext
│ └── JwtSettings.cs # JWT配置
└── Program.cs
4.3 EF Core + MySQL配置
// Program.cs
using Microsoft.EntityFrameworkCore;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
// 配置MySQL连接
builder.Services.AddDbContext<IndustrialDbContext>(options =>
options.UseMySql(
builder.Configuration.GetConnectionString("DefaultConnection"),
new MySqlServerVersion(new Version(8, 0, 21)),
mySqlOptions => mySqlOptions.EnableRetryOnFailure()
));
// 添加JWT认证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
// 注册服务
builder.Services.AddScoped<IPlcService, PlcService>();
builder.Services.AddScoped<IProductionRepository, ProductionRepository>();
var app = builder.Build();
app.Run();
4.4 生产数据Controller示例
[ApiController]
[Route("api/[controller]")]
[Authorize] // JWT认证
public class ProductionController : ControllerBase
{
private readonly IProductionRepository _repository;
private readonly IPlcService _plcService;
[HttpGet("realtime")]
public async Task<ActionResult<ProductionDataDto>> GetRealtimeData()
{
// 从PLC读取实时数据
var data = await _plcService.GetRealtimeDataAsync();
return Ok(data);
}
[HttpPost("batch")]
public async Task<IActionResult> UploadBatchData([FromBody] List<ProductionDataDto> data)
{
await _repository.InsertBatchAsync(data);
return Ok(new { Count = data.Count, Message = "数据上传成功" });
}
[HttpGet("history")]
public async Task<ActionResult<PagedResult<ProductionDataDto>>> GetHistory(
[FromQuery] DateTime start,
[FromQuery] DateTime end,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 50)
{
var result = await _repository.GetPagedAsync(start, end, page, pageSize);
return Ok(result);
}
}
五、关键技术对比与选型建议
5.1 技术栈演进路线
| 阶段 | 技术组合 | 适用场景 |
|---|---|---|
| 传统工业 | WinForm + SerialPort + SQLite | 单机版产线终端 |
| 设备联网 | WinForm + Modbus TCP + S7 | 多PLC集中监控 |
| 现代上位机 | WPF + MVVM + S7NetPlus | 复杂交互、数据可视化 |
| 工业物联网 | .NET Core + Web API + MySQL | 云端数据汇总、远程监控 |
5.2 性能优化要点
-
PLC通信优化:
-
使用批量读取代替单次读取(减少网络往返)
-
心跳间隔建议3-5秒(避免PLC CPU过载)
-
采用指数退避重连策略(1s→2s→4s→30s上限)
-
-
数据库优化:
-
SQLite使用连接池(Pooling=True)
-
MySQL启用失败重试(EnableRetryOnFailure)
-
大数据量采用分表或归档策略
-
-
UI响应优化:
-
所有IO操作必须异步(async/await)
-
使用
IProgress<T>报告进度到UI线程 -
数据绑定使用
ObservableCollection+INotifyPropertyChanged
-