ROS探索总结(三十四)——rviz plugin

  • 内容
  • 评论
  • 相关

rvizROS官方的一款3D可视化工具,几乎我们需要用到的所有机器人相关数据都可以在rviz中展现,当然由于机器人系统的需求不同,很多时候rviz中已有的一些功能仍然无法满足我们的需求,这个时候rvizplugin机制就派上用场了。上一篇我们探索了插件的概念和基本实现,这一篇通过rviz中的插件实现,来进行巩固加深。

rviz作为一种可扩展化的视图工具,可以使用这种插件机制来扩展丰富的功能,进行二次开发,我们在rviz中常常使用的激光数据可视化显示、图像数据可视化显示,其实都是官方提供的插件。所以,我们完全可以在rviz的基础上,打造属于我们自己的机器人人机界面。

 

一、目标

在《Mastering ROS for Robotics Programming》这本书的第六章,讲解了一个发布速度控制的rviz plugin,这里我们就根据这个plugin的实现来学习。

plugin的整体效果如下:

image

从上边的图片我们有一个大致的感性认识:

首先,这是一个可视化的界面,在ros的编程中,好像没有可视化的编程语句,那么如何实现可视化编程呢?如果你使用过ROSrqt工具箱,应该就会想到Qt,没错,Qt是一个实现GUI的优秀框架,ROS中的可视化工具绝大部分都是基于Qt进行开发的,rvizplugin也不例外。所以我们需要了解Qt的一些相关知识。

其次,这个界面里包含三个输入框,分别对应topic名称、线速度值、角速度值,这就需要我们读取用户的输入,然后转换成ROStopic,这里也会涉及到Qt中的重要概念——信号、槽,类似于回调函数,可以自行百度学习一下。

好的,接下来,我们就分步骤看一下如何实现这个plugin,也可以跟着一起做。

完整的功能包代码可以在github上下载。

 

二、创建功能包

首先,我们来创建一个功能包,用来放置plugin的所有相关代码:

  1. $ catkin_create_pkg rviz_telop_commander roscpp rviz std_msgs

这个功能包的依赖于rviz,因为rviz是基于Qt开发的,所以省去了对Qt的依赖。

 

三、实现主代码

接下来,我们在功能包的src文件夹下开始做代码的实现,这个plugin相对简单,只需要一个cpp文件就可以完成,当然对应的还要有它的头文件。

 

3.1 头文件

  1. #ifndef TELEOP_PAD_H
  2. #define TELEOP_PAD_H
  3.  
  4. //所需要包含的头文件
  5. #include <ros/ros.h>
  6. #include <ros/console.h>
  7. #include <rviz/panel.h>   //plugin基类的头文件
  8.  
  9. class QLineEdit;
  10.  
  11. namespace rviz_telop_commander
  12. {
  13. // 所有的plugin都必须是rviz::Panel的子类
  14. class TeleopPanel: public rviz::Panel
  15. {
  16. // 后边需要用到Qt的信号和槽,都是QObject的子类,所以需要声明Q_OBJECT宏
  17. Q_OBJECT
  18. public:
  19.   // 构造函数,在类中会用到QWidget的实例来实现GUI界面,这里先初始化为0即可
  20.   TeleopPanel( QWidget* parent = 0 );
  21.  
  22.   // 重载rviz::Panel积累中的函数,用于保存、加载配置文件中的数据,在我们这个plugin
  23.   // 中,数据就是topic的名称
  24.   virtual void load( const rviz::Config& config );
  25.   virtual void save( rviz::Config config ) const;
  26.  
  27.   // 公共槽.
  28. public Q_SLOTS:
  29.   // 当用户输入topic的命名并按下回车后,回调用此槽来创建一个相应名称的topic publisher
  30.   void setTopic( const QString& topic );
  31.  
  32.   // 内部槽.
  33. protected Q_SLOTS:
  34.   void sendVel();                 // 发布当前的速度值
  35.   void update_Linear_Velocity();  // 根据用户的输入更新线速度值
  36.   void update_Angular_Velocity(); // 根据用户的输入更新角速度值
  37.   void updateTopic();             // 根据用户的输入更新topic name
  38.  
  39.   // 内部变量.
  40. protected:
  41.   // topic name输入框
  42.   QLineEdit* output_topic_editor_;
  43.   QString output_topic_;
  44.  
  45.   // 线速度值输入框
  46.   QLineEdit* output_topic_editor_1;
  47.   QString output_topic_1;
  48.  
  49.   // 角速度值输入框
  50.   QLineEdit* output_topic_editor_2;
  51.   QString output_topic_2;
  52.  
  53.   // ROS的publisher,用来发布速度topic
  54.   ros::Publisher velocity_publisher_;
  55.  
  56.   // The ROS node handle.
  57.   ros::NodeHandle nh_;
  58.  
  59.   // 当前保存的线速度和角速度值
  60.   float linear_velocity_;
  61.   float angular_velocity_;
  62. };
  63.  
  64. } // end namespace rviz_plugin_tutorials
  65.  
  66. #endif // TELEOP_PANEL_H

 

