戰(zhàn):從msg設(shè)計(jì)到話題觀測(cè))
這周調(diào)四輪差速底盤(pán)時(shí)我又被同一個(gè)問(wèn)題絆了一下輪速、電流、溫度、PWM這些狀態(tài)量到底用哪種ROS2話題消息發(fā)下游節(jié)點(diǎn)才看得最省心用Float32MultiArray當(dāng)然能發(fā)但下標(biāo)一多收的一方根本不知道第3個(gè)float是電流還是占空比。于是我把這套協(xié)議寫(xiě)成了自定義msg順手把ROS2自定義消息設(shè)計(jì)、功能包配置、發(fā)布訂閱實(shí)現(xiàn)和話題觀測(cè)的完整流程走了一遍。這篇記錄就是這次實(shí)戰(zhàn)的復(fù)盤(pán)適合剛?cè)腴T(mén)ROS2、被自定義消息繞暈的新手也適合想規(guī)范自己消息接口的中級(jí)開(kāi)發(fā)者。看完你能獨(dú)立完成一個(gè)msg功能包從設(shè)計(jì)字段到編譯驗(yàn)證再通過(guò)命令行和可視化工具把話題數(shù)據(jù)看得明明白白。1. 自定義消息的適用邊界什么時(shí)候該自己寫(xiě)msg什么時(shí)候直接用標(biāo)準(zhǔn)類型1.1 從底盤(pán)狀態(tài)發(fā)布需求說(shuō)起一個(gè)下標(biāo)引發(fā)的協(xié)議混亂先還原一下場(chǎng)景。我要把四個(gè)輪子的目標(biāo)轉(zhuǎn)速、實(shí)際轉(zhuǎn)速、電機(jī)溫度、母線電流、PWM占空比發(fā)出來(lái)每50ms一幀總共需要十幾二十個(gè)字段。第一反應(yīng)是用std_msgs/Float32MultiArray定義一個(gè)20維的數(shù)組。發(fā)布端攥著下標(biāo)一個(gè)values[3]xxx地賦值訂閱端也要靠下標(biāo)去猜一旦兩個(gè)版本對(duì)不上輕則讀錯(cuò)數(shù)據(jù)重則把溫度當(dāng)成了轉(zhuǎn)速去跑安全判斷。這已經(jīng)不是代碼風(fēng)格的問(wèn)題是數(shù)據(jù)安全隱患。用自定義msg之后完全不同msg里字段有名字、有類型、有注釋motor_id就是motor_idtemperature就是temperature。發(fā)布端寫(xiě)起來(lái)是msg.temperature45.2訂閱端一眼就知道這個(gè)數(shù)代表什么代碼可讀性和可維護(hù)性直接上了一個(gè)臺(tái)階。1.2 標(biāo)準(zhǔn)消息庫(kù)能覆蓋大多數(shù)場(chǎng)景但個(gè)性化協(xié)議還得自己來(lái)不是說(shuō)所有消息都要自定義ROS2發(fā)展到現(xiàn)在標(biāo)準(zhǔn)接口庫(kù)已經(jīng)非常全。我最常用的幾個(gè)標(biāo)準(zhǔn)消息類型適用場(chǎng)景std_msgs/String、Bool、Int32簡(jiǎn)單狀態(tài)量、開(kāi)關(guān)、日志字符串std_msgs/Header帶時(shí)間戳和frame_id的消息頭geometry_msgs/Pose、Twist、Transform位姿、速度、坐標(biāo)變換相關(guān)sensor_msgs/Image、LaserScan、Imu相機(jī)、激光雷達(dá)、IMU傳感器數(shù)據(jù)nav_msgs/Odometry里程計(jì)判斷是否要自定義消息我一般看三條這個(gè)數(shù)據(jù)結(jié)構(gòu)會(huì)不會(huì)在多個(gè)節(jié)點(diǎn)之間復(fù)用字段是否相對(duì)固定下游是否依賴字段名字做邏輯判斷如果三個(gè)答案都是“是”就值得寫(xiě)自定義msg。如果只是一次性臨時(shí)傳參沒(méi)人在乎字段名那用MultiArray之類的通用結(jié)構(gòu)也能湊合但別讓這種代碼活過(guò)一星期。還有一個(gè)重要權(quán)衡標(biāo)準(zhǔn)消息類型往往自帶工具鏈支持。sensor_msgs/Imu可以在rviz2里直接可視化nav_msgs/Odometry可以被導(dǎo)航棧直接消費(fèi)。自定義消息想讓外部工具看懂就得自己寫(xiě)插件或者靠Foxglove這類通用工具兜底。所以我的原則是能復(fù)用標(biāo)準(zhǔn)消息就別自創(chuàng)只有標(biāo)準(zhǔn)類型無(wú)法準(zhǔn)確表達(dá)業(yè)務(wù)語(yǔ)義時(shí)才自寫(xiě)。2. 從零創(chuàng)建一個(gè)獨(dú)立msg功能包依賴、文件配置與編譯鏈路2.1 為什么我建議把msg單獨(dú)放在一個(gè)功能包里項(xiàng)目一多你就知道消息接口和業(yè)務(wù)邏輯混裝在一個(gè)包里麻煩會(huì)接踵而至。A包要引用B包里的msgB包又要依賴A包的消息很容易出現(xiàn)循環(huán)依賴。更常見(jiàn)的是業(yè)務(wù)包更新頻繁消息包也跟著一遍遍重新編譯所有依賴它的節(jié)點(diǎn)全要連帶重編開(kāi)發(fā)效率被拖得很低。所以我現(xiàn)在只要項(xiàng)目里有自定義消息都會(huì)給它們單獨(dú)建一個(gè)功能包比如叫my_msgs或者robot_interfaces。命名上建議用“項(xiàng)目名interfaces”或者“項(xiàng)目名msgs”既清楚又符合社區(qū)習(xí)慣。功能包構(gòu)建類型用ament_cmake雖然消息也能在ament_python包里生成但CMake版對(duì)跨語(yǔ)言支持最省事Python和C節(jié)點(diǎn)都能直接消費(fèi)不用額外處理安裝路徑。2.2 創(chuàng)建功能包與編寫(xiě)MotorStatus.msg創(chuàng)建命令很簡(jiǎn)單cd ~/ros2_ws/src ros2 pkg create my_msgs --build-type ament_cmake mkdir my_msgs/msg然后在msg目錄下新建MotorStatus.msg內(nèi)容可以這樣設(shè)計(jì)# 電機(jī)狀態(tài)消息 int32 motor_id # 電機(jī)編號(hào) float32 speed # 當(dāng)前轉(zhuǎn)速 rad/s float32 target_speed # 目標(biāo)轉(zhuǎn)速 rad/s float32 current # 母線電流 A float32 temperature # 電機(jī)溫度 ℃ builtin_interfaces/Time stamp # 時(shí)間戳每一行就是一個(gè)字段格式是“類型 名字 # 注釋”。注釋會(huì)在ros2 interface show里顯示出來(lái)等于把設(shè)計(jì)文檔直接帶進(jìn)了工具鏈。2.3 CMakeLists.txt與package.xml里缺一不可的配置寫(xiě)好msg文件后最關(guān)鍵的配置在CMakeLists.txt和package.xml里。CMakeLists.txt中至少在ament_package()之前加find_package(rosidl_default_generators REQUIRED) find_package(builtin_interfaces REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} msg/MotorStatus.msg )package.xml里需要補(bǔ)齊這幾行buildtool_dependrosidl_default_generators/buildtool_depend exec_dependrosidl_default_runtime/exec_depend dependbuiltin_interfaces/depend member_of_grouprosidl_interface_packages/member_of_group這個(gè)member_of_group是最容易被忽略的一行。漏掉之后消息雖然能編譯出來(lái)但其他功能包可能沒(méi)法正常識(shí)別這個(gè)包提供的接口現(xiàn)象就是找不到類型、build依賴報(bào)錯(cuò)或者ros2 interface list里搜不到。2.4 編譯、source與接口驗(yàn)證缺一步都會(huì)讓你懷疑人生配置完成后回到工作空間根目錄編譯cd ~/ros2_ws colcon build --packages-select my_msgs source install/setup.bash然后驗(yàn)證接口是否被正確識(shí)別ros2 interface show my_msgs/msg/MotorStatus看到剛才寫(xiě)的字段說(shuō)明消息包已經(jīng)注冊(cè)到系統(tǒng)里了。這一步一定要做。如果不做就急著去寫(xiě)發(fā)布訂閱節(jié)點(diǎn)遇到import報(bào)錯(cuò)再回頭排查浪費(fèi)的時(shí)間遠(yuǎn)超你想象。3. msg字段設(shè)計(jì)原則類型選型、命名習(xí)慣與版本演進(jìn)3.1 字段類型怎么選從基本類型到時(shí)間和嵌套消息ROS2 msg的基本類型和大多數(shù)語(yǔ)言差不多bool、int8/16/32/64、uint8/16/32/64、float32、float64、string。需要注意兩點(diǎn)一是float32對(duì)應(yīng)C的floatPython則統(tǒng)一用float二是ROS2里時(shí)間戳字段推薦顯式寫(xiě)builtin_interfaces/TimeDuration同理用builtin_interfaces/Duration不要再用舊版本那種裸time/duration寫(xiě)法跨包依賴時(shí)容易出問(wèn)題。如果涉及坐標(biāo)、位姿等數(shù)據(jù)可以直接嵌套其他包的消息std_msgs/Header header geometry_msgs/Pose pose sensor_msgs/PointCloud2 cloud嵌套能極大復(fù)用已有生態(tài)下游拿到Pose后可以直接配合TF、rviz等工具使用。數(shù)組也很常用float32[] ranges表示變長(zhǎng)數(shù)組float32[4] wheel_speeds表示定長(zhǎng)數(shù)組。設(shè)計(jì)時(shí)想清楚業(yè)務(wù)需要的是定長(zhǎng)還是變長(zhǎng)定長(zhǎng)數(shù)組在內(nèi)存布局上更規(guī)整變長(zhǎng)數(shù)組更適合點(diǎn)云這類動(dòng)態(tài)數(shù)據(jù)。3.2 常量定義與注釋規(guī)范msg里允許定義常量用法和枚舉很像uint8 MODE_IDLE0 uint8 MODE_RUN1 uint8 MODE_FAULT2 uint8 mode下游判斷模式時(shí)不用寫(xiě)裸數(shù)字直接用MotorStatus.MODE_RUN代碼可讀性提高一大截。注釋用#建議把單位、范圍、異常值都寫(xiě)在注釋里ros2 interface show都能看到等于給協(xié)議做了可查詢的活文檔。我見(jiàn)過(guò)很多項(xiàng)目里msg文件干干凈凈沒(méi)有任何注釋過(guò)兩個(gè)月連自己都得猜字段含義這習(xí)慣真的得改。3.3 版本迭代時(shí)盡量向后兼容消息協(xié)議一旦在多個(gè)節(jié)點(diǎn)間流傳改動(dòng)就要格外謹(jǐn)慎。加一個(gè)字段所有發(fā)布端和訂閱端都更新基本沒(méi)問(wèn)題但刪字段、改類型哪怕只是int32改成float32都可能讓舊節(jié)點(diǎn)按錯(cuò)誤的字節(jié)序解析數(shù)據(jù)出現(xiàn)完全不正常的值。我的經(jīng)驗(yàn)是能加字段就加字段不要?jiǎng)h改舊字段如果實(shí)在要改語(yǔ)義就新增加一個(gè)字段舊字段保留并標(biāo)記為deprecated。另外msg功能包升級(jí)后依賴它的業(yè)務(wù)功能包必須重新編譯并source否則節(jié)點(diǎn)還在跑舊類型定義容易出現(xiàn)字段對(duì)不上、內(nèi)存解析錯(cuò)位的詭異問(wèn)題。4. 發(fā)布與訂閱自定義消息Python與C的最小可運(yùn)行實(shí)現(xiàn)4.1 Python發(fā)布端與訂閱端從import到publish只需要幾行Python端流程很清晰發(fā)布節(jié)點(diǎn)import rclpy from rclpy.node import Node from my_msgs.msg import MotorStatus class MotorPublisher(Node): def __init__(self): super().__init__(motor_status_publisher) self.publisher_ self.create_publisher(MotorStatus, motor_status, 10) self.timer self.create_timer(0.1, self.timer_callback) def timer_callback(self): msg MotorStatus() msg.motor_id 1 msg.speed 120.5 msg.target_speed 120.0 msg.current 3.2 msg.temperature 45.2 self.publisher_.publish(msg) def main(argsNone): rclpy.init(argsargs) node MotorPublisher() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()訂閱節(jié)點(diǎn)唯一需要注意的是回調(diào)函數(shù)只接收一個(gè)msg參數(shù)class MotorSubscriber(Node): def __init__(self): super().__init__(motor_status_subscriber) self.subscription self.create_subscription( MotorStatus, motor_status, self.listener_callback, 10) def listener_callback(self, msg): self.get_logger().info( fmotor_id{msg.motor_id} temp{msg.temperature:.2f})我自己項(xiàng)目里最容易犯的錯(cuò)是msg字段拼寫(xiě)和定義不一致比如定義的是target_speed寫(xiě)代碼時(shí)寫(xiě)成targetSpeedPython會(huì)在賦值時(shí)才報(bào)AttributeError一報(bào)錯(cuò)就先慌半天。其實(shí)翻一眼ros2 interface show就能對(duì)上排查成本極低。4.2 C實(shí)現(xiàn)要點(diǎn)蛇形頭文件與CMake鏈接C工程里消息頭文件是蛇形命名的my_msgs/msg/motor_status.hpp。寫(xiě)一個(gè)最小發(fā)布端#include rclcpp/rclcpp.hpp #include my_msgs/msg/motor_status.hpp using my_msgs::msg::MotorStatus; class MotorPublisher : public rclcpp::Node { public: MotorPublisher() : Node(motor_status_publisher) { publisher_ this-create_publisherMotorStatus(motor_status, 10); timer_ this-create_wall_timer(std::chrono::milliseconds(100), [this]() { auto msg std::make_sharedMotorStatus(); msg-motor_id 1; msg-speed 120.5; msg-target_speed 120.0; msg-current 3.2; msg-temperature 45.2; publisher_-publish(*msg); }); } private: rclcpp::PublisherMotorStatus::SharedPtr publisher_; rclcpp::TimerBase::SharedPtr timer_; };CMakeLists里要鏈接my_msgsfind_package(my_msgs REQUIRED) ament_target_dependencies(motor_pub rclcpp my_msgs)漏掉find_package(my_msgs REQUIRED)時(shí)編譯報(bào)錯(cuò)通常是找不到頭文件提示并不直觀容易讓人誤以為是路徑問(wèn)題。如果發(fā)現(xiàn)編譯一報(bào)錯(cuò)就是fatal error: my_msgs/msg/motor_status.hpp: No such file or directory九成是這句沒(méi)寫(xiě)。4.3 QoS匹配是topic echo沒(méi)數(shù)據(jù)最常見(jiàn)的元兇發(fā)布訂閱都寫(xiě)對(duì)了ros2 topic echo還是沒(méi)數(shù)據(jù)十有八九是QoS策略不兼容。簡(jiǎn)單說(shuō)QoS就是收發(fā)雙方對(duì)消息可靠性、時(shí)效性、歷史深度的約定。depth是隊(duì)列深度reliability分reliable和best_effort前者保證不丟包但可能延遲后者犧牲可靠性換低延遲。如果發(fā)布端是sensor_databest_effort訂閱端卻是默認(rèn)的reliable兩邊匹配不上數(shù)據(jù)就傳不過(guò)去。排查方法很直接ros2 topic info /motor_status -v這個(gè)命令會(huì)顯示發(fā)布者和訂閱者各自的QoS配置。如果reliability一側(cè)是Reliable另一側(cè)是Best Effort就需要在create_subscription里顯式指定QoS或者把發(fā)布端的QoS改成兼容版本。我用攝像頭數(shù)據(jù)時(shí)經(jīng)常遇到這個(gè)問(wèn)題而自己寫(xiě)的控制消息默認(rèn)reliable通常不太會(huì)踩坑但一旦踩了排查起來(lái)很隱蔽。5. 話題觀測(cè)的完整工具箱CLI、可視化工具與日志兜底5.1 ros2 topic系列命令list、echo、info、hz、bw一次說(shuō)清命令行是觀測(cè)話題的第一道門(mén)我基本每天都會(huì)用到這一組命令ros2 topic list # 列出所有話題 ros2 topic list -t # 列出話題及其消息類型 ros2 topic echo /motor_status # 持續(xù)打印話題數(shù)據(jù) ros2 topic echo /motor_status --once # 只打印一幀 ros2 topic info /motor_status -v # 查看發(fā)布訂閱數(shù)量和QoS ros2 topic hz /motor_status # 統(tǒng)計(jì)真實(shí)發(fā)布頻率 ros2 topic bw /motor_status # 統(tǒng)計(jì)話題帶寬占用 ros2 interface show my_msgs/msg/MotorStatus # 查看字段定義其中hz最實(shí)用。比如你設(shè)置了10Hz定時(shí)器但回調(diào)耗時(shí)太長(zhǎng)hz顯示出來(lái)只有2Hz不用猜就能定位是發(fā)布側(cè)卡了還是調(diào)度出了問(wèn)題。bw則在排查大消息點(diǎn)云、圖像占用帶寬時(shí)很有用。5.2 rqt_graph與rviz2可視化觀測(cè)能做到什么程度rqt_graph能把節(jié)點(diǎn)和話題關(guān)系畫(huà)成一張圖是理解系統(tǒng)拓?fù)渥钪庇^的工具rqt_graph看到節(jié)點(diǎn)間的箭頭就能快速判斷話題名和節(jié)點(diǎn)連接是否符合預(yù)期。如果某個(gè)話題的箭頭沒(méi)畫(huà)出來(lái)或者多了一個(gè)意料之外的節(jié)點(diǎn)連接通常說(shuō)明命名或邏輯有問(wèn)題。我在調(diào)多節(jié)點(diǎn)系統(tǒng)時(shí)幾乎每次都會(huì)先開(kāi)rqt_graph再?zèng)Q定下一步看哪里。rviz2對(duì)自定義消息的支持就有限了。像Pose、LaserScan、Imu這種標(biāo)準(zhǔn)消息rviz2有專門(mén)的Display插件可以直接可視化但MotorStatus這種純數(shù)值消息rviz2沒(méi)有默認(rèn)Display能顯示。我一般不在rviz2里看這種消息而是用Foxglove Studio。Foxglove對(duì)自定義消息支持得更好能直接以表格和曲線形式看任意字段非常適合電機(jī)狀態(tài)這類遙測(cè)數(shù)據(jù)。5.3 自寫(xiě)觀測(cè)節(jié)點(diǎn)和ros2 bag不依賴圖形界面的兜底方案如果目標(biāo)環(huán)境沒(méi)有圖形界面比如工控機(jī)或者Docker容器里命令行和自寫(xiě)節(jié)點(diǎn)就很重要。我個(gè)人的習(xí)慣是在每個(gè)業(yè)務(wù)包里留一個(gè)debug_sub節(jié)點(diǎn)用最簡(jiǎn)單的方式打印關(guān)鍵字段必要時(shí)還可以直接pdb.set_trace()進(jìn)入調(diào)試單幀數(shù)據(jù)就能停在那慢慢看。另一個(gè)被低估的工具是ros2 bagros2 bag record /motor_status ros2 bag play rosbag2_xxx現(xiàn)場(chǎng)測(cè)完一包數(shù)據(jù)回到工位回放配合topic echo和Foxglove慢慢分析。尤其是偶發(fā)異??咳搜鄱onsole根本盯不住錄包回放是唯一高效的辦法。這個(gè)習(xí)慣幫我解決過(guò)好幾次“現(xiàn)場(chǎng)復(fù)現(xiàn)不了”的疑難雜癥。6. 踩坑實(shí)錄自定義msg使用中最容易翻車(chē)的五個(gè)細(xì)節(jié)6.1 source順序和構(gòu)建緩存導(dǎo)致的消息類型“消失”最常見(jiàn)也最氣人的問(wèn)題明明已經(jīng)colcon build了ros2 interface show卻提示找不到類型。原因通常是當(dāng)前終端沒(méi)有source新生成的install/setup.bash或者之前source的是另一個(gè)工作空間的setup.bash。解決辦法是新開(kāi)終端cd ~/ros2_ws source install/setup.bash確保路徑里同時(shí)包含基礎(chǔ)ROS2環(huán)境和當(dāng)前工作空間。如果source沒(méi)問(wèn)題但還是找不到就得懷疑build/install目錄里的舊緩存了。這種時(shí)候我一般直接刪掉build和install目錄再重新build雖然慢一點(diǎn)但能把各種莫名其妙的舊文件問(wèn)題一次性清干凈。6.2 修改msg后調(diào)用方還在用舊字段我試過(guò)在MotorStatus里把current字段改名為bus_current所有被依賴的包都重新編譯了但跑起來(lái)的一個(gè)Python節(jié)點(diǎn)一直報(bào)AttributeError。原因是想當(dāng)然以為source了新環(huán)境就萬(wàn)事大吉實(shí)際上某些ros2 run是從舊終端啟動(dòng)的進(jìn)程還加載著舊環(huán)境變量或者install目錄里舊包沒(méi)被覆蓋。解決方法是全關(guān)終端重新source必要時(shí)把install目錄下對(duì)應(yīng)包的緩存刪掉再build。經(jīng)驗(yàn)就是當(dāng)你肉眼看到字段沒(méi)生效先殺終端進(jìn)程和舊節(jié)點(diǎn)而不是只編譯一次。6.3 消息包被多個(gè)業(yè)務(wù)包依賴時(shí)的構(gòu)建順序問(wèn)題當(dāng)my_msgs被底盤(pán)控制、導(dǎo)航、狀態(tài)機(jī)三個(gè)包同時(shí)依賴時(shí)colcon build的默認(rèn)順序一般沒(méi)問(wèn)題它會(huì)自己解析依賴。但如果只用了--packages-select去編譯某個(gè)業(yè)務(wù)包而my_msgs還沒(méi)編譯或沒(méi)source那結(jié)果一定是找不到頭文件或import報(bào)錯(cuò)。正確做法是用--packages-up-tocolcon build --packages-up-to motor_controller這個(gè)命令會(huì)把依賴鏈上游一起編譯而--packages-select只編譯指定包本身。這兩個(gè)參數(shù)的差別踩過(guò)坑的自然懂。6.4 echo有話題但數(shù)據(jù)為空或出現(xiàn)nan這個(gè)坑我踩得莫名其妙。發(fā)布端打印出來(lái)的msg里明明是正常數(shù)值但訂閱端收到后卻是nan或者0。后來(lái)發(fā)現(xiàn)是C代碼里創(chuàng)建了消息對(duì)象但沒(méi)有給字段賦值就發(fā)布某些字段的值取決于內(nèi)存初始狀態(tài)不一定是0。另一個(gè)常見(jiàn)情況是把float32字段定義成數(shù)組了或者數(shù)據(jù)結(jié)構(gòu)改了但解析代碼沒(méi)改。建議發(fā)布前先斷言一遍關(guān)鍵字段if (!std::isfinite(msg-speed)) return;這種防御式寫(xiě)法能把臟數(shù)據(jù)擋在源頭省得下游節(jié)點(diǎn)拿到異常值后像無(wú)頭蒼蠅一樣亂轉(zhuǎn)。6.5 話題名和消息類型對(duì)不上排查思路固定化還有一類錯(cuò)誤跟msg設(shè)計(jì)無(wú)關(guān)但特別容易在項(xiàng)目里反復(fù)出現(xiàn)發(fā)布的是/motor_status訂閱端監(jiān)聽(tīng)的是/motor_status/statusrqt_graph一看全是孤立節(jié)點(diǎn)。這種問(wèn)題排查思路可以固定下來(lái)先用ros2 topic list -t確認(rèn)當(dāng)前話題名單和類型再用ros2 topic info /topic -v確認(rèn)收發(fā)雙方都在最后再用rqt_graph看拓?fù)洹0催@個(gè)順序走一般十分鐘內(nèi)能定位。文末不放什么宏大結(jié)論只講兩個(gè)個(gè)人體會(huì)。第一個(gè)自定義msg并不是越早設(shè)計(jì)越好先在小范圍場(chǎng)景里跑通發(fā)布訂閱等字段趨于穩(wěn)定后再抽出獨(dú)立消息包會(huì)少改很多協(xié)議。第二個(gè)我在my_msgs里維護(hù)了一個(gè)protocol_design.md把每個(gè)字段的含義、單位、有效范圍都寫(xiě)清楚這個(gè)文件比代碼注釋有用十倍每次改協(xié)議先改文檔再改msg下游同事都能少問(wèn)很多問(wèn)題。如果你也在折騰自定義消息建議從最簡(jiǎn)單的MotorStatus這類狀態(tài)消息開(kāi)始先看完topic echo輸出再談其他。