Compare commits
4 Commits
97829ad0d5
...
07e3abdf73
| Author | SHA1 | Date | |
|---|---|---|---|
| 07e3abdf73 | |||
| ffb0947e32 | |||
| 12e8483804 | |||
| e8445bb286 |
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -16,15 +16,6 @@ import org.nl.iot.modular.iot.entity.IotConnect;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RValue {
|
||||
/**
|
||||
* 设备
|
||||
*/
|
||||
// private DeviceBO device;
|
||||
|
||||
/**
|
||||
* 位号 - 子设备
|
||||
*/
|
||||
// private PointBO point;
|
||||
/**
|
||||
* 配置
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 和 1;VD4 → 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
261
nl-web-app/src/test/java/org/nl/OpcDaConnectionTest.java
Normal file
261
nl-web-app/src/test/java/org/nl/OpcDaConnectionTest.java
Normal 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("测试方案 4:Administrator 账户");
|
||||
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() - 本地用户格式");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user