Tbot 3D 导航底盘 SDK 开发文档

基于 rosbridge WebSocket 协议,无需 ROS 环境即可跨语言控制底盘。本文档完整列出每个 Topic / Service 的请求字段与响应字段(类型 + 说明)。

连接:ws://192.168.1.151:9090
通用响应包络:所有 Service 调用后,rosbridge 回一条 {"op":"service_response","service":"/xxx","values":{...},"result":true}。下表"响应"里的 JSON 即 values 内容。

一、协议概览

方向数量名称
订阅 Topic(底盘 → 应用)5/fast_lio_localization、/aft_pgo_path、/map_list_kitty、/ebot_goal_status_local_planner、/ebot_work_state
调用 Service(应用 → 底盘)9/start_mapping_kitty、/save_map_kitty、/select_map_and_start_kitty、/delete_map_kitty、/docking_kitty、/navigate_to_pose(导航+取消)、/add_goal_kitty、/delete_goal_kitty

二、Topic 订阅(底盘 → 应用)

用 {"op":"subscribe","topic":"/xxx","type":"消息类型"} 订阅。收到的消息在 msg 字段。

三、Service 汇总表

Service服务类型主要字段
/start_mapping_kittystd_srvs/Trigger空请求 → success,message
/save_map_kittykitty_runtime_manager/SaveMapfilepath → success,message
/select_map_and_start_kittykitty_runtime_manager/SaveMapfilepath → success,message
/delete_map_kittykitty_runtime_manager/DeleteMapmap_name → success,message
/docking_kittystd_srvs/Trigger空请求 → success,message
/navigate_to_posenav_msgs/GetPlangoal.pose.position.x/y/z → plan.poses[]
/add_goal_kittykitty_runtime_manager/AddGoalmap_name,goal_name,x,y,theta → success,message
/delete_goal_kittykitty_runtime_manager/DeleteGoalmap_name,goal_name → success,message

2.1 机器人定位 nav_msgs/Odometry

Topic:/fast_lio_localization,底盘定时推送当前位姿。

收到的 msg:
{
  "pose": { "pose": {
    "position": {"x": 1.23, "y": 4.56, "z": 0.0},
    "orientation": {"x": 0, "y": 0, "z": 0.5, "w": 0.866}
  }}
}
字段类型说明
pose.pose.position.xfloat64机器人 x 坐标(米)
pose.pose.position.yfloat64机器人 y 坐标(米)
pose.pose.position.zfloat64机器人 z 坐标(米,底盘固定 0)
pose.pose.orientation.x/y/z/wfloat64四元数朝向;前端用 yaw = atan2(2*(w*z+x*y), 1-2*(z*z+y*y)) 转弧度角

2.2 全局路径 nav_msgs/Path

Topic:/aft_pgo_path。启动导航后约 5 秒发一次,共发 5 次(约 30 秒)。

收到的 msg:
{ "poses": [
  {"pose":{"position":{"x":0,"y":0,"z":0}}},
  {"pose":{"position":{"x":1,"y":2,"z":0}}}
]}
字段类型说明
posesarray[PoseStamped]规划路径点数组
poses[].pose.position.xfloat64路径点 x 坐标(米)
poses[].pose.position.yfloat64路径点 y 坐标(米)
poses[].pose.position.zfloat64路径点 z 坐标(底盘恒为 0)

2.3 地图列表 kitty_runtime_manager/MapList

Topic:/map_list_kitty。订阅后返回所有地图及其已保存目标点。

收到的 msg:
{ "maps": [
  {"map_name":"Lobby","map_path":"/maps/floor1",
   "goals":[
     {"name":"Reception","x":1.0,"y":2.0,"theta":0.5},
     {"name":"电梯口","x":3.0,"y":4.0,"theta":1.2}
   ]}
]}
字段类型说明
mapsarray[Map]地图数组
maps[].map_namestring地图显示名称
maps[].map_pathstring地图文件路径(内部用)
maps[].goalsarray[Goal]该地图下已保存目标点
maps[].goals[].namestring目标点名称
maps[].goals[].xfloat64目标点 x(米)
maps[].goals[].yfloat64目标点 y(米)
maps[].goals[].thetafloat64目标点朝向(弧度)

2.4 导航状态 std_msgs/Float64MultiArray

Topic:/ebot_goal_status_local_planner。

收到的 msg:{"data": [1]}
字段类型说明
data[0]float641 = Arrived at goal;0 = 未到达。Docking场景下只有真正充上电才发 1

2.5 电量与充电状态 std_msgs/Float64MultiArray

Topic:/ebot_work_state。

收到的 msg:{"data": [2, 75.5]}
字段类型说明
data[0]float64充电状态:1 = Charging,2 = Not charging
data[1]float64电量百分比(0~100)

3.1 开始建图 std_srvs/Trigger

Service:/start_mapping_kitty

请求:
{}
(空请求)
响应 values:
{ "success": true, "message": "Mapping started" }
字段类型方向说明
successbool响应操作是否成功
messagestring响应结果信息(失败时给原因)

