Skip to content

Commit

Permalink
Adds routines to encode railcom data for a sender. (#553)
Browse files Browse the repository at this point in the history
- Adds 6-to-8 encode table
- Adds helper functions to perform encoding
- Adds unittests that validate that encoding and decoding are correctly inverse of each other.

Fixes constants:
- Aligns with RCN-217 on the two different ACK values
- Adds the newly appointed NACK value from RCN-217

Refactoring in the RailCom header:
- Makes RailcomDefs to a struct from a namespace. This is aligned better with the style of the rest of the codebase.

===


* Adds routines to encode railcom data for a sender.

- Adds 6-to-8 encode table
- Adds helper functions to perform encoding
- Adds unittests that validate that encoding and decoding are correctly inverse of each other.

Fixes contants:
- Aligns with RCN-217 on the two different ACK values
- Adds the newly appointed NACK value from RCN-217

Refactoring in the RailCom header:
- Makes RailcomDefs to a struct from a namespace. This is aligned better with the style of the rest of the codebase.

* Fix comment.
  • Loading branch information
balazsracz committed Aug 2, 2021
1 parent 05e5d61 commit 9efe188
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 37 deletions.
91 changes: 82 additions & 9 deletions src/dcc/RailCom.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,21 @@
#include "dcc/RailCom.hxx"

namespace dcc {
using RailcomDefs::INV;
using RailcomDefs::ACK;
using RailcomDefs::NACK;
using RailcomDefs::BUSY;
using RailcomDefs::RESVD1;
using RailcomDefs::RESVD2;
using RailcomDefs::RESVD3;
static constexpr uint8_t INV = RailcomDefs::INV;
static constexpr uint8_t ACK = RailcomDefs::ACK;
static constexpr uint8_t NACK = RailcomDefs::NACK;
static constexpr uint8_t BUSY = RailcomDefs::BUSY;
static constexpr uint8_t RESVD1 = RailcomDefs::RESVD1;
static constexpr uint8_t RESVD2 = RailcomDefs::RESVD2;
const uint8_t railcom_decode[256] =
{ INV, INV, INV, INV, INV, INV, INV, INV,
INV, INV, INV, INV, INV, INV, INV, NACK,
INV, INV, INV, INV, INV, INV, INV, ACK,
INV, INV, INV, INV, INV, INV, INV, 0x33,
INV, INV, INV, 0x34, INV, 0x35, 0x36, INV,
INV, INV, INV, INV, INV, INV, INV, 0x3A,
INV, INV, INV, 0x3B, INV, 0x3C, 0x37, INV,
INV, INV, INV, 0x3F, INV, 0x3D, 0x38, INV,
INV, 0x3E, 0x39, INV, RESVD3, INV, INV, INV,
INV, 0x3E, 0x39, INV, NACK, INV, INV, INV,
INV, INV, INV, INV, INV, INV, INV, 0x24,
INV, INV, INV, 0x23, INV, 0x22, 0x21, INV,
INV, INV, INV, 0x1F, INV, 0x1E, 0x20, INV,
Expand All @@ -79,6 +78,73 @@ const uint8_t railcom_decode[256] =
INV, INV, INV, INV, INV, INV, INV, INV,
};

const uint8_t railcom_encode[64] = {
0b10101100,
0b10101010,
0b10101001,
0b10100101,
0b10100011,
0b10100110,
0b10011100,
0b10011010,
0b10011001,
0b10010101,
0b10010011,
0b10010110,
0b10001110,
0b10001101,
0b10001011,
0b10110001,
0b10110010,
0b10110100,
0b10111000,
0b01110100,
0b01110010,
0b01101100,
0b01101010,
0b01101001,
0b01100101,
0b01100011,
0b01100110,
0b01011100,
0b01011010,
0b01011001,
0b01010101,
0b01010011,
0b01010110,
0b01001110,
0b01001101,
0b01001011,
0b01000111,
0b01110001,
0b11101000,
0b11100100,
0b11100010,
0b11010001,
0b11001001,
0b11000101,
0b11011000,
0b11010100,
0b11010010,
0b11001010,
0b11000110,
0b11001100,
0b01111000,
0b00010111,
0b00011011,
0b00011101,
0b00011110,
0b00101110,
0b00110110,
0b00111010,
0b00100111,
0b00101011,
0b00101101,
0b00110101,
0b00111001,
0b00110011,
};

/// Helper function to parse a part of a railcom packet.
///
/// @param fb_channel Which hardware channel did the railcom message arrive
Expand Down Expand Up @@ -159,6 +225,13 @@ void parse_internal(uint8_t fb_channel, uint8_t railcom_channel,
len = 2;
}
break;
case RMOB_XPOM0:
case RMOB_XPOM1:
case RMOB_XPOM2:
case RMOB_XPOM3:
type = RailcomPacket::MOB_XPOM0 + (packet_id - RMOB_XPOM0);
len = 6;
break;
case RMOB_DYN:
type = RailcomPacket::MOB_DYN;
len = 3;
Expand Down
70 changes: 64 additions & 6 deletions src/dcc/RailCom.cxxtest
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,29 @@ TEST_F(RailcomDecodeTest, SimpleAck) {
}

TEST_F(RailcomDecodeTest, MultipleAckNackBusy) {
fb_.add_ch1_data(0xF0);
fb_.add_ch1_data(0xE1);
fb_.add_ch2_data(0x0F);
fb_.add_ch1_data(0xF0); // one type of ack
fb_.add_ch1_data(0xE1); // NMRA busy
fb_.add_ch2_data(0x3C); // RCN nack
fb_.add_ch2_data(0x0F); // other ack
decode();
EXPECT_THAT(output_,
ElementsAre(RailcomPacket(3, 1, RailcomPacket::ACK, 0),
RailcomPacket(3, 1, RailcomPacket::BUSY, 0),
RailcomPacket(3, 2, RailcomPacket::NACK, 0),
RailcomPacket(3, 2, RailcomPacket::ACK, 0)));
}

TEST_F(RailcomDecodeTest, MultipleAckNackBusyCode) {
fb_.add_ch1_data(RailcomDefs::CODE_ACK); // one type of ack
fb_.add_ch1_data(RailcomDefs::CODE_BUSY); // NMRA busy
fb_.add_ch2_data(RailcomDefs::CODE_NACK); // RCN nack
fb_.add_ch2_data(RailcomDefs::CODE_ACK2); // other ack
decode();
EXPECT_THAT(output_, ElementsAre(RailcomPacket(3, 1, RailcomPacket::ACK, 0),
RailcomPacket(3, 1, RailcomPacket::BUSY, 0),
RailcomPacket(3, 2, RailcomPacket::NACK, 0)));
EXPECT_THAT(output_,
ElementsAre(RailcomPacket(3, 1, RailcomPacket::ACK, 0),
RailcomPacket(3, 1, RailcomPacket::BUSY, 0),
RailcomPacket(3, 2, RailcomPacket::NACK, 0),
RailcomPacket(3, 2, RailcomPacket::ACK, 0)));
}

TEST_F(RailcomDecodeTest, Ch2Ext) {
Expand Down Expand Up @@ -124,4 +140,46 @@ TEST_F(RailcomDecodeTest, FalseChannelBoundaryProblem) {
EXPECT_THAT(output_, ElementsAre(RailcomPacket(3, 1, RailcomPacket::GARBAGE, 0), RailcomPacket(3, 2, RailcomPacket::MOB_EXT, 128)));
}

// Verifies that the railcom encode and railcom decode table are inverse of
// each other.
TEST(RailcomEncodeTest, BitsMatch) {
for (unsigned i = 0; i < 63; i++)
{
uint8_t encoded = railcom_encode[i];
uint8_t decoded = railcom_decode[encoded];
EXPECT_EQ(i, decoded);
}
EXPECT_EQ(RailcomDefs::ACK, railcom_decode[RailcomDefs::CODE_ACK]);
EXPECT_EQ(RailcomDefs::ACK, railcom_decode[RailcomDefs::CODE_ACK2]);
EXPECT_EQ(RailcomDefs::NACK, railcom_decode[RailcomDefs::CODE_NACK]);
}

// Verifies that the railcom encode 12 command works correctly.
TEST_F(RailcomDecodeTest, Encode12) {
uint16_t d = RailcomDefs::encode12(RMOB_ADRHIGH, 42);
fb_.add_ch1_data(d>>8);
fb_.add_ch1_data(d & 0xff);
decode();
EXPECT_THAT(output_,
ElementsAre(RailcomPacket(3, 1, RailcomPacket::MOB_ADRHIGH, 42)));
}

// Verifies that the railcom encode 12 command works correctly.
TEST_F(RailcomDecodeTest, Append12) {
RailcomDefs::append12(RMOB_ADRHIGH, 42, fb_.ch1Data);
fb_.ch1Size = 2;
decode();
EXPECT_THAT(output_,
ElementsAre(RailcomPacket(3, 1, RailcomPacket::MOB_ADRHIGH, 42)));
}

// Verifies that the railcom encode 36 command works correctly.
TEST_F(RailcomDecodeTest, Append36) {
RailcomDefs::append36(RMOB_XPOM2, 0xfedc5432, fb_.ch2Data);
fb_.ch2Size = 6;
decode();
EXPECT_THAT(output_,
ElementsAre(RailcomPacket(3, 2, RailcomPacket::MOB_XPOM2, 0xfedc5432u)));
}

} // namespace dcc
113 changes: 91 additions & 22 deletions src/dcc/RailCom.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -79,33 +79,94 @@ struct Feedback : public DCCFeedback
/// Formats a dcc::Feedback message into a debug string.
std::string railcom_debug(const Feedback& fb);

/// Special constant values returned by the @ref railcom_decode[] array.
namespace RailcomDefs
{
/// invalid value (not conforming to the 4bit weighting requirement)
static const uint8_t INV = 0xff;
/// Railcom ACK; the decoder received the message ok. NOTE: some early
/// software versions may have ACK and NACK exchanged.
static const uint8_t ACK = 0xfe;
/// The decoder rejected the packet.
static const uint8_t NACK = 0xfd;
/// The decoder is busy; send the packet again. This is typically returned
/// when a POM CV write is still pending; the caller must re-try sending the
/// packet later.
static const uint8_t BUSY = 0xfc;
/// Reserved for future expansion.
static const uint8_t RESVD1 = 0xfb;
/// Reserved for future expansion.
static const uint8_t RESVD2 = 0xfa;
/// Reserved for future expansion.
static const uint8_t RESVD3 = 0xf8;
}

/** Table for 8-to-6 decoding of railcom data. This table can be indexed by the
* 8-bit value read from the railcom channel, and the return value will be
* either a 6-bit number, or one of the constants in @ref RailcomDefs. If the
* value is invalid, the INV constant is returned. */
extern const uint8_t railcom_decode[256];
/// Table for 6-to-8 encoding of railcom data. The table can be indexed by a
/// 6-bit value that is the semantic content of a railcom byte, and returns the
/// matching 8-bit value to put out on the UART. This table only contains the
/// standard codes, for the special codes like ACK use RailcomDefs::ACK.
extern const uint8_t railcom_encode[64];

/// Special constant values returned by the @ref railcom_decode[] array.
struct RailcomDefs
{
// These values appear in the railcom_decode table to mean special symbols.
enum
{
/// invalid value (not conforming to the 4bit weighting requirement)
INV = 0xff,
/// Railcom ACK; the decoder received the message ok. NOTE: There are
/// two codepoints that map to this.
ACK = 0xfe,
/// The decoder rejected the packet.
NACK = 0xfd,
/// The decoder is busy; send the packet again. This is typically
/// returned when a POM CV write is still pending; the caller must
/// re-try sending the packet later.
BUSY = 0xfc,

/// Reserved for future expansion.
RESVD1 = 0xfb,
/// Reserved for future expansion.
RESVD2 = 0xfa,
};

// These values need to be sent on the UART
enum
{
/// Code point for ACK (according to RCN-217)
CODE_ACK = 0xf0,
/// Another accepted code point for ACK (according to RCN-217)
CODE_ACK2 = 0x0f,
/// Code point for NACK (according to RCN-217)
CODE_NACK = 0x3c,
/// Code point for BUSY (according to NMRA S-9.3.2)
CODE_BUSY = 0xE1,
};

/// Encodes 12 bits of useful payload into 16 bits of UART data to transmit.
/// @param nibble top 4 bits of the payload to send
/// @param data bottom 8 bits of payload to send.
/// @return the uart bytes, first byte in the high 8 bits, second byte in
/// the low 8 bits.
static uint16_t encode12(uint8_t nibble, uint8_t data)
{
return (railcom_encode[((nibble << 2) | (data >> 6)) & 0x3F] << 8) |
railcom_encode[data & 0x3f];
}

/// Encodes 12 bits of useful payload into 16 bits of UART data to transmit.
/// @param nibble top 4 bits of the payload to send
/// @param data bottom 8 bits of payload to send.
/// @param dst this is where the payload will be stored.
static void append12(uint8_t nibble, uint8_t data, uint8_t* dst)
{
*dst++ = railcom_encode[((nibble << 2) | (data >> 6)) & 0x3F];
*dst++ = railcom_encode[data & 0x3f];
}

/// Encodes a 36-bit railcom datagram into UART bytes.
/// @param nibble the railcom ID (top 4 bits)
/// @param data the 32 bit payload. Will be transmitted MSbyte-first.
/// @param dst this is where the payload will be stored.
static void append36(uint8_t nibble, uint32_t data, uint8_t* dst)
{
*dst++ = railcom_encode[((nibble << 2) | (data >> 30)) & 0x3F];
*dst++ = railcom_encode[(data >> 24) & 0x3F];
*dst++ = railcom_encode[(data >> 18) & 0x3F];
*dst++ = railcom_encode[(data >> 12) & 0x3F];
*dst++ = railcom_encode[(data >> 6) & 0x3F];
*dst++ = railcom_encode[data & 0x3F];
}

private:
/// This struct cannot be instantiated.
RailcomDefs();
};


/// Packet identifiers from Mobile Decoders.
enum RailcomMobilePacketId
Expand All @@ -115,6 +176,10 @@ enum RailcomMobilePacketId
RMOB_ADRLOW = 2,
RMOB_EXT = 3,
RMOB_DYN = 7,
RMOB_XPOM0 = 8,
RMOB_XPOM1 = 9,
RMOB_XPOM2 = 10,
RMOB_XPOM3 = 11,
RMOB_SUBID = 12,
};

Expand All @@ -134,6 +199,10 @@ struct RailcomPacket
MOB_ADRLOW,
MOB_EXT,
MOB_DYN,
MOB_XPOM0,
MOB_XPOM1,
MOB_XPOM2,
MOB_XPOM3,
MOB_SUBID
};
/// which detector supplied this data
Expand Down

0 comments on commit 9efe188

Please sign in to comment.