Android HIDL单模块,多接口,c++服务

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

最近做的一个东西,需要跟硬件通讯,给上层app提供接口,因为涉及定制的硬件比较多,但是业务逻辑不复杂,只需要简单的通讯,就单独做成一个模块,专门用于这些业务逻辑不复杂的硬件通讯。具体的修改还是参照之前的那一篇C++ HIDL,具体路径如下:

https://blog.csdn.net/jamecer/article/details/123652457

因为只使用一个服务进行启动及处理,代码中有关线程方面可能会有一些坑,如果有更好的方法,谢谢指正。


一、有关selinux的设置

这里就不先从cpp等文件的设置开始了,直接先从selinux的添加开始
之前的安全并未对这些语句进行解释,现在对这些语句进行解释
具体可见如下博客,对selinux的一些说明

https://blog.csdn.net/xxdw1992/article/details/120881548
https://blog.csdn.net/luoshengyang/article/details/38054645
https://blog.csdn.net/ldswfun/article/details/125284523
https://blog.csdn.net/qidi_huang/article/details/107472617

1-1 system/sepolicy/vendor目录

在/system/sepolicy/vendor/中新建hal_devicesServer_default.te

//这段代码表示 将 hal_devicesServer_default 声明为一个进程
//至于domain这个属性可以做什么事情,在system/sepolicy/public/domain.te中可知
type hal_devicesServer_default, domain;

/*下面这几个语句,是使用hal_server_domain这一个宏,
本质上跟上面那个语句的语法是一样的,
具体做了什么事情,我们下面进行分析,
这个宏是在system/sepolicy/public/te_macros中*/
/* 具体用法及功能,在文件中有详细说明
define(`hal_server_domain', `
typeattribute $1 halserverdomain;
typeattribute $1 $2_server;
typeattribute $1 $2;')
*/
/*将参数带入,可见如下声明(具体声明的意义,见上面提到的两篇博客):
typeattribute	hal_devicesServer_default	halserverdomain;
typeattribute	hal_devicesServer_default	hal_deviceA_server;
typeattribute	hal_devicesServer_default	hal_deviceA;
*/
hal_server_domain(hal_devicesServer_default, hal_deviceA)
hal_server_domain(hal_devicesServer_default, hal_deviceB)
hal_server_domain(hal_devicesServer_default, hal_deviceC)

//继续做类型声明
type hal_devicesServer_default_exec, exec_type, vendor_file_type, file_type;
/*同样见te_macros中:
define(`init_daemon_domain', `
domain_auto_trans(init, $1_exec, $1)
')
#执行了如下:
domain_auto_trans(init, hal_devicesServer_default_exec, hal_devicesServer_default)

define(`domain_auto_trans', `
# Allow the necessary permissions.
domain_trans($1,$2,$3)
# Make the transition occur by default.
type_transition $1 $2:process $3;
')
#执行了如下
domain_trans(init, hal_devicesServer_default_exec, hal_devicesServer_default)
type_transition init hal_devicesServer_default_exec:process hal_devicesServer_default;

define(`domain_trans', `
# Old domain may exec the file and transition to the new domain.
allow $1 $2:file { getattr open read execute map };
allow $1 $3:process transition;
# New domain is entered by executing the file.
allow $3 $2:file { entrypoint open read execute getattr map };
# New domain can send SIGCHLD to its caller.
ifelse($1, `init', `', `allow $3 $1:process sigchld;')
# Enable AT_SECURE, i.e. libc secure mode.
dontaudit $1 $3:process noatsecure;
# XXX dontaudit candidate but requires further study.
allow $1 $3:process { siginh rlimitinh };
')
执行了如下:
dontaudit init hal_devicesServer_default:process noatsecure;
allow init hal_devicesServer_default:process { siginh rlimitinh };
*/
init_daemon_domain(hal_devicesServer_default)

在system/sepolicy/vendor/file_contexts中添加如下

/*将vendor.mediatek.hardware.devicesServer@1.0-service这个服务,
授予hal_devicesServer_default_exec相关的权限,即该服务是一个可执行的安全对象
*/
/(vendor|system/vendor)/bin/hw/vendor\.mediatek\.hardware\.devicesServer@1\.0-service     u:object_r:hal_devicesServer_default_exec:s0