3.2 cpp文件

接下来是cpp代码文件 teleop_pad.cpp

  1. #include <stdio.h>
  2.  
  3. #include <QPainter>
  4. #include <QLineEdit>
  5. #include <QVBoxLayout>
  6. #include <QHBoxLayout>
  7. #include <QLabel>
  8. #include <QTimer>
  9.  
  10. #include <geometry_msgs/Twist.h>
  11. #include <QDebug>
  12.  
  13. #include "teleop_pad.h"
  14.  
  15. namespace rviz_telop_commander
  16. {
  17.  
  18. // 构造函数,初始化变量
  19. TeleopPanel::TeleopPanel( QWidget* parent )
  20.   : rviz::Panel( parent )
  21.   , linear_velocity_( 0 )
  22.   , angular_velocity_( 0 )
  23. {
  24.  
  25.   // 创建一个输入topic命名的窗口
  26.   QVBoxLayout* topic_layout = new QVBoxLayout;
  27.   topic_layout->addWidget( new QLabel( "Teleop Topic:" ));
  28.   output_topic_editor_ = new QLineEdit;
  29.   topic_layout->addWidget( output_topic_editor_ );
  30.  
  31.   // 创建一个输入线速度的窗口
  32.   topic_layout->addWidget( new QLabel( "Linear Velocity:" ));
  33.   output_topic_editor_1 = new QLineEdit;
  34.   topic_layout->addWidget( output_topic_editor_1 );
  35.  
  36.   // 创建一个输入角速度的窗口
  37.   topic_layout->addWidget( new QLabel( "Angular Velocity:" ));
  38.   output_topic_editor_2 = new QLineEdit;
  39.   topic_layout->addWidget( output_topic_editor_2 );
  40.  
  41.   QHBoxLayout* layout = new QHBoxLayout;
  42.   layout->addLayout( topic_layout );
  43.   setLayout( layout );
  44.  
  45.   // 创建一个定时器,用来定时发布消息
  46.   QTimer* output_timer = new QTimer( this );
  47.  
  48.   // 设置信号与槽的连接
  49.   connect( output_topic_editor_, SIGNAL( editingFinished() ), this, SLOT( updateTopic() ));             // 输入topic命名,回车后,调用updateTopic()
  50.   connect( output_topic_editor_1, SIGNAL( editingFinished() ), this, SLOT( update_Linear_Velocity() )); // 输入线速度值,回车后,调用update_Linear_Velocity()
  51.   connect( output_topic_editor_2, SIGNAL( editingFinished() ), this, SLOT( update_Angular_Velocity() ));// 输入角速度值,回车后,调用update_Angular_Velocity()
  52.  
  53.   // 设置定时器的回调函数,按周期调用sendVel()
  54.   connect( output_timer, SIGNAL( timeout() ), this, SLOT( sendVel() ));
  55.  
  56.   // 设置定时器的周期,100ms
  57.   output_timer->start( 100 );
  58. }
  59.  
  60. // 更新线速度值
  61. void TeleopPanel::update_Linear_Velocity()
  62. {
  63.     // 获取输入框内的数据
  64.     QString temp_string = output_topic_editor_1->text();
  65.  
  66.     // 将字符串转换成浮点数
  67.     float lin = temp_string.toFloat();
  68.  
  69.     // 保存当前的输入值
  70.     linear_velocity_ = lin;
  71. }
  72.  
  73. // 更新角速度值
  74. void TeleopPanel::update_Angular_Velocity()
  75. {
  76.     QString temp_string = output_topic_editor_2->text();
  77.     float ang = temp_string.toFloat() ;
  78.     angular_velocity_ = ang;
  79. }
  80.  
  81. // 更新topic命名
  82. void TeleopPanel::updateTopic()
  83. {
  84.   setTopic( output_topic_editor_->text() );
  85. }
  86.  
  87. // 设置topic命名
  88. void TeleopPanel::setTopic( const QString& new_topic )
  89. {
  90.   // 检查topic是否发生改变.
  91.   if( new_topic != output_topic_ )
  92.   {
  93.     output_topic_ = new_topic;
  94.  
  95.     // 如果命名为空,不发布任何信息
  96.     if( output_topic_ == "" )
  97.     {
  98.       velocity_publisher_.shutdown();
  99.     }
  100.     // 否则,初始化publisher
  101.     else
  102.     {
  103.       velocity_publisher_ = nh_.advertise<geometry_msgs::Twist>( output_topic_.toStdString(), 1 );
  104.     }
  105.  
  106.     Q_EMIT configChanged();
  107.   }
  108. }
  109.  
  110. // 发布消息
  111. void TeleopPanel::sendVel()
  112. {
  113.   if( ros::ok() && velocity_publisher_ )
  114.   {
  115.     geometry_msgs::Twist msg;
  116.     msg.linear.x = linear_velocity_;
  117.     msg.linear.y = 0;
  118.     msg.linear.z = 0;
  119.     msg.angular.x = 0;
  120.     msg.angular.y = 0;
  121.     msg.angular.z = angular_velocity_;
  122.     velocity_publisher_.publish( msg );
  123.   }
  124. }
  125.  
  126. // 重载父类的功能
  127. void TeleopPanel::save( rviz::Config config ) const
  128. {
  129.   rviz::Panel::save( config );
  130.   config.mapSetValue( "Topic", output_topic_ );
  131. }
  132.  
  133. // 重载父类的功能,加载配置数据
  134. void TeleopPanel::load( const rviz::Config& config )
  135. {
  136.   rviz::Panel::load( config );
  137.   QString topic;
  138.   if( config.mapGetString( "Topic", &topic ))
  139.   {
  140.     output_topic_editor_->setText( topic );
  141.     updateTopic();
  142.   }
  143. }
  144.  
  145. } // end namespace rviz_plugin_tutorials
  146.  
  147. // 声明此类是一个rviz的插件
  148. #include <pluginlib/class_list_macros.h>
  149. PLUGINLIB_EXPORT_CLASS(rviz_telop_commander::TeleopPanel,rviz::Panel )
  150. // END_TUTORIAL

