奥特学园ROS笔记--1(25-74节)------发布、订阅、服务/客户端、

【奥特学园】ROS机器人入门课程《ROS理论与实践》零基础教程_哔哩哔哩_bilibili

下一篇:奥特学园ROS笔记--2(76-120节)_echo_gou的博客-CSDN博客

目录

25节 基本配置

28节(1.4.2):

30节 launch文件 1.4.3

----------话题通信---------

38节 发布者和订阅者的具体建立流程

40节 发布方实现

41节  发布方按频率发送数据

42节 订阅方实现

43节 没有添加依赖/前几条数据丢失/回调函数

44节 计算图

45、46、48节 python实现发布者

47节 python实现订阅者

50节 解耦合

51节 自定义msg

52节 自定义msg具体实现

53节 C++使用自定义msg(配置)

 54节 使用C++自定义msg 发布方

55节 使用c++自定义msg 订阅方

57节 使用python自定义msg (配置)

58节 使用python自定义msg 发布方

 59节 使用python自定义msg 订阅方

61节 总结

----------服务通信---------

63节 理论模型

64、65节 自定义srv(准备)

66节 自定义srv(服务端)

 67、68节 自定义srv(客户端)

69节 先启动客户端

70 节 python实现srv(准备)

71节 python实现srv(服务端)

72、73节 python实现srv(客户端)

74节 先启动客户端(python)


25节 基本配置

1 建立文件夹,文件夹下建立src

2 在src文件外面:catkin_make后生成build和devel

3 code .打开vscode,然后vscode中ctrl+shift+b,选择后面小齿轮,将以下代码对tasks.json进行覆盖。这里目的是配置编译相关的参数,之后ctrl+shift+b即可编译。

{
// 有关 tasks.json 格式的文档,请参见
    // https://go.microsoft.com/fwlink/?LinkId=733558
    "version": "2.0.0",
    "tasks": [
        {
            "label": "catkin_make:debug", //代表提示的描述性信息
            "type": "shell",  //可以选择shell或者process,如果是shell代码是在shell里面运行一个命令,如果是process代表作为一个进程来运行
            "command": "catkin_make",//这个是我们需要运行的命令
            "args": [],//如果需要在命令后面加一些后缀,可以写在这里,比如-DCATKIN_WHITELIST_PACKAGES=“pac1;pac2”
            "group": {"kind":"build","isDefault":true},
            "presentation": {
                "reveal": "always"//可选always或者silence,代表是否输出信息
            },
            "problemMatcher": "$msCompile"
        }
    ]
}

4 右键src创建ros功能包:,create catkin package ,分别输入名字和依赖(roscpp rospy std_mags)。

运行步骤

1 roscore

2 source ./devel/setup.bash

3 rosun ......

修改代码之后一定记得重新编译

注:在写代码的时候有红色波浪线则,如果确实没有错那就ctrl+shift+b编译一下。

python步骤:

1 编写代码,代码中第一二行应该加入

#! /usr/bin/env python
#coding=UTF-8

2 添加可执行权限:

1打开scripts所在的文件夹的终端
2输入:chmod +x *.py

3 cmakelist修改:

改成对应的文件名称

 4 ctrl+shift+b编译

5 然后运行

1 roscore
2 source ./devel/setup.bash
rosrun plumbing_pub_sub demo01_pub_p.py

28节(1.4.2):

1 ROSnoetic版本不配置CMakeLists的python的话,只能支持python2不能支持python3

2 出现问题:/usr/bin/env:没有那个文件或者目录

30节 launch文件 1.4.3

1 这个文件可以一次性启动多个节点

2 是一个XML文件

3 在功能包下面创建文件夹launch,然后创建.launch文件

<!-- 启动乌龟gui和键盘控制节点 -->
<launch>
    <!--添加被执行的节点-->
    <node pkg="turtlesim" type="turtlesim_node" name="turtle1" output="screen"/>
    <node pkg="turtlesim" type="turtle_teleop_key" name="key" output="screen"/>
