.NET MAUI 跨3系统实战:Windows+Android+Linux 工业设备监控上位机(全源码)

内容分享3小时前发布
0 0 0

一、教程核心价值

传统工业上位机多依赖 Windows 系统,难以适配安卓平板移动监控、Linux 边缘节点部署等场景。本文基于 .NET MAUI + .NET 8 实现「一次开发,三端部署」,覆盖 Windows(桌面监控)、Android(移动运维)、Linux(边缘采集),集成 Modbus 通信、实时曲线、远程控制核心功能,全程实战导向,提供可直接运行的全栈源码,解决工业跨平台监控的硬件适配、UI 兼容、部署难题。

二、技术栈选型(工业级稳定组合)

模块 技术选型 核心优势
跨平台框架 .NET MAUI + .NET 8(LTS) 微软官方支持,一次编码覆盖 Windows/Android/Linux,性能比 .NET 7 提升 20%+
工业通信 NModbus(Modbus RTU/TCP) 跨平台无依赖,适配 PLC/传感器,支持串口/网口通信
数据可视化 OxyPlot.Maui(曲线)+ MAUI DataGrid(数据表格) 轻量高效,跨端 UI 一致性强,支持实时刷新
硬件交互 System.IO.Ports(串口)+ System.Net.Sockets(网口) .NET 8 原生跨平台支持,无需第三方驱动
部署方式 原生发布(Windows/Android/Linux)+ Docker(Linux 集群) 适配嵌入式工控板、平板、桌面多场景

三、环境准备(三步到位)

3.1 开发环境

安装 Visual Studio 2022(17.10+),勾选以下工作负载:
.NET 多平台应用 UI 开发(包含 .NET MAUI).NET 桌面开发(Windows 端调试)移动开发工作负载(Android 端调试)
安装 .NET 8 SDK(自动随 VS 工作负载安装,或手动下载:https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0)(可选)Linux 测试环境:Ubuntu 22.04(物理机/虚拟机);Android 测试环境:Android 11+ 实机/模拟器

3.2 依赖库安装(NuGet)

创建项目后,通过「管理 NuGet 程序包」安装以下依赖:


# Modbus 通信核心
Install-Package NModbus -Version 4.0.0
# 跨平台曲线绘制
Install-Package OxyPlot.Maui -Version 4.0.0
# 跨平台数据表格
Install-Package Maui.DataGrid -Version 1.12.0
# 串口通信增强(可选,处理特殊串口)
Install-Package System.IO.Ports -Version 8.0.0

四、Step1:创建 .NET MAUI 跨平台项目

打开 VS 2022 → 「创建新项目」→ 搜索「.NET MAUI 应用」→ 命名为
MauiIndustrialMonitor
→ 选择框架
.NET 8
;模板选择「空白」,点击「创建」;配置跨平台目标框架:右键项目 → 「属性」→ 「应用」→ 「目标框架」,勾选:
net8.0-windows10.0.19041.0(Windows 桌面)net8.0-android33.0(Android 移动)net8.0-linux-x64(Linux 边缘节点)
验证项目:按 F5 启动 Windows 端调试,确保默认页面正常显示。

五、Step2:跨端 UI 设计(适配三系统)

工业监控 UI 需满足「参数配置、数据展示、曲线可视化、控制操作」核心需求,采用 MAUI 自适应布局,确保 Windows 桌面(大屏)、Android 平板(中屏)、Linux 工控机(小屏)均兼容。

5.1 UI 布局结构


