Files
IOT_API/Service/Implement/Config/DeviceCommandService.cs
T

179 lines
8.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using DeviceCommand.Base;
using Model;
using Model.Dto.Config;
using Model.Entity.Config;
using ORM;
using Service.Interface.Config;
using SqlSugar;
using System;
using System.Net;
using System.Threading.Tasks;
namespace Service.Implement.Config
{
/// <summary>
/// 设备指令下发 服务实现
/// 目前无网关运行时/采集引擎:每次调用先落一条设备日志(始终可验证),
/// 设备在线时才实际尝试 Modbus 写(GatewayCode 需为 "ip:port",默认 127.0.0.1:502)。
/// </summary>
public class DeviceCommandService : IDeviceCommandService
{
public async Task<Result> 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<IotDeviceEntity>()
.Where(x => x.Id == deviceId && x.IsDel == 0).FirstAsync();
if (device == null)
return Result.Error("设备不存在或已被删除");
var point = await SqlSugarContext.DbContext.Queryable<ThingModelPointEntity>()
.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("设备离线,指令已记录但未发送");
}
// 实际 Modbus 写
var (host, port) = ParseGateway(device.GatewayCode);
using var modbus = new ModbusTcp();
modbus.ConfigureDevice(host, port, 3000, 3000);
bool connected = await modbus.ConnectAsync();
if (!connected)
{
await WriteLogAsync(device, "Error", "指令",
$"网关 {device.GatewayCode} 连接失败,指令未发送(点 {point.Name ?? point.Code} 值 {dto.Value}");
return Result.Error($"网关 {device.GatewayCode} 连接失败,指令未发送");
}
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<IotDeviceEntity>()
.SetColumns(x => new IotDeviceEntity { LastCollectTime = now, LastError = null })
.Where(x => x.Id == device.Id).ExecuteCommandAsync();
await WriteLogAsync(device, "Info", "指令", $"下发成功:{desc}");
return Result.Success();
}
catch (Exception ex)
{
await WriteLogAsync(device, "Error", "指令", $"下发失败:{ex.Message}");
return Result.Error($"指令下发异常:{ex.Message}", ex);
}
}
/// <summary>
/// 工程值 → 寄存器原始值:raw = (value - Offset) / Scale,取整并夹到寄存器范围
/// Int16 按有符号处理(补码),其余类型按 0..65535
/// </summary>
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);
}
/// <summary>
/// 工程值 → 32 位双寄存器字数组(raw32 = (value - Offset) / Scale
/// Int32 按有符号整数编码,Float 按 IEEE754 编码;字序按 Modbus 常规高字在前(AB CD
/// </summary>
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) };
}
/// <summary>
/// 解析 GatewayCode 为 host:port;非法/缺失回退 127.0.0.1:502
/// </summary>
private static (string host, int port) ParseGateway(string? gatewayCode)
{
if (!string.IsNullOrWhiteSpace(gatewayCode))
{
var idx = gatewayCode.LastIndexOf(':');
if (idx > 0 && IPAddress.TryParse(gatewayCode[..idx], out _) && int.TryParse(gatewayCode[(idx + 1)..], out var p) && p > 0)
return (gatewayCode[..idx], p);
}
return ("127.0.0.1", 502);
}
/// <summary>
/// 写一条设备日志(设备独享日志,前端设备日志抽屉可见)
/// </summary>
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();
}
}
}