</launch>

这三步就相当于之前启动的三个终端执行的语句,其中前后两个<launch>对应的是之前启动roscore的步骤

然后在命令行source以下后

roslaunch xxx文件夹 xxx.launch  即可

----------话题通信---------

38节 发布者和订阅者的具体建立流程

1发布者和订阅者分别在master中进行注册

2master根据注册表信息对两者进行匹配,匹配后master给订阅者发送发布者的rpc地址

3订阅者通过地址和发布者进行链接,然后开始通信

注:使用的协议有rpc和tcp协议;发布者和订阅者建立链接后,master就可以关闭了;上述实现流程已经封装了,了解即可;我们关注的是发布者和订阅者的实现和消息载体;master的作用可以理解为管理和匹配话题、建立两者之间的连接用的

40节 发布方实现

图中一个是发布者一个是订阅者,创建在src中。

    实现流程:
        1.包含头文件
        2.初始化 ROS 节点:命名(唯一)
        3.实例化 ROS 句柄
        4.实例化 发布者 对象
        5.组织被发布的数据,并编写逻辑发布数据

    //初始化节点
    ros::init(argc,argv,"ergouzi");
    
    //创建节点句柄
    ros::NodeHandle nh;
    
    //创建发布对象
    ros::Publisher pub = nh.advertise<std_msgs::String>("fang",10);  //10是队列长度,如果发出还没有被接受就会放到队列里面。
    
    //创建消息对象的变量,用于发布数据
    std_msgs::String msg; 
    
    //发布消息
    while(ros::ok())  //节点是否存活,节点死亡ctrl+c结束就死亡
    {
        msg.data="hello";
        pub.publish(msg);  //
    }

其中ros::ok()代表节点是否还存活;

41节  发布方按频率发送数据

上一节的补充

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>

//发布方实现
int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    //初始化节点
    ros::init(argc,argv,"ergouzi");
    
    //创建节点句柄
    ros::NodeHandle nh;
    
    //创建发布对象
    ros::Publisher pub = nh.advertise<std_msgs::String>("fang",10);
    
    //创建消息对象的变量

    std_msgs::String msg;
    //发布频率
    ros::Rate rate(10);

    int count=0;
    //发布消息
    while(ros::ok())
    {
        count++;
        //msg.data="hello";
        //实现字符串拼接数字
        std::stringstream ss;
        ss<<"hello --- -"<<count;   //将hello字符和count数字输入到ss
        msg.data=ss.str();
        pub.publish(msg);
        //添加日志
        ROS_INFO("发布的数据是%s",ss.str().c_str());  //打印到控制台上
        rate.sleep();  //睡眠0.1秒
    }

    return 0;
}

42节 订阅方实现

步骤和上面两节相似

 实现流程:
        1.包含头文件
        2.初始化 ROS 节点:命名(唯一)
        3.实例化 ROS 句柄
        4.实例化 订阅者 对象
        5.处理订阅的消息(回调函数)
        6.设置循环调用回调函数

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>

void doMsg(const std_msgs::String::ConstPtr &msg){  //通过msg这个参数获取并且操作订阅道到的数据
    ROS_INFO("cuihua订阅到的数据是:%s",msg->data.c_str());
}

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"cuihua");  //节点名称具有唯一性
    ros::NodeHandle nh; //句柄
    ros::Subscriber sub=nh.subscribe("fang",10,doMsg); //topic一定和ergouzi保持一致才可通信.发布者那边的泛型可以不添加,订阅者这边可以自动识别。
    ros::spin(); //因为上一行的回调函数doMsg需要被频繁的执行去获取数据,所以这一行作用就是处理回调函数;
    // while(ros::ok()){  这样效果也一样
    //     ros::spinOnce();
    // }
    return 0;
}

43节 没有添加依赖/前几条数据丢失/回调函数

上一节出现的问题的一个补充

1 在创建功能包的时候如果没有添加依赖,建议直接删除这个包重新创建。

