Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for DCC extended accessories #769

Merged
merged 9 commits into from
Feb 4, 2024
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
80 changes: 80 additions & 0 deletions src/openlcb/DccAccyConsumer.cxxtest
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public:
class DccAccyTest : public AsyncNodeTest
{
protected:
DccAccyTest()
{
wait();
}

StrictMock<MockPacketQueue> trackSendQueue_;
DccAccyConsumer consumer_{node_, &trackSendQueue_};
};
Expand All @@ -72,6 +77,19 @@ TEST_F(DccAccyTest, identify_boot_invalid)
":X198F4111N0101020000FF01F0;", ":X194C722AN0101020000FF01F0;");
}

TEST_F(DccAccyTest, global_identify)
{
clear_expect(true);
// Consumer range for two 4096 long ranges.
expect_packet(":X194A422AN0101020000FF0FFF;");
expect_packet(":X194A422AN0101020000FE0FFF;");

// identify events addressed.
send_packet(":X19968111N022A;");
wait();
}


TEST_F(DccAccyTest, identify_throw_identify)
{
EXPECT_CALL(trackSendQueue_, arrived(_, _)).Times(2);
Expand Down Expand Up @@ -122,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
178 changes: 166 additions & 12 deletions src/openlcb/DccAccyConsumer.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,31 @@ protected:
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 done->notify();
return;
}
if (registry_entry.event ==
TractionDefs::ACTIVATE_BASIC_DCC_ACCESSORY_EVENT_BASE)
{
event->event_write_helper<1>()->WriteAsync(node_,
Defs::MTI_CONSUMER_IDENTIFIED_RANGE, WriteHelper::global(),
eventid_to_buffer(EncodeRange(
TractionDefs::ACTIVATE_BASIC_DCC_ACCESSORY_EVENT_BASE,
4095)),
done->new_child());
}
if (registry_entry.event ==
TractionDefs::INACTIVATE_BASIC_DCC_ACCESSORY_EVENT_BASE)
{
event->event_write_helper<2>()->WriteAsync(node_,
Defs::MTI_CONSUMER_IDENTIFIED_RANGE, WriteHelper::global(),
eventid_to_buffer(EncodeRange(
TractionDefs::INACTIVATE_BASIC_DCC_ACCESSORY_EVENT_BASE,
4095)),
done->new_child());
}
event->event_write_helper<1>()->WriteAsync(node_,
Defs::MTI_CONSUMER_IDENTIFIED_RANGE, WriteHelper::global(),
eventid_to_buffer(EncodeRange(
TractionDefs::ACTIVATE_BASIC_DCC_ACCESSORY_EVENT_BASE, 2044)),
done->new_child());
event->event_write_helper<2>()->WriteAsync(node_,
Defs::MTI_CONSUMER_IDENTIFIED_RANGE, WriteHelper::global(),
eventid_to_buffer(EncodeRange(
TractionDefs::INACTIVATE_BASIC_DCC_ACCESSORY_EVENT_BASE, 2044)),
done->new_child());
done->notify();
}

void handle_event_report(const EventRegistryEntry &registry_entry,
Expand Down Expand Up @@ -250,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;
atanisoft marked this conversation as resolved.
Show resolved Hide resolved
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