代码文件就是这样,并没有很多代码,还是比较好理解的。

 

四、完成编译文件

为了编译成功,还需要完成一些编译文件的设置。

4.1 plugin的描述文件

在功能包的根目录下需要创建一个plugin的描述文件 plugin_description.xml

  1. <library path="lib/librviz_telop_commander">
  2.   <class name="rviz_telop_commander/Teleop"
  3.          type="rviz_telop_commander::TeleopPanel"
  4.          base_class_type="rviz::Panel">
  5.     <description>
  6.       A panel widget allowing simple diff-drive style robot base control.
  7.     </description>
  8.   </class>
  9. </library>

 

4.2  package.xml

然后在 package.xml文件里添加plugin_description.xml

  1.   <export>
  2.       <rviz plugin="${prefix}/plugin_description.xml"/>
  3.   </export>

 

4.3  CMakeLists.txt

当然, CMakeLists.txt文件也必须要加入相应的编译规则:

  1. ## This plugin includes Qt widgets, so we must include Qt like so:
  2. find_package(Qt4 COMPONENTS QtCore QtGui REQUIRED)
  3. include(${QT_USE_FILE})
  4.  
  5. ## I prefer the Qt signals and slots to avoid defining "emit", "slots",
  6. ## etc because they can conflict with boost signals, so define QT_NO_KEYWORDS here.
  7. add_definitions(-DQT_NO_KEYWORDS)
  8.  
  9. ## Here we specify which header files need to be run through "moc",
  10. ## Qt's meta-object compiler.
  11. qt4_wrap_cpp(MOC_FILES
  12.   src/teleop_pad.h
  13. )
  14.  
  15. ## Here we specify the list of source files, including the output of
  16. ## the previous command which is stored in ``${MOC_FILES}``.
  17. set(SOURCE_FILES
  18.   src/teleop_pad.cpp 
  19.   ${MOC_FILES}
  20. )
  21.  
  22. ## An rviz plugin is just a shared library, so here we declare the
  23. ## library to be called ``${PROJECT_NAME}`` (which is
  24. ## "rviz_plugin_tutorials", or whatever your version of this project
  25. ## is called) and specify the list of source files we collected above
  26. ## in ``${SOURCE_FILES}``.
  27. add_library(${PROJECT_NAME} ${SOURCE_FILES})
  28.  
  29. ## Link the library with whatever Qt libraries have been defined by
  30. ## the ``find_package(Qt4 ...)`` line above, and with whatever libraries
  31. ## catkin has included.
  32. ##
  33. ## Although this puts "rviz_plugin_tutorials" (or whatever you have
  34. ## called the project) as the name of the library, cmake knows it is a
  35. ## library and names the actual file something like
  36. ## "librviz_plugin_tutorials.so", or whatever is appropriate for your
  37. ## particular OS.
  38. target_link_libraries(${PROJECT_NAME} ${QT_LIBRARIES} ${catkin_LIBRARIES})