2 订阅者订阅发布者的数据时,前面几条数据可能会丢失。

原因是publisher还没有在roscore中注册完毕。

解决:可以在注册后加入休眠步骤,来延迟第一条数据的发送。

 即ros::Duration(3).sleep();  这里休眠3秒。

3 回调函数:

每次订阅者订阅到外部的消息就执行一次回调函数,而不是代码上出现一次就调用一次

44节 查看计算图

终端输入

rqt_graph 

显示计算图,了解节点之间的关系

注:计算图的查看必须要在程序运行的时候才可以查看

45、46、48节 python实现发布者

45节:

        1导包

        2初始化ros节点

        3创建发布者对象

        4编写发布逻辑并且发送数据

46节:

        设置发布频率

48节:

        设置sleep,从而保证订阅者可以订阅到发布者发布的所有数据。

#! /usr/bin/env python
#coding=UTF-8

import rospy
from std_msgs.msg import String

if __name__=="__main__":
    rospy.init_node("sandai") #传入节点名称    #初始化节点
    pub=rospy.Publisher("che",String,queue_size=10)    #创建发布者对象
    msg=String()
    rate=rospy.Rate(1) #一秒一次
    count=0     #计数
    rospy.sleep(3)
    while not rospy.is_shutdown():
        count+=1
        msg.data="hello"+str(count)
        pub.publish(msg)  #向外发布数据msg
        rospy.loginfo("写出的数据:%s",msg.data)
        rate.sleep()

代码中第一行和第二行都必须加上

注:python在终端打印使用的是rospy.loginfo而不是rospy.INFO

47节 python实现订阅者

47节:

        导包

        初始化ros节点

        创建订阅者对象

        回调函数处理数据

        spin()

48节:

        休眠

#! /usr/bin/env python
#coding=UTF-8

import rospy
from std_msgs.msg import String

def doMsg(msg): #回调函数,msg只是一个参数,随便起什么名字都行
    rospy.loginfo("我订阅到的数据:%s",msg.data) 

if __name__=="__main__":
    rospy.init_node("huahua")  #初始化节点
    sub=rospy.Subscriber("che",String,doMsg,queue_size=10) #创建订阅者对象
    rospy.spin()  #回调

50节 解耦合

不同编程语言编写的节点之间也可以进行数据交换,只需要两者的话题一致即可,如下图中topic都是“fang”。

 

可以发现计算图输出,一边是c++编写的一边是python编写的。

51节 自定义msg

在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty.... 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,比如: 激光雷达的信息... std_msgs 由于描述性较差而显得力不从心,这种场景下可以使用自定义的消息类型

msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型有:

  • int8, int16, int32, int64 (或者无符号类型: uint*)

  • float32, float64

  • string

  • time, duration

  • other msg files   //其他msg

  • variable-length array[] and fixed-length array[C]  //变长和定长数组

 ROS中还有一种特殊类型:Header,标头包含时间戳和ROS中常用的坐标帧信息。会经常看到msg文件的第一行具有Header标头

52节 自定义msg具体实现

msg文件可以理解成一个结构体的作用,用于一次性传输多个不同类型的数据。

1 定义msg文件

创建文件夹msg和在下面创建msg文件,并且输入数据格式类型。

2 编辑配置文件

理解:package我理解为事先声明,cmakelists为实际编译执行过程,要先有package声明了之后cmakelists执行的时候才找得到相关文件。

package.xml中添加了56和65行,前者表示添加了编译的时候需要依赖的包message_generation,后者表示执行的时候需要的包message_runtime。

cmakelists.txt中:

添加14行,表示编译包的时候需要依赖于message_generation

修改51-54行,,108行,添加自己定义的msg文件

放开注释71-74行,表示如果我要编译自己定义的Person.msg需要依赖于std_msgs,因为自己建立的复合的消息实现Person.msg是由标准消息std_msgs组成的

 放开108行并加上message_runtime,表示运行的时候依赖

