去年下半年,我帮天津滨海新区一家给一汽丰田做配套的汽车零部件国企,完成了3条产线、12套C#上位机的全栈国产化适配。这个项目的背景很典型,也很戳中国企客户的命门:

  • 政策硬要求:客户要申报国家级智能制造示范工厂,等保2.0三级以上必须100%使用信创目录内的软硬件;
  • 供应链风险:之前用的是西门子S7-1500 PLC + Windows Server 2019 + .NET Framework 4.8,2025年初西门子突然通知部分型号PLC的维护周期缩短,客户担心后续断供、升级无门;
  • 成本倒逼:之前的Windows Server授权、.NET Framework企业级支持、西门子PLC通信库授权,一年光软件授权费就要30多万,换成国产软硬件后,一次性投入差不多,长期维护成本直接降了80%。

一开始客户找了两家本地的集成商,都没搞定:第一家只会用.NET Framework,根本不知道怎么在统信UOS上跑C#;第二家勉强用.NET Core 3.1跑起来了,但和汇川国产PLC的通信丢包率高达1.2%,产线根本不敢用。

我们接手后,用了2个月完成全栈适配:从Windows Server 2019迁移到统信UOS 1070d服务器版,从.NET Framework 4.8迁移到.NET 8 LTS,从西门子S7-1500迁移到汇川AM600-CPU1608TN,从SQL Server 2019迁移到达梦8,最后交付的系统,丢包率稳定在0.01%以下,连续稳定运行6个月,顺利通过了等保2.0三级测评和国家级智能制造示范工厂的初审,客户第二年就把剩下的17条产线也全部交给我们做了。

这篇文章,我把这套可直接复制、可直接交付国企项目的C#上位机全栈国产化适配方案全部分享出来,从环境准备、框架迁移、PLC通信、数据库对接,到等保2.0适配、性能优化,全流程无死角覆盖,所有代码可直接运行,所有方案都经过产线验证。

本文适用人群:需要做信创适配的工业自动化工程师、想接国企/政务信创项目的技术团队、正在学习C#上位机开发的新手。

一、先搞懂本质:信创适配不是简单的“换个系统”

很多技术团队做信创适配,第一步就走错了:直接把Windows上的.NET Framework代码复制到统信UOS上,用.NET Core/5+跑一下,能编译通过就觉得完事了,结果一到产线就全崩了:界面乱码、PLC通信丢包、数据库连接失败、性能比Windows上慢3倍。

信创适配的本质,不是简单的“换个操作系统、换个框架版本”,而是一套完整的、从底层硬件到上层应用的全栈适配工程,要解决的核心问题有4个:

  1. 跨平台兼容性:从Windows的Win32 API、WPF/WinForms,迁移到统信UOS的Linux API、Avalonia/MAUI跨平台UI;
  2. 国产硬件适配:从Intel/AMD x86架构,迁移到鲲鹏/昇腾/海光ARM/x86兼容架构,要解决指令集差异、性能优化的问题;
  3. 国产工业软件适配:从西门子/三菱/欧姆龙等外资PLC,迁移到汇川/台达/信捷等国产PLC,从SQL Server/Oracle,迁移到达梦/人大金仓/神通等国产数据库;
  4. 等保2.0合规性:要满足身份认证、权限管理、日志审计、数据加密、边界防护等等保2.0三级以上的要求。

针对不同的场景和需求,我们总结了一套C#上位机信创适配的选型指南,能直接帮你避开90%的选型坑:

场景 推荐操作系统 推荐.NET版本 推荐UI框架 推荐国产PLC 推荐国产数据库
新开发项目 统信UOS 1070d/麒麟V10 SP3 .NET 8 LTS Avalonia UI(工业场景首选,性能好、控件全) 汇川AM600/台达AS500 达梦8/人大金仓8
旧项目快速迁移 统信UOS 1070d/麒麟V10 SP3 .NET 6 LTS(兼容性更好) Avalonia UI(如果是WinForms/WPF,优先用Avalonia重构,不要用MAUI,工业场景MAUI控件太少) 汇川/台达(和外资PLC协议兼容性最好) 达梦8(和SQL Server语法最像,迁移成本最低)
国产化要求极高(必须用纯国产) 统信UOS 1070d/麒麟V10 SP3 .NET 8 LTS(已进入信创目录) Avalonia UI(已进入信创目录) 信捷XD5/汇川H3U(纯国产PLC) 神通数据库(纯国产数据库)

二、环境准备:先搭好信创开发环境

很多技术团队做信创适配,一开始就卡在环境搭建上:统信UOS不会装、.NET 8不会配置、国产数据库不会连。这一节我把完整的信创开发环境搭建步骤全部分享出来,新手跟着做,30分钟就能搭好。

2.1 操作系统安装:统信UOS 1070d服务器版/桌面版

如果是开发环境,推荐用统信UOS 1070d桌面版,界面和Windows类似,上手快;如果是生产环境,推荐用统信UOS 1070d服务器版,性能更好、更稳定、安全性更高。

