ROS编程实现
1. 创造工作空间和功能包
工作空间(workspace)是一个存放工程开发相关文件的文件夹
src:代码空间(Source Space),用来放置功能包的代码、配置文件、launch文件
build:编译空间(Build Space),编译过程中产生的中间文件,二进制文件,不用考虑
devel:开发空间(Development Space)放置开发中编译生成的可执行文件,一些库、脚本
install:安装空间(Install Space),安装指令/程序的位置,放置开发后生成的可执行文件,和devel文件夹类似
1.1 创建工作空间
创建工作空间
1 2 3
| mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src catkin_init_workspace
|
编译工作空间
1 2
| cd ~/catkin_ws/ catkin_make
|
设置环境变量
检查环境变量
如果想产生install文件夹,可以通过catkin_make install
编译后即可产生install安装空间,存放最终编译的可执行文件
1.2 创建功能包
创建功能包
1 2
| cd ~/catkin_ws/src catkin_create_pkg test_pkg std_msgs rospy roscpp catkin_create_pkg <package_name> [depend 1][depend 2][depend 3]
|
test_pkg编译过程中用到哪些库,就需要跟哪些依赖,比如std_msgs、rospy、roscpp,这些库都放到src文件夹中。
编译功能包
1 2 3
| cd ~/catkin_ws catkin_make source ~/catkin_ws/devel/setup.bash
|
同一个工作空间下,不允许存在同名功能包,不同工作空间下,允许存在同名功能包
编译过后,C++头文件可以放到include文件夹中,功能包中一定有CMakeLists.txt和package.xml,这两个文件说明该文件夹为功能包。
package.xml的作用:记录该功能包相关的信息、个人信息、开源许可证、功能包依赖信息等,如果需要增加相应的依赖,可以再进行拓展。
CMakeLists.txt的作用:描述功能包的编译规则,主要的语法是Cmake语法。
2. 发布者Publisher的编程实现
发布者编程实现原理图:
2.1 创建功能包
具体操作步骤:
1 2
| cd ~catkin_ws/src catkin_create_pkg learning_topic rospy roscpp std_msgs geometry_msgs turtlesim
|
2.2 配置发布者代码编译规则
如何配置CMakeLists.txt中的编译规则:
- 设置需要编译的代码和生成的可执行文件;
- 设置链接库;
- CMakeLists.txt中主要描述了如何编译以及编译的规则。
在~/catkin_ws/src/learning_topic
文件夹下的CMakeLists.txt
文件中加入如下代码:
1 2
| add_executable(velocity_publisher src/velocity_publisher.cpp) target_link_libraries(velocity_publisher ${catkin_LIBRARIES})
|
2.3 用C++/python实现发布者
在src文件夹中创建cpp文件
C++代码实现一个发布者:
- 初始化ROS节点;
- 向ROS Master注册节点信息,包括发布的话题名和话题中的消息类型;
- 创建消息数据;
- 按照一定频率循环发布消息。
具体实现的C++代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
#include <ros/ros.h> #include <geometry_msgs/Twist.h>
int main(int argc, char **argv) { ros::init(argc, argv, "velocity_publisher"); ros::NodeHandle n; ros==Publisher turtle_vel_pub = n.advertise<geometry_msgs==Twist>("/turtle1/cmd_vel",10); ros::Rate loop_rate(10); int count = 0; while (ros::ok()) { geometry_msgs::Twist vel_msg; vel_msg.linear.x = 0.5; vel_msg.angular.z = 0.2; turtle_vel_pub.publish(vel_msg); ROS_INFO("Publish turtlesim velocity command[%0.2f m/s, %0.2f rad/s]",vel_msg.linear.x, vel_msg.angular.z); loop_rate.sleep(); } return 0; }
|
python代码实现一个发布者:
- 初始化ROS节点;
- 向ROS Master注册节点信息,包括发布的话题名和话题中的消息类型;
- 创建消息数据;
- 按照一定频率循环发布消息。
具体实现的python代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
import rospy from geometry_msgs.msg import Twist
def velocity_publisher(): rospy.init_node('velocity_publisher', anonymous=True)
turtle_vel_pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size=10)
rate = rospy.Rate(10)
while not rospy.is_shutdown(): vel_msg = Twist() vel_msg.linear.x = 0.5 vel_msg.angular.z = 0.2
turtle_vel_pub.publish(vel_msg) rospy.loginfo("Publsh turtle velocity command[%0.2f m/s, %0.2f rad/s]", vel_msg.linear.x, vel_msg.angular.z)
rate.sleep()
if __name__ == '__main__': try: velocity_publisher() except rospy.ROSInterruptException: pass
|
2.4 编译并运行发布者
编译具体操作如下:
1 2 3 4 5 6
| cd ~/catkin_ws catkin_make source devel/setup.bash roscore rosrun turtlesim turtlesim_node rosrun learning_topic velocity_publisher
|
python文件放在scripts文件夹中,同时注意要设置python文件具有可执行文件。
3. 订阅者Subscriber的编程实现
订阅者编程实现原理图:
3.1 配置发布者代码编译规则
如何配置CMakeLists.txt中的编译规则:
- 设置需要编译的代码和生成的可执行文件;
- 设置链接库
在~/catkin_ws/src/learning_topic
文件夹下的CMakeLists.txt
文件中加入如下代码:
1 2
| add_executable(pose_subscriber src/pose_subscriber.cpp) target_link_libraries(pose_subscriber ${catkin_LIBRARIES})
|
3.2 用C++/python实现订阅者
C++代码实现一个订阅者:
- 初始化ROS节点;
- 订阅需要的话题;
- 循环等待话题消息,接收到消息后进入回调函数;
- 在回调函数中完成消息处理。
具体实现的C++代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
#include <ros/ros.h> #include "turtlesim/Pose.h"
void poseCallback(const turtlesim==Pose==ConstPtr& msg) { ROS_INFO("Turtle pose: x:%0.6f, y:%0.6f", msg->x, msg->y); }
int main(int argc, char **argv) { ros::init(argc, argv, "pose_subscriber");
ros::NodeHandle n;
ros::Subscriber pose_sub = n.subscribe("/turtle1/pose", 10, poseCallback);
ros::spin();
return 0; }
|
python代码实现一个订阅者:
- 初始化ROS节点;
- 订阅需要的话题;
- 循环等待话题消息,接收到消息后进入回调函数;
- 在回调函数中完成消息处理。
具体实现的python代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
import rospy from turtlesim.msg import Pose
def poseCallback(msg): rospy.loginfo("Turtle pose: x:%0.6f, y:%0.6f", msg.x, msg.y)
def pose_subscriber(): rospy.init_node('pose_subscriber', anonymous=True)
rospy.Subscriber("/turtle1/pose", Pose, poseCallback)
rospy.spin()
if __name__ == '__main__': pose_subscriber()
|
python可以不用编译,直接可以运行。
3.3 编译并运行发布者
编译具体操作如下:
1 2 3 4 5 6 7
| cd ~/catkin_ws catkin_make source devel/setup.bash roscore rosrun turtlesim turtlesim_node rosrun learning_topic velocity_subscriber rosrun turtlesim turtle_teleop_key
|
4. 话题消息的定义与使用
4.1 自定义消息文件的配置
步骤一:定义msg文件
在learning_topic
文件夹中创建msg
文件夹,跟消息相关定义都存放在msg
文件夹中,方便管理,创建文件person.msg
,打开文件,将下列文件放入person.msg
文件中。
1 2 3 4 5 6 7
| string name uint8 sex uint8 age
uint8 unknown = 0 uint8 male = 1 uint8 female = 2
|
步骤二:在package.xml中添加功能包依赖
在learning_topic
文件夹中打开package.xml
文件,添加下面的语句
1 2
| <build_depend>message_generation<build_depend> <exec_depend>message_runtime</exec_depend>
|
一个是编译依赖,编译依赖是一个动态产生message的功能包;一个是执行依赖,执行依赖是一个执行message的功能包。如下图:
步骤三:在CMakeLists.txt添加编译选项
- find_package(…… message_generation)
- add_message_files(FILES Person.msg) 将定义的Person.msg作为接口,针对其来进行编译
generate_messages(DEPENDENCIES std_msgs) 编译Person.msg需要ROS已有的库或者包,需要依赖std_msgs
- catkin_package(…… message_runtime) 创建message运行依赖
分别对应CMakeLists.txt文件中的内容为:
步骤四:编译生成语言相关文件
在工作空间的根目录catkin_ws
下通过catkin_make
进行编译,生成Person.h头文件。
4.2 话题消息的使用
如何根据上面4.1生成的Person.h
头文件,通过程序来使用它,具体操作步骤如下:
在src文件夹中创建cpp文件
4.2.1 C++代码实现发布者
- 初始化ROS节点;
- 向ROS Master注册节点信息,包括发布的话题名和话题中的消息类型;
- 创建消息数据;
- 按照一定频率循环发布消息。
具体实现的C++代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
#include <ros/ros.h> #include "learning_topic/Person.h"
int main(int argc, char **argv) { ros::init(argc, argv, "person_publisher");
ros::NodeHandle n;
ros==Publisher person_info_pub = n.advertise<learning_topic==Person>("/person_info", 10);
ros::Rate loop_rate(1);
int count = 0; while (ros::ok()) { learning_topic::Person person_msg; person_msg.name = "Tom"; person_msg.age = 18; person_msg.sex = learning_topic==Person==male;
person_info_pub.publish(person_msg);
ROS_INFO("Publish Person Info: name:%s age:%d sex:%d", person_msg.name.c_str(), person_msg.age, person_msg.sex);
loop_rate.sleep(); }
return 0; }
|
4.2.2 C++代码实现订阅者
- 初始化ROS节点;
- 订阅需要的话题;
- 循环等待话题消息,接收到消息后进入回调函数;
- 在回调函数中完成消息处理。
具体实现的C++代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
#include <ros/ros.h> #include "learning_topic/Person.h"
void personInfoCallback(const learning_topic==Person==ConstPtr& msg) { ROS_INFO("Subcribe Person Info: name:%s age:%d sex:%d", msg->name.c_str(), msg->age, msg->sex); }
int main(int argc, char **argv) { ros::init(argc, argv, "person_subscriber");
ros::NodeHandle n;
ros::Subscriber person_info_sub = n.subscribe("/person_info", 10, personInfoCallback);
ros::spin();
return 0; }
|
4.2.3 编译配置文件
添加订阅者和发布者相关程序后,需要对CMakeList_List.txt文件进行配置
配置CMakeLists.txt中
的编译规则:
- 设置需要编译的代码和生成的可执行文件;
- 设置链接库;
- 添加依赖项。
在catkin_ws/src/learning_topic
文件夹下的CMakeList_List.txt
文件中加入以下代码:
1 2 3 4 5 6 7
| add_executable(person_publisher src/person_publisher.cpp) target_link_libraries(person_publisher ${catkin_LIBRARIES}) add_dependencies(person_publisher ${PROJECT_NAME}_generate_messages_cpp)
add_executable(person_subscriber src/person_subscriber.cpp) target_link_libraries(person_subscriber ${catkin_LIBRARIES}) add_dependencies(person_subscriber ${PROJECT_NAME}_generate_messages_cpp)
|
有一些代码是动态生成的,需要将可执行文件和动态生成的程序产生依赖关系,因此需要通过add_dependencies
命令来做一些连接。要和自己自定义的消息进行连接必须要添加这样一行代码。如图所示:
在工作空间的根目录catkin_ws
下,通过catkin_make
进行编译。
ROS Master是帮助节点之间创建连接,一旦两者创建连接后,关闭roscore
,publisher和subscriber依然发布消息和订阅消息,此时ROS Master不再起作用。
4.2.4 python代码实现
发布者python实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
import rospy from learning_topic.msg import Person
def velocity_publisher(): rospy.init_node('person_publisher', anonymous=True)
person_info_pub = rospy.Publisher('/person_info', Person, queue_size=10)
rate = rospy.Rate(10)
while not rospy.is_shutdown(): person_msg = Person() person_msg.name = "Tom"; person_msg.age = 18; person_msg.sex = Person.male;
person_info_pub.publish(person_msg) rospy.loginfo("Publsh person message[%s, %d, %d]", person_msg.name, person_msg.age, person_msg.sex)
rate.sleep()
if __name__ == '__main__': try: velocity_publisher() except rospy.ROSInterruptException: pass
|
订阅者python实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
import rospy from learning_topic.msg import Person
def personInfoCallback(msg): rospy.loginfo("Subcribe Person Info: name:%s age:%d sex:%d", msg.name, msg.age, msg.sex)
def person_subscriber(): rospy.init_node('person_subscriber', anonymous=True)
rospy.Subscriber("/person_info", Person, personInfoCallback)
rospy.spin()
if __name__ == '__main__': person_subscriber()
|
5. 客户端Client的编程实现
客户端Client编程实现原理图:
5.1创建功能包
代码实现如下:
1 2
| cd ~/catkin_ws/src catkin_create_pkg learning_service roscpp rospy std_msgs geometry_msgs turtlesim
|
5.2 C++/python代码实现客户端
5.2.1 C++代码实现客户端
在learning_service文件夹下的src文件中,新建turtle_spawn.cpp文件,实现流程如下:
- 初始化ROS节点;
- 创建一个Client实例;
- 发布服务请求数据;
- 等待Server处理之后的应答结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
#include <ros/ros.h> #include <turtlesim/Spawn.h>
int main(int argc, char** argv) { ros::init(argc, argv, "turtle_spawn");
ros::NodeHandle node;
ros==service==waitForService("/spawn"); ros==ServiceClient add_turtle = node.serviceClient<turtlesim==Spawn>("/spawn"); turtlesim::Spawn srv; srv.request.x = 2.0; srv.request.y = 2.0; srv.request.name = "turtle2";
ROS_INFO("Call service to spwan turtle[x:%0.6f, y:%0.6f, name:%s]", srv.request.x, srv.request.y, srv.request.name.c_str());
add_turtle.call(srv);
ROS_INFO("Spwan turtle successfully [name:%s]", srv.response.name.c_str());
return 0; };
|
5.2.2 python代码实现客户端
在learning_service文件夹下的src文件中,新建turtle_spawn.cpp文件,实现流程如下:
- 初始化ROS节点;
- 创建一个Client实例;
- 发布服务请求数据;
- 等待Server处理之后的应答结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
import sys import rospy from turtlesim.srv import Spawn
def turtle_spawn(): rospy.init_node('turtle_spawn')
rospy.wait_for_service('/spawn') try: add_turtle = rospy.ServiceProxy('/spawn', Spawn)
response = add_turtle(2.0, 2.0, 0.0, "turtle2") return response.name except rospy.ServiceException, e: print "Service call failed: %s"%e
if __name__ == "__main__": print "Spwan turtle successfully [name:%s]" %(turtle_spawn())
|
5.3 配置客户端代码编译规则
如何配置CMakeLists.txt
中的编译规则
1 2
| add_executable(turtle_spawn src/turtle_spawn.cpp) //将turtle_spawn.cpp文件编译成turtle_spawn的可执行文件 target_link_libraries(turtle_spawn ${catkin_LIBRARIES}) //将turtle_spawn链接到ROS对应的库
|
5.4 编译并运行客户端
回到工作空间的根目录catkin_ws
文件夹下,通过catkin_make
进行编译。
在catkin_ws/devel/lib
文件夹下产生的learning_service
文件夹就是编译后产生的,里面的turtle_spawn
文件就是编译产生的可执行文件。
1 2 3 4 5 6
| cd ~/catkin_ws catkin_make source devel/setup.bash roscore rosrun turtlesim turtlesim_node rosrun learning_service turtle_spawn
|
注意:要将python文件的权限选择允许作为程序执行文件,如下所示。
6. 服务端Server的编程实现
服务端Server编程实现原理图:
6.1 用C++/python实现一个服务器
6.1.1 用C++实现一个服务器
在learning_service文件夹下的src文件中,新建turtle_command_server.cpp文件,实现流程如下:
- 初始化ROS节点;
- 创建一个Server实例;
- 循环等待服务请求,进入回调函数;
- 在回调函数中完成服务功能的处理,并反馈应答数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
#include <ros/ros.h> #include <geometry_msgs/Twist.h> #include <std_srvs/Trigger.h>
ros::Publisher turtle_vel_pub; bool pubCommand = false;
bool commandCallback(std_srvs==Trigger==Request &req, std_srvs==Trigger==Response &res) { pubCommand = !pubCommand;
ROS_INFO("Publish turtle velocity command [%s]", pubCommand==true?"Yes":"No");
res.success = true; res.message = "Change turtle command state!"
return true; }
int main(int argc, char **argv) { ros::init(argc, argv, "turtle_command_server");
ros::NodeHandle n;
ros::ServiceServer command_service = n.advertiseService("/turtle_command", commandCallback);
turtle_vel_pub = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 10);
ROS_INFO("Ready to receive turtle command.");
ros::Rate loop_rate(10);
while(ros::ok()) { ros::spinOnce(); if(pubCommand) { geometry_msgs::Twist vel_msg; vel_msg.linear.x = 0.5; vel_msg.angular.z = 0.2; turtle_vel_pub.publish(vel_msg); }
loop_rate.sleep(); }
return 0; }
|
6.1.2 用python实现一个服务器
在python程序中没有spinOnce这条指令,通过python中的多线程指令来完成标志位是True还是False。
在learning_service文件夹下的scripts文件中,新建turtle_command_server.py文件,实现流程如下:
- 初始化ROS节点;
- 创建一个Server实例;
- 循环等待服务请求,进入回调函数;
- 在回调函数中完成服务功能的处理,并反馈应答数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
import rospy import thread,time from geometry_msgs.msg import Twist from std_srvs.srv import Trigger, TriggerResponse
pubCommand = False; turtle_vel_pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size=10)
def command_thread(): while True: if pubCommand: vel_msg = Twist() vel_msg.linear.x = 0.5 vel_msg.angular.z = 0.2 turtle_vel_pub.publish(vel_msg) time.sleep(0.1)
def commandCallback(req): global pubCommand pubCommand = bool(1-pubCommand)
rospy.loginfo("Publish turtle velocity command![%d]", pubCommand)
return TriggerResponse(1, "Change turtle command state!")
def turtle_command_server(): rospy.init_node('turtle_command_server')
s = rospy.Service('/turtle_command', Trigger, commandCallback)
print "Ready to receive turtle command."
thread.start_new_thread(command_thread, ()) rospy.spin()
if __name__ == "__main__": turtle_command_server()
|
6.2 配置服务器代码编译规则
如何配置CMakeList.txt
中的编译规则:
- 设置需要编译的代码和生成的可执行文件;
- 设置链接库。
将下面的规则加入CMakeList.txt中:
1 2
| add_executable(turtle_command_server src/turtle_command_server.cpp) target_link_libraries(turtle_command_server ${catkin_LIBRARIES})
|
6.3 编译并运行服务器
6.3.1 编译运行C++程序
编译运行流程:
1 2 3 4 5 6 7
| cd ~/catkin_ws catkin_make source devel/setup.bash roscore rosrun turtlesim turtlesim_node rosrun learning_service turtle_command_server rosservice call/turtle_command "{}"
|
输入后的效果:
再一次最后一行命令的效果:
6.3.2 编译运行python程序
编译运行流程:
1 2 3 4 5 6 7
| cd ~/catkin_ws catkin_make source devel/setup.bash roscore rosrun turtlesim turtlesim_node rosrun learning_service turtle_command_server.py rosservice call/turtle_command "{}"
|
7. 服务数据的定义和使用
服务模型的结构图:
7.1 如何自定义服务数据
步骤一:定义srv文件
打开catkin_ws/src/learning_service/srv
文件夹,创建Person.srv
文件,打开,并输入以下代码:
1 2 3 4 5 6 7 8 9
| string name uint8 age uint8 sex
uint8 unknown = 0 uint8 male = 1 uint8 female = 2 --- string result
|
在---
上面是request
,在---
下面是response
。
步骤二:在package.xml中添加功能包依赖
打开catkin_ws/src/learning_service
文件夹中打开package.xml
文件,将下面的代码加入其中:
1 2
| <build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend>
|
步骤三:在CMakeLists.txt添加编译选项
打开catkin_ws/src/learning_service
文件夹中打开CMakeLists.txt文件中做如下修改
1 2 3 4 5 6
| find_package(... message_generation)
add_service_files(FILES Person.srv) generate_messages(DEPENDENCIES std_msgs)
catkin_package(... message_runtime)
|
具体实现的效果如图:
步骤四:编译生成语言相关文件
回到工作空间根目录catkin_ws
下,通过catkin_make
进行编译。
编译后在Person.h
文件中包含了PersonRequest.h
和PersonResponse.h
文件,用的时候包含一个Person.h
就可以
7.2 相关代码实现
7.2.1 C++相关代码实现
Client端的C++代码编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
#include <ros/ros.h> #include "learning_service/Person.h"
int main(int argc, char** argv) { ros::init(argc, argv, "person_client");
ros::NodeHandle node;
ros==service==waitForService("/show_person"); ros==ServiceClient person_client = node.serviceClient<learning_service==Person>("/show_person");
learning_service::Person srv; srv.request.name = "Tom"; srv.request.age = 20; srv.request.sex = learning_service==Person==Request::male;
ROS_INFO("Call service to show person[name:%s, age:%d, sex:%d]", srv.request.name.c_str(), srv.request.age, srv.request.sex);
person_client.call(srv);
ROS_INFO("Show person result : %s", srv.response.result.c_str());
return 0; };
|
代码22行中的名字和srv的文件名一致,如下图:
Server端的C++代码编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
#include <ros/ros.h> #include "learning_service/Person.h"
bool personCallback(learning_service==Person==Request &req, learning_service==Person==Response &res) { ROS_INFO("Person: name:%s age:%d sex:%d", req.name.c_str(), req.age, req.sex);
res.result = "OK";
return true; }
int main(int argc, char **argv) { ros::init(argc, argv, "person_server");
ros::NodeHandle n;
ros::ServiceServer person_service = n.advertiseService("/show_person", personCallback);
ROS_INFO("Ready to show person informtion."); ros::spin();
return 0; }
|
7.2.2 python相关代码实现
客户端相关的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
import sys import rospy from learning_service.srv import Person, PersonRequest
def person_client(): rospy.init_node('person_client')
rospy.wait_for_service('/show_person') try: person_client = rospy.ServiceProxy('/show_person', Person)
response = person_client("Tom", 20, PersonRequest.male) return response.result except rospy.ServiceException, e: print "Service call failed: %s"%e
if __name__ == "__main__": print "Show person result : %s" %(person_client())
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
import rospy from learning_service.srv import Person, PersonResponse
def personCallback(req): rospy.loginfo("Person: name:%s age:%d sex:%d", req.name, req.age, req.sex)
return PersonResponse("OK")
def person_server(): rospy.init_node('person_server')
s = rospy.Service('/show_person', Person, personCallback)
print "Ready to show person informtion." rospy.spin()
if __name__ == "__main__": person_server()
|
7.3 配置服务器/客户端代码编译规则
配置CMakeLists.txt
中的编译规则:
- 设置需要编译的代码和生成的可执行文件;
- 设置链接库;
- 添加依赖项,依赖动态生成的cpp文件。
1 2 3 4 5 6 7
| add_executable(person_server src/person_server.cpp) target_link_libraries(person_server ${catkin_LIBRARIES}) add_dependencies(person_server ${PROJECT_NAME}_gencpp)
add_executable(person_client src/person_client.cpp) target_link_libraries(person_client ${catkin_LIBRARIES}) add_dependencies(person_client ${PROJECT_NAME}_gencpp)
|
7.4 编译并运行发布者和订阅者
编译运行流程:
1 2 3 4 5 6
| cd ~/catkin_ws catkin_make source devel/setup.bash roscore rosrun learning_service person_server rosrun learning_service person_client
|
客户端每请求一次服务端显示一次。
8. 参数的使用与编程方法
参数模型结构图:
ROS Master中有一个参数服务器Parameter Server,是一个全局字典,用来保存各个节点之间的配置参数,各个节点都可以访问。
8.1 参数模型的使用方法
参数模型实现:
首先创建功能包:
1 2
| cd ~/catkin_ws/src catkin_create_pkg learning_parameter roscpp rospy std_ws
|
rosparam命令行的使用方式:
rosparam list
列出当前有多少参数
rosparam get param_key
显示某个参数值
rosparam set param_key param_value
设置某个参数值
rosparam dump file_name
保存参数到文件
rosparam load file_name
从文件读取参数
rosparam delete param_key
删除参数
rosparam
通过rosparam可以看到对该句命令的解释
以小海龟案例为例:
1 2 3
| roscore rosrun turtlesim turtlesim_node rosparam list
|
可以显示如下:
background_b、background_g、background_r是小海龟背景的颜色值
rosdistro是当前的ros是Melodic
rosversion是小海龟当前的版本
run_id为进程的id号
如果想得到某个参数的值,可以通过rosparam get /background_b
获得:
1
| rosparam get /background_b
|
如果想修改某个参数的值,可以通过rosparam set /background_b
修改:
1
| rosparam set /background_b 100
|
需要发送一个请求才能把小海龟背景颜色给改变:
1
| rosservice call /clear "{}"
|
这个请求的服务名字为clear,发送空即可,发送请求后仿真器会查询rgb值,如果有更改,会根据新的值刷新背景颜色。
保存ros系统的参数
1
| rosparam dump yww_param.yaml
|
回车之后会自动保存为yww_param.yaml
文件,没有任何提示。
也可以更改yww_param.yaml
中的数据,然后用以下命令进行加载:
1
| rosparam load yww_param.yaml
|
修改好之后通过获取rosparam get /background_b
之后的值。
8.2 用C++/python实现参数的使用
8.2.1 用C++实现参数的使用
通过以下代码实现:
1 2
| cd ~/catkin_ws/src/learning_parameter/src touch parameter_config.cpp
|
其中parameter_config.cpp
代码的结构为:
- 初始化ROS节点;
- get函数获取参数;
- set函数设置参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
#include <string> #include <ros/ros.h> #include <std_srvs/Empty.h>
int main(int argc, char **argv) { int red, green, blue;
ros::init(argc, argv, "parameter_config");
ros::NodeHandle node;
ros==param==get("/background_r", red); ros==param==get("/background_g", green); ros==param==get("/background_b", blue);
ROS_INFO("Get Backgroud Color[%d, %d, %d]", red, green, blue);
ros==param==set("/background_r", 255); ros==param==set("/background_g", 255); ros==param==set("/background_b", 255);
ROS_INFO("Set Backgroud Color[255, 255, 255]");
ros==param==get("/background_r", red); ros==param==get("/background_g", green); ros==param==get("/background_b", blue);
ROS_INFO("Re-get Backgroud Color[%d, %d, %d]", red, green, blue);
ros==service==waitForService("/clear"); ros==ServiceClient clear_background = node.serviceClient<std_srvs==Empty>("/clear"); std_srvs::Empty srv; clear_background.call(srv); sleep(1);
return 0; }
|
8.2.2 用python实现参数的使用
其中parameter_config.py
代码的结构为:
- 初始化ROS节点;
- get函数获取参数;
- set函数设置参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
import sys import rospy from std_srvs.srv import Empty
def parameter_config(): rospy.init_node('parameter_config', anonymous=True)
red = rospy.get_param('/background_r') green = rospy.get_param('/background_g') blue = rospy.get_param('/background_b')
rospy.loginfo("Get Backgroud Color[%d, %d, %d]", red, green, blue)
rospy.set_param("/background_r", 255); rospy.set_param("/background_g", 255); rospy.set_param("/background_b", 255);
rospy.loginfo("Set Backgroud Color[255, 255, 255]");
red = rospy.get_param('/background_r') green = rospy.get_param('/background_g') blue = rospy.get_param('/background_b')
rospy.loginfo("Get Backgroud Color[%d, %d, %d]", red, green, blue)
rospy.wait_for_service('/clear') try: clear_background = rospy.ServiceProxy('/clear', Empty)
response = clear_background() return response except rospy.ServiceException, e: print "Service call failed: %s"%e
if __name__ == "__main__": parameter_config()
|
8.3 配置代码编译规则
配置CMakeLists.txt
中的编译规则:
将以下代码加入CMakeLists.txt
中:
1 2
| add_executable(parameter_config src/parameter_config.cpp) target_link_libraries(parameter_config ${catkin_LIBRARIES})
|
然后在工作空间catkin_ws
中用catkin_make
进行编译,具体步骤如下:
1 2 3 4 5 6
| cd ~/catkin_ws catkin_make source devel/setup.bash roscore rosrun turtlesim turtlesim_node rosrun learning_parameter parameter_config
|
现象:小海龟的背景颜色由蓝色变为白色。
9. ROS中的坐标系管理系统
机器人中的坐标变换(参考《机器人学导论》):
通过tf管理两个坐标系。
坐标变换的示例,打开终端输入以下命令:
1 2 3 4
| sudo apt-get install ros-melodic-turtle-tf roslaunch turtle_tf turtle_tf_demo.launch rosrun turtlesim turtle_teleop_key rosrun tf view_frames
|
第一句命令的意思是需要安装turtle_tf
示例的功能包。
工具一:看到系统中所有tf之间的关系
rosrun tf view_frames
可以可视化看到系统中所有tf之间的关系,也是调试机器人经常用到的工具,输入命令后需要监听5s,5s内所有坐标系之间的关系都保存下来。
world坐标系就是全局坐标系,表示的是整个仿真器的坐标原点,即左下角的原点,这个坐标点是不会动的。
工具二:通过tf_echo
查看两个坐标系之间的关系:
1
| rosrun tf tf_echo turtle1 turtle2
|
这个描述了turtle2坐标系通过怎样的变换变换到turtle1坐标系。
Translation:即在xyz三个方向上的平移。
Rotation:坐标系通过怎样的旋转可以变成一样的姿态。分别进行四元数、弧度、角度单位来进行描述。
工具三:通过rviz
查看两个坐标系之间的关系:
1
| rosrun rviz rviz -d `rospack find turtle_tf` /rviz/turtle_rviz.rviz
|
两个小海龟之间的坐标系的关系,可以通过两者和world坐标系的关系计算得到,两个四元矩阵具体计算如下:
10. tf坐标系广播与监听的编程实现
创建功能包
1 2
| cd ~/catkin_ws/src catkin_create_pkg learning_tf roscpp rospy tf turtlesim
|
10.1 创建tf广播器代码(C++/python)
如何实现一个tf广播器
- 定义TF广播器(TransformBroadcaster);
- 创建坐标变换值;
- 发布坐标变换(SendTransform)。
在catkin_ws/src/learning_tf/src
文件夹下创建turtle_tf_broadcaste.cpp
文件,具体C++代码内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
|
#include <ros/ros.h> #include <tf/transform_broadcaster.h> #include <turtlesim/Pose.h>
std::string turtle_name;
void poseCallback(const turtlesim::PoseConstPtr& msg) { static tf::TransformBroadcaster br;
tf::Transform transform; transform.setOrigin( tf::Vector3(msg->x, msg->y, 0.0) ); tf::Quaternion q; q.setRPY(0, 0, msg->theta); transform.setRotation(q);
br.sendTransform(tf==StampedTransform(transform, ros==Time::now(), "world", turtle_name)); }
int main(int argc, char** argv) { ros::init(argc, argv, "my_tf_broadcaster");
if (argc != 2) { ROS_ERROR("need turtle name as argument"); return -1; }
turtle_name = argv[1];
ros::NodeHandle node; ros::Subscriber sub = node.subscribe(turtle_name+"/pose", 10, &poseCallback);
ros::spin();
return 0; };
|
具体python代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
import roslib roslib.load_manifest('learning_tf') import rospy
import tf import turtlesim.msg
def handle_turtle_pose(msg, turtlename): br = tf.TransformBroadcaster() br.sendTransform((msg.x, msg.y, 0), tf.transformations.quaternion_from_euler(0, 0, msg.theta), rospy.Time.now(), turtlename, "world")
if __name__ == '__main__': rospy.init_node('turtle_tf_broadcaster') turtlename = rospy.get_param('~turtle') rospy.Subscriber('/%s/pose' % turtlename, turtlesim.msg.Pose, handle_turtle_pose, turtlename) rospy.spin()
|
10.2 创建tf监听器代码(C++/python)
如何实现一个tf监听器
- 定义TF广播器(TransformListener);
- 查找坐标变换(waitForTransform、lookupTransform)。
在catkin_ws/src/learning_tf/src
文件夹下创建turtle_tf_Listener.cpp
文件,具体内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
|
#include <ros/ros.h> #include <tf/transform_listener.h> #include <geometry_msgs/Twist.h> #include <turtlesim/Spawn.h>
int main(int argc, char** argv) { ros::init(argc, argv, "my_tf_listener");
ros::NodeHandle node;
ros==service==waitForService("/spawn"); ros==ServiceClient add_turtle = node.serviceClient<turtlesim==Spawn>("/spawn"); turtlesim::Spawn srv; add_turtle.call(srv);
ros==Publisher turtle_vel = node.advertise<geometry_msgs==Twist>("/turtle2/cmd_vel", 10);
tf::TransformListener listener;
ros::Rate rate(10.0); while (node.ok()) { tf::StampedTransform transform; try { listener.waitForTransform("/turtle2", "/turtle1", ros==Time(0), ros==Duration(3.0)); listener.lookupTransform("/turtle2", "/turtle1", ros::Time(0), transform); } catch (tf::TransformException &ex) { ROS_ERROR("%s",ex.what()); ros::Duration(1.0).sleep(); continue; }
geometry_msgs::Twist vel_msg; vel_msg.angular.z = 4.0 * atan2(transform.getOrigin().y(), transform.getOrigin().x()); vel_msg.linear.x = 0.5 * sqrt(pow(transform.getOrigin().x(), 2) + pow(transform.getOrigin().y(), 2)); turtle_vel.publish(vel_msg);
rate.sleep(); } return 0; };
|
具体python代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
import roslib roslib.load_manifest('learning_tf') import rospy import math import tf import geometry_msgs.msg import turtlesim.srv
if __name__ == '__main__': rospy.init_node('turtle_tf_listener')
listener = tf.TransformListener()
rospy.wait_for_service('spawn') spawner = rospy.ServiceProxy('spawn', turtlesim.srv.Spawn) spawner(4, 2, 0, 'turtle2')
turtle_vel = rospy.Publisher('turtle2/cmd_vel', geometry_msgs.msg.Twist,queue_size=1)
rate = rospy.Rate(10.0) while not rospy.is_shutdown(): try: (trans,rot) = listener.lookupTransform('/turtle2', '/turtle1', rospy.Time(0)) except (tf.LookupException, tf.ConnectivityException, tf.ExtrapolationException): continue
angular = 4 * math.atan2(trans[1], trans[0]) linear = 0.5 * math.sqrt(trans[0] ** 2 + trans[1] ** 2) cmd = geometry_msgs.msg.Twist() cmd.linear.x = linear cmd.angular.z = angular turtle_vel.publish(cmd)
rate.sleep()
|
10.3 配置编译规则
打开catkin_ws/src/learning_tf/src
文件夹,配置CMakeLists.txt
中的编译规则:
- 设置需要编译的代码和生成 的可执行文件
- 设置链接库
1 2 3 4 5
| add_executable(turtle_tf_broadcaster src/turtle_tf_broadcaster.cpp) target_link_libraries(turtle_tf_broadcaster ${catkin_LIBRARIES})
add_executable(turtle_tf_listener src/turtle_tf_listener.cpp) target_link_libraries(turtle_tf_listener ${catkin_LIBRARIES})
|
然后回到工作空间的根目录catkin_ws通过catkin_make
进行编译。
通过以下命令运行:
1 2 3 4 5 6 7 8 9
| cd ~/catkin_ws catkin_make source devel/setup.bash roscore rosrun turtlesim turtlesim_node rosrun learning_tf turtle_tf_broadcaster_name:=turtle1_tf_broadcaster /turtle1 rosrun learning_tf turtle_tf_broadcaster_name:=turtle2_tf_broadcaster /turtle2 rosrun learning_tf turtle_tf_listener rosrun turtlesim turtle_teleop_key
|
上面第6、7行代码中turtle1_tf_broadcaster
会取代程序中的my_tf_broadcaster
,因此虽然是同一个程序,但是两个节点的名字不一样,可以运行两次。
11. launch启动文件的使用方法
launch文件:通过XML文件实现多节点的配置和启动(可自动启动ROS Master)
launch文件中有很多标签,标签中有很多属性,用launch文件可以不用打开很多终端启动很多节点。
原来是通过roscore
启动ROS Master,launch文件可以自动启动ROS Master。
1 2 3 4
| <launch> <node pkg="turtlesim" name="sim1" type="turtlesim_node"/> <node pkg="turtlesim" name="sim2" type="turtlesim_node"/> </launch>
|
基本属性:
pkg:节点所在的功能包名称
type:节点的可执行文件名称
name:节点运行时的名称
可选属性:
output:是否将某个节点的日志信息打印到终端
respawn:节点启动失败是否进行重启
required:某个节点是否一定要启动起来
ns:namespace,给每个节点一个命名空间,避免命名冲突
args:给每个节点输入一些参数来使用
其他常用属性:
<param>/<rosparam>
:设置ROS系统运行中的参数,存储在参数服务器中。
<param name="output_frame" value="odom"/>
加载参数文件中的多个参数:<rosparam file="params.yaml" command="load" ns= "params"/>
<arg>
:launch文件内部的局部变量,仅限于launch文件使用,也可以给node作为输入参数
<arg name="arg-name" default="arg-value"/>
调用:
<param name="foo" value="$(arg arg-name)"/>
<node name="node" pkg="package" type="type" args="$arg arg-name"/>
两种参数设置对比:
11.1 launch文件编程实现-示例一
launch文件编程实现:
1 2
| cd ~/catkin_ws/src catkin_create_pkg learning_launch
|
一般在创建launch功能包之后,里面没有launch文件,通过新建launch文件夹方便管理功能包里面的各种资源。
在launch文件夹中新建simple.launch文件,里面的内容为:
1 2 3 4
| <launch> <node pkg="learning_topic" type="person_subscriber" name="talker" output="screen" /> <node pkg="learning_topic" type="person_publisher" name="listener" output="screen" /> </launch>
|
两个node
标签分别启动两个node
节点,一个是learning_topic
功能包下面的talker
节点,一个是learning_topic
功能包下面的listener
节点,talker
的节点名为person_subscriber
,listener
的节点名为person_publisher
,默认两个节点名不会打印到终端,为了保证显示效果,将其output
设置为screen
。
在工作空间的根目录~/catkin_ws
中通过catkin_make
编译一下。
1
| rosluanch learning_launch simple.launch
|
可以通过图片看出,所有的节点在一个终端启动,publisher发布一个信息,subscriber订阅一个信息。
11.2 launch文件编程实现-示例二
在launch文件夹中,新建turtlesim_parameter_config.launch
文件,具体内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <launch>
<param name="/turtle_number" value="2"/>
<node pkg="turtlesim" type="turtlesim_node" name="turtlesim_node"> <param name="turtle_name1" value="Tom"/> <param name="turtle_name2" value="Jerry"/>
<rosparam file="$(find learning_launch)/config/param.yaml" command="load"/> </node>
<node pkg="turtlesim" type="turtle_teleop_key" name="turtle_teleop_key" output="screen"/>
</launch>
|
param和rosparam都是设置变量的,第一个参数名为turtle_number,数值为2。rosparam通过查找地址,加载一个参数文件param.yaml。
$(find learning_launch)
指的是搜索learning_launch
文件夹的完整路径。
在learning_launch
文件夹下新建config
文件夹,在config
文件夹下新建param.yaml
文件,param.yaml
具体内容如下:
1 2 3 4 5 6
| A: 123 B: "hello"
group: C: 456 D: "hello"
|
通过roslaunch learning_launch turtlesim_parameter_config.launch
命令来启动launch
文件。
通过rosparam list
列出参数:
11.3 launch文件编程实现-示例三
在~/catkin_ws/src/learning_launch/launch
文件夹中,新建start_tf_demo_c++.launch
文件,具体内容如下:
相应的launch文件内容为:
1 2 3 4 5 6 7 8 9 10 11 12
| <launch>
<node pkg="turtlesim" type="turtlesim_node" name="sim"/> <node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/>
<node pkg="learning_tf" type="turtle_tf_broadcaster" args="/turtle1" name="turtle1_tf_broadcaster" /> <node pkg="learning_tf" type="turtle_tf_broadcaster" args="/turtle2" name="turtle2_tf_broadcaster" />
<node pkg="learning_tf" type="turtle_tf_listener" name="listener" />
</launch>
|
在终端中运行命令roslaunch learning_launch start_tf_demo_c++.launch
,得到如下效果:
可以通过键盘控制小海龟1的移动,小海龟2会跟着移动。
针对python编写的launch文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <launch>
<node pkg="turtlesim" type="turtlesim_node" name="sim"/> <node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/>
<node name="turtle1_tf_broadcaster" pkg="learning_tf" type="turtle_tf_broadcaster.py"> <param name="turtle" type="string" value="turtle1" /> </node> <node name="turtle2_tf_broadcaster" pkg="learning_tf" type="turtle_tf_broadcaster.py"> <param name="turtle" type="string" value="turtle2" /> </node>
<node pkg="learning_tf" type="turtle_tf_listener.py" name="listener" />
</launch>
|
11.4 launch文件编程实现-示例四
在~/catkin_ws/src/learning_launch/launch
文件夹中,新建turtlesim_remap.launch
文件,具体内容如下:
1 2 3 4 5 6 7 8 9
| <launch>
<include file="$(find learning_launch)/launch/simple.launch" />
<node pkg="turtlesim" type="turtlesim_node" name="turtlesim_node"> <remap from="/turtle1/cmd_vel" to="/cmd_vel"/> </node>
</launch>
|
一个launch文件包含另外一个launch文件(simple.launch)。
第6行代码是将/turtle1/cmd_vel
改为/cmd_vel
。
1
| roslaunch learning_launch turtlesim_remap.launch
|
效果如下:/turtle1/cmd_vel
变为/cmd_vel
。发布一个话题,可以看到小乌龟发生移动。
12. 常用可视化工具的使用
12.1 Qt工具箱
Qt工具箱常用工具:
日志输出工具——rqt_console
计算图可视化工具——rqt_graph
数据绘图软件——rqt_plot
图像渲染工具——rqt_image_view
例程实现:
1 2
| roscore rosrun turtlesim turtlesim_node
|
在终端中输入rqt_
+Tab键,可以看到相关的工具,输入rqt_console
命令:
例如通过控制小海龟移动撞墙后会有warning警告:
通过rqt_plot
命令打开rqt_plot:
命令rqt_image_view
用来显示摄像头图像,界面如下图:
也可以通过在终端中输入rqt
来进行,可以通过选择各种插件Plugins
来进行相关操作。
12.2 Rviz
Rviz是一款三维可视化工具,可以很好的兼容基于ROS软件框架的机器人平台。
- 在rviz中,可以使用可扩展标记语言XML对机器人、周围物体等任何事物进行尺寸、质量、位置、材质、关节等属性的描述,并且在界面中呈现出来。
- 同时,rviz还可以通过图形化的方式,实时显示机器人传感器的信息、机器人的运动状态、周围环境的变化等信息。
- 总而言之,rviz通过机器人模型参数、机器人发布的传感信息等数据,为用户进行所有可监测信息的图形化显示。用户和开发者也可以在rviz的控制界面下,通过按钮、滑动条、数值等方式,控制机器人的行为。
12.3 Gazebo
Gazebo是一款功能强大的三维物理仿真平台
- 具备强大的物理引擎
- 高质量的渲染
- 方便的编程与图形接口
- 开源免费
其典型应用场景包括:
- 测试机器人算法
- 机器人的设计
- 现实场景下的回溯测试
Gazebo的启动:
通过命令roslaunch gazebo_ros
可以看到相关launch文件。
13. 课程总结与进阶攻略
参考链接:
http://wiki.ros,org/ros_control
http://wiki.ros,org/gmapping/
http://wiki.ros,org/hector_slam
斯坦福大学公开课——机器人学:https://www.bilibili.com/video/av4506104
台湾交通大学——机器人学:https://www.bilibili.com/video/av18516816
Andrew Davison的机器人学讲座课程:http://www.doc.ic.ac.uk/~ajd/Robotics/index.html
ETH - Robotic Systems Lab:http://www.rsl.ethz.ch/education-students/lectures.html
古月学院:PC:https://class.guyuehome.com 手机:“古月居”微信公众号→古月学院
ROS:https://www.ros.org/
ROS Wiki:http://wiki.ros.org/
ROS Con 2012 ~2019:https://roscon.ros.org/world/2021/
ROS Robots:https://robots.ros.org/
Ubuntu Wiki:https://wiki.ubuntu.org.cn/
古月居:https://www.guyuehome.com
古月居泡泡:https://www.guyuehome.com/Bubble
古月学院:https://class.guyuehome.com