3.2 保存地图 kitty_runtime_manager/SaveMap

Service:/save_map_kitty

请求:
{ "filepath": "map_name" }
响应 values:
{ "success": true, "message": "Map saved" }
字段类型方向说明
filepathstring请求map_name(不要带路径)
successbool响应操作是否成功
messagestring响应结果信息

3.3 选择地图并启动导航 kitty_runtime_manager/SaveMap

Service:/select_map_and_start_kitty

请求:
{ "filepath": "map_name" }
响应 values:
{ "success": true, "message": "Map selected and navigation started" }
字段类型方向说明
filepathstring请求map_name(不要带路径)
successbool响应操作是否成功
messagestring响应结果信息

3.4 删除地图 kitty_runtime_manager/DeleteMap

Service:/delete_map_kitty

请求:
{ "map_name": "map_name" }
响应 values:
{ "success": true, "message": "Deleted successfully" }
字段类型方向说明
map_namestring请求要删除的map_name
successbool响应操作是否成功
messagestring响应结果信息

3.5 回充电桩 std_srvs/Trigger

Service:/docking_kitty

请求:
{}
响应 values:
{ "success": true, "message": "Docking started" }
字段类型方向说明
successbool响应操作是否成功(只表示Docking流程已启动,不代表已充上电)
messagestring响应结果信息

3.6 导航到目标点 nav_msgs/GetPlan

Service:/navigate_to_pose

请求:
{
  "goal": { "pose": { "position": { "x": 1.0, "y": 2.0, "z": 0.0 } } }
}
响应 values:
{ "plan": { "poses": [
  {"pose":{"position":{"x":0,"y":0,"z":0}}},
  {"pose":{"position":{"x":1,"y":2,"z":0}}}
]}}
字段类型方向说明
goal.pose.position.xfloat64请求目标点 x(米)
goal.pose.position.yfloat64请求目标点 y(米)
goal.pose.position.zfloat64请求目标点 z(底盘恒传 0)
plan.posesarray[PoseStamped]响应规划出的路径点数组(可前端画路径)
注意:是否真的到达,不要看本响应,要订阅 /ebot_goal_status_local_planner 收到 data[0]==1。

3.7 取消导航 nav_msgs/GetPlan

Service:/navigate_to_pose(与导航同服务,特殊坐标区分)

请求:
{
  "goal": { "pose": { "position": { "x": 999999, "y": 888888, "z": 0 } } }
}
响应 values:
{ "plan": { "poses": [] } }
字段类型方向说明
goal.pose.position.xfloat64请求固定 999999(取消标志)
goal.pose.position.yfloat64请求固定 888888(取消标志)
goal.pose.position.zfloat64请求固定 0
plan.posesarray响应空或截断路径

3.8 添加目标点 kitty_runtime_manager/AddGoal

Service:/add_goal_kitty

请求:
{
  "map_name": "Lobby",
  "goal_name": "Reception",
  "x": 1.0, "y": 2.0, "theta": 0.5
}
响应 values:
{ "success": true, "message": "Added successfully" }
字段类型方向说明
map_namestring请求map_name
goal_namestring请求目标点名称
xfloat64请求目标点 x(米)
yfloat64请求目标点 y(米)
thetafloat64请求朝向角(弧度)
successbool响应操作是否成功
messagestring响应结果信息

3.9 删除目标点 kitty_runtime_manager/DeleteGoal

Service:/delete_goal_kitty

请求:
{ "map_name": "Lobby", "goal_name": "Reception" }
响应 values:
{ "success": true, "message": "Deleted successfully" }
字段类型方向说明
map_namestring请求map_name
goal_namestring请求目标点名称
successbool响应操作是否成功
messagestring响应结果信息

四、多语言开发示例

每种语言默认显示快速示例(连接 + 订阅Battery + Docking),点击右侧「查看完整代码」可展开覆盖全部 5 个 Topic + 9 个 Service 的完整可运行 demo(含每个 Topic 响应字段解析、每个 Service 响应 success/message/plan 字段读取)。

4.1 Python(roslibpy)

依赖:pip install roslibpy

import roslibpy, time
client = roslibpy.Ros(host='192.168.1.151', port=9090)
client.run()

# Subscribe to battery state
roslibpy.Topic(client,'/ebot_work_state','std_msgs/Float64MultiArray').subscribe(
    lambda m: print(f"Battery {m['data'][1]}% charging={m['data'][0]==1}"))
# Dock to charger
roslibpy.Service(client,'/docking_kitty','std_srvs/Trigger').call(roslibpy.ServiceRequest(),
    callback=lambda r: print("Docking:", r['message']))
try:
    while True: time.sleep(1)
except KeyboardInterrupt: client.terminate()
import roslibpy, time, math
client = roslibpy.Ros(host='192.168.1.151', port=9090)
client.run()