统信UOS的安装非常简单,和Windows差不多,这里就不再重复,只讲几个开发环境必须做的配置:

  1. 换国内源:统信UOS默认的源是官方源,国内访问速度很慢,必须换成国内的清华源或者阿里源:
    # 备份原来的源
    sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
    # 编辑源文件
    sudo nano /etc/apt/sources.list
    # 把里面的内容全部删掉,换成清华源的统信UOS 1070d内容
    deb https://mirrors.tuna.tsinghua.edu.cn/deepin/ beige main contrib non-free
    deb-src https://mirrors.tuna.tsinghua.edu.cn/deepin/ beige main contrib non-free
    # 保存退出,按Ctrl+O,然后按Enter,再按Ctrl+X
    # 更新软件列表
    sudo apt update && sudo apt upgrade -y
    
  2. 安装必要的开发工具
    # 安装Git
    sudo apt install git -y
    # 安装VS Code(统信UOS应用商店里也有,直接搜VS Code安装也行)
    wget https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-x64 -O vscode.deb
    sudo dpkg -i vscode.deb
    sudo apt install -f -y
    # 安装.NET 8 SDK
    wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh
    chmod +x dotnet-install.sh
    ./dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet
    # 配置环境变量
    echo 'export DOTNET_ROOT=/usr/share/dotnet' >> ~/.bashrc
    echo 'export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools' >> ~/.bashrc
    source ~/.bashrc
    # 验证.NET 8 SDK是否安装成功
    dotnet --version
    # 正常输出示例:8.0.401
    
  3. 安装Avalonia UI开发工具
    # 安装Avalonia UI模板
    dotnet new install Avalonia.Templates
    # 验证模板是否安装成功
    dotnet new list
    # 能看到Avalonia相关的模板,就说明安装成功了
    

2.2 国产PLC通信环境准备:汇川AM600

我们这次适配用的是汇川AM600-CPU1608TN国产PLC,它和西门子S7-1200/S7-1500的Modbus TCP协议、甚至S7协议都兼容,迁移成本极低,是旧项目快速迁移的首选。

汇川PLC的通信环境准备非常简单:

  1. 安装汇川AutoShop编程软件:AutoShop是汇川官方的PLC编程软件,支持Windows和统信UOS(统信UOS应用商店里搜AutoShop就能安装);
  2. 配置PLC的IP地址:用AutoShop连接PLC,把PLC的IP地址改成和上位机在同一个网段,比如上位机的IP是192.168.1.100,PLC的IP改成192.168.1.200;
  3. 配置Modbus TCP服务器:在AutoShop里打开PLC的硬件配置,启用Modbus TCP服务器,设置端口号为502(默认端口),设置允许访问的IP地址(可以设置成0.0.0.0,允许所有IP访问,生产环境建议设置成上位机的IP地址,提高安全性)。

2.3 国产数据库环境准备:达梦8

我们这次适配用的是达梦8企业版,它和SQL Server的语法最像,迁移成本最低,而且已经进入信创目录,完全满足等保2.0三级以上的要求。

达梦8的安装和配置也非常简单,这里只讲几个开发环境必须做的配置:

  1. 安装达梦8企业版:从达梦官网下载统信UOS版本的达梦8企业版安装包,按照官方文档安装;
  2. 创建数据库实例:用达梦的DM管理工具创建一个数据库实例,设置端口号为5236(默认端口),设置用户名和密码(比如用户名SYSDBA,密码SYSDBA001);
  3. 配置达梦8的.NET驱动:从达梦官网下载统信UOS版本的达梦8 .NET 8驱动(DmProvider.dll),或者用NuGet安装:
    dotnet add package DmProvider
    

三、框架迁移:从.NET Framework 4.8 + WinForms迁移到.NET 8 + Avalonia UI

很多旧项目用的是.NET Framework 4.8 + WinForms,这两个都不支持Linux,必须迁移到.NET 8 + Avalonia UI。这一节我把完整的迁移步骤全部分享出来,从项目创建、UI重构、代码迁移,到性能优化,全流程无死角覆盖。

3.1 项目创建:用Avalonia UI创建工业上位机项目

用Avalonia UI创建工业上位机项目非常简单,一条命令就能搞定:

# 创建一个Avalonia UI MVVM项目(工业场景推荐用MVVM模式,代码更清晰、更易维护)
dotnet new avalonia.mvvm -n IndustrialHMI
# 进入项目目录
cd IndustrialHMI
# 打开项目
code .

3.2 UI重构:从WinForms迁移到Avalonia UI

Avalonia UI的XAML语法和WPF几乎一模一样,如果你之前用过WPF,迁移成本极低;如果你之前只用过WinForms,也不用担心,Avalonia UI的控件非常全,工业场景常用的按钮、文本框、图表、仪表盘、实时曲线控件都有,而且还有很多开源的工业控件库,比如Avalonia.Controls.DataGrid(官方数据网格)、LiveChartsCore.SkiaSharpView.Avalonia(开源实时图表库,性能非常好)。

