From f0a0091b3d051a5e06bf00feff11a1c13e0ac8eb Mon Sep 17 00:00:00 2001 From: Balazs Racz Date: Sun, 4 Feb 2024 15:38:39 +0100 Subject: [PATCH] Adds support for DCC extended accessories (#769) - Adds helper functions to generate dcc extended accessory packets in dcc::Packet - Adds well-known event range (from the current draft eventidentifiers standard) - Adds DccExtAccyConsumer, which listens to events and turns them into DCC packets. Unlike the basic dcc accy consumer, the extended consumer does not remember whatthe last set state was. This is because it takes a lot more memory (2 kbytes), and it's a lot less useful, since different decoders may be interpreting this last set command very differently (such as a signal, a turntable, or a timed turnout decoder). === * Adds command to generate dcc extended accessory packet. * Adds well-known range for dcc extended accessories (according to the draft event identifiers standard). * Adds accessory consumer for extended accessories. Generates the dcc packets on the rails. --- src/dcc/Packet.cxx | 8 ++ src/dcc/Packet.cxxtest | 15 +++ src/dcc/Packet.hxx | 11 +++ src/openlcb/DccAccyConsumer.cxxtest | 62 ++++++++++++ src/openlcb/DccAccyConsumer.hxx | 144 ++++++++++++++++++++++++++++ src/openlcb/TractionDefs.hxx | 2 + 6 files changed, 242 insertions(+) diff --git a/src/dcc/Packet.cxx b/src/dcc/Packet.cxx index 8d51abb29..757a448d5 100644 --- a/src/dcc/Packet.cxx +++ b/src/dcc/Packet.cxx @@ -346,6 +346,14 @@ void Packet::add_dcc_basic_accessory(unsigned address, bool is_activate) add_dcc_checksum(); } +void Packet::add_dcc_ext_accessory(unsigned address, uint8_t aspect) +{ + add_dcc_accy_address(false, address); + payload[dlc++] = aspect; + add_dcc_checksum(); +} + + void Packet::set_dcc_logon_enable( Defs::LogonEnableParam param, uint16_t cid, uint8_t session_id) { diff --git a/src/dcc/Packet.cxxtest b/src/dcc/Packet.cxxtest index 8cae24eb5..530c51968 100644 --- a/src/dcc/Packet.cxxtest +++ b/src/dcc/Packet.cxxtest @@ -373,6 +373,21 @@ TEST_F(PacketTest, DccBasicAccyPom) 0b10011100)); } +TEST_F(PacketTest, DccExtAccySet) +{ + pkt_.add_dcc_ext_accessory(1733, 0x5a); + // 1733 = 0x6c5 = 0b 110 1100 0101 + uint8_t b1 = 0b10110001; + uint8_t b2 = 0b00010011; + uint8_t b3 = 0x5a; + EXPECT_THAT(get_packet(), ElementsAre(b1, b2, b3, b1 ^ b2 ^ b3)); + + pkt_.clear(); + pkt_.add_dcc_ext_accessory(1733, 0x80); + b3 = 0x80; + EXPECT_THAT(get_packet(), ElementsAre(b1, b2, b3, b1 ^ b2 ^ b3)); +} + TEST_F(PacketTest, SvcProgDirectByte) { diff --git a/src/dcc/Packet.hxx b/src/dcc/Packet.hxx index c7ca853ad..712191d98 100644 --- a/src/dcc/Packet.hxx +++ b/src/dcc/Packet.hxx @@ -310,6 +310,17 @@ struct Packet : public DCCPacket /// @param is_normal true for normal, false for reverse /// @param is_activate true for activate, false for deactivate void set_dcc_basic_accy_params(bool is_normal, bool is_activate); + + /// Adds a DCC extended accessory decoder command packet and the checksum + /// byte. + /// @param address is the 11-bit binary address, 0..2047. No bits have to be + /// inverted. This will be A10..A0 on the track. (To convert from a user + /// address, see accy_address_user_to_binary in dcc::Defs.) + /// @param aspect is the argument byte to the extended + /// accessory. Traditionally this was used as an aspect for a signal + /// decoder, but different accessories might have different interpretation + /// of it. + void add_dcc_ext_accessory(unsigned address, uint8_t aspect); /// Sets the packet to a logon enable packet. /// @param param defines which decoders should be requested to logon. diff --git a/src/openlcb/DccAccyConsumer.cxxtest b/src/openlcb/DccAccyConsumer.cxxtest index a09dbcb28..13bbac1f1 100644 --- a/src/openlcb/DccAccyConsumer.cxxtest +++ b/src/openlcb/DccAccyConsumer.cxxtest @@ -140,4 +140,66 @@ TEST_F(DccAccyTest, packet_throw) wait(); } + +class DccExtAccyTest : public AsyncNodeTest +{ +protected: + DccExtAccyTest() + { + wait(); + } + + StrictMock trackSendQueue_; + DccExtAccyConsumer consumer_{node_, &trackSendQueue_}; +}; + +TEST_F(DccExtAccyTest, create) +{ +} + +TEST_F(DccExtAccyTest, global_identify) +{ + clear_expect(true); + // Consumer range for one very long range (11 bit plus 8 bit). + expect_packet(":X194A422AN010102000107FFFF;"); + + // identify events addressed. + send_packet(":X19968111N022A;"); + wait(); +} + + +TEST_F(DccExtAccyTest, identify_unknown) +{ + // unknown identify + send_packet_and_expect_response( + ":X198F4111N0101020001035a77;", ":X194C722AN0101020001035a77;"); +} + +TEST_F(DccExtAccyTest, packet_throw) +{ + uint8_t hdr = 0b01100100; + EXPECT_CALL(trackSendQueue_, + arrived(hdr, ElementsAre(0b10110001, 0b00010011, 0xa5, _))); + + // set to 0xa5 + send_packet(":X195B4111N010102000106c5a5;"); + + + EXPECT_CALL(trackSendQueue_, + arrived(hdr, ElementsAre(0b10110001, 0b00010011, 0x00, _))); + + // set to 0x00 + send_packet(":X195B4111N010102000106c500;"); + + EXPECT_CALL(trackSendQueue_, + arrived(hdr, ElementsAre(0b10110001, 0b00010011, 0xff, _))); + + // set to 0xff + send_packet(":X195B4111N010102000106c5ff;"); + + wait(); +} + + } // namespace openlcb diff --git a/src/openlcb/DccAccyConsumer.hxx b/src/openlcb/DccAccyConsumer.hxx index 5bc716829..f486ea614 100644 --- a/src/openlcb/DccAccyConsumer.hxx +++ b/src/openlcb/DccAccyConsumer.hxx @@ -260,6 +260,150 @@ private: dcc::TrackIf *track_; }; +/// Base (generic protocol) implementation of the DCC extended accessory +/// consumer. Unlike the basic accessory version, this one does not remember +/// the last set state. +class DccExtAccyConsumerBase : public SimpleEventHandler +{ +protected: + /// How may addresses are there for extended accessories. + static constexpr unsigned NUM_ADDRESS = 2048; + /// How may aspects are supported per accessory. + static constexpr unsigned NUM_ASPECT = 256; + /// Total number of events we are listening for. + static constexpr unsigned NUM_EVENT = NUM_ASPECT * NUM_ADDRESS; + + /// Constructs a listener for DCC extended accessory control. + /// @param node is the virtual node that will be listening for events and + /// responding to Identify messages. + DccExtAccyConsumerBase(Node *node) + : node_(node) + { + EventRegistry::instance()->register_handler( + EventRegistryEntry( + this, TractionDefs::EXT_DCC_ACCESSORY_EVENT_BASE), + 11+8 /*number of bits*/); + } + + /// Destructor. + ~DccExtAccyConsumerBase() + { + EventRegistry::instance()->unregister_handler(this); + } + + void handle_identify_global(const EventRegistryEntry ®istry_entry, + EventReport *event, BarrierNotifiable *done) OVERRIDE + { + AutoNotify an(done); + if (event->dst_node && event->dst_node != node_) + { + return; + } + event->event_write_helper<1>()->WriteAsync(node_, + Defs::MTI_CONSUMER_IDENTIFIED_RANGE, WriteHelper::global(), + eventid_to_buffer(EncodeRange( + TractionDefs::EXT_DCC_ACCESSORY_EVENT_BASE, NUM_EVENT - 1)), + done->new_child()); + } + + void handle_event_report(const EventRegistryEntry ®istry_entry, + EventReport *event, BarrierNotifiable *done) override + { + AutoNotify an(done); + if (!parse_event(event->event)) + { + return; + } + send_accy_command(); + } + + void handle_identify_consumer(const EventRegistryEntry &entry, + EventReport *event, BarrierNotifiable *done) override + { + AutoNotify an(done); + if (!parse_event(event->event)) + { + return; + } + event->event_write_helper<1>()->WriteAsync(node_, + Defs::MTI_CONSUMER_IDENTIFIED_UNKNOWN, WriteHelper::global(), + eventid_to_buffer(event->event), done->new_child()); + } + + /// Send the actual accessory command. + virtual void send_accy_command() = 0; + + /// Parses an event into an openlcb accessory offset. + /// @return true if the event is in the accessory range, false if this + /// event can be ignored. + /// @param on_off will be set to true if this is an activate event, false + /// if it is an inactivate event. + /// @param ofs will be set to the offset in the state_ arrays. + /// @param mask will be set to a single bit value that marks the location + /// in the state_ arrays. + bool parse_event(EventId event) + { + if (event >= TractionDefs::EXT_DCC_ACCESSORY_EVENT_BASE && + event < + TractionDefs::EXT_DCC_ACCESSORY_EVENT_BASE + NUM_EVENT) + { + aspect_ = event & 0xff; + dccAddress_ = (event >> 8) & (NUM_ADDRESS - 1); + return true; + } + else + { + return false; + } + } + + + /// Parsed event state: dcc address (0..2047) without inverting or encoding. + unsigned dccAddress_ : 11; + /// Parsed event state: the aspect commanded. + unsigned aspect_ : 8; + + /// OpenLCB node to export the consumer on. + Node *node_; +}; + +/// Specialized (DCC protocol) implementation of a DCC extended accessory +/// consumer. +class DccExtAccyConsumer : public DccExtAccyConsumerBase +{ +public: + /// Constructs a listener for DCC accessory control. + /// @param node is the virtual node that will be listening for events and + /// responding to Identify messages. + /// @param track is the interface through which we will be writing DCC + /// accessory packets. + DccExtAccyConsumer(Node *node, dcc::TrackIf *track) + : DccExtAccyConsumerBase(node) + , track_(track) + { + } + + /// Destructor. + ~DccExtAccyConsumer() + { + } + +private: + /// Send the actual accessory command. + void send_accy_command() override + { + dcc::TrackIf::message_type *pkt; + mainBufferPool->alloc(&pkt); + pkt->data()->add_dcc_ext_accessory(dccAddress_, aspect_); + pkt->data()->packet_header.rept_count = 3; + track_->send(pkt); + } + + /// Track to send DCC packets to. + dcc::TrackIf *track_; +}; + + } // namespace openlcb #endif // _OPENLCB_DCCACCYCONSUMER_HXX_ diff --git a/src/openlcb/TractionDefs.hxx b/src/openlcb/TractionDefs.hxx index 2ab6dbdb6..4927b0d4b 100644 --- a/src/openlcb/TractionDefs.hxx +++ b/src/openlcb/TractionDefs.hxx @@ -73,6 +73,8 @@ struct TractionDefs { static constexpr uint64_t ACTIVATE_BASIC_DCC_ACCESSORY_EVENT_BASE = 0x0101020000FF0000ULL; /// Base address of DCC accessory decoder well-known event range (inactive) static constexpr uint64_t INACTIVATE_BASIC_DCC_ACCESSORY_EVENT_BASE = 0x0101020000FE0000ULL; + /// Base address of DCC extended accessory decoder well-known event range + static constexpr uint64_t EXT_DCC_ACCESSORY_EVENT_BASE = 0x0101020001000000ULL; /// Node ID space allocated for DC blocks. static const uint64_t NODE_ID_DC_BLOCK = 0x060000000000ULL; /// Node ID space allocated for DCC locomotives.