# ===== Subscribe to all 5 Topics =====
# 1) Localization
def on_odom(m):
    p = m['pose']['pose']['position']; o = m['pose']['pose']['orientation']
    yaw = math.atan2(2*(o['w']*o['z']+o['x']*o['y']), 1-2*(o['z']**2+o['y']**2))
    print(f"[Pose] x={p['x']:.2f} y={p['y']:.2f} yaw={math.degrees(yaw):.1f}°")
roslibpy.Topic(client,'/fast_lio_localization','nav_msgs/Odometry').subscribe(on_odom)
# 2) Global path
roslibpy.Topic(client,'/aft_pgo_path','nav_msgs/Path').subscribe(
    lambda m: print(f"[Path] {len(m['poses'])}  points"))
# 3) Map list
roslibpy.Topic(client,'/map_list_kitty','kitty_runtime_manager/MapList').subscribe(
    lambda m: [print(f"[Map] {mp['map_name']}  goals:{[g['name'] for g in mp['goals']]}") for mp in m['maps']])
# 4) Navigation status
roslibpy.Topic(client,'/ebot_goal_status_local_planner','std_msgs/Float64MultiArray').subscribe(
    lambda m: m['data'][0]==1 and print("✅ Arrived"))
# 5) Battery
roslibpy.Topic(client,'/ebot_work_state','std_msgs/Float64MultiArray').subscribe(
    lambda m: print(f"[Battery] {m['data'][1]:.0f}% {'Charging' if m['data'][0]==1 else 'Not charging'}"))

# ===== Call all 9 Services =====
def call(svc, args, srv):
    r = roslibpy.Service(client, svc, srv).call(roslibpy.ServiceRequest(args))
    print(f"[SVC] {svc} → {dict(r)}")
time.sleep(1)
call('/start_mapping_kitty', {}, 'std_srvs/Trigger')
call('/save_map_kitty', {'filepath':'test_map'}, 'kitty_runtime_manager/SaveMap')
call('/select_map_and_start_kitty', {'filepath':'test_map'}, 'kitty_runtime_manager/SaveMap')
call('/navigate_to_pose', {'goal':{'pose':{'position':{'x':1.0,'y':2.0,'z':0.0}}}}, 'nav_msgs/GetPlan')
call('/navigate_to_pose', {'goal':{'pose':{'position':{'x':999999,'y':888888,'z':0}}}}, 'nav_msgs/GetPlan')
call('/add_goal_kitty', {'map_name':'test_map','goal_name':'Reception','x':1.0,'y':2.0,'theta':0.5}, 'kitty_runtime_manager/AddGoal')
call('/delete_goal_kitty', {'map_name':'test_map','goal_name':'Reception'}, 'kitty_runtime_manager/DeleteGoal')
call('/docking_kitty', {}, 'std_srvs/Trigger')
call('/delete_map_kitty', {'map_name':'test_map'}, 'kitty_runtime_manager/DeleteMap')

try:
    while True: time.sleep(1)
except KeyboardInterrupt: client.terminate()

4.2 JavaScript / HTML

无需依赖,粘贴到 HTML <script> 即可

const ws = new WebSocket('ws://192.168.1.151:9090');
ws.onopen = () => {
  ws.send(JSON.stringify({op:'subscribe',topic:'/ebot_work_state',type:'std_msgs/Float64MultiArray'}));
  ws.send(JSON.stringify({op:'call_service',service:'/docking_kitty',args:{}}));
};
ws.onmessage = e => {
  const m = JSON.parse(e.data);
  if (m.op==='publish' && m.topic==='/ebot_work_state')
    console.log('[Battery]', m.msg.data[1]+'%', m.msg.data[0]===1?'充电':'Not charging');
  if (m.op==='service_response')
    console.log('[SVC]', m.service, '→', m.values);
};
const ws = new WebSocket('ws://192.168.1.151:9090');
ws.onopen = () => {
  // Subscribe to all 5 Topics
  sub('/fast_lio_localization','nav_msgs/Odometry');
  sub('/aft_pgo_path','nav_msgs/Path');
  sub('/map_list_kitty','kitty_runtime_manager/MapList');
  sub('/ebot_goal_status_local_planner','std_msgs/Float64MultiArray');
  sub('/ebot_work_state','std_msgs/Float64MultiArray');
  // Call all 9 Services
  svc('/start_mapping_kitty', {});
  svc('/save_map_kitty', {filepath:'test_map'});
  svc('/select_map_and_start_kitty', {filepath:'test_map'});
  svc('/navigate_to_pose', {goal:{pose:{position:{x:1,y:2,z:0}}}});
  svc('/navigate_to_pose', {goal:{pose:{position:{x:999999,y:888888,z:0}}}});
  svc('/add_goal_kitty', {map_name:'test_map',goal_name:'Reception',x:1,y:2,theta:0.5});
  svc('/delete_goal_kitty', {map_name:'test_map',goal_name:'Reception'});
  svc('/docking_kitty', {});
  svc('/delete_map_kitty', {map_name:'test_map'});
};
function sub(t,type){ws.send(JSON.stringify({op:'subscribe',topic:t,type}));}
function svc(n,args){ws.send(JSON.stringify({op:'call_service',service:n,args}));}
ws.onmessage = e => {
  const m = JSON.parse(e.data);
  if (m.op==='publish') {
    if (m.topic==='/ebot_work_state')
      console.log('[Battery]', m.msg.data[1]+'%', m.msg.data[0]===1?'充电':'Not charging');
    else if (m.topic==='/ebot_goal_status_local_planner' && m.msg.data[0]===1)
      console.log('✅ Arrived');
    else if (m.topic==='/fast_lio_localization') {
      const p = m.msg.pose.pose.position;
      console.log('[Pose]', p.x.toFixed(2), p.y.toFixed(2));
    }
  }
  if (m.op==='service_response')
    console.log('[SVC]', m.service, '→', m.values);
};