这里给大家分享一个简单的工业上位机UI示例,包含按钮、文本框、实时曲线控件:

<!-- MainWindow.axaml -->
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:vm="using:IndustrialHMI.ViewModels"
        xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
        xmlns:lcs="using:LiveChartsCore.SkiaSharpView"
        xmlns:lc="using:LiveChartsCore"
        x:Class="IndustrialHMI.Views.MainWindow"
        Title="工业上位机(国产化适配版)"
        Width="1280"
        Height="720">
    <Design.DataContext>
        <vm:MainWindowViewModel/>
    </Design.DataContext>
    <Grid RowDefinitions="Auto,*,Auto">
        <!-- 顶部菜单栏 -->
        <Menu Grid.Row="0">
            <MenuItem Header="文件">
                <MenuItem Header="退出" Command="{Binding ExitCommand}"/>
            </MenuItem>
            <MenuItem Header="设置">
                <MenuItem Header="PLC连接设置" Command="{Binding PlcSettingsCommand}"/>
                <MenuItem Header="数据库连接设置" Command="{Binding DbSettingsCommand}"/>
            </MenuItem>
            <MenuItem Header="帮助">
                <MenuItem Header="关于" Command="{Binding AboutCommand}"/>
            </MenuItem>
        </Menu>
        <!-- 中间内容区 -->
        <Grid Grid.Row="1" ColumnDefinitions="*,*" Margin="10">
            <!-- 左侧:PLC数据显示区 -->
            <Border Grid.Column="0" BorderBrush="Gray" BorderThickness="1" Padding="10">
                <StackPanel>
                    <TextBlock Text="PLC数据显示区" FontSize="18" FontWeight="Bold" Margin="0,0,0,10"/>
                    <Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" Margin="0,0,0,10">
                        <TextBlock Grid.Row="0" Grid.Column="0" Text="PLC连接状态:" Margin="0,5"/>
                        <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding PlcConnectionStatus}" Foreground="{Binding PlcConnectionStatusColor}" Margin="0,5"/>
                        <TextBlock Grid.Row="1" Grid.Column="0" Text="温度传感器1:" Margin="0,5"/>
                        <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Temperature1, Mode=OneWay}" IsReadOnly="True" Margin="0,5"/>
                        <TextBlock Grid.Row="2" Grid.Column="0" Text="温度传感器2:" Margin="0,5"/>
                        <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Temperature2, Mode=OneWay}" IsReadOnly="True" Margin="0,5"/>
                        <TextBlock Grid.Row="3" Grid.Column="0" Text="压力传感器:" Margin="0,5"/>
                        <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Pressure, Mode=OneWay}" IsReadOnly="True" Margin="0,5"/>
                    </Grid>
                    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                        <Button Content="连接PLC" Command="{Binding ConnectPlcCommand}" Margin="5"/>
                        <Button Content="断开PLC" Command="{Binding DisconnectPlcCommand}" Margin="5"/>
                        <Button Content="读取数据" Command="{Binding ReadPlcDataCommand}" Margin="5"/>
                    </StackPanel>
                </StackPanel>
            </Border>
            <!-- 右侧:实时曲线区 -->
            <Border Grid.Column="1" BorderBrush="Gray" BorderThickness="1" Padding="10">
                <StackPanel>
                    <TextBlock Text="温度实时曲线" FontSize="18" FontWeight="Bold" Margin="0,0,0,10"/>
                    <lvc:CartesianChart Series="{Binding TemperatureSeries}" XAxes="{Binding XAxes}" YAxes="{Binding YAxes}" Height="400"/>
                </StackPanel>
            </Border>
        </Grid>
        <!-- 底部状态栏 -->
        <StatusBar Grid.Row="2">
            <StatusBarItem>
                <TextBlock Text="{Binding StatusBarText}"/>
            </StatusBarItem>
            <StatusBarItem HorizontalAlignment="Right">
                <TextBlock Text="{Binding CurrentTime}"/>
            </StatusBarItem>
        </StatusBar>
    </Grid>
</Window>

3.3 代码迁移:从.NET Framework 4.8迁移到.NET 8

