即使对方给的协议,modbus 青鸟
我还是不知道怎么写,因为往往上位机如何使用这些接口数据也不是那么简单,就3个接口,但是使用起来却也有些麻烦,那么怎么编写这个驱动呢
现在有了AI编程确实厉害,我不懂.net,它能帮我按照我想法编出来,但作为开发工程师还是要看懂它到底写了些什么?
驱动
驱动注解
c#中规定:如果一个类继承自 System.Attribute,那么在使用时,可以省略类名的 Attribute 后缀。下面代码相当于定义了Driver的一个注解
// AttributeTargets.Class,类似@Target(ElementType.TYPE),标识只能用在类上// Inherited = false,子类不继承这个注解,Java 里还要加 @Retention(RetentionPolicy.RUNTIME),否则反射读不到。C# 默认就是运行时可见,不用额外声明。[AttributeUsage(AttributeTargets.Class,Inherited=false)]publicsealedclassDriverAttribute(stringname):Attribute{publicstringName{get;}=name;}// sealed 标识不能被集成,跟java的final一样[Driver("jbf293k")]publicsealedclassJbf293kDriver:IDeviceDriver{// C# 可空引用类型。null! 意思是“我知道这里是 null,但编译器你别警告我,我保证后面会赋值”。Java 没这个概念。privateDeviceContext_ctx=null!;// 问号表示这个字段可以为 null。Java 里所有对象都可为 null,C# 默认不可为 null(需要显式加 ?)privateIFrameTransport?_transport;// C# 9 的目标类型 new,等价于 new SemaphoreSlim(1, 1)。Java 10 也有 var,但 C# 这个更激进,连类型名都省了。privatereadonlySemaphoreSlim_ioLock=new(1,1);privatebool_isTcp;privatebyte_unit;privatestring_machineType="alarm";// ushort:无符号 16 位整数(0~65535),Java 没有,只能用 int 代替。privateushort_tid;// => 是表达式体成员,等价于 { return ...; }。publicstringDriverName=>"jbf293k";// { get; } 是只读属性,只能读不能写,类似 Java 的 final 字段 + getter。// SupportsPolling: true 是命名参数,Java 没有,只能按构造函数参数顺序传。但python有publicDriverCapabilitiesCapabilities{get;}=new(SupportsPolling:true,SupportsReport:false,SupportsCommands:false,DefaultPollIntervalMs:2000);// C# 的 event 是委托类型的事件,类似 Java 的监听器模式。// `event`:事件,本质是对委托的封装。对外只能 `+=`(add)、`-=`(remove),不能直接赋值。 // `?`:可空,这个事件可以是 `null`,没有订阅者时就是 null// `add`:当外部代码 `obj.OnTelemetry += Handler` 的时候,执行 `add` 里面的代码// `remove`:当外部代码 `obj.OnTelemetry -= Handler` 的时候,执行 `remove` 里面的代码// 纯轮询型:上报事件不支持publiceventFunc<Telemetry,Task>?OnTelemetry{add{}remove{}}/// <summary>/// 这是驱动启动时调用的方法,相当于 Java 里的 @PostConstruct 或 init()。/// Task 是 C# 的异步返回类型,等价于 Java 的 CompletableFuture。Task(无泛型)相当于 CompletableFuture<Void>。/// CancellationToken 是 C# 的取消令牌,等价于 Java 的 Thread.interrupt() 或 Reactor 的 Subscription。只是用途上有部分重叠,底层模型完全不一样。/// </summary>publicTaskStartAsync(DeviceContextcontext,CancellationTokenct){// 保存上下文_ctx=context;varconn=context.Connection;// 从站地址,如果配置为0,默认用1_unit=(byte)(conn.UnitId==0?1:conn.UnitId);// 读取配置// ?? 是空合并运算符,等价于 Java 的 != null ? :。// Dictionary类java中的Map,键值对容器,也是非线程安全varopts=conn.Options??newDictionary<string,string>();// TryGetValue("machineType", out var mt) 是 C# 的输出参数,Java 没有// 如果字典里有 machineType 这个键,返回 true,并且把**键对应的 value 塞给 mt**;如果**没有这个键**,返回false,mt=null。 _machineType=opts.TryGetValue("machineType",outvarmt)&&mt.Length>0?mt:"alarm";// 位表字典即 machineType 合法值清单,启动即校验(非法配置不能静默降级)_=Jbf293kProtocol.BitTableFor(7,_machineType);// 线格式:options.protocol 显式指定(tcp/rtu),缺省按连接类型推导(serial → RTU,其余 → TCP)varprotocol=opts.TryGetValue("protocol",outvarp)&&p.Length>0?p.ToLowerInvariant():(conn.ConnectionType.Equals("serial",StringComparison.OrdinalIgnoreCase)?"rtu":"tcp");_isTcp=protocolswitch{"tcp"=>true,"rtu"=>false,_=>thrownewNotSupportedException($"options.protocol '{protocol}' 非法(支持 tcp / rtu)"),};// 非法组合启动即报错(能力声明不能静默降级)varctLower=conn.ConnectionType.ToLowerInvariant();if(_isTcp&&ctLowerisnot("tcp"))thrownewNotSupportedException($"Modbus TCP 线格式要求 connectionType=tcp(ZLAN 透传),当前 '{conn.ConnectionType}'");if(!_isTcp&&ctLowerisnot("serial"or"tcp"))thrownewNotSupportedException($"Modbus RTU 线格式要求 connectionType=serial(485 直连)或 tcp(RTU over TCP),当前 '{conn.ConnectionType}'");// 点表全量预校验:编址合法 + 位名存在于机型位表 + 属性名不重复// StringComparer.Ordinal,序号比较(二进制,逐 char 编码值对比;区分大小写;速度最快;文化无关// StringComparer.OrdinalIgnoreCase 序号比较,忽略大小写,二进制对比,大小写不敏感;工业场景常用varseen=newHashSet<string>(StringComparer.Ordinal);foreach(varpointincontext.Points){// 元组解构 var (loop, part, bit) = ...:C# 特有var(loop,part,bit)=Jbf293kProtocol.ParseRawPoint(point.RawPoint);// _ = 又是“不关心返回值”。if(bitisnotnull)_=Jbf293kProtocol.FindBit(loop,_machineType,bit);// HashSet.Add 返回 bool,如果元素已存在返回 false,!false = true,触发异常。等价于 Java 的 if (!seen.add(...)) throw ...。if(!seen.Add(point.Property))// $"..." 是字符串插值,等价于 Java 15+ 的 "..." + variable 或 String.formatthrownewInvalidDataException($"点表属性名重复:{point.Property}");}// 表示“同步完成的 Task”,等价于 Java 的 CompletableFuture.completedFuture(null)returnTask.CompletedTask;}// 核心轮询逻辑// async 表示方法内部有 await,等价于 Java 的 CompletableFuture 链式调用。publicasyncTask<IReadOnlyList<Telemetry>>PollAsync(CancellationTokenct){// 加锁await_ioLock.WaitAsync(ct);try{// ??= 是空合并赋值:如果左边是 null,就赋值;否则不变。_transport??=FrameTransport.Create(_ctx.Connection);if(!_transport.IsConnected)await_transport.ConnectAsync(ct);// 断线由 DriverRuntime 退避重试// 解析点表// Select 等价于 Java 的 stream().map()// (Pt: pt, Loop: 0, ...) 是命名元组,C# 特有// 按回路 × 页分组:同一回路的点只可能落在两页(1~100 / 101~200),每页整查 100 个寄存器varparsed=_ctx.Points.Select(pt=>(Pt:pt,Loop:0,Part:0,Bit:(string?)null)).ToList();for(inti=0;i<parsed.Count;i++){var(loop,part,bit)=Jbf293kProtocol.ParseRawPoint(parsed[i].Pt.RawPoint);parsed[i]=(parsed[i].Pt,loop,part,bit);}// 按回路分组轮询varprops=newList<TelemetryProperty>(_ctx.Points.Count);// GroupBy(x => x.Loop) = Java 的 Collectors.groupingBy。// OrderBy(g => g.Key) = Java 的 sorted(Comparator.comparing(...))。// grp.Key = 分组键(回路号)// BitTableFor(loop, _machineType):取该回路的位表。//foreach(vargrpinparsed.GroupBy(x=>x.Loop).OrderBy(g=>g.Key)){intloop=grp.Key;vartable=Jbf293kProtocol.BitTableFor(loop,_machineType);// PageStarts = [1, 101],两页。foreach(varpageStartinJbf293kProtocol.PageStarts){// grp.Where(...) = Java 的 filter。// item.Part >= pageStart && item.Part < pageStart + 100:判断这个点属于哪一页varpagePoints=grp.Where(x=>x.Part>=pageStart&&x.Part<pageStart+Jbf293kProtocol.PageQuantity).ToList();if(pagePoints.Count==0)continue;ushortstart=Jbf293kProtocol.PageAddress(loop,pageStart);ushorttid=_tid++;// 构造请求帧varrequest=Jbf293kProtocol.BuildReadRequest(_isTcp,_unit,start,tid);await_transport.WriteAsync(request,ct);// 读应答,返回 200 字节数据。vardata=awaitJbf293kProtocol.ReadResponseAsync(_transport,_isTcp,_unit,tid,ct);foreach(variteminpagePoints){// 算出这个点在 200 字节数据里的位置intoffset=item.Part-pageStart;// 部位号在页内偏移 = 部位号 - 页起始// 取出对应的 2 字节寄存器值。ushortreg=Jbf293kProtocol.RegisterAt(data,offset);props.Add(item.Bitisnull?newTelemetryProperty(item.Pt.Property,Jbf293kProtocol.Summary(reg,table)):newTelemetryProperty(item.Pt.Property,Jbf293kProtocol.ReadBit(reg,Jbf293kProtocol.FindBit(loop,_machineType,item.Bit))));}}}// 构造一个遥测属性return[newTelemetry(_ctx.DeviceId,DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),props,_ctx.ProductKey)];}// 释放锁finally{_ioLock.Release();}}// => 表达式体,等价于 Java 的 { return ...; }。// 这个方法直接返回“不支持命令”,因为 JBF293K 是只读设备。publicTask<CommandResult>InvokeAsync(DeviceCommandcommand,CancellationTokenct)=>Task.FromResult(newCommandResult(false,$"驱动{DriverName}为只读火警输出卡(协议仅 0x03 读保持寄存器),不支持命令 '{command.Name}'"));// ValueTask 是 Task 的轻量版,用于可能同步完成的异步方法。Java 没有直接对应,用 CompletableFuture 就行。publicasyncValueTaskDisposeAsync(){// _transport is { } t 是模式匹配:如果 _transport 不是 null,就赋给 t。等价于 Java 的 if (_transport != null) { var t = _transport; ... }。if(_transportis{}t)awaitt.DisposeAsync();_ioLock.Dispose();}}