在Ubuntu22.04下編譯ros2 humble的Android版本
前提
我一直想搗鼓一下機(jī)器人開發(fā),想著自己獨(dú)立去設(shè)計(jì)一款機(jī)器人,但了解到整個(gè)流程中所要學(xué)習(xí)的技能以及自己為數(shù)不多的業(yè)余時(shí)間,這事兒讓我覺得有點(diǎn)犯難。直到去年看到古月居發(fā)布了OriginBot智能小車,在看完它的整體資料后,我覺得它有以下幾個(gè)特點(diǎn)比較吸引我:
開源特性,基本上展現(xiàn)了如何從頭到尾去設(shè)計(jì)開發(fā)一款小型的機(jī)器人,這和我的初始想法非常吻合。
軟硬件的可拓展性,旭日X3派提升了它的可開發(fā)性,畢竟我不希望買它回來只是跑跑例程。
價(jià)格,在同類型的產(chǎn)品中它的價(jià)格比較親民。
在組裝好小車并跑了一些例程后,我覺得小車還缺少一個(gè)遙控器,畢竟不能一直端著筆記本電腦去控制小車吧,但是我又不想買手柄,于是我把目光投向了我身邊的那部沉睡已久的安卓手機(jī),想法來了,那就用手機(jī)去遙控小車吧。
其實(shí)用手機(jī)控制小車這也不是什么新鮮事,網(wǎng)上例子也很多,藍(lán)牙,WiFi都可以,雖然都是學(xué)習(xí),但是我也想整點(diǎn)不一樣的,后來查資料發(fā)現(xiàn)ROS2也有安卓的版本,這就正合我意了,而且有了ROS2也能做更多的事情了。
我其實(shí)并不會(huì)安卓開發(fā),也不會(huì)java,但是安卓開發(fā)作為一門很成熟的技術(shù),咱們程序員稍微學(xué)習(xí)一下,開發(fā)點(diǎn)簡單的應(yīng)用應(yīng)該沒什么問題。問題的關(guān)鍵是如何編譯ROS2的安卓版本,下面就來講講整個(gè)編譯過程以及我踩過的坑吧。
編譯過程
系統(tǒng):Ubuntu22.04
GitHub上的原項(xiàng)目:https://github.com/esteve/ros2_java
編譯的步驟基本和原項(xiàng)目一致,但是這個(gè)項(xiàng)目的版本比較老,有些步驟直接使用會(huì)報(bào)錯(cuò),所以經(jīng)過我踩坑后的步驟如下:
1、配置Android SDK
可以直接下載Android studio進(jìn)行配置,在初次啟動(dòng)Android studio時(shí)會(huì)提示用戶安裝必要的sdk和其他模塊,一般Ubuntu系統(tǒng)會(huì)安裝在用戶目錄下。
2、配置Android NDK
可以直接在Android studio中進(jìn)行下載配置,也可以前往NDK官網(wǎng)下載,下載后可以和SDK放在同一目錄,至于NDK的版本選最新的就可以。
3、設(shè)置環(huán)境變量
在.bashrc文件最后一排設(shè)置SDK和NDK的環(huán)境變量,如下
export?ANDROID_HOEM=~/Android/Sdk export?PATH=$PATH:$ANDROID_HOEM/tools:$ANDROID_HOEM/platform-tool export?ANDROID_NDK=~/Android/ndk-r25b
4、克隆ros2和ros2 java源碼
mkdir?-p?$HOME/ros2_android_ws/src cd?$HOME/ros2_android_ws curl?https://raw.githubusercontent.com/ros2-java/ros2_java/main/ros2_java_android.repos?|?vcs?import?src
原項(xiàng)目的版本是ros2 galactic,想要換成最新的長期支持版本humble也可以,需要將ros2_java_android.repos里面的galactic字樣修改為humble,其中Fast DDS的版本也可以更新為最新的版本。
我也整理了一份humble的repos:
curl?https://raw.githubusercontent.com/uglymie/ros2-humble-for-android/main/ros2_java_android.repos?|?vcs?import?src
此處要特別注意一個(gè)問題,因?yàn)楸卷?xiàng)目需要大量下載GitHub上的ROS相關(guān)模塊,所以要求電腦終端必須要能流暢的訪問GitHub,強(qiáng)調(diào)一下,這個(gè)是必要條件。
最終得到的文件目錄如下圖:
5、設(shè)置編譯配置
export?PYTHON3_EXEC="$(?which?python3?)" export?PYTHON3_LIBRARY="$(?${PYTHON3_EXEC}?-c?'import?os.path;?from?distutils?import?sysconfig;?print(os.path.realpath(os.path.join(sysconfig.get_config_var("LIBPL"),?sysconfig.get_config_var("LDLIBRARY"))))'?)" export?PYTHON3_INCLUDE_DIR="$(?${PYTHON3_EXEC}?-c?'from?distutils?import?sysconfig;?print(sysconfig.get_config_var("INCLUDEPY"))'?)" export?ANDROID_ABI=armeabi-v7a export?ANDROID_NATIVE_API_LEVEL=android-29 export?ANDROID_TOOLCHAIN_NAME=arm-linux-androideabi-clang
其中ANDROID相關(guān)選項(xiàng)可以更換,如
export?ANDROID_ABI=arm64-v8a export?ANDROID_NATIVE_API_LEVEL=android-29 export?ANDROID_TOOLCHAIN_NAME=aarch64-linux-android-clang
這里我們選擇 ANDROID_ABI 為arm64-v8a
6、編譯命令
colcon?build?\ ???--packages-ignore?cyclonedds?rcl_logging_log4cxx?rcl_logging_spdlog?rosidl_generator_py?rclandroid?ros2_talker_android?ros2_listener_android?\ ???--cmake-args?\ ???-DPYTHON_EXECUTABLE=${PYTHON3_EXEC}?\ ???-DPYTHON_LIBRARY=${PYTHON3_LIBRARY}?\ ???-DPYTHON_INCLUDE_DIR=${PYTHON3_INCLUDE_DIR}?\ ???-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}?\ ???-DANDROID=ON?\ ???-DANDROID_FUNCTION_LEVEL_LINKING=OFF?\ ???-DANDROID_NATIVE_API_LEVEL=${ANDROID_TARGET}?\ ???-DANDROID_TOOLCHAIN_NAME=${ANDROID_TOOLCHAIN_NAME}?\ ???-DANDROID_STL=c++_shared?\ ???-DANDROID_ABI=${ANDROID_ABI}?\ ???-DANDROID_NDK=${ANDROID_NDK}?\ ???-DTHIRDPARTY=ON??\ ???-DCOMPILE_EXAMPLES=OFF?\ ???-DCMAKE_FIND_ROOT_PATH="${PWD}/install"?\ ???-DBUILD_TESTING=OFF?\ ???-DRCL_LOGGING_IMPLEMENTATION=rcl_logging_noop?\ ???-DTHIRDPARTY_android-ifaddrs=FORCE
此編譯命令經(jīng)過多次問題排查更改,已經(jīng)可以避免大多數(shù)錯(cuò)誤。但是仍然可能出現(xiàn)其他錯(cuò)誤:
找不到j(luò)ni.h和jni_md.h
fatal?error:?jni.h:?No?such?file?or?directory
解決方法: 可以添加全局搜索路徑到 .bashrc 或者配置文件 /etc/profile 中,其中java-11-openjdk-amd64為當(dāng)前系統(tǒng)已經(jīng)安裝的jdk版本,CPATH關(guān)鍵字表示適用于所有語言。
export?CPATH=/usr/lib/jvm/java-11-openjdk-amd64/include:$CPATH export?CPATH=/usr/lib/jvm/java-11-openjdk-amd64/include/linux:$CPATH
編譯到Fast-DDS時(shí)可能出現(xiàn)找不到asio和tinyxml2的頭文件
解決方法: 可以在其目錄下(eProsima/Fast-DDS)的CMakeList.txt文件里添加包含頭文件路徑
include_directories(thirdparty/asio/asio/include) include_directories(thirdparty/tinyxml2)
打包過程
1、jar文件和so文件
編譯完成后在install目錄下會(huì)生成很多文件夾,其中包含了所有的jar文件和so文件,可以直接在目錄下搜索,如下圖:
可以將兩種文件分別拷貝并整理到單獨(dú)的文件目錄下,比如:
ros2-humble/arm64-v8a/jar ros2-humble/arm64-v8a/so
2.、根據(jù)需要加載庫文件
編譯好的jar文件和so文件包含了比較完整的ros常用功能包,以下是so文件:
可以看到共有1144個(gè)文件,事實(shí)上很多消息類的庫文件我們是按需所用,比如項(xiàng)目中只需要用到std_msgs,那么其他的消息類庫文件就可以不用再加載,如果加載所有的庫文件會(huì)使最終的apk文件變得比較大。
測(cè)試應(yīng)用程序
下面是一個(gè)測(cè)試APP的簡單說明,這個(gè)應(yīng)用也是我之前學(xué)習(xí)的時(shí)候在GitHub上找到的:https://github.com/YasuChiba/ros2-android-test-app
我覺得拿來作為測(cè)試用的應(yīng)用比較合適,當(dāng)然這個(gè)應(yīng)用使用的庫文件是ros2 galactic編譯好的,我將其替換成humble也是沒問題的。
測(cè)試APP主界面包括四個(gè)Button和一個(gè)TextView(空白處),如下圖:
要實(shí)現(xiàn)的功能也比較簡單,建立一個(gè)發(fā)布者和一個(gè)訂閱者,定時(shí)發(fā)布消息,其話題名稱為/chatter,消息類型為字符串,可以開始和暫停發(fā)布或訂閱。
Android相關(guān)代碼這里就不做說明,可以看上面完整的項(xiàng)目;下面看一下在java語言下的ros發(fā)布及訂閱節(jié)點(diǎn)的實(shí)現(xiàn)。
發(fā)布者節(jié)點(diǎn),TalkerNode.java:
package?com.example.ros2_android_test_app;import?java.util.concurrent.TimeUnit;import?android.util.Log;import?org.ros2.rcljava.node.BaseComposableNode;?//?引入ROS2節(jié)點(diǎn)相關(guān)庫import?org.ros2.rcljava.publisher.Publisher;?//?引入發(fā)布者相關(guān)庫import?org.ros2.rcljava.timer.WallTimer;?//?引入計(jì)時(shí)器相關(guān)庫public?class?TalkerNode?extends?BaseComposableNode?{private?static?String?logtag?=?TalkerNode.class.getName();private?final?String?topic;?//?定義節(jié)點(diǎn)發(fā)布的話題public?Publisher<std_msgs.msg.String>?publisher;?//?聲明發(fā)布者private?int?count;?//?定義計(jì)數(shù)器private?WallTimer?timer;?//?聲明計(jì)時(shí)器public?TalkerNode(final?String?name,?final?String?topic)?{ ????super(name); ????this.topic?=?topic; ????//?創(chuàng)建發(fā)布者 ????this.publisher?=?this.node.<std_msgs.msg.String>createPublisher( ????????????std_msgs.msg.String.class,?this.topic);}public?void?start()?{ ????Log.d(logtag,?"TalkerNode::start()"); ????if?(this.timer?!=?null)?{ ????????this.timer.cancel();?//?如果計(jì)時(shí)器已存在,取消計(jì)時(shí)器 ????} ????this.count?=?0;?//?將計(jì)數(shù)器歸零 ????//?創(chuàng)建計(jì)時(shí)器,每500毫秒執(zhí)行一次onTimer函數(shù) ????this.timer?=?node.createWallTimer(500,?TimeUnit.MILLISECONDS,?this::onTimer);}private?void?onTimer()?{ ????std_msgs.msg.String?msg?=?new?std_msgs.msg.String();?//?創(chuàng)建消息對(duì)象 ????msg.setData("Hello!?ROS2?Humble!?"?+?this.count);?//?設(shè)置消息內(nèi)容 ????this.count++;?//?計(jì)數(shù)器自增 ????this.publisher.publish(msg);?//?發(fā)布消息}public?void?stop()?{ ????Log.d(logtag,?"TalkerNode::stop()"); ????if?(this.timer?!=?null)?{ ????????this.timer.cancel();?//?如果計(jì)時(shí)器已存在,取消計(jì)時(shí)器 ????}}
訂閱者節(jié)點(diǎn),ListenerNode.java:
package?com.example.ros2_android_test_app;import?android.util.Log;import?android.widget.TextView;import?org.ros2.rcljava.node.BaseComposableNode;import?org.ros2.rcljava.subscription.Subscription;public?class?ListenerNode?extends?BaseComposableNode?{ ????private?final?String?topic;???//?訂閱的?ROS2?消息主題名稱 ????private?final?TextView?listenerView;??//?用于在?Android?UI?上顯示消息內(nèi)容的?TextView?控件 ????private?Subscription<std_msgs.msg.String>?subscriber;??//?ROS2?訂閱者對(duì)象,用于接收消息 ????public?ListenerNode(final?String?name,?final?String?topic, ????????????????????????final?TextView?listenerView)?{ ????????super(name);??//?調(diào)用父類?BaseComposableNode?的構(gòu)造方法,傳入節(jié)點(diǎn)名稱 ????????this.topic?=?topic;??//?保存訂閱的主題名稱 ????????this.listenerView?=?listenerView;??//?保存用于顯示消息內(nèi)容的?TextView?控件 ????????//?創(chuàng)建?ROS2?訂閱者對(duì)象,訂閱指定主題的?std_msgs/String?類型的消息 ????????this.subscriber?=?this.node.<std_msgs.msg.String>createSubscription( ????????????????std_msgs.msg.String.class,?this.topic,?msg????????????????????????->?{ ????????????????????//?當(dāng)接收到新消息時(shí),將其內(nèi)容顯示在?TextView?控件上 ????????????????????this.listenerView.setText("Hello?ROS2?from?Android:?"?+?msg.getData()?+ ????????????????????????????"\r\n"?+?listenerView.getText()); ????????????????}); ????}}
用于管理 ROS 的執(zhí)行器(Executor)和在 Android 設(shè)備上運(yùn)行的 ROS 節(jié)點(diǎn),ROSActivity.java:
package?com.example.hyperbot;import?android.os.Bundle;import?android.os.Handler;import?androidx.appcompat.app.AppCompatActivity;import?org.ros2.rcljava.RCLJava;import?org.ros2.rcljava.executors.Executor;import?org.ros2.rcljava.executors.SingleThreadedExecutor;import?java.util.Timer;import?java.util.TimerTask;public?class?ROSActivity?extends?AppCompatActivity?{ ????private?Executor?rosExecutor;??//?ROS2?執(zhí)行器對(duì)象,用于處理節(jié)點(diǎn)的消息 ????private?Timer?timer;??//?定時(shí)器對(duì)象,定時(shí)執(zhí)行節(jié)點(diǎn)的?spinSome()?方法 ????private?Handler?handler;??//?Android?UI?線程的?Handler?對(duì)象,用于在?UI?線程上執(zhí)行節(jié)點(diǎn)的?spinSome()?方法 ????private?static?String?logtag?=?ROSActivity.class.getName(); ????private?static?long?SPINNER_DELAY?=?0;??//?定時(shí)器的啟動(dòng)延遲時(shí)間(單位:毫秒) ????private?static?long?SPINNER_PERIOD_MS?=?200;??//?定時(shí)器的周期時(shí)間(單位:毫秒) ????//?生命周期方法,當(dāng)活動(dòng)第一次創(chuàng)建時(shí)調(diào)用 ????@Override ????public?void?onCreate(final?Bundle?savedInstanceState)?{ ????????super.onCreate(savedInstanceState); ????????this.handler?=?new?Handler(getMainLooper());??//?創(chuàng)建?Android?UI?線程的?Handler?對(duì)象 ????????RCLJava.rclJavaInit();??//?初始化?RCLJava?庫 ????????this.rosExecutor?=?this.createExecutor();??//?創(chuàng)建?ROS2?執(zhí)行器對(duì)象 ????} ????//?生命周期方法,當(dāng)活動(dòng)從暫停狀態(tài)恢復(fù)時(shí)調(diào)用 ????@Override ????protected?void?onResume()?{ ????????super.onResume(); ????????timer?=?new?Timer();??//?創(chuàng)建定時(shí)器對(duì)象 ????????timer.scheduleAtFixedRate(new?TimerTask()?{ ????????????public?void?run()?{ ????????????????Runnable?runnable?=?new?Runnable()?{ ????????????????????public?void?run()?{ ????????????????????????rosExecutor.spinSome();??//?在?UI?線程上執(zhí)行節(jié)點(diǎn)的?spinSome()?方法 ????????????????????} ????????????????}; ????????????????handler.post(runnable);??//?將?Runnable?對(duì)象提交到?UI?線程的消息隊(duì)列中,?避免在子線程中更新UI ????????????} ????????},?this.getDelay(),?this.getPeriod());??//?啟動(dòng)定時(shí)器,定時(shí)執(zhí)行節(jié)點(diǎn)的?spinSome()?方法 ????} ????//?生命周期方法,當(dāng)活動(dòng)暫停時(shí)調(diào)用 ????@Override ????protected?void?onPause()?{ ????????super.onPause(); ????????if?(timer?!=?null)?{ ????????????timer.cancel();??//?取消定時(shí)器的執(zhí)行 ????????} ????} ????public?void?run()?{ ????????rosExecutor.spinSome();??//?執(zhí)行節(jié)點(diǎn)的?spinSome()?方法 ????} ????public?Executor?getExecutor()?{ ????????return?this.rosExecutor;??//?獲取?ROS2?執(zhí)行器對(duì)象 ????} ????protected?Executor?createExecutor()?{ ????????return?new?SingleThreadedExecutor();??//?創(chuàng)建單線程的?ROS2?執(zhí)行器對(duì)象 ????} ????protected?long?getDelay()?{ ????????return?SPINNER_DELAY;??//?獲取定時(shí)器的啟動(dòng)延遲時(shí)間 ????} ????protected?long?getPeriod()?{ ????????return?SPINNER_PERIOD_MS;??//?獲取定時(shí)器的周期時(shí)間 ????}}
最后在MainActivity中創(chuàng)建新的發(fā)布和訂閱者節(jié)點(diǎn)對(duì)象,并將其添加到執(zhí)行器:
listenerNode?=?new?ListenerNode("ros2_humble_node_listener",?"/chatter",?listenerView);talkerNode?=?new?TalkerNode("ros2_humble_node_talker",?"/chatter");getExecutor().addNode(listenerNode);getExecutor().addNode(TalkerNode);
最后的運(yùn)行效果如下:
將手機(jī)和電腦連接到同一網(wǎng)絡(luò)下,可以在終端中看到手機(jī)端發(fā)布的話題及消息,如下圖:
至此,關(guān)于ros2 humble的Android版本的編譯以及測(cè)試到此結(jié)束;下一篇來聊聊如何開發(fā)APP來控制OriginBot的運(yùn)動(dòng),敬請(qǐng)期待。