Compare commits

...

4 Commits

Author SHA1 Message Date
07e3abdf73 fix: s7地址解析 2026-03-11 15:05:04 +08:00
ffb0947e32 fix: s7地址解析 2026-03-11 10:34:35 +08:00
12e8483804 fix: 读写 2026-03-11 10:14:17 +08:00
e8445bb286 feat: da测试 2026-03-09 11:29:48 +08:00
12 changed files with 845 additions and 104 deletions

View File

@@ -0,0 +1,122 @@
package org.nl.iot.core.driver;
import lombok.extern.slf4j.Slf4j;
import org.nl.common.exception.CommonException;
import org.nl.iot.core.driver.protocol.modbustcp.ModBusProtocolDriverImpl;
import org.nl.iot.core.driver.protocol.opcda.OpcDaProtocolDriverImpl;
import org.nl.iot.core.driver.protocol.opcua.OpcUaProtocolDriverImpl;
import org.nl.iot.core.driver.protocol.plcs7.PlcS7ProtocolDriverImpl;
import org.nl.iot.core.driver.service.DriverCustomService;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定义通信协议驱动工厂
* 通过协议编码获取对应的协议驱动单例对象
*
* <p>重要说明:
* <ul>
* <li>工厂返回的驱动实例是 Spring 容器中的 Bean 对象(单例)</li>
* <li>所有驱动类都标注了 @Service 注解,由 Spring 管理生命周期</li>
* <li>工厂通过构造函数注入获取这些 Bean并直接返回引用</li>
* <li>多次调用 getDriver() 返回的是同一个 Spring Bean 实例</li>
* </ul>
*
* @author: lyd
* @date: 2026/3/2
*/
@Slf4j
@Component
public class DriverCustomFactory {
private final ModBusProtocolDriverImpl modBusProtocolDriver;
private final OpcUaProtocolDriverImpl opcUaProtocolDriver;
private final PlcS7ProtocolDriverImpl plcS7ProtocolDriver;
private final OpcDaProtocolDriverImpl opcDaProtocolDriver;
/**
* 协议驱动缓存Map
* key: 协议编码MODBUS、OPCUA、PLCS7、OPCDA
* value: 对应的协议驱动实例
*/
private final Map<String, DriverCustomService> driverMap = new ConcurrentHashMap<>();
/**
* 构造函数注入所有协议驱动实例
*/
public DriverCustomFactory(ModBusProtocolDriverImpl modBusProtocolDriver,
OpcUaProtocolDriverImpl opcUaProtocolDriver,
PlcS7ProtocolDriverImpl plcS7ProtocolDriver,
OpcDaProtocolDriverImpl opcDaProtocolDriver) {
this.modBusProtocolDriver = modBusProtocolDriver;
this.opcUaProtocolDriver = opcUaProtocolDriver;
this.plcS7ProtocolDriver = plcS7ProtocolDriver;
this.opcDaProtocolDriver = opcDaProtocolDriver;
// 初始化协议驱动映射
initDriverMap();
}
/**
* 初始化协议驱动映射关系
*/
private void initDriverMap() {
driverMap.put("MODBUS", modBusProtocolDriver);
driverMap.put("MODBUS-TCP", modBusProtocolDriver);
driverMap.put("OPCUA", opcUaProtocolDriver);
driverMap.put("OPC-UA", opcUaProtocolDriver);
driverMap.put("PLCS7", plcS7ProtocolDriver);
driverMap.put("PLC-S7", plcS7ProtocolDriver);
driverMap.put("OPCDA", opcDaProtocolDriver);
driverMap.put("OPC-DA", opcDaProtocolDriver);
log.info("协议驱动工厂初始化完成,支持的协议: {}", driverMap.keySet());
}
/**
* 根据协议编码获取对应的协议驱动单例对象
*
* <p>返回的驱动实例是 Spring 容器中的 Bean 对象(单例):
* <ul>
* <li>工厂不会创建新对象,只是返回已注入的 Spring Bean 引用</li>
* <li>多次调用返回的是同一个对象(使用 == 比较为 true</li>
* <li>与直接 @Autowired 注入的驱动 Bean 是同一个实例</li>
* </ul>
*
* @param protocolCode 协议编码MODBUS、OPCUA、PLCS7、OPCDA不区分大小写
* @return 对应的协议驱动实例Spring Bean 单例对象)
* @throws CommonException 如果协议编码不支持,抛出异常
*/
public DriverCustomService getDriver(String protocolCode) {
if (protocolCode == null || protocolCode.trim().isEmpty()) {
throw new CommonException("协议编码不能为空");
}
// 转换为大写进行匹配
String upperProtocolCode = protocolCode.trim().toUpperCase();
DriverCustomService driver = driverMap.get(upperProtocolCode);
if (driver == null) {
log.error("不支持的协议编码: {}", protocolCode);
throw new CommonException("不支持的协议编码: " + protocolCode + ",支持的协议: " + driverMap.keySet());
}
log.debug("获取协议驱动: {} -> {}", protocolCode, driver.getClass().getSimpleName());
return driver;
}
/**
* 检查是否支持指定的协议编码
*
* @param protocolCode 协议编码
* @return 如果支持返回true否则返回false
*/
public boolean isSupported(String protocolCode) {
if (protocolCode == null || protocolCode.trim().isEmpty()) {
return false;
}
return driverMap.containsKey(protocolCode.trim().toUpperCase());
}
}

View File

@@ -0,0 +1,30 @@
package org.nl.iot.core.driver.bo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.nl.iot.core.driver.enums.MetadataOperateTypeEnum;
import java.io.Serial;
import java.io.Serializable;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MetadataEventDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 连接id
*/
private String connectId;
/**
* 元数据操作类型, 新增, 删除, 修改
*/
private MetadataOperateTypeEnum operateType;
}

View File