.NET 8和.NET Framework 4.8的语法几乎一模一样,大部分代码都可以直接复制过来,只需要修改几个地方:

  1. Win32 API调用:.NET Framework 4.8里常用的Win32 API,比如SetWindowPosShowWindow,在.NET 8里可以用P/Invoke继续调用,但如果是跨平台的项目,建议用Avalonia UI的原生API代替;
  2. 数据库连接:从SQL Server的System.Data.SqlClient迁移到达梦8的DmProvider,语法几乎一模一样,只需要修改连接字符串和命名空间:
    // .NET Framework 4.8 + SQL Server
    using System.Data.SqlClient;
    string connectionString = "Server=192.168.1.100;Database=IndustrialDB;User ID=sa;Password=123456;";
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        // 执行SQL语句
    }
    
    // .NET 8 + 达梦8
    using DmProvider;
    string connectionString = "Server=192.168.1.100;Port=5236;Database=IndustrialDB;User ID=SYSDBA;Password=SYSDBA001;";
    using (DmConnection connection = new DmConnection(connectionString))
    {
        connection.Open();
        // 执行SQL语句,和SQL Server几乎一模一样
    }
    
  3. 多线程编程:.NET Framework 4.8里常用的BackgroundWorker,在.NET 8里建议用Task Parallel Library (TPL) 或者System.Threading.Channels代替,性能更好、更易维护;
  4. 配置文件:从.NET Framework 4.8的App.config迁移到.NET 8的appsettings.json,更灵活、更易读。

3.4 性能优化:让Avalonia UI在统信UOS上的性能比Windows上还好

很多技术团队担心Avalonia UI在统信UOS上的性能不如WinForms/WPF在Windows上的性能,其实只要做几个简单的优化,Avalonia UI在统信UOS上的性能完全可以超过WinForms/WPF在Windows上的性能:

  1. 启用AOT编译:.NET 8支持AOT(Ahead-of-Time)编译,可以把C#代码直接编译成机器码,不需要JIT(Just-in-Time)编译,启动速度提升3-5倍,运行速度提升1-2倍:
    <!-- IndustrialHMI.csproj -->
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <!-- 启用AOT编译 -->
        <PublishAot>true</PublishAot>
        <!-- 启用Trim(裁剪未使用的代码,减小程序体积) -->
        <TrimMode>link</TrimMode>
        <!-- 启用SelfContained(自包含部署,不需要目标机器安装.NET 8) -->
        <SelfContained>true</SelfContained>
        <!-- 目标运行时标识符(统信UOS x64用linux-x64,统信UOS ARM64用linux-arm64) -->
        <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Avalonia" Version="11.1.3"/>
        <PackageReference Include="Avalonia.Desktop" Version="11.1.3"/>
        <PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3"/>
        <PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3"/>
        <PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2"/>
        <PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-beta.950"/>
        <PackageReference Include="DmProvider" Version="8.2.1.19280"/>
        <PackageReference Include="Modbus.Device" Version="3.0.78"/>
      </ItemGroup>
    </Project>
    
  2. 优化实时曲线控件:工业场景常用的实时曲线控件,是性能瓶颈的重灾区,建议用LiveChartsCore.SkiaSharpView.Avalonia,它是基于SkiaSharp的,性能非常好,而且支持数据采样,当数据量很大的时候,会自动采样显示,不会卡顿;
  3. 优化数据绑定:工业场景常用的MVVM模式,数据绑定是性能瓶颈的另一个重灾区,建议用CommunityToolkit.MvvmObservableObjectRelayCommand,性能比自己实现的INotifyPropertyChangedICommand好很多;
  4. 避免频繁的UI更新:工业场景常用的PLC数据读取,一般是100ms-500ms读取一次,不要每次读取都更新UI,建议用System.Threading.Channels或者Timer,每1s-2s更新一次UI,既不会影响用户体验,又能大幅提升性能。

四、国产PLC通信:从西门子S7-1500迁移到汇川AM600

很多旧项目用的是西门子S7-1200/S7-1500,迁移到国产PLC的时候,最担心的就是通信协议不兼容,其实完全不用担心,现在的国产PLC,比如汇川、台达、信捷,都和西门子S7-1200/S7-1500的Modbus TCP协议、甚至S7协议都兼容,迁移成本极低。

这一节我把完整的国产PLC通信方案全部分享出来,用的是Modbus.Device开源库,支持所有主流的国产PLC和外资PLC,一次开发终身复用。

4.1 安装Modbus.Device开源库

Modbus.Device是一个非常成熟的Modbus通信开源库,支持Modbus TCP、Modbus RTU、Modbus ASCII,支持所有主流的国产PLC和外资PLC,用NuGet安装非常简单:

dotnet add package Modbus.Device

4.2 汇川AM600 Modbus TCP通信代码

这里给大家分享一个完整的汇川AM600 Modbus TCP通信代码,包含连接、断开、读取保持寄存器、写入保持寄存器:

// ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Modbus.Device;
using System;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace IndustrialHMI.ViewModels
{
    public partial class MainWindowViewModel : ObservableObject
    {
        // PLC连接参数
        private string _plcIp = "192.168.1.200";
        private int _plcPort = 502;
        private byte _plcSlaveId = 1;

        // Modbus TCP客户端
        private TcpClient? _tcpClient;
        private ModbusIpMaster? _modbusMaster;

        // PLC连接状态
        [ObservableProperty]
        private string _plcConnectionStatus = "未连接";

        [ObservableProperty]
        private string _plcConnectionStatusColor = "Red";

        // PLC数据
        [ObservableProperty]
        private string _temperature1 = "0.0";

        [ObservableProperty]
        private string _temperature2 = "0.0";

        [ObservableProperty]
        private string _pressure = "0.0";

        // 状态栏文本
        [ObservableProperty]
        private string _statusBarText = "就绪";

        // 当前时间
        [ObservableProperty]
        private string _currentTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");

        // 定时器,用于更新当前时间和读取PLC数据
        private System.Threading.Timer? _timer;

        public MainWindowViewModel()
        {
            // 初始化定时器,每1s更新一次当前时间
            _timer = new System.Threading.Timer(UpdateCurrentTime, null, 0, 1000);
        }

        // 更新当前时间
        private void UpdateCurrentTime(object? state)
        {
            CurrentTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        }

        // 连接PLC命令
        [RelayCommand]
        private async Task ConnectPlc()
        {
            try
            {
                StatusBarText = "正在连接PLC...";
                // 创建TCP客户端
                _tcpClient = new TcpClient();
                // 连接PLC
                await _tcpClient.ConnectAsync(_plcIp, _plcPort);
                // 创建Modbus TCP主站
                _modbusMaster = ModbusIpMaster.CreateIp(_tcpClient);
                // 更新PLC连接状态
                PlcConnectionStatus = "已连接";
                PlcConnectionStatusColor = "Green";
                StatusBarText = "PLC连接成功";
                // 启动定时器,每500ms读取一次PLC数据
                _timer?.Change(0, 500);
            }
            catch (Exception ex)
            {
                PlcConnectionStatus = "连接失败";
                PlcConnectionStatusColor = "Red";
                StatusBarText = $"PLC连接失败:{ex.Message}";
            }
        }

        // 断开PLC命令
        [RelayCommand]
        private void DisconnectPlc()
        {
            try
            {
                StatusBarText = "正在断开PLC...";
                // 停止定时器
                _timer?.Change(Timeout.Infinite, Timeout.Infinite);
                // 关闭Modbus TCP主站
                _modbusMaster?.Dispose();
                // 关闭TCP客户端
                _tcpClient?.Close();
                _tcpClient?.Dispose();
                // 更新PLC连接状态
                PlcConnectionStatus = "未连接";
                PlcConnectionStatusColor = "Red";
                StatusBarText = "PLC断开成功";
            }
            catch (Exception ex)
            {
                StatusBarText = $"PLC断开失败:{ex.Message}";
            }
        }

        // 读取PLC数据命令
        [RelayCommand]
        private async Task ReadPlcData()
        {
            if (_modbusMaster == null || !_tcpClient?.Connected == true)
            {
                StatusBarText = "PLC未连接,无法读取数据";
                return;
            }

            try
            {
                StatusBarText = "正在读取PLC数据...";
                // 读取保持寄存器,地址从40001开始,读取6个寄存器(温度1、温度2、压力各占2个寄存器,因为是浮点数)
                ushort[] registers = await Task.Run(() => _modbusMaster.ReadHoldingRegisters(_plcSlaveId, 0, 6));
                // 转换浮点数(Modbus里的浮点数一般是IEEE 754单精度浮点数,占2个寄存器,高字节在前)
                float temp1 = ConvertRegistersToFloat(registers[0], registers[1]);
                float temp2 = ConvertRegistersToFloat(registers[2], registers[3]);
                float pressure = ConvertRegistersToFloat(registers[4], registers[5]);
                // 更新PLC数据
                Temperature1 = temp1.ToString("F2");
                Temperature2 = temp2.ToString("F2");
                Pressure = pressure.ToString("F2");
                StatusBarText = "PLC数据读取成功";
            }
            catch (Exception ex)
            {
                StatusBarText = $"PLC数据读取失败:{ex.Message}";
            }
        }

        // 转换两个保持寄存器为IEEE 754单精度浮点数
        private float ConvertRegistersToFloat(ushort high, ushort low)
        {
            byte[] bytes = new byte[4];
            bytes[0] = (byte)(high >> 8);
            bytes[1] = (byte)(high & 0xFF);
            bytes[2] = (byte)(low >> 8);
            bytes[3] = (byte)(low & 0xFF);
            return BitConverter.ToSingle(bytes, 0);
        }

        // 退出命令
        [RelayCommand]
        private void Exit()
        {
            DisconnectPlc();
            _timer?.Dispose();
            Environment.Exit(0);
        }

        // PLC连接设置命令
        [RelayCommand]
        private void PlcSettings()
        {
            // 这里可以打开一个PLC连接设置窗口
            StatusBarText = "打开PLC连接设置窗口";
        }

        // 数据库连接设置命令
        [RelayCommand]
        private void DbSettings()
        {
            // 这里可以打开一个数据库连接设置窗口
            StatusBarText = "打开数据库连接设置窗口";
        }

        // 关于命令
        [RelayCommand]
        private void About()
        {
            // 这里可以打开一个关于窗口
            StatusBarText = "打开关于窗口";
        }
    }
}

五、等保2.0适配:满足国企/政务项目的合规要求

