open62541 client读取server端自定义数据类型

概述

平常我们客户端都是读取服务器中的标准数据类型,比如UA_Float、UA_String、UA_Int32…本章主要讲述客户端如何去读取服务器中的自定义数据类型。从open62541中提供的例程来看,在服务器和客户端中都添加了自定义的数据类型,这样操作很方便可以根据对应的数据类型直接获取其值。但实际上在大部分的使用场景中服务器和客户端都不是同一个人写的,所以如果服务端自定了某种数据类型,我们这边是没办法通过这种方式去获取的。在使用UaExpert时我们可以发现,无论我们定义了啥类型,在它那里都能够正确的解析出来。接下来将讲述如何去像UaExpert一样在未知类型时获取对应的值。

服务端

我们使用其例程server_types_custom.c生成一个带有多种自定义数据类型的服务器,代码如下

/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
 * See http://creativecommons.org/publicdomain/zero/1.0/ for more information. */

#include "open62541.h"

#include <signal.h>
#include <stdlib.h>

#include "custom_datatype.h"

UA_Boolean running = true;
const UA_NodeId pointVariableTypeId = {
    1, UA_NODEIDTYPE_NUMERIC, {4243}};
const UA_NodeId measurementVariableTypeId = {
    1, UA_NODEIDTYPE_NUMERIC, {4444}};
const UA_NodeId optstructVariableTypeId = {
    1, UA_NODEIDTYPE_NUMERIC, {4645}};
const UA_NodeId unionVariableTypeId = {
    1, UA_NODEIDTYPE_NUMERIC, {4846}};

static void stopHandler(int sig) {
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
    running = false;
}

static void add3DPointDataType(UA_Server* server)
{
    UA_DataTypeAttributes attr = UA_DataTypeAttributes_default;
    attr.displayName = UA_LOCALIZEDTEXT("en-US", "3D Point Type");

    UA_Server_addDataTypeNode(
        server, PointType.typeId, UA_NODEID_NUMERIC(0, UA_NS0ID_STRUCTURE),
        UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), UA_QUALIFIEDNAME(1, "3D.Point"), attr, NULL, NULL);
}

static void
add3DPointVariableType(UA_Server *server) {
    UA_VariableTypeAttributes dattr = UA_VariableTypeAttributes_default;
    dattr.description = UA_LOCALIZEDTEXT("en-US", "3D Point");
    dattr.displayName = UA_LOCALIZEDTEXT("en-US", "3D Point");
    dattr.dataType = PointType.typeId;
    dattr.valueRank = UA_VALUERANK_SCALAR;

    Point p;
    p.x = 10.0;
    p.y = 20.0;
    p.z = 30.0;
    UA_Variant_setScalar(&dattr.value, &p, &PointType);

    UA_Server_addVariableTypeNode(server, pointVariableTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
                                  UA_QUALIFIEDNAME(1, "3D.Point"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
                                  dattr, NULL, NULL);

}

static void
add3DPointVariable(UA_Server *server) {
    Point p;
    p.x = 97;
    p.y = 97;
    p.z = 97;
    UA_VariableAttributes vattr = UA_VariableAttributes_default;
    vattr.description = UA_LOCALIZEDTEXT("en-US", "3D Point");
    vattr.displayName = UA_LOCALIZEDTEXT("en-US", "3D Point");
    vattr.dataType = PointType.typeId;
    vattr.valueRank = UA_VALUERANK_SCALAR;
    UA_Variant_setScalar(&vattr.value, &p, &PointType);

    UA_Server_addVariableNode(server, UA_NODEID_STRING(1, "3D.Point"),
                              UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                              UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
                              UA_QUALIFIEDNAME(1, "3D.Point"),
                              pointVariableTypeId, vattr, NULL, NULL);
}

static void addMeasurementSeriesDataType(UA_Server* server)
{
    UA_DataTypeAttributes attr = UA_DataTypeAttributes_default;
    attr.displayName = UA_LOCALIZEDTEXT("en-US", "Measurement Series (Array) Type");

    UA_Server_addDataTypeNode(
        server, MeasurementType.typeId, UA_NODEID_NUMERIC(0, UA_NS0ID_STRUCTURE),
        UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), UA_QUALIFIEDNAME(1, "Measurement Series"), attr, NULL, NULL);
}

