diff --git a/src/Z21.Client.UnitTest/Core/Codecs/AddressCodecTest.cs b/src/Z21.Client.UnitTest/Core/Codecs/AddressCodecTest.cs index 5457435..a2c7af5 100644 --- a/src/Z21.Client.UnitTest/Core/Codecs/AddressCodecTest.cs +++ b/src/Z21.Client.UnitTest/Core/Codecs/AddressCodecTest.cs @@ -172,6 +172,33 @@ public void CombineCvAddress_IsInverseOfSplit(ushort cvAddress) Assert.That(_codec.CombineCvAddress(msb, lsb), Is.EqualTo(cvAddress)); } + [Test] + [TestCase((ushort)0, 0x00, 0x00)] + [TestCase((ushort)1, 0x00, 0x01)] + [TestCase((ushort)255, 0x00, 0xFF)] + [TestCase((ushort)256, 0x01, 0x00)] + [TestCase((ushort)768, 0x03, 0x00)] + [TestCase((ushort)1023, 0x03, 0xFF)] + public void SplitPomCvAddress_ReturnsCvHighBitsAndLsb(ushort cvAddress, byte expectedHighBits, byte expectedLsb) + { + (byte cvHighBits, byte cvLsb) = _codec.SplitPomCvAddress(cvAddress); + Assert.Multiple(() => + { + Assert.That(cvHighBits, Is.EqualTo(expectedHighBits), "CV high bits are incorrect"); + Assert.That(cvLsb, Is.EqualTo(expectedLsb), "CV LSB is incorrect"); + }); + } + + [Test] + [TestCase((ushort)1024)] + [TestCase((ushort)1025)] + [TestCase((ushort)65535)] + public void SplitPomCvAddress_AboveTenBitRange_ThrowsWithMessage(ushort cvAddress) + { + ArgumentOutOfRangeException exception = Assert.Throws(() => _codec.SplitPomCvAddress(cvAddress))!; + Assert.That(exception.Message, Does.Contain("0 and 1023")); + } + [Test] public void EncodeAccessoryPomAddress_WholeDecoder_SetsCddNibbleToZero() { diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryWriteBitCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryWriteBitCommandTest.cs index 71d6572..88e2bc1 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryWriteBitCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryWriteBitCommandTest.cs @@ -6,9 +6,9 @@ namespace Z21.UnitTest.Core.Command.Programming public class CvPomAccessoryWriteBitCommandTest : CommandTestFixture { [Test] - [TestCase((ushort)1, true, (byte)0, (ushort)0, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE8, 0x00, 0x0A, 0x25 })] - [TestCase((ushort)1, true, (byte)0, (ushort)0, (byte)2, false, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE8, 0x00, 0x02, 0x2D })] - [TestCase((ushort)1, true, (byte)0, (ushort)256, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE9, 0x00, 0x0A, 0x24 })] + [TestCase((ushort)1, true, (byte)0, (ushort)0, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE8, 0x00, 0xFA, 0xD5 })] + [TestCase((ushort)1, true, (byte)0, (ushort)0, (byte)2, false, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE8, 0x00, 0xF2, 0xDD })] + [TestCase((ushort)1, true, (byte)0, (ushort)256, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE9, 0x00, 0xFA, 0xD4 })] public void Ctor_SetsCorrectDataBits(ushort decoderAddress, bool wholeDecoder, byte output, ushort cvAddress, byte bitPosition, bool bitValue, byte[] expected) { CvPomAccessoryWriteBitCommand command = Factory.Create(decoderAddress, wholeDecoder, output, cvAddress, bitPosition, bitValue); diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomWriteBitCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomWriteBitCommandTest.cs index 50a5e4f..06e3b48 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomWriteBitCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomWriteBitCommandTest.cs @@ -6,9 +6,9 @@ namespace Z21.UnitTest.Core.Command.Programming public class CvPomWriteBitCommandTest : CommandTestFixture { [Test] - [TestCase((ushort)3, (ushort)0, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xE8, 0x00, 0x0A, 0x37 })] - [TestCase((ushort)3, (ushort)0, (byte)2, false, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xE8, 0x00, 0x02, 0x3F })] - [TestCase((ushort)3, (ushort)256, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xE9, 0x00, 0x0A, 0x36 })] + [TestCase((ushort)3, (ushort)0, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xE8, 0x00, 0xFA, 0xC7 })] + [TestCase((ushort)3, (ushort)0, (byte)2, false, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xE8, 0x00, 0xF2, 0xCF })] + [TestCase((ushort)3, (ushort)256, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xE9, 0x00, 0xFA, 0xC6 })] public void Ctor_SetsCorrectDataBits(ushort locoAddress, ushort cvAddress, byte bitPosition, bool bitValue, byte[] expected) { CvPomWriteBitCommand command = Factory.Create(locoAddress, cvAddress, bitPosition, bitValue); diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/NewHandlerContractTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/NewHandlerContractTest.cs index 38eb3bf..51e8a3f 100644 --- a/src/Z21.Client.UnitTest/Core/ResponseHandler/NewHandlerContractTest.cs +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/NewHandlerContractTest.cs @@ -29,7 +29,7 @@ private static IEnumerable Handlers() { TestCaseData Case(string name, IZ21ResponseHandler handler, byte[] valid) => new TestCaseData(handler, valid).SetName(name); - yield return Case("CvResult", new CvResultResponseHandler(new AddressCodec()), Set(Frame(6, 0x40), (4, 0x64), (5, 0x14))); + yield return Case("CvResult", new CvResultResponseHandler(new AddressCodec()), Set(Frame(9, 0x40), (4, 0x64), (5, 0x14))); yield return Case("CvNack", new CvNackResponseHandler(), Set(Frame(6, 0x40), (4, 0x61), (5, 0x13))); yield return Case("CvNackSc", new CvNackShortCircuitResponseHandler(), Set(Frame(6, 0x40), (4, 0x61), (5, 0x12))); yield return Case("RmBus", new RmBusDataChangedResponseHandler(), Frame(15, 0x80)); diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvResultResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvResultResponseHandlerTest.cs index c04bede..18efec3 100644 --- a/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvResultResponseHandlerTest.cs +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvResultResponseHandlerTest.cs @@ -21,6 +21,8 @@ public void CanHandle_ValidResponse_ReturnsTrue() [Test] [TestCase(new byte[] { 0x0A, 0x00, 0x40, 0x00, 0x64, 0x13, 0x00, 0x1C, 0x05, 0x00 }, TestName = "Wrong DB0")] [TestCase(new byte[] { 0x00 }, TestName = "Response too small")] + [TestCase(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x64, 0x14, 0x00 }, TestName = "Signature matches but value byte truncated (length 7)")] + [TestCase(new byte[] { 0x08, 0x00, 0x40, 0x00, 0x64, 0x14, 0x00, 0x1C }, TestName = "Signature matches but value byte missing (length 8)")] public void CanHandle_InvalidResponse_ReturnsFalse(byte[] response) { Assert.That(_handler.CanHandle(response), Is.False); diff --git a/src/Z21.Client/Core/Codecs/AddressCodec.cs b/src/Z21.Client/Core/Codecs/AddressCodec.cs index f9e6524..ca14837 100644 --- a/src/Z21.Client/Core/Codecs/AddressCodec.cs +++ b/src/Z21.Client/Core/Codecs/AddressCodec.cs @@ -66,6 +66,16 @@ public ushort CombineCvAddress(byte msb, byte lsb) return (ushort)((msb << 8) + lsb); } + public (byte cvHighBits, byte cvLsb) SplitPomCvAddress(ushort cvAddress) + { + if (cvAddress > 0x3FF) + throw new ArgumentOutOfRangeException(nameof(cvAddress), cvAddress, "POM CV address must be between 0 and 1023 (CV1..CV1024); higher CVs are not addressable on the main track."); + + byte cvHighBits = (byte)((cvAddress >> 8) & 0x03); + byte cvLsb = (byte)(cvAddress & 0xFF); + return (cvHighBits, cvLsb); + } + public (byte db1, byte db2) EncodeAccessoryPomAddress(ushort decoderAddress, bool wholeDecoder, byte output) { int cddd = wholeDecoder ? 0x00 : (0x08 | (output & 0x07)); diff --git a/src/Z21.Client/Core/Codecs/IAddressCodec.cs b/src/Z21.Client/Core/Codecs/IAddressCodec.cs index 35918d7..a0fb2dc 100644 --- a/src/Z21.Client/Core/Codecs/IAddressCodec.cs +++ b/src/Z21.Client/Core/Codecs/IAddressCodec.cs @@ -41,6 +41,14 @@ public interface IAddressCodec /// ushort CombineCvAddress(byte msb, byte lsb); + /// + /// Splits a CV address (0 = CV1) for a POM (main-track) command into the two high MM option + /// bits (folded into the DB3 option byte) and the CVAdr_LSB wire byte. POM addresses + /// CVs through a 10-bit field, so only CV addresses 0..1023 (CV1..CV1024) are representable. + /// + /// Thrown when exceeds 1023. + (byte cvHighBits, byte cvLsb) SplitPomCvAddress(ushort cvAddress); + /// /// Encodes an accessory decoder address for POM commands into the two wire bytes /// aaaaa / AAAACDDD. When is true the CV refers to the diff --git a/src/Z21.Client/Core/Command/Programming/CvPomAccessoryReadByteCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryReadByteCommand.cs index ab480de..64b08a4 100644 --- a/src/Z21.Client/Core/Command/Programming/CvPomAccessoryReadByteCommand.cs +++ b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryReadByteCommand.cs @@ -12,8 +12,8 @@ public class CvPomAccessoryReadByteCommand : IZ21Command public CvPomAccessoryReadByteCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort decoderAddress, bool wholeDecoder, byte output, ushort cvAddress) { (byte db1, byte db2) = addressCodec.EncodeAccessoryPomAddress(decoderAddress, wholeDecoder, output); - byte db3 = (byte)(0xE4 | ((cvAddress >> 8) & 0x03)); - byte cvLsb = (byte)(cvAddress & 0xFF); + (byte cvHighBits, byte cvLsb) = addressCodec.SplitPomCvAddress(cvAddress); + byte db3 = (byte)(0xE4 | cvHighBits); Data = frameBuilder.BuildXBus(0xE6, 0x31, db1, db2, db3, cvLsb, 0x00); } diff --git a/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteBitCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteBitCommand.cs index f264d30..786370e 100644 --- a/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteBitCommand.cs +++ b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteBitCommand.cs @@ -12,9 +12,11 @@ public class CvPomAccessoryWriteBitCommand : IZ21Command public CvPomAccessoryWriteBitCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort decoderAddress, bool wholeDecoder, byte output, ushort cvAddress, byte bitPosition, bool bitValue) { (byte db1, byte db2) = addressCodec.EncodeAccessoryPomAddress(decoderAddress, wholeDecoder, output); - byte db3 = (byte)(0xE8 | ((cvAddress >> 8) & 0x03)); - byte cvLsb = (byte)(cvAddress & 0xFF); - byte db5 = (byte)((bitValue ? 0x08 : 0x00) | (bitPosition & 0x07)); + (byte cvHighBits, byte cvLsb) = addressCodec.SplitPomCvAddress(cvAddress); + byte db3 = (byte)(0xE8 | cvHighBits); + // DB5 is the DCC bit-manipulation data byte 1111VPPP (S-9.2.1): high nibble 0xF0 marks a write + // (the "111K" opcode with K=1), V = new bit value, PPP = bit position. + byte db5 = (byte)(0xF0 | (bitValue ? 0x08 : 0x00) | (bitPosition & 0x07)); Data = frameBuilder.BuildXBus(0xE6, 0x31, db1, db2, db3, cvLsb, db5); } diff --git a/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteByteCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteByteCommand.cs index 276d6e7..5db9f14 100644 --- a/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteByteCommand.cs +++ b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteByteCommand.cs @@ -12,8 +12,8 @@ public class CvPomAccessoryWriteByteCommand : IZ21Command public CvPomAccessoryWriteByteCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort decoderAddress, bool wholeDecoder, byte output, ushort cvAddress, byte value) { (byte db1, byte db2) = addressCodec.EncodeAccessoryPomAddress(decoderAddress, wholeDecoder, output); - byte db3 = (byte)(0xEC | ((cvAddress >> 8) & 0x03)); - byte cvLsb = (byte)(cvAddress & 0xFF); + (byte cvHighBits, byte cvLsb) = addressCodec.SplitPomCvAddress(cvAddress); + byte db3 = (byte)(0xEC | cvHighBits); Data = frameBuilder.BuildXBus(0xE6, 0x31, db1, db2, db3, cvLsb, value); } diff --git a/src/Z21.Client/Core/Command/Programming/CvPomReadByteCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomReadByteCommand.cs index e079118..9ab8e30 100644 --- a/src/Z21.Client/Core/Command/Programming/CvPomReadByteCommand.cs +++ b/src/Z21.Client/Core/Command/Programming/CvPomReadByteCommand.cs @@ -12,8 +12,8 @@ public class CvPomReadByteCommand : IZ21Command public CvPomReadByteCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress, ushort cvAddress) { (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); - byte db3 = (byte)(0xE4 | ((cvAddress >> 8) & 0x03)); - byte cvLsb = (byte)(cvAddress & 0xFF); + (byte cvHighBits, byte cvLsb) = addressCodec.SplitPomCvAddress(cvAddress); + byte db3 = (byte)(0xE4 | cvHighBits); Data = frameBuilder.BuildXBus(0xE6, 0x30, msb, lsb, db3, cvLsb, 0x00); } diff --git a/src/Z21.Client/Core/Command/Programming/CvPomWriteBitCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomWriteBitCommand.cs index 9f4848b..9bf1ded 100644 --- a/src/Z21.Client/Core/Command/Programming/CvPomWriteBitCommand.cs +++ b/src/Z21.Client/Core/Command/Programming/CvPomWriteBitCommand.cs @@ -11,9 +11,11 @@ public class CvPomWriteBitCommand : IZ21Command public CvPomWriteBitCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress, ushort cvAddress, byte bitPosition, bool bitValue) { (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); - byte db3 = (byte)(0xE8 | ((cvAddress >> 8) & 0x03)); - byte cvLsb = (byte)(cvAddress & 0xFF); - byte db5 = (byte)((bitValue ? 0x08 : 0x00) | (bitPosition & 0x07)); + (byte cvHighBits, byte cvLsb) = addressCodec.SplitPomCvAddress(cvAddress); + byte db3 = (byte)(0xE8 | cvHighBits); + // DB5 is the DCC bit-manipulation data byte 1111VPPP (S-9.2.1): high nibble 0xF0 marks a write + // (the "111K" opcode with K=1), V = new bit value, PPP = bit position. + byte db5 = (byte)(0xF0 | (bitValue ? 0x08 : 0x00) | (bitPosition & 0x07)); Data = frameBuilder.BuildXBus(0xE6, 0x30, msb, lsb, db3, cvLsb, db5); } diff --git a/src/Z21.Client/Core/Command/Programming/CvPomWriteByteCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomWriteByteCommand.cs index ad18223..f7a4c32 100644 --- a/src/Z21.Client/Core/Command/Programming/CvPomWriteByteCommand.cs +++ b/src/Z21.Client/Core/Command/Programming/CvPomWriteByteCommand.cs @@ -11,8 +11,8 @@ public class CvPomWriteByteCommand : IZ21Command public CvPomWriteByteCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress, ushort cvAddress, byte value) { (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); - byte db3 = (byte)(0xEC | ((cvAddress >> 8) & 0x03)); - byte cvLsb = (byte)(cvAddress & 0xFF); + (byte cvHighBits, byte cvLsb) = addressCodec.SplitPomCvAddress(cvAddress); + byte db3 = (byte)(0xEC | cvHighBits); Data = frameBuilder.BuildXBus(0xE6, 0x30, msb, lsb, db3, cvLsb, value); } diff --git a/src/Z21.Client/Core/ResponseHandler/Programming/CvResultResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Programming/CvResultResponseHandler.cs index 2ae7a91..dbc2b76 100644 --- a/src/Z21.Client/Core/ResponseHandler/Programming/CvResultResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/Programming/CvResultResponseHandler.cs @@ -20,7 +20,7 @@ public class CvResultResponseHandler(IAddressCodec addressCodec) : ICvResultResp public string Name => "LAN_X_CV_RESULT"; public bool CanHandle(byte[] response) => - ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (3, 0x00), (4, 0x64), (5, 0x14)); + ((IZ21ResponseHandler)this).MatchesFrame(response, 9, (2, 0x40), (3, 0x00), (4, 0x64), (5, 0x14)); public void Handle(byte[] response) {