Skip to content

Commit fa15019

Browse files
author
OpenClaw
committed
feat: parse JSON from std_msgs/String in ROS2 parser
1 parent e88cef2 commit fa15019

5 files changed

Lines changed: 201 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ find_package(Boost REQUIRED)
1010
find_package(tf2_msgs REQUIRED)
1111
find_package(tf2_ros REQUIRED)
1212
find_package(plotjuggler REQUIRED)
13+
find_package(nlohmann_json REQUIRED)
1314

1415

1516
cmake_policy (SET CMP0020 NEW)

src/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ add_library( commonROS STATIC
1717
parser_configuration.cpp
1818
parser_configuration.h
1919
ros_parsers/ros2_parser.cpp
20+
ros_parsers/json_string_parser.cpp
21+
ros_parsers/json_string_parser.h
2022
${COMMON_UI_SRC}
2123
)
2224

2325
target_link_libraries( commonROS
2426
PUBLIC
2527
Qt5::Widgets
2628
Qt5::Xml
29+
nlohmann_json::nlohmann_json
2730
rclcpp::rclcpp
2831
rcpputils::rcpputils
2932
rosbag2_transport::rosbag2_transport
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#include "json_string_parser.h"
2+
3+
#include <QDebug>
4+
5+
#include <cstdint>
6+
#include <stdexcept>
7+
8+
using namespace PJ;
9+
10+
namespace
11+
{
12+
uint32_t ReadLe32(const uint8_t* ptr)
13+
{
14+
return (uint32_t(ptr[0]) << 0) | (uint32_t(ptr[1]) << 8) | (uint32_t(ptr[2]) << 16) |
15+
(uint32_t(ptr[3]) << 24);
16+
}
17+
}
18+
19+
JsonStringParser::JsonStringParser(const std::string& topic_name, PJ::PlotDataMapRef& data)
20+
: MessageParser(topic_name, data)
21+
{
22+
qInfo().noquote() << QString("[JsonStringParser] created parser for topic=%1")
23+
.arg(QString::fromStdString(topic_name));
24+
}
25+
26+
QString JsonStringParser::topicPrefix() const
27+
{
28+
return QString::fromStdString(_topic_name);
29+
}
30+
31+
bool JsonStringParser::parseRos2StringPayload(const PJ::MessageRef serialized_msg, std::string& text) const
32+
{
33+
const uint8_t* data = serialized_msg.data();
34+
const size_t size = serialized_msg.size();
35+
36+
if (size < 8)
37+
{
38+
qWarning().noquote() << QString("[%1] ROS2 String message too short to parse (%2 bytes)")
39+
.arg(topicPrefix())
40+
.arg(size);
41+
return false;
42+
}
43+
44+
const uint32_t cdr_header = ReadLe32(data);
45+
if (cdr_header != 0x00010000 && cdr_header != 0x00000000)
46+
{
47+
qWarning().noquote() << QString("[%1] unexpected CDR encapsulation for std_msgs/String: 0x%2")
48+
.arg(topicPrefix())
49+
.arg(cdr_header, 8, 16, QLatin1Char('0'));
50+
}
51+
52+
const uint32_t string_size = ReadLe32(data + 4);
53+
const size_t payload_end = size_t(8) + size_t(string_size);
54+
if (payload_end > size || string_size == 0)
55+
{
56+
qWarning().noquote() << QString("[%1] invalid std_msgs/String payload size: %2")
57+
.arg(topicPrefix())
58+
.arg(string_size);
59+
return false;
60+
}
61+
62+
const char* str_ptr = reinterpret_cast<const char*>(data + 8);
63+
if (str_ptr[string_size - 1] != '\0')
64+
{
65+
qWarning().noquote() << QString("[%1] std_msgs/String payload is not null-terminated")
66+
.arg(topicPrefix());
67+
return false;
68+
}
69+
70+
text.assign(str_ptr, str_ptr + string_size - 1);
71+
return true;
72+
}
73+
74+
void JsonStringParser::pushNumeric(const std::string& key, double timestamp, double value)
75+
{
76+
if (key.empty())
77+
{
78+
return;
79+
}
80+
81+
const QString qkey = QString::fromStdString(key);
82+
if (!_known_series.contains(qkey))
83+
{
84+
if (_known_series.size() >= qsizetype(_max_series))
85+
{
86+
qWarning().noquote() << QString("[%1] refusing to create additional JSON series beyond limit %2: %3")
87+
.arg(topicPrefix())
88+
.arg(_max_series)
89+
.arg(qkey);
90+
return;
91+
}
92+
_known_series.insert(qkey);
93+
}
94+
getSeries(key).pushBack({ timestamp, value });
95+
}
96+
97+
void JsonStringParser::flattenJson(const nlohmann::json& value, const std::string& prefix,
98+
double timestamp)
99+
{
100+
if (value.is_object())
101+
{
102+
for (auto it = value.begin(); it != value.end(); ++it)
103+
{
104+
const std::string child_key = prefix.empty() ? it.key() : prefix + "." + it.key();
105+
flattenJson(it.value(), child_key, timestamp);
106+
}
107+
return;
108+
}
109+
110+
if (value.is_number_integer())
111+
{
112+
pushNumeric(prefix, timestamp, static_cast<double>(value.get<int64_t>()));
113+
return;
114+
}
115+
116+
if (value.is_number_unsigned())
117+
{
118+
pushNumeric(prefix, timestamp, static_cast<double>(value.get<uint64_t>()));
119+
return;
120+
}
121+
122+
if (value.is_number_float())
123+
{
124+
pushNumeric(prefix, timestamp, value.get<double>());
125+
return;
126+
}
127+
}
128+
129+
bool JsonStringParser::parseMessage(const PJ::MessageRef serialized_msg, double& timestamp)
130+
{
131+
qInfo().noquote() << QString("[JsonStringParser] parseMessage topic=%1 size=%2 timestamp=%3")
132+
.arg(topicPrefix())
133+
.arg(serialized_msg.size())
134+
.arg(timestamp, 0, 'g', 17);
135+
136+
std::string text;
137+
if (!parseRos2StringPayload(serialized_msg, text))
138+
{
139+
return false;
140+
}
141+
142+
nlohmann::json value;
143+
try
144+
{
145+
value = nlohmann::json::parse(text);
146+
}
147+
catch (const std::exception& ex)
148+
{
149+
qWarning().noquote() << QString("[%1] failed to parse JSON from std_msgs/String: %2")
150+
.arg(topicPrefix())
151+
.arg(ex.what());
152+
return false;
153+
}
154+
155+
if (!value.is_object())
156+
{
157+
qWarning().noquote() << QString("[%1] expected top-level JSON object in std_msgs/String")
158+
.arg(topicPrefix());
159+
return false;
160+
}
161+
162+
qInfo().noquote() << QString("[JsonStringParser] parsed JSON object topic=%1 keys=%2")
163+
.arg(topicPrefix())
164+
.arg(int(value.size()));
165+
166+
flattenJson(value, "", timestamp);
167+
return true;
168+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#pragma once
2+
3+
#include <PlotJuggler/messageparser_base.h>
4+
#include <nlohmann/json.hpp>
5+
6+
#include <QSet>
7+
#include <QString>
8+
9+
class JsonStringParser : public PJ::MessageParser
10+
{
11+
public:
12+
JsonStringParser(const std::string& topic_name, PJ::PlotDataMapRef& data);
13+
14+
bool parseMessage(const PJ::MessageRef serialized_msg, double& timestamp) override;
15+
16+
private:
17+
bool parseRos2StringPayload(const PJ::MessageRef serialized_msg, std::string& text) const;
18+
void flattenJson(const nlohmann::json& value, const std::string& prefix, double timestamp);
19+
void pushNumeric(const std::string& key, double timestamp, double value);
20+
QString topicPrefix() const;
21+
22+
size_t _max_series = 200;
23+
QSet<QString> _known_series;
24+
};

src/ros_parsers/ros2_parser.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "ros2_parser.h"
2+
#include "json_string_parser.h"
23

34
#include <set>
45

@@ -162,5 +163,9 @@ TopicInfo CreateTopicInfo(const std::string& topic_name, const std::string& type
162163
std::shared_ptr<PJ::MessageParser> CreateParserROS2(const PJ::ParserFactories& factories, const std::string& topic_name,
163164
const std::string& type_name, PJ::PlotDataMapRef& data)
164165
{
166+
if (type_name == "std_msgs/msg/String" || type_name == "std_msgs/String")
167+
{
168+
return std::make_shared<JsonStringParser>(topic_name, data);
169+
}
165170
return factories.at("ros2msg")->createParser(topic_name, type_name, CreateSchema(type_name), data);
166171
}

0 commit comments

Comments
 (0)