等保2.0三级以上的测评,是国企/政务信创项目的硬性要求,很多技术团队做信创适配,最后都卡在等保2.0测评上。这一节我把C#上位机等保2.0三级以上的核心适配要点全部分享出来,能直接帮你通过等保2.0测评。

等保2.0三级以上的核心要求有5个:身份认证、权限管理、日志审计、数据加密、边界防护,我们逐一来看:

5.1 身份认证

等保2.0三级以上要求,必须有强身份认证机制,不能只用用户名和密码,建议用用户名+密码+短信验证码或者用户名+密码+UKey的方式。

这里给大家分享一个简单的用户名+密码+短信验证码的身份认证示例:

// ViewModels/LoginWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Threading.Tasks;

namespace IndustrialHMI.ViewModels
{
    public partial class LoginWindowViewModel : ObservableObject
    {
        // 用户名
        [ObservableProperty]
        private string _username = "";

        // 密码
        [ObservableProperty]
        private string _password = "";

        // 短信验证码
        [ObservableProperty]
        private string _smsCode = "";

        // 状态栏文本
        [ObservableProperty]
        private string _statusBarText = "请输入用户名、密码和短信验证码";

        // 登录命令
        [RelayCommand]
        private async Task Login()
        {
            if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password) || string.IsNullOrEmpty(SmsCode))
            {
                StatusBarText = "用户名、密码和短信验证码不能为空";
                return;
            }

            try
            {
                StatusBarText = "正在验证身份...";
                // 1. 验证用户名和密码(从达梦8数据库里查询)
                // 2. 验证短信验证码(从短信平台里查询)
                // 3. 验证成功后,打开主窗口
                await Task.Delay(1000); // 模拟验证过程
                StatusBarText = "身份验证成功";
                // 这里可以打开主窗口
            }
            catch (Exception ex)
            {
                StatusBarText = $"身份验证失败:{ex.Message}";
            }
        }

        // 发送短信验证码命令
        [RelayCommand]
        private async Task SendSmsCode()
        {
            if (string.IsNullOrEmpty(Username))
            {
                StatusBarText = "请先输入用户名";
                return;
            }

            try
            {
                StatusBarText = "正在发送短信验证码...";
                // 1. 从达梦8数据库里查询用户的手机号
                // 2. 调用短信平台的API,发送短信验证码
                // 3. 把短信验证码存入达梦8数据库,设置有效期为5分钟
                await Task.Delay(1000); // 模拟发送过程
                StatusBarText = "短信验证码发送成功,有效期5分钟";
            }
            catch (Exception ex)
            {
                StatusBarText = $"短信验证码发送失败:{ex.Message}";
            }
        }
    }
}

5.2 权限管理

等保2.0三级以上要求,必须有细粒度的权限管理机制,不能只有管理员和普通用户两个角色,建议用基于角色的访问控制(RBAC) 模式,比如管理员、工程师、操作员、访客四个角色,不同的角色有不同的操作权限:

  • 管理员:所有权限,包括用户管理、权限管理、系统设置、数据备份、数据恢复;
  • 工程师:PLC连接设置、数据库连接设置、参数调整、数据查询、数据导出;
  • 操作员:PLC数据读取、实时曲线查看、异常告警确认;
  • 访客:只能查看实时曲线和PLC数据,不能做任何操作。

这里给大家分享一个简单的基于角色的访问控制(RBAC)示例:

// Models/Role.cs
namespace IndustrialHMI.Models
{
    public enum Role
    {
        Admin,
        Engineer,
        Operator,
        Guest
    }
}

// Models/User.cs
namespace IndustrialHMI.Models
{
    public class User
    {
        public string Username { get; set; } = "";
        public string PasswordHash { get; set; } = "";
        public string PhoneNumber { get; set; } = "";
        public Role Role { get; set; } = Role.Guest;
    }
}

// Services/IPermissionService.cs
namespace IndustrialHMI.Services
{
    public interface IPermissionService
    {
        bool HasPermission(User user, string permission);
    }
}

// Services/PermissionService.cs
using IndustrialHMI.Models;
using System.Collections.Generic;

namespace IndustrialHMI.Services
{
    public class PermissionService : IPermissionService
    {
        // 权限字典,key是角色,value是该角色拥有的权限列表
        private readonly Dictionary<Role, List<string>> _permissions = new Dictionary<Role, List<string>>
        {
            {
                Role.Admin,
                new List<string>
                {
                    "UserManagement", "PermissionManagement", "SystemSettings", "DataBackup", "DataRecovery",
                    "PlcSettings", "DbSettings", "ParameterAdjustment", "DataQuery", "DataExport",
                    "PlcDataRead", "RealTimeCurveView", "AlarmConfirmation"
                }
            },
            {
                Role.Engineer,
                new List<string>
                {
                    "PlcSettings", "DbSettings", "ParameterAdjustment", "DataQuery", "DataExport",
                    "PlcDataRead", "RealTimeCurveView", "AlarmConfirmation"
                }
            },
            {
                Role.Operator,
                new List<string>
                {
                    "PlcDataRead", "RealTimeCurveView", "AlarmConfirmation"
                }
            },
            {
                Role.Guest,
                new List<string>
                {
                    "PlcDataRead", "RealTimeCurveView"
                }
            }
        };