static void
addMeasurementSeriesVariableType(UA_Server *server) {
    UA_VariableTypeAttributes dattr = UA_VariableTypeAttributes_default;
    dattr.description = UA_LOCALIZEDTEXT("en-US", "Measurement Series");
    dattr.displayName = UA_LOCALIZEDTEXT("en-US", "Measurement Series");
    dattr.dataType = MeasurementType.typeId;
    dattr.valueRank = UA_VALUERANK_ANY;

    Measurements m;
    memset(&m, 0, sizeof(Measurements));
    m.description = UA_STRING("");
    m.measurementSize = 0;
    m.measurement = (UA_Float *) UA_Array_new(m.measurementSize, &MeasurementType);
    UA_Variant_setScalar(&dattr.value, &m, &MeasurementType);

    UA_Server_addVariableTypeNode(server, measurementVariableTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
                                  UA_QUALIFIEDNAME(1, "Measurement Series"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
                                  dattr, NULL, NULL);

}

static void
addMeasurementSeriesVariable(UA_Server *server) {
    Measurements m;
    m.description = UA_STRING_ALLOC("TestDesc");
    m.measurementSize = 3;
    m.measurement = (UA_Float *) UA_Array_new(m.measurementSize, &MeasurementType);
    m.measurement[0] = (UA_Float) 19.1;
    m.measurement[1] = (UA_Float) 20.2;
    m.measurement[2] = (UA_Float) 19.7;

    UA_VariableAttributes vattr = UA_VariableAttributes_default;
    vattr.description = UA_LOCALIZEDTEXT("en-US", "Temp Measurement (Array Example)");
    vattr.displayName = UA_LOCALIZEDTEXT("en-US", "Temp Measurement (Array Example)");
    vattr.dataType = MeasurementType.typeId;
    vattr.valueRank = UA_VALUERANK_ANY;
    UA_Variant_setScalar(&vattr.value, &m, &MeasurementType);

    UA_Server_addVariableNode(server, UA_NODEID_STRING(1, "Temp.Measurement"),
                              UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                              UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
                              UA_QUALIFIEDNAME(1, "Temp Measurement (Array Example)"),
                              measurementVariableTypeId, vattr, NULL, NULL);
    UA_clear(&m, &MeasurementType);
}

static void addOptStructDataType(UA_Server* server)
{
    UA_DataTypeAttributes attr = UA_DataTypeAttributes_default;
    attr.displayName = UA_LOCALIZEDTEXT("en-US", "OptStruct Example Type");

    UA_Server_addDataTypeNode(
        server, OptType.typeId, UA_NODEID_NUMERIC(0, UA_NS0ID_STRUCTURE),
        UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), UA_QUALIFIEDNAME(1, "OptStruct Example"), attr, NULL, NULL);
}

static void
addOptStructVariableType(UA_Server *server) {
    UA_VariableTypeAttributes dattr = UA_VariableTypeAttributes_default;
    dattr.description = UA_LOCALIZEDTEXT("en-US", "OptStruct Example");
    dattr.displayName = UA_LOCALIZEDTEXT("en-US", "OptStruct Example");
    dattr.dataType = OptType.typeId;
    dattr.valueRank = UA_VALUERANK_SCALAR;

    Opt o;
    memset(&o, 0, sizeof(Opt));
    UA_Variant_setScalar(&dattr.value, &o, &OptType);

    UA_Server_addVariableTypeNode(server, optstructVariableTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
                                  UA_QUALIFIEDNAME(1, "OptStruct Example"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
                                  dattr, NULL, NULL);
}

static void
addOptStructVariable(UA_Server *server) {
    Opt o;
    memset(&o, 0, sizeof(Opt));
    o.a = 3;
    o.b = NULL;
    o.c = UA_Float_new();
    *o.c = (UA_Float) 10.10;

    UA_VariableAttributes vattr = UA_VariableAttributes_default;
    vattr.description = UA_LOCALIZEDTEXT("en-US", "OptStruct Example");
    vattr.displayName = UA_LOCALIZEDTEXT("en-US", "OptStruct Example");
    vattr.dataType = OptType.typeId;
    vattr.valueRank = UA_VALUERANK_SCALAR;
    UA_Variant_setScalar(&vattr.value, &o, &OptType);

    UA_Server_addVariableNode(server, UA_NODEID_STRING(1, "Optstruct.Value"),
                              UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                              UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
                              UA_QUALIFIEDNAME(1, "OptStruct Example"),
                              optstructVariableTypeId, vattr, NULL, NULL);
    UA_clear(&o, &OptType);
}

static void addUnionExampleDataType(UA_Server* server)
{
    UA_DataTypeAttributes attr = UA_DataTypeAttributes_default;
    attr.displayName = UA_LOCALIZEDTEXT("en-US", "Union Example Type");

    UA_Server_addDataTypeNode(
        server, UniType.typeId, UA_NODEID_NUMERIC(0, UA_NS0ID_UNION),
        UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), UA_QUALIFIEDNAME(1, "Union Example"), attr, NULL, NULL);
}