@@ -16,15 +16,6 @@ import org.nl.iot.modular.iot.entity.IotConnect;
@NoArgsConstructor
@AllArgsConstructor
public class RValue {
/**
* 设备
*/
// private DeviceBO device;
/**
* 位号 - 子设备
*/
// private PointBO point;
/**
* 配置
*/

View File

@@ -0,0 +1,80 @@
package org.nl.iot.core.driver.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Optional;
@Getter
@AllArgsConstructor
public enum MetadataOperateTypeEnum {
/**
* 新增
*/
ADD((byte) 0, "add", "新增"),
/**
* 删除
*/
DELETE((byte) 1, "delete", "删除"),
/**
* 更新
*/
UPDATE((byte) 2, "update", "更新"),
;
/**
* 索引
*/
@EnumValue
private final Byte index;
/**
* 编码
*/
private final String code;
/**
* 内容
*/
private final String remark;
/**
* 根据枚举索引获取枚举
*
* @param index 索引
* @return {@link MetadataOperateTypeEnum}
*/
public static MetadataOperateTypeEnum ofIndex(Byte index) {
Optional<MetadataOperateTypeEnum> any = Arrays.stream(MetadataOperateTypeEnum.values()).filter(type -> type.getIndex().equals(index)).findFirst();
return any.orElse(null);
}
/**
* 根据枚举编码获取枚举
*
* @param code 编码
* @return {@link MetadataOperateTypeEnum}
*/
public static MetadataOperateTypeEnum ofCode(String code) {
Optional<MetadataOperateTypeEnum> any = Arrays.stream(MetadataOperateTypeEnum.values()).filter(type -> type.getCode().equals(code)).findFirst();
return any.orElse(null);
}
/**
* 根据枚举内容获取枚举
*
* @param name 枚举内容
* @return {@link MetadataOperateTypeEnum}
*/
public static MetadataOperateTypeEnum ofName(String name) {
try {
return valueOf(name);
} catch (IllegalArgumentException e) {
return null;
}
}
}

View File

@@ -1,11 +1,14 @@
package org.nl.iot.core.driver.protocol.modbustcp;
import com.alibaba.fastjson.JSONObject;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.nl.common.exception.CommonException;
import org.nl.iot.core.driver.bo.AttributeBO;
import org.nl.iot.core.driver.bo.MetadataEventDTO;
import org.nl.iot.core.driver.entity.RValue;
import org.nl.iot.core.driver.entity.WValue;
import org.nl.iot.core.driver.enums.MetadataOperateTypeEnum;
import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusFactory;
import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusMaster;
import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
@@ -50,20 +53,31 @@ public class ModBusProtocolDriverImpl implements DriverCustomService {
}
@Override
public RValue read(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect connect, IotConfig config) {
return new RValue(config, connect, readValue(getConnector(connect.getId().toString(), driverConfig), pointConfig));
public void event(MetadataEventDTO metadataEvent) {
MetadataOperateTypeEnum operateType = metadataEvent.getOperateType();
log.info("Device metadata event: connectId: {}, operate: {}", metadataEvent.getConnectId(), operateType);
// When the device is updated or deleted, remove the corresponding connection handle
if (MetadataOperateTypeEnum.DELETE.equals(operateType) || MetadataOperateTypeEnum.UPDATE.equals(operateType)) {
connectMap.remove(metadataEvent.getConnectId());
}
}
@Override
public Boolean write(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect connect, IotConfig config, WValue wValue) {
public RValue read(IotConnect connect, IotConfig config) {
return new RValue(config, connect, readValue(getConnector(connect), connect, config));
}
@Override
public Boolean write(IotConnect connect, IotConfig config, WValue wValue) {
/*
* 写入设备点位数据
*
* 提示: 此处逻辑仅供参考, 请务必结合实际应用场景进行修改。
* 通过 Modbus 连接器将指定值写入设备的点位, 并返回写入结果。
*/
ModbusMaster modbusMaster = getConnector(connect.getId().toString(), driverConfig);
return writeValue(modbusMaster, pointConfig, wValue);
ModbusMaster modbusMaster = getConnector(connect);
return writeValue(modbusMaster,connect, config, wValue);
}
/**
@@ -78,19 +92,19 @@ public class ModBusProtocolDriverImpl implements DriverCustomService {
* @return ModbusMaster 返回与设备关联的 Modbus Master 连接器
* @throws CommonException 如果连接器初始化失败, 抛出此异常
*/
private ModbusMaster getConnector(String connectId, Map<String, AttributeBO> driverConfig) {
log.debug("Modbus Tcp Connection Info: {}", driverConfig);
ModbusMaster modbusMaster = connectMap.get(connectId);
private ModbusMaster getConnector(IotConnect connect) {
log.debug("Modbus Tcp Connection Info: {}", connect);
ModbusMaster modbusMaster = connectMap.get(connect.getId().toString());
if (Objects.isNull(modbusMaster)) {
IpParameters params = new IpParameters();
params.setHost(driverConfig.get("host").getValueByClass(String.class));
params.setPort(driverConfig.get("port").getValueByClass(Integer.class));
params.setHost(connect.getHost());
params.setPort(connect.getPort());
modbusMaster = modbusFactory.createTcpMaster(params, true);
try {
modbusMaster.init();
connectMap.put(connectId, modbusMaster);
connectMap.put(connect.getId().toString(), modbusMaster);
} catch (ModbusInitException e) {
connectMap.entrySet().removeIf(next -> next.getKey().equals(connectId));
connectMap.entrySet().removeIf(next -> next.getKey().equals(connect.getId().toString()));
log.error("Connect modbus master error: {}", e.getMessage(), e);
throw new CommonException(e.getMessage());
}
@@ -113,10 +127,11 @@ public class ModBusProtocolDriverImpl implements DriverCustomService {
* @param type 点位值类型, 用于确定寄存器中数据的解析方式
* @return String 返回读取到的点位值, 以字符串形式表示。如果功能码不支持, 则返回 "0"。
*/
private String readValue(ModbusMaster modbusMaster, Map<String, AttributeBO> pointConfig) {
String type = pointConfig.get("data_type").getValueByClass(String.class);
int slaveId = pointConfig.get("slaveId").getValueByClass(Integer.class);
int offset = pointConfig.get("offset").getValueByClass(Integer.class);
private String readValue(ModbusMaster modbusMaster, IotConnect connect, IotConfig config) {
JSONObject pointConfig = JSONObject.parseObject(connect.getProperties());
String type = config.getDataType();
int slaveId = pointConfig.getIntValue("slaveId");
int offset = Integer.parseInt(config.getRegisterAddress());
int functionCode = ModBusTcpUtils.getFunctionCode(offset);
// 计算实际的寄存器地址Modbus协议地址从0开始
@@ -161,12 +176,13 @@ public class ModBusProtocolDriverImpl implements DriverCustomService {
* @param wValue 待写入的值, 包含值类型和具体数值
* @return boolean 返回写入结果, true 表示写入成功, false 表示写入失败或不支持的功能码
*/
private boolean writeValue(ModbusMaster modbusMaster, Map<String, AttributeBO> pointConfig, WValue wValue) {
String type = pointConfig.get("data_type").getValueByClass(String.class);
wValue.setType(type);
int slaveId = pointConfig.get("slaveId").getValueByClass(Integer.class);
int offset = pointConfig.get("offset").getValueByClass(Integer.class);
private boolean writeValue(ModbusMaster modbusMaster, IotConnect connect, IotConfig config, WValue wValue) {
JSONObject pointConfig = JSONObject.parseObject(connect.getProperties());
String type = config.getDataType();
int slaveId = pointConfig.getIntValue("slaveId");
int offset = Integer.parseInt(config.getRegisterAddress());
int functionCode = ModBusTcpUtils.getFunctionCode(offset);
wValue.setType(type);
// 计算实际的寄存器地址Modbus协议地址从0开始
int actualAddress = ModBusTcpUtils.getActualAddress(offset, functionCode);

View File

@@ -1,13 +1,17 @@
package org.nl.iot.core.driver.protocol.opcda;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.jinterop.dcom.common.JIException;
import org.jinterop.dcom.core.JIVariant;
import org.nl.common.exception.CommonException;
import org.nl.iot.core.driver.bo.AttributeBO;
import org.nl.iot.core.driver.bo.MetadataEventDTO;
import org.nl.iot.core.driver.entity.RValue;
import org.nl.iot.core.driver.entity.WValue;
import org.nl.iot.core.driver.enums.MetadataOperateTypeEnum;
import org.nl.iot.core.driver.enums.PointTypeFlagEnum;
import org.nl.iot.core.driver.protocol.opcda.org.openscada.opc.lib.common.AlreadyConnectedException;
import org.nl.iot.core.driver.protocol.opcda.org.openscada.opc.lib.common.ConnectionInformation;
@@ -51,14 +55,25 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService {
}
@Override
public RValue read(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect connect, IotConfig config) {
return new RValue(config, connect, readValue(getConnector(connect.getId().toString(), driverConfig), pointConfig));
public void event(MetadataEventDTO metadataEvent) {
MetadataOperateTypeEnum operateType = metadataEvent.getOperateType();
log.info("Device metadata event: connectId: {}, operate: {}", metadataEvent.getConnectId(), operateType);
// When the device is updated or deleted, remove the corresponding connection handle
if (MetadataOperateTypeEnum.DELETE.equals(operateType) || MetadataOperateTypeEnum.UPDATE.equals(operateType)) {
connectMap.remove(metadataEvent.getConnectId());
}
}
@Override
public Boolean write(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect device, IotConfig point, WValue wValue) {
Server server = getConnector(device.getId().toString(), driverConfig);
return writeValue(server, pointConfig, wValue);
public RValue read(IotConnect connect, IotConfig config) {
return new RValue(config, connect, readValue(getConnector(connect), connect, config));
}
@Override
public Boolean write(IotConnect connect, IotConfig config, WValue wValue) {
Server server = getConnector(connect);
return writeValue(server, connect, config, wValue);
}
/**
@@ -71,21 +86,22 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService {
* @return Server 返回与设备ID对应的 OPC DA 服务器连接
* @throws ConnectorException 如果连接 OPC DA 服务器时发生异常, 则抛出此异常
*/
private Server getConnector(String deviceId, Map<String, AttributeBO> driverConfig) {
log.debug("Opc Da Server Connection Info {}", driverConfig);
Server server = connectMap.get(deviceId);
private Server getConnector(IotConnect connect) {
log.debug("Opc Da Server Connection Info {}", connect);
JSONObject config = JSONObject.parseObject(connect.getProtocol());
Server server = connectMap.get(connect.getId().toString());
if (Objects.isNull(server)) {
String host = driverConfig.get("host").getValueByClass(String.class);
String clsId = driverConfig.get("clsId").getValueByClass(String.class);
String user = driverConfig.get("username").getValueByClass(String.class);
String password = driverConfig.get("password").getValueByClass(String.class);
String host = connect.getHost();
String clsId = config.getString("clsId");
String user = config.getString("username");
String password = ObjectUtil.isEmpty(config.get("password")) ? "" : config.getString("password");
ConnectionInformation connectionInformation = new ConnectionInformation(host, clsId, user, password);
server = new Server(connectionInformation, Executors.newSingleThreadScheduledExecutor());
try {
server.connect();
connectMap.put(deviceId, server);
connectMap.put(connect.getId().toString(), server);
} catch (AlreadyConnectedException | UnknownHostException | JIException e) {
connectMap.entrySet().removeIf(next -> next.getKey().equals(deviceId));
connectMap.entrySet().removeIf(next -> next.getKey().equals(connect.getId().toString()));
log.error("Connect opc da server error: {}", e.getMessage(), e);
throw new CommonException(e.getMessage());
}
@@ -104,9 +120,9 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService {
* @return String 返回读取到的位号值
* @throws ReadPointException 如果读取位号值时发生异常, 则抛出此异常
*/
private String readValue(Server server, Map<String, AttributeBO> pointConfig) {
private String readValue(Server server, IotConnect connect, IotConfig config) {
try {
Item item = getItem(server, pointConfig);
Item item = getItem(server, connect, config);
return readItem(item);
} catch (NotConnectedException | JIException | AddFailedException | DuplicateGroupException |
UnknownHostException e) {
@@ -131,15 +147,15 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService {
* @throws DuplicateGroupException 如果尝试添加已存在的组, 则抛出此异常
* @throws AddFailedException 如果添加组或 Item 失败, 则抛出此异常
*/
public Item getItem(Server server, Map<String, AttributeBO> pointConfig) throws NotConnectedException, JIException, UnknownHostException, DuplicateGroupException, AddFailedException {
public Item getItem(Server server, IotConnect connect, IotConfig config) throws NotConnectedException, JIException, UnknownHostException, DuplicateGroupException, AddFailedException {
Group group;
String groupName = pointConfig.get("group").getValueByClass(String.class);
String groupName = connect.getCode();
try {
group = server.findGroup(groupName);
} catch (UnknownGroupException e) {
group = server.addGroup(groupName);
}
return group.addItem(pointConfig.get("tag").getValueByClass(String.class));
return group.addItem(groupName + "." + config.getDeviceCode() + "." + config.getAlias());
}
/**
@@ -193,9 +209,9 @@ public class OpcDaProtocolDriverImpl implements DriverCustomService {
* @return boolean 返回写入操作是否成功
* @throws WritePointException 如果写入位号值时发生异常, 则抛出此异常
*/
private boolean writeValue(Server server, Map<String, AttributeBO> pointConfig, WValue wValue) {
private boolean writeValue(Server server, IotConnect connect, IotConfig config, WValue wValue) {
try {
Item item = getItem(server, pointConfig);
Item item = getItem(server, connect, config);
return writeItem(item, wValue);
} catch (NotConnectedException | AddFailedException | DuplicateGroupException | UnknownHostException |
JIException e) {

View File

@@ -1,5 +1,6 @@
package org.nl.iot.core.driver.protocol.opcua;
import com.alibaba.fastjson.JSONObject;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
@@ -13,8 +14,10 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.nl.common.exception.CommonException;
import org.nl.iot.core.driver.bo.AttributeBO;
import org.nl.iot.core.driver.bo.MetadataEventDTO;
import org.nl.iot.core.driver.entity.RValue;
import org.nl.iot.core.driver.entity.WValue;
import org.nl.iot.core.driver.enums.MetadataOperateTypeEnum;
import org.nl.iot.core.driver.enums.PointTypeFlagEnum;
import org.nl.iot.core.driver.service.DriverCustomService;
import org.nl.iot.modular.iot.entity.IotConfig;
@@ -48,11 +51,20 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService {
}
@Override
public void event(MetadataEventDTO metadataEvent) {
MetadataOperateTypeEnum operateType = metadataEvent.getOperateType();
log.info("Device metadata event: connectId: {}, operate: {}", metadataEvent.getConnectId(), operateType);
// When the device is updated or deleted, remove the corresponding connection handle
if (MetadataOperateTypeEnum.DELETE.equals(operateType) || MetadataOperateTypeEnum.UPDATE.equals(operateType)) {
connectMap.remove(metadataEvent.getConnectId());
}
}
@Override
public RValue read(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect connect, IotConfig config) {
return new RValue(config, connect, readValue(getConnector(connect.getId().toString(), driverConfig), pointConfig));
public RValue read(IotConnect connect, IotConfig config) {
return new RValue(config, connect, readValue(getConnector(connect), config));
}
/**
@@ -63,13 +75,14 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService {
* @return OpcUaClient 返回与指定设备关联的 OPC UA 客户端实例
* @throws CommonException 如果连接 OPC UA 服务器失败, 抛出此异常
*/
private OpcUaClient getConnector(String deviceId, Map<String, AttributeBO> driverConfig) {
log.debug("OPC UA server connection info: {}", driverConfig);
OpcUaClient opcUaClient = connectMap.get(deviceId);
private OpcUaClient getConnector(IotConnect connect) {
log.debug("OPC UA server connection info: {}", connect);
OpcUaClient opcUaClient = connectMap.get(connect.getId().toString());
if (Objects.isNull(opcUaClient)) {
String host = driverConfig.get("host").getValueByClass(String.class);
int port = driverConfig.get("port").getValueByClass(Integer.class);
String path = driverConfig.get("path").getValueByClass(String.class);
JSONObject driverConfig = JSONObject.parseObject(connect.getProperties());
String host = connect.getHost();
int port = connect.getPort();
String path = driverConfig.getString("path");
String url = String.format("opc.tcp://%s:%s%s", host, port, path);
try {
opcUaClient = OpcUaClient.create(
@@ -80,9 +93,9 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService {
.setRequestTimeout(Unsigned.uint(5000)) // 设置请求超时时间为 5000 毫秒
.build()
);
connectMap.put(deviceId, opcUaClient);
connectMap.put(connect.getId().toString(), opcUaClient);
} catch (UaException e) {
connectMap.entrySet().removeIf(next -> next.getKey().equals(deviceId));
connectMap.entrySet().removeIf(next -> next.getKey().equals(connect.getId().toString()));
log.error("Failed to connect OPC UA client: {}", e.getMessage(), e);
throw new CommonException(e.getMessage());
}
@@ -94,13 +107,13 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService {
* 读取 OPC UA 节点的值
*
* @param client OPC UA 客户端实例
* @param pointConfig 点位配置信息
* @param config 点位配置信息
* @return 读取到的节点值
* @throws CommonException 如果读取操作失败, 抛出此异常
*/
private String readValue(OpcUaClient client, Map<String, AttributeBO> pointConfig) {
private String readValue(OpcUaClient client, IotConfig config) {
try {
NodeId nodeId = getNode(pointConfig);
NodeId nodeId = getNode(config);
// 确保客户端已连接
client.connect().get(10, TimeUnit.SECONDS);
@@ -126,23 +139,23 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService {
}
@Override
public Boolean write(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect device, IotConfig point, WValue wValue) {
OpcUaClient client = getConnector(device.getId().toString(), driverConfig);
return writeValue(client, pointConfig, wValue);
public Boolean write(IotConnect connect, IotConfig config, WValue wValue) {
OpcUaClient client = getConnector(connect);
return writeValue(client, config, wValue);
}
/**
* 写入 OPC UA 节点的值
*
* @param client OPC UA 客户端实例
* @param pointConfig 点位配置信息
* @param config 点位配置信息
* @param wValue 写入值
* @return 写入操作是否成功
* @throws CommonException 如果写入操作失败, 抛出此异常
*/
private boolean writeValue(OpcUaClient client, Map<String, AttributeBO> pointConfig, WValue wValue) {
private boolean writeValue(OpcUaClient client, IotConfig config, WValue wValue) {
try {
NodeId nodeId = getNode(pointConfig);
NodeId nodeId = getNode(config);
// 确保客户端已连接,设置超时时间
client.connect().get(10, TimeUnit.SECONDS);
return writeNode(client, nodeId, wValue);
@@ -162,9 +175,69 @@ public class OpcUaProtocolDriverImpl implements DriverCustomService {
* @param pointConfig 点位配置信息, 包含命名空间和标签
* @return OPC UA 节点标识
*/
private NodeId getNode(Map<String, AttributeBO> pointConfig) {
int namespace = pointConfig.get("namespace").getValueByClass(Integer.class);
String tag = pointConfig.get("tag").getValueByClass(String.class);
private NodeId getNode(IotConfig config) {
// 解析地址ns=3;s=Temperature
return parseAddress(config.getRegisterAddress());
}
/**
* 解析地址字符串返回NodeId对象
* @param address 地址字符串格式要求ns=3;s=xxx
* @return 包含namespace(固定3)和tag的NodeId对象
* @throws IllegalArgumentException 输入格式错误或ns值不为3时抛出异常
*/
public static NodeId parseAddress(String address) {
// 校验输入不为空
if (address == null || address.trim().isEmpty()) {
throw new IllegalArgumentException("地址字符串不能为空");
}
// 按分号拆分字符串
String[] parts = address.split(";");
if (parts.length != 2) {
throw new IllegalArgumentException("地址格式错误正确格式应为ns=x;s=xxx");
}
int namespace = -1;
String tag = null;
// 遍历拆分后的部分提取ns和s的值
for (String part : parts) {
String[] keyValue = part.split("=");
if (keyValue.length != 2) {
throw new IllegalArgumentException("地址格式错误键值对格式应为key=value");
}
String key = keyValue[0].trim();
String value = keyValue[1].trim();
switch (key) {
case "ns":
// 解析ns的值并校验是否为3
try {
namespace = Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("ns的值必须是数字", e);
}
if (namespace != 3) {
throw new IllegalArgumentException("ns的值必须等于3");
}
break;
case "s":
// 提取s对应的标签值
tag = value;
break;
default:
throw new IllegalArgumentException("不支持的键:" + key + "仅支持ns和s");
}
}
// 校验s的值不为空
if (tag == null || tag.isEmpty()) {
throw new IllegalArgumentException("s的标签值不能为空");
}
// 返回NodeId对象
return new NodeId(namespace, tag);
}

View File

@@ -9,9 +9,11 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.nl.common.exception.CommonException;
import org.nl.iot.core.driver.bo.AttributeBO;
import org.nl.iot.core.driver.bo.MetadataEventDTO;
import org.nl.iot.core.driver.entity.RValue;
import org.nl.iot.core.driver.entity.WValue;
import org.nl.iot.core.driver.enums.AttributeTypeFlagEnum;
import org.nl.iot.core.driver.enums.MetadataOperateTypeEnum;
import org.nl.iot.core.driver.protocol.plcs7.com.github.s7.PlcS7PointVariable;
import org.nl.iot.core.driver.protocol.plcs7.com.github.s7.api.S7Connector;
import org.nl.iot.core.driver.protocol.plcs7.com.github.s7.api.S7Serializer;
@@ -54,7 +56,18 @@ public class PlcS7ProtocolDriverImpl implements DriverCustomService {
}
@Override
public RValue read(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect connect, IotConfig config) {
public void event(MetadataEventDTO metadataEvent) {
MetadataOperateTypeEnum operateType = metadataEvent.getOperateType();
log.info("Device metadata event: connectId: {}, operate: {}", metadataEvent.getConnectId(), operateType);
// When the device is updated or deleted, remove the corresponding connection handle
if (MetadataOperateTypeEnum.DELETE.equals(operateType) || MetadataOperateTypeEnum.UPDATE.equals(operateType)) {
connectMap.remove(metadataEvent.getConnectId());
}
}
@Override
public RValue read(IotConnect connect, IotConfig config) {
/*
* PLC S7 数据读取逻辑
*
@@ -66,14 +79,14 @@ public class PlcS7ProtocolDriverImpl implements DriverCustomService {
* 4. 将读取到的数据封装为 RValue 对象返回。
* 5. 捕获并记录异常, 确保锁在 finally 块中释放。
*/
log.debug("Plc S7 Read, device: {}, point: {}", driverConfig, pointConfig);
MyS7Connector myS7Connector = getS7Connector(connect.getId().toString(), driverConfig);
log.debug("Plc S7 Read, connect: {}, config: {}", connect, config);
MyS7Connector myS7Connector = getS7Connector(connect);
try {
myS7Connector.lock.writeLock().lock();
S7Serializer serializer = S7SerializerFactory.buildSerializer(myS7Connector.getConnector());
String type = pointConfig.get("data_type").getValueByClass(String.class);
PlcS7PointVariable plcs7PointVariable = getPointVariable(pointConfig, type);
String type = config.getDataType();
PlcS7PointVariable plcs7PointVariable = getPointVariable(config, type);
return new RValue(config, connect, String.valueOf(serializer.dispense(plcs7PointVariable)));
} catch (Exception e) {
log.error("Plc S7 Read Error: {}", e.getMessage());
@@ -84,12 +97,12 @@ public class PlcS7ProtocolDriverImpl implements DriverCustomService {
}
@Override
public Boolean write(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect device, IotConfig point, WValue wValue) {
public Boolean write(IotConnect device, IotConfig point, WValue wValue) {
log.debug("Plc S7 Write, device: {}, value: {}", device, wValue);
MyS7Connector myS7Connector = getS7Connector(String.valueOf(device.getId()), driverConfig);
MyS7Connector myS7Connector = getS7Connector(device);
myS7Connector.lock.writeLock().lock();
S7Serializer serializer = S7SerializerFactory.buildSerializer(myS7Connector.getConnector());
PlcS7PointVariable plcs7PointVariable = getPointVariable(pointConfig, wValue.getType());
PlcS7PointVariable plcs7PointVariable = getPointVariable(point, wValue.getType());
try {
store(serializer, plcs7PointVariable, wValue.getType(), wValue.getValue());
@@ -173,23 +186,23 @@ public class PlcS7ProtocolDriverImpl implements DriverCustomService {
* @return 返回与设备ID对应的 {@link MyS7Connector} 对象, 包含 S7 连接器和读写锁
* @throws CommonException 如果连接器创建失败, 抛出此异常
*/
private MyS7Connector getS7Connector(String connectId, Map<String, AttributeBO> driverConfig) {
MyS7Connector myS7Connector = connectMap.get(connectId);
private MyS7Connector getS7Connector(IotConnect connect) {
MyS7Connector myS7Connector = connectMap.get(connect.getId().toString());
if (Objects.isNull(myS7Connector)) {
myS7Connector = new MyS7Connector();
log.debug("Plc S7 Connection Info {}", driverConfig);
log.debug("Plc S7 Connection Info {}", connect);
try {
S7Connector s7Connector = S7ConnectorFactory.buildTCPConnector()
.withHost(driverConfig.get("host").getValueByClass(String.class))
.withPort(driverConfig.get("port").getValueByClass(Integer.class))
.withHost(connect.getHost())
.withPort(connect.getPort())
.build();
myS7Connector.setLock(new ReentrantReadWriteLock());
myS7Connector.setConnector(s7Connector);
} catch (Exception e) {
throw new CommonException("new s7connector fail" + e.getMessage());
}
connectMap.put(connectId, myS7Connector);
connectMap.put(connect.getId().toString(), myS7Connector);
}
return myS7Connector;
}
@@ -212,14 +225,124 @@ public class PlcS7ProtocolDriverImpl implements DriverCustomService {
* @return 返回封装好的 {@link PlcS7PointVariable} 对象, 包含点位变量的详细信息
* @throws NullPointerException 如果点位配置中缺少必要的属性, 抛出此异常
*/
private PlcS7PointVariable getPointVariable(Map<String, AttributeBO> pointConfig, String type) {
log.debug("Plc S7 Point Attribute Config {}", pointConfig);
return new PlcS7PointVariable(
pointConfig.get("dbNum").getValueByClass(Integer.class),
pointConfig.get("byteOffset").getValueByClass(Integer.class),
pointConfig.get("bitOffset").getValueByClass(Integer.class),
pointConfig.get("blockSize").getValueByClass(Integer.class),
type);
private PlcS7PointVariable getPointVariable(IotConfig config, String type) {
log.debug("Plc S7 Point Attribute Config {}", config);
// DB 块地址解析
return parseS7Address(config.getRegisterAddress(), type);
}
// 通用解析方法支持DB格式DB1.DBX10.3和S7-200格式VB1、VD4、V1.3
public static PlcS7PointVariable parseS7Address(String address, String type) {
if (address == null || address.trim().isEmpty()) {
throw new IllegalArgumentException("S7地址不能为空");
}
String cleanAddress = address.trim().toUpperCase();
int dbNum = 0;
String area = "";
int byteOffset = -1;
int bitOffset = -1;
int size = -1;
// ========== 分支1解析S7-1200/1500的DB格式DB1.DBX10.3、DB2.DBD5 ==========
if (cleanAddress.startsWith("DB")) {
area = "DB";
// 拆分DB编号和地址主体DB1.DBX10.3 → DB1 和 DBX10.3
String[] dbAndAddress = cleanAddress.split("\\.DB", 2);
if (dbAndAddress.length != 2) {
throw new IllegalArgumentException("DB地址格式错误" + address);
}
// 解析DB编号
String dbNumStr = dbAndAddress[0].replace("DB", "");
try {
dbNum = Integer.parseInt(dbNumStr);
if (dbNum < 1) throw new IllegalArgumentException("DB编号必须≥1" + dbNumStr);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("DB编号不是有效数字" + dbNumStr, e);
}
// 解析类型和偏移DBX10.3 → X10.3
String addressBody = "DB" + dbAndAddress[1];
char typeSuffix = addressBody.charAt(2);
String offsetStr = addressBody.substring(3);
// 按类型解析
switch (typeSuffix) {
case 'X': // BOOL
String[] byteBit = offsetStr.split("\\.");
if (byteBit.length != 2) throw new IllegalArgumentException("BOOL地址需包含位偏移" + address);
byteOffset = parseIntCheck(byteBit[0], "字节偏移");
bitOffset = parseIntCheck(byteBit[1], "位偏移");
if (bitOffset < 0 || bitOffset > 7) throw new IllegalArgumentException("位偏移需0-7" + bitOffset);
size = 1;
break;
case 'B': // BYTE
byteOffset = parseIntCheck(offsetStr, "字节偏移");
size = 1;
break;
case 'W': // WORD
byteOffset = parseIntCheck(offsetStr, "字节偏移");
size = 2;
break;
case 'D': // DWORD/REAL
byteOffset = parseIntCheck(offsetStr, "字节偏移");
size = 4;
break;
case 'R': // REAL
byteOffset = parseIntCheck(offsetStr, "字节偏移");
size = 4;
break;
default:
throw new IllegalArgumentException("不支持的DB类型后缀" + typeSuffix);
}
// ========== 分支2解析S7-200的VB/VD/V1.3格式 ==========
} else {
// 第一步判断是BOOL位V1.3、I0.1)还是字节/双字VB1、VD4
if (cleanAddress.contains(".")) {
// 处理BOOL位V1.3、I0.1、M2.5
String[] areaByteBit = cleanAddress.split("\\.");
if (areaByteBit.length != 2) throw new IllegalArgumentException("S7-200 BOOL地址格式错误" + address);
// 拆分存储区和字节偏移V1 → V 和 1
String areaByteStr = areaByteBit[0];
area = areaByteStr.substring(0, 1); // 取第一个字符V/I/Q/M/SM
String byteOffsetStr = areaByteStr.substring(1);
// 校验存储区合法性
if (!"VIQMSM".contains(area)) throw new IllegalArgumentException("不支持的S7-200存储区" + area);
// 解析字节和位偏移
byteOffset = parseIntCheck(byteOffsetStr, "字节偏移");
bitOffset = parseIntCheck(areaByteBit[1], "位偏移");
if (bitOffset < 0 || bitOffset > 7) throw new IllegalArgumentException("位偏移需0-7" + bitOffset);
size = 1; // BOOL占1字节
} else {
// 处理字节/双字VB1、VD4、IW2、MB3
// 拆分存储区+类型 和 地址VB1 → VB 和 1VD4 → VD 和 4
char typeSuffix = cleanAddress.charAt(1); // 第二个字符是类型B/W/D
area = cleanAddress.substring(0, 1); // 第一个字符是存储区V/I/Q/M/SM
String offsetStr = cleanAddress.substring(2);
// 校验存储区和类型
if (!"VIQMSM".contains(area)) throw new IllegalArgumentException("不支持的S7-200存储区" + area);
if (!"BWD".contains(String.valueOf(typeSuffix))) throw new IllegalArgumentException("不支持的S7-200类型后缀" + typeSuffix);
// 解析字节偏移和大小
byteOffset = parseIntCheck(offsetStr, "字节偏移");
switch (typeSuffix) {
case 'B': size = 1; break; // BYTE
case 'W': size = 2; break; // WORD
case 'D': size = 4; break; // DWORD/REAL
}
}
}
// 校验字节偏移合法性
if (byteOffset < 0) throw new IllegalArgumentException("字节偏移不能为负数:" + byteOffset);
return new PlcS7PointVariable(dbNum, byteOffset, bitOffset, size, type);
}
// 工具方法:解析数字并捕获异常
private static int parseIntCheck(String str, String fieldName) {
try {
return Integer.parseInt(str);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(fieldName + "不是有效数字:" + str, e);
}
}
/**

View File

@@ -1,6 +1,7 @@
package org.nl.iot.core.driver.service;
import org.nl.iot.core.driver.bo.AttributeBO;
import org.nl.iot.core.driver.bo.MetadataEventDTO;
import org.nl.iot.core.driver.entity.RValue;
import org.nl.iot.core.driver.entity.WValue;
import org.nl.iot.modular.iot.entity.IotConfig;
@@ -28,6 +29,8 @@ public interface DriverCustomService {
*/
void schedule();
void event(MetadataEventDTO metadataEvent);
// todo驱动事件暂时不需要
/**
@@ -43,7 +46,7 @@ public interface DriverCustomService {
* @param point 位号对象, 包含位号的基本信息和属性
* @return 返回读取到的数据, 封装在 {@link RValue} 对象中
*/
RValue read(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect device, IotConfig point);
RValue read(IotConnect device, IotConfig point);
/**
* 执行写操作
@@ -59,6 +62,6 @@ public interface DriverCustomService {
* @param wValue 待写入的数据, 封装在 {@link WValue} 对象中
* @return 返回写入操作是否成功, 若成功则返回 {@code true}, 否则返回 {@code false} 或抛出异常
*/
Boolean write(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, IotConnect device, IotConfig point, WValue wValue);
Boolean write(IotConnect device, IotConfig point, WValue wValue);
}

View File

@@ -1,6 +1,7 @@
package org.nl.iot.modular.iot.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -68,4 +69,7 @@ public class IotConfig implements Serializable {
@Schema(description = "修改用户")
private String updateName;
@TableField(exist = false)
private String deviceCode;
}

View File

@@ -497,16 +497,38 @@ public class ApiTest {
opcDaProtocolDriver.initial();
// 构建驱动配置(连接配置)
// 重要提示:
// 1. 本地连接使用 "127.0.0.1" 或 "localhost"
// 2. username 格式:
// - 本地用户:直接使用用户名,如 "Administrator" 或 "YourUsername"
// - 域用户:使用 "DOMAIN\\username" 格式
// - 如果 OPC 服务器配置为允许匿名访问,可以尝试空字符串
// 3. password 必须是 Windows 用户的实际密码
// 4. 确保 Windows DCOM 配置正确(参考 Matrikon OPC 文档)
Map<String, AttributeBO> driverConfig = new HashMap<>();
driverConfig.put("host", AttributeBO.builder().value("localhost").build());
driverConfig.put("clsId", AttributeBO.builder().value("F8582CF2-88FB-11D0-B850-00C0F0104305").build()); // OPC DA Server CLSID
driverConfig.put("username", AttributeBO.builder().value("Administrator").build());
driverConfig.put("password", AttributeBO.builder().value("password").build());
driverConfig.put("host", AttributeBO.builder().value("127.0.0.1").build());
driverConfig.put("clsId", AttributeBO.builder().value("F8582CF2-88FB-11D0-B850-00C0F0104305").build());
// 方案1使用当前登录用户推荐本地测试
// 获取当前 Windows 用户名:在 CMD 中运行 "echo %USERNAME%"
String currentUser = System.getProperty("user.name"); // 获取当前 Java 进程的用户名
// driverConfig.put("username", AttributeBO.builder().value(currentUser).build());
// driverConfig.put("password", AttributeBO.builder().value("6614").build()); // 替换为你的 Windows 密码
// 方案2如果模拟器配置了特定用户使用该用户
driverConfig.put("username", AttributeBO.builder().value("Administrator").build());
driverConfig.put("password", AttributeBO.builder().value("12356").build());
// 方案3尝试匿名访问某些 OPC 服务器支持)
// driverConfig.put("username", AttributeBO.builder().value("").build());
// driverConfig.put("password", AttributeBO.builder().value("").build());
// 构建点位配置
// 根据你的模拟器截图Item ID 是 "Random.Int1"
Map<String, AttributeBO> pointConfig = new HashMap<>();
pointConfig.put("group", AttributeBO.builder().value("Group1").build()); // 组名
pointConfig.put("tag", AttributeBO.builder().value("Channel1.Device1.Tag1").build()); // 标签名
pointConfig.put("group", AttributeBO.builder().value("Group0").build()); // 组名
pointConfig.put("tag", AttributeBO.builder().value("Random.Int1").build()); // 标签名,与模拟器中的 Item ID 一致
// 构建连接对象
IotConnect connect = IotConnect.builder()
@@ -524,7 +546,7 @@ public class ApiTest {
.connectId(1)
.alias("sensor1")
.aliasName("传感器1")
.registerAddress("Channel1.Device1.Tag1")
.registerAddress("Random.Int1")
.dataType("int")
.readonly(true)
.enabled(true)

View File

@@ -0,0 +1,261 @@
package org.nl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.nl.iot.core.driver.bo.AttributeBO;
import org.nl.iot.core.driver.entity.RValue;
import org.nl.iot.core.driver.protocol.opcda.OpcDaProtocolDriverImpl;
import org.nl.iot.modular.iot.entity.IotConfig;
import org.nl.iot.modular.iot.entity.IotConnect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.HashMap;
import java.util.Map;
/**
* OPC DA 连接测试 - 多种认证方案
*
* 使用说明:
* 1. 确保 Matrikon OPC 模拟器正在运行
* 2. 确保已配置 DCOM 权限(参考 OPC_DA_DCOM_完整配置指南.md
* 3. 依次尝试不同的测试方法,直到连接成功
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class OpcDaConnectionTest {
@Autowired
private OpcDaProtocolDriverImpl opcDaProtocolDriver;
/**
* 方案 1空用户名和密码本地连接最简单
* 适用场景本地测试DCOM 配置为允许匿名访问
*/
@Test
public void testWithEmptyCredentials() {
System.out.println("\n========================================");
System.out.println("测试方案 1空用户名和密码");
System.out.println("========================================");
testConnection("", "");
}
/**
* 方案 2使用当前 Java 进程用户
* 适用场景:使用当前登录的 Windows 用户
* 注意:需要输入正确的 Windows 密码
*/
@Test
public void testWithCurrentUser() {
System.out.println("\n========================================");
System.out.println("测试方案 2当前用户");
System.out.println("========================================");
String currentUser = System.getProperty("user.name");
System.out.println("当前用户: " + currentUser);
System.out.println("请在代码中填入该用户的 Windows 密码!");
// TODO: 替换为你的 Windows 密码
String password = "YOUR_PASSWORD_HERE";
if ("YOUR_PASSWORD_HERE".equals(password)) {
System.err.println("错误:请先在代码中设置正确的密码!");
return;
}
testConnection(currentUser, password);
}
/**
* 方案 3使用 计算机名\用户名 格式
* 适用场景:明确指定本地计算机的用户
*/
@Test
public void testWithComputerNameUser() {
System.out.println("\n========================================");
System.out.println("测试方案 3计算机名\\用户名");
System.out.println("========================================");
String computerName = System.getenv("COMPUTERNAME");
String userName = System.getProperty("user.name");
String fullUserName = computerName + "\\" + userName;
System.out.println("计算机名: " + computerName);
System.out.println("用户名: " + userName);
System.out.println("完整用户名: " + fullUserName);
System.out.println("请在代码中填入该用户的 Windows 密码!");
// TODO: 替换为你的 Windows 密码
String password = "YOUR_PASSWORD_HERE";
if ("YOUR_PASSWORD_HERE".equals(password)) {
System.err.println("错误:请先在代码中设置正确的密码!");
return;
}
testConnection(fullUserName, password);
}
/**
* 方案 4使用 Administrator 账户
* 适用场景:使用管理员账户
* 注意:确保 Administrator 账户已启用
*/
@Test
public void testWithAdministrator() {
System.out.println("\n========================================");
System.out.println("测试方案 4Administrator 账户");
System.out.println("========================================");
System.out.println("注意:确保 Administrator 账户已启用");
System.out.println("启用命令net user Administrator /active:yes");
System.out.println("请在代码中填入 Administrator 的密码!");
// TODO: 替换为 Administrator 的密码
String password = "YOUR_ADMIN_PASSWORD_HERE";
if ("YOUR_ADMIN_PASSWORD_HERE".equals(password)) {
System.err.println("错误:请先在代码中设置 Administrator 的密码!");
return;
}
testConnection("Administrator", password);
}
/**
* 方案 5使用 .\用户名 格式(本地用户)
* 适用场景:明确指定本地用户
*/
@Test
public void testWithDotUser() {
System.out.println("\n========================================");
System.out.println("测试方案 5.\\用户名 格式");
System.out.println("========================================");
String userName = System.getProperty("user.name");
String fullUserName = ".\\" + userName;
System.out.println("用户名: " + fullUserName);
System.out.println("请在代码中填入该用户的 Windows 密码!");
// TODO: 替换为你的 Windows 密码
String password = "YOUR_PASSWORD_HERE";
if ("YOUR_PASSWORD_HERE".equals(password)) {
System.err.println("错误:请先在代码中设置正确的密码!");
return;
}
testConnection(fullUserName, password);
}
/**
* 核心测试方法
*/
private void testConnection(String username, String password) {
opcDaProtocolDriver.initial();
Map<String, AttributeBO> driverConfig = new HashMap<>();
driverConfig.put("host", AttributeBO.builder().value("127.0.0.1").build());
driverConfig.put("clsId", AttributeBO.builder().value("F8582CF2-88FB-11D0-B850-00C0F0104305").build());
driverConfig.put("username", AttributeBO.builder().value(username).build());
driverConfig.put("password", AttributeBO.builder().value(password).build());
Map<String, AttributeBO> pointConfig = new HashMap<>();
pointConfig.put("group", AttributeBO.builder().value("Group0").build());
pointConfig.put("tag", AttributeBO.builder().value("Random.Int1").build());
IotConnect connect = IotConnect.builder()
.id(1)
.code("OPC_DA_TEST")
.host("127.0.0.1")
.protocol("opc-da")
.enabled(true)
.description("OPC DA 连接测试")
.build();
IotConfig config = IotConfig.builder()
.id(1)
.connectId(1)
.alias("test_sensor")
.aliasName("测试传感器")
.registerAddress("Random.Int1")
.dataType("int")
.readonly(true)
.enabled(true)
.description("OPC DA 测试点位")
.build();
try {
System.out.println("\n--- 连接参数 ---");
System.out.println("主机: " + driverConfig.get("host").getValue());
System.out.println("CLSID: " + driverConfig.get("clsid").getValue());
System.out.println("用户名: " + (username.isEmpty() ? "(空)" : username));
System.out.println("密码: " + (password.isEmpty() ? "(空)" : "******"));
System.out.println("组名: " + pointConfig.get("group").getValue());
System.out.println("标签: " + pointConfig.get("tag").getValue());
System.out.println("\n正在连接...");
RValue result = opcDaProtocolDriver.read(driverConfig, pointConfig, connect, config);
if (result != null) {
System.out.println("\n✅ 连接成功!");
System.out.println("读取值: " + result.getValue());
System.out.println("\n========================================");
System.out.println("成功配置:");
System.out.println("用户名: " + username);
System.out.println("请在正式代码中使用此配置!");
System.out.println("========================================");
} else {
System.err.println("\n❌ 连接失败:返回结果为 null");
}
} catch (Exception e) {
System.err.println("\n❌ 连接失败");
System.err.println("错误信息: " + e.getMessage());
// 分析错误并给出建议
String errorMsg = e.getMessage();
if (errorMsg != null) {
if (errorMsg.contains("0x00000005") || errorMsg.contains("Access is denied")) {
System.err.println("\n可能原因");
System.err.println("1. 用户名或密码错误");
System.err.println("2. DCOM 权限未正确配置");
System.err.println("3. 用户未在 'Distributed COM Users' 组中");
System.err.println("\n建议");
System.err.println("- 检查密码是否正确");
System.err.println("- 运行 dcomcnfg 配置权限");
System.err.println("- 参考 OPC_DA_DCOM_完整配置指南.md");
} else if (errorMsg.contains("0x00000533")) {
System.err.println("\n可能原因");
System.err.println("- 用户账户被禁用");
System.err.println("\n建议");
System.err.println("- 运行: net user " + username + " /active:yes");
}
}
}
}
/**
* 显示系统信息
*/
@Test
public void showSystemInfo() {
System.out.println("\n========================================");
System.out.println("系统信息");
System.out.println("========================================");
System.out.println("计算机名: " + System.getenv("COMPUTERNAME"));
System.out.println("当前用户: " + System.getProperty("user.name"));
System.out.println("用户主目录: " + System.getProperty("user.home"));
System.out.println("操作系统: " + System.getProperty("os.name"));
System.out.println("========================================");
System.out.println("\n建议测试顺序");
System.out.println("1. testWithEmptyCredentials() - 最简单");
System.out.println("2. testWithCurrentUser() - 使用当前用户");
System.out.println("3. testWithComputerNameUser() - 完整格式");
System.out.println("4. testWithAdministrator() - 管理员账户");
System.out.println("5. testWithDotUser() - 本地用户格式");
}
}