从0到1掌握Eclipse Milo变量节点创建:工业级OPC UA服务器开发指南
引言:解决工业数据交互的核心难题
你是否在工业物联网(IIoT)项目中遇到过这些痛点?设备数据采集延迟、多厂商协议不兼容、数据类型转换错误、权限控制混乱?作为OPC UA(开放平台通信统一架构,Open Platform Communications Unified Architecture)的Java开源实现,Eclipse Milo为解决这些问题提供了强大支持。本文将聚焦变量节点(Variable Node)创建这一核心技术,通过10个实战案例和6个避坑指南,帮助你构建稳定、高效的工业数据交互系统。
读完本文后,你将能够:
- 掌握7种基础数据类型变量节点的创建方法
- 实现动态更新与历史数据追踪功能
- 构建复杂的自定义数据类型节点
- 解决权限控制与数据安全难题
- 优化节点性能以应对高并发场景
OPC UA变量节点基础:核心概念与架构
变量节点的核心构成
OPC UA变量节点是工业数据的载体,包含以下关键属性:
| 属性名称 | 数据类型 | 描述 | 重要性 |
|---|---|---|---|
| NodeId | NodeId | 节点唯一标识符 | 必须 |
| BrowseName | QualifiedName | 浏览名称 | 必须 |
| DisplayName | LocalizedText | 显示名称 | 必须 |
| DataType | NodeId | 数据类型标识符 | 必须 |
| Value | DataValue | 节点值 | 必须 |
| AccessLevel | Byte | 访问权限 | 推荐 |
| UserAccessLevel | Byte | 用户访问权限 | 推荐 |
| ValueRank | Int32 | 值维度 | 数组节点必须 |
| ArrayDimensions | UInt32[] | 数组维度 | 固定数组必须 |
| TypeDefinition | NodeId | 类型定义 | 推荐 |
Eclipse Milo节点创建架构
Eclipse Milo提供了灵活的节点创建API,主要涉及以下核心类:
实战指南:从基础到高级的变量节点创建
1. 静态标量节点创建:基础数据类型实现
标量节点是最简单的变量节点类型,用于存储单一值。以下代码展示了如何创建常用的基础数据类型节点:
// 创建标量类型文件夹
UaFolderNode scalarTypesFolder = new UaFolderNode(
getNodeContext(),
newNodeId("HelloWorld/ScalarTypes"),
newQualifiedName("ScalarTypes"),
LocalizedText.english("ScalarTypes")
);
getNodeManager().addNode(scalarTypesFolder);
rootNode.addOrganizes(scalarTypesFolder);
// 布尔类型节点
UaVariableNode booleanNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/ScalarTypes/Boolean"))
.setAccessLevel(AccessLevel.READ_WRITE)
.setUserAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName("Boolean"))
.setDisplayName(LocalizedText.english("Boolean"))
.setDataType(Identifiers.Boolean)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.build();
booleanNode.setValue(new DataValue(new Variant(false)));
getNodeManager().addNode(booleanNode);
scalarTypesFolder.addOrganizes(booleanNode);
// 整数类型节点
UaVariableNode int32Node = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/ScalarTypes/Int32"))
.setAccessLevel(AccessLevel.READ_WRITE)
.setUserAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName("Int32"))
.setDisplayName(LocalizedText.english("Int32"))
.setDataType(Identifiers.Int32)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.build();
int32Node.setValue(new DataValue(new Variant(32)));
getNodeManager().addNode(int32Node);
scalarTypesFolder.addOrganizes(int32Node);
// 浮点数类型节点
UaVariableNode doubleNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/ScalarTypes/Double"))
.setAccessLevel(AccessLevel.READ_WRITE)
.setUserAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName("Double"))
.setDisplayName(LocalizedText.english("Double"))
.setDataType(Identifiers.Double)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.build();
doubleNode.setValue(new DataValue(new Variant(3.14d)));
getNodeManager().addNode(doubleNode);
scalarTypesFolder.addOrganizes(doubleNode);
2. 数组节点创建:处理批量数据
数组节点用于存储多个相同类型的数据,适用于传感器数组、历史数据等场景:
// 创建数组类型文件夹
UaFolderNode arrayTypesFolder = new UaFolderNode(
getNodeContext(),
newNodeId("HelloWorld/ArrayTypes"),
newQualifiedName("ArrayTypes"),
LocalizedText.english("ArrayTypes")
);
getNodeManager().addNode(arrayTypesFolder);
rootNode.addOrganizes(arrayTypesFolder);
// 创建整数数组节点
Integer[] intArray = new Integer[5];
for (int i = 0; i < 5; i++) {
intArray[i] = i * 10;
}
Variant arrayVariant = new Variant(intArray);
UaVariableNode intArrayNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/ArrayTypes/Int32Array"))
.setAccessLevel(AccessLevel.READ_WRITE)
.setUserAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName("Int32Array"))
.setDisplayName(LocalizedText.english("Int32Array"))
.setDataType(Identifiers.Int32)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.setValueRank(ValueRank.OneDimension.getValue()) // 一维数组
.setArrayDimensions(new UInteger[]{uint(5)}) // 固定大小5
.setValue(new DataValue(arrayVariant))
.build();
getNodeManager().addNode(intArrayNode);
arrayTypesFolder.addOrganizes(intArrayNode);
// 创建动态大小浮点数数组
Double[] doubleArray = new Double[3];
for (int i = 0; i < 3; i++) {
doubleArray[i] = Math.PI * (i + 1);
}
Variant dynamicArrayVariant = new Variant(doubleArray);
UaVariableNode dynamicArrayNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/ArrayTypes/DoubleArray"))
.setAccessLevel(AccessLevel.READ_WRITE)
.setUserAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName("DoubleArray"))
.setDisplayName(LocalizedText.english("DoubleArray"))
.setDataType(Identifiers.Double)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.setValueRank(ValueRank.OneDimension.getValue()) // 一维数组
.setArrayDimensions(new UInteger[]{uint(0)}) // 动态大小
.setValue(new DataValue(dynamicArrayVariant))
.build();
getNodeManager().addNode(dynamicArrayNode);
arrayTypesFolder.addOrganizes(dynamicArrayNode);
3. 动态节点创建:实时数据更新
动态节点的值会随时间自动更新,适用于实时传感器数据等场景:
// 创建动态节点文件夹
UaFolderNode dynamicFolder = new UaFolderNode(
getNodeContext(),
newNodeId("HelloWorld/Dynamic"),
newQualifiedName("Dynamic"),
LocalizedText.english("Dynamic")
);
getNodeManager().addNode(dynamicFolder);
rootNode.addOrganizes(dynamicFolder);
// 创建随机整数动态节点
UaVariableNode dynamicIntNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/Dynamic/RandomInt32"))
.setAccessLevel(AccessLevel.READ_ONLY)
.setBrowseName(newQualifiedName("RandomInt32"))
.setDisplayName(LocalizedText.english("RandomInt32"))
.setDataType(Identifiers.Int32)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.build();
dynamicIntNode.setValue(new DataValue(new Variant(0)));
// 添加值过滤器实现动态更新
dynamicIntNode.getFilterChain().addLast(
AttributeFilters.getValue(ctx -> {
// 生成-1000到1000之间的随机整数
int randomValue = new Random().nextInt(2001) - 1000;
return new DataValue(new Variant(randomValue));
})
);
getNodeManager().addNode(dynamicIntNode);
dynamicFolder.addOrganizes(dynamicIntNode);
// 创建周期性更新的浮点数节点
UaVariableNode sineWaveNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/Dynamic/SineWave"))
.setAccessLevel(AccessLevel.READ_ONLY)
.setBrowseName(newQualifiedName("SineWave"))
.setDisplayName(LocalizedText.english("SineWave"))
.setDataType(Identifiers.Double)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.build();
// 使用定时任务更新值
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
AtomicDouble angle = new AtomicDouble(0.0);
scheduler.scheduleAtFixedRate(() -> {
try {
double value = Math.sin(angle.getAndAdd(0.1));
sineWaveNode.setValue(new DataValue(new Variant(value)));
} catch (Exception e) {
logger.error("Error updating sine wave value", e);
}
}, 0, 100, TimeUnit.MILLISECONDS);
getNodeManager().addNode(sineWaveNode);
dynamicFolder.addOrganizes(sineWaveNode);
4. 访问控制节点:数据安全实现
通过访问控制节点,可以限制不同用户对数据的访问权限:
// 创建管理员可写文件夹
UaFolderNode adminWritableFolder = new UaFolderNode(
getNodeContext(),
newNodeId("HelloWorld/OnlyAdminCanWrite"),
newQualifiedName("OnlyAdminCanWrite"),
LocalizedText.english("OnlyAdminCanWrite")
);
getNodeManager().addNode(adminWritableFolder);
rootNode.addOrganizes(adminWritableFolder);
// 创建管理员可写节点
UaVariableNode adminWritableNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/OnlyAdminCanWrite/CriticalValue"))
.setAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName("CriticalValue"))
.setDisplayName(LocalizedText.english("CriticalValue"))
.setDataType(Identifiers.Double)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.setValue(new DataValue(new Variant(25.0)))
.build();
// 添加访问控制过滤器
adminWritableNode.getFilterChain().addLast(new RestrictedAccessFilter(identity -> {
// 检查用户身份
if ("admin".equals(identity)) {
return AccessLevel.READ_WRITE; // 管理员有读写权限
} else {
return AccessLevel.READ_ONLY; // 普通用户只有读权限
}
}));
getNodeManager().addNode(adminWritableNode);
adminWritableFolder.addOrganizes(adminWritableNode);
// 创建只写节点
UaFolderNode writeOnlyFolder = new UaFolderNode(
getNodeContext(),
newNodeId("HelloWorld/WriteOnly"),
newQualifiedName("WriteOnly"),
LocalizedText.english("WriteOnly")
);
getNodeManager().addNode(writeOnlyFolder);
rootNode.addOrganizes(writeOnlyFolder);
UaVariableNode writeOnlyNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/WriteOnly/Command"))
.setAccessLevel(AccessLevel.WRITE_ONLY) // 只写权限
.setUserAccessLevel(AccessLevel.WRITE_ONLY)
.setBrowseName(newQualifiedName("Command"))
.setDisplayName(LocalizedText.english("Command"))
.setDataType(Identifiers.String)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.setValue(new DataValue(new Variant("")))
.build();
getNodeManager().addNode(writeOnlyNode);
writeOnlyFolder.addOrganizes(writeOnlyNode);
5. 自定义数据类型节点:复杂工业数据建模
在工业场景中,经常需要自定义复杂数据类型。以下是创建自定义枚举、结构体和联合类型节点的完整实现:
5.1 自定义枚举类型
// 定义自定义枚举类型
public enum CustomEnumType {
IDLE(0, "Idle"),
RUNNING(1, "Running"),
PAUSED(2, "Paused"),
ERROR(3, "Error");
private final int value;
private final String name;
CustomEnumType(int value, String name) {
this.value = value;
this.name = name;
}
public int getValue() {
return value;
}
public String getName() {
return name;
}
@Nullable
public static CustomEnumType from(int value) {
for (CustomEnumType type : values()) {
if (type.getValue() == value) {
return type;
}
}
return null;
}
}
// 注册枚举类型编解码器
public class CustomEnumTypeCodec implements DataTypeCodec<CustomEnumType> {
public static final NodeId TYPE_ID = new NodeId(2, "CustomEnumType");
public static final NodeId BINARY_ENCODING_ID = new NodeId(2, "CustomEnumType.Binary");
@Override
public Class<CustomEnumType> getType() {
return CustomEnumType.class;
}
@Override
public NodeId getDataTypeId() {
return TYPE_ID;
}
@Override
public NodeId getBinaryEncodingId() {
return BINARY_ENCODING_ID;
}
@Override
public CustomEnumType decode(SerializationContext context, UaDecoder decoder) throws UaSerializationException {
int value = decoder.readInt32("Value");
CustomEnumType type = CustomEnumType.from(value);
if (type == null) {
throw new UaSerializationException("Invalid CustomEnumType value: " + value);
}
return type;
}
@Override
public void encode(SerializationContext context, UaEncoder encoder, CustomEnumType value) throws UaSerializationException {
encoder.writeInt32("Value", value.getValue());
}
}
// 在命名空间中注册自定义枚举类型
private void registerCustomEnumType() {
// 创建枚举类型节点
UaDataTypeNode enumTypeNode = new UaDataTypeNode(
getNodeContext(),
CustomEnumTypeCodec.TYPE_ID,
newQualifiedName("CustomEnumType"),
LocalizedText.english("CustomEnumType"),
LocalizedText.english("Custom enumeration type for equipment status"),
false
);
getNodeManager().addNode(enumTypeNode);
// 创建枚举定义
EnumField[] fields = new EnumField[CustomEnumType.values().length];
for (int i = 0; i < fields.length; i++) {
CustomEnumType enumValue = CustomEnumType.values()[i];
fields[i] = new EnumField(
LocalizedText.english(enumValue.getName()),
LocalizedText.english(enumValue.getName() + " status"),
uint(enumValue.getValue()),
null
);
}
EnumDefinition enumDefinition = new EnumDefinition(uint(32), fields);
// 设置枚举定义属性
enumTypeNode.setAttribute(
AttributeId.Value,
new DataValue(new Variant(ExtensionObject.encode(
getServer().getSerializationContext(),
enumDefinition,
Identifiers.EnumDefinition_Encoding_DefaultBinary
)))
);
// 注册编解码器
getServer().getSerializationManager().registerCodec(new CustomEnumTypeCodec());
}
// 创建枚举类型变量节点
private void addCustomEnumTypeVariable(UaFolderNode folderNode) {
UaVariableNode enumNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/CustomEnumVariable"))
.setAccessLevel(AccessLevel.READ_WRITE)
.setUserAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName("CustomEnumVariable"))
.setDisplayName(LocalizedText.english("CustomEnumVariable"))
.setDataType(CustomEnumTypeCodec.TYPE_ID)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.setValue(new DataValue(new Variant(CustomEnumType.RUNNING)))
.build();
getNodeManager().addNode(enumNode);
folderNode.addOrganizes(enumNode);
}
5.2 自定义结构体类型
// 定义自定义结构体
public class CustomStructType implements UaSerializable {
public static final NodeId TYPE_ID = new NodeId(2, "CustomStructType");
public static final NodeId BINARY_ENCODING_ID = new NodeId(2, "CustomStructType.Binary");
private final String foo;
private final UInteger bar;
private final CustomEnumType customEnumType;
public CustomStructType(String foo, UInteger bar, CustomEnumType customEnumType) {
this.foo = foo;
this.bar = bar;
this.customEnumType = customEnumType;
}
public String getFoo() {
return foo;
}
public UInteger getBar() {
return bar;
}
public CustomEnumType getCustomEnumType() {
return customEnumType;
}
@Override
public NodeId getTypeId() {
return TYPE_ID;
}
@Override
public NodeId getBinaryEncodingId() {
return BINARY_ENCODING_ID;
}
@Override
public NodeId getXmlEncodingId() {
return null; // XML编码可选
}
}
// 注册结构体编解码器
public class CustomStructTypeCodec implements DataTypeCodec<CustomStructType> {
@Override
public Class<CustomStructType> getType() {
return CustomStructType.class;
}
@Override
public NodeId getDataTypeId() {
return CustomStructType.TYPE_ID;
}
@Override
public NodeId getBinaryEncodingId() {
return CustomStructType.BINARY_ENCODING_ID;
}
@Override
public CustomStructType decode(SerializationContext context, UaDecoder decoder) throws UaSerializationException {
String foo = decoder.readString("Foo");
UInteger bar = decoder.readUInt32("Bar");
CustomEnumType customEnumType = decoder.readEnum("CustomEnumType", CustomEnumType.class);
return new CustomStructType(foo, bar, customEnumType);
}
@Override
public void encode(SerializationContext context, UaEncoder encoder, CustomStructType value) throws UaSerializationException {
encoder.writeString("Foo", value.getFoo());
encoder.writeUInt32("Bar", value.getBar());
encoder.writeEnum("CustomEnumType", value.getCustomEnumType());
}
}
// 注册自定义结构体类型
private void registerCustomStructType() {
// 创建结构体类型节点
UaDataTypeNode structTypeNode = new UaDataTypeNode(
getNodeContext(),
CustomStructType.TYPE_ID,
newQualifiedName("CustomStructType"),
LocalizedText.english("CustomStructType"),
LocalizedText.english("Custom structure type with multiple fields"),
false
);
getNodeManager().addNode(structTypeNode);
// 创建结构体字段定义
StructureField[] fields = new StructureField[3];
fields[0] = new StructureField(
"Foo",
LocalizedText.english("Foo string field"),
LocalizedText.english("String field for demonstration"),
Identifiers.String,
ValueRanks.Scalar,
null,
true
);
fields[1] = new StructureField(
"Bar",
LocalizedText.english("Bar integer field"),
LocalizedText.english("Unsigned integer field for demonstration"),
Identifiers.UInt32,
ValueRanks.Scalar,
null,
true
);
fields[2] = new StructureField(
"CustomEnumType",
LocalizedText.english("Custom enum field"),
LocalizedText.english("Custom enumeration field"),
CustomEnumTypeCodec.TYPE_ID,
ValueRanks.Scalar,
null,
true
);
// 创建结构体定义
StructureDefinition structDefinition = new StructureDefinition(
StructureType.Structure,
Identifiers.BaseDataType,
null,
fields
);
// 设置结构体定义属性
structTypeNode.setAttribute(
AttributeId.Value,
new DataValue(new Variant(ExtensionObject.encode(
getServer().getSerializationContext(),
structDefinition,
Identifiers.StructureDefinition_Encoding_DefaultBinary
)))
);
// 注册结构体编解码器
getServer().getSerializationManager().registerCodec(new CustomStructTypeCodec());
}
// 创建结构体类型变量节点
private void addCustomStructTypeVariable(UaFolderNode folderNode) {
CustomStructType structValue = new CustomStructType(
"Sample string",
uint(42),
CustomEnumType.RUNNING
);
UaVariableNode structNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("HelloWorld/CustomStructVariable"))
.setAccessLevel(AccessLevel.READ_WRITE)
.setUserAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName("CustomStructVariable"))
.setDisplayName(LocalizedText.english("CustomStructVariable"))
.setDataType(CustomStructType.TYPE_ID)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.setValue(new DataValue(new Variant(structValue)))
.build();
getNodeManager().addNode(structNode);
folderNode.addOrganizes(structNode);
}
6. 数据访问节点:工业标准类型实现
OPC UA定义了专门的数据访问类型,如AnalogItemType,用于工业数据采集:
// 创建数据访问文件夹
UaFolderNode dataAccessFolder = new UaFolderNode(
getNodeContext(),
newNodeId("HelloWorld/DataAccess"),
newQualifiedName("DataAccess"),
LocalizedText.english("DataAccess")
);
getNodeManager().addNode(dataAccessFolder);
rootNode.addOrganizes(dataAccessFolder);
// 创建模拟量节点
try {
AnalogItemTypeNode analogNode = (AnalogItemTypeNode) getNodeFactory().createNode(
newNodeId("HelloWorld/DataAccess/Temperature"),
Identifiers.AnalogItemType,
new NodeFactory.InstantiationCallback() {
@Override
public boolean includeOptionalNode(NodeId typeDefinitionId, QualifiedName browseName) {
return true; // 包含所有可选节点
}
}
);
analogNode.setBrowseName(newQualifiedName("Temperature"));
analogNode.setDisplayName(LocalizedText.english("Temperature"));
analogNode.setDataType(Identifiers.Double);
analogNode.setValue(new DataValue(new Variant(25.5)));
// 设置工程单位范围
analogNode.setEURange(new Range(0.0, 100.0));
// 设置工程单位
analogNode.setEngineeringUnits(new EUInformation(
CefactEngineeringUnits0.Temperature_Degree_Celsius.getUnitId(),
CefactEngineeringUnits0.Temperature_Degree_Celsius.getUnitSymbol(),
LocalizedText.english("Degree Celsius"),
CefactEngineeringUnits0.Temperature_Degree_Celsius.getNumericValue()
));
// 设置仪器范围
analogNode.setInstrumentRange(new Range(-20.0, 150.0));
// 设置精度
analogNode.setPrecision(uint(1));
getNodeManager().addNode(analogNode);
dataAccessFolder.addOrganizes(analogNode);
} catch (UaException e) {
logger.error("Error creating AnalogItemType instance: {}", e.getMessage(), e);
}
高级应用:构建完整的节点创建流程
节点创建完整流程
以下是创建变量节点的完整流程图:
工业设备状态监控示例
以下是一个完整的工业设备状态监控节点创建示例,整合了多种节点类型:
private void createEquipmentMonitoringNodes(UaFolderNode rootNode) {
// 创建设备文件夹
UaFolderNode equipmentFolder = new UaFolderNode(
getNodeContext(),
newNodeId("Equipment"),
newQualifiedName("Equipment"),
LocalizedText.english("Equipment Monitoring")
);
getNodeManager().addNode(equipmentFolder);
rootNode.addOrganizes(equipmentFolder);
// 创建挤出机设备文件夹
UaFolderNode extruderFolder = new UaFolderNode(
getNodeContext(),
newNodeId("Equipment/Extruder1"),
newQualifiedName("Extruder1"),
LocalizedText.english("Extruder #1")
);
getNodeManager().addNode(extruderFolder);
equipmentFolder.addOrganizes(extruderFolder);
// 创建温度传感器节点 (模拟量类型)
try {
AnalogItemTypeNode tempNode = (AnalogItemTypeNode) getNodeFactory().createNode(
newNodeId("Equipment/Extruder1/Temperature"),
Identifiers.AnalogItemType,
(typeDefinitionId, browseName) -> true
);
tempNode.setBrowseName(newQualifiedName("Temperature"));
tempNode.setDisplayName(LocalizedText.english("Temperature"));
tempNode.setDataType(Identifiers.Double);
tempNode.setValue(new DataValue(new Variant(235.7)));
tempNode.setEURange(new Range(0.0, 300.0));
tempNode.setEngineeringUnits(new EUInformation(
CefactEngineeringUnits0.Temperature_Degree_Celsius.getUnitId(),
"°C",
LocalizedText.english("Degree Celsius"),
0.0
));
getNodeManager().addNode(tempNode);
extruderFolder.addOrganizes(tempNode);
} catch (UaException e) {
logger.error("Error creating temperature node", e);
}
// 创建压力传感器节点 (动态更新)
UaVariableNode pressureNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("Equipment/Extruder1/Pressure"))
.setAccessLevel(AccessLevel.READ_ONLY)
.setBrowseName(newQualifiedName("Pressure"))
.setDisplayName(LocalizedText.english("Pressure"))
.setDataType(Identifiers.Double)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.build();
pressureNode.getFilterChain().addLast(
AttributeFilters.getValue(ctx -> {
// 模拟压力波动 (100-120 bar之间)
double basePressure = 110.0;
double fluctuation = (new Random().nextDouble() - 0.5) * 10.0;
return new DataValue(new Variant(basePressure + fluctuation));
})
);
getNodeManager().addNode(pressureNode);
extruderFolder.addOrganizes(pressureNode);
// 创建设备状态节点 (自定义枚举)
UaVariableNode statusNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("Equipment/Extruder1/Status"))
.setAccessLevel(AccessLevel.READ_ONLY)
.setBrowseName(newQualifiedName("Status"))
.setDisplayName(LocalizedText.english("Status"))
.setDataType(CustomEnumTypeCodec.TYPE_ID)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.setValue(new DataValue(new Variant(CustomEnumType.RUNNING)))
.build();
getNodeManager().addNode(statusNode);
extruderFolder.addOrganizes(statusNode);
// 创建生产数据节点 (自定义结构体)
CustomStructType productionData = new CustomStructType(
"Batch #12345",
uint(1500),
CustomEnumType.RUNNING
);
UaVariableNode productionNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("Equipment/Extruder1/ProductionData"))
.setAccessLevel(AccessLevel.READ_ONLY)
.setBrowseName(newQualifiedName("ProductionData"))
.setDisplayName(LocalizedText.english("Production Data"))
.setDataType(CustomStructType.TYPE_ID)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.setValue(new DataValue(new Variant(productionData)))
.build();
getNodeManager().addNode(productionNode);
extruderFolder.addOrganizes(productionNode);
// 创建报警阈值节点 (管理员可写)
UaVariableNode alarmThresholdNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("Equipment/Extruder1/AlarmThreshold"))
.setAccessLevel(AccessLevel.READ_WRITE)
.setBrowseName(newQualifiedName("AlarmThreshold"))
.setDisplayName(LocalizedText.english("Alarm Threshold"))
.setDataType(Identifiers.Double)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.setValue(new DataValue(new Variant(280.0)))
.build();
alarmThresholdNode.getFilterChain().addLast(new RestrictedAccessFilter(identity ->
"admin".equals(identity) ? AccessLevel.READ_WRITE : AccessLevel.READ_ONLY
));
getNodeManager().addNode(alarmThresholdNode);
extruderFolder.addOrganizes(alarmThresholdNode);
}
性能优化与最佳实践
节点创建性能优化
- 批量创建节点:使用节点管理器的批量添加方法减少锁竞争
// 批量添加节点示例
List<UaNode> nodesToAdd = new ArrayList<>();
// 创建多个节点并添加到列表
for (int i = 0; i < 100; i++) {
UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("Sensors/TempSensor" + i))
.setBrowseName(newQualifiedName("TempSensor" + i))
.setDisplayName(LocalizedText.english("Temperature Sensor " + i))
.setDataType(Identifiers.Double)
.setTypeDefinition(Identifiers.BaseDataVariableType)
.setValue(new DataValue(new Variant(25.0)))
.build();
nodesToAdd.add(node);
}
// 批量添加节点
getNodeManager().addNodes(nodesToAdd);
- 使用节点池:对动态创建的临时节点使用对象池
// 简单的节点池实现
public class NodePool {
private final Queue<UaVariableNode> nodeQueue = new ConcurrentLinkedQueue<>();
private final NodeContext nodeContext;
private final int maxPoolSize;
public NodePool(NodeContext nodeContext, int maxPoolSize) {
this.nodeContext = nodeContext;
this.maxPoolSize = maxPoolSize;
// 预创建节点
for (int i = 0; i < maxPoolSize / 2; i++) {
nodeQueue.add(createNode());
}
}
private UaVariableNode createNode() {
return new UaVariableNode.UaVariableNodeBuilder(nodeContext)
.setNodeId(newNodeId("TempNode_" + UUID.randomUUID()))
.setBrowseName(newQualifiedName("TempNode"))
.setDisplayName(LocalizedText.english("Temporary Node"))
.setDataType(Identifiers.String)
.setTypeDefinition
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