4.3 C++

依赖:vcpkg install websocketpp nlohmann-json

#include <websocketpp/client.hpp>
#include <websocketpp/config/asio_client.hpp>
#include <nlohmann/json.hpp>
#include <iostream>
using json=nlohmann::json;
typedef websocketpp::client<websocketpp::config::asio_client> WSC;
int main(){
  WSC c; c.init_asio();
  c.set_message_handler([](auto, WSC::message_ptr m){
    auto j=json::parse(m->get_payload());
    if(j["op"]=="publish" && j["topic"]=="/ebot_work_state")
      std::cout<<"[Battery] "<<j["msg"]["data"][1]<<"%\n";
  });
  c.set_open_handler([&c](auto h){
    c.send(h, R"({"op":"call_service","service":"/docking_kitty","args":{}})",
           websocketpp::frame::opcode::text);
  });
  c.connect(c.get_connection("ws://192.168.1.151:9090").lock());
  c.run();
}
#include <websocketpp/client.hpp>
#include <websocketpp/config/asio_client.hpp>
#include <nlohmann/json.hpp>
#include <iostream>
using json=nlohmann::json;
typedef websocketpp::client<websocketpp::config::asio_client> WSC;
int main(){
  WSC c; c.init_asio();
  // Unified message handler: parse all Topic pushes and Service responses
  c.set_message_handler([](auto, WSC::message_ptr m){
    auto j=json::parse(m->get_payload());
    if(j["op"]=="publish"){
      std::string t=j["topic"];
      if(t=="/fast_lio_localization"){          // Localization
        auto p=j["msg"]["pose"]["pose"]["position"];
        auto o=j["msg"]["pose"]["pose"]["orientation"];
        std::cout<<"[Pose] x="<<p["x"]<<" y="<<p["y"]
                 <<" qw="<<o["w"]<<"\n";
      } else if(t=="/aft_pgo_path"){             // Global path
        std::cout<<"[Path] "<<j["msg"]["poses"].size()<<"  points\n";
      } else if(t=="/map_list_kitty"){           // Map list
        for(auto& mp: j["msg"]["maps"])
          std::cout<<"[Map] "<<mp["map_name"]<<"  goals_count="<<mp["goals"].size()<<"\n";
      } else if(t=="/ebot_goal_status_local_planner"){  // Arrival status
        if(j["msg"]["data"][0]==1) std::cout<<"✅ Arrived at goal\n";
      } else if(t=="/ebot_work_state"){          // Battery
        std::cout<<"[Battery] "<<j["msg"]["data"][1]<<"%"
                 <<(j["msg"]["data"][0]==1?" Charging":" Not charging")<<"\n";
      }
    } else if(j["op"]=="service_response"){     // Service response
      auto& v=j["values"];
      std::cout<<"[SVC] "<<j["service"]
               <<" success="<<v.value("success",false)
               <<" msg="<<v.value("message","")<<"\n";
    }
  });
  c.set_open_handler([&c](auto h){
    auto send=[&](json j){c.send(h,j.dump(),websocketpp::frame::opcode::text);};
    // Subscribe to 5 Topics
    send({{"op","subscribe"},{"topic","/fast_lio_localization"},{"type","nav_msgs/Odometry"}});
    send({{"op","subscribe"},{"topic","/aft_pgo_path"},{"type","nav_msgs/Path"}});
    send({{"op","subscribe"},{"topic","/map_list_kitty"},{"type","kitty_runtime_manager/MapList"}});
    send({{"op","subscribe"},{"topic","/ebot_goal_status_local_planner"},{"type","std_msgs/Float64MultiArray"}});
    send({{"op","subscribe"},{"topic","/ebot_work_state"},{"type","std_msgs/Float64MultiArray"}});
    // Call 9 Services
    send({{"op","call_service"},{"service","/start_mapping_kitty"},{"args",json::object()}});
    send({{"op","call_service"},{"service","/save_map_kitty"},{"args",{{"filepath","test_map"}}}});
    send({{"op","call_service"},{"service","/select_map_and_start_kitty"},{"args",{{"filepath","test_map"}}}});
    send({{"op","call_service"},{"service","/navigate_to_pose"},{"args",{{"goal",{{"pose",{{"position",{{"x",1.0},{"y",2.0},{"z",0.0}}}}}}}}}});
    send({{"op","call_service"},{"service","/navigate_to_pose"},{"args",{{"goal",{{"pose",{{"position",{{"x",999999.0},{"y",888888.0},{"z",0.0}}}}}}}}}});
    send({{"op","call_service"},{"service","/add_goal_kitty"},{"args",{{"map_name","test_map"},{"goal_name","front"},{"x",1.0},{"y",2.0},{"theta",0.5}}}});
    send({{"op","call_service"},{"service","/delete_goal_kitty"},{"args",{{"map_name","test_map"},{"goal_name","front"}}}});
    send({{"op","call_service"},{"service","/docking_kitty"},{"args",json::object()}});
    send({{"op","call_service"},{"service","/delete_map_kitty"},{"args",{{"map_name","test_map"}}}});
  });
  c.connect(c.get_connection("ws://192.168.1.151:9090").lock());
  c.run();
}

