从0到1掌握Eclipse Milo变量节点创建:工业级OPC UA服务器开发指南

从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变量节点是工业数据的载体,包含以下关键属性:

属性名称数据类型描述重要性
NodeIdNodeId节点唯一标识符必须
BrowseNameQualifiedName浏览名称必须
DisplayNameLocalizedText显示名称必须
DataTypeNodeId数据类型标识符必须
ValueDataValue节点值必须
AccessLevelByte访问权限推荐
UserAccessLevelByte用户访问权限推荐
ValueRankInt32值维度数组节点必须
ArrayDimensionsUInt32[]数组维度固定数组必须
TypeDefinitionNodeId类型定义推荐

Eclipse Milo节点创建架构

Eclipse Milo提供了灵活的节点创建API,主要涉及以下核心类:

mermaid

实战指南:从基础到高级的变量节点创建

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);
}

高级应用:构建完整的节点创建流程

节点创建完整流程

以下是创建变量节点的完整流程图:

mermaid

工业设备状态监控示例

以下是一个完整的工业设备状态监控节点创建示例,整合了多种节点类型:

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);
}

性能优化与最佳实践

节点创建性能优化

  1. 批量创建节点:使用节点管理器的批量添加方法减少锁竞争
// 批量添加节点示例
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);
  1. 使用节点池:对动态创建的临时节点使用对象池
// 简单的节点池实现
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),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值