Skip to content

Commit

Permalink
Adds SNIPClient, a helper library for sending SNIP requests to remote…
Browse files Browse the repository at this point in the history
… 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
balazsracz authored Oct 19, 2020
1 parent 0e2303a commit be87ee7
Show file tree
Hide file tree
Showing 3 changed files with 360 additions and 2 deletions.
4 changes: 2 additions & 2 deletions src/openlcb/If.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ void buffer_to_error(const Payload &payload, uint16_t *error_code,
error_message->clear();
if (payload.size() >= 2 && error_code)
{
*error_code = (((uint16_t)payload[0]) << 8) | payload[1];
*error_code = (((uint16_t)payload[0]) << 8) | (uint8_t)payload[1];
}
if (payload.size() >= 4 && mti)
{
*mti = (((uint16_t)payload[2]) << 8) | payload[3];
*mti = (((uint16_t)payload[2]) << 8) | (uint8_t)payload[3];
}
if (payload.size() > 4 && error_message)
{
Expand Down
160 changes: 160 additions & 0 deletions src/openlcb/SNIPClient.cxxtest
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
198 changes: 198 additions & 0 deletions src/openlcb/SNIPClient.hxx
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_

0 comments on commit be87ee7

Please sign in to comment.