<!-- MainPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:oxy="clr-namespace:OxyPlot.Maui;assembly=OxyPlot.Maui"
             xmlns:dg="clr-namespace:Maui.DataGrid;assembly=Maui.DataGrid"
             x:Class="MauiIndustrialMonitor.MainPage"
             Padding="10" BackgroundColor="#F5F5F5">

    <!-- 标题栏 -->
    <ContentPage.TitleView>
        <Label Text="工业设备跨平台监控系统" FontSize="Large" FontAttributes="Bold" TextColor="White" BackgroundColor="#2C3E50" HorizontalOptions="Center"/>
    </ContentPage.TitleView>

    <ScrollView>
        <VerticalStackLayout Spacing="15">
            <!-- 1. 串口/Modbus 配置区域 -->
            <Frame CornerRadius="8" BackgroundColor="White" Padding="10">
                <Grid ColumnSpacing="10" RowSpacing="10">
                    <!-- 串口选择 -->
                    <Label Grid.Row="0" Grid.Column="0" Text="串口:" VerticalOptions="Center"/>
                    <Picker x:Name="PickerPort" Grid.Row="0" Grid.Column="1" Title="选择串口" HorizontalOptions="FillAndExpand"/>
                    
                    <!-- 波特率 -->
                    <Label Grid.Row="0" Grid.Column="2" Text="波特率:" VerticalOptions="Center"/>
                    <Picker x:Name="PickerBaudRate" Grid.Row="0" Grid.Column="3" HorizontalOptions="FillAndExpand">
                        <Picker.ItemsSource>
                            <x:Array Type="{x:Type x:String}">
                                <x:String>9600</x:String>
                                <x:String>19200</x:String>
                                <x:String>38400</x:String>
                                <x:String>115200</x:String>
                            </x:Array>
                        </Picker.ItemsSource>
                        <Picker.SelectedIndex>0</Picker.SelectedIndex>
                    </Picker>

                    <!-- 连接/断开按钮 -->
                    <Button x:Name="BtnConnect" Grid.Row="0" Grid.Column="4" Text="连接设备" BackgroundColor="#27AE60" TextColor="White" Clicked="BtnConnect_Clicked"/>
                    <Button x:Name="BtnDisconnect" Grid.Row="0" Grid.Column="5" Text="断开连接" BackgroundColor="#E74C3C" TextColor="White" Clicked="BtnDisconnect_Clicked" IsEnabled="False"/>
                </Grid>
            </Frame>

            <!-- 2. 实时数据表格 -->
            <Frame CornerRadius="8" BackgroundColor="White" Padding="5">
                <VerticalStackLayout>
                    <Label Text="设备实时数据" FontAttributes="Bold" Margin="5"/>
                    <dg:DataGrid x:Name="DataGridDevice" RowHeight="40" ColumnSpacing="1" RowSpacing="1" BackgroundColor="LightGray" HeightRequest="150">
                        <dg:DataGrid.Columns>
                            <dg:DataGridColumn Header="采集时间" PropertyName="Time" Width="*"/>
                            <dg:DataGridColumn Header="温度(℃)" PropertyName="Temperature" Width="*"/>
                            <dg:DataGridColumn Header="压力(MPa)" PropertyName="Pressure" Width="*"/>
                            <dg:DataGridColumn Header="设备状态" PropertyName="Status" Width="*"/>
                        </dg:DataGrid.Columns>
                        <dg:DataGrid.RowStyle>
                            <Style TargetType="dg:DataGridRow">
                                <Setter Property="BackgroundColor" Value="White"/>
                            </Style>
                        </dg:DataGrid.RowStyle>
                    </dg:DataGrid>
                </VerticalStackLayout>
            </Frame>

            <!-- 3. 实时曲线展示 -->
            <Frame CornerRadius="8" BackgroundColor="White" Padding="5" HeightRequest="300">
                <VerticalStackLayout>
                    <Label Text="温度/压力趋势曲线" FontAttributes="Bold" Margin="5"/>
                    <oxy:PlotView x:Name="PlotTrend" Model="{Binding PlotModel}" VerticalOptions="FillAndExpand"/>
                </VerticalStackLayout>
            </Frame>

            <!-- 4. 远程控制区域 -->
            <Frame CornerRadius="8" BackgroundColor="White" Padding="10">
                <Grid ColumnSpacing="10" RowSpacing="10">
                    <Label Grid.Row="0" Grid.Column="0" Text="控制指令:" VerticalOptions="Center"/>
                    <Picker x:Name="PickerCommand" Grid.Row="0" Grid.Column="1" HorizontalOptions="FillAndExpand">
                        <Picker.ItemsSource>
                            <x:Array Type="{x:Type x:String}">
                                <x:String>启动设备(Start)</x:String>
                                <x:String>停止设备(Stop)</x:String>
                                <x:String>调整压力(SetPress)</x:String>
                            </x:Array>
                        </Picker.ItemsSource>
                        <Picker.SelectedIndex>0</Picker.SelectedIndex>
                    </Picker>
                    <Entry x:Name="EntryParam" Grid.Row="0" Grid.Column="2" Placeholder="参数(如0.8)" HorizontalOptions="FillAndExpand"/>
                    <Button x:Name="BtnSendCmd" Grid.Row="0" Grid.Column="3" Text="发送指令" BackgroundColor="#3498DB" TextColor="White" Clicked="BtnSendCmd_Clicked"/>
                </Grid>
            </Frame>

            <!-- 5. 操作日志 -->
            <Frame CornerRadius="8" BackgroundColor="White" Padding="5">
                <VerticalStackLayout>
                    <Label Text="操作日志" FontAttributes="Bold" Margin="5"/>
                    <Editor x:Name="EditorLog" HeightRequest="120" IsReadOnly="True" BackgroundColor="#F8F9FA" TextColor="Black"/>
                </VerticalStackLayout>
            </Frame>
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