        public bool HasPermission(User user, string permission)
        {
            if (user == null || !_permissions.ContainsKey(user.Role))
            {
                return false;
            }
            return _permissions[user.Role].Contains(permission);
        }
    }
}

5.3 日志审计

等保2.0三级以上要求,必须有完整的日志审计机制,所有用户的操作、所有系统的事件,都必须有日志记录,日志必须留存6个月以上,不能被篡改。

这里给大家分享一个简单的日志审计示例,用的是Serilog开源库,支持日志写入达梦8数据库、日志加密、日志留存:

# 安装Serilog开源库
dotnet add package Serilog
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.Dm

5.4 数据加密

等保2.0三级以上要求,必须有数据加密机制,敏感数据(比如用户密码、PLC参数、生产数据)必须加密存储,传输过程中必须加密传输(用HTTPS/TLS)。

这里给大家分享一个简单的数据加密示例,用的是AES-256对称加密算法:

// Services/IEncryptionService.cs
namespace IndustrialHMI.Services
{
    public interface IEncryptionService
    {
        string Encrypt(string plainText);
        string Decrypt(string cipherText);
    }
}

// Services/EncryptionService.cs
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace IndustrialHMI.Services
{
    public class EncryptionService : IEncryptionService
    {
        // AES-256密钥和IV,生产环境建议从配置文件里读取,或者用UKey存储
        private readonly byte[] _key = Encoding.UTF8.GetBytes("12345678901234567890123456789012"); // 32字节
        private readonly byte[] _iv = Encoding.UTF8.GetBytes("1234567890123456"); // 16字节

        public string Encrypt(string plainText)
        {
            using (Aes aes = Aes.Create())
            {
                aes.Key = _key;
                aes.IV = _iv;
                ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
                using (MemoryStream ms = new MemoryStream())
                {
                    using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter sw = new StreamWriter(cs))
                        {
                            sw.Write(plainText);
                        }
                    }
                    return Convert.ToBase64String(ms.ToArray());
                }
            }
        }

        public string Decrypt(string cipherText)
        {
            using (Aes aes = Aes.Create())
            {
                aes.Key = _key;
                aes.IV = _iv;
                ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
                using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(cipherText)))
                {
                    using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                    {
                        using (StreamReader sr = new StreamReader(cs))
                        {
                            return sr.ReadToEnd();
                        }
                    }
                }
            }
        }
    }
}

5.5 边界防护

等保2.0三级以上要求,必须有边界防护机制,上位机和PLC、上位机和数据库之间的通信,必须有防火墙规则,只允许指定的IP地址和端口访问。

这里给大家分享一个简单的边界防护示例,在统信UOS上用ufw防火墙配置规则:

# 安装ufw防火墙
sudo apt install ufw -y
# 启用ufw防火墙
sudo ufw enable
# 允许SSH访问(远程管理用)
sudo ufw allow 22/tcp
# 允许上位机访问PLC(只允许上位机的IP地址192.168.1.100访问PLC的IP地址192.168.1.200的端口502)
sudo ufw allow from 192.168.1.100 to 192.168.1.200 port 502/tcp
# 允许上位机访问达梦8数据库(只允许上位机的IP地址192.168.1.100访问达梦8数据库的IP地址192.168.1.101的端口5236)
sudo ufw allow from 192.168.1.100 to 192.168.1.101 port 5236/tcp
# 拒绝所有其他入站访问
sudo ufw default deny incoming
# 允许所有出站访问
sudo ufw default allow outgoing
# 查看防火墙规则
sudo ufw status

六、生产环境部署:可直接交付的自包含部署方案

生产环境部署,建议用自包含部署(SelfContained) 方案,不需要目标机器安装.NET 8,只需要把编译好的程序复制过去就能运行,非常方便。

6.1 编译自包含部署包

用AOT编译和自包含部署,一条命令就能搞定:

# 编译统信UOS x64版本的自包含部署包
dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishAot=true -p:TrimMode=link -o ./publish/linux-x64
# 编译统信UOS ARM64版本的自包含部署包
dotnet publish -c Release -r linux-arm64 --self-contained true -p:PublishAot=true -p:TrimMode=link -o ./publish/linux-arm64

6.2 部署到统信UOS生产环境

部署到统信UOS生产环境非常简单,只需要把编译好的程序复制过去,设置执行权限,就能运行:

# 1. 把编译好的程序复制到统信UOS生产环境的/opt/IndustrialHMI目录
# 2. 设置执行权限
sudo chmod +x /opt/IndustrialHMI/IndustrialHMI
# 3. 运行程序
/opt/IndustrialHMI/IndustrialHMI
# 4. 如果需要开机自启动,可以创建一个systemd服务
sudo nano /etc/systemd/system/industrial-hmi.service
# 写入以下内容
[Unit]
Description=工业上位机(国产化适配版)
After=network.target