4.4 C(libwebsockets + cJSON)

依赖:libwebsockets、cJSON

#include <libwebsockets.h>
#include <cjson/cJSON.h>
#include <string.h>
static int cb(struct lws*wsi, enum lws_callback_reasons r, void*, void*in, size_t n){
  if(r==LWS_CALLBACK_CLIENT_ESTABLISHED)
    lws_write(wsi, (unsigned char*)"", 0, LWS_TEXT); // see full version
  if(r==LWS_CALLBACK_CLIENT_RECEIVE) printf("recv: %.*s\n",(int)n,(char*)in);
  return 0;
}
#include <libwebsockets.h>
#include <cjson/cJSON.h>
#include <string.h>
// Send JSON to rosbridge
static void send_json(struct lws*wsi,const char*s){
  unsigned char buf[LWS_PRE+1024];
  memcpy(&buf[LWS_PRE],s,strlen(s));
  lws_write(wsi,&buf[LWS_PRE],strlen(s),LWS_WRITE_TEXT);
}
static int cb(struct lws*wsi, enum lws_callback_reasons r, void*u, void*in, size_t n){
  if(r==LWS_CALLBACK_CLIENT_ESTABLISHED){
    // ---- Subscribe to 5 Topics ----
    send_json(wsi,"{\"op\":\"subscribe\",\"topic\":\"/fast_lio_localization\",\"type\":\"nav_msgs/Odometry\"}");
    send_json(wsi,"{\"op\":\"subscribe\",\"topic\":\"/aft_pgo_path\",\"type\":\"nav_msgs/Path\"}");
    send_json(wsi,"{\"op\":\"subscribe\",\"topic\":\"/map_list_kitty\",\"type\":\"kitty_runtime_manager/MapList\"}");
    send_json(wsi,"{\"op\":\"subscribe\",\"topic\":\"/ebot_goal_status_local_planner\",\"type\":\"std_msgs/Float64MultiArray\"}");
    send_json(wsi,"{\"op\":\"subscribe\",\"topic\":\"/ebot_work_state\",\"type\":\"std_msgs/Float64MultiArray\"}");
    // ---- Call 9 Services ----
    send_json(wsi,"{\"op\":\"call_service\",\"service\":\"/start_mapping_kitty\",\"args\":{}}");
    send_json(wsi,"{\"op\":\"call_service\",\"service\":\"/save_map_kitty\",\"args\":{\"filepath\":\"test_map\"}}");
    send_json(wsi,"{\"op\":\"call_service\",\"service\":\"/select_map_and_start_kitty\",\"args\":{\"filepath\":\"test_map\"}}");
    send_json(wsi,"{\"op\":\"call_service\",\"service\":\"/navigate_to_pose\",\"args\":{\"goal\":{\"pose\":{\"position\":{\"x\":1,\"y\":2,\"z\":0}}}}}");
    send_json(wsi,"{\"op\":\"call_service\",\"service\":\"/add_goal_kitty\",\"args\":{\"map_name\":\"test_map\",\"goal_name\":\"front\",\"x\":1,\"y\":2,\"theta\":0.5}}");
    send_json(wsi,"{\"op\":\"call_service\",\"service\":\"/delete_goal_kitty\",\"args\":{\"map_name\":\"test_map\",\"goal_name\":\"front\"}}");
    send_json(wsi,"{\"op\":\"call_service\",\"service\":\"/docking_kitty\",\"args\":{}}");
    send_json(wsi,"{\"op\":\"call_service\",\"service\":\"/delete_map_kitty\",\"args\":{\"map_name\":\"test_map\"}}");
  }
  if(r==LWS_CALLBACK_CLIENT_RECEIVE){
    cJSON*root=cJSON_ParseWithLength((char*)in,n);
    if(root){
      cJSON*op=cJSON_GetObjectItemCaseSensitive(root,"op");
      if(op && cJSON_IsString(op)){
        if(!strcmp(op->valuestring,"publish")){
          cJSON*topic=cJSON_GetObjectItemCaseSensitive(root,"topic");
          cJSON*msg=cJSON_GetObjectItemCaseSensitive(root,"msg");
          if(!strcmp(topic->valuestring,"/ebot_work_state")){
            cJSON*data=cJSON_GetObjectItemCaseSensitive(msg,"data");
            // data[0]: 1=charging 2=not charging  data[1]: battery %
            printf("[Battery] %.0f%% %s\n",
              cJSON_GetArrayItem(data,1)->valuedouble,
              cJSON_GetArrayItem(data,0)->valuedouble==1?"Charging":"Not charging");
          } else if(!strcmp(topic->valuestring,"/ebot_goal_status_local_planner")){
            if(cJSON_GetObjectItemCaseSensitive(msg,"data")->child->valuedouble==1)
              printf("Arrived at goal\n");
          } else if(!strcmp(topic->valuestring,"/fast_lio_localization")){
            cJSON*p=cJSON_GetObjectItemCaseSensitive(msg,"pose")->child->child;
            printf("[Pose] x=%.2f y=%.2f\n",
              cJSON_GetObjectItemCaseSensitive(p,"x")->valuedouble,
              cJSON_GetObjectItemCaseSensitive(p,"y")->valuedouble);
          }
        } else if(!strcmp(op->valuestring,"service_response")){
          cJSON*svc=cJSON_GetObjectItemCaseSensitive(root,"service");
          cJSON*val=cJSON_GetObjectItemCaseSensitive(root,"values");
          cJSON*succ=cJSON_GetObjectItemCaseSensitive(val,"success");
          cJSON*msg=cJSON_GetObjectItemCaseSensitive(val,"message");
          printf("[SVC] %s success=%d msg=%s\n",
            svc->valuestring, succ?succ->valueint:0, msg?msg->valuestring:"");
        }
      }
      cJSON_Delete(root);
    }
  }
  return 0;
}