1-2 system/sepolicy/public和/system/sepolicy/prebuilts目录

system/sepolicy/public/hal_devicesServer.te

# HwBinder IPC from client to server, and callbacks

# deviceA
binder_call(hal_deviceA_client, hal_deviceA_server)
binder_call(hal_deviceA_server, hal_deviceA_client)
add_hwservice(hal_deviceA_server, hal_deviceA_hwservice)
allow hal_deviceA_client hal_deviceA_hwservice:hwservice_manager find;
/*
#####################################
# binder_call(clientdomain, serverdomain)
# Allow clientdomain to perform binder IPC to serverdomain.
define(`binder_call', `
# Call the server domain and optionally transfer references to it.
allow $1 $2:binder { call transfer };
# Allow the serverdomain to transfer references to the client on the reply.
allow $2 $1:binder transfer;
# Receive and use open files from the server.
allow $1 $2:fd use;
')
执行如下:
allow hal_deviceA_client hal_deviceA_server:binder { call transfer };
allow hal_deviceA_server hal_deviceA_client:binder transfer;
allow hal_deviceA_client hal_deviceA_server:fd use;

define(`add_hwservice', `
  allow $1 $2:hwservice_manager { add find };
  allow $1 hidl_base_hwservice:hwservice_manager add;
  neverallow { domain -$1 } $2:hwservice_manager add;
')
执行如下:
allow hal_deviceA_server hal_deviceA_hwservice:hwservice_manager { add find };
allow hal_deviceA_server hidl_base_hwservice:hwservice_manager add;
neverallow { domain -hal_deviceA_server } hal_deviceA_hwservice:hwservice_manager add;

*/

# deviceB
binder_call(hal_deviceB_client, hal_deviceB_server)
binder_call(hal_deviceB_server, hal_deviceB_client)
add_hwservice(hal_deviceB_server, hal_deviceB_hwservice)
allow hal_deviceB_client hal_deviceB_hwservice:hwservice_manager find;

# deviceC
binder_call(hal_deviceC_client, hal_deviceC_server)
binder_call(hal_deviceC_server, hal_deviceC_client)
add_hwservice(hal_deviceC_server, hal_deviceC_hwservice)
allow hal_deviceC_client hal_deviceC_hwservice:hwservice_manager find;

在/system/sepolicy/public/hwservice.te添加如下

type hal_deviceA_hwservice, hwservice_manager_type;
type hal_deviceB_hwservice, hwservice_manager_type;
type hal_deviceC_hwservice, hwservice_manager_type;

在system/sepolicy/public/attributes添加如下

hal_attribute(deviceA);
hal_attribute(deviceB);
hal_attribute(deviceC);
/*
#####################################
# hal_attribute(hal_name)
# Add an attribute for hal implementations along with necessary
# restrictions.
define(`hal_attribute', `
attribute hal_$1;
expandattribute hal_$1 true;
attribute hal_$1_client;
expandattribute hal_$1_client true;
attribute hal_$1_server;
expandattribute hal_$1_server false;

neverallow { hal_$1_server -halserverdomain } domain:process fork;
# hal_*_client and halclientdomain attributes are always expanded for
# performance reasons. Neverallow rules targeting expanded attributes can not be
# verified by CTS since these attributes are already expanded by that time.
build_test_only(`
neverallow { hal_$1_server -hal_$1 } domain:process fork;
neverallow { hal_$1_client -halclientdomain } domain:process fork;
')
')
*/

在sepolicy/prebuilts/api/30.0/public/attributes、sepolicy/prebuilts/api/30.0/public/hal_devicesServer.te、sepolicy/prebuilts/api/30.0/public/hwservice.te中的修改同上方一致。

1-3 system/sepolicy/private/和/system/sepolicy/prebuilts目录

在system/sepolicy/private/hwservice_contexts做如下添加

