Skip to content

Commit

Permalink
Adds support for DCC extended accessories (#769)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
balazsracz authored Feb 4, 2024
1 parent d2c82ca commit f0a0091
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/dcc/Packet.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
15 changes: 15 additions & 0 deletions src/dcc/Packet.cxxtest
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
11 changes: 11 additions & 0 deletions src/dcc/Packet.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
62 changes: 62 additions & 0 deletions src/openlcb/DccAccyConsumer.cxxtest
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,66 @@ TEST_F(DccAccyTest, packet_throw)
wait();
}


class DccExtAccyTest : public AsyncNodeTest
{
protected:
DccExtAccyTest()
{
wait();
}

StrictMock<MockPacketQueue> 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
144 changes: 144 additions & 0 deletions src/openlcb/DccAccyConsumer.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -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 &registry_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 &registry_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_
2 changes: 2 additions & 0 deletions src/openlcb/TractionDefs.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit f0a0091

Please sign in to comment.