using DeviceCommand.Base; using Model; using Model.Dto.Config; using Model.Entity.Config; using ORM; using Service.Interface; using Service.Interface.Config; using SqlSugar; using System; using System.Net; using System.Threading.Tasks; namespace Service.Implement.Config { /// /// 设备指令下发 服务实现 /// 目前无网关运行时/采集引擎:每次调用先落一条设备日志(始终可验证), /// 设备在线时才实际尝试 Modbus 写(连接参数从网关表按 GatewayId 加载)。 /// 每次下发都写审计日志(操作人/角色/时间/前后值),满足实验室审计要求。 /// public class DeviceCommandService : IDeviceCommandService { private readonly IAuditRecorder _audit; public DeviceCommandService(IAuditRecorder audit) { _audit = audit; } public async Task SendCommandAsync(DeviceCommandDto dto) { if (dto == null || !long.TryParse(dto.Id, out var deviceId) || deviceId <= 0) return Result.Error("设备Id无效"); if (string.IsNullOrWhiteSpace(dto.PointId) || !long.TryParse(dto.PointId, out var pointId) || pointId <= 0) return Result.Error("物模型点Id无效"); var now = DateTime.Now; // 载入设备与点 var device = await SqlSugarContext.DbContext.Queryable() .Where(x => x.Id == deviceId && x.IsDel == 0).FirstAsync(); if (device == null) return Result.Error("设备不存在或已被删除"); var point = await SqlSugarContext.DbContext.Queryable() .Where(x => x.Id == pointId && x.OwnerType == ThingOwnerTypeEnum.Device && x.OwnerId == deviceId && x.IsDel == 0).FirstAsync(); if (point == null) return Result.Error("物模型点不存在或不属于该设备"); // 校验可写 if (point.Rw == ThingRwEnum.ReadOnly) return Result.Error($"点【{point.Name ?? point.Code}】为只读,无法下发指令"); if (device.ProtocolType != IotDeviceProtocolEnum.ModbusTcp) return Result.Error("当前仅支持 Modbus TCP 协议设备下发指令"); try { // 模拟模式 / 设备离线 → 只记录、不发网络报文 if (dto.IsSimulated || device.OnlineStatus != IotDeviceOnlineStatusEnum.Online) { await WriteLogAsync(device, "Warn", "指令", dto.IsSimulated ? $"【模拟】点 {point.Name ?? point.Code} 下发值 {dto.Value}(未发送网络报文)" : $"设备离线,指令已记录但未发送(点 {point.Name ?? point.Code} 值 {dto.Value})"); return dto.IsSimulated ? Result.Success() : Result.Error("设备离线,指令已记录但未发送"); } // 从网关表加载连接参数 var gateway = await SqlSugarContext.DbContext.Queryable() .Where(x => x.Id == device.GatewayId && x.IsDel == 0).FirstAsync(); if (gateway == null) return Result.Error($"设备关联的网关不存在(GatewayId={device.GatewayId})"); if (string.IsNullOrWhiteSpace(gateway.Host) || !gateway.Port.HasValue) return Result.Error($"网关「{gateway.Code}」未配置 IP 地址或端口号"); var host = gateway.Host!; var port = gateway.Port!.Value; // 实际 Modbus 写 using var modbus = new ModbusTcp(); modbus.ConfigureDevice(host, port, 3000, 3000); bool connected = await modbus.ConnectAsync(); if (!connected) { await WriteLogAsync(device, "Error", "指令", $"网关 {gateway.Code}({host}:{port}) 连接失败,指令未发送(点 {point.Name ?? point.Code} 值 {dto.Value})"); return Result.Error($"网关 {gateway.Code} 连接失败,指令未发送"); } string desc; if (point.RegisterType == ThingRegisterTypeEnum.Coil) { bool coil = dto.Value != 0; await modbus.WriteSingleCoilAsync(device.SlaveId, point.Address, coil); desc = $"写线圈 {point.Name ?? point.Code} 地址 {point.Address} = {coil}"; } else if (point.DataType == ThingDataTypeEnum.Int32 || point.DataType == ThingDataTypeEnum.Float) { // 32 位类型占连续 2 个寄存器,走 FC16 连写(Int32 按有符号整数编码,Float 按 IEEE754 编码) ushort[] words = ToWords(dto.Value, point); await modbus.WriteMultipleRegistersAsync(device.SlaveId, point.Address, words); desc = $"写双寄存器 {point.Name ?? point.Code} 地址 {point.Address}~{point.Address + 1} = [{words[0]}, {words[1]}](工程值 {dto.Value})"; } else { ushort raw = ToRaw(dto.Value, point); await modbus.WriteSingleRegisterAsync(device.SlaveId, point.Address, raw); desc = $"写寄存器 {point.Name ?? point.Code} 地址 {point.Address} = {raw}(工程值 {dto.Value})"; } await SqlSugarContext.DbContext.Updateable() .SetColumns(x => new IotDeviceEntity { LastCollectTime = now, LastError = null }) .Where(x => x.Id == device.Id).ExecuteCommandAsync(); await WriteLogAsync(device, "Info", "指令", $"下发成功:{desc}"); await _audit.RecordAsync("Command", "IotDevice", device.Id, device.Id, oldValue: null, newValue: $"点={point.Code}, 值={dto.Value}, 模拟={dto.IsSimulated}", description: $"设备 {device.Code} 下发指令:{desc}"); return Result.Success(); } catch (Exception ex) { await WriteLogAsync(device, "Error", "指令", $"下发失败:{ex.Message}"); return Result.Error($"指令下发异常:{ex.Message}", ex); } } /// /// 工程值 → 寄存器原始值:raw = (value - Offset) / Scale,取整并夹到寄存器范围 /// Int16 按有符号处理(补码),其余类型按 0..65535 /// private static ushort ToRaw(double value, ThingModelPointEntity point) { double raw = (value - point.Offset) / point.Scale; if (point.DataType == ThingDataTypeEnum.Int16) { if (raw <= short.MinValue) return unchecked((ushort)short.MinValue); if (raw >= short.MaxValue) return (ushort)short.MaxValue; return unchecked((ushort)(short)Math.Round(raw)); } if (raw <= 0) return 0; if (raw >= 65535) return 65535; return (ushort)Math.Round(raw); } /// /// 工程值 → 32 位双寄存器字数组(raw32 = (value - Offset) / Scale) /// Int32 按有符号整数编码,Float 按 IEEE754 编码;字序按 Modbus 常规高字在前(AB CD) /// private static ushort[] ToWords(double value, ThingModelPointEntity point) { uint bits; if (point.DataType == ThingDataTypeEnum.Float) { bits = BitConverter.SingleToUInt32Bits((float)((value - point.Offset) / point.Scale)); } else { double raw = Math.Round((value - point.Offset) / point.Scale); if (raw < int.MinValue) raw = int.MinValue; if (raw > int.MaxValue) raw = int.MaxValue; bits = unchecked((uint)(int)raw); } return new[] { (ushort)(bits >> 16), (ushort)(bits & 0xFFFF) }; } /// /// 写一条设备日志(设备独享日志,前端设备日志抽屉可见) /// private static async Task WriteLogAsync(IotDeviceEntity device, string level, string logType, string message) { var now = DateTime.Now; await SqlSugarContext.DbContext.Insertable(new DeviceLogEntity { DeviceId = device.Id, DeviceCode = device.Code, Level = level, LogType = logType, Message = message, LogTime = now, CreateTime = now }).ExecuteCommandAsync(); } } }