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_kitty | std_srvs/Trigger | 空请求 → success,message |
| /save_map_kitty | kitty_runtime_manager/SaveMap | filepath → success,message |
| /select_map_and_start_kitty | kitty_runtime_manager/SaveMap | filepath → success,message |
| /delete_map_kitty | kitty_runtime_manager/DeleteMap | map_name → success,message |
| /docking_kitty | std_srvs/Trigger | 空请求 → success,message |
| /navigate_to_pose | nav_msgs/GetPlan | goal.pose.position.x/y/z → plan.poses[] |
| /add_goal_kitty | kitty_runtime_manager/AddGoal | map_name,goal_name,x,y,theta → success,message |
| /delete_goal_kitty | kitty_runtime_manager/DeleteGoal | map_name,goal_name → success,message |
2.1 机器人定位 nav_msgs/Odometry
Topic:/fast_lio_localization,底盘定时推送当前位姿。
{
"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.x | float64 | 机器人 x 坐标(米) |
| pose.pose.position.y | float64 | 机器人 y 坐标(米) |
| pose.pose.position.z | float64 | 机器人 z 坐标(米,底盘固定 0) |
| pose.pose.orientation.x/y/z/w | float64 | 四元数朝向;前端用 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 秒)。
{ "poses": [
{"pose":{"position":{"x":0,"y":0,"z":0}}},
{"pose":{"position":{"x":1,"y":2,"z":0}}}
]}| 字段 | 类型 | 说明 |
|---|---|---|
| poses | array[PoseStamped] | 规划路径点数组 |
| poses[].pose.position.x | float64 | 路径点 x 坐标(米) |
| poses[].pose.position.y | float64 | 路径点 y 坐标(米) |
| poses[].pose.position.z | float64 | 路径点 z 坐标(底盘恒为 0) |
2.3 地图列表 kitty_runtime_manager/MapList
Topic:/map_list_kitty。订阅后返回所有地图及其已保存目标点。
{ "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}
]}
]}| 字段 | 类型 | 说明 |
|---|---|---|
| maps | array[Map] | 地图数组 |
| maps[].map_name | string | 地图显示名称 |
| maps[].map_path | string | 地图文件路径(内部用) |
| maps[].goals | array[Goal] | 该地图下已保存目标点 |
| maps[].goals[].name | string | 目标点名称 |
| maps[].goals[].x | float64 | 目标点 x(米) |
| maps[].goals[].y | float64 | 目标点 y(米) |
| maps[].goals[].theta | float64 | 目标点朝向(弧度) |
2.4 导航状态 std_msgs/Float64MultiArray
Topic:/ebot_goal_status_local_planner。
{"data": [1]}| 字段 | 类型 | 说明 |
|---|---|---|
| data[0] | float64 | 1 = Arrived at goal;0 = 未到达。Docking场景下只有真正充上电才发 1 |
2.5 电量与充电状态 std_msgs/Float64MultiArray
Topic:/ebot_work_state。
{"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
{}(空请求){ "success": true, "message": "Mapping started" }| 字段 | 类型 | 方向 | 说明 |
|---|---|---|---|
| success | bool | 响应 | 操作是否成功 |
| message | string | 响应 | 结果信息(失败时给原因) |
3.2 保存地图 kitty_runtime_manager/SaveMap
Service:/save_map_kitty
{ "filepath": "map_name" }{ "success": true, "message": "Map saved" }| 字段 | 类型 | 方向 | 说明 |
|---|---|---|---|
| filepath | string | 请求 | map_name(不要带路径) |
| success | bool | 响应 | 操作是否成功 |
| message | string | 响应 | 结果信息 |
3.3 选择地图并启动导航 kitty_runtime_manager/SaveMap
Service:/select_map_and_start_kitty
{ "filepath": "map_name" }{ "success": true, "message": "Map selected and navigation started" }| 字段 | 类型 | 方向 | 说明 |
|---|---|---|---|
| filepath | string | 请求 | map_name(不要带路径) |
| success | bool | 响应 | 操作是否成功 |
| message | string | 响应 | 结果信息 |
3.4 删除地图 kitty_runtime_manager/DeleteMap
Service:/delete_map_kitty
{ "map_name": "map_name" }{ "success": true, "message": "Deleted successfully" }| 字段 | 类型 | 方向 | 说明 |
|---|---|---|---|
| map_name | string | 请求 | 要删除的map_name |
| success | bool | 响应 | 操作是否成功 |
| message | string | 响应 | 结果信息 |
3.5 回充电桩 std_srvs/Trigger
Service:/docking_kitty
{}{ "success": true, "message": "Docking started" }| 字段 | 类型 | 方向 | 说明 |
|---|---|---|---|
| success | bool | 响应 | 操作是否成功(只表示Docking流程已启动,不代表已充上电) |
| message | string | 响应 | 结果信息 |
3.6 导航到目标点 nav_msgs/GetPlan
Service:/navigate_to_pose
{
"goal": { "pose": { "position": { "x": 1.0, "y": 2.0, "z": 0.0 } } }
}{ "plan": { "poses": [
{"pose":{"position":{"x":0,"y":0,"z":0}}},
{"pose":{"position":{"x":1,"y":2,"z":0}}}
]}}| 字段 | 类型 | 方向 | 说明 |
|---|---|---|---|
| goal.pose.position.x | float64 | 请求 | 目标点 x(米) |
| goal.pose.position.y | float64 | 请求 | 目标点 y(米) |
| goal.pose.position.z | float64 | 请求 | 目标点 z(底盘恒传 0) |
| plan.poses | array[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 } } }
}{ "plan": { "poses": [] } }| 字段 | 类型 | 方向 | 说明 |
|---|---|---|---|
| goal.pose.position.x | float64 | 请求 | 固定 999999(取消标志) |
| goal.pose.position.y | float64 | 请求 | 固定 888888(取消标志) |
| goal.pose.position.z | float64 | 请求 | 固定 0 |
| plan.poses | array | 响应 | 空或截断路径 |
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
}{ "success": true, "message": "Added successfully" }| 字段 | 类型 | 方向 | 说明 |
|---|---|---|---|
| map_name | string | 请求 | map_name |
| goal_name | string | 请求 | 目标点名称 |
| x | float64 | 请求 | 目标点 x(米) |
| y | float64 | 请求 | 目标点 y(米) |
| theta | float64 | 请求 | 朝向角(弧度) |
| success | bool | 响应 | 操作是否成功 |
| message | string | 响应 | 结果信息 |
3.9 删除目标点 kitty_runtime_manager/DeleteGoal
Service:/delete_goal_kitty
{ "map_name": "Lobby", "goal_name": "Reception" }{ "success": true, "message": "Deleted successfully" }| 字段 | 类型 | 方向 | 说明 |
|---|---|---|---|
| map_name | string | 请求 | map_name |
| goal_name | string | 请求 | 目标点名称 |
| success | bool | 响应 | 操作是否成功 |
| message | string | 响应 | 结果信息 |
四、多语言开发示例
每种语言默认显示快速示例(连接 + 订阅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,可索取工程模板与示例代码包。