/*
因为 demoComponent HAL 依赖 hwbinder 进行通信,
所以我们还需要编写 hwservice_contexts。
这个文件名也是由 SELinux 框架固定的。
我们在这个文件中将新增的接口定义为一个安全对象,作用和 file_contexts 类似。
*/
vendor.mediatek.hardware.devicesServer::IDeviceAOfHidl u:object_r:hal_deviceA_hwservice:s0
vendor.mediatek.hardware.devicesServer::IDeviceBOfHidl u:object_r:hal_deviceB_hwservice:s0
vendor.mediatek.hardware.devicesServer::IDeviceCOfHidl u:object_r:hal_deviceC_hwservice:s0

在system/sepolicy/prebuilts/api/30.0/private/compat/26.0/26.0.ignore.cil和
system/sepolicy/prebuilts/api/30.0/private/compat/27.0/27.0.ignore.cil和
system/sepolicy/prebuilts/api/30.0/private/compat/28.0/28.0.ignore.cil和
system/sepolicy/prebuilts/api/30.0/private/compat/29.0/29.0.ignore.cil做如下添加

hal_deviceA_hwservice
hal_deviceB_hwservice
hal_deviceC_hwservice

在system/sepolicy/prebuilts/api/30.0/private/hwservice_contexts和
sepolicy/prebuilts/api/30.0/private/compat/26.0/26.0.ignore.cil、
sepolicy/prebuilts/api/30.0/private/compat/27.0/27.0.ignore.cil、
sepolicy/prebuilts/api/30.0/private/compat/28.0/28.0.ignore.cil、
sepolicy/prebuilts/api/30.0/private/compat/29.0/29.0.ignore.cil中的修改同上方一致。

二、其他的添加

兼容性矩阵的添加
hardware/interfaces/compatibility_matrices/compatibility_matrix.5.xml

<hal format="hidl" optional="true">
	<name>vendor.mediatek.hardware.devicesServer</name>
	<version>1.0</version>
	<interface>
		<name>IDeviceAOfHidl</name>
		<instance>default</instance>
	</interface>
	<interface>
		<name>IDeviceBOfHidl</name>
		<instance>default</instance>
	</interface>
	<interface>
		<name>IDeviceCOfHidl</name>
		<instance>default</instance>
	</interface>
</hal>

device/mediatek/mtxxxx/manifest.xml的添加

<hal format="hidl">
	<name>vendor.mediatek.hardware.devicesServer</name>
	<transport>hwbinder</transport>
	<version>1.0</version>
	<interface>
		<name>IDeviceAOfHidl</name>
		<instance>default</instance>
	</interface>
	<interface>
		<name>IDeviceBOfHidl</name>
		<instance>default</instance>
	</interface>
	<interface>
		<name>IDeviceCOfHidl</name>
		<instance>default</instance>
	</interface>
</hal>

device/mediatek/mtxxxx/

三、创建HIDL接口及对应的服务

这里只创建deviceA作为示例,其余实例也相似,具体看自己需求

3-1 HAL接口的创建

路径:vendor/mediatek/proprietary/hardware/interface/devicesServer/1.0下创建
IDeviceAOfHidl.hal

package vendor.mediatek.hardware.devicesServer@1.0;
import vendor.mediatek.hardware.devicesServer@1.0::IDeviceADataCallback;
interface IDeviceAOfHidl{
   init();
   release();
   sendCommand(vec<uint8_t> data);
   setDataCallback(IDeviceADataCallback callback);
};

IDeviceADataCallback.hal

package vendor.mediatek.hardware.devicesServer@1.0;
interface IDeviceADataCallback{
    onDataReceived(vec<uint8_t> data);
};

Android.bp

// This file is autogenerated by hidl-gen -Landroidbp.

hidl_interface {
    name: "vendor.mediatek.hardware.devicesServer@1.0",
    root: "vendor.mediatek.hardware",
    srcs: [
        "IDeviceADataCallback.hal",
        "IDeviceAOfHidl.hal",
        "IDeviceBDataCallback.hal",
        "IDeviceBOfHidl.hal",
        "IDeviceCDataCallback.hal",
        "IDeviceCOfHidl.hal",
    ],
    interfaces: [
        "android.hidl.base@1.0",
    ],
    gen_java: true,
}
subdirs = [
    "default",
]