弹幕:使用roboware直接配置好你的文件?

3 编译

ctrl+shift+b

生成了Person.h和_Person.py

53节 C++使用自定义msg(配置)

1 vscode配置

不配置也是可以运行,但为了方便代码提示以及避免误抛异常,需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性:

首先找到路径,右键include选择在终端中打开。

 输入pwd之后复制路径

 将路径包含到.vscode中的.json文件中的includepath中,注意格式和逗号。

 54节 使用C++自定义msg 发布方

建立文件

 编写代码

        1包含头文件#include "plumbing_pub_sub/Person.h"

        2初始化节点 3创建句柄 4创建发布者对象 5编写发布逻辑

#include "ros/ros.h"
#include "plumbing_pub_sub/Person.h"

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ROS_INFO("这是消息发布方");
    ros::init(argc,argv,"banzhuren");  //班主任
    ros::NodeHandle nh;
    ros::Publisher pub=nh.advertise<plumbing_pub_sub::Person>("liaotian",10);
    plumbing_pub_sub::Person person;
    person.age=1;
    person.height=1.73;
    person.name="张三";
    ros::Rate rate(1);

    while(ros::ok()){
        ROS_INFO("发布的消息:%s,%d,%.2f",person.name.c_str(),person.age,person.height);  //注意这里要转化格式:person.name.c_str()
        pub.publish(person);
        person.age+=1;
        rate.sleep();
        ros::spinOnce();
    }

    return 0;
}

配置CMakeLists

 

 上面三图前两图和之前非自定义msg一样,第三图中的作用(也是和之前的普通发布订阅所不同的地方)是可以保证我们调用的时候的依赖关系:因为demo03_pub_person.cpp需要依赖于msg中的person.msg文件即demo03中#include "plumbing_pub_sub/Person.h",所以我们需要在编译的时候保证person.msg文件要在demo03_pub_person.cpp的编译之前进行编译,这一句就是起这个作用。

然后执行结果:

进入工作空间source一下再用rostopic打印出来

55节 使用c++自定义msg 订阅方

代码实现

#include "ros/ros.h"
#include "plumbing_pub_sub/Person.h"

void doperson(const plumbing_pub_sub::Person::ConstPtr& person){  //常引用,只能读不能通过引用来修改
    ROS_INFO("订阅的信息是:%s,%d,%.2f",person->name.c_str(),person->age,person->height);
}

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ROS_INFO("订阅方实现:");
    ros::init(argc,argv,"jiazhang");  //家长
    ros::NodeHandle nh;
    ros::Subscriber sub = nh.subscribe("liaotian",10,doperson);
    ros::spin();
    return 0;
}

配置文件demo04_sub_person

 

57节 使用python自定义msg (配置)

和53节一样要先配置vscode避免导包的时候抛出异常和出现导包的提示,具体是将前面生成的 python 文件路径配置进 settings.json(之前c++是配置c_cpp_properties.json)

右键dist-packages ,在终端中打开

 

利用pwd获取路径并且复制

 在.json文件中加入到第四和第八行中(和视频中不一样):

58节 使用python自定义msg 发布方

步骤

        1导包 2创建节点 3创建对象 4编写发布逻辑

代码

#! /usr/bin/env python
#coding=UTF-8

import rospy
from plumbing_pub_sub.msg import Person


if __name__=="__main__":
    rospy.init_node("dama")
    pub = rospy.Publisher("jiaosheyou",Person,queue_size=10)  #创建发布者对象
    p = Person()
    p.age=8
    p.name="奥特曼"
    p.height=1.85
    rate = rospy.Rate(1)

    while not rospy.is_shutdown():
        pub.publish(p)
        rospy.loginfo("发布的消息:%s,%d,%.2f",p.name,p.age,p.height)
        rate.sleep()

权限

 cmake编写

运行

 59节 使用python自定义msg 订阅方

#! /usr/bin/env python
#coding-UTF_8

import rospy
from plumbing_pub_sub.msg import Person