static void
addUnionExampleVariableType(UA_Server *server) {
    UA_VariableTypeAttributes dattr = UA_VariableTypeAttributes_default;
    dattr.description = UA_LOCALIZEDTEXT("en-US", "Union Example");
    dattr.displayName = UA_LOCALIZEDTEXT("en-US", "Union Example");
    dattr.dataType = UniType.typeId;
    dattr.valueRank = UA_VALUERANK_SCALAR;

    Uni u;
    memset(&u, 0, sizeof(Uni));
    UA_Variant_setScalar(&dattr.value, &u, &UniType);

    UA_Server_addVariableTypeNode(server, unionVariableTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
                                  UA_QUALIFIEDNAME(1, "Union Example"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
                                  dattr, NULL, NULL);
}

static void
addUnionExampleVariable(UA_Server *server) {
    Uni u;
    u.switchField = UA_UNISWITCH_OPTIONB;
    u.fields.optionB = UA_STRING("test string");

    UA_VariableAttributes vattr = UA_VariableAttributes_default;
    vattr.description = UA_LOCALIZEDTEXT("en-US", "Union Example");
    vattr.displayName = UA_LOCALIZEDTEXT("en-US", "Union Example");
    vattr.dataType = UniType.typeId;
    vattr.valueRank = UA_VALUERANK_SCALAR;
    UA_Variant_setScalar(&vattr.value, &u, &UniType);

    UA_Server_addVariableNode(server, UA_NODEID_STRING(1, "Union.Value"),
                              UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                              UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
                              UA_QUALIFIEDNAME(1, "Union Example"),
                              unionVariableTypeId, vattr, NULL, NULL);
}

int main(void) {
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

    UA_Server *server = UA_Server_new();
    UA_ServerConfig *config = UA_Server_getConfig(server);
    UA_ServerConfig_setDefault(config);

    /* Make your custom datatype known to the stack */
    UA_DataType *types = (UA_DataType*)UA_malloc(4 * sizeof(UA_DataType));
    UA_DataTypeMember *pointMembers = (UA_DataTypeMember*)UA_malloc(sizeof(UA_DataTypeMember) * 3);
    pointMembers[0] = Point_members[0];
    pointMembers[1] = Point_members[1];
    pointMembers[2] = Point_members[2];
    types[0] = PointType;
    types[0].members = pointMembers;

    types[1] = MeasurementType;
    UA_DataTypeMember *measurementMembers = (UA_DataTypeMember*)UA_malloc(sizeof(UA_DataTypeMember) * 2);
    measurementMembers[0] = Measurements_members[0];
    measurementMembers[1] = Measurements_members[1];
    types[1].members = measurementMembers;

    types[2] = OptType;
    UA_DataTypeMember *optMembers = (UA_DataTypeMember*)UA_malloc(sizeof(UA_DataTypeMember) * 3);
    optMembers[0] = Opt_members[0];
    optMembers[1] = Opt_members[1];
    optMembers[2] = Opt_members[2];
    types[2].members = optMembers;

    types[3] = UniType;
    UA_DataTypeMember *uniMembers = (UA_DataTypeMember*)UA_malloc(sizeof(UA_DataTypeMember) * 2);
    uniMembers[0] = Uni_members[0];
    uniMembers[1] = Uni_members[1];
    types[3].members = uniMembers;

    /* Attention! Here the custom datatypes are allocated on the stack. So they
     * cannot be accessed from parallel (worker) threads. */
    UA_DataTypeArray customDataTypes = {config->customDataTypes, 4, types};
    config->customDataTypes = &customDataTypes;

    add3DPointDataType(server);
    add3DPointVariableType(server);
    add3DPointVariable(server);

    addMeasurementSeriesDataType(server);
    addMeasurementSeriesVariableType(server);
    addMeasurementSeriesVariable(server);

    addOptStructDataType(server);
    addOptStructVariableType(server);
    addOptStructVariable(server);

    addUnionExampleDataType(server);
    addUnionExampleVariableType(server);
    addUnionExampleVariable(server);

    UA_Server_run(server, &running);

    UA_Server_delete(server);
    UA_free(pointMembers);
    UA_free(measurementMembers);
    UA_free(optMembers);
    UA_free(uniMembers);
    UA_free(types);
    return EXIT_SUCCESS;
}

服务器这部分代码就不进行说明了,可见自定义数据类型一文。我们使用Uaexpert去连接改服务器可以看到对应的自定义变量已经成功添加。
在这里插入图片描述

客户端

这里我们已3D Point这个自定义变量为例讲述如何去获取其值。我们通过UaExpert查看其NodeId为ns=1;s=3D.Point,也可以通过浏览服务器中的节点进行查找。由于这里只是讲述读取自定义数据类型值,这里就不多赘述,整体代码如下,

/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
 * See http://creativecommons.org/publicdomain/zero/1.0/ for more information. */

#include "open62541.h"

int main(int argc, char *argv[]) 
{
    UA_Client *client = UA_Client_new();
    UA_ClientConfig* config = UA_Client_getConfig(client);
    UA_ClientConfig_setDefault(config);

	UA_StatusCode retval;
    /* Connect to a server */
	retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
    if(retval != UA_STATUSCODE_GOOD) {
        printf("UA_Client_connect failed, retval = %d\n", retval);
        UA_Client_delete(client);
        system("pause");
        return EXIT_FAILURE;
    }

    UA_NodeId varNode = UA_NODEID_STRING(1, "3D.Point");
    UA_NodeId dataTypeNodeId = UA_NODEID_NULL;
    UA_Variant variant;
    UA_Variant_init(&variant);

    // 获取节点的数据类型节点ID
    retval = UA_Client_readDataTypeAttribute(client, varNode, &dataTypeNodeId);
    if (retval != UA_STATUSCODE_GOOD) {
        printf("UA_Client_readDataTypeAttribute failed, retval = %d\n", retval);
        UA_Client_disconnect(client);
        UA_Client_delete(client);
        system("pause");
        return EXIT_FAILURE;
    }

    // 获取该数据类型的浏览名称
    UA_LocalizedText dataTypeName;
    UA_LocalizedText_init(&dataTypeName);
    retval = UA_Client_readDisplayNameAttribute(client, dataTypeNodeId, &dataTypeName);
    if (retval != UA_STATUSCODE_GOOD) {
        printf("UA_Client_readDisplayNameAttribute failed, retval = %d\n", retval);
        UA_Client_disconnect(client);
        UA_Client_delete(client);
        system("pause");
        return EXIT_FAILURE;
    }
    printf("borowse name = %.*s\n", dataTypeName.text.length, dataTypeName.text.data);

    // 获取该数据类型的描述信息
    UA_StructureDefinition structureDefinition;
    retval = __UA_Client_readAttribute(client, &dataTypeNodeId, UA_ATTRIBUTEID_DATATYPEDEFINITION, &structureDefinition, &UA_TYPES[UA_TYPES_STRUCTUREDEFINITION]);
    if (retval != UA_STATUSCODE_GOOD) {
        printf("__UA_Client_readAttribute failed, retval = %d\n", retval);
        UA_Client_disconnect(client);
        UA_Client_delete(client);
        system("pause");
        return EXIT_FAILURE;
    }

    // 根据读取到server端的数据类型构造对应的数据类型
    UA_DataType* dataType = (UA_DataType * )malloc(sizeof(UA_DataType));
    UA_DataTypeMember* dataTypeMembers = (UA_DataTypeMember*)malloc(sizeof(UA_DataTypeMember) * structureDefinition.fieldsSize);
    for (int ii = 0; ii < structureDefinition.fieldsSize; ii++)
    {
        UA_DataTypeMember* member = &dataTypeMembers[ii];
        UA_StructureField* field = &structureDefinition.fields[ii];
        member->memberName = (char*)malloc(sizeof(char) * (field->name.length + 1));
        memcpy_s((void*)member->memberName, field->name.length, field->name.data, field->name.length);
        member->memberType = &UA_TYPES[UA_TYPES_FLOAT]; // field字段中有类型的NodeId,这里就不多判断了
        member->padding = 0;  // 成员之间的padding可以通过计算获得
        member->isOptional = false;// field->isOptional;
        member->isArray = false;// 可通过读取ValueRank属性去进行判断
    }
    dataType->typeName = "unknow";
    dataType->binaryEncodingId = structureDefinition.defaultEncodingId;
    dataType->typeId = dataTypeNodeId;
    dataType->memSize = 12;
    dataType->typeKind = UA_DATATYPEKIND_STRUCTURE;// 可根据structureDefinition.structureType判定;
    dataType->pointerFree = true;
    dataType->overlayable = false;
    dataType->membersSize = structureDefinition.fieldsSize;
    dataType->members = dataTypeMembers;

    // 将该类型添加进client自定义数据类型中
    UA_DataTypeArray customDataTypes = { NULL, 1, dataType };
    config->customDataTypes = &customDataTypes;

    retval = UA_Client_readValueAttribute(client, varNode, &variant);
    if (retval != UA_STATUSCODE_GOOD) {
        printf("UA_Client_readValueAttribute failed, retval = %d\n", retval);
        UA_Client_disconnect(client);
        UA_Client_delete(client);
        system("pause");
        return EXIT_FAILURE;
    }

    printf("value type name = %s\n", variant.type->typeName);

    // 输出对应的值
    for (int ii = 0; ii < structureDefinition.fieldsSize; ii++)
    {
        unsigned int offset = 0;
        UA_StructureField* field = &structureDefinition.fields[ii];
        UA_NodeId floatNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_FLOAT);
        if (UA_NodeId_equal(&field->dataType, &floatNodeId)) {
            UA_Float* ptr = (UA_Float*)variant.data;
            printf("value %d = %f, ", ii, *(ptr+offset));
            offset += sizeof(UA_Float);
        }
        else
        {
            // ....处理其他类型数据
        }
    }
    printf("\n");

    UA_Client_disconnect(client);
    UA_Client_delete(client);
	system("pause");
    return EXIT_SUCCESS;
}

