简介这份托利多中间件电子秤数据接口C#源码包面向需要在Windows平台对接托利多电子秤的开发者重点解决RL00、BCOM等多型号设备的数据读取、指令交互与业务系统集成问题源码已测试通过并附带厂家SDK。包内以C#源码、项目工程和SDK依赖为核心配合配置、说明文档及示例程序可辅助梳理通信协议适配、接口调用与多型号兼容思路。资源共132个文件涵盖10个cs源码、3个csproj与1个sln工程文件、31个dll含SDK依赖、49个xml及6个config配置、5个exe和若干doc、txt说明压缩包约32.67MB目录按工程与运行依赖组织便于检索和复现测试环境。已有1442人学习。对从事工业称重、产线自动化和仓储零售数据采集的读者可据此研究中间件封装方式、设备接口调试和兼容性处理也可借鉴现成工程结构快速搭建自己的采集程序减少从零摸索与反复联调的成本。1. 电子秤数据接口不是“读串口”这么简单先看这套 C# 中间件能解决什么电子秤数据接口 C# 源码、厂家 SDK、多型号兼容这几个词放在一起时很多人的第一反应是“不就是读个串口吗”。真到产线上RL 类秤型和 BCOM 类协议混着用有的走串口有的必须调厂家 SDK业务系统还要求重量、单位、稳定标志、时间戳一次性拿全事情就没那么轻了。这套源码做的是中间件层把不同型号的电子秤收敛成统一接口带厂家 SDK 桥接和测试工程C# 项目可以直接跑起来联调。适合正在做称重工位、上位机采集、MES 或仓储系统对接的开发者尤其适合手里有多台不同型号秤、又不想把协议代码散落在业务系统里的场景。2. 技术选型与工程定位C#、厂家 SDK、串口/网口三层的边界怎么划2.1 为什么用 C# 做中间件而不是把协议塞进业务系统工业现场的称重设备接口有一个很现实的特点协议差异不在业务侧而在设备侧。同样叫“读重量”A 型号可能返回一帧 ASCIIB 型号走二进制帧C 型号必须加载厂家 SDK 后注册回调。业务系统如果直接写串口读取后面每加一个型号就要改一次订单、库存或产线逻辑改到最后没人敢动。C# 在这里的价值不是语言本身多新而是上位机生态成熟串口、网口、Windows 服务、WinForms 调试工具、厂家 SDK 的托管封装都能在同一套工程里解决。我一般会把中间件拆成三层设备接口层只定义“我要什么数据”适配层负责“怎么从这台秤拿到数据”SDK 桥接层负责“厂家动态库怎么初始化、回调怎么转成事件”。业务系统只依赖IWeightDevice不关心背后是串口还是 SDK。这样做的直接好处是新增型号时改的是适配器不是业务代码测试时也可以用回放帧替代真实秤不用每次把产线停掉。血泪经验是千万不要让业务层直接引用厂家 SDK 的 DLL一旦位数不匹配或者 SDK 升级整个业务程序都要跟着重编译。从工程定位看这套源码更像“设备访问层 小型驱动框架”不是完整的上位机业务系统。它把连接、读帧、校验、单位换算、异常重试、日志这些公共动作收在中间件里业务侧拿到的是统一的数据对象。对熟手来说重点看它的接口边界和适配器注册方式对新手来说先跑测试工程确认本机环境、串口名、SDK 位数都对再上真实设备。这个顺序反了很容易把环境问题误判成协议问题。2.2 运行环境与依赖清单.NET、串口、SDK、日志、配置这套 C# 源码常见做法是兼容 .NET Framework 4.7.2 和 .NET 6/8 两套目标现场老工控机跑前者新项目跑后者。串口用System.IO.Ports网口用TcpClient厂家 SDK 以托管 DLL 或非托管 DLL P/Invoke 的方式接入。日志不要用Console.WriteLine硬扛NLog 或 Serilog 都行关键是能按设备号、帧内容、异常堆栈落到文件里。配置建议放appsettings.json或 ini串口号、波特率、超时、重试次数、SDK 路径都不要写死。组件作用现场注意点.NET Framework 4.7.2 / .NET 6运行宿主老工控机优先确认运行时版本System.IO.Ports串口通信端口名、占用、USB 转串口驱动都要核厂家 SDK部分型号必需x86/x64 必须和主程序一致NLog / Serilog日志与排查记录原始帧别只记“读取失败”SQLite 或本地文件断网缓存可选不是所有项目都需要别过度设计测试工程离线回放先跑通再上设备环境准备可以按下面几步走。先确认 SDK 和运行时dotnet --info这条命令看三件事当前 SDK 版本、运行时列表、进程位数。如果项目是 x86而机器上只有 x64 运行时后面加载厂家 DLL 会直接翻车。第二步把厂家 SDK 的 DLL 放到输出目录下的libs或sdk文件夹并在项目文件里设置“始终复制”。第三步用虚拟串口或回放文件跑测试工程不要一上来就接真实秤。第四步打开日志目录确认每一帧原始数据都能落盘。项目文件里常见的复制配置如下Project SdkMicrosoft.NET.Sdk PropertyGroup TargetFrameworksnet472;net8.0/TargetFrameworks Platformsx86;x64/Platforms LangVersionlatest/LangVersion /PropertyGroup ItemGroup !-- 厂家 SDK 按实际目录放主程序位数必须与 DLL 一致 -- None Includelibs\**\*.dll CopyToOutputDirectoryPreserveNewest / /ItemGroup /Project逻辑说明TargetFrameworks让同一套代码能覆盖老工控机和新项目Platforms是为了把 x86/x64 的差异显式暴露出来避免“开发机 x64 能跑、现场 x86 加载失败”。参数说明libs\**\*.dll要按实际 SDK 目录改CopyToOutputDirectory选PreserveNewest可以减少手工拷贝。注意不要把 SDK DLL 提交到公共仓库现场部署时按授权方式随程序分发。提示串口名在 Windows 下可能是COM3在 Linux 下可能是/dev/ttyUSB0。中间件配置里不要只写死一种格式最好留一个设备地址解析器。2.3 从“能读数”到“能交付”接口层、适配层、业务层的职责“能读数”和“能交付”之间差的是边界。接口层定义统一契约比如连接、断开、读一次重量、订阅连续重量、获取设备状态适配层实现具体协议串口适配器只管帧怎么收、怎么校验SDK 适配器只管回调怎么转事件业务层只处理重量值、单位、稳定标志和时间戳。这样拆完新增一个型号就是新增一个适配器类再在工厂里注册不需要改业务判断。现场最怕的是if (型号 A) ... else if (型号 B) ...散落在十几个文件里后面加型号就是灾难。我一般会要求适配层不抛“业务异常”只抛设备异常或协议异常并且异常里带设备号、原始帧、重试次数。业务层拿到异常后决定是重试、降级还是报警。测试工程则直接调适配器用回放帧模拟粘包、半包、校验错、超时。这样的结构对新手也友好先看接口知道系统要什么再看某个适配器知道某型号怎么工作最后看测试知道边界条件覆盖了哪些。中间件不是把代码写复杂而是把变化点关进小盒子里。3. 目录结构与通信链路一个称重请求从串口到业务对象的完整路径3.1 源码目录怎么分层Core、Adapters、SdkBridge、Tests、Tools源码目录一般不会平铺按职责分比较稳。下面是一个常见的目录骨架实际项目里名字可能不同但分层思路基本一致ScaleBridge/ src/ ScaleBridge.Core/ # 接口、数据模型、配置、异常 ScaleBridge.Adapters/ # 串口、网口、SDK 适配器 ScaleBridge.SdkBridge/ # 厂家 SDK 封装、回调、生命周期 ScaleBridge.Host/ # 控制台或 WinForms 调试宿主 tests/ ScaleBridge.Tests/ # 回放帧、粘包、校验、重试测试 tools/ FramePlayer/ # 离线帧回放工具 PortMonitor/ # 串口监视与日志查看 config/ devices.sample.json # 设备配置样例Core里不要放任何厂家 SDK 引用保证业务层和测试工程能独立编译。Adapters按通信方式分SerialScaleAdapter、TcpScaleAdapter、SdkScaleAdapter各自独立。SdkBridge单独一层是为了把非托管资源、回调线程、释放逻辑关起来避免适配器里到处写DllImport。Tests不要只测“正常一帧”要覆盖半包、粘包、校验失败、超时、重复帧。Tools看起来可选但现场排查时很有用尤其是帧回放工具能把日志里的原始字节重新灌进解析器复现问题比盯着秤按按钮快得多。配置样例建议带设备 ID、协议类型、地址、超时、单位换算因子。不要把所有型号的字段揉成一个巨型配置公共字段放外层协议私有字段放options里。这样新增型号不会影响老配置。目录分层之后编译产物也要分开SDK 相关 DLL 放独立目录部署时不容易漏。3.2 连接建立与心跳串口参数、网口连接、SDK 初始化串口连接的核心参数是波特率、数据位、停止位、校验位、流控和读写超时。称重设备常见 9600、8 数据位、无校验、1 停止位但这不是万能答案必须看设备手册。流控如果设错表现可能是“能打开但收不到完整帧”。读写超时不要设得太短连续称重时设备响应有间隔也不要太长否则业务线程会被拖住。网口设备还要考虑心跳和断线重连SDK 设备则要先初始化再注册回调。配置可以长这样{ devices: [ { id: scale-01, protocol: serial, address: COM3, baudRate: 9600, dataBits: 8, stopBits: One, parity: None, readTimeoutMs: 800, retry: 2, options: { stableOnly: true, unit: kg } }, { id: scale-02, protocol: sdk, address: USB, sdkPath: libs/vendor-scale.dll, retry: 1 } ] }逻辑说明protocol决定工厂创建哪个适配器address对串口是端口名对 SDK 可能是设备句柄或 USB 标识。readTimeoutMs控制单次读取等待retry控制失败重试次数stableOnly表示业务是否只接受稳定重量。参数说明baudRate、dataBits、stopBits、parity必须和设备手册一致sdkPath要相对于输出目录不要写开发机绝对路径。注意stableOnly不是所有业务都适合发货扫描可能要求稳定过程监控可能允许波动。连接建立顺序建议固定先校验配置再创建设备再打开连接最后启动心跳或订阅。SDK 设备还要多一步“加载 DLL 并检查位数”失败时日志里要写清楚是找不到 DLL、入口点错误还是位数不匹配。心跳不要用业务线程做单独定时器或后台任务失败后按退避策略重连。现场网络抖动很常见重连逻辑要能区分“设备离线”和“协议解析失败”否则日志会变成一锅粥。3.3 数据帧解析与统一模型重量、单位、稳定标志、时间戳业务层不关心帧长什么样只关心统一模型。模型里至少要有设备 ID、重量值、单位、是否稳定、原始文本或字节、采集时间。重量值建议用decimal不要用double做金额或贸易结算相关计算单位统一转成业务约定的单位比如 kg转换因子放配置。稳定标志很关键很多现场问题不是读不到数而是把波动值当稳定值用了。public sealed class WeightSample { public string DeviceId { get; init; } string.Empty; public decimal Value { get; init; } public string Unit { get; init; } kg; public bool IsStable { get; init; } public DateTimeOffset Timestamp { get; init; } DateTimeOffset.Now; public string Raw { get; init; } string.Empty; // 业务侧只拿统一模型不直接碰协议字节 public decimal ToUnit(string targetUnit) { return Unit targetUnit ? Value : UnitConverter.Convert(Value, Unit, targetUnit); } }逻辑说明Raw保留原始帧排查时能对照协议Timestamp用DateTimeOffset避免时区问题ToUnit把单位换算收在模型里业务层不用到处乘系数。参数说明DeviceId要和配置里的id一致IsStable由适配器解析稳定位后填入Unit不要直接信任设备返回值最好做一次白名单校验。解析流程一般是收字节、找帧头帧尾、校验、截取载荷、按型号解析重量和标志、单位归一、生成模型、抛给业务或写入队列。任何一步失败都要记录原始帧不然就是黑匣子。4. 核心模块代码拆解接口抽象、协议适配器、SDK 桥接与异常重试4.1 统一接口 IWeightDevice 与设备工厂接口不要设计得太花够用就行。连接、断开、读一次、连续订阅、状态查询这五个动作能覆盖大多数称重场景。读一次适合扫描工位连续订阅适合产线监控。接口里不要暴露串口对象或 SDK 句柄否则抽象就漏了。工厂根据配置里的protocol创建适配器新增型号时只加分支或注册表。public interface IWeightDevice : IDisposable { string DeviceId { get; } bool IsConnected { get; } Task ConnectAsync(CancellationToken ct default); Task DisconnectAsync(CancellationToken ct default); TaskWeightSample ReadOnceAsync(CancellationToken ct default); // 连续称重用事件或 IAsyncEnumerable二选一即可 event EventHandlerWeightSample? WeightChanged; } public static class WeightDeviceFactory { public static IWeightDevice Create(DeviceConfig cfg, ILogger logger) { return cfg.Protocol switch { serial new SerialScaleAdapter(cfg, logger), tcp new TcpScaleAdapter(cfg, logger), sdk new SdkScaleAdapter(cfg, new VendorSdkBridge(cfg, logger), logger), _ throw new NotSupportedException($未注册的协议: {cfg.Protocol}) }; } }逻辑说明ReadOnceAsync给请求式业务用WeightChanged给连续采集用工厂集中处理协议到适配器的映射业务层不写switch。参数说明DeviceConfig里带设备 ID、地址、超时、重试和协议私有参数ILogger从宿主注入方便测试替换。注意Dispose要真的释放串口和 SDK 回调不要只置空引用。新增型号时优先加注册表不要在主工厂里堆几十个分支。4.2 串口协议适配器读帧、校验、粘包处理串口适配器最容易翻车的地方不是打开端口而是粘包和半包。称重设备连续发送时一次Read可能读到半帧也可能读到一帧半。稳妥做法是维护一个字节缓冲区每次读到数据就追加循环查找帧头帧尾完整帧取出来解析剩余字节留在缓冲区。解析失败要丢弃当前帧头之前的数据避免死循环。校验失败记录原始帧并继续找下一帧不要直接让整个连接断掉。private readonly Listbyte _buffer new(); private async TaskWeightSample? ReadFrameAsync(CancellationToken ct) { var chunk new byte[256]; var n await _serialPort.BaseStream.ReadAsync(chunk, ct); if (n 0) return null; _buffer.AddRange(chunk.Take(n)); // 帧头 0x02帧尾 0x03长度按实际协议调整 while (true) { var start _buffer.IndexOf(0x02); var end _buffer.IndexOf(0x03); if (start 0 || end 0 || end start) break; var frame _buffer.Skip(start).Take(end - start 1).ToArray(); _buffer.RemoveRange(0, end 1); if (!ChecksumOk(frame)) { _logger.Warn(校验失败原始帧: {Frame}, BitConverter.ToString(frame)); continue; } return ParseFrame(frame); } // 防止异常数据把缓冲区撑爆 if (_buffer.Count 4096) _buffer.RemoveRange(0, _buffer.Count - 1024); return null; }逻辑说明先追加再循环找完整帧找到后从缓冲区移除校验失败只记日志并继续不破坏后续帧。参数说明0x02、0x03要换成实际帧头帧尾ChecksumOk按型号实现4096是缓冲区保护阈值可按最大帧长调整。注意ReadAsync的取消令牌要传到底否则断开连接时可能卡住。半包场景下返回null是正常的调用方继续读即可不要当成设备故障。4.3 厂家 SDK 桥接托管回调、线程安全、资源释放SDK 桥接层要把非托管边界关住。初始化、注册回调、注销回调、释放句柄都放在这一层适配器只接收统一事件。SDK 回调线程通常不是 UI 线程所以事件里不要直接更新界面入队或切回同步上下文。回调里也不要抛异常异常跨非托管边界可能直接让进程退出。资源释放要幂等重复Dispose不能崩。public sealed class VendorSdkBridge : IDisposable { private readonly object _gate new(); private readonly ILogger _logger; private IntPtr _handle IntPtr.Zero; private bool _disposed; public event EventHandlerWeightSample? WeightChanged; public void Start() { lock (_gate) { if (_disposed) throw new ObjectDisposedException(nameof(VendorSdkBridge)); _handle VendorScaleSdk.Open(); // 虚构的 SDK 入口 if (_handle IntPtr.Zero) throw new InvalidOperationException(SDK 初始化失败); VendorScaleSdk.SetCallback(_handle, OnNativeWeight); } } private void OnNativeWeight(int value, int stable, IntPtr userData) { try { // 回调线程不可靠先转成统一模型再抛给上层 var sample new WeightSample { DeviceId sdk-scale, Value value / 1000m, Unit kg, IsStable stable 1, Raw $sdk:{value},{stable} }; WeightChanged?.Invoke(this, sample); } catch (Exception ex) { _logger.Error(ex, SDK 回调处理失败); } } public void Dispose() { lock (_gate) { if (_disposed) return; _disposed true; if (_handle ! IntPtr.Zero) { VendorScaleSdk.SetCallback(_handle, null); VendorScaleSdk.Close(_handle); _handle IntPtr.Zero; } } } }逻辑说明_gate保护句柄和释放过程回调里用try/catch兜底避免异常穿透非托管边界。参数说明value / 1000m是按 SDK 返回值缩放实际项目要按手册确认小数位stable 1是稳定标志约定不同 SDK 可能用位掩码userData可用于回传上下文。注意回调注册和注销必须成对退出程序前不注销可能留下后台线程或句柄泄漏。4.4 重试、超时与日志别让一次读失败拖垮整条产线重试不是万能药读超时和校验失败要分开处理。串口读超时可能是设备没发也可能是波特率错校验失败通常说明帧边界或协议解析有问题。重试次数不要太多2 到 3 次足够间隔用短退避。连续订阅场景不要每次失败都重连先清缓冲区再试。日志要记录设备 ID、操作、耗时、重试次数、原始帧摘要不然现场只能靠猜。public async TaskWeightSample ReadWithRetryAsync(CancellationToken ct) { var lastEx default(Exception); for (var i 0; i _cfg.Retry; i) { var sw Stopwatch.StartNew(); try { var sample await ReadOnceCoreAsync(ct); _logger.Info({DeviceId} 读取成功耗时 {Elapsed}ms, _cfg.Id, sw.ElapsedMilliseconds); return sample; } catch (OperationCanceledException) { throw; } catch (Exception ex) { lastEx ex; _logger.Warn(ex, {DeviceId} 第 {Attempt} 次读取失败耗时 {Elapsed}ms, _cfg.Id, i 1, sw.ElapsedMilliseconds); if (i _cfg.Retry) await Task.Delay(100 * (i 1), ct); } } throw new ScaleReadException($设备 {_cfg.Id} 读取失败, lastEx); }逻辑说明取消异常直接抛出不参与重试每次失败记录尝试次数和耗时最后一次失败包装成业务可识别的ScaleReadException。参数说明_cfg.Retry是额外重试次数100 * (i 1)是简单退避实际可配。注意不要吞掉OperationCanceledException否则程序退出时线程退不干净。5. 避坑与排查串口能打开但读不到数、SDK 位数不匹配这些坑怎么定位5.1 现象一端口打开成功重量一直是 0现象串口Open()返回成功日志也没有异常但读出来的重量一直是 0或者只收到空帧。原因通常有三类波特率、数据位、校验位和设备不一致设备需要先发命令才返回数据USB 转串口驱动把端口占住了程序打开的是另一个虚拟端口。解决时先看原始字节日志如果一帧都没有优先核对串口参数和接线如果有一帧但解析为 0检查帧头帧尾和重量字段偏移。用串口监视工具抓一次真实数据和配置逐项对照比反复重启程序有效。5.2 现象二x86/x64 不匹配导致 SDK 初始化失败现象串口型号正常切到 SDK 型号就报“找不到入口点”“无法加载 DLL”或直接BadImageFormatException。原因多半是主程序位数和厂家 SDK 不一致x64 进程加载了 x86 的 DLL或者 SDK 依赖的 VC 运行库缺失。解决时先确认宿主进程位数再把项目平台目标改成对应位数SDK 依赖的 DLL 一起复制到输出目录。日志里要打印IntPtr.Size和 DLL 路径。注意不要只改项目属性里的“首选 32 位”发布后的启动方式也可能影响位数。5.3 现象三连续称重时粘包/半包数据错位现象单次读取正常连续称重时偶尔出现重量跳变、单位错乱或者解析器卡住不再出数。原因是串口是流式传输一次读取不等于一帧缓冲区里可能有多帧或半帧。解决时用固定缓冲区循环找帧头帧尾校验失败只丢弃当前帧不要清空整个缓冲区同时加最大缓冲保护防止异常数据撑爆内存。测试工程里要专门构造“半帧 完整帧 粘包”的回放数据别只测一帧正常数据。这个坑很玄学现场复现难离线回放最省时间。5.4 现象四UI 卡死或退出后串口占用现象点“连接”后界面卡住或者程序退出后端口仍被占用必须拔插 USB 才能恢复。原因通常是把同步Read放在了 UI 线程或者Dispose没关串口、没注销 SDK 回调。解决时串口读写全部异步化UI 只订阅事件退出时按“停止订阅、注销回调、关闭连接、释放句柄”的顺序清理。Dispose要幂等异常也要继续释放。注意调试时不要同时开两个程序抢同一个串口日志里看到“拒绝访问”先查占用不要先怀疑协议。5.5 现象五多型号切换后单位和小数点错乱现象A 型号显示 12.34 kg切到 B 型号变成 1234 g 或 0.012 t业务侧按 kg 计算就全错。原因是不同型号返回的单位和小数位不同适配器直接透传了原始值。解决时在适配器里做单位归一统一转成业务约定单位并保留原始单位和原始值用于排查。小数点位数不要靠猜按协议字段或 SDK 返回值缩放。配置里加单位白名单遇到未知单位直接报错而不是静默转换。测试用例要覆盖 kg、g、t 三种返回别让单位问题留到现场。6. 二次开发怎么改新增一个型号、接一套业务系统、做可验证的回归二次开发最常做三件事新增型号、接业务系统、补回归测试。新增型号不要改业务层按“新建适配器 - 实现解析 - 工厂注册 - 回放测试”的顺序走。比如新增一个走网口的型号先复制TcpScaleAdapter的骨架改帧头和校验再在工厂里加tcp-v2分支。接业务系统时只暴露IWeightDevice和WeightSample不要让业务方引用 SDK 命名空间。回归测试用日志里的原始帧做回放覆盖正常帧、半包、校验错、超时四类改完解析器先跑测试再上设备。下面是一个注册示例public static IWeightDevice Create(DeviceConfig cfg, ILogger logger) { return cfg.Protocol switch { serial new SerialScaleAdapter(cfg, logger), tcp new TcpScaleAdapter(cfg, logger), tcp-v2 new TcpV2ScaleAdapter(cfg, logger), // 新增型号只加这里 sdk new SdkScaleAdapter(cfg, new VendorSdkBridge(cfg, logger), logger), _ throw new NotSupportedException($未注册的协议: {cfg.Protocol}) }; }改动点可以按下面这张表核对避免漏项改动点要改哪里验证方式新协议解析新增适配器类回放帧测试工厂注册WeightDeviceFactory配置切换能创建单位换算适配器或模型kg/g/t 用例SDK 依赖SdkBridge和输出目录位数与路径检查日志字段适配器日志原始帧可追溯我一般会先让解析器在离线回放里跑通再接一台真实秤做短时联调最后才放进产线灰度。从那以后我每次接新型号都强制走一遍“回放用例 位数检查 单位用例”不给自己留后悔药。源码包、测试工程和厂家 SDK 适配层放在同一份资源里拿到后先跑测试工程再按现场设备改配置。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?