feat: 网关管理模块 + RBAC权限认证 + 组织架构 + 审计日志
- 网关管理:GatewayEntity/Service/Controller CRUD + 测试连接;设备 GatewayId 外键关联 - JWT 认证:登录签发 Token(权限编码写入 Claims)、RequirePermission 权限过滤器、CurrentUser 上下文 - RBAC:用户/角色/权限实体与 CRUD,预定义 4 角色 + 6 权限种子(admin/admin123) - 组织架构:sys_org 固定层级树(公司/实验室/部门/班组白夜班),层级校验,用户挂 OrgId/岗位/技能标签 - 数据权限:角色 DataScope(全部/本组织及下级),用户列表按组织子树过滤 - 防锁死保护:禁止删/禁自己,保证至少一名活跃管理员,角色摘除 user:manage 前校验 - 审计日志:AuditRecorder 接入设备增删改/指令下发/登录登出/组织变更,AuditController 查询 - 设备指令下发按网关表取连接参数;设备列表支持 gatewayId/productId 筛选 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ using Model;
|
||||
using Model.Dto.Config;
|
||||
using Model.Entity.Config;
|
||||
using ORM;
|
||||
using Service.Interface;
|
||||
using Service.Interface.Config;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
@@ -14,10 +15,18 @@ namespace Service.Implement.Config
|
||||
/// <summary>
|
||||
/// 设备指令下发 服务实现
|
||||
/// 目前无网关运行时/采集引擎:每次调用先落一条设备日志(始终可验证),
|
||||
/// 设备在线时才实际尝试 Modbus 写(GatewayCode 需为 "ip:port",默认 127.0.0.1:502)。
|
||||
/// 设备在线时才实际尝试 Modbus 写(连接参数从网关表按 GatewayId 加载)。
|
||||
/// 每次下发都写审计日志(操作人/角色/时间/前后值),满足实验室审计要求。
|
||||
/// </summary>
|
||||
public class DeviceCommandService : IDeviceCommandService
|
||||
{
|
||||
private readonly IAuditRecorder _audit;
|
||||
|
||||
public DeviceCommandService(IAuditRecorder audit)
|
||||
{
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<Result> SendCommandAsync(DeviceCommandDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.Id, out var deviceId) || deviceId <= 0)
|
||||
@@ -57,16 +66,26 @@ namespace Service.Implement.Config
|
||||
: Result.Error("设备离线,指令已记录但未发送");
|
||||
}
|
||||
|
||||
// 从网关表加载连接参数
|
||||
var gateway = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||||
.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 写
|
||||
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} 连接失败,指令未发送");
|
||||
$"网关 {gateway.Code}({host}:{port}) 连接失败,指令未发送(点 {point.Name ?? point.Code} 值 {dto.Value})");
|
||||
return Result.Error($"网关 {gateway.Code} 连接失败,指令未发送");
|
||||
}
|
||||
|
||||
string desc;
|
||||
@@ -95,6 +114,10 @@ namespace Service.Implement.Config
|
||||
.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)
|
||||
@@ -143,20 +166,6 @@ namespace Service.Implement.Config
|
||||
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>
|
||||
|
||||
@@ -3,11 +3,13 @@ using Model.Dto.Config;
|
||||
using Model.Entity.Config;
|
||||
using Model.Mapper;
|
||||
using ORM;
|
||||
using Service.Interface;
|
||||
using Service.Interface.Config;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement.Config
|
||||
@@ -17,10 +19,17 @@ namespace Service.Implement.Config
|
||||
/// </summary>
|
||||
public class DeviceService : IDeviceService
|
||||
{
|
||||
private readonly IAuditRecorder _audit;
|
||||
|
||||
public DeviceService(IAuditRecorder audit)
|
||||
{
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询设备列表(支持关键字搜索编号/名称/类型,可按所属产品筛选)
|
||||
/// </summary>
|
||||
public async Task<Result<List<IotDeviceDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword, long? productId = null)
|
||||
public async Task<Result<List<IotDeviceDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword, long? productId = null, long? gatewayId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -29,11 +38,13 @@ namespace Service.Implement.Config
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword),
|
||||
x => x.Code.Contains(keyword!) || x.Name.Contains(keyword!) || x.DeviceType.Contains(keyword!))
|
||||
.WhereIF(productId.HasValue && productId.Value > 0, x => x.ProductId == productId!.Value)
|
||||
.WhereIF(gatewayId.HasValue && gatewayId.Value > 0, x => x.GatewayId == gatewayId!.Value)
|
||||
.OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
|
||||
var dtos = list.ToDtoList();
|
||||
await FillProductNames(dtos);
|
||||
await FillGatewayNames(dtos);
|
||||
return Result<List<IotDeviceDto>>.Success(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -57,6 +68,7 @@ namespace Service.Implement.Config
|
||||
|
||||
var dto = entity.ToDto();
|
||||
await FillProductNames(new List<IotDeviceDto> { dto });
|
||||
await FillGatewayNames(new List<IotDeviceDto> { dto });
|
||||
return Result<IotDeviceDto>.Success(dto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -87,6 +99,10 @@ namespace Service.Implement.Config
|
||||
// 初始离线
|
||||
entity.OnlineStatus = IotDeviceOnlineStatusEnum.Offline;
|
||||
await SqlSugarContext.DbContext.Insertable(entity).ExecuteCommandAsync();
|
||||
|
||||
await _audit.RecordAsync("Create", "IotDevice", entity.Id, entity.Id,
|
||||
null, JsonSerializer.Serialize(new { entity.Code, entity.Name, entity.DeviceType, entity.GatewayId, entity.SlaveId }),
|
||||
description: $"新增设备 {entity.Code}({entity.Name})");
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -112,9 +128,18 @@ namespace Service.Implement.Config
|
||||
if (exists)
|
||||
return Result.Error($"设备编号【{entity.Code}】已存在");
|
||||
|
||||
// 操作前值(审计):先查旧记录
|
||||
var oldEntity = await SqlSugarContext.DbContext.Queryable<IotDeviceEntity>()
|
||||
.Where(x => x.Id == entity.Id).FirstAsync();
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable(entity)
|
||||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel, x.OnlineStatus, x.LastCollectTime, x.LastError })
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
await _audit.RecordAsync("Update", "IotDevice", entity.Id, entity.Id,
|
||||
oldValue: oldEntity == null ? null : JsonSerializer.Serialize(new { oldEntity.Code, oldEntity.Name, oldEntity.DeviceType, oldEntity.GatewayId, oldEntity.SlaveId }),
|
||||
newValue: JsonSerializer.Serialize(new { entity.Code, entity.Name, entity.DeviceType, entity.GatewayId, entity.SlaveId }),
|
||||
description: $"修改设备 {entity.Code}({entity.Name})");
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -133,10 +158,17 @@ namespace Service.Implement.Config
|
||||
|
||||
try
|
||||
{
|
||||
var oldEntity = await SqlSugarContext.DbContext.Queryable<IotDeviceEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<IotDeviceEntity>()
|
||||
.SetColumns(x => x.IsDel == 1)
|
||||
.Where(x => x.Id == id)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
await _audit.RecordAsync("Delete", "IotDevice", id, id,
|
||||
oldValue: oldEntity == null ? null : JsonSerializer.Serialize(new { oldEntity.Code, oldEntity.Name }),
|
||||
description: $"删除设备 {oldEntity?.Code}({oldEntity?.Name})");
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -165,6 +197,31 @@ namespace Service.Implement.Config
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 给设备 DTO 填充所属网关名称(GatewayName 展示用)
|
||||
/// </summary>
|
||||
private async Task FillGatewayNames(List<IotDeviceDto> dtos)
|
||||
{
|
||||
if (dtos == null || dtos.Count == 0)
|
||||
return;
|
||||
|
||||
var ids = dtos.Where(x => long.TryParse(x.GatewayId, out var gid) && gid > 0)
|
||||
.Select(x => long.Parse(x.GatewayId!)).Distinct().ToList();
|
||||
if (ids.Count == 0)
|
||||
return;
|
||||
|
||||
var gateways = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||||
.Where(x => ids.Contains(x.Id) && x.IsDel == 0)
|
||||
.ToListAsync();
|
||||
var map = gateways.ToDictionary(g => g.Id, g => $"{g.Code}({g.Name})");
|
||||
|
||||
foreach (var d in dtos)
|
||||
{
|
||||
if (long.TryParse(d.GatewayId, out var gid) && map.TryGetValue(gid, out var name))
|
||||
d.GatewayName = name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 给设备 DTO 填充所属产品的型号/名称(ProductName 展示用)
|
||||
/// </summary>
|
||||
|
||||
@@ -1,12 +1,262 @@
|
||||
using Service.Interface;
|
||||
using Model;
|
||||
using Model.Dto.Config;
|
||||
using Model.Entity.Config;
|
||||
using Model.Mapper;
|
||||
using ORM;
|
||||
using Service.Interface.Config;
|
||||
using SqlSugar;
|
||||
using System.IO.Ports;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Service.Implement
|
||||
namespace Service.Implement.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// 网关管理 服务实现
|
||||
/// </summary>
|
||||
public class GatewayService : IGatewayService
|
||||
{
|
||||
// TODO: 实现 网关管理 相关方法
|
||||
public async Task<Result<List<GatewayDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, int? protocolType = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||||
.Where(x => x.IsDel == 0)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Code.Contains(keyword!) || x.Name.Contains(keyword!))
|
||||
.WhereIF(protocolType.HasValue, x => x.ProtocolType == (IotDeviceProtocolEnum)protocolType!.Value)
|
||||
.OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
return Result<List<GatewayDto>>.Success(list.ToDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<GatewayDto>>.Error("查询网关列表失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<GatewayDto>> GetByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<GatewayDto>.Error("网关不存在");
|
||||
return Result<GatewayDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<GatewayDto>.Error("查询网关详情失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<GatewayDto>> AddAsync(GatewayDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 编码唯一校验
|
||||
var exists = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||||
.Where(x => x.Code == dto.Code && x.IsDel == 0).AnyAsync();
|
||||
if (exists) return Result<GatewayDto>.Error($"网关编码「{dto.Code}」已存在");
|
||||
|
||||
var entity = dto.ToEntity();
|
||||
entity.Id = 0; // 雪花ID自动生成
|
||||
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
|
||||
entity.Id = id;
|
||||
return Result<GatewayDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<GatewayDto>.Error("新增网关失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<GatewayDto>> UpdateAsync(GatewayDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!long.TryParse(dto.Id, out var id) || id <= 0)
|
||||
return Result<GatewayDto>.Error("网关Id无效");
|
||||
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<GatewayDto>.Error("网关不存在");
|
||||
|
||||
// 编码唯一校验(排除自身)
|
||||
var codeExists = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||||
.Where(x => x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||
if (codeExists) return Result<GatewayDto>.Error($"网关编码「{dto.Code}」已存在");
|
||||
|
||||
var newEntity = dto.ToEntity();
|
||||
newEntity.Id = id;
|
||||
await SqlSugarContext.DbContext.Updateable(newEntity)
|
||||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
|
||||
.ExecuteCommandAsync();
|
||||
return Result<GatewayDto>.Success(newEntity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<GatewayDto>.Error("修改网关失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result.Error("网关不存在");
|
||||
|
||||
// 软删除网关
|
||||
await SqlSugarContext.DbContext.Updateable<GatewayEntity>()
|
||||
.SetColumns(x => x.IsDel == 1)
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
|
||||
// 该网关下设备的 GatewayId 置 0(不级联删除设备)
|
||||
await SqlSugarContext.DbContext.Updateable<IotDeviceEntity>()
|
||||
.SetColumns(x => x.GatewayId == 0)
|
||||
.Where(x => x.GatewayId == id).ExecuteCommandAsync();
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("删除网关失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试网关连接:TCP 尝试 TcpClient.ConnectAsync 3s 超时,串口尝试 SerialPort.Open,
|
||||
/// 成功则 OnlineStatus=Online+更新 LastConnectedTime,失败则 OnlineStatus=Fault+记录 LastError
|
||||
/// </summary>
|
||||
public async Task<Result<GatewayTestResultDto>> TestConnectionAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<GatewayTestResultDto>.Error("网关不存在");
|
||||
|
||||
bool success = false;
|
||||
string message = "";
|
||||
var now = DateTime.Now;
|
||||
|
||||
switch (entity.ProtocolType)
|
||||
{
|
||||
case IotDeviceProtocolEnum.ModbusTcp:
|
||||
case IotDeviceProtocolEnum.Tcp:
|
||||
case IotDeviceProtocolEnum.S7:
|
||||
// TCP 类:尝试连接 IP:Port
|
||||
if (string.IsNullOrWhiteSpace(entity.Host) || !entity.Port.HasValue)
|
||||
{
|
||||
message = "未配置 IP 地址或端口号";
|
||||
}
|
||||
else
|
||||
{
|
||||
(success, message) = await TestTcpAsync(entity.Host, entity.Port!.Value, 3000);
|
||||
}
|
||||
break;
|
||||
|
||||
case IotDeviceProtocolEnum.ModbusRtu:
|
||||
case IotDeviceProtocolEnum.Serial:
|
||||
// 串口类:尝试打开串口
|
||||
if (string.IsNullOrWhiteSpace(entity.ComPort))
|
||||
{
|
||||
message = "未配置串口号";
|
||||
}
|
||||
else
|
||||
{
|
||||
(success, message) = TestSerial(entity.ComPort, entity.BaudRate ?? 9600);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
message = $"暂不支持 {entity.ProtocolType} 协议的连接测试";
|
||||
break;
|
||||
}
|
||||
|
||||
// 更新网关状态
|
||||
await SqlSugarContext.DbContext.Updateable<GatewayEntity>()
|
||||
.SetColumns(x => new GatewayEntity
|
||||
{
|
||||
OnlineStatus = success ? (byte)1 : (byte)3,
|
||||
LastConnectedTime = success ? now : x.LastConnectedTime,
|
||||
LastError = success ? null : message
|
||||
})
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
|
||||
return Result<GatewayTestResultDto>.Success(new GatewayTestResultDto
|
||||
{
|
||||
Success = success,
|
||||
Message = success ? $"连接成功({entity.Host}:{entity.Port ?? 0})" : message
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<GatewayTestResultDto>.Error("测试连接失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<List<GatewayOptionDto>>> GetOptionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.IsEnabled)
|
||||
.OrderBy(x => x.Code)
|
||||
.ToListAsync();
|
||||
return Result<List<GatewayOptionDto>>.Success(list.ToOptionDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<GatewayOptionDto>>.Error("查询网关选项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
#region 私有:连接测试方法
|
||||
|
||||
private static async Task<(bool success, string message)> TestTcpAsync(string host, int port, int timeoutMs)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
var cts = new System.Threading.CancellationTokenSource(timeoutMs);
|
||||
await client.ConnectAsync(host, port, cts.Token);
|
||||
return (true, "");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return (false, $"连接超时({timeoutMs / 1000}s):{host}:{port}");
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
return (false, $"连接失败:{host}:{port}({ex.Message})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, $"连接异常:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static (bool success, string message) TestSerial(string comPort, int baudRate)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var sp = new SerialPort(comPort, baudRate);
|
||||
sp.Open();
|
||||
sp.Close();
|
||||
return (true, "");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return (false, $"串口 {comPort} 被占用");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, $"打开串口 {comPort} 失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user