diff --git a/src/openlcb/EventIdentifyGlobal.cxxtest b/src/openlcb/EventIdentifyGlobal.cxxtest new file mode 100644 index 000000000..0d0ded98b --- /dev/null +++ b/src/openlcb/EventIdentifyGlobal.cxxtest @@ -0,0 +1,157 @@ +/** @copyright + * Copyright (c) 2021, Stuart Baker + * 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 EventIdentifyGlobal.cxxtest + * + * Unit tests for Event Identify Global messages. + * + * @author Stuart Baker + * @date 5 April 2021 + */ + +#include "utils/async_if_test_helper.hxx" + +#include "openlcb/EventIdentifyGlobal.hxx" +#include "os/FakeClock.hxx" + +namespace openlcb +{ + +class EventIdentifyGlobalTest : public AsyncNodeTest +{ +protected: + EventIdentifyGlobalTest() + { + flow_.reset(new EventIdentifyGlobal(node_)); + } + + ~EventIdentifyGlobalTest() + { + wait(); + } + + /// Helper function for sleeping. + /// @param clk fake clock or nullptr if no fake clock exists + /// @param len_msec how long to sleep + /// @param step_msec what granularity to use for sleeping wiht fake clock. + void sleep_helper(unsigned len_msec, unsigned step_msec = 50) + { + for (unsigned i = 0; i < len_msec; i += step_msec) + { + clk_.advance(MSEC_TO_NSEC(step_msec)); + wait(); + } + } + + /// Generate the random "hash" from the test Node ID. + long long generate_random() + { + uint64_t h = TEST_NODE_ID * 0x1c19a66d; + uint32_t hh = h ^ (h >> 32); + return (hh ^ (hh >> 12) ^ (hh >> 24)) & 0x1FF; + + } + + FakeClock clk_; + std::unique_ptr flow_; +}; + +TEST_F(EventIdentifyGlobalTest, Create) +{ +} + +TEST_F(EventIdentifyGlobalTest, Arm) +{ + ::testing::Sequence s1; + + flow_->arm(); + + expect_packet(":X1997022AN;").Times(0).InSequence(s1); + sleep_helper(1500 + generate_random() - 50); + + expect_packet(":X1997022AN;").Times(1).InSequence(s1); + sleep_helper(100); +} + +TEST_F(EventIdentifyGlobalTest, ArmMultipleTimes) +{ + ::testing::Sequence s1; + + flow_->arm(); + flow_->arm(); + + expect_packet(":X1997022AN;").Times(0).InSequence(s1); + sleep_helper(1500 + generate_random() - 50); + + flow_->arm(); + + expect_packet(":X1997022AN;").Times(1).InSequence(s1); + sleep_helper(100); +} + +TEST_F(EventIdentifyGlobalTest, ArmWithAbort) +{ + ::testing::Sequence s1; + + flow_->arm(); + + expect_packet(":X1997022AN;").Times(0).InSequence(s1); + sleep_helper(1400); + send_packet(":X19970123N;"); + + expect_packet(":X1997022AN;").Times(0).InSequence(s1); + sleep_helper(100 + generate_random() - 50); + + expect_packet(":X1997022AN;").Times(0).InSequence(s1); + sleep_helper(100); +} + +TEST_F(EventIdentifyGlobalTest, ArmWithLateInitialize) +{ + ::testing::Sequence s1; + + node_->clear_initialized(); + flow_->arm(); + + expect_packet(":X1997022AN;").Times(0).InSequence(s1); + sleep_helper(1500 + generate_random(), 20); + + EXPECT_CALL(canBus_, + mwrite(":X1910022AN02010D000003;")).Times(1).InSequence(s1); + // Causes all nodes to grab a new alias and send out node initialization + // done messages. This object owns itself and will do `delete this;` at the + // end of the process. + new ReinitAllNodes(node_->iface()); + + expect_packet(":X1997022AN;").Times(0).InSequence(s1); + sleep_helper(1500 + generate_random() - 100); + + expect_packet(":X1997022AN;").Times(1).InSequence(s1); + sleep_helper(150); + +} + +} // namespace openlcb diff --git a/src/openlcb/EventIdentifyGlobal.hxx b/src/openlcb/EventIdentifyGlobal.hxx new file mode 100644 index 000000000..19d8e4695 --- /dev/null +++ b/src/openlcb/EventIdentifyGlobal.hxx @@ -0,0 +1,157 @@ +/** @copyright + * Copyright (c) 2021, Stuart Baker + * 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 EventIdentifyGlobal.hxx + * + * Implements the necessary logic to produce EventIdentifyGlobal messages. + * + * @author Stuart Baker + * @date 4 April 2021 + */ + +#ifndef _OPENLCB_EVENTIDENTIFYGLOBAL_HXX_ +#define _OPENLCB_EVENTIDENTIFYGLOBAL_HXX_ + +#include "openlcb/If.hxx" +#include "utils/Atomic.hxx" + +namespace openlcb +{ + +/// Helper object for producing an Event Identify Global message. Once armed, +/// will produce an Event Identify Global message on the node's interface +/// at a pseudo random time between 1.500 and 2.011 seconds in the future. The +/// randomness comes from the 9 least significant bits of the Node ID and aids +/// in reducing the likeliness of multiple nodes of the same design producing +/// the same Event Identify Global message unnecessarily. +class EventIdentifyGlobal : public StateFlowBase, private Atomic +{ +public: + /// Constructor. + /// @param node Node ID associated with this object + EventIdentifyGlobal(Node *node) + : StateFlowBase(node->iface()) + , eventIdentifyGlobalHandler_( + this, &EventIdentifyGlobal::handle_incoming_event_identify_global) + , timer_(this) + , node_(node) + , aborted_(false) + { + node_->iface()->dispatcher()->register_handler( + &eventIdentifyGlobalHandler_, Defs::MTI_EVENTS_IDENTIFY_GLOBAL, + Defs::MTI_EXACT); + } + + /// Destructor. + ~EventIdentifyGlobal() + { + node_->iface()->dispatcher()->unregister_handler( + &eventIdentifyGlobalHandler_, Defs::MTI_EVENTS_IDENTIFY_GLOBAL, + Defs::MTI_EXACT); + } + + /// Arm the EventIdentifyGlobal request. Multi-thread safe. + void arm() + { + AtomicHolder h(this); + if (is_terminated()) + { + aborted_ = false; + start_flow(STATE(entry)); + } + } + +private: + /// Callback upon receiving a Defs::MTI_EVENTS_IDENTIFY_GLOBAL message. + /// @param msg unused, need to unref to prevent memory leak + void handle_incoming_event_identify_global(Buffer *msg) + { + msg->unref(); + AtomicHolder h(this); + aborted_ = true; + } + + /// Entry/reset point into state machine. + Action entry() + { + uint64_t h = node_->node_id() * 0x1c19a66d; + uint32_t hh = h ^ (h >> 32); + hh = hh ^ (hh >> 12) ^ (hh >> 24); + // get a pseudo random number between 1.500 and 2.011 seconds + long long timeout_msec = 1500 + (hh & 0x1FF); + + return sleep_and_call( + &timer_, MSEC_TO_NSEC(timeout_msec), STATE(timeout)); + } + + /// Will be called on the executor of the timer. + Action timeout() + { + { + // Making the state flow termination atomic cleans up a potential + // race condition with an arm() call coming in after the if + // statement but before the StateFlowBase::exit() call. + AtomicHolder h(this); + if (aborted_) + { + // no need to send Event Identify Global, already detected one + return exit(); + } + } + + if (!node_->is_initialized()) + { + // node not initialized yet, try again later + return call_immediately(STATE(entry)); + } + + // allocate a buffer for the Event Identify Global message + return allocate_and_call( + node_->iface()->global_message_write_flow(), STATE(fill_buffer)); + } + + /// Fill in allocated buffer and send Event Identify Global message + Action fill_buffer() + { + auto *b = get_allocation_result( + node_->iface()->global_message_write_flow()); + b->data()->reset( + Defs::MTI_EVENTS_IDENTIFY_GLOBAL, node_->node_id(), EMPTY_PAYLOAD); + node_->iface()->global_message_write_flow()->send(b); + + return exit(); + } + + /// handler for incoming messages + MessageHandler::GenericHandler eventIdentifyGlobalHandler_; + StateFlowTimer timer_; ///< timer object for handling the timeout + Node *node_; ///< node to send message from + uint8_t aborted_ : 1; ///< true if message received, abort need to send +}; + +} // namespace openlcb + +#endif // _OPENLCB_EVENTIDENTIFYGLOBAL_HXX_