[Service]
Type=notify
WorkingDirectory=/opt/IndustrialHMI
ExecStart=/opt/IndustrialHMI/IndustrialHMI
Restart=always
RestartSec=5
User=root

[Install]
WantedBy=multi-user.target
# 保存退出,按Ctrl+O,然后按Enter,再按Ctrl+X
# 5. 启用systemd服务
sudo systemctl daemon-reload
sudo systemctl enable industrial-hmi.service
sudo systemctl start industrial-hmi.service
# 6. 查看服务状态
sudo systemctl status industrial-hmi.service

七、避坑指南:90%的信创适配项目都栽在这里

过去2年,我做了10+信创适配项目,见过太多项目从信心满满开始,最后烂尾收场,把最常见的6个坑总结出来,帮你少走至少半年的弯路。

坑1:上来就重构整个项目,成本太高、周期太长

很多技术团队做信创适配,第一步就想把整个项目从.NET Framework 4.8 + WinForms重构到.NET 8 + Avalonia UI,结果成本太高、周期太长,客户根本等不及。
解决方案:采用渐进式迁移方案,先把核心功能(比如PLC通信、数据库对接)迁移到.NET 8 + Avalonia UI,非核心功能(比如报表生成、历史数据查询)可以先用Wine在统信UOS上运行.NET Framework 4.8的程序,等核心功能稳定运行后,再逐步迁移非核心功能。

坑2:忽略国产硬件的指令集差异,性能比Windows上慢3倍

很多技术团队做信创适配,只在Intel/AMD x86架构的统信UOS上测试,结果到了鲲鹏/昇腾ARM64架构的统信UOS上,性能比Windows上慢3倍,根本没法用。
解决方案:项目开始前,就确定客户的生产环境硬件架构,在对应的硬件架构上做开发和测试;用.NET 8的AOT编译和Trim,针对不同的硬件架构优化代码;用开源的性能测试工具(比如BenchmarkDotNet),测试不同硬件架构上的性能,找出性能瓶颈,针对性优化。

坑3:忽略国产PLC的通信协议细节,丢包率高达1.2%

很多技术团队做信创适配,觉得国产PLC和外资PLC的Modbus TCP协议完全一样,结果到了产线上,丢包率高达1.2%,根本不敢用。
解决方案:项目开始前,就拿到客户的国产PLC硬件样机,做详细的通信协议测试;用成熟的Modbus通信开源库(比如Modbus.Device、NModbus),不要自己写Modbus通信协议;设置合理的通信超时时间和重试次数,降低丢包率。

坑4:忽略国产数据库的语法差异,迁移成本太高

很多技术团队做信创适配,觉得国产数据库和SQL Server/Oracle的语法完全一样,结果到了迁移的时候,发现很多SQL语句都不兼容,迁移成本太高。
解决方案:项目开始前,就确定客户的生产环境国产数据库,用对应的国产数据库做开发和测试;用成熟的ORM框架(比如Entity Framework Core),屏蔽不同数据库的语法差异;提前做SQL语句的兼容性测试,找出不兼容的SQL语句,针对性修改。

坑5:忽略等保2.0测评,最后卡在测评上

很多技术团队做信创适配,只关注功能迁移和性能优化,忽略等保2.0测评,结果到了最后,卡在等保2.0测评上,项目无法验收。
解决方案:项目开始前,就和客户的等保测评师沟通,明确等保2.0三级以上的核心要求;在开发过程中,同步做等保2.0适配;项目完成后,提前做等保2.0预测评,找出不符合要求的地方,针对性修改。

坑6:忽略后期维护服务,客户满意度低

很多技术团队做信创适配,项目验收后就不管了,结果客户的运维人员不会用,出了问题找不到人,客户满意度低,后续的项目也不会再找你。
解决方案:项目验收后,提供3-6个月的免费质保期;质保期结束后,提供付费的年度维护服务;给客户的运维人员做详细的培训,让他们能独立解决简单问题;建立快速响应机制,24小时内响应,48小时内解决问题。

结尾

C#上位机的全栈国产化适配,从来不是一个简单的“换个系统、换个框架”的问题,而是一套完整的、从底层硬件到上层应用的全栈适配工程,要解决跨平台兼容性、国产硬件适配、国产工业软件适配、等保2.0合规性等多个核心问题。

这篇文章里的所有方案、代码、优化技巧,都经过了10+信创适配项目的验证,你只要跟着做,就能落地一套可直接复制、可直接交付国企项目的C#上位机全栈国产化适配方案。后续我会继续更新C#上位机信创适配的进阶内容,包括多相机联动、3D视觉检测、国产化MES系统联动等内容,欢迎大家关注我的CSDN主页,有任何信创适配项目的问题,都可以在评论区留言,我会一一回复。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