3-2 service.cpp、rc文件、DeviceAOfHidl.cpp以及Andorid.bp的创建

路径:vendor/mediatek/proprietary/hardware/interface/devicesServer/1.0/default下创建
vendor.mediatek.hardware.devicesServer@1.0-service.rc

service devicesServer-hal-1-0 /vendor/bin/hw/vendor.mediatek.hardware.devicesServer@1.0-service
    class hal
    user root
    group system

service.cpp

#define TAG "devicesServer-1.0-service"
 
#include <android/log.h>
#include <hidl/HidlTransportSupport.h>
#include <vendor/mediatek/hardware/devicesServer/1.0/IDeviceAOfHidl.h>
#include <vendor/mediatek/hardware/devicesServer/1.0/IDeviceBOfHidl.h>
#include <vendor/mediatek/hardware/devicesServer/1.0/IDeviceCOfHidl.h>
#include <hidl/LegacySupport.h>
#include "DeviceAOfHidl.h"
#include "DeviceBOfHidl.h"
#include "DeviceCOfHidl.h"

using android::sp;
using android::status_t;
using android::OK;
using android::hardware::configureRpcThreadpool;
using android::hardware::joinRpcThreadpool;
using android::hardware::defaultPassthroughServiceImplementation;
using vendor::mediatek::hardware::devicesServer::V1_0::IDeviceAOfHidl;
using vendor::mediatek::hardware::devicesServer::implementation::DeviceAOfHidl;
using vendor::mediatek::hardware::devicesServer::V1_0::IDeviceBOfHidl;
using vendor::mediatek::hardware::devicesServer::implementation::DeviceBOfHidl;
using vendor::mediatek::hardware::devicesServer::V1_0::IDeviceCOfHidl;
using vendor::mediatek::hardware::devicesServer::implementation::DeviceCOfHidl;

int main()
{

    android::sp<IDeviceAOfHidl> deviceA_service = new DeviceAOfHidl();
    android::sp<IDeviceBOfHidl> deviceB_service = new DeviceBOfHidl();
    android::sp<IDeviceCOfHidl> deviceC_service = new DeviceCOfHidl();
    configureRpcThreadpool(32, true /*callerWillJoin*/);
    
    status_t status = deviceA_service->registerAsService();
    if (status == OK){   
        deviceA_service->init();
    }

    status = deviceB_service->registerAsService();
    if (status == OK){   
        deviceB_service->init();
    }

    status = deviceC_service->registerAsService();
    if (status == OK){   
        deviceC_service->init();
    }

    joinRpcThreadpool();
    LOGD("Cannot register CascadeOfHidlhidl HAL service");
    return 1;
}

DeviceAOfHidl.h

// FIXME: your file license if you have one

#pragma once

#include <vendor/mediatek/hardware/devicesServer/1.0/IDeviceAOfHidl.h>
#include <hidl/MQDescriptor.h>
#include <hidl/Status.h>
#include <android/log.h>
#include <thread>
#include <deque>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <math.h>
#include <threads.h>
#include <condition_variable>
#include <mutex>
#include <cstring>
#include <termios.h>


namespace vendor::mediatek::hardware::devicesServer::implementation {

using ::android::hardware::hidl_array;
using ::android::hardware::hidl_memory;
using ::android::hardware::hidl_string;
using ::android::hardware::hidl_vec;
using ::android::hardware::Return;
using ::android::hardware::Void;
using ::android::sp;

#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG, __VA_ARGS__) 
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG , TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO  , TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN  , TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR  , TAG, __VA_ARGS__)

struct DeviceAOfHidl : public V1_0::IDeviceAOfHidl {
    Return<void> init() override;
    Return<void> release() override;
    Return<void> sendCommand(const hidl_vec<uint8_t>& data) override;
    Return<void> setDataCallback(const sp<::vendor::mediatek::hardware::devicesServer::V1_0::IDeviceADataCallback>& callback) override;

private:
    sp<V1_0::IDeviceADataCallback> mCallback = nullptr;

    //指令队列
    std::deque<char*> deqIn;

