Skip to content

Commit 5a69bf0

Browse files
authored
[primary] Print a warning on additional data in RobotState submessages (UniversalRobots#460)
Before, parsing sub-messages of a RobotState message with added bytes was causing a failure when parsing the RobotState message. With this change by default the primary parser only prints a warning in that case. Mainly for testing purposes we added a "strict" mode that throws an exception if unknown bytes are inside the message. This way we will achieve a failing test when running against a version with added bytes that we don't know, yet. I broke up strict mode in a runtime setting for the parser and a compile setting for the primary client. This way we can test the parser with both, while deciding the behavior for the primary client at configure time.
1 parent 1f4d9bd commit 5a69bf0

6 files changed

Lines changed: 155 additions & 18 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,14 @@ jobs:
5858
- name: setup chrome
5959
uses: browser-actions/setup-chrome@v2
6060
- name: configure
61-
run: mkdir build && cd build && cmake .. -DBUILDING_TESTS=1 -DINTEGRATION_TESTS=1 -DWITH_ASAN=ON
61+
run: >
62+
mkdir build &&
63+
cd build &&
64+
cmake ..
65+
-DBUILDING_TESTS=1
66+
-DINTEGRATION_TESTS=1
67+
-DWITH_ASAN=ON
68+
-DPRIMARY_CLIENT_STRICT_PARSING=ON
6269
env:
6370
CXXFLAGS: -g -O2 -fprofile-arcs -ftest-coverage
6471
CFLAGS: -g -O2 -fprofile-arcs -ftest-coverage

CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ endif()
88

99
option(WITH_ASAN "Compile with address sanitizer support" OFF)
1010
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
11+
option(PRIMARY_CLIENT_STRICT_PARSING_ "Enable strict parsing of primary client messages" OFF)
1112

1213
if(MSVC)
1314
set(BUILD_SHARED_LIBS OFF)
@@ -63,6 +64,8 @@ add_library(urcl
6364
)
6465
add_library(ur_client_library::urcl ALIAS urcl)
6566
target_compile_features(urcl PUBLIC cxx_std_17)
67+
configure_file("src/compile_options.h.in"
68+
"include/ur_client_library/compile_options.h")
6669

6770
if(MSVC)
6871
target_link_libraries(urcl ws2_32)
@@ -94,6 +97,9 @@ target_include_directories(urcl PUBLIC
9497
target_include_directories(urcl PRIVATE
9598
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/3rdparty>
9699
)
100+
target_include_directories(urcl PRIVATE
101+
"${CMAKE_CURRENT_BINARY_DIR}/include"
102+
)
97103

98104
find_package(Threads REQUIRED)
99105
if(THREADS_HAVE_PTHREAD_ARG)

include/ur_client_library/primary/primary_parser.h

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,22 @@ class PrimaryParser : public comm::Parser<PrimaryPackage>
4949
PrimaryParser() = default;
5050
virtual ~PrimaryParser() = default;
5151

52+
/**!
53+
* \brief Set the strict mode for parsing. If strict mode is enabled, the parser will throw an
54+
* exception if a sub-package is not parsed completely. If strict mode is disabled, the parser
55+
* will just log a warning and continue parsing the next sub-package.
56+
*
57+
* Strict mode is meant for testing only, as it may break existing applications when running
58+
* against a newer software version that adds new fields to existing packages, which would lead
59+
* to parsing errors of the respective sub-package.
60+
*
61+
* \param strict_mode Whether to enable strict mode or not.
62+
*/
63+
void setStrictMode(bool strict_mode)
64+
{
65+
strict_mode_ = strict_mode;
66+
}
67+
5268
/*!
5369
* \brief Uses the given BinParser to create package objects from the contained serialization.
5470
*
@@ -109,9 +125,14 @@ class PrimaryParser : public comm::Parser<PrimaryPackage>
109125

110126
if (!sbp.empty())
111127
{
112-
URCL_LOG_ERROR("Sub-package of type %d was not parsed completely!", static_cast<int>(type));
113128
sbp.debug();
114-
return false;
129+
if (strict_mode_)
130+
{
131+
throw UrException("Sub-package of type " + std::string(robotStateString(type)) +
132+
" was not parsed completely, and strict mode is enabled, so aborting parsing!");
133+
}
134+
URCL_LOG_WARN("Sub-package of type %s was not parsed completely!", robotStateString(type));
135+
sbp.consume();
115136
}
116137
}
117138

@@ -224,6 +245,10 @@ class PrimaryParser : public comm::Parser<PrimaryPackage>
224245
return new RobotMessage(timestamp, source, type);
225246
}
226247
}
248+
249+
bool strict_mode_ = false; //!< If true, the parser will return false if it encounters unknown
250+
// package types or sub-package types. If false, it will ignore unknown types and try to continue
251+
// parsing.
227252
};
228253

229254
} // namespace primary_interface

src/compile_options.h.in

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#pragma once
2+
3+
#cmakedefine01 PRIMARY_CLIENT_STRICT_PARSING_
4+
5+
namespace urcl
6+
{
7+
struct CompileOptions
8+
{
9+
constexpr static bool PRIMARY_CLIENT_STRICT_PARSING = static_cast<bool>(PRIMARY_CLIENT_STRICT_PARSING_);
10+
};
11+
12+
inline constexpr CompileOptions COMPILE_OPTIONS;
13+
} // namespace urcl

src/primary/primary_client.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
#include <ur_client_library/primary/robot_state.h>
3434
#include "ur_client_library/exceptions.h"
3535
#include <ur_client_library/helpers.h>
36+
#include <ur_client_library/compile_options.h>
37+
3638
#include <chrono>
3739
namespace urcl
3840
{
@@ -41,6 +43,7 @@ namespace primary_interface
4143
PrimaryClient::PrimaryClient(const std::string& robot_ip, comm::INotifier& notifier)
4244
: stream_(robot_ip, UR_PRIMARY_PORT)
4345
{
46+
parser_.setStrictMode(COMPILE_OPTIONS.PRIMARY_CLIENT_STRICT_PARSING);
4447
prod_.reset(new comm::URProducer<PrimaryPackage>(stream_, parser_));
4548

4649
consumer_.reset(new PrimaryConsumer());

tests/test_primary_parser.cpp

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -182,15 +182,30 @@ const unsigned char RUNTIME_EXCEPTION_MESSAGE[] = {
182182
0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x3a, 0x74, 0x78, 0x74, 0x6d, 0x73, 0x67, 0x3a
183183
};
184184

185-
TEST(primary_parser, parse_calibration_data)
185+
class PrimaryParserTest : public ::testing::Test
186+
{
187+
protected:
188+
virtual void SetUp() override
189+
{
190+
// In these tests we use strict mode, which means that the parser will not ignore any extra
191+
// bytes in the payload. This allows us to verify that the parser correctly identifies the size
192+
// of each message and does not consume more bytes than it should.
193+
// If new software versions add new fields to the messages, these tests will fail, which is
194+
// desirable as it will prompt us to update the parser with the new fields.
195+
parser_.setStrictMode(true);
196+
}
197+
198+
primary_interface::PrimaryParser parser_;
199+
};
200+
201+
TEST_F(PrimaryParserTest, parse_calibration_data)
186202
{
187203
unsigned char raw_data[sizeof(ROBOT_STATE)];
188204
memcpy(raw_data, ROBOT_STATE, sizeof(ROBOT_STATE));
189205
comm::BinParser bp(raw_data, sizeof(raw_data));
190206

191207
std::vector<std::unique_ptr<primary_interface::PrimaryPackage>> products;
192-
primary_interface::PrimaryParser parser;
193-
parser.parse(bp, products);
208+
parser_.parse(bp, products);
194209

195210
EXPECT_EQ(products.size(), 13);
196211

@@ -205,26 +220,24 @@ TEST(primary_parser, parse_calibration_data)
205220
}
206221
}
207222

208-
TEST(primary_parser, parse_robot_state_with_single_parser)
223+
TEST_F(PrimaryParserTest, parse_robot_state_with_single_parser)
209224
{
210225
unsigned char raw_data[sizeof(ROBOT_STATE)];
211226
memcpy(raw_data, ROBOT_STATE, sizeof(ROBOT_STATE));
212227
comm::BinParser bp(raw_data, sizeof(raw_data));
213228

214229
std::unique_ptr<primary_interface::PrimaryPackage> product;
215-
primary_interface::PrimaryParser parser;
216-
ASSERT_FALSE(parser.parse(bp, product));
230+
ASSERT_FALSE(parser_.parse(bp, product));
217231
};
218232

219-
TEST(primary_parser, parse_version_message)
233+
TEST_F(PrimaryParserTest, parse_version_message)
220234
{
221235
unsigned char raw_data[sizeof(VERSION_MESSAGE)];
222236
memcpy(raw_data, VERSION_MESSAGE, sizeof(VERSION_MESSAGE));
223237
comm::BinParser bp(raw_data, sizeof(raw_data));
224238

225239
std::unique_ptr<primary_interface::PrimaryPackage> product;
226-
primary_interface::PrimaryParser parser;
227-
ASSERT_TRUE(parser.parse(bp, product));
240+
ASSERT_TRUE(parser_.parse(bp, product));
228241

229242
EXPECT_NE(product, nullptr);
230243
if (primary_interface::VersionMessage* data = dynamic_cast<primary_interface::VersionMessage*>(product.get()))
@@ -241,15 +254,14 @@ TEST(primary_parser, parse_version_message)
241254
}
242255
}
243256

244-
TEST(primary_parser, parse_key_message)
257+
TEST_F(PrimaryParserTest, parse_key_message)
245258
{
246259
unsigned char raw_data[sizeof(KEY_MESSAGE)];
247260
memcpy(raw_data, KEY_MESSAGE, sizeof(KEY_MESSAGE));
248261
comm::BinParser bp(raw_data, sizeof(raw_data));
249262

250263
std::vector<std::unique_ptr<primary_interface::PrimaryPackage>> products;
251-
primary_interface::PrimaryParser parser;
252-
ASSERT_TRUE(parser.parse(bp, products));
264+
ASSERT_TRUE(parser_.parse(bp, products));
253265

254266
ASSERT_EQ(products.size(), 1);
255267
if (auto data = dynamic_cast<primary_interface::KeyMessage*>(products[0].get()))
@@ -274,15 +286,14 @@ TEST(primary_parser, parse_key_message)
274286
}
275287
}
276288

277-
TEST(primary_parser, parse_runtime_exception_message)
289+
TEST_F(PrimaryParserTest, parse_runtime_exception_message)
278290
{
279291
unsigned char raw_data[sizeof(RUNTIME_EXCEPTION_MESSAGE)];
280292
memcpy(raw_data, RUNTIME_EXCEPTION_MESSAGE, sizeof(RUNTIME_EXCEPTION_MESSAGE));
281293
comm::BinParser bp(raw_data, sizeof(raw_data));
282294

283295
std::vector<std::unique_ptr<primary_interface::PrimaryPackage>> products;
284-
primary_interface::PrimaryParser parser;
285-
ASSERT_TRUE(parser.parse(bp, products));
296+
ASSERT_TRUE(parser_.parse(bp, products));
286297

287298
EXPECT_EQ(products.size(), 1);
288299

@@ -305,6 +316,78 @@ TEST(primary_parser, parse_runtime_exception_message)
305316
}
306317
}
307318

319+
TEST_F(PrimaryParserTest, parse_robot_state_with_oversized_submessage)
320+
{
321+
// KinematicsInfo parses exactly 220 bytes of payload:
322+
// 6×uint32 (checksum) + 4×vector6d (DH params) + uint32 (calibration_status)
323+
const size_t ki_payload_size = 6 * sizeof(uint32_t) + 4 * 6 * sizeof(double) + sizeof(uint32_t);
324+
const size_t extra_bytes = 10;
325+
const size_t sub1_size = sizeof(uint32_t) + sizeof(uint8_t) + ki_payload_size + extra_bytes;
326+
const size_t sub2_size = sizeof(uint32_t) + sizeof(uint8_t) + 1;
327+
const size_t total_size = sizeof(int32_t) + sizeof(uint8_t) + sub1_size + sub2_size;
328+
329+
std::vector<uint8_t> test_data(total_size, 0);
330+
size_t offset = 0;
331+
332+
// Total packet size (big-endian int32)
333+
test_data[offset++] = (total_size >> 24) & 0xFF;
334+
test_data[offset++] = (total_size >> 16) & 0xFF;
335+
test_data[offset++] = (total_size >> 8) & 0xFF;
336+
test_data[offset++] = total_size & 0xFF;
337+
338+
// Packet type: ROBOT_STATE (0x10)
339+
test_data[offset++] = 0x10;
340+
341+
// Submessage 1: KinematicsInfo with extra trailing bytes
342+
test_data[offset++] = (sub1_size >> 24) & 0xFF;
343+
test_data[offset++] = (sub1_size >> 16) & 0xFF;
344+
test_data[offset++] = (sub1_size >> 8) & 0xFF;
345+
test_data[offset++] = sub1_size & 0xFF;
346+
test_data[offset++] = 0x05; // KINEMATICS_INFO
347+
offset += ki_payload_size; // payload is all zeros
348+
memset(test_data.data() + offset, 0xAA, extra_bytes);
349+
offset += extra_bytes;
350+
351+
// Submessage 2: ADDITIONAL_INFO (parsed by base RobotState → rawData)
352+
test_data[offset++] = (sub2_size >> 24) & 0xFF;
353+
test_data[offset++] = (sub2_size >> 16) & 0xFF;
354+
test_data[offset++] = (sub2_size >> 8) & 0xFF;
355+
test_data[offset++] = sub2_size & 0xFF;
356+
test_data[offset++] = 0x08; // ADDITIONAL_INFO
357+
test_data[offset++] = 0x42; // 1 byte of payload
358+
359+
ASSERT_EQ(offset, total_size);
360+
361+
{
362+
// With strict mode enabled, an execption should be thrown due to the oversized KinematicsInfo
363+
// submessage
364+
std::vector<std::unique_ptr<primary_interface::PrimaryPackage>> products;
365+
unsigned char raw_data[total_size];
366+
memcpy(raw_data, test_data.data(), test_data.size());
367+
comm::BinParser bp(raw_data, total_size);
368+
EXPECT_THROW(parser_.parse(bp, products), UrException);
369+
}
370+
371+
{
372+
// Using a non-strict parser that allows oversized submessages should succeed and parse all
373+
// submessages
374+
std::vector<std::unique_ptr<primary_interface::PrimaryPackage>> products;
375+
unsigned char raw_data[total_size];
376+
memcpy(raw_data, test_data.data(), test_data.size());
377+
comm::BinParser bp(raw_data, total_size);
378+
primary_interface::PrimaryParser non_strict_parser;
379+
EXPECT_TRUE(non_strict_parser.parse(bp, products));
380+
381+
// Both submessages should be parsed despite the oversized first one
382+
ASSERT_EQ(products.size(), 2);
383+
384+
auto* ki = dynamic_cast<primary_interface::KinematicsInfo*>(products[0].get());
385+
ASSERT_NE(ki, nullptr);
386+
EXPECT_EQ(ki->calibration_status_, 0u);
387+
EXPECT_EQ(ki->dh_theta_, vector6d_t({ 0, 0, 0, 0, 0, 0 }));
388+
}
389+
}
390+
308391
int main(int argc, char* argv[])
309392
{
310393
::testing::InitGoogleTest(&argc, argv);

0 commit comments

Comments
 (0)