这里主要是通过找到该变量的数据类型(DataType),然后通过获取该数据类型的信息去构造对应的数据类型并添加进客户端自定义数据类型中去。在structureDefinition中有服务器端添加该自定义数据类型带有的所有信息,调试时看到的相关信息如下
在这里插入图片描述
相关字段的描述都非常详细,与UaExpert中的相关信息一致
在这里插入图片描述
这是举例比较简单的类型,更复杂的类型padding以及结构大小等一系列值需要进行动态计算。在这个例程中没有进行处理。值的获取通过指针的偏移来进行实现。运行该客户端,结果如下
在这里插入图片描述可见对应的值以及获取成功了,与UaExpert中的结果一致。

说一说

获取自定义数据类型节点值还是比较麻烦,因为我们这边没有特定的数据结构去转换对应的值,还需将其结构解析出来并添加到自身的自定义数据类型中。刚开始花了很久时间去直接根据类型解析对应的值,发现里面的值都是存在问题的,于是查看了处理消息的部分源码如下

/* Processes the received service response. Either with an async callback or by
 * decoding the message and returning it "upwards" in the
 * SyncResponseDescription. */
static UA_StatusCode
processServiceResponse(void *application, UA_SecureChannel *channel,UA_MessageType messageType, UA_UInt32 requestId, UA_ByteString *message) 
{
	......
	/* Decode the response */
    retval = UA_decodeBinaryInternal(message, &offset, rd->response, rd->responseType,
                                     rd->client->config.customDataTypes);
	......
}
                  