    //DeviceA线程锁+条件变量
    std::condition_variable cv;
    std::mutex mtx;

    //DeviceA打开的读写文件buffer
    int fd_tty;

    //串口初始化方法
    int init_tty(int fd_tty);

    void covertArray2Vector(const char *in, int len, std::vector<uint8_t> &out);
    void covertVector2Array(std::vector<uint8_t> in, char *out);

    //设置数据回调
    int DataReturn(int channel, unsigned char returnData[128], int returnDataLen);

public:
    //DeviceA的读写线程
    int WriteDataThread();
    int ReadDataThread();

};

}

DeviceAOfHidl.cpp

// FIXME: your file license if you have one
#define TAG "devicesServer-1.0-service"

#include "DeviceAOfHidl.h"

namespace vendor::mediatek::hardware::devicesServer::implementation {

Return<void> DeviceAOfHidl::init() {
    // TODO implement
	std::thread th_read(&DeviceAOfHidl::ReadDataThread, this);
    std::thread th_write(&DeviceAOfHidl::WriteDataThread, this);
	if(th_read.joinable()){
		//这里使用线程分离,不阻塞,避免在service.cpp中出现阻塞,不能注册下一个硬件接口的服务
		th_read.detach();
	}
    if(th_write.joinable()){
		th_write.detach();
	}
    return Void();
}

int DeviceAOfHidl::ReadDataThread()
{
    unsigned char tty_ret_val_head[3];
    unsigned char tty_ret_val_data[128];
    unsigned char ret_val[128];
    int ret = 0;
    fd_tty = open("/dev/tty0", O_RDWR);
	if (fd_tty < 0){
		LOGE("open /dev/tty0 failed!\n");
	}

    if (init_tty(fd_tty) == -1) {
		LOGE("init_tty in failed!\n");
	}

    while(1){
        ret = read(fd_tty, &tty_ret_val_head, sizeof(tty_ret_val_head));
        DataReturn(0, tty_ret_val_head, 3);
    }   
    return 0;
}


int DeviceAOfHidl::WriteDataThread()
{
    unsigned char tty_val_cmd[128];
    int cmd_len = 0;

    while(1){
        std::unique_lock <std::mutex> lck(mtx);
        if(deqIn.empty()){
            cv.wait(lck);
        }else{
            char *p = deqIn.front();
            memset(tty_val_cmd, 0x00, sizeof(tty_val_cmd));
            memcpy(tty_val_cmd, p, 3);
            tcflush(fd_tty, TCIFLUSH); //清空输入缓冲区
			tcflush(fd_tty, TCOFLUSH); //清空输入缓冲区
            int byte_count = write(fd_tty, tty_val_cmd, 3);
            deqIn.pop_front();
        }
    }
    return 0;
}

int DeviceAOfHidl::DataReturn(int channel, unsigned char returnData[128], int returnDataLen)
{
	if(mCallback != nullptr){
			std::vector<uint8_t> out;
			char in1[returnDataLen];
			for(int i = 0; i < returnDataLen; i++){
				in1[i] = returnData[i];
			}
			covertArray2Vector(in1, returnDataLen, out);
            if(channel == 1){}
            mCallback->onDataReceived(out);
	}
    return 0;
}


/*设置串口参数*/
int DeviceAOfHidl::init_tty(int fd_tty)
{
	struct termios termios_rfid;

	// bzero(&termios_rfid, sizeof(termios_rfid));//清空结构体
	memset(&termios_rfid, 0x00, sizeof(termios_rfid));
	cfmakeraw(&termios_rfid); //设置终端属性,激活选项

	cfsetispeed(&termios_rfid, B115200); //输入波特率
	cfsetospeed(&termios_rfid, B115200); //输出波特率

	termios_rfid.c_cflag |= CLOCAL | CREAD; //本地连接和接收使能

	termios_rfid.c_cflag &= ~CSIZE; //清空数据位
	termios_rfid.c_cflag |= CS8;	//数据位为8位

	termios_rfid.c_cflag &= ~PARENB; //无奇偶校验

	termios_rfid.c_cflag &= ~CSTOPB; //一位停止位

	tcflush(fd_tty, TCIFLUSH);

	termios_rfid.c_cc[VTIME] = 10; //设置等待时间
	termios_rfid.c_cc[VMIN] = 1;

	tcflush(fd_tty, TCIFLUSH); //清空输入缓冲区

	if (tcsetattr(fd_tty, TCSANOW, &termios_rfid)) //激活串口设置
		return 0;
	return 1;
}

Return<void> DeviceAOfHidl::release() {
    // TODO implement
    return Void();
}

void DeviceAOfHidl::covertVector2Array(std::vector<uint8_t> in, char *out)
{
    int size = in.size();
    for (int i = 0; i < size; i++)
    {
        out[i] = in.at(i);
    }
}

void DeviceAOfHidl::covertArray2Vector(const char *in, int len, std::vector<uint8_t> &out)
{
    out.clear();
    for (int i = 0; i < len; i++)
    {
        out.push_back(in[i]);
    }
}

Return<void> DeviceAOfHidl::sendCommand(const hidl_vec<uint8_t>& data) {
    // TODO implement
    char *cstr_data = new char[data.size() + 1];
    covertVector2Array(data, cstr_data);
    std::unique_lock <std::mutex> lck(mtx); 
    deqIn.push_back(cstr_data);
    cv.notify_all();
    return Void();
}

Return<void> DeviceAOfHidl::setDataCallback(const sp<::vendor::mediatek::hardware::devicesServer::V1_0::IDeviceADataCallback>& callback) {
    mCallback = callback;
    return Void();
}
} 