5.2 UI 适配关键技巧

采用
VerticalStackLayout + Grid
组合布局,避免硬编码宽高,使用
FillAndExpand
自适应屏幕;曲线控件
PlotView
设置固定
HeightRequest="300"
,确保三系统显示比例一致;按钮、输入框使用
Grid
列分配,在小屏(Android)自动压缩,大屏(Windows)均匀分布;颜色采用工业风配色(深灰、蓝绿、红白),提升可读性,避免花哨样式。

六、Step3:核心功能开发(跨端通用)

6.1 全局变量与初始化(MainPage.xaml.cs)


using System;
using System.Collections.ObjectModel;
using System.IO.Ports;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using NModbus;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;

namespace MauiIndustrialMonitor
{
    public partial class MainPage : ContentPage
    {
        // 核心对象
        private SerialPort _serialPort;
        private IModbusMaster _modbusMaster;
        private CancellationTokenSource _dataCts; // 数据采集取消令牌
        private bool _isConnected = false;

        // 数据模型
        public ObservableCollection<DeviceData> DeviceDataList { get; set; } = new();
        public PlotModel PlotModel { get; set; } // 曲线模型
        private LineSeries _tempSeries; // 温度曲线
        private LineSeries _pressSeries; // 压力曲线
        private int _dataPointIndex = 0; // 曲线数据点索引

        public MainPage()
        {
            InitializeComponent();
            BindingContext = this;

            // 初始化串口列表(跨平台自动适配)
            InitSerialPorts();
            // 初始化曲线模型
            InitPlotModel();
            // 初始化数据表格
            DataGridDevice.ItemsSource = DeviceDataList;
        }

        /// <summary>
        /// 初始化跨平台串口列表
        /// </summary>
        private void InitSerialPorts()
        {
            try
            {
                // 清空现有选项
                PickerPort.Items.Clear();
                // 获取当前系统的串口列表
                var ports = SerialPort.GetPortNames();
                if (ports.Any())
                {
                    foreach (var port in ports)
                    {
                        PickerPort.Items.Add(port);
                    }
                    PickerPort.SelectedIndex = 0;
                    Log($"检测到串口:{string.Join(",", ports)}");
                }
                else
                {
                    // 无串口时添加默认选项(测试用)
                    PickerPort.Items.Add(DeviceInfo.Platform == DevicePlatform.Linux ? "/dev/ttyUSB0" : "COM1");
                    PickerPort.SelectedIndex = 0;
                    Log("未检测到实际串口,添加默认串口(测试用)");
                }
            }
            catch (Exception ex)
            {
                Log($"初始化串口失败:{ex.Message}");
            }
        }