发现客户端在解析收到的二进制数据时会根据自定义的数据类型去进行解析,如果解析不成功的话返回的值是存在问题的。客户端在没有添加对应的数据类型时获取到的UA_Variant值类型是ExtensionObject的
在这里插入图片描述
在添加构造的数据类型后,我们可以看到它的结构发生了变化
在这里插入图片描述
可以看出后者是解析成功了。我们可以在UA_decodeBinaryInternal处查看message对应的内存信息,会发现从服务器端过来的信息是以一定格式存储并且包含了值的相关信息,客户端这边没有对应格式的数据类型定义的话无法正常进行解析。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
在使用open62541数据时,需要进行以下步骤: 1. 首先,需要在CMakeLists.txt中添加相关的依赖库和头文件路径。例如,使用`include_directories`指令包含open62541的头文件路径,使用`find_library`指令找到open62541的库文件。 2. 然后,在代码中创建一个UA_DataSource结构体实例,该结构体包含了读和写两个函数指针。 3. 对于写数据,可以使用`UA_Server_writeValue`函数来写入指定的节点的值。该函数需要传入UA_Server实例、节点ID、写入的值等参数。 4. 在写入数据之前,需要先创建一个UA_VariableAttributes结构体实例来设置节点的属性,包括数据类型、访问级别等。 5. 然后,可以使用`UA_Server_addVariableNode`函数来创建一个变量节点,并将其添加到服务器中。该函数需要传入UA_Server实例、节点ID、父节点ID、节点属性等参数。 6. 接下来,可以使用写入函数指针writeForVariable来写入数据。该函数指针需要自定义实现,用于将数据写入到指定的节点中。 这样,就可以使用open62541来写入数据了。 <span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [open62541简单的封装类](https://download.csdn.net/download/weixin_43829959/12920766)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [学习open62541 --- [29] 数据源的使用细节](https://blog.csdn.net/whahu1989/article/details/106298733)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

复杂的世界311

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

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

抵扣说明:

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

余额充值