C#跨平台移动工业监控:从TCP Socket到MVVM的完整实现

C#跨平台移动工业监控:从TCP Socket到MVVM的完整实现 工业监控终端过去多数固定在中控室或工控机旁边现场问题往往要回到大屏前才能确认。要做移动跨平台工业监控C# 是一条很自然的技术路线因为不少工厂已有的数据采集和上位机代码都是 C# 写的。移动化之后巡检人员可以直接在手机上查看温度、压力、转速和设备状态异常发生时也能第一时间收到反馈。如果你正在做与 B1151 类似的课题下面这套设计可以当作一个可运行的起点。这篇文章以一个最小闭环为主线先用 C# 写一个模拟设备数据源通过 TCP 定时推送温度数据再用 Xamarin.Forms 写一个跨平台移动端 App完成连接、接收、解析、展示、告警和断线重连。读完这份实践后你可以把同一套思路迁移到 PLC、扫码枪、相机 SDK、MQTT 等更复杂的工业接入场景。1. 移动跨平台工业监控应用的需求边界与技术选型1.1 需求边界移动监控不是简单把桌面端缩小工业监控应用的核心需求通常包括五类实时数据查看温度、压力、液位、转速等过程量按秒级或毫秒级刷新。设备状态展示运行、停机、故障、维护等状态需要直观可见。告警提醒数据越限、设备异常时主动通知相关人员。历史查询查看趋势曲线、报表和操作记录。远程操作启动、停止、参数写入等控制类操作且必须有权限校验。桌面端上位机通常屏幕大、电源稳定、网络固定可以在一个界面放几十个变量。移动端则完全不同手机屏幕小、网络会抖动、系统会限制后台运行导致移动监控不是把上位机界面缩小而是需要重新设计信息的展示密度、交互方式和通信策略。在设计阶段就要想清楚三个问题移动端需要展示哪些数据不需要展示哪些数据。网络不稳定或断线时App 应该表现成什么样。告警是在设备端判断还是在服务端判断还是移动端自行判断。这些边界决定后续架构和数据流的设计方向。1.2 为什么选择 C# 跨平台方案C# 跨平台移动开发主要对应 Xamarin.Forms 和 .NET MAUI 两条路线。选择它不是因为跨平台技术本身有多热门而是因为它和很多工厂已有的 C# 技术栈高度重合。方案技术栈适合场景需要重点考虑的问题C# 跨平台C# XAML已有 C# 上位机、采集服务团队熟悉 C#需要学习 XAML 和 MVVM包体相对较大原生 Android 原生 iOSKotlin Swift追求最强系统特性和极致性能双端各写一套维护成本高Web / 混合方案HTML / JS展示型应用不需要强后台能力硬件访问受限离线体验和推送较弱FlutterDartUI 要求高且统一与 C# 服务端代码难以复用C# 上位机开发中的委托、事件、Socket、串口通讯、字节解析经验在移动端基本可以原样迁移。比如桌面端写过一个SerialPort.DataReceived事件处理程序到了移动端只是把串口换成 TcpClient处理逻辑依然是“收到字节 → 解析协议 → 触发事件 → 更新界面”。需要特别明确一点移动端不会直接去连接底层 PLC 或串口设备。跨平台移动系统对蓝牙、串口、USB 外设的支持差异很大典型做法是让移动端连接一个服务层服务层再去和设备通信。这样移动端协议栈保持稳定服务层负责适配各种设备。1.3 这篇文章的落点一个可复现的最小闭环为了让整个设计可学习、可复现文章采用一个最小闭环作为主线模拟数据源(TcpListener) ↓ 以 \n 结尾的 JSON 帧 移动端 Socket 客户端 ↓ 事件 ViewModel ↓ 数据绑定 UI 实时展示模拟数据源每 2 秒推送一条温度数据。移动端连接后持续接收解析成数据点刷新温度卡片。当温度超过阈值时界面进入告警状态。整个过程覆盖连接、接收、解析、绑定、告警五个核心环节也包含半包处理、跨线程更新、断线重连这些工业现场一定会遇到的问题。2. 三层通信模型把设备数据从现场送到手机屏幕2.1 设备层、服务层、移动端各自负责什么工业监控最怕“什么都想自己干”。如果移动端既要做协议解析又要处理设备差异还要保证多个厂家设备的兼容性工作量会迅速失控。典型的设计是三层模型。层级职责常见技术设备层PLC、传感器、仪表、扫码枪、相机负责产生原始数据Modbus TCP、FINS UDP、OPC UA、厂家 SDK服务层采集、缓存、协议转换、数据推送、鉴权控制C# 上位机、边缘网关、MQTT Broker移动端实时展示、告警、查询、操作入口Xamarin.Forms / .NET MAUI服务层承担两个重要职责一是把不同设备的协议统一成一种内部数据格式二是把移动端与复杂的工业协议隔离。这样设备换了协议服务层做适配移动端几乎不用改。2.2 通信方式对比TCP Socket、HTTP、WebSocket、MQTT移动端与服务层之间的通信方式直接影响实时性、弱网表现和开发复杂度。方式实时性典型场景注意事项TCP Socket高局域网内专有协议采集延迟低需要自己处理粘包/半包、心跳、断线重连HTTP 轮询低到中查询类接口周期请求实时性差频繁请求浪费流量WebSocket高双向高频推送适合界面实时刷新服务端需要支持 WebSocketMQTT高弱网、多设备、需要离线消息需要引入 Broker设计主题和 QoS这个 Demo 选择 TCP Socket有两点考虑一是 TCP 最接近工业现场。很多 PLC 和采集网关都提供 TCP 服务理解 TCP 的流式传输特性是后面做任何工业通讯的基础。二是 TCP 能直观地把“字节流 → 帧 → 对象”的链路讲清楚而这些知识在 Modbus、FINS 等协议里同样适用。如果目标是弱网环境下的大规模设备接入生产环境更推荐 MQTT。移动端订阅主题设备数据由服务层发布断线后可以保留消息补收功能比裸 TCP 容易实现。2.3 最小 Demo 的数据流设计定义一个最简单的 JSON 帧作为服务层与移动端的统一数据格式{tag:temperature,value:63.5,time:2025-01-01 10:22:33,alarm:false}字段说明字段含义示例tag数据点位标识temperaturevalue数值63.5time采集时间2025-01-01 10:22:33alarm是否越限告警false帧与帧之间用换行符\n分隔。为什么用换行符因为 JSON 本身包含{、、:等字符不能用这些字符作为边界而换行符在 JSON 中可以通过转义避免冲突适合学习阶段使用。生产环境更推荐“定长头部 长度字段 正文”的帧结构容错性更强。3. 环境准备与跨平台项目骨架搭建3.1 IDE、SDK 和运行时要求开始写代码前先确认开发环境。不同框架版本对应的工具链差异较大落地前要以自己机器的实际版本为准。环境项说明Visual Studio安装“使用 .NET 的移动开发”工作负载Xamarin或对应 .NET MAUI 工作负载.NET SDK根据项目目标框架安装建议保持与模板一致Android SDK / 模拟器Android 开发和调试必需Xcode仅 iOSmacOS 环境编译 iOS 包时需要真机Android / iOS 手机或平板联调验证必备学习环境的建议优先使用 Android 模拟器跑通流程成本最低。等到需要测试后台推送、电量消耗、网络切换时再用真机验证。iOS 编译必须在 macOS 上完成如果只有 Windows 机器可以先把 Android 端跑通。3.2 三个项目的划分一个容易犯的错误是把所有代码都塞进移动 App 项目。更好的做法是拆分出共享核心库让网络通讯、协议解析、数据模型独立于 UI。IndustrialMonitor/ ├── DeviceSimulator/ │ ├── DeviceSimulator.csproj │ └── Program.cs ├── MobileApp/ │ ├── MobileApp.csproj │ └── Views/MainPage.xaml └── IndustrialMonitor.Core/ ├── IndustrialMonitor.Core.csproj ├── Networking/DeviceDataClient.cs ├── Protocols/DeviceDataPointParser.cs └── Models/DeviceDataPoint.csDeviceSimulator模拟设备数据源独立的控制台程序。MobileAppXamarin.Forms 跨平台 UI 项目负责页面和绑定。IndustrialMonitor.Core共享类库存放网络客户端、协议解析和数据模型。共享库的好处是协议解析和网络客户端可以脱离 UI 单独测试。你可以在单元测试项目里直接构造一包数据验证解析逻辑是否正确而不需要启动手机。3.3 权限配置Android 网络权限与 iOS ATS移动端访问局域网 TCP 服务必须先配置网络权限否则连接会静默失败。Android 在AndroidManifest.xml中加入uses-permission android:nameandroid.permission.INTERNET /如果是调试阶段访问局域网明文服务还需要根据 Android 版本检查网络安全配置。从 Android 9 开始默认禁止明文流量开发阶段可以临时允许生产环境必须基于 HTTPS 或 TLS 重新评估。iOS 端需要关注 App Transport SecurityATS。ATS 默认阻止不安全的 HTTP 连接。调试局域网 TCP 时可以在Info.plist中做临时例外配置keyNSAppTransportSecurity/key dict keyNSAllowsArbitraryLoads/key false/ keyNSExceptionDomains/key dict key192.168.1.0/key dict keyNSExceptionAllowsInsecureHTTPLoads/key true/ /dict /dict /dict注意这个配置只用于开发联调不应直接搬到生产包。生产环境的域名、证书和网络策略要单独设计。4. 先做一个模拟数据源再写移动端通讯层4.1 模拟设备数据源TcpListener 定时推送先写服务端模拟器。它的作用是监听 9000 端口每 2 秒向连接的客户端推送一条温度 JSON 帧。using System.Net; using System.Net.Sockets; using System.Text; class DeviceSimulator { static async Task Main() { var listener new TcpListener(IPAddress.Any, 9000); listener.Start(); Console.WriteLine(模拟设备数据源已启动监听端口 9000); while (true) { var client await listener.AcceptTcpClientAsync(); Console.WriteLine($客户端接入: {client.Client.RemoteEndPoint}); _ HandleClientAsync(client); } } static async Task HandleClientAsync(TcpClient client) { using (client) { var stream client.GetStream(); while (client.Connected) { double value Random.Shared.Next(20, 90); string frame ${{\tag\:\temperature\,\value\:{value:F1},\time\:\{DateTime.Now:HH:mm:ss}\,\alarm\:{value 70}}}; byte[] bytes Encoding.UTF8.GetBytes(frame \n); await stream.WriteAsync(bytes); await Task.Delay(2000); } } } }关键点有三个使用AcceptTcpClientAsync接收客户端每个客户端用独立任务处理避免多个移动端接入时互相阻塞。每条数据帧以\n结尾模拟一个最简单的帧边界。使用Encoding.UTF8编码文本。如果现场设备使用 ASCII 或 GBK解析端要同步调整编码否则中文和特殊字符会乱码。运行这个控制台程序看到模拟设备数据源已启动监听端口 9000说明服务端就绪。4.2 移动端 Socket 客户端封装移动端通讯层的核心类DeviceDataClient负责三件事建立连接、接收数据、上报状态。public class DeviceDataClient { public event Actionstring DataReceived; public event Actionbool ConnectionChanged; private readonly string _host; private readonly int _port; private TcpClient _client; public DeviceDataClient(string host, int port) { _host host; _port port; } public async Task ConnectAsync() { try { _client new TcpClient(); await _client.ConnectAsync(_host, _port); ConnectionChanged?.Invoke(true); _ ReceiveLoopAsync(_client); } catch (Exception ex) { Console.WriteLine($连接失败: {ex.Message}); ConnectionChanged?.Invoke(false); } } private async Task ReceiveLoopAsync(TcpClient client) { var buffer new byte[4096]; try { var stream client.GetStream(); while (client.Connected) { int read await stream.ReadAsync(buffer, 0, buffer.Length); if (read 0) break; string chunk Encoding.UTF8.GetString(buffer, 0, read); ProcessChunk(chunk); } } catch (Exception ex) { Console.WriteLine($接收异常: {ex.Message}); } finally { ConnectionChanged?.Invoke(false); } } private void ProcessChunk(string chunk) { // 半包/粘包处理下一节实现 } }这段代码体现了 C# 事件机制的主要用法通讯线程只负责收发字节不关心界面怎么展示。ViewModel 订阅DataReceived事件收到数据后再决定如何更新 UI这样通讯层可以独立测试也方便日后替换成其他通信方式。4.3 协议帧、byte[] 与结构体的关系网络传输的底层是字节流不是字符串。C# 里最直接的载体是byte[]。从TcpClient.GetStream().ReadAsync读到的数据必须先转换成字符串再解析成对象。工业协议解析常用两种方式文本协议每一帧是 ASCII 字符串用分隔符或换行符切分可读性好。二进制协议每一帧由定长头部和正文组成比如 Modbus TCP 的 MBAP 头部包含事务标识、协议标识、长度字段和单元标识。二进制协议解析时需要按偏移地址读取字节并用BitConverter或BinaryPrimitives转换大小端。比如 Modbus 读取一个保持寄存器返回的是两个字节实际值可能是高位在前也可能是低位在前解析错误会导致数值完全不对。帧经过解析后存放在结构体或类中。这里定义一个数据点结构体public struct DeviceDataPoint { public string Tag { get; set; } public double Value { get; set; } public DateTime Time { get; set; } public bool Alarm { get; set; } }对应解析函数public static DeviceDataPoint Parse(string line) { using var doc System.Text.Json.JsonDocument.Parse(line); var root doc.RootElement; return new DeviceDataPoint { Tag root.GetProperty(tag).GetString(), Value root.GetProperty(value).GetDouble(), Time DateTime.Parse(root.GetProperty(time).GetString()), Alarm root.GetProperty(alarm).GetBoolean() }; }使用JsonDocument解析而不是直接反序列化成类是为了减少反射开销。在移动端高频接收数据时每帧只做一次解析性能影响不大但如果是几百个点位高频刷新解析方式的差异就会明显体现出来。4.4 半包/粘包问题要先解决否则解析一定出错TCP 是流式协议没有天然的消息边界。一次WriteAsync发送的数据可能被拆成多次ReadAsync接收称为半包也可能和下一帧数据连在一起到达称为粘包。如果每次收到字节就直接JsonDocument.Parse会出现偶发的解析异常。正确的做法是引入缓冲区把新数据追加到缓冲区然后循环查找帧结束符切出完整的一行再处理。private readonly StringBuilder _buffer new StringBuilder(); private void ProcessChunk(string chunk) { _buffer.Append(chunk); while (true) { int newlineIndex _buffer.ToString().IndexOf(\n); if (newlineIndex 0) break; string line _buffer.ToString(0, newlineIndex).Trim(); _buffer.Remove(0, newlineIndex 1); if (line.Length 0) { DataReceived?.Invoke(line); } } }调用Trim是为了去掉可能残存的\r字符。在 Windows 上如果服务端发送\r\n解析端只按\n切分就会留下\r导致 JSON 解析失败。注意不要只在正常流程下测试解析逻辑。要专门构造“一帧大报文”“多帧连续发送”“半帧 半帧”三种情况确认缓冲区切分逻辑是健壮的。4.5 用委托和事件把数据从通讯线程推给 ViewModel通讯层的事件已经定义好接下来在 ViewModel 中订阅。这里会遇到 C# 工业开发中最常见的线程问题网络线程是后台线程UI 更新必须在主线程。private void OnDataReceived(string line) { var point DeviceDataPointParser.Parse(line); MainThread.BeginInvokeOnMainThread(() { Temperature ${point.Value:F1} °C; IsAlarm point.Alarm; }); }使用MainThread.BeginInvokeOnMainThread把 UI 更新操作切回主线程。如果不切换Xamarin.Forms 在部分平台会抛出跨线程异常另一些平台上则表现为界面不刷新问题更难定位。5. 用 MVVM 绑定实时数据完成监控页面5.1 ViewModel 基类与属性通知MVVM 模式下ViewModel 通过INotifyPropertyChanged通知界面属性变化。先定义一个基类public abstract class BindableBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected bool SetPropertyT( ref T field, T value, [CallerMemberName] string propertyName null) { if (EqualityComparerT.Default.Equals(field, value)) return false; field value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); return true; } }SetProperty里先比较再赋值可以避免每次收到数据都触发界面刷新。在温度值没有变化时不重复刷新 UI可以减少绘制开销。5.2 MainViewModel 设计监控页面只需要几个属性温度、设备连接状态、告警状态、连接命令。public class MainViewModel : BindableBase { private readonly string _host 192.168.1.100; private readonly int _port 9000; private DeviceDataClient _client; private string _temperature --; private string _deviceStatus 未连接; private bool _isAlarm; public string Temperature { get _temperature; set SetProperty(ref _temperature, value); } public string DeviceStatus { get _deviceStatus; set SetProperty(ref _deviceStatus, value); } public bool IsAlarm { get _isAlarm; set { if (SetProperty(ref _isAlarm, value)) { OnPropertyChanged(nameof(StatusColor)); } } } public Color StatusColor IsAlarm ? Color.FromArgb(#D32F2F) : Color.FromArgb(#388E3C); public ICommand ConnectCommand new Command(async () await StartClientAsync()); private async Task StartClientAsync() { if (_client ! null) return; _client new DeviceDataClient(_host, _port); _client.DataReceived OnDataReceived; _client.ConnectionChanged OnConnectionChanged; await _client.ConnectAsync(); } private void OnDataReceived(string line) { var point DeviceDataPointParser.Parse(line); MainThread.BeginInvokeOnMainThread(() { Temperature ${point.Value:F1} °C; IsAlarm point.Alarm; DeviceStatus point.Alarm ? 告警 : 已连接; }); } private void OnConnectionChanged(bool connected) { MainThread.BeginInvokeOnMainThread(() { DeviceStatus connected ? 已连接 : 连接断开; }); } }这里要区分两件容易混淆的事界面层检测变量变化和业务层检测变量变化。界面层检测变化靠的是INotifyPropertyChanged。属性值变界面跟着变。业务层检测变化靠的是数据到达后的阈值判断。告警逻辑不能在界面里做应该放在服务端或 ViewModel 的数据处理里这样即使 UI 没打开告警规则依然生效。在上面的代码里模拟器已经在帧内写好了alarm字段移动端只是读取并展示。实际项目中告警阈值可能由服务端下发移动端仍然只负责展示结果。5.3 实时数据卡片与告警高亮页面的 XAML 保持简洁ContentPage xmlnshttp://xamarin.com/schemas/2014/forms xmlns:xhttp://schemas.microsoft.com/winfx/2009/xaml x:ClassMobileApp.Views.MainPage ContentPage.Content StackLayout Padding20 Spacing20 Frame BorderColor#CCCCCC CornerRadius8 StackLayout Label Text当前温度 FontSize14 TextColor#666666/ Label Text{Binding Temperature} FontSize40 TextColor{Binding StatusColor}/ /StackLayout /Frame Label Text{Binding DeviceStatus} HorizontalOptionsCenter TextColor#333333/ Button Text连接服务 Command{Binding ConnectCommand} BackgroundColor#1976D2 TextColorWhite/ /StackLayout /ContentPage.Content /ContentPage温度超过阈值时StatusColor从绿色变成红色用户一眼就能看到异常。在实际车间环境里还可以把告警颜色和声音震动结合起来但颜色的优先级是最高的因为现场嘈杂时不一定能听到声音。页面构造函数中需要设置绑定上下文public MainPage() { InitializeComponent(); BindingContext new MainViewModel(); }忘记设置BindingContext是新手最容易遇到的问题。设置了文本绑定但页面没有设置数据源界面就会一直显示默认值。6. 联调验证抓包、日志和断点要配合使用6.1 分步运行联调按三个步骤走每步确认无误后再进入下一步。第一步运行模拟数据源。确认控制台显示监听端口已启动。第二步启动 Android 模拟器或真机。注意到一个问题模拟器里的127.0.0.1指向模拟器自身不是开发机。要访问开发机上的模拟数据源需要填局域网 IP或者在 Android 模拟器上用10.0.2.2映射宿主机。第三步在 App 中点击“连接服务”。预期现象是模拟数据源控制台出现客户端接入App 温度标签每 2 秒刷新一次。6.2 用抓包工具确认通信内容当程序不能正常工作而且日志没有明显线索时要使用抓包工具确认网络层发生了什么。抓包的重点看三件事TCP 三次握手是否完成。数据帧是否完整到达。连接是否被异常重置。在开发机上用 Wireshark 抓包时过滤条件可以写tcp.port 9000。如果能看到稳定的数据包流量说明服务端发送正常如果客户端连不上要查看是否存在 TCP SYN 重传或端口被防火墙拦截。抓包工具在生产现场可能受到网络权限限制。更通用的做法是在通讯层打结构化日志输出连接成功、数据帧原文、异常堆栈。至少保证日志里有时间和关键字否则排查排错时无从下手。6.3 预期运行结果与异常现象检查点预期异常形态模拟数据源控制台每 2 秒输出一条温度帧无输出、端口冲突、进程被占用App 连接状态变为“已连接”一直“未连接”或点击后无反应温度标签按 2 秒周期刷新数值不变、偶尔跳变、显示异常值告警状态温度大于 70 时显示告警色阈值判断不生效或颜色刷新延迟有一点需要反复强调联调时不要只看“程序没有崩溃”还要验证数据的输入、输出、异常分支和日志是否符合预期。比如把温度调到超过阈值确认告警状态切换了把服务端停掉确认 App 能感知断开并进入重连逻辑。7. 工业现场常见问题与排查路径7.1 连接失败权限、IP、端口和防火墙现象点击“连接服务”后设备状态一直停留在“未连接”。排查按以下顺序进行确认模拟数据源已经启动并且监听的端口是 9000。确认手机和开发机在同一个局域网网段。确认代码里填的不是127.0.0.1。移动端连接的是远端电脑不是本机。检查 Android 的INTERNET权限是否已在 Manifest 中声明。检查开发机防火墙是否放行 9000 端口。用抓包工具确认 TCP SYN 报文是否发出、是否有响应。常见原因是防火墙拦截。Windows 上第一次运行 TcpListener 时系统会弹出防火墙授权窗口如果点了取消后续连接就会被静默丢弃。注意生产环境的连接问题要区分两种场景一种是网络根本不通另一种是服务进程已经异常退出。前者抓包能看出来后者要看进程状态和日志。7.2 半包/粘包导致 JSON 解析失败现象程序偶尔抛出JsonException或者有两条数据连在一起显示。原因TCP 是流式协议没有消息边界。直接对ReadAsync读到的字符串做JsonDocument.Parse迟早会遇到半包或粘包。解决方案维护StringBuilder缓冲区先追加再按换行符切分。具体代码参考 4.4 节。预防建议不要只用“每 2 秒一条数据”的频率测试。构造一帧很大的报文再构造多条小报文连续发送。如果协议允许使用“头部长度字段 正文”的结构比分隔符更可靠。7.3 UI 不刷新与跨线程更新现象模拟数据源正常输出但界面温度一直不变。排查路径在OnDataReceived里加断点确认事件有没有触发。确认BindingContext是否绑定了 MainViewModel。确认更新属性时是否切到了主线程。检查绑定路径是否写错比如 XAML 里写成了{Binding Temperature}但 ViewModel 属性名是CurrentTemperature。很多情况下问题不是出在 UI 框架而是事件根本没有被订阅或者属性名不一致。先加日志确认事件链路再怀疑 UI 框架。7.4 断线重连、内存增长与后台限制现象现场网络波动后 App 无法恢复长时间运行后出现卡顿。原因主要有四类ReceiveLoopAsync异常退出后没有触发有效的重连机制。DataReceived事件被多次订阅导致同一个数据帧被处理多次。缓冲区无限增长长时间运行后内存升高。Android 和 iOS 对后台 Socket 连接有限制锁屏或切后台后连接可能被系统回收。处理方式用CancellationToken控制重连循环设置最大重试次数。在页面销毁时取消事件订阅避免重复订阅。给缓冲区设置最大长度超过阈值清理或断开。生产环境考虑前台服务或系统推送能力但要在了解各平台规则后再设计。7.5 排查顺序与速查表工业现场排查问题建议按“输入 → 路径 → 配置 → 日志 → 工具”的顺序推进现象优先检查次要检查处理建议连接失败服务进程、网络连通性权限、防火墙、IP 端口用 telnet 或 nc 测试端口解析失败日志原文、帧边界编码、大小端按帧切分补全半包处理UI 不刷新事件是否触发BindingContext、主线程加调试日志确认链路经常断开网络稳定性心跳、后台限制增加心跳限制重连频率内存增长事件订阅次数缓冲区清理检查订阅生命周期设置缓冲上限这些条目应该作为开发完成前的自测清单而不是出了问题再临时翻。8. 从 Demo 到生产环境工程化扩展与实践清单8.1 安全基线连接凭证与数据保护Demo 里 IP 和端口直接写在代码中这种方法只适合学习。生产环境必须解决三个安全问题不硬编码连接凭证。IP、端口、账号、密码应从配置中心或环境变量读取。传输层加密。局域网可以选 TLS公网必须使用证书验证。控制指令必须鉴权。移动端触发设备启动、停止等操作时服务端要校验用户身份和权限并记录审计日志。日志中不要打印密码、Token 和完整报文。工业现场数据可能是商业机密日志脱敏应该作为代码审查的一项基本要求。8.2 协议升级方向WebSocket 和 MQTT当系统从单机 Demo 发展到多车间、多设备时裸 TCP 的维护成本会上升。核心问题是缺少统一的心跳、订阅、离线消息和权限控制。推荐按以下方向演进WebSocket当页面需要双向高频交互且服务端已经是 HTTP 服务时优先选择。MQTT当设备数量多、网络弱、需要离线消息时MQTT 提供现成的主题订阅和 QoS 机制。OPC UA当设备层需要标准化建模时服务层接入 OPC UA移动端仍通过 WebSocket 或 MQTT 获取数据。移动端协议栈保持稳定升级发生在服务层。这是本文设计的核心原则之一。8.3 设备接入扩展PLC、扫码枪、相机 SDK 的接入思路C# 上位机开发中常见的设备接入包括 Modbus TCP、FINS UDP、串口扫码枪、工业相机 SDK 等。这些设备协议完全不一样但接入思路是统一的设备差异收敛在服务层对外输出统一数据格式。设备类型常见协议服务层处理方式移动端关注点PLCModbus TCP、FINS UDP轮询或订阅采集转换为统一 JSON 帧点位标识、单位、告警阈值扫码枪串口、USB HID、网络监听触发事件输出条码数据条码与工单、物料关联工业相机厂家 SDK图像采集、视觉识别结果回调展示结果和图片确认状态具体到移动端不建议直接在 App 中集成相机 SDK 或串口扫码枪驱动原因是平台兼容性差、设备供应商 SDK 更新频繁。正确做法是让服务层完成驱动和采集移动端只接收已经加工好的结果。8.4 从 Demo 到生产的可复用检查清单发布到生产环境前把下面这张清单逐项过一遍检查项是否完成说明协议设计有帧边界、超时、心跳、重连策略不能用 Demo 里的换行符直接上生产网络容错连接超时、断线重连、缓冲区上限防止异常网络拖垮 App数据正确性大小端、编码、单位、精度已验证数值错误比界面卡顿更危险界面刷新MVVM 绑定正确UI 更新在主线程杜绝跨线程异常安全配置凭证不硬编码、传输加密、控制鉴权生产环境必须覆盖日志连接、数据、异常、操作都有日志日志要有时间戳和事件类型监控内存、CPU、连接数、后台状态长时间运行不能内存上涨回滚配置化发布支持版本回退避免一次故障导致整个系统不可用把这条清单保存下来每个项目发布前都对照检查一遍可以帮助你降低把 Demo 直接搬到生产环境的风险。移动跨平台工业监控应用的核心并不是把界面搬到手机上而是让数据链路稳定、可观测、可恢复。这个设计里最值得记住的判断是先把协议、连接和解析做对再谈界面和扩展。如果你的项目也要接入 PLC、扫码枪或相机可以在服务层把这些设备统一转换成一种内部协议移动端只面向这一种内部协议开发。下一步建议做三件事给通讯层补上心跳和自动重连把可变的 IP 和阈值从配置读取再为协议解析写几个单元测试。这样一个 Demo 就能逐步长成一个可以运维的工业监控应用。