4.5 Java

依赖:org.java-websocket:Java-WebSocket、org.json:json

import org.java_websocket.client.*;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONObject;
import java.net.URI;
public class Tbot {
  public static void main(String[] a) throws Exception {
    WebSocketClient ws = new WebSocketClient(new URI("ws://192.168.1.151:9090")) {
      public void onOpen(ServerHandshake h){
        send("{\"op\":\"call_service\",\"service\":\"/docking_kitty\",\"args\":{}}");
      }
      public void onMessage(String s){ System.out.println(s); }
      public void onClose(int c,String r,boolean b){}
      public void onError(Exception e){ e.printStackTrace(); }
    };
    ws.connectBlocking();
  }
}
import org.java_websocket.client.*;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONObject;
import java.net.URI;
public class Tbot {
  static WebSocketClient ws;
  static void sub(String t,String type){
    ws.send(new JSONObject().put("op","subscribe").put("topic",t).put("type",type).toString());
  }
  static void svc(String n,JSONObject args){
    ws.send(new JSONObject().put("op","call_service").put("service",n).put("args",args).toString());
  }
  public static void main(String[] a) throws Exception {
    ws = new WebSocketClient(new URI("ws://192.168.1.151:9090")) {
      public void onOpen(ServerHandshake h){
        sub("/fast_lio_localization","nav_msgs/Odometry");
        sub("/aft_pgo_path","nav_msgs/Path");
        sub("/map_list_kitty","kitty_runtime_manager/MapList");
        sub("/ebot_goal_status_local_planner","std_msgs/Float64MultiArray");
        sub("/ebot_work_state","std_msgs/Float64MultiArray");
        svc("/start_mapping_kitty", new JSONObject());
        svc("/save_map_kitty", new JSONObject().put("filepath","test_map"));
        svc("/select_map_and_start_kitty", new JSONObject().put("filepath","test_map"));
        JSONObject pos=new JSONObject().put("x",1.0).put("y",2.0).put("z",0.0);
        JSONObject goal=new JSONObject().put("pose",new JSONObject().put("position",pos));
        svc("/navigate_to_pose", new JSONObject().put("goal",goal));
        svc("/add_goal_kitty", new JSONObject().put("map_name","test_map").put("goal_name","front")
            .put("x",1.0).put("y",2.0).put("theta",0.5));
        svc("/delete_goal_kitty", new JSONObject().put("map_name","test_map").put("goal_name","front"));
        svc("/docking_kitty", new JSONObject());
        svc("/delete_map_kitty", new JSONObject().put("map_name","test_map"));
      }
      public void onMessage(String s){
        JSONObject m=new JSONObject(s);
        String op=m.optString("op");
        if("publish".equals(op)){
          String t=m.optString("topic");
          JSONObject msg=m.getJSONObject("msg");
          if("/fast_lio_localization".equals(t)){
            JSONObject p=msg.getJSONObject("pose").getJSONObject("pose").getJSONObject("position");
            System.out.println("[Pose] x="+p.getDouble("x")+" y="+p.getDouble("y"));
          } else if("/aft_pgo_path".equals(t)){
            System.out.println("[Path] "+msg.getJSONArray("poses").length()+"  points");
          } else if("/map_list_kitty".equals(t)){
            var maps=msg.getJSONArray("maps");
            for(int i=0;i<maps.length();i++){
              JSONObject mp=maps.getJSONObject(i);
              System.out.println("[Map] "+mp.getString("map_name")+"  goals_count="+mp.getJSONArray("goals").length());
            }
          } else if("/ebot_goal_status_local_planner".equals(t)){
            if(msg.getJSONArray("data").getDouble(0)==1) System.out.println("✅ Arrived at goal");
          } else if("/ebot_work_state".equals(t)){
            var d=msg.getJSONArray("data");
            System.out.println("[Battery] "+d.getDouble(1)+"% "+(d.getDouble(0)==1?"Charging":"Not charging"));
          }
        } else if("service_response".equals(op)){
          JSONObject v=m.getJSONObject("values");
          System.out.println("[SVC] "+m.getString("service")+" success="+v.optBoolean("success")+" msg="+v.optString("message"));
        }
      }
      public void onClose(int c,String r,boolean b){}
      public void onError(Exception e){ e.printStackTrace(); }
    };
    ws.connectBlocking();
  }
}

