显示状态信息

在实际项目开发中,我们往往需要从机器人上得到各种数据,然后进行可视化显示;这一步在ROS2中一般通过发布和订阅自定义消息实现; 关于ROS2如何自定义消息我这里就不再赘述了,可以直接参考官方文档;在 OriginBot 的功能包里就有一个自定义的消息包:originbot_msgs,在这个包里定义了一个msg文件和三个srv文件;

"msg/OriginbotStatus.msg"
"srv/OriginbotBuzzer.srv"
"srv/OriginbotLed.srv"
"srv/OriginbotPID.srv"

其中OriginbotStatus.msg 文件里提供了 OriginBot 的电源电压、蜂鸣器状态及LED灯状态三个状态信息;

OriginbotBuzzer.srv,OriginbotLed.srv,OriginbotPID.srv三个服务文件则分别提供蜂鸣器开关、LED开关以及小车PID参数调节三个服务;

如果要在Android端订阅这些消息和建立服务,则需要将这个包进行交叉编译;只需要将自定义的包放到源文件目录下(比如可以放到 ros2/common_interfaces 目录),然后按照第一章的步骤编译即可生成相关的库文件,我编译好的库文件如下图:

同时会生 originbot_msgs_messages.jar 文件如下图:

得到库文件和jar文件之后,按照之前的方法在Android项目中加载即可。同时在Android项目中添加一个订阅小车状态参数的节点类,代码如下:

package com.example.originbot;

import android.annotation.SuppressLint;
import android.widget.TextView;
import org.ros2.rcljava.node.BaseComposableNode;
import org.ros2.rcljava.subscription.Subscription;

import originbot_msgs.msg.OriginbotStatus;

public class RobotInfoSubNode extends BaseComposableNode {
    private final String topic;
    private final TextView power_text;
    private final TextView led_text;
    private Subscription<OriginbotStatus> bot_state_sub;
    private double sum_v = (12.8-9.0); // 小车的电压在9.0~12.8V之间
    private boolean led_state;

    public RobotInfoSubNode(String name, final String topic, final TextView power_t, final TextView led_t) {
        super(name);
        this.topic = topic;
        this.power_text = power_t;
        this.led_text = led_t;
        this.bot_state_sub = this.node.createSubscription(OriginbotStatus.class, this.topic, this::BotStateCallBack);
    }

    @SuppressLint({"DefaultLocale", "SetTextI18n"})
    private void BotStateCallBack(OriginbotStatus msg) {
        double data = msg.getBatteryVoltage();
        double pa_v = 100*(data-9.0)/(12.8-9.0); // 将电量百分化处理
        this.power_text.setText(String.format("电量: %1$.2f", pa_v)+"%");

        led_state = msg.getLedOn();
        if(led_state)
            this.led_text.setText("LED:开");
        else
            this.led_text.setText("LED:关");
    }

    public boolean getLedState(){
        return led_state;
    }
}

以上节点用于订阅机器人状态消息(这里主要是电池电压以及LED 状态,对于获取到的电压信息进行了百分化处理),然后分别显示在对应的 TextView 控件上;同时实现一个 getLedState() 方法,用于获取当前 LED 的状态。

然后在MainActivity中调用此节点,代码如下:

private RobotInfoSubNode botstate_sub_node;
private TextView power_view;
private TextView led_view;

power_view = findViewById(R.id.power_textView);
led_view = findViewById(R.id.led_textView);
botstate_sub_node = new RobotInfoSubNode("OriginBot_state_info", "/originbot_status", power_view, led_view);
getExecutor().addNode(botstate_sub_node);

在实际项目开发中往往会按照需求对数据进行处理,OriginbotStatus.msg 里面也只提供了三个状态信息,如果需要其他的数据,则按照相同的方法进行添加即可。

通过ROS2服务控制小车的状态

下面添加LED灯的状态控制和显示,主要是为了测试Android端与小车之间的ROS2服务通讯;根据具体的需求也可以改成蜂鸣器、PID参数控制或者再添加其他的服务。

首先在Android项目中添加一个ROS2客户端节点类,代码如下:

package com.example.originbot;

import android.content.Context;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;

import org.ros2.rcljava.node.BaseComposableNode;
import org.ros2.rcljava.client.Client;

