-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds SNIPClient, a helper library for sending SNIP requests to remote…
… nodes. (#451) * Adds SNIPClient, a helper library for sending SNIP requests to remote nodes. Fixes a bug in the parser method used for OIR errors. * fix typo.
- Loading branch information
1 parent
0e2303a
commit be87ee7
Showing
3 changed files
with
360 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
/** \copyright | ||
* Copyright (c) 2020, Balazs Racz | ||
* All rights reserved. | ||
* | ||
* Redistribution and use in source and binary forms, with or without | ||
* modification, are permitted provided that the following conditions are met: | ||
* | ||
* - Redistributions of source code must retain the above copyright notice, | ||
* this list of conditions and the following disclaimer. | ||
* | ||
* - Redistributions in binary form must reproduce the above copyright notice, | ||
* this list of conditions and the following disclaimer in the documentation | ||
* and/or other materials provided with the distribution. | ||
* | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | ||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||
* POSSIBILITY OF SUCH DAMAGE. | ||
* | ||
* \file SNIPClient.cxxtest | ||
* | ||
* Unit test for SNIP client library. | ||
* | ||
* @author Balazs Racz | ||
* @date 18 Oct 2020 | ||
*/ | ||
|
||
static long long snipTimeout = 50 * 1000000; | ||
#define SNIP_CLIENT_TIMEOUT_NSEC snipTimeout | ||
|
||
#include "openlcb/SNIPClient.hxx" | ||
|
||
#include "openlcb/SimpleNodeInfo.hxx" | ||
#include "openlcb/SimpleNodeInfoMockUserFile.hxx" | ||
#include "utils/async_if_test_helper.hxx" | ||
|
||
namespace openlcb | ||
{ | ||
|
||
const char *const SNIP_DYNAMIC_FILENAME = MockSNIPUserFile::snip_user_file_path; | ||
|
||
const SimpleNodeStaticValues SNIP_STATIC_DATA = { | ||
4, "TestingTesting", "Undefined model", "Undefined HW version", "0.9"}; | ||
|
||
class SNIPClientTest : public AsyncNodeTest | ||
{ | ||
protected: | ||
SNIPClientTest() | ||
{ | ||
eb_.release_block(); | ||
run_x([this]() { | ||
ifTwo_.alias_allocator()->TEST_add_allocated_alias(0xFF2); | ||
}); | ||
wait(); | ||
} | ||
|
||
~SNIPClientTest() | ||
{ | ||
wait(); | ||
} | ||
|
||
MockSNIPUserFile userFile_ {"Undefined node name", "Undefined node descr"}; | ||
|
||
/// General flow for simple info requests. | ||
SimpleInfoFlow infoFlow_ {ifCan_.get()}; | ||
/// Handles SNIP requests. | ||
SNIPHandler snipHandler_ {ifCan_.get(), node_, &infoFlow_}; | ||
/// The actual client to test. | ||
SNIPClient client_ {ifCan_.get()}; | ||
|
||
// These objects create a second node on the CAN bus (with its own | ||
// interface). | ||
BlockExecutor eb_ {&g_executor}; | ||
static constexpr NodeID TWO_NODE_ID = 0x02010d0000ddULL; | ||
|
||
IfCan ifTwo_ {&g_executor, &can_hub0, local_alias_cache_size, | ||
remote_alias_cache_size, local_node_count}; | ||
AddAliasAllocator alloc_ {TWO_NODE_ID, &ifTwo_}; | ||
DefaultNode nodeTwo_ {&ifTwo_, TWO_NODE_ID}; | ||
}; | ||
|
||
TEST_F(SNIPClientTest, create) | ||
{ | ||
} | ||
|
||
static const char kExpectedData[] = | ||
"\x04TestingTesting\0Undefined model\0Undefined HW version\0" | ||
"0.9\0" | ||
"\x02Undefined node name\0Undefined node descr"; // C adds another \0. | ||
|
||
TEST_F(SNIPClientTest, localhost) | ||
{ | ||
auto b = invoke_flow(&client_, node_, NodeHandle(node_->node_id())); | ||
EXPECT_EQ(0, b->data()->resultCode); | ||
EXPECT_EQ( | ||
string(kExpectedData, sizeof(kExpectedData)), b->data()->response); | ||
|
||
// do another request. | ||
auto bb = invoke_flow(&client_, node_, NodeHandle(node_->node_id())); | ||
EXPECT_EQ(0, bb->data()->resultCode); | ||
EXPECT_EQ( | ||
string(kExpectedData, sizeof(kExpectedData)), bb->data()->response); | ||
} | ||
|
||
TEST_F(SNIPClientTest, remote) | ||
{ | ||
auto b = invoke_flow(&client_, &nodeTwo_, NodeHandle(node_->node_id())); | ||
EXPECT_EQ(0, b->data()->resultCode); | ||
EXPECT_EQ( | ||
string(kExpectedData, sizeof(kExpectedData)), b->data()->response); | ||
|
||
// do another request. | ||
auto bb = invoke_flow(&client_, &nodeTwo_, NodeHandle(node_->node_id())); | ||
EXPECT_EQ(0, bb->data()->resultCode); | ||
EXPECT_EQ( | ||
string(kExpectedData, sizeof(kExpectedData)), bb->data()->response); | ||
} | ||
|
||
TEST_F(SNIPClientTest, timeout) | ||
{ | ||
long long start = os_get_time_monotonic(); | ||
auto b = invoke_flow(&client_, &nodeTwo_, NodeHandle(nodeTwo_.node_id())); | ||
EXPECT_EQ(SNIPClientRequest::OPENMRN_TIMEOUT, b->data()->resultCode); | ||
EXPECT_EQ(0u, b->data()->response.size()); | ||
long long time = os_get_time_monotonic() - start; | ||
EXPECT_LT(MSEC_TO_NSEC(49), time); | ||
} | ||
|
||
TEST_F(SNIPClientTest, reject) | ||
{ | ||
SyncNotifiable n; | ||
auto b = get_buffer_deleter(client_.alloc()); | ||
b->data()->reset(node_, NodeHandle(NodeAlias(0x555))); | ||
b->data()->done.reset(&n); | ||
|
||
expect_packet(":X19DE822AN0555;"); | ||
client_.send(b.get()); | ||
wait(); | ||
clear_expect(true); | ||
EXPECT_EQ(SNIPClientRequest::OPERATION_PENDING, b->data()->resultCode); | ||
|
||
send_packet(":X19068555N022A209905EB;"); | ||
wait(); | ||
EXPECT_EQ(SNIPClientRequest::OPERATION_PENDING, b->data()->resultCode); | ||
|
||
send_packet(":X19068555N022A20990DE8;"); | ||
wait(); | ||
EXPECT_EQ(SNIPClientRequest::ERROR_REJECTED | 0x2099, b->data()->resultCode); | ||
n.wait_for_notification(); | ||
|
||
} | ||
|
||
} // namespace openlcb |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
/** \copyright | ||
* Copyright (c) 2020, Balazs Racz | ||
* All rights reserved. | ||
* | ||
* Redistribution and use in source and binary forms, with or without | ||
* modification, are permitted provided that the following conditions are met: | ||
* | ||
* - Redistributions of source code must retain the above copyright notice, | ||
* this list of conditions and the following disclaimer. | ||
* | ||
* - Redistributions in binary form must reproduce the above copyright notice, | ||
* this list of conditions and the following disclaimer in the documentation | ||
* and/or other materials provided with the distribution. | ||
* | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | ||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||
* POSSIBILITY OF SUCH DAMAGE. | ||
* | ||
* \file SNIPClient.hxx | ||
* | ||
* A client library for talking to an arbitrary openlcb Node and ask it for the | ||
* Simple Node Ident Info data. | ||
* | ||
* @author Balazs Racz | ||
* @date 18 Oct 2020 | ||
*/ | ||
|
||
#ifndef _OPENLCB_SNIPCLIENT_HXX_ | ||
#define _OPENLCB_SNIPCLIENT_HXX_ | ||
|
||
#include "executor/CallableFlow.hxx" | ||
#include "openlcb/Defs.hxx" | ||
#include "openlcb/If.hxx" | ||
|
||
namespace openlcb | ||
{ | ||
|
||
/// Buffer contents for invoking the SNIP client. | ||
struct SNIPClientRequest : public CallableFlowRequestBase | ||
{ | ||
/// Helper function for invoke_subflow. | ||
/// @param src the openlcb node to call from | ||
/// @param dst the openlcb node to target | ||
void reset(Node *src, NodeHandle dst) | ||
{ | ||
reset_base(); | ||
resultCode = OPERATION_PENDING; | ||
src_ = src; | ||
dst_ = dst; | ||
} | ||
|
||
enum | ||
{ | ||
OPERATION_PENDING = 0x20000, //< cleared when done is called. | ||
ERROR_REJECTED = 0x200000, //< Target node has rejected the request. | ||
OPENMRN_TIMEOUT = 0x80000, //< Timeout waiting for ack/nack. | ||
}; | ||
|
||
/// Source node where to send the request from. | ||
Node *src_; | ||
/// Destination node to query. | ||
NodeHandle dst_; | ||
/// Response payload if successful. | ||
Payload response; | ||
}; | ||
|
||
#if !defined(GTEST) || !defined(SNIP_CLIENT_TIMEOUT_NSEC) | ||
/// Specifies how long to wait for a SNIP request to get a response. Writable | ||
/// for unittesting purposes. | ||
static constexpr long long SNIP_CLIENT_TIMEOUT_NSEC = MSEC_TO_NSEC(1500); | ||
#endif | ||
|
||
class SNIPClient : public CallableFlow<SNIPClientRequest> | ||
{ | ||
public: | ||
/// Constructor. | ||
/// @param s service of the openlcb executor. | ||
SNIPClient(Service *s) | ||
: CallableFlow<SNIPClientRequest>(s) | ||
{ | ||
} | ||
|
||
Action entry() override | ||
{ | ||
request()->resultCode = SNIPClientRequest::OPERATION_PENDING; | ||
return allocate_and_call( | ||
iface()->addressed_message_write_flow(), STATE(write_request)); | ||
} | ||
|
||
private: | ||
enum | ||
{ | ||
MTI_1a = Defs::MTI_TERMINATE_DUE_TO_ERROR, | ||
MTI_1b = Defs::MTI_OPTIONAL_INTERACTION_REJECTED, | ||
MASK_1 = ~(MTI_1a ^ MTI_1b), | ||
MTI_1 = MTI_1a, | ||
|
||
MTI_2 = Defs::MTI_IDENT_INFO_REPLY, | ||
MASK_2 = Defs::MTI_EXACT, | ||
}; | ||
|
||
/// Called once the allocation is complete. Sends out the SNIP request to | ||
/// the bus. | ||
Action write_request() | ||
{ | ||
auto *b = | ||
get_allocation_result(iface()->addressed_message_write_flow()); | ||
b->data()->reset(Defs::MTI_IDENT_INFO_REQUEST, | ||
request()->src_->node_id(), request()->dst_, EMPTY_PAYLOAD); | ||
|
||
iface()->dispatcher()->register_handler( | ||
&responseHandler_, MTI_1, MASK_1); | ||
iface()->dispatcher()->register_handler( | ||
&responseHandler_, MTI_2, MASK_2); | ||
|
||
iface()->addressed_message_write_flow()->send(b); | ||
|
||
return sleep_and_call( | ||
&timer_, SNIP_CLIENT_TIMEOUT_NSEC, STATE(response_came)); | ||
} | ||
|
||
/// Callback from the response handler. | ||
/// @param message the incoming response message from the bus | ||
void handle_response(Buffer<GenMessage> *message) | ||
{ | ||
auto rb = get_buffer_deleter(message); | ||
if (request()->src_ != message->data()->dstNode || | ||
!iface()->matching_node(request()->dst_, message->data()->src)) | ||
{ | ||
// Not from the right place. | ||
return; | ||
} | ||
if (message->data()->mti == Defs::MTI_OPTIONAL_INTERACTION_REJECTED || | ||
message->data()->mti == Defs::MTI_TERMINATE_DUE_TO_ERROR) | ||
{ | ||
uint16_t mti, error_code; | ||
buffer_to_error( | ||
message->data()->payload, &error_code, &mti, nullptr); | ||
LOG(INFO, "rejection err %04x mti %04x", error_code, mti); | ||
if (mti && mti != Defs::MTI_IDENT_INFO_REQUEST) | ||
{ | ||
// Got error response for a different interaction. Ignore. | ||
return; | ||
} | ||
request()->resultCode = | ||
error_code | SNIPClientRequest::ERROR_REJECTED; | ||
} | ||
else if (message->data()->mti == Defs::MTI_IDENT_INFO_REPLY) | ||
{ | ||
request()->response = std::move(message->data()->payload); | ||
request()->resultCode = 0; | ||
} | ||
else | ||
{ | ||
// Dunno what this MTI is. Ignore. | ||
LOG(INFO, "Unexpected MTI for SNIP response handler: %04x", | ||
message->data()->mti); | ||
return; | ||
} | ||
// Wakes up parent flow. | ||
request()->resultCode &= ~SNIPClientRequest::OPERATION_PENDING; | ||
timer_.trigger(); | ||
} | ||
|
||
Action response_came() | ||
{ | ||
iface()->dispatcher()->unregister_handler_all(&responseHandler_); | ||
if (request()->resultCode & SNIPClientRequest::OPERATION_PENDING) | ||
{ | ||
return return_with_error(SNIPClientRequest::OPENMRN_TIMEOUT); | ||
} | ||
return return_with_error(request()->resultCode); | ||
} | ||
|
||
/// @return openlcb source interface. | ||
If *iface() | ||
{ | ||
return request()->src_->iface(); | ||
} | ||
|
||
/// Handles the timeout feature. | ||
StateFlowTimer timer_ {this}; | ||
/// Registered handler for response messages. | ||
IncomingMessageStateFlow::GenericHandler responseHandler_ { | ||
this, &SNIPClient::handle_response}; | ||
}; | ||
|
||
} // namespace openlcb | ||
|
||
#endif // _OPENLCB_SNIPCLIENT_HXX_ |