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.