想要在安卓上運行ROS節點,需要rosjava這個client library,以及構建在rosjava之上的RosActivity類
安卓上的每個ros節點,跟ubuntu上的常規節點不一樣,不是進程而是線程,亦稱作nodelet,所以在一個安卓app裏可以運行多個ros node,構成一個node網絡
node通過NodeMain接口定義,NodeMain定義了2個函數:setup和loop
前者讓線程有機會做初始化操作(等價於常規節點的subscriber/publisher/server/client實例化),後者是線程的消息循環(等價於常規節點的spinOnce方法)處理
問題來了,如果UI線程要向ros節點發送消息,該怎麼發呢?
我最初想到的是用安卓的MessageQueue來實現,但很快發現問題所在,Looper.loop方法 跟 NodeMain的loop方法 不能並存
因爲Looper.loop()一旦運行,則線程進入Looper消息循環,永遠不會執行NodeMain的loop方法
後來想出了個方法,UI線程(通過調用相應方法)修改ros節點的成員變量,ros節點在消息循環裏裏讀取該成員變量,完成消息傳遞,這有2個問題:①消息類型固定②如果還有其他線程發消息,則需要考慮同步(線程安全)
爲了解決問題②,一番尋找,發現BlockingQueue可以達成效果,且該類一直是線程間通訊的標準方法,自己用MessageQueue反而顯得孤陋寡聞了
那怎麼解決問題①呢?考慮到NodeMain的loop方法不像Looper.loop()那樣是個不返回的死循環,所以,理論上應該是可以將ros的消息循環揉到android的消息循環裏的
比如,定義一個Tick Message,在Looper.prepare後,Looper.loop前,發送該Tick(boot自舉),然後在Handler裏調用NodeMain的loop,然後再向自身發送Tick,使得ros消息循環也運轉起來,這樣問題①和問題②都解決了,還實現了在一個線程裏運行兩個消息循環
上代碼,首先是這個運行在android上的ros節點
package org.ros.android.android_tutorial_pubsub;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.format.Time;
import android.util.Log;
import org.ros.concurrent.CancellableLoop;
import org.ros.message.MessageListener;
import org.ros.namespace.GraphName;
import org.ros.node.AbstractNodeMain;
import org.ros.node.ConnectedNode;
import org.ros.node.Node;
import org.ros.node.service.ServiceResponseBuilder;
import org.ros.node.service.ServiceServer;
import org.ros.node.topic.Subscriber;
import java.util.logging.MemoryHandler;
import rosjava_test_msgs.AddTwoIntsRequest;
import rosjava_test_msgs.AddTwoIntsResponse;
/**
* Created by haipeng on 16-3-17.
*/
public class SpeechSynNode extends AbstractNodeMain {
private String topicName;
private Context mUiCtx;
private Handler mUiHandler;
private Handler mHandler;
private static String TAG = "SpeechSynNode";
public SpeechSynNode(Context ctx, Handler handler, String topic)
{
mUiCtx = ctx;
topicName = topic;
mUiHandler = handler;
}
@Override
public GraphName getDefaultNodeName() {
return GraphName.of("speech_synthesizer");
}
@Override
public void onStart(final ConnectedNode connectedNode) {
//create subscriber
final Subscriber<std_msgs.String> subscriber =
connectedNode.newSubscriber(topicName, std_msgs.String._TYPE);
subscriber.addMessageListener(new MessageListener<std_msgs.String>() {
@Override
public void onNewMessage(std_msgs.String message) {
String text = message.getData();
Log.i(TAG, "I heard msg from ubuntu : \"" + text + "\"");
Message msg = mHandler.obtainMessage(CommonMsg.PLAY_MSG, text);
mHandler.sendMessage(msg);
}
});
connectedNode.executeCancellableLoop(new CancellableLoop() {
@Override
protected void setup() {
Looper.prepare();//allocate MQ, required by ifly
mHandler = new Handler(){
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case CommonMsg.TICK_NODE_SYN: {
try {
//process ros msg
loop();
//enter next tick
Message tick = mHandler.obtainMessage(CommonMsg.TICK_NODE_SYN);
mHandler.sendMessage(tick);
}catch(InterruptedException e){
Log.e(TAG, "node thread interrupted " + e.getMessage());
}
break;
}
case CommonMsg.PLAY_MSG:{
String text = (String)msg.obj;
playMsg(text);
break;
}
default: {
break;
}
}
}
};
//trigger the ros msg loop
Message msg = mHandler.obtainMessage(CommonMsg.TICK_NODE_SYN);
mHandler.sendMessage(msg);
//tell UI thread node's handler
msg = mUiHandler.obtainMessage(CommonMsg.HANDLER_SYN, mHandler);
mUiHandler.sendMessage(msg);
//enter Looper loop, and never comeback
Looper.loop();
}
@Override
protected void loop() throws InterruptedException {
long time = System.currentTimeMillis();
if (time % 1000 == 0){
Log.i(TAG, "ros_node run again after 1s");
}
}
});
}
private void playMsg(String text){
if(!bSpeaking) {
int code = mTts.startSpeaking(text, mTtsListener);
if (code != ErrorCode.SUCCESS) {
Log.e(TAG, "語音合成失敗,錯誤碼: " + code);
}
}else{
Log.w(TAG, "語音合成繁忙,丟棄!");
}
}
@Override
public void onShutdown(Node arg0) {
try {
// 釋放連接
mTts.stopSpeaking();
mTts.destroy();
android.util.Log.i(TAG, "Synthesizer destroyed");
}catch (Throwable e) {
android.util.Log.e(TAG, "error when destroying synthesizer");
}
}
}
再看UI線程怎麼給node發消息
package org.ros.android.android_tutorial_pubsub;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import org.ros.android.RosActivity;
import org.ros.node.NodeConfiguration;
import org.ros.node.NodeMainExecutor;
import com.hankcs.hanlp.HanLP;
import java.util.List;
import java.util.Map;
/**
* @author [email protected] (Damon Kohler)
*/
public class MainActivity extends RosActivity {
private Button btnPlayMsg;
private static final String TAG = "MainRosActivity";
private Handler mSynHandler = null;
private SpeechSynNode speechSynthesizer;
private final Handler mHandler = new Handler(){
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case CommonMsg.HANDLER_SYN:{
mSynHandler = (Handler)msg.obj;
break;
}
default: {
assert false;
break;
}
}
};
};
public MainActivity() {
// The RosActivity constructor configures the notification title and ticker
// messages.
super("Robot", "Robot");
}
@SuppressWarnings("unchecked")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btnPlayMsg = (Button) findViewById(R.id.play_msg);
btnPlayMsg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (null != mSynHandler){
Message msg = mSynHandler.obtainMessage(CommonMsg.PLAY_MSG, "今天是個好日子");
mSynHandler.sendMessage(msg);
}
}
});
}
@Override
protected void init(NodeMainExecutor nodeMainExecutor) {
speechSynthesizer = new SpeechSynNode(this, mHandler, "speech_syn");
// At this point, the user has already been prompted to either enter the URI
// of a master to use or to start a master locally.
// The user can easily use the selected ROS Hostname in the master chooser
// activity.
NodeConfiguration nodeConfiguration = NodeConfiguration.newPublic(getRosHostname());
nodeConfiguration.setMasterUri(getMasterUri());
nodeMainExecutor.execute(speechSynthesizer, nodeConfiguration);
}
@Override
protected void onDestroy(){
super.onDestroy();
Log.i(TAG, "onDestroy");
nodeMainExecutorService.shutdownNodeMain(speechSynthesizer);
nodeMainExecutorService.forceShutdown();
}
}