import java.time.Duration;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import originbot_msgs.srv.OriginbotLed;
import originbot_msgs.srv.OriginbotLed_Request;
import originbot_msgs.srv.OriginbotLed_Response;

public class LedControlSrvNode extends BaseComposableNode{
    private final String topic;
    private final Context context;
    private Client<OriginbotLed> led_control_cli;
    private OriginbotLed_Request led_req = new OriginbotLed_Request();
    private boolean is_connect = false;
    private OriginbotLed_Response led_res = new OriginbotLed_Response();
//    boolean led_state = false;
    public LedControlSrvNode(String name, final String topic, Context mContext){
        super(name);
        this.topic = topic;
        this.context = mContext;
        try{
            this.led_control_cli = this.node.createClient(OriginbotLed.class, this.topic);
        }catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            is_connect = this.led_control_cli.waitForService(Duration.ofSeconds(1));
            if(!is_connect)
                Toast.makeText(this.context, "服务不可用", Toast.LENGTH_SHORT).show();
        }
    }

    public void sendRequest(boolean state){
        is_connect = this.led_control_cli.isServiceAvailable();
        if(is_connect)
        {
            led_req.setOn(state);
            Future<OriginbotLed_Response> result = this.led_control_cli.asyncSendRequest(led_req);
            try {
                led_res = result.get();
//                led_state = led_res.getResult();
//                Log.d("MyApp", "led_state: "+ led_state );
            }catch (InterruptedException | ExecutionException e)
            {
                e.printStackTrace();
            }
        }
        else
        {
            Toast.makeText(this.context, "服务不可用", Toast.LENGTH_SHORT).show();
        }
    }
}

以上节点用于建立一个客户端,连接服务端,建立 sendRequest 方法用于请求更改LED灯的开关状态;同时在程序中也添加了 Toast 提示,如果连接断开则会提示“服务不可用”;

注意

  • 在调用 sendRequest 方法时,首先需要判断此时客户端和服务端之间是否仍处于连接状态,因为在连接断开后再调用 asyncSendRequest 方法时会使程序进入阻塞状态,这会导致程序崩溃。
  • 服务端的返回参数 led_state = led_res.getResult(),它并不是LED当前的状态值,而是代表LED是否设置成功。

接下来在界面添加一个按钮用于控制LED的状态,同时在 mainActivity 中调用上面的节点,代码如下:

private LedControlSrvNode led_control_node;
private ImageButton led_control_btn;
private boolean led_state;

led_control_node = new LedControlSrvNode("OriginBot_state_control", "/originbot_led", mContext);
getExecutor().addNode(led_control_node);
led_control_btn = findViewById(R.id.led_btn);
led_control_btn.setOnClickListener(changeLedState);

在测试过程中,我发现小车上的LED灯在小车启动时并不是一直处于同一种状态,所以需要优先获取LED的状态用于初始化按钮的显示状态,代码如下:

led_state = botstate_sub_node.getLedState();
if(!led_state){
    led_control_btn.setImageResource(R.drawable.led_off);
}else{
    led_control_btn.setImageResource(R.drawable.led_on);
}

这段代码在建立获取小车状态的节点后调用,根据LED的开关状态,设置按钮的显示图片。

最后实现按钮的响应函数 changeLedState,代码如下:

private View.OnClickListener changeLedState = new View.OnClickListener()
    {
        public void onClick(final View view) {
            led_state = botstate_sub_node.getLedState();
            if(!led_state)
            {
                led_control_node.sendRequest(true);
                led_control_btn.setImageResource(R.drawable.led_off);
            }
            else
            {
                led_control_node.sendRequest(false);
                        led_control_btn.setImageResource(R.drawable.led_on);
            }
        }
    };

在点击按钮时也需要优先获取当前LED的开关状态,然后根据状态来进行打开或关闭的操作。

结果展示

APP 运行界面如下:

总结与延伸

在这篇文章中,我编译了Android端的ros2自定义消息,并编写订阅节点获取小车的电量以及LED状态;同时编写客户端节点通过按钮控制LED的开关状态。

下一篇文章来聊一聊如何在剩余的空白界面中实时显示从摄像头获取的图像数据。