From 4fec273a27bb6cc8bbf6fb7292c257026bbbc135 Mon Sep 17 00:00:00 2001 From: Balazs Racz Date: Mon, 16 Aug 2021 16:17:02 +0200 Subject: [PATCH] Adds state flow for logon sequence (#564) - Implements logon feedback callbacks in the LogonHandler - Implements sending logon enable in the LogonHandler - Adds a separate state flow that runs on the discovered locomotives to send addressed messages: select info and logon assign - Adds constants for the address partitions according to S-9.2.1.1 - Adds testability to the API of LogonHandler - Adds unit test exercising logon enable, select info and assign packets and their feedback - Adds the API for the commandstation to implement in the LogonHandlerModule - Adds a default implementation of this API with simple storage components. Misc fixes: - removes PacketFlowInterface as it seems to be duplicate to TrackIf, but less intuitive in the name. - Moves mock TrackIf to a header file to be reusable. === * Merges PacketFlowInterface and TrackIf as these are the same definitions. Moves the mock track IF to a header file to be reused. * Starts implementing the state flow for sending logon packets. * Fixes compile errors. * Adds feedback callbacks to the logon class. Instantiates it in a test. Checks that logon messages are sent every 300 msec. * Adds the state flow that sends out addressed packets to the decoders found. Adds a default (inefficient) implementation of the storage module. * Adds a test that the logon class compiles with the empty logon module. * Wires up the logon feedback to the storage module. Adds a test for checking the select / get short info. * Adds an error state into which locomotives end up when the logon flow fails for some reason. * Completes the assignment sequence. * Fixes style. * update comment * Fixes incorrect constant value. * Fixes logon bugs: - Having no railcom response (empty feedback) should not signal the flow that there was a decoder logging on. - Correctly define the packet repetition count. * Fixes comments. --- src/dcc/Defs.hxx | 25 + src/dcc/Logon.cxxtest | 156 ++++++ src/dcc/Logon.hxx | 609 ++++++++++++++++++++++- src/dcc/LogonModule.hxx | 149 ++++++ src/dcc/PacketFlowInterface.hxx | 49 -- src/dcc/SimpleUpdateLoop.cxx | 3 +- src/dcc/SimpleUpdateLoop.hxx | 4 +- src/dcc/UpdateLoop.hxx | 2 +- src/openlcb/DccAccyConsumer.cxxtest | 4 +- src/openlcb/DccAccyConsumer.hxx | 13 +- src/openlcb/TractionCvSpace.cxx | 2 +- src/openlcb/TractionCvSpace.cxxtest | 27 +- src/openlcb/TractionCvSpace.hxx | 13 +- src/utils/async_traction_test_helper.hxx | 22 +- 14 files changed, 979 insertions(+), 99 deletions(-) create mode 100644 src/dcc/Logon.cxxtest create mode 100644 src/dcc/LogonModule.hxx delete mode 100644 src/dcc/PacketFlowInterface.hxx diff --git a/src/dcc/Defs.hxx b/src/dcc/Defs.hxx index 84f1f626a..732352899 100644 --- a/src/dcc/Defs.hxx +++ b/src/dcc/Defs.hxx @@ -134,6 +134,31 @@ enum CMD_READ_BLOCK = 0b11111110, CMD_READ_BACKGROUND = 0b11111101, CMD_WRITE_BLOCK = 0b11111100, + + // Address partitions as defined by S-9.2.1.1. These are 6-bit values for + // the first byte of the reported and assigned address. + /// 7-bit mobile decoders + ADR_MOBILE_SHORT = 0b00111000, + /// Mask for 7-bit mobile decoders + ADR_MOBILE_SHORT_MASK = 0xFF, + /// 14-bit mobile decoders + ADR_MOBILE_LONG = 0, + /// Maximum value of the first byte for a 14-bit mobile decoder. + MAX_MOBILE_LONG = 0b00100111, + /// 11-bit extended accessory decoder + ADR_ACC_EXT = 0b00101000, + /// Mask for 11-bit extended accessory decoder + MASK_ACC_EXT = 0b00111000, + /// 9-bit basic accessory decoder + ADR_ACC_BASIC = 0b00110000, + /// Mask for 9-bit basic accessory decoder + MASK_ACC_BASIC = 0b00111000, + + /// This value, when given to a decoder, represents an invalid + /// (unassignable) address. This is a 2-byte value that can go to the wire + /// -- above we only have the constants for address partitions, which is + /// the first byte. + ADR_INVALID = (ADR_MOBILE_SHORT << 8), }; /// Parameters for the Logon Enable command. diff --git a/src/dcc/Logon.cxxtest b/src/dcc/Logon.cxxtest new file mode 100644 index 000000000..276ca6e77 --- /dev/null +++ b/src/dcc/Logon.cxxtest @@ -0,0 +1,156 @@ +#include "dcc/Logon.hxx" + +#include "dcc/LogonModule.hxx" +#include "os/FakeClock.hxx" +#include "utils/async_traction_test_helper.hxx" + +using ::testing::ElementsAre; + +namespace dcc +{ + +class LogonTest : public openlcb::TractionTest +{ +protected: + ~LogonTest() + { + logonHandler_.shutdown(); + twait(); + } + + DefaultLogonModule module_; + RailcomHubFlow railcomHub_ {&g_service}; + StrictMock track_; + LogonHandler logonHandler_ { + &g_service, &track_, &railcomHub_, &module_}; +}; + +// This function is never called, and thus is optimized away at linker +// stage. However, it ensures that the logon handler can be compiled with the +// interface as the module parameter. +void compile_test() +{ + LogonHandler *f = nullptr; + f->~LogonHandler(); +} + +TEST_F(LogonTest, create) +{ +} + +TEST_F(LogonTest, logon_per_300msec) +{ + FakeClock clk; + EXPECT_CALL( + track_, packet(ElementsAre(254, 255, 0x22, 0x11, 0x5a), 0xFEFC0000ull)); + logonHandler_.startup_logon(0x2211, 0x5a); + wait(); + Mock::VerifyAndClear(&track_); + clk.advance(MSEC_TO_NSEC(20)); + wait(); + + clk.advance(MSEC_TO_NSEC(250)); + + EXPECT_CALL( + track_, packet(ElementsAre(254, 255, 0x22, 0x11, 0x5a), 0xFEFC0000ull)); + clk.advance(MSEC_TO_NSEC(50)); + wait(); + + EXPECT_CALL( + track_, packet(ElementsAre(254, 255, 0x22, 0x11, 0x5a), 0xFEFC0000ull)); + clk.advance(MSEC_TO_NSEC(300)); + wait(); +} + +TEST_F(LogonTest, select_shortinfo) +{ + FakeClock clk; + EXPECT_CALL( + track_, packet(ElementsAre(254, 255, 0x22, 0x11, 0x5a), 0xFEFC0000ull)) + .Times(AtLeast(1)); + logonHandler_.startup_logon(0x2211, 0x5a); + wait(); + + uint64_t decoder_id = 0x39944332211ull; + auto *b = railcomHub_.alloc(); + RailcomDefs::add_did_feedback(decoder_id, b->data()); + b->data()->feedbackKey = 0xFEFC0000ull; + + EXPECT_CALL(track_, + packet(ElementsAre(254, 0xD3, 0x99, 0x44, 0x33, 0x22, 0x11, 0xFF, _), + 0xFEDFF000ull)); + + railcomHub_.send(b); + wait(); + + clk.advance(MSEC_TO_NSEC(99)); + + // If there is no feedback for a while, the packet will get repeated. + EXPECT_CALL(track_, + packet(ElementsAre(254, 0xD3, 0x99, 0x44, 0x33, 0x22, 0x11, 0xFF, _), + 0xFEDFF000ull)); + clk.advance(MSEC_TO_NSEC(10)); + wait(); + + // After one re-try no more packets are generated for this locomotive. + clk.advance(MSEC_TO_NSEC(500)); + wait(); +} + +TEST_F(LogonTest, full_assign_sequence) +{ + FakeClock clk; + EXPECT_CALL( + track_, packet(ElementsAre(254, 255, 0x22, 0x11, 0x5a), 0xFEFC0000ull)) + .Times(AtLeast(1)); + logonHandler_.startup_logon(0x2211, 0x5a); + wait(); + + uint64_t decoder_id = 0x39944332211ull; + auto *b = railcomHub_.alloc(); + RailcomDefs::add_did_feedback(decoder_id, b->data()); + b->data()->feedbackKey = 0xFEFC0000ull; + + const uintptr_t SELECT_FB_KEY = 0xFEDFF000ull; + EXPECT_CALL(track_, + packet(ElementsAre(254, 0xD3, 0x99, 0x44, 0x33, 0x22, 0x11, 0xFF, _), + SELECT_FB_KEY)); + + railcomHub_.send(b); + wait(); + EXPECT_TRUE( + module_.loco_flags(0) & LogonHandlerModule::FLAG_PENDING_GET_SHORTINFO); + + // Select / shortinfo feedback. + b = railcomHub_.alloc(); + RailcomDefs::add_shortinfo_feedback( + (Defs::ADR_MOBILE_SHORT << 8) | 3, 17, 0, 0, b->data()); + b->data()->feedbackKey = SELECT_FB_KEY; + + const uintptr_t ASSIGN_FB_KEY = 0xFEE00000ull; + + // Assign packet + EXPECT_CALL(track_, + packet(ElementsAre(254, 0xE3, 0x99, 0x44, 0x33, 0x22, 0x11, + 0xC0 | (10000 >> 8), 10000 & 0xFF, _), + ASSIGN_FB_KEY)); + + railcomHub_.send(b); + wait(); + + EXPECT_TRUE( + module_.loco_flags(0) & LogonHandlerModule::FLAG_PENDING_ASSIGN); + + // Assign feedback. + b = railcomHub_.alloc(); + RailcomDefs::add_assign_feedback(0xff, 0xfff, 0, 0, b->data()); + b->data()->feedbackKey = ASSIGN_FB_KEY; + + railcomHub_.send(b); + wait(); + + uint8_t &flags = module_.loco_flags(0); + EXPECT_EQ(LogonHandlerModule::FLAG_COMPLETE, flags); +} + +} // namespace dcc diff --git a/src/dcc/Logon.hxx b/src/dcc/Logon.hxx index a436b62aa..7ca9b5e84 100644 --- a/src/dcc/Logon.hxx +++ b/src/dcc/Logon.hxx @@ -34,10 +34,10 @@ #ifndef _DCC_LOGON_HXX_ #define _DCC_LOGON_HXX_ +#include "dcc/LogonFeedback.hxx" #include "dcc/PacketSource.hxx" #include "dcc/TrackIf.hxx" #include "dcc/UpdateLoop.hxx" -#include "dcc/LogonFeedback.hxx" #include "executor/StateFlow.hxx" namespace dcc @@ -47,14 +47,73 @@ namespace dcc /// Handler. class LogonHandlerModule { +public: + /// @return the number of locomotives known. The locomotive IDs are + /// 0..num_locos() - 1. + unsigned num_locos(); + + /// @param loco_id a locomotive identifier + /// @return true if this is valid and belongs to a loco we know about. + bool is_valid_loco_id(unsigned loco_id); + + /// Finds the storage cell for a locomotive and returns the flag byte for + /// it. + /// @param loco_id a valid locomotive ID. + /// @return the flag byte for this loco. + uint8_t &loco_flags(unsigned loco_id); + + /// Retrieves the decoder unique ID. + /// @param loco_id the dense locomotive identifier. + /// @return the decoder unique ID (44 bit, LSb-aligned). + uint64_t loco_did(unsigned loco_id); + + /// Creates a new locomotive by decoder ID, or looks up an existing + /// locomotive by decoder ID. + /// @param decoder_id 44-bit decoder ID (aligned to LSb). + /// @return locomotive ID for this cell. + unsigned create_or_lookup_loco(uint64_t decoder_id); + + /// Runs the locomotive address policy. After the address policy is run, + /// the loco should have the ability to answer the assigned_address + /// question. + /// @param loco_id which locomotive this is + /// @param desired_address the S-9.2.1.1 encoded desired address for this + /// decoder. + void run_address_policy(unsigned loco_id, uint16_t desired_address); + + /// @param loco_id + /// @return the address to be assigned to this locomotive. 14-bit. + uint16_t assigned_address(unsigned loco_id); + + /// Flags for the logon handler module. + enum Flags + { + /// This decoder needs a get shortinfo command. + FLAG_NEEDS_GET_SHORTINFO = 0x01, + /// We sent a get shortinfo command. + FLAG_PENDING_GET_SHORTINFO = 0x02, + + /// This decoder needs an assign command. + FLAG_NEEDS_ASSIGN = 0x04, + /// We sent an assign command + FLAG_PENDING_ASSIGN = 0x08, + /// 1 if we completed the address assignment. + FLAG_COMPLETE = 0x10, + /// 1 if we ended up in an error state for this loco. + FLAG_ERROR_STATE = 0x20, + /// 1 if we have asked for a re-try. + FLAG_PENDING_RETRY = 0x40, + /// This is a 1-bit pre-scaler on a shared 50 msec timer that controls + /// the delay of re-tries. This makes a retry happen in 50 to 100 msec + /// time from the original failed attempt. + FLAG_PENDING_TICK = 0x80, + }; }; // LogonHandlerModule /// Handles the automatic logon flow for DCC decoders. template -class LogonHandler : public NonTrainPacketSource, - public StateFlowBase, - public RailcomHubPortInterface +class LogonHandler : public StateFlowBase, private LogonFeedbackCallbacks { public: /// Constructor @@ -62,25 +121,559 @@ public: /// @param service points to the executor to use. /// @param track pointer to the track interface to send DCC packets to. /// @param rcom_hub will register to this railcom hub to get feedback. - LogonHandler(Service *service, TrackIf *track, RailcomHubFlow *rcom_hub) + /// @param m the module for the storage and CS interface + LogonHandler( + Service *service, TrackIf *track, RailcomHubFlow *rcom_hub, Module *m) : StateFlowBase(service) , trackIf_(track) + , module_(m) + , fbParser_(this, rcom_hub) + , hasLogonEnableConflict_(0) + , hasLogonEnableFeedback_(0) + , needShutdown_(0) { } /// Initiates a logon sequence at startup. - void startup_logon() + void startup_logon(uint16_t cid, uint8_t session_id) { - start_flow(STATE(startup_logon)); + cid_ = cid; + sessionId_ = session_id; + start_flow(STATE(allocate_logon_now)); + } + +#ifdef GTEST + void shutdown() + { + needShutdown_ = 1; + timer_.ensure_triggered(); + logonSelect_.ensure_triggered(); + } +#endif + + // Callbacks from LogonFeedback + + /// Determines based on feedback key what the given DCC packet was. + /// @param feedback_key from the railcom packet. + /// @return the packet classification wrt the logon feature. + PacketType classify_packet(uintptr_t feedback_key) override + { + LOG(INFO, "classify %x", (unsigned)feedback_key); + if (is_logon_enable_key(feedback_key)) + { + return LOGON_ENABLE; + } + else if (is_select_shortinfo_key(feedback_key)) + { + return SELECT_SHORTINFO; + } + else if (is_logon_assign_key(feedback_key)) + { + return LOGON_ASSIGN; + } + return UNKNOWN; + } + + /// Handles a Select ShortInfo feedback message. + /// @param feedback_key refers to the packet it came from. + /// @param error true if there was a transmission error or the data came in + /// incorrect format. + /// @param data 48 bits of payload. + void process_select_shortinfo( + uintptr_t feedback_key, bool error, uint64_t data) override + { + if (!is_select_shortinfo_key(feedback_key)) + { + LOG(WARNING, "Unexpected select shortinfo key: %08x", + (unsigned)feedback_key); + return; + } + unsigned loco_id = feedback_key & LOCO_ID_MASK; + if (!module_->is_valid_loco_id(loco_id)) + { + LOG(WARNING, + "Unexpected select shortinfo key: %08x - invalid loco id", + (unsigned)feedback_key); + return; + } + uint8_t &flags = module_->loco_flags(loco_id); + flags &= ~LogonHandlerModule::FLAG_PENDING_GET_SHORTINFO; + if (error) + { + if (flags & LogonHandlerModule::FLAG_PENDING_RETRY) + { + flags &= ~LogonHandlerModule::FLAG_PENDING_RETRY; + flags |= LogonHandlerModule::FLAG_ERROR_STATE; + return; + } + else + { + flags |= LogonHandlerModule::FLAG_NEEDS_GET_SHORTINFO | + LogonHandlerModule::FLAG_PENDING_RETRY; + logonSelect_.wakeup(); + return; + } + } + if (flags & + (LogonHandlerModule::FLAG_NEEDS_ASSIGN | + LogonHandlerModule::FLAG_PENDING_ASSIGN)) + { + // Got multiple returns. + return; + } + module_->run_address_policy(loco_id, (data >> 32) & 0x3FFF); + flags |= LogonHandlerModule::FLAG_NEEDS_ASSIGN; + logonSelect_.wakeup(); + } + + /// Handles a Logon Assign feedback message. + /// @param feedback_key refers to the packet it came from. + /// @param error true if there was a transmission error or the data came in + /// incorrect format. + /// @param data 48 bits of payload. + void process_logon_assign( + uintptr_t feedback_key, bool error, uint64_t data) override + { + if (!is_logon_assign_key(feedback_key)) + { + LOG(WARNING, "Unexpected logon assign key: %08x", + (unsigned)feedback_key); + return; + } + unsigned loco_id = feedback_key & LOCO_ID_MASK; + if (!module_->is_valid_loco_id(loco_id)) + { + LOG(WARNING, "Unexpected logon assign key: %08x - invalid loco id", + (unsigned)feedback_key); + return; + } + uint8_t &flags = module_->loco_flags(loco_id); + flags &= ~LogonHandlerModule::FLAG_PENDING_ASSIGN; + if (flags & LogonHandlerModule::FLAG_COMPLETE) + { + // duplicate responses. + return; + } + if (error) + { + if (flags & LogonHandlerModule::FLAG_PENDING_RETRY) + { + flags &= ~LogonHandlerModule::FLAG_PENDING_RETRY; + flags |= LogonHandlerModule::FLAG_ERROR_STATE; + return; + } + else + { + flags |= LogonHandlerModule::FLAG_NEEDS_ASSIGN | + LogonHandlerModule::FLAG_PENDING_RETRY; + logonSelect_.wakeup(); + return; + } + } + flags |= LogonHandlerModule::FLAG_COMPLETE; + flags &= ~LogonHandlerModule::FLAG_PENDING_TICK; + LOG(INFO, "Assign completed for loco %d address %d", loco_id, + module_->assigned_address(loco_id)); + } + + /// Handles a Decoder ID feedback message. + /// @param feedback_key refers to the packet it came from. + /// @param error true if there was a transmission error or the data came in + /// incorrect format. + /// @param data 48 bits of payload. The low 44 bits of this is a decoder ID. + void process_decoder_id(uintptr_t feedback_key, bool error, uint64_t data) + { + timer_.ensure_triggered(); + if (data) + { + hasLogonEnableFeedback_ = 1; + } else { + // No railcom feedback returned. + return; + } + if (error) + { + hasLogonEnableConflict_ = 1; + return; + } + uint64_t did = data & DECODER_ID_MASK; + auto lid = module_->create_or_lookup_loco(did); + if (!module_->is_valid_loco_id(lid)) + { + return; + } + auto &flags = module_->loco_flags(lid); + flags |= LogonHandlerModule::FLAG_NEEDS_GET_SHORTINFO; + logonSelect_.wakeup(); } private: - Action startup_logon() + /// Allocates a buffer and sends a Logon Enable(now) packet. + Action allocate_logon_now() + { + return allocate_and_call(trackIf_, STATE(send_logon_now)); + } + + /// Sends a Logon Enable(now) packet. + Action send_logon_now() + { + logon_send_helper(Defs::LogonEnableParam::NOW, 0); + return wait_and_call(STATE(start_logon_wait)); + } + + /// Called when the logon now packet is released. This means the packet is + /// enqueued in the device driver, but not necessarily that it is on the + /// track yet. + /// + /// Computes the next time that we need to send out a logon packet, and + /// starts a sleep. + Action start_logon_wait() + { + if (needShutdown_) + { + return exit(); + } + auto next_time = lastLogonTime_ + MSEC_TO_NSEC(LOGON_PERIOD_MSEC); + timer_.start_absolute(next_time); + return wait_and_call(STATE(evaluate_logon)); + } + + /// Called when the logon timer expires or is cancelled due to feedback. + Action evaluate_logon() + { + if (needShutdown_) + { + return exit(); + } + if (timer_.is_triggered()) + { + // found something via logon + if (hasLogonEnableConflict_) + { + hasLogonEnableConflict_ = 0; + hasLogonEnableFeedback_ = 0; + return call_immediately(STATE(allocate_logon_many)); + } + // Not sure why we were woken up, let's start a sleep again. + return call_immediately(STATE(start_logon_wait)); + } + else + { + // timer expired, send another logon. + return call_immediately(STATE(allocate_logon_now)); + } + } + + /// Called when we have seen a conflict on a logon enable packet. Allocates + /// a buffer to send out many logon packets. + Action allocate_logon_many() + { + return allocate_and_call(trackIf_, STATE(send_logon_many)); + } + + /// Send out the repeated logon request. + Action send_logon_many() + { + logon_send_helper(Defs::LogonEnableParam::ALL, 3); + return wait_and_call(STATE(many_logon_wait)); + } + + /// Called after the many logon packets' buffer is freed. + Action many_logon_wait() + { + if (countLogonToSend_ >= 4) + { + countLogonToSend_ -= 4; + return call_immediately(STATE(allocate_logon_many)); + } + else + { + countLogonToSend_ = 0; + /// @TODO we should really evaluate whether we've seen conflicts + /// coming back. + return call_immediately(STATE(start_logon_wait)); + } + } + + /// Helper function to send out logon enable commands. Requirement: a + /// buffer is allocated before calling. + /// @param param option of which logon enable command to send out. + /// @param rept 0-3 for how many repeats to send out (N-1, so it means 1-4 + /// repeats). + void logon_send_helper(Defs::LogonEnableParam param, unsigned rept) { + HASSERT(rept < 4u); + auto *b = get_allocation_result(trackIf_); + b->data()->set_dcc_logon_enable(param, cid_, sessionId_); + b->data()->feedback_key = LOGON_ENABLE_KEY; + b->data()->packet_header.rept_count = rept; + b->set_done(bn_.reset(this)); + + hasLogonEnableFeedback_ = 0; + hasLogonEnableConflict_ = 0; + lastLogonTime_ = os_get_time_monotonic(); + + trackIf_->send(b); } + class LogonSelect; + friend class LogonSelect; + + /// Flow that sends out addressed packets that are part of the logon + /// sequences. + class LogonSelect : public StateFlowBase, public ::Timer + { + public: + LogonSelect(LogonHandler *parent) + : StateFlowBase(parent->service()) + , ::Timer(parent->service()->executor()->active_timers()) + , parent_(parent) + { + start(MSEC_TO_NSEC(50)); + } + + /// Notifies the flow that there is work to do. + void wakeup() + { + if (is_terminated()) + { + cycleNextId_ = 0; + start_flow(STATE(search)); + } + } + + /// Called by a timer every 50 msec. + void tick() + { + bool need_wakeup = false; + for (unsigned id = 0; id < m()->num_locos() && id < MAX_LOCO_ID; + ++id) + { + uint8_t &fl = m()->loco_flags(cycleNextId_); + if (fl & LogonHandlerModule::FLAG_PENDING_TICK) + { + fl &= ~LogonHandlerModule::FLAG_PENDING_TICK; + } + else if (fl & LogonHandlerModule::FLAG_PENDING_RETRY) + { + fl &= ~LogonHandlerModule::FLAG_PENDING_RETRY; + fl |= LogonHandlerModule::FLAG_ERROR_STATE; + } + else if (fl & LogonHandlerModule::FLAG_ERROR_STATE) + { + /// @todo locomotives that are in error state should be + // re-tried every now and then. We would probably need an + // extra counter for this though somewhere. + } + else if (fl & LogonHandlerModule::FLAG_PENDING_GET_SHORTINFO) + { + fl &= ~LogonHandlerModule::FLAG_PENDING_GET_SHORTINFO; + fl |= LogonHandlerModule::FLAG_NEEDS_GET_SHORTINFO | + LogonHandlerModule::FLAG_PENDING_RETRY; + need_wakeup = true; + } + else if (fl & LogonHandlerModule::FLAG_PENDING_ASSIGN) + { + fl &= ~LogonHandlerModule::FLAG_PENDING_ASSIGN; + fl |= LogonHandlerModule::FLAG_NEEDS_ASSIGN | + LogonHandlerModule::FLAG_PENDING_RETRY; + need_wakeup = true; + } + } + if (need_wakeup) + { + wakeup(); + } + } + + private: + /// Timer callback. + long long timeout() override + { + if (parent_->needShutdown_) + { + return 0; + } + tick(); + return RESTART; + } + + /// @return the pointer to the storage module. + Module *m() + { + return parent_->module_; + } + + /// @return the pointer to the track interface. + TrackIf *track() + { + return parent_->trackIf_; + } + + /// Entry to the flow. Looks through the states to see what we needs to + /// be done. + Action search() + { + if (parent_->needShutdown_) + { + return exit(); + } + bool mid_cycle = (cycleNextId_ != 0); + for (; + cycleNextId_ < m()->num_locos() && cycleNextId_ <= MAX_LOCO_ID; + ++cycleNextId_) + { + uint8_t fl = m()->loco_flags(cycleNextId_); + if (fl & LogonHandlerModule::FLAG_NEEDS_GET_SHORTINFO) + { + return allocate_and_call( + parent_->trackIf_, STATE(send_get_shortinfo)); + } + if (fl & LogonHandlerModule::FLAG_NEEDS_ASSIGN) + { + return allocate_and_call( + parent_->trackIf_, STATE(send_assign)); + } + } + cycleNextId_ = 0; + // Check if we need to run the search for the first half of the + // loco space too. + if (mid_cycle) + { + return again(); + } + return exit(); + } + + /// Called with a buffer allocated. Sends a get shortinfo command to + /// the current decoder. + Action send_get_shortinfo() + { + auto *b = get_allocation_result(parent_->trackIf_); + uint64_t did = m()->loco_did(cycleNextId_); + b->data()->set_dcc_select_shortinfo(did); + b->data()->feedback_key = + SELECT_SHORTINFO_KEY | (cycleNextId_ & LOCO_ID_MASK); + b->set_done(bn_.reset((StateFlowBase *)this)); + track()->send(b); + uint8_t &fl = m()->loco_flags(cycleNextId_); + fl &= ~LogonHandlerModule::FLAG_NEEDS_GET_SHORTINFO; + fl |= LogonHandlerModule::FLAG_PENDING_GET_SHORTINFO | + LogonHandlerModule::FLAG_PENDING_TICK; + return wait_and_call(STATE(search)); + } + + /// Called with a buffer allocated. Sends an assign command to + /// the current decoder. + Action send_assign() + { + auto *b = get_allocation_result(parent_->trackIf_); + uint64_t did = m()->loco_did(cycleNextId_); + b->data()->set_dcc_logon_assign( + did, m()->assigned_address(cycleNextId_)); + b->data()->feedback_key = + LOGON_ASSIGN_KEY | (cycleNextId_ & LOCO_ID_MASK); + b->set_done(bn_.reset((StateFlowBase *)this)); + track()->send(b); + uint8_t &fl = m()->loco_flags(cycleNextId_); + fl &= ~LogonHandlerModule::FLAG_NEEDS_ASSIGN; + fl |= LogonHandlerModule::FLAG_PENDING_ASSIGN | + LogonHandlerModule::FLAG_PENDING_TICK; + return wait_and_call(STATE(search)); + } + + /// Owning logon handler. + LogonHandler *parent_; + + /// Identifier of the storage that provides the next locomotive to look + /// at. + unsigned cycleNextId_; + + /// Helper for self notification. + BarrierNotifiable bn_; + } logonSelect_ {this}; + + /// We send this as feedback key for logon enable packets. + static constexpr uintptr_t LOGON_ENABLE_KEY = + uint32_t((Defs::ADDRESS_LOGON << 24) | (Defs::DCC_LOGON_ENABLE << 16)); + + /// Checks if a feedback key is for logon enable. + /// @param feedback_key the key + /// @return true if this is for a logon enable + static constexpr bool is_logon_enable_key(uintptr_t feedback_key) + { + return feedback_key == LOGON_ENABLE_KEY; + } + + /// We send this as feedback key for select/get short info packets. + static constexpr uintptr_t SELECT_SHORTINFO_KEY = + uint32_t((Defs::ADDRESS_LOGON << 24) | (Defs::DCC_SELECT << 16) | + (Defs::CMD_READ_SHORT_INFO << 12)); + + /// Checks if a feedback key is for select shortinfo. + /// @param feedback_key the key + /// @return true if this is for a select short info + static constexpr bool is_select_shortinfo_key(uintptr_t feedback_key) + { + return ((feedback_key & ~LOCO_ID_MASK) == SELECT_SHORTINFO_KEY); + } + + /// We send this as feedback key for logon assign packets. + static constexpr uintptr_t LOGON_ASSIGN_KEY = + uint32_t((Defs::ADDRESS_LOGON << 24) | (Defs::DCC_LOGON_ASSIGN << 16)); + + /// Checks if a feedback key is for logon assign. + /// @param feedback_key the key + /// @return true if this is for a logon assign + static constexpr bool is_logon_assign_key(uintptr_t feedback_key) + { + return ((feedback_key & ~LOCO_ID_MASK) == LOGON_ASSIGN_KEY); + } + + /// How often to send logon enable packets. + static constexpr unsigned LOGON_PERIOD_MSEC = 295; + + /// Maximum allowed locomotive ID. + static constexpr unsigned MAX_LOCO_ID = 0xfff; + /// Mask selecting bits that belong to the locomotive ID. + static constexpr uintptr_t LOCO_ID_MASK = MAX_LOCO_ID; + + /// Mask selecting bits that belong to the decoder ID. + static constexpr uint64_t DECODER_ID_MASK = (1ull << 44) - 1; + /// If we need to send packets to the track, we can do it here directly. TrackIf *trackIf_; + + /// Storage module. + Module *module_; + + /// Helper object for timing. + StateFlowTimer timer_ {this}; + + /// Helper object for parsing railcom feedback from the railcom hub. + LogonFeedbackParser fbParser_; + + BarrierNotifiable bn_; + + /// Timestamp of the last logon packet we sent out. + long long lastLogonTime_ {0}; + + /// Command station unique ID. + uint16_t cid_; + /// Session ID of the current session. + uint8_t sessionId_; + + /// 1 if we got an error (presumably a conflict) in the logon enable + /// feedback. + uint8_t hasLogonEnableConflict_ : 1; + /// 1 if we got any feedback packet from logon enable. + uint8_t hasLogonEnableFeedback_ : 1; + /// Signals that we need to shut down the flow. + uint8_t needShutdown_ : 1; + + /// Tracks how many logons to send out. + uint8_t countLogonToSend_ {0}; + }; // LogonHandler } // namespace dcc diff --git a/src/dcc/LogonModule.hxx b/src/dcc/LogonModule.hxx new file mode 100644 index 000000000..66649a102 --- /dev/null +++ b/src/dcc/LogonModule.hxx @@ -0,0 +1,149 @@ +/** \copyright + * Copyright (c) 2021, 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 LogonModule.hxx + * Default implementation of a storage and policy module for automatic logon. + * + * @author Balazs Racz + * @date 14 Aug 2021 + */ + +#ifndef _DCC_LOGONMODULE_HXX_ +#define _DCC_LOGONMODULE_HXX_ + +#include +#include + +#include "dcc/Defs.hxx" +#include "dcc/Logon.hxx" + +namespace dcc +{ + +/// Default implementation of the storage and policy module for trains. +class DefaultLogonModule : public LogonHandlerModule +{ +public: + /// We store this structure about each locomotive. + struct LocoInfo + { + /// State machine flags about this loco. + uint8_t flags_ {0}; + + /// The assigned DCC address. The encoding is in the S-9.2.1.1 format. + /// The default value is an invalid address causing an error on the + /// locomotive. + uint16_t assignedAddress_ {Defs::ADR_INVALID}; + + /// 44-bit decoder unique ID. + uint64_t decoderId_; + }; + + std::vector locos_; + std::map ids_; + + /// @return the number of locomotives known. The locomotive IDs are + /// 0..num_locos() - 1. + unsigned num_locos() + { + return locos_.size(); + } + + /// @param loco_id a locomotive identifier + /// @return true if this is valid and belongs to a loco we know about. + bool is_valid_loco_id(unsigned loco_id) + { + return loco_id < num_locos(); + } + + /// Finds the storage cell for a locomotive and returns the flag byte for + /// it. + /// @param loco_id a valid locomotive ID. + /// @return the flag byte for this loco. + uint8_t &loco_flags(unsigned loco_id) + { + return locos_[loco_id].flags_; + } + + /// Retrieves the decoder unique ID. + /// @param loco_id the dense locomotive identifier. + /// @return the decoder unique ID (44 bit, LSb-aligned). + uint64_t loco_did(unsigned loco_id) + { + return locos_[loco_id].decoderId_; + } + + /// Creates a new locomotive by decoder ID, or looks up an existing + /// locomotive by decoder ID. + /// @param decoder_id 44-bit decoder ID (aligned to LSb). + /// @return locomotive ID for this cell. + unsigned create_or_lookup_loco(uint64_t decoder_id) + { + auto it = ids_.find(decoder_id); + if (it == ids_.end()) + { + // create new. + uint16_t lid = locos_.size(); + locos_.emplace_back(); + locos_[lid].decoderId_ = decoder_id; + ids_[decoder_id] = lid; + return lid; + } + else + { + return it->second; + } + } + + /// Runs the locomotive address policy. After the address policy is run, + /// the loco should have the ability to answer the assigned_address + /// question. + /// @param loco_id which locomotive this is + /// @param desired_address the S-9.2.1.1 encoded desired address for this + /// decoder. + void run_address_policy(unsigned loco_id, uint16_t desired_address) + { + /// @todo support accessory decoders. + + // Note: we ignore the desired address and start assigning addresses + // from 10000 and up. + locos_[loco_id].assignedAddress_ = nextAddress_++; + } + + /// @param loco_id + /// @return the address to be assigned to this locomotive. 14-bit. + uint16_t assigned_address(unsigned loco_id) + { + return locos_[loco_id].assignedAddress_; + } + + uint16_t nextAddress_ {(Defs::ADR_MOBILE_LONG << 8) + 10000}; + +}; // class DefaultLogonModule + +} // namespace dcc + +#endif // _DCC_LOGONMODULE_HXX_ diff --git a/src/dcc/PacketFlowInterface.hxx b/src/dcc/PacketFlowInterface.hxx deleted file mode 100644 index f6f5b900f..000000000 --- a/src/dcc/PacketFlowInterface.hxx +++ /dev/null @@ -1,49 +0,0 @@ -/** \copyright - * Copyright (c) 2015, 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 PacketFlowInterface.hxx - * - * Shared declarations for sending DCC packets. - * - * @author Balazs Racz - * @date 16 May 2015 - */ - -#ifndef _DCC_PACKETFLOWINTERFACE_HXX_ -#define _DCC_PACKETFLOWINTERFACE_HXX_ - -#include "executor/StateFlow.hxx" -#include "dcc/Packet.hxx" - -namespace dcc { - -/// Interface for flows and ports receiving a sequence of DCC (track) packets. -typedef FlowInterface> PacketFlowInterface; - -} // namespace dcc - - -#endif diff --git a/src/dcc/SimpleUpdateLoop.cxx b/src/dcc/SimpleUpdateLoop.cxx index 70e7bbb4f..1383b808e 100644 --- a/src/dcc/SimpleUpdateLoop.cxx +++ b/src/dcc/SimpleUpdateLoop.cxx @@ -40,8 +40,7 @@ namespace dcc { -SimpleUpdateLoop::SimpleUpdateLoop(Service *service, - PacketFlowInterface *track_send) +SimpleUpdateLoop::SimpleUpdateLoop(Service *service, TrackIf *track_send) : StateFlow(service) , trackSend_(track_send) , nextRefreshIndex_(0) diff --git a/src/dcc/SimpleUpdateLoop.hxx b/src/dcc/SimpleUpdateLoop.hxx index 5943bb514..cb91dbaaa 100644 --- a/src/dcc/SimpleUpdateLoop.hxx +++ b/src/dcc/SimpleUpdateLoop.hxx @@ -62,7 +62,7 @@ class SimpleUpdateLoop : public StateFlow, QList<1>>, private UpdateLoopBase { public: - SimpleUpdateLoop(Service *service, PacketFlowInterface *track_send); + SimpleUpdateLoop(Service *service, TrackIf *track_send); ~SimpleUpdateLoop(); /** Adds a new refresh source to the background refresh packets. */ @@ -94,7 +94,7 @@ public: private: // Place where we forward the packets filled in. - PacketFlowInterface *trackSend_; + TrackIf *trackSend_; // Packet sources to ask about refreshing data periodically. vector refreshSources_; diff --git a/src/dcc/UpdateLoop.hxx b/src/dcc/UpdateLoop.hxx index 0d19be8b8..bd8c3f65a 100644 --- a/src/dcc/UpdateLoop.hxx +++ b/src/dcc/UpdateLoop.hxx @@ -35,8 +35,8 @@ #ifndef _DCC_UPDATELOOP_HXX_ #define _DCC_UPDATELOOP_HXX_ +#include "dcc/TrackIf.hxx" #include "utils/Singleton.hxx" -#include "dcc/PacketFlowInterface.hxx" namespace dcc { diff --git a/src/openlcb/DccAccyConsumer.cxxtest b/src/openlcb/DccAccyConsumer.cxxtest index 1b88a7cc3..d961e3878 100644 --- a/src/openlcb/DccAccyConsumer.cxxtest +++ b/src/openlcb/DccAccyConsumer.cxxtest @@ -32,16 +32,16 @@ * @date 4 Feb 2017 */ +#include "dcc/TrackIf.hxx" #include "openlcb/DccAccyConsumer.hxx" #include "utils/async_traction_test_helper.hxx" -#include "dcc/PacketFlowInterface.hxx" namespace openlcb { using ::testing::ElementsAre; -class MockPacketQueue : public dcc::PacketFlowInterface +class MockPacketQueue : public dcc::TrackIf { public: MOCK_METHOD2(arrived, void(uint8_t, vector)); diff --git a/src/openlcb/DccAccyConsumer.hxx b/src/openlcb/DccAccyConsumer.hxx index 44b89f42a..1f6c47721 100644 --- a/src/openlcb/DccAccyConsumer.hxx +++ b/src/openlcb/DccAccyConsumer.hxx @@ -36,9 +36,9 @@ #ifndef _OPENLCB_DCCACCYCONSUMER_HXX_ #define _OPENLCB_DCCACCYCONSUMER_HXX_ -#include "openlcb/TractionDefs.hxx" +#include "dcc/TrackIf.hxx" #include "openlcb/EventHandlerTemplates.hxx" -#include "dcc/PacketFlowInterface.hxx" +#include "openlcb/TractionDefs.hxx" namespace openlcb { @@ -51,8 +51,9 @@ public: /// responding to Identify messages. /// @param track is the interface through which we will be writing DCC /// accessory packets. - DccAccyConsumer(Node *node, dcc::PacketFlowInterface* track) - : node_(node), track_(track) + DccAccyConsumer(Node *node, dcc::TrackIf *track) + : node_(node) + , track_(track) { EventRegistry::instance()->register_handler( EventRegistryEntry( @@ -110,7 +111,7 @@ public: lastSetState_[eventOfs_] &= ~m; } - dcc::PacketFlowInterface::message_type *pkt; + dcc::TrackIf::message_type *pkt; mainBufferPool->alloc(&pkt); pkt->data()->add_dcc_basic_accessory(dccAddress_, onOff_); pkt->data()->packet_header.rept_count = 3; @@ -216,7 +217,7 @@ private: /// OpenLCB node to export the consumer on. Node *node_; /// Track to send DCC packets to. - dcc::PacketFlowInterface* track_; + dcc::TrackIf *track_; }; } // namespace openlcb diff --git a/src/openlcb/TractionCvSpace.cxx b/src/openlcb/TractionCvSpace.cxx index ab85de087..c345938ca 100644 --- a/src/openlcb/TractionCvSpace.cxx +++ b/src/openlcb/TractionCvSpace.cxx @@ -52,7 +52,7 @@ namespace openlcb { TractionCvSpace::TractionCvSpace(MemoryConfigHandler *parent, - dcc::PacketFlowInterface *track, + dcc::TrackIf *track, dcc::RailcomHubFlow *railcom_hub, uint8_t space_id) : StateFlowBase(parent->service()) diff --git a/src/openlcb/TractionCvSpace.cxxtest b/src/openlcb/TractionCvSpace.cxxtest index 17e322c58..dc95cd53c 100644 --- a/src/openlcb/TractionCvSpace.cxxtest +++ b/src/openlcb/TractionCvSpace.cxxtest @@ -1,10 +1,11 @@ -#include "utils/async_traction_test_helper.hxx" -#include "openlcb/TractionTestTrain.hxx" #include "openlcb/TractionCvSpace.hxx" -#include "openlcb/MemoryConfig.hxx" -#include "openlcb/DatagramCan.hxx" -#include "dcc/PacketFlowInterface.hxx" + #include "dcc/RailcomHub.hxx" +#include "dcc/TrackIf.hxx" +#include "openlcb/DatagramCan.hxx" +#include "openlcb/MemoryConfig.hxx" +#include "openlcb/TractionTestTrain.hxx" +#include "utils/async_traction_test_helper.hxx" using ::testing::ElementsAre; @@ -13,20 +14,6 @@ namespace openlcb extern Pool *const g_incoming_datagram_allocator = mainBufferPool; -class MockTrackIf : public dcc::PacketFlowInterface -{ -public: - MOCK_METHOD2(packet, - void(const vector &payload, uintptr_t feedback_key)); - void send(Buffer *b, unsigned prio) OVERRIDE - { - vector payload; - payload.assign(b->data()->payload, - b->data()->payload + b->data()->dlc - 1); - this->packet(payload, b->data()->feedback_key); - } -}; - class TractionCvTestBase : public TractionTest { protected: @@ -83,7 +70,7 @@ protected: CanDatagramService datagram_support_{ifCan_.get(), 10, 2}; MemoryConfigHandler memory_config_handler_{&datagram_support_, nullptr, 3}; dcc::RailcomHubFlow railcom_hub_{&g_service}; - StrictMock track_if_; + StrictMock track_if_; TractionCvSpace cv_space_{&memory_config_handler_, &track_if_, &railcom_hub_, 0xEF}; }; diff --git a/src/openlcb/TractionCvSpace.hxx b/src/openlcb/TractionCvSpace.hxx index 656512a99..39b65587d 100644 --- a/src/openlcb/TractionCvSpace.hxx +++ b/src/openlcb/TractionCvSpace.hxx @@ -36,11 +36,11 @@ #ifndef _OPENLCB_TRACTIONCVSPACE_HXX_ #define _OPENLCB_TRACTIONCVSPACE_HXX_ -#include "openlcb/MemoryConfig.hxx" -#include "executor/StateFlow.hxx" -#include "dcc/PacketFlowInterface.hxx" #include "dcc/RailCom.hxx" #include "dcc/RailcomHub.hxx" +#include "dcc/TrackIf.hxx" +#include "executor/StateFlow.hxx" +#include "openlcb/MemoryConfig.hxx" namespace openlcb { @@ -68,9 +68,8 @@ class TractionCvSpace : private MemorySpace, public StateFlowBase { public: - TractionCvSpace(MemoryConfigHandler *parent, - dcc::PacketFlowInterface *track, - dcc::RailcomHubFlow *railcom_hub, uint8_t space_id); + TractionCvSpace(MemoryConfigHandler *parent, dcc::TrackIf *track, + dcc::RailcomHubFlow *railcom_hub, uint8_t space_id); ~TractionCvSpace(); @@ -117,7 +116,7 @@ private: void record_railcom_status(unsigned code); MemoryConfigHandler *parent_; - dcc::PacketFlowInterface *track_; + dcc::TrackIf *track_; dcc::RailcomHubFlow *railcomHub_; uint16_t dccAddress_; uint16_t cvNumber_; diff --git a/src/utils/async_traction_test_helper.hxx b/src/utils/async_traction_test_helper.hxx index 32d1a15ee..a49518da8 100644 --- a/src/utils/async_traction_test_helper.hxx +++ b/src/utils/async_traction_test_helper.hxx @@ -3,6 +3,7 @@ #include "utils/async_if_test_helper.hxx" +#include "dcc/TrackIf.hxx" #include "openlcb/TractionDefs.hxx" #include "openlcb/TractionTrain.hxx" #include "utils/MockTrain.hxx" @@ -23,7 +24,26 @@ protected: StrictMock m1_, m2_; }; - } // namespace openlcb +namespace dcc +{ + +class MockTrackIf : public dcc::TrackIf +{ +public: + MOCK_METHOD2( + packet, void(const vector &payload, uintptr_t feedback_key)); + void send(Buffer *b, unsigned prio) OVERRIDE + { + vector payload; + payload.assign( + b->data()->payload, b->data()->payload + b->data()->dlc - 1); + this->packet(payload, b->data()->feedback_key); + b->unref(); + } +}; + +} + #endif // _UTILS_ASYNC_TRACTION_TEST_HELPER_HXX_