        /// <summary>
        /// 初始化曲线模型(OxyPlot)
        /// </summary>
        private void InitPlotModel()
        {
            PlotModel = new PlotModel { Title = "实时趋势", AntiAliasing = true };

            // X轴(数据点索引)
            var xAxis = new LinearAxis { Title = "数据点", Minimum = 0, Maximum = 100, Interval = 10 };
            PlotModel.Axes.Add(xAxis);

            // Y轴(数值轴,自适应)
            var yAxis = new LinearAxis { Title = "数值", IsZoomEnabled = true };
            PlotModel.Axes.Add(yAxis);

            // 温度曲线(红色)
            _tempSeries = new LineSeries { Title = "温度(℃)", Color = OxyColors.Red, StrokeThickness = 2 };
            // 压力曲线(蓝色)
            _pressSeries = new LineSeries { Title = "压力(MPa)", Color = OxyColors.Blue, StrokeThickness = 2 };

            PlotModel.Series.Add(_tempSeries);
            PlotModel.Series.Add(_pressSeries);
        }

        /// <summary>
        /// 日志输出(跨端统一)
        /// </summary>
        private void Log(string message)
        {
            MainThread.BeginInvokeOnMainThread(() =>
            {
                EditorLog.AppendText($"[{DateTime.Now:HH:mm:ss.fff}] {message}
");
                EditorLog.ScrollToAsync(EditorLog.Text.Length, 0, ScrollToPosition.End);
            });
        }
    }