4.6 C#

依赖:dotnet add package Newtonsoft.Json

using System; using System.Net.WebSockets; using System.Text;
using System.Threading; using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
class Tbot {
  static async Task Main() {
    var ws=new ClientWebSocket();
    await ws.ConnectAsync(new Uri("ws://192.168.1.151:9090"),CancellationToken.None);
    var buf=new byte[8192];
    while(true){
      var r=await ws.ReceiveAsync(new ArraySegment<byte>(buf),CancellationToken.None);
      var m=JObject.Parse(Encoding.UTF8.GetString(buf,0,r.Count));
      if(m["op"]?.ToString()=="service_response")
        Console.WriteLine(m["service"],"→",m["values"]);
    }
  }
}
using System; using System.Net.WebSockets; using System.Text;
using System.Threading; using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
class Tbot {
  static ClientWebSocket ws;
  static async Task Send(string msg){
    var b=Encoding.UTF8.GetBytes(msg);
    await ws.SendAsync(new ArraySegment<byte>(b),WebSocketMessageType.Text,true,CancellationToken.None);
  }
  static async Task Main() {
    ws=new ClientWebSocket();
    await ws.ConnectAsync(new Uri("ws://192.168.1.151:9090"),CancellationToken.None);
    // Subscribe to 5 Topics
    await Send("{\"op\":\"subscribe\",\"topic\":\"/fast_lio_localization\",\"type\":\"nav_msgs/Odometry\"}");
    await Send("{\"op\":\"subscribe\",\"topic\":\"/aft_pgo_path\",\"type\":\"nav_msgs/Path\"}");
    await Send("{\"op\":\"subscribe\",\"topic\":\"/map_list_kitty\",\"type\":\"kitty_runtime_manager/MapList\"}");
    await Send("{\"op\":\"subscribe\",\"topic\":\"/ebot_goal_status_local_planner\",\"type\":\"std_msgs/Float64MultiArray\"}");
    await Send("{\"op\":\"subscribe\",\"topic\":\"/ebot_work_state\",\"type\":\"std_msgs/Float64MultiArray\"}");
    // Call 9 Services
    await Send("{\"op\":\"call_service\",\"service\":\"/start_mapping_kitty\",\"args\":{}}");
    await Send("{\"op\":\"call_service\",\"service\":\"/save_map_kitty\",\"args\":{\"filepath\":\"test_map\"}}");
    await Send("{\"op\":\"call_service\",\"service\":\"/select_map_and_start_kitty\",\"args\":{\"filepath\":\"test_map\"}}");
    await Send("{\"op\":\"call_service\",\"service\":\"/navigate_to_pose\",\"args\":{\"goal\":{\"pose\":{\"position\":{\"x\":1,\"y\":2,\"z\":0}}}}}");
    await Send("{\"op\":\"call_service\",\"service\":\"/add_goal_kitty\",\"args\":{\"map_name\":\"test_map\",\"goal_name\":\"front\",\"x\":1,\"y\":2,\"theta\":0.5}}");
    await Send("{\"op\":\"call_service\",\"service\":\"/delete_goal_kitty\",\"args\":{\"map_name\":\"test_map\",\"goal_name\":\"front\"}}");
    await Send("{\"op\":\"call_service\",\"service\":\"/docking_kitty\",\"args\":{}}");
    await Send("{\"op\":\"call_service\",\"service\":\"/delete_map_kitty\",\"args\":{\"map_name\":\"test_map\"}}");
    var buf=new byte[8192];
    while(true){
      var r=await ws.ReceiveAsync(new ArraySegment<byte>(buf),CancellationToken.None);
      var m=JObject.Parse(Encoding.UTF8.GetString(buf,0,r.Count));
      if(m["op"]?.ToString()=="publish" && m["topic"]?.ToString()=="/ebot_work_state"){
        var d=(JArray)m["msg"]["data"];
        Console.WriteLine($"[Battery] {d[1]}% charging={d[0]}");
      }
      if(m["op"]?.ToString()=="service_response")
        Console.WriteLine($"[SVC] {m["service"]} → {m["values"]}");
    }
  }
}

