diff --git a/src/openlcb/If.cxx b/src/openlcb/If.cxx index 5e15582df..d53d06008 100644 --- a/src/openlcb/If.cxx +++ b/src/openlcb/If.cxx @@ -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) { diff --git a/src/openlcb/SNIPClient.cxxtest b/src/openlcb/SNIPClient.cxxtest new file mode 100644 index 000000000..c921df941 --- /dev/null +++ b/src/openlcb/SNIPClient.cxxtest @@ -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 diff --git a/src/openlcb/SNIPClient.hxx b/src/openlcb/SNIPClient.hxx new file mode 100644 index 000000000..4d87ab99c --- /dev/null +++ b/src/openlcb/SNIPClient.hxx @@ -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 +{ +public: + /// Constructor. + /// @param s service of the openlcb executor. + SNIPClient(Service *s) + : CallableFlow(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 *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_