Android.bp

// FIXME: your file license if you have one

cc_binary {
    name: "vendor.mediatek.hardware.devicesServer@1.0-service",
    relative_install_path: "hw",
    proprietary: true,
    vendor: true,
    defaults: ["hidl_defaults"],
    init_rc: ["vendor.mediatek.hardware.devicesServer@1.0-service.rc"],
    srcs: [
        "DeviceAOfHidl.cpp",
        "DeviceBOfHidl.cpp",
        "DeviceCOfHidl.cpp",
        "service.cpp"
    ],
    shared_libs: [
        "liblog",
        "libcutils",
        "libdl",
        "libbase",
        "libutils",
        "libhardware",
        "libhidlbase",
        "vendor.mediatek.hardware.devicesServer@1.0",
    ],
}

总结

之前一般是一个硬件设备对应一个模块,在这里是针对一些执行功能比较单一,业务逻辑不复杂的硬件设备进行的分类,多个硬件设备集中在一个模块中,在上层app中使用多个接口进行通信。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要添加HIDL接口,你需要按照以下步骤进行操作: 1. 创建一个新的HIDL接口文件,它应该包含你想要添加的新接口定义。例如,如果你想添加一个名为“IMyInterface”的新接口,则应该创建一个名为“IMyInterface.hidl”的文件。 2. 在该文件定义你的接口方法,例如: ``` interface IMyInterface { void myMethod(int32_t arg1, int32_t arg2); }; ``` 这将定义一个名为“myMethod”的方法,它接受两个整数参数。 3. 在你的HIDL服务实现新接口方法。这通常涉及创建一个新的C++类来实现你的接口,并将其添加到你的服务。 4. 在你的服务的main函数注册新的接口。这可以通过调用registerAsService函数来完成,例如: ``` sp<IMyInterface> myInterface = new MyInterfaceImpl(); status_t status = myInterface->registerAsService(); if (status != OK) { ALOGE("Could not register MyInterface service (%d).", status); return -1; } ``` 这将创建一个名为“MyInterface”的服务,并将其注册到系统服务管理器。 5. 在你的客户端应用程序使用新接口。这通常涉及使用getService函数获取对接口的引用,并调用其方法。例如: ``` sp<IMyInterface> myInterface = IMyInterface::getService(); if (myInterface == nullptr) { ALOGE("Could not get MyInterface service."); return -1; } myInterface->myMethod(42, 24); ``` 这将获取名为“MyInterface”的服务的引用,并调用其“myMethod”方法,将42和24作为参数传递。 以上是添加HIDL接口的基本步骤,你可以根据你的具体需求进行修改。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值