[特殊字符] 工业上位机开发技术栈完整笔记

一、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进行协议验证:

  1. Modbus TCP测试:使用LabVIEW的Modbus库快速验证寄存器地址映射

  2. 数据监控:通过LabVIEW前面板实时观察PLC数据变化

  3. 报文分析:利用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 性能优化要点

  1. PLC通信优化

    • 使用批量读取代替单次读取(减少网络往返)

    • 心跳间隔建议3-5秒(避免PLC CPU过载)

    • 采用指数退避重连策略(1s→2s→4s→30s上限)

  2. 数据库优化

    • SQLite使用连接池(Pooling=True)

    • MySQL启用失败重试(EnableRetryOnFailure)

    • 大数据量采用分表或归档策略

  3. UI响应优化

    • 所有IO操作必须异步(async/await)

    • 使用IProgress<T>报告进度到UI线程

    • 数据绑定使用ObservableCollection + INotifyPropertyChanged

相关推荐
愤豆2 小时前
07-Java语言核心-JVM原理-JVM对象模型详解
java·jvm·c#
2401_873544923 小时前
使用Fabric自动化你的部署流程
jvm·数据库·python
IAUTOMOBILE3 小时前
C++ 入门基础:开启编程新世界的大门
java·jvm·c++
qq_148115373 小时前
Python上下文管理器(with语句)的原理与实践
jvm·数据库·python
qwehjk20083 小时前
机器学习模型部署:将模型转化为Web API
jvm·数据库·python
蜜獾云4 小时前
DDD 架构分层,MQ消息要放到那一层处理?
java·jvm·架构
愤豆5 小时前
06-Java语言核心-JVM原理-JVM内存区域详解
java·开发语言·jvm
Fortune795 小时前
用Pandas处理时间序列数据(Time Series)
jvm·数据库·python
2401_878530215 小时前
高级爬虫技巧:处理JavaScript渲染(Selenium)
jvm·数据库·python