现在就可以开始编译了!

 

五、实现效果

编译成功后,我们来运行rviz,需要注意的是:一定要source devel文件夹下的setup脚本,来生效路径,否则会找不到插件

  1. rosrun rviz rviz

启动之后应该并没有什么不同,点击菜单栏中的“Panels”选项,选择“Add New Panel”,在打开的窗口中的最下方,就可以看到我们创建的plugin了,点中之后,可以在下方的"Description"中看到我们在plugin_description.xml文件中对plugin的描述。

image

点击“OK”,就会弹出“TeleopPanel”插件啦:

image

 

我们分别填写三行所对应的内容:

image

然后打开一个终端查看消息:

 

image

image

OK,实现一个rvizplugin是不是很简单,重点是理解ROSQt的结合使用。使用类似的方法和Qt的编程技巧,就可以基于rviz打造属于自己的人机交互界面了!


原创文章,转载请注明: 转载自古月居

本文链接地址: ROS探索总结(三十四)——rviz plugin

微信 OR 支付宝 扫描二维码
为本文作者 打个赏
pay_weixinpay_weixin

评论

53条评论
  1. Gravatar 头像

    得光 回复

    古月老师您好,我想做一个初始化机械臂的一个插件,就是那种只含一个button的插件,按照这个思路写了,显示找不到相应的库,这个例子里plugin_description.xml的lib/librviz_telop_commander是自带的库是吗?那在我这个例子里,这个库应该写什么呢?

    • 古月

      古月 回复

      @得光 librviz_telop_commander这个是此处生成的插件名,你插件编译的是什么名称这里就是什么

  2. Gravatar 头像

    James 回复

    老师你好,请问use_sim_time可不可以在一个launch文件里用两次?

    • 古月

      古月 回复

      @James 最好不要这样做,可以分解为两个launch

  3. Gravatar 头像

    单腾飞 回复

    胡老师,我试了一下您上面提供的github里的包,build通过了,但是在加载的时候出现[ERROR] [1511083575.461585030]: PluginlibFactory: The plugin for class 'rviz_teleop_commander/TeleopPanel' failed to load. Error: Could not find library corresponding to plugin rviz_teleop_commander/TeleopPanel. Make sure the plugin description XML file has the correct name of the library and that the library actually exists.我试了一下mastering ros for robotics里的包,也出现了同样的问题。代码我都是直接用的您的,没有改动过,我的环境是ubuntu14.04+indigo,Q&A里看了一下,有的说没有source,大部分说description有问题,试着改了改,但是没有效果。

      • Gravatar 头像

        单腾飞 回复

        @古月 重试了一遍好了,谢谢胡老师

      • Gravatar 头像

        单腾飞 回复

        @古月 胡老师,还有个问题,例程中的save函数和load函数都已经overide了,但是在cpp文件中定义了这两个函数后在哪里调用的呢,没有看见调用的地方。函数参数中的config配置文件也没有找到。所以这两个函数不是很清楚,请问是怎样被调用的呢?

        • 古月

          古月 回复

          @单腾飞 这个是rviz插件机制需要调用的,没有体现在我们自己的程序中

          • Gravatar 头像

            单腾飞 回复

            @古月 那是意味着所有自己写的的插件都必须要重载这两个函数吗?是只需重载这两个函数还是其他的类成员函数也要重载呢?

              • Gravatar 头像

                单腾飞 回复

                @古月 谢谢老师回复。胡老师,还有点小问题想请教:
                1.我是否可以这样理解:这两个函数是在rviz插件加载的时候调用load,然后将存于“Topic”的话题名称给到qstring topic然后更新output_topic_editor_的内容?save函数是rviz 插件关闭的时候,将output_topic_里的话题名称存到“Topic”里吗?但是我delte panel 后重新add这个插件并没有自动在要发布的话题名称框里显示上次的话题,这样理解不对吧?
                2.假如是这样的话重载的部分应该不是必须要有的吧?也就是说我网一个固定的话题/服务/action接口上发布数据,您例程里的load函数和save函数需要怎样重写还是不需要重写?

                • 古月

                  古月 回复

                  @单腾飞 这两个重载的函数是用来更新界面的,但应该不是必须重载:virtual void load( const rviz::Config& config );
                  virtual void save( rviz::Config config ) const;

  4. Gravatar 头像

    单腾飞 回复

    古月老师您好,请问在您在ros-industrial中介绍的调试界面是如上述在rviz中不断的添加插件做出来的吗?

  5. Gravatar 头像

    James 回复

    古月老师您好,咨询您一个问题。我用ROS采集两间教室的地图,再用cartographer离线跑着两组数据,分开跑是可以的,请问可不可以将两组数据同时在一个RVIZ中跑出来??

    • 古月

      古月 回复

      @James 可以创建两个地图显示项,地图话题不能同名,然后分别订阅,试试可不可以

      • Gravatar 头像

        James 回复

        @古月 这里两个地图数据不是同时采集的对同时显示有没有影响,也就是时间戳这边会不会出现问题?

        • 古月

          古月 回复

          @James 这个我也没做过,感觉没问题,你可以试试

          • Gravatar 头像

            James 回复

            @古月 对的,时间戳这边没有问题,试过了,现在就是话题名的问题,不知改怎么修改?

              • Gravatar 头像

                James 回复

                @古月 重映射要不要修改发布与订阅的.cpp文件呢?

                  • Gravatar 头像

                    James 回复

                    @古月 问题有点多啊,崩溃!!是不是在采集数据的时候就可以改变话题名?这个还怎么修改呢?

                    • 古月

                      古月

                      @James 看你需要修改哪些话题名了,一般是在launch或者命令行里重命名,也可以启动的时候加上命名空间试试

                  • Gravatar 头像

                    James 回复

                    @古月 用turtlebot搭载激光雷达采集激光雷达和里程计的数据,要把这两个数据的话题名改了?

                    • 古月

                      古月

                      @James 如果有两个机器人发布同样话题的消息的话,就需要修改话题名了,否则数据会出问题

      • Gravatar 头像

        James 回复

        @古月 采集好的地图数据话题信息怎么修改,是通过修改发布消息和订阅消息的文件吗?

  6. Gravatar 头像

    肖翼甫 回复

    古月前辈,您好!
    我想请教大神一个问题,请问urdf文件,除了做仿真,还有什么其他的作用吗?

    • 古月

      古月 回复

      @肖翼甫 你好,还可以可视化显示,有些功能包中的算法也会用到机器人的URDF模型

  7. Gravatar 头像

    Constance 回复

    大神,我想请问一下在QT-ros环境下开发rviz的插件,开发过程中,我先不让他编译成库,先让他生成可执行文件,可是怎么不能运行呢,一直报The program has unexpectedly finished.这样的错误,我检查了一下,如果不继承自rviz::Panel这个类,继承QT原生的QWidget就可以运行。还有我调试的时候直接进到像内存一样的界面,完全看不懂的那种。就像是正常调试程序出现段错误进去那种界面。请问是不是rviz::Panel不可以实例化?

    • Gravatar 头像

      Constance 回复

      @Constance 现在在开发过程中我无法运行看效果,这样比较困惑,怎样设置才可以呢,古月大神。

    • 古月

      古月 回复

      @Constance 如果继承QT原生的QWidget,应该就是一个原生的qt程序,可以编译成可执行文件,但不算是插件,如果不需要依托rviz,这样做是没问题的。关于rviz::Panel,可以看相关的文档:http://docs.ros.org/jade/api/rviz/html/c++/classrviz_1_1Panel.html

      • Gravatar 头像

        Constance 回复

        @古月 可是我就是需要开发一个rviz的插件,所以我要继承rviz::Panel,但是这样就无法调试吗?

        • 古月

          古月 回复

          @Constance rviz插件的在线调试我也没试过,不确定是否可以

  8. Gravatar 头像

    rospoor 回复

    古月老师您好,我按照步骤都弄完了,但是我在rviz 中add时没有看到那个插件,我的编译没有问题,source也做了。我是个新手,目前比较困惑 🙂

    • 古月

      古月 回复

      @rospoor 你好,不是点rviz左下角的add,是在菜单栏里选Panel --> Add New Panel

  9. Gravatar 头像

    rospoor 回复

    古月老师您好,我按照步骤都弄完了,但是我在rviz 中add时没有看到那个插件,我的编译没有问题,source也做了。我是个新手,目前比较困惑

    • Gravatar 头像

      real_dg 回复

      @rospoor 是在rviz界面的左上角,有files panels help

  10. Gravatar 头像

    lyyh 回复

    大神你好,有个问题,机械臂在moveit中从a点到b点的轨迹需要保持rx,ry坐标不变,怎么在程序中设置约束?

    • 古月

      古月 回复

      @lyyh 你好,也就是只在z向运动么?可以使用moveit中的cartesian_path走直线

      • Gravatar 头像

        lyyh 回复

        @古月 不是z方向运动 是规划轨迹时约束末端执行器运动时orientation保持水平(即orientation x 和orientation y不变) 但是orientation z 是可变的(由moveit规划决定),这样的约束如何实现

  11. Gravatar 头像

    Richard 回复

    古月老师,您好!
    我按照按照本篇博客的内容搭了环境,跑下来碰到两个问题:
    1. 用catkin_make编译时,qt4有报错: "/usr/include/boot/type_traits/detail/has_binary_operator.hp: Parse error at "BOOST_JOIN" ",有workaround可以绕过:https://stackoverflow.com/questions/15455178/qt4-cgal-parse-error-at-boost-join,最终编译可以通过。
    2. 编译成功以后,打开rviz,在"Add New Panel"里面可以找到新添加的"TeleopPanel"插件,但只要一运行程序就崩溃: Segmentation fault (core dumped)。以前没调试过这种段错误,所以这个问题没找到怎么解决,特来请教。非常感谢。

    不确定是否是我环境的问题,我用的是generic Ubuntu 16.04 (desktop x64), 系统自带了QT (QMake version 2.01a, Qt version 4.8.7),ROS版本是Kinetic。现在官网QT版本最新已经是5.9了,不太清楚是不是因为Qt版本太旧了。

    • 古月

      古月 回复

      @Richard 我用的是ubuntu14.04+ros indigo版本,在16.04和kinetic上可能会有依赖软件版本不同的问题,这个我还没试过

      • Gravatar 头像

        Richard 回复

        @古月 谢谢古月老师回复,通过修改CMakelists.txt解决了这个问题,看起来的确是一些运行时的依赖库问题。

  12. Gravatar 头像

    JIAMIN 回复

    古月老师,您好,想问下,您有做过ROS与android的通讯连接吗?我在示例中看有rosbridge这个包,但是不是特别懂

    • 古月

      古月 回复

      @JIAMIN ros有java的接口实现,可以在android上跑ros节点,可以google找一下,turtlebot好像也有android的控制客户端

      • Gravatar 头像

        JIAMIN 回复

        @古月 好的,那我找找看,谢谢您了

  13. Gravatar 头像

    Celine 回复

    古月老师,您好!
    咨询您一个问题!
    我现在使用的是window操作系统,想远程控制Ubuntu,嘿嘿,就是Ubuntu上面安装了ROS。
    现在按照网络上的教材可以远程连接上了,问题是只有命令行界面,没有图形化界面。
    只要运行类似RVIZ,GAZEBO之类的就不行。
    请问,有什么办法能够让远程控制的Ubuntu能够运行这些图形界面么?
    非常感谢!
    Celine
    Best Regards
    Hope you happy every day!

发表评论

电子邮件地址不会被公开。 必填项已用*标注