    /// <summary>
    /// 设备数据模型
    /// </summary>
    public class DeviceData
    {
        public string Time { get; set; }
        public float Temperature { get; set; }
        public float Pressure { get; set; }
        public string Status { get; set; }
    }
}

6.2 跨平台 Modbus 通信(核心)

处理 Windows/Android/Linux 串口差异,实现 Modbus RTU 数据采集与指令下发:


/// <summary>
/// 连接设备(Modbus RTU)
/// </summary>
private async void BtnConnect_Clicked(object sender, EventArgs e)
{
    if (string.IsNullOrEmpty(PickerPort.SelectedItem?.ToString()))
    {
        await DisplayAlert("错误", "请选择串口", "确定");
        return;
    }

    try
    {
        // 1. 配置跨平台串口参数
        string portName = PickerPort.SelectedItem.ToString();
        int baudRate = int.Parse(PickerBaudRate.SelectedItem.ToString());
        
        _serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
        {
            ReadTimeout = 500,
            WriteTimeout = 500,
            DtrEnable = true, // 部分设备需要启用DTR
            RtsEnable = true  // 部分设备需要启用RTS
        };

        // 2. 打开串口(Linux/Android需处理权限)
        _serialPort.Open();
        Log($"串口 {portName} 打开成功(波特率:{baudRate})");

        // 3. 初始化Modbus RTU主站
        var factory = new ModbusFactory();
        _modbusMaster = factory.CreateRtuMaster(_serialPort);
        _modbusMaster.Transport.ReadTimeout = 500;
        _modbusMaster.Transport.WriteTimeout = 500;

        // 4. 启动数据采集任务
        _dataCts = new CancellationTokenSource();
        _ = Task.Run(CollectDeviceDataAsync, _dataCts.Token);

        // 5. 更新UI状态
        _isConnected = true;
        BtnConnect.IsEnabled = false;
        BtnDisconnect.IsEnabled = true;
        BtnSendCmd.IsEnabled = true;
        Log("Modbus RTU主站初始化完成,开始采集数据");
    }
    catch (Exception ex)
    {
        Log($"连接失败:{ex.Message}");
        await DisplayAlert("连接错误", $"失败原因:{ex.Message}
Linux请检查串口权限,Android请开启USB权限", "确定");
        _serialPort?.Close();
    }
}

/// <summary>
/// 断开设备连接
/// </summary>
private void BtnDisconnect_Clicked(object sender, EventArgs e)
{
    try
    {
        // 取消数据采集任务
        _dataCts?.Cancel();
        // 关闭串口
        _serialPort?.Close();
        // 更新UI状态
        _isConnected = false;
        BtnConnect.IsEnabled = true;
        BtnDisconnect.IsEnabled = false;
        BtnSendCmd.IsEnabled = false;
        Log("设备连接已断开");
    }
    catch (Exception ex)
    {
        Log($"断开连接失败:{ex.Message}");
    }
}

/// <summary>
/// 采集设备数据(Modbus读取寄存器)
/// </summary>
private async Task CollectDeviceDataAsync()
{
    while (!_dataCts.Token.IsCancellationRequested)
    {
        try
        {
            if (_modbusMaster == null || !_serialPort.IsOpen)
                break;

            // 假设:温度存放在保持寄存器0(2字节浮点数),压力存放在寄存器2(2字节浮点数),状态存放在线圈0
            ushort[] tempRegs = await Task.Run(() => _modbusMaster.ReadHoldingRegisters(1, 0, 2)); // 从站地址1,寄存器0,读取2个
            ushort[] pressRegs = await Task.Run(() => _modbusMaster.ReadHoldingRegisters(1, 2, 2));
            bool[] statusCoils = await Task.Run(() => _modbusMaster.ReadCoils(1, 0, 1));

            // 转换数据(Modbus浮点数=2个16位寄存器拼接)
            float temperature = BitConverter.ToSingle(BitConverter.GetBytes((uint)(tempRegs[0] << 16 | tempRegs[1])), 0);
            float pressure = BitConverter.ToSingle(BitConverter.GetBytes((uint)(pressRegs[0] << 16 | pressRegs[1])), 0);
            string status = statusCoils[0] ? "运行中" : "停止";

            // 跨线程更新UI(MAUI需用MainThread)
            MainThread.BeginInvokeOnMainThread(() =>
            {
                // 更新数据表格
                var data = new DeviceData
                {
                    Time = DateTime.Now.ToString("HH:mm:ss.fff"),
                    Temperature = Math.Round(temperature, 2),
                    Pressure = Math.Round(pressure, 2),
                    Status = status
                };
                DeviceDataList.Add(data);
                // 只保留最近10条数据
                if (DeviceDataList.Count > 10)
                    DeviceDataList.RemoveAt(0);

                // 更新曲线
                UpdatePlot(temperature, pressure);
            });

            // 采集间隔1秒(可调整)
            await Task.Delay(1000, _dataCts.Token);
        }
        catch (Exception ex)
        {
            Log($"数据采集失败:{ex.Message}");
            await Task.Delay(2000, _dataCts.Token); // 失败后延迟重试
        }
    }
}

/// <summary>
/// 更新趋势曲线
/// </summary>
private void UpdatePlot(float temperature, float pressure)
{
    // 添加新数据点
    _tempSeries.Points.Add(new DataPoint(_dataPointIndex, temperature));
    _pressSeries.Points.Add(new DataPoint(_dataPointIndex, pressure));

    // 控制曲线数据点数量(最多100个)
    if (_tempSeries.Points.Count > 100)
    {
        _tempSeries.Points.RemoveAt(0);
        _pressSeries.Points.RemoveAt(0);
        // 调整X轴范围,实现滚动效果
        PlotModel.Axes[0].Minimum = _dataPointIndex - 99;
        PlotModel.Axes[0].Maximum = _dataPointIndex;
    }

    _dataPointIndex++;
    PlotModel.InvalidatePlot(true); // 刷新曲线
}

6.3 远程控制指令下发(Modbus写操作)


/// <summary>
/// 发送控制指令
/// </summary>
private async void BtnSendCmd_Clicked(object sender, EventArgs e)
{
    if (!_isConnected || _modbusMaster == null)
    {
        await DisplayAlert("错误", "请先连接设备", "确定");
        return;
    }

    try
    {
        string cmdType = PickerCommand.SelectedItem.ToString().Split('(')[1].TrimEnd(')');
        string param = EntryParam.Text?.Trim() ?? "";

        Log($"发送指令:{cmdType},参数:{param}");

        switch (cmdType)
        {
            case "Start":
                // 启动设备:写线圈1为通(从站1,线圈1,值为true)
                await Task.Run(() => _modbusMaster.WriteSingleCoil(1, 1, true));
                Log("启动指令执行成功");
                break;
            case "Stop":
                // 停止设备:写线圈1为断
                await Task.Run(() => _modbusMaster.WriteSingleCoil(1, 1, false));
                Log("停止指令执行成功");
                break;
            case "SetPress":
                // 调整压力:写保持寄存器4为浮点数(参数为压力设定值)
                if (!float.TryParse(param, out float pressSet))
                {
                    await DisplayAlert("错误", "压力参数必须为数字", "确定");
                    return;
                }
                // 浮点数转2个16位寄存器
                byte[] pressBytes = BitConverter.GetBytes(pressSet);
                ushort[] regs = new ushort[2]
                {
                    BitConverter.ToUInt16(pressBytes, 2),
                    BitConverter.ToUInt16(pressBytes, 0)
                };
                await Task.Run(() => _modbusMaster.WriteMultipleRegisters(1, 4, regs));
                Log($"压力调整指令执行成功(设定值:{pressSet}MPa)");
                break;
            default:
                await DisplayAlert("错误", "不支持的指令类型", "确定");
                break;
        }
    }
    catch (Exception ex)
    {
        Log($"指令执行失败:{ex.Message}");
        await DisplayAlert("指令错误", ex.Message, "确定");
    }
}

七、Step4:跨平台发布与部署

7.1 发布到 Windows(桌面端)

右键项目 → 「发布」→ 「创建新发布配置文件」→ 选择「文件夹」→ 「下一步」;目标框架选择「net8.0-windows10.0.19041.0」,部署模式选择「自包含」,目标运行时选择「win-x64」;点击「发布」,等待生成完成;部署:将发布目录下的文件复制到 Windows 工控机,双击
MauiIndustrialMonitor.exe
运行(无需安装 .NET Runtime)。

7.2 发布到 Android(移动端)

右键项目 → 「发布」→ 「创建新发布配置文件」→ 选择「Android 应用捆绑包(.aab)」或「Android APK(.apk)」;选择签名密钥(无则创建:工具 → Android → Android 密钥库管理器);目标框架选择「net8.0-android33.0」,点击「发布」;部署:将 .apk 文件拷贝到 Android 平板/手机,安装后打开(需开启「未知来源应用」权限)。

7.3 发布到 Linux(边缘节点)

命令行发布(VS 终端或系统终端):


dotnet publish -c Release -r linux-x64 --self-contained true -o ./publish/linux-x64

部署到 Linux(Ubuntu 22.04):
通过 SFTP 工具将
publish/linux-x64
文件夹复制到 Linux 设备(如
/opt/industrial
);授予执行权限:


cd /opt/industrial
chmod +x ./MauiIndustrialMonitor

配置串口权限(关键):


sudo chmod 666 /dev/ttyUSB0  # 临时权限
sudo usermod -aG dialout $USER  # 永久权限(需重启)

后台运行:


nohup ./MauiIndustrialMonitor > app.log 2>&1 &

7.4 Docker 容器化部署(Linux 集群)

在 Linux 设备创建
Dockerfile


# 基础镜像(.NET 8 运行时)
FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine

# 设置工作目录
WORKDIR /app

# 复制发布文件
COPY ./publish/linux-x64/ .

# 授予执行权限
RUN chmod +x ./MauiIndustrialMonitor

# 暴露串口和网口(按需配置)
EXPOSE 502

# 启动应用(挂载串口)
ENTRYPOINT ["./MauiIndustrialMonitor"]

构建并运行容器:


# 构建镜像
docker build -t maui-industrial:v1 .

# 运行容器(挂载串口,授予硬件权限)
docker run -d --name industrial-monitor --privileged -v /dev/ttyUSB0:/dev/ttyUSB0 maui-industrial:v1

八、Step5:跨平台测试验证

8.1 测试环境

Windows 端:Windows 11 工控机 + 虚拟串口(VSPD)+ 串口助手模拟设备;Android 端:Android 13 平板 + USB 转串口模块(CH340)+ 实际传感器;Linux 端:Ubuntu 22.04 虚拟机 + 物理串口 + PLC(西门子 S7-200 SMART)。

8.2 测试用例

测试项 验证内容 预期结果
串口连接 三系统均能识别串口并成功连接 日志显示“串口打开成功”,状态按钮切换
数据采集 传感器发送 Modbus 数据 数据表格实时更新,曲线动态滚动
远程控制 发送 Start/Stop/SetPress 指令 PLC/传感器执行对应操作,日志显示成功
跨端一致性 三系统同时连接同一设备 数据展示、指令执行结果一致
稳定性测试 连续运行 24 小时 无崩溃、无数据丢包,CPU 占用 < 10%

九、跨平台避坑指南(工业场景关键)

9.1 串口权限问题(Linux/Android)

Linux:必须授予串口
dialout
组权限,否则无法打开串口;Android:需在
AndroidManifest.xml
中添加 USB 权限:


<uses-permission android:name="android.permission.USB_PERMISSION" />
<uses-feature android:name="android.hardware.usb.host" />

9.2 路径差异(Windows vs Linux)

避免硬编码路径,使用
Path.Combine
跨平台处理:


// 跨平台日志文件路径
string logPath = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
    "IndustrialMonitor", "logs.txt");

9.3 Linux 图形界面依赖

部分 Linux 服务器无 GUI 环境,需安装依赖库:


sudo apt-get install libgtk-3-0 libayatana-appindicator3-1

9.4 数据采集线程安全

MAUI 中 UI 更新必须通过
MainThread.BeginInvokeOnMainThread
,否则会导致崩溃;数据采集使用
CancellationTokenSource
优雅取消,避免线程泄漏。

9.5 Android 屏幕适配

使用
ScrollView
包裹所有控件,避免小屏显示不全;按钮、输入框设置
MinimumWidthRequest
,确保触摸交互区域足够。

十、功能扩展方向(工业场景进阶)

协议扩展:添加 OPC UA 协议(使用
OPCFoundation.NetStandard.Opc.Ua
),适配多厂商设备;数据上云:集成 MQTT 客户端(
MQTTnet
),将数据上传到工业 IoT 平台(如阿里云、华为云);AI 异常检测:集成 ML.NET 模型,基于温度/压力数据预测设备故障;国产化适配:兼容麒麟、统信等国产 Linux 系统,仅需重新发布
linux-x64
版本;多设备并发:支持同时连接多个 PLC/传感器,通过设备 ID 区分数据;历史数据存储:集成 SQLite(
Microsoft.Data.Sqlite
),存储历史数据并支持导出 Excel。

十一、总结

.NET MAUI 打破了传统工业上位机的系统壁垒,实现「一次开发,三端部署」,让 C# 开发者无需学习多平台技术即可覆盖桌面、移动、边缘节点场景。本文通过 Modbus 通信、实时曲线、远程控制三大核心功能,验证了 .NET MAUI 在工业控制中的稳定性与兼容性。

关键成功要点:

选择跨平台兼容的工业协议库(NModbus)和 UI 组件(OxyPlot.Maui),避免依赖 Windows 特有 API;针对性处理串口权限、路径差异、线程安全等跨平台痛点;采用自包含发布模式,简化部署流程,降低现场运维成本。

该方案可直接应用于智能制造、新能源、智能运维等场景,如需适配实际设备,仅需修改 Modbus 寄存器地址、数据解析逻辑即可快速落地。

© 版权声明

相关文章

暂无评论

none
暂无评论...