def doperson(p):
    rospy.loginfo("接受的数据是:%s,%d,%.2f",p.name,p.age,p.height)

if __name__=="__main__":
    rospy.init_node("daye")
    sub =rospy.Subscriber("jiaoshetou",Person,doperson,queue_size=10)
    rospy.spin()

然后chmod +x *.py

然后cmake(注:开始的时候忘了这一步也可以执行和正常运行,为什么?)

然后执行

61节 总结

----------服务通信---------

服务通信应用于对实时性有一定要求的场景,双向通信。

63节 理论模型

理论模型和之前话题通信类似,只不过服务端必须在客户端之前启动,服务端和客户端都可以多个。最后建立链接后,是客户端先发出请求,然后得到服务端的响应。客户端请求一次,服务端响应一次。

master就相当于服务平台

服务端类比保洁公司

客户端类比客户

64、65节 自定义srv(准备)

接下来实现两个数的求和并且返回

1 创建新的功能包plumbing_server_client(依赖roscpp rospy std_msgs)

2  创建一个文件夹,并且在文件夹下创建AddInts.srv文件,在文件中输入自定义的消息格式。其中请求和响应的格式中间用“---”隔开

3编辑package.xml和CMakeLists文件

package中和之前一样添加55、62行

 CMakeLists中分别添加14行、修改60行、71-74放开注释、108加上message_runtime

 

 4 配置vscode,方便导包(添加11行,因为之前写过话题通行,所以已经添加好了)

66节 自定义srv(服务端)

1代码编写

#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"

bool doNums(plumbing_server_client::AddInts::Request &request,
            plumbing_server_client::AddInts::Response &response){
    int num1= request.num1;
    int num2=request.num2;
    ROS_INFO("收到的数据为%d,%d \n求和为:%d",num1,num2,num1+num2);
    response.sum=num1+num2;
    return true;
}

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"heishui");  //“保洁公司”
    ros::NodeHandle nh; 
    ros::ServiceServer server =nh.advertiseService("addints",doNums); //注意这里是ServiceServer和advertiseService
    ROS_INFO("服务器端启动");
    ros::spin();
    return 0;
}

2 配置CMakeLists(136、146、149-151、):

 其中${PROJECT_NAME}是调用变量,因为开头有写着他的name

 3运行

先编译

然后运行

然后再打开一个终端使用 rosservice call addints 模拟客户端发送请求这个步骤 来进行测试:将其中num1和num2

 然后服务器端的窗口:

 67、68节 自定义srv(客户端)

服务端有spin回调函数而客户端没有

67节:

代码

#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"dabao");
    ros::NodeHandle nh;
    ros::ServiceClient client= nh.serviceClient<plumbing_server_client::AddInts>("addints");
    //提交请求
    plumbing_server_client::AddInts ai;  //创建对象,利用这个对象来进行数据通信
    ai.request.num1=100;
    ai.request.num2=292;
    //处理响应
    bool flag = client.call(ai); //通过client.call来对server提交申请,如果成功响应则返回true
    if(flag){
        ROS_INFO("响应成功");
        ROS_INFO("结果为%d",ai.response.sum);
    }
    else{
        ROS_INFO("处理失败");
    }
    return 0;
}

配置CMakeLists(三部分添加)

68节:

修改67节代码实现动态提交

代码:

#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"

int main(int argc, char *argv[])
{
    //优化实现,获取命令中参数
    setlocale(LC_ALL,"");
    if(argc!=3){
        ROS_INFO("提交的参数个数不对");
        return 1;
    }
    ros::init(argc,argv,"dabao");
    ros::NodeHandle nh;
    ros::ServiceClient client= nh.serviceClient<plumbing_server_client::AddInts>("addints");
    //提交请求
    plumbing_server_client::AddInts ai;  //创建对象,利用这个对象来进行数据通信
    ai.request.num1=atoi(argv[1]);
    ai.request.num2=atoi(argv[2]);
    //处理响应
    bool flag = client.call(ai); //通过client.call来对server提交申请,如果成功响应则返回true
    if(flag){
        ROS_INFO("响应成功");
        ROS_INFO("结果为%d",ai.response.sum);
    }
    else{
        ROS_INFO("处理失败");
    }
    return 0;
}

然后分别运行服务端和客户端,其中客户端的运行和之前有区别:        

rosrun plumbing_server_client demo02_client 12 34

其中要在最后加上想要求和的两个数,不然argc传入的参数个数就是1

argc表示的是传入的参数的个数,本例中为3,一个是文件名,另外两个是传入的参数。代码argv[1]和argv[2]表示传入的两个数的值,argv[0]表示函数的名称。我们将argv[0]进行输出得到:/home/gzy/ROS/autolabor/demo3_ws/devel/lib/plumbing_server_client/demo02_client

69节 先启动客户端

上个例子中我们如果先启动客户端会报错处理失败

方法一: 我们在客户端发送请求的代码之前添加一个用于判断服务器状态的函数即可(24行),服务器如果没有启动则不继续往下执行,

方法二:在同样的位置添加以下代码(和方法一不同的是需要传参):

    ros::service::waitForService("addints");

70 节 python实现srv(准备)

之前在使用自定义msg的时候已经配置过了.json文件 ,这里也是同样的配置

71节 python实现srv(服务端)

代码:其中注意导包

#! /usr/bin/env python
#coding=UTF-8

import rospy
from plumbing_server_client.srv import AddInts,AddIntsRequest,AddIntsResponse
#from plumbing_server_client.srv import *  #这两种方式都可

#回调函数的参数(request)封装了请求数据,返回为响应数据
def doNum(request):
    num1=request.num1
    num2=request.num2
    sum=num1+num2
    response=AddIntsResponse()
    response.sum=sum
    rospy.loginfo("解析的数据num1=%d,num2=%d,结果为%d",num1,num2,sum)
    return response

if __name__=="__main__":
    rospy.init_node("heishui")
    server = rospy.Service("addints",AddInts,doNum)
    rospy.loginfo("服务器已经启动")
    rospy.spin()

添加执行权限:

配置CMakeLists:

 测试运行

 

72、73节 python实现srv(客户端)

代码:

#! /usr/bin/env python
#coding=UTF-8

import rospy
from plumbing_server_client.srv import AddInts

if __name__=="__main__":
    rospy.init_node("erhei")
    client = rospy.ServiceProxy("addints",AddInts)  //注意这里的函数名称为ServiceProxy
    response = client.call(12,34)
    rospy.loginfo("响应的数据为:%d",response.sum)

权限:

执行

 73 动态传入两个参数,这部分对应68节的内容

代码,需要import sys

#! /usr/bin/env python
#coding=UTF-8

import rospy
from plumbing_server_client.srv import AddInts
import sys

if __name__=="__main__":

    if len(sys.argv)!=3:
        rospy.loginfo("传入参数个数不对")
        sys.exit(1)

    rospy.init_node("erhei")
    client = rospy.ServiceProxy("addints",AddInts)
    num1=int(sys.argv[1])
    num2=int(sys.argv[2])
    response=client.call(num1,num2)
    rospy.loginfo("响应的数据为:%d",response.sum)

执行

74节 先启动客户端(python)

对应69节

#! /usr/bin/env python
#coding=UTF-8

import rospy
from plumbing_server_client.srv import AddInts
import sys

if __name__=="__main__":

    if len(sys.argv)!=3:
        rospy.loginfo("传入参数个数不对")
        sys.exit(1)

    rospy.init_node("erhei")
    client = rospy.ServiceProxy("addints",AddInts)
    num1=int(sys.argv[1])
    num2=int(sys.argv[2])
    #用于判断服务器状态
    #client.wait_for_service()
    rospy.wait_for_service("addints")
    response=client.call(num1,num2)
    rospy.loginfo("响应的数据为:%d",response.sum)

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值