4.7 Go

依赖:go get github.com/gorilla/websocket

package main
import ("fmt";"log";
  "github.com/gorilla/websocket")
func main(){
  c,_,err:=websocket.DefaultDialer.Dial("ws://192.168.1.151:9090",nil)
  if err!=nil{log.Fatal(err)}
  defer c.Close()
  c.WriteJSON(map[string]interface{}{"op":"call_service","service":"/docking_kitty","args":map[string]interface{}{}})
  for{
    _,m,_:=c.ReadMessage()
    fmt.Println(string(m))
  }
}
package main
import ("encoding/json";"fmt";"log";
  "github.com/gorilla/websocket")
func sub(c *websocket.Conn, t, typ string){
  c.WriteJSON(map[string]interface{}{"op":"subscribe","topic":t,"type":typ})
}
func svc(c *websocket.Conn, name string, args map[string]interface{}){
  c.WriteJSON(map[string]interface{}{"op":"call_service","service":name,"args":args})
}
func main(){
  c,_,err:=websocket.DefaultDialer.Dial("ws://192.168.1.151:9090",nil)
  if err!=nil{log.Fatal(err)}
  defer c.Close()
  // Subscribe to 5 Topics
  sub(c,"/fast_lio_localization","nav_msgs/Odometry")
  sub(c,"/aft_pgo_path","nav_msgs/Path")
  sub(c,"/map_list_kitty","kitty_runtime_manager/MapList")
  sub(c,"/ebot_goal_status_local_planner","std_msgs/Float64MultiArray")
  sub(c,"/ebot_work_state","std_msgs/Float64MultiArray")
  // Call 9 Services
  svc(c,"/start_mapping_kitty",map[string]interface{}{})
  svc(c,"/save_map_kitty",map[string]interface{}{"filepath":"test_map"})
  svc(c,"/select_map_and_start_kitty",map[string]interface{}{"filepath":"test_map"})
  svc(c,"/navigate_to_pose",map[string]interface{}{"goal":map[string]interface{}{
    "pose":map[string]interface{}{"position":map[string]float64{"x":1,"y":2,"z":0}}}})
  svc(c,"/add_goal_kitty",map[string]interface{}{
    "map_name":"test_map","goal_name":"front","x":1.0,"y":2.0,"theta":0.5})
  svc(c,"/delete_goal_kitty",map[string]interface{}{"map_name":"test_map","goal_name":"front"})
  svc(c,"/docking_kitty",map[string]interface{}{})
  svc(c,"/delete_map_kitty",map[string]interface{}{"map_name":"test_map"})
  for{
    _,msg,_:=c.ReadMessage()
    var m map[string]interface{}
    json.Unmarshal(msg,&m)
    op,_:=m["op"].(string)
    if op=="publish"{
      t,_:=m["topic"].(string)
      md,_:=m["msg"].(map[string]interface{})
      switch t{
      case "/fast_lio_localization":
        p:=md["pose"].(map[string]interface{})["pose"].(map[string]interface{})["position"].(map[string]interface{})
        fmt.Printf("[Pose] x=%v y=%v\n",p["x"],p["y"])
      case "/aft_pgo_path":
        poses:=md["poses"].([]interface{})
        fmt.Printf("[Path] %d  points\n",len(poses))
      case "/map_list_kitty":
        for _,mp:=range md["maps"].([]interface{}){
          mpi:=mp.(map[string]interface{})
          fmt.Printf("[Map] %v  goals_count=%d\n",mpi["map_name"],len(mpi["goals"].([]interface{})))
        }
      case "/ebot_goal_status_local_planner":
        d:=md["data"].([]interface{})
        if d[0].(float64)==1 { fmt.Println("✅ Arrived at goal") }
      case "/ebot_work_state":
        d:=md["data"].([]interface{})
        fmt.Printf("[Battery] %.0f%% charging=%v\n",d[1],d[0])
      }
    } else if op=="service_response"{
      v:=m["values"].(map[string]interface{})
      fmt.Printf("[SVC] %v success=%v msg=%v\n",m["service"],v["success"],v["message"])
    }
  }
}

五、命令行验证

npm install -g wscat
wscat -c ws://192.168.1.151:9090

# Subscribe to battery state
{"op":"subscribe","topic":"/ebot_work_state","type":"std_msgs/Float64MultiArray"}

# Dock to charger, watch service_response
{"op":"call_service","service":"/docking_kitty","args":{}}
# Response:
# {"op":"service_response","service":"/docking_kitty",
#  "values":{"success":true,"message":"Docking started"},"result":true}
技术支持:support@tobotai.com,可索取工程模板与示例代码包。

← 返回 Tbot 平台

需要机器人整体解决方案?

外观、结构、硬件、软件、平台一站式定制,从样机验证到批量生产。