diff --git a/CMakeLists.txt b/CMakeLists.txt index f0d0e52..b7e0364 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -394,6 +394,7 @@ if(LIBICSNEO_BUILD_TESTS) test/eventmanagertest.cpp test/ethernetpacketizertest.cpp test/i2cencoderdecodertest.cpp + test/a2bencoderdecodertest.cpp ) target_link_libraries(libicsneo-tests gtest gtest_main) diff --git a/api/icsneocpp/event.cpp b/api/icsneocpp/event.cpp index 3e156a0..9a17694 100644 --- a/api/icsneocpp/event.cpp +++ b/api/icsneocpp/event.cpp @@ -112,6 +112,7 @@ static constexpr const char* ATOMIC_OPERATION_RETRIED = "An operation failed to static constexpr const char* ATOMIC_OPERATION_COMPLETED_NONATOMICALLY = "An ideally-atomic operation was completed nonatomically."; static constexpr const char* WIVI_STACK_REFRESH_FAILED = "The Wireless neoVI stack encountered a communication error."; static constexpr const char* WIVI_UPLOAD_STACK_OVERFLOW = "The Wireless neoVI upload stack has encountered an overflow condition."; +static constexpr const char* A2B_MESSAGE_INCOMPLETE_FRAME = "At least one of the frames of the A2B message does not contain samples for each channel and stream."; // Transport Errors static constexpr const char* FAILED_TO_READ = "A read operation failed."; @@ -241,6 +242,8 @@ const char* APIEvent::DescriptionForType(Type type) { return WIVI_STACK_REFRESH_FAILED; case Type::WiVIUploadStackOverflow: return WIVI_UPLOAD_STACK_OVERFLOW; + case Type::A2BMessageIncompleteFrame: + return A2B_MESSAGE_INCOMPLETE_FRAME; // Transport Errors case Type::FailedToRead: diff --git a/communication/encoder.cpp b/communication/encoder.cpp index b7da23c..5ff9d59 100644 --- a/communication/encoder.cpp +++ b/communication/encoder.cpp @@ -8,6 +8,8 @@ #include "icsneo/communication/message/ethphymessage.h" #include "icsneo/communication/packet/i2cpacket.h" #include "icsneo/communication/message/i2cmessage.h" +#include "icsneo/communication/packet/a2bpacket.h" + using namespace icsneo; @@ -70,6 +72,18 @@ bool Encoder::encode(const Packetizer& packetizer, std::vector& result, // packets to the device. This function just encodes them back to back into `result` return HardwareISO9141Packet::EncodeFromMessage(*isomsg, result, report, packetizer); } // End of Network::Type::ISO9141 + case Network::Type::A2B: { + auto a2bmsg = std::dynamic_pointer_cast(message); + if(!a2bmsg) { + report(APIEvent::Type::MessageFormattingError, APIEvent::Severity::Error); + return false; + } + buffer = &result; + if(!HardwareA2BPacket::EncodeFromMessage(*a2bmsg, result, report)) { + return false; + } + break; + } // End of Network::Type::A2B case Network::Type::I2C: { auto i2cmsg = std::dynamic_pointer_cast(message); if(!i2cmsg) { @@ -81,7 +95,7 @@ bool Encoder::encode(const Packetizer& packetizer, std::vector& result, return false; } break; - } + } // End of Network::Type::I2C default: report(APIEvent::Type::UnexpectedNetworkType, APIEvent::Severity::Error); return false; diff --git a/communication/message/callback/streamoutput/a2bwavoutput.cpp b/communication/message/callback/streamoutput/a2bwavoutput.cpp index 5736ab8..5ad5632 100644 --- a/communication/message/callback/streamoutput/a2bwavoutput.cpp +++ b/communication/message/callback/streamoutput/a2bwavoutput.cpp @@ -70,11 +70,11 @@ bool A2BWAVOutput::writeSamples(const std::shared_ptr& msg, A2BMessa uint8_t numChannels = msg->getNumChannels(); uint8_t channel = 0; - uint32_t sampleIndex = 0; + uint32_t frame = 0; uint8_t bitDepth = msg->getBitDepth(); while(true) { - auto sample = msg->getSample(dir, channel, sampleIndex); + auto sample = msg->getSample(dir, channel, frame); if(!sample) { if(channel == 0) { @@ -90,7 +90,7 @@ bool A2BWAVOutput::writeSamples(const std::shared_ptr& msg, A2BMessa channel = (channel + 1) % numChannels; if(channel == 0) { - sampleIndex++; + frame++; } } diff --git a/communication/packet/a2bpacket.cpp b/communication/packet/a2bpacket.cpp index 2a28146..b89b737 100644 --- a/communication/packet/a2bpacket.cpp +++ b/communication/packet/a2bpacket.cpp @@ -1,13 +1,15 @@ #include "icsneo/communication/packet/a2bpacket.h" -#include "icsneo/communication/message/a2bmessage.h" #include -using namespace icsneo; -std::shared_ptr HardwareA2BPacket::DecodeToMessage(const std::vector &bytestream) { - - constexpr uint8_t coreMiniMessageHeaderSize = 28; - +namespace icsneo { + +const size_t HardwareA2BPacket::coreMiniMessageHeaderSize = 28; +const size_t HardwareA2BPacket::a2bMessageMaxLength = (size_t)HardwareA2BPacket::coreMiniMessageHeaderSize + 1024; +const size_t HardwareA2BPacket::a2bHeaderSize = 6; + +std::shared_ptr HardwareA2BPacket::DecodeToMessage(const std::vector& bytestream) { + if(bytestream.size() < coreMiniMessageHeaderSize) { return nullptr; @@ -31,9 +33,14 @@ std::shared_ptr HardwareA2BPacket::DecodeToMessage(const std::vectorheader.channelSize16 ? 2 : 4; uint8_t numChannels = data->header.channelNum; uint8_t bitDepth = data->header.channelSize16 ? A2BPCM_L16 : A2BPCM_L24; - bool monitor = data->header.monitor; - std::shared_ptr msg = std::make_shared(bitDepth, bytesPerChannel, numChannels, monitor); + std::shared_ptr msg = std::make_shared(bitDepth, bytesPerChannel, numChannels); + msg->channelSize16 = data->header.channelSize16; + msg->monitor = data->header.monitor; + msg->txmsg = data->header.txmsg; + msg->errIndicator = data->header.errIndicator; + msg->syncFrame = data->header.syncFrame; + msg->rfu2 = data->header.rfu2; const uint8_t *bytes = bytestream.data(); bytes+=coreMiniMessageHeaderSize; @@ -58,3 +65,99 @@ std::shared_ptr HardwareA2BPacket::DecodeToMessage(const std::vector& bytestream, const device_eventhandler_t& report) { + + if(message.getBytesPerSample() != 2 && message.getBytesPerSample() != 4) { + report(APIEvent::Type::MessageFormattingError, APIEvent::Severity::Error); + return false; + } + + size_t sampleBytes = message.getNumSamples() * static_cast(message.getBytesPerSample()); + size_t totalSize = coreMiniMessageHeaderSize + sampleBytes; + + if(totalSize > a2bMessageMaxLength) { + report(APIEvent::Type::MessageMaxLengthExceeded, APIEvent::Severity::Error); + return false; + } + + bytestream.reserve(totalSize); + + bytestream.push_back(message.getNumChannels()); + bytestream.push_back(message.channelSize16 ? 1 : 0); + + uint8_t a2b2Bits = 0; + + if(message.monitor) { + a2b2Bits = a2b2Bits | 1; + } + + if(message.txmsg) { + a2b2Bits = a2b2Bits | (1 << 1); + } + + if(message.errIndicator) { + a2b2Bits = a2b2Bits | (1 << 2); + } + + if(message.syncFrame) { + a2b2Bits = a2b2Bits | (1 << 3); + } + + bytestream.push_back(a2b2Bits); + bytestream.push_back(0); + bytestream.push_back(static_cast(message.rfu2)); + bytestream.push_back(static_cast(message.rfu2 >> 8)); + + for(size_t i = 0; i < (coreMiniMessageHeaderSize - a2bHeaderSize); i++) + bytestream.push_back(0); + + uint8_t numChannels = message.getNumChannels(); + + uint8_t channel = 0; + uint32_t frame = 0; + + auto writeSample = [&](A2BPCMSample&& sample) { + for(uint32_t i = 0; i < static_cast(message.getBytesPerSample()); i++) { + bytestream.push_back(static_cast((sample >> (i*8)))); + } + }; + + while(true) { + auto dsSample = message.getSample(A2BMessage::A2BDirection::DownStream, channel, frame); + auto usSample = message.getSample(A2BMessage::A2BDirection::UpStream, channel, frame); + + + // Check if getSample failed for both downstream and upstream + if(!dsSample && !usSample) { + + if(channel != 0) { + //Incomplete frame, the frame we are currently on does not contain all channel samples + report(APIEvent::Type::A2BMessageIncompleteFrame, APIEvent::Severity::Error); + return false; + } + // Since no samples have been written for the current frame yet and there are no more + // samples in both upstream and downstream, we can break and end parsing. + break; + } + // Since the first case failed, at least one of the streams still has samples. + // This case checks to see if the other stream does not have a sample. + else if(!dsSample || !usSample) { + // Report an error since we must have a one to one correspondence between upstream + // and downstream. + report(APIEvent::Type::A2BMessageIncompleteFrame, APIEvent::Severity::Error); + return false; + } + + writeSample(std::move(dsSample.value())); + writeSample(std::move(usSample.value())); + + channel = (channel + 1) % numChannels; + if(channel == 0) + frame++; + } + + return true; +} + +} \ No newline at end of file diff --git a/include/icsneo/api/event.h b/include/icsneo/api/event.h index f7ad670..84a2752 100644 --- a/include/icsneo/api/event.h +++ b/include/icsneo/api/event.h @@ -90,6 +90,7 @@ public: WiVIStackRefreshFailed = 0x2036, WiVIUploadStackOverflow = 0x2037, I2CMessageExceedsMaxLength = 0x2038, + A2BMessageIncompleteFrame = 0x2039, // Transport Events FailedToRead = 0x3000, diff --git a/include/icsneo/communication/message/a2bmessage.h b/include/icsneo/communication/message/a2bmessage.h index 11c6749..12706ee 100644 --- a/include/icsneo/communication/message/a2bmessage.h +++ b/include/icsneo/communication/message/a2bmessage.h @@ -31,16 +31,15 @@ public: A2BMessage() = delete; - A2BMessage(uint8_t bitDepth, uint8_t bytesPerSample, uint8_t numChannels, bool monitor) : - mBitDepth(bitDepth), - mBytesPerSample(bytesPerSample), - mMonitor(monitor) + A2BMessage(uint8_t bitDepth, uint8_t bytesPerSample, uint8_t numChannels) : + bDepth(bitDepth), + bps(bytesPerSample) { downstream.resize(numChannels); upstream.resize(numChannels); } - void addSample(A2BPCMSample &&sample, A2BDirection dir, uint8_t channel) { + void addSample(A2BPCMSample&& sample, A2BDirection dir, uint8_t channel) { if(dir == A2BDirection::DownStream) { downstream[channel].push_back(std::move(sample)); } @@ -61,18 +60,18 @@ public: return upstream[channel].data(); } - std::optional getSample(A2BDirection dir, uint8_t channel, uint32_t sampleIndex) const { + std::optional getSample(A2BDirection dir, uint8_t channel, uint32_t frame) const { const A2BPCMSample* samples = getSamples(dir, channel); auto numSamplesInChannel = getNumSamplesInChannel(dir, channel); if( samples == nullptr || - sampleIndex >= numSamplesInChannel.value_or(0) + frame >= numSamplesInChannel.value_or(0) ) { return std::nullopt; } - return samples[sampleIndex]; + return samples[frame]; } std::optional getNumSamplesInChannel(A2BDirection dir, uint8_t channel) const { @@ -104,25 +103,41 @@ public: } uint8_t getBitDepth() const { - return mBitDepth; + return bDepth; } uint8_t getBytesPerSample() const { - return mBytesPerSample; + return bps; } - bool isMonitor() const { - return mMonitor; + // Equals operation for testing. + bool operator==(const A2BMessage& rhs) const { + return channelSize16 == rhs.channelSize16 && + monitor == rhs.monitor && + txmsg == rhs.txmsg && + errIndicator == rhs.errIndicator && + syncFrame == rhs.syncFrame && + rfu2 == rhs.rfu2 && + downstream == rhs.downstream && + upstream == rhs.upstream && + totalSamples == rhs.totalSamples && + bDepth == rhs.bDepth && + bps == rhs.bps; } + bool channelSize16; + bool monitor; + bool txmsg; + bool errIndicator; + bool syncFrame; + uint16_t rfu2; private: std::vector downstream; std::vector upstream; size_t totalSamples = 0; - uint8_t mBitDepth; - uint8_t mBytesPerSample; - bool mMonitor; + uint8_t bDepth; + uint8_t bps; }; } diff --git a/include/icsneo/communication/packet/a2bpacket.h b/include/icsneo/communication/packet/a2bpacket.h index b27afdd..e18391a 100644 --- a/include/icsneo/communication/packet/a2bpacket.h +++ b/include/icsneo/communication/packet/a2bpacket.h @@ -7,9 +7,7 @@ #include #include #include -#include "icsneo/communication/message/message.h" - - +#include "icsneo/communication/message/a2bmessage.h" namespace icsneo { @@ -18,6 +16,7 @@ typedef uint16_t icscm_bitfield; struct HardwareA2BPacket { static std::shared_ptr DecodeToMessage(const std::vector& bytestream); + static bool EncodeFromMessage(const A2BMessage& message, std::vector& bytestream, const device_eventhandler_t& report); struct { // CxA2B @@ -34,6 +33,10 @@ struct HardwareA2BPacket { icscm_bitfield : 11; icscm_bitfield rfu2; } header; + + static const size_t coreMiniMessageHeaderSize; + static const size_t a2bMessageMaxLength; + static const size_t a2bHeaderSize; }; } diff --git a/test/a2bencoderdecodertest.cpp b/test/a2bencoderdecodertest.cpp new file mode 100644 index 0000000..a3ad52e --- /dev/null +++ b/test/a2bencoderdecodertest.cpp @@ -0,0 +1,235 @@ +#include "icsneo/icsneocpp.h" +#include "icsneo/communication/encoder.h" +#include "icsneo/communication/packet/a2bpacket.h" +#include "icsneo/communication/packetizer.h" +#include "icsneo/api/eventmanager.h" +#include "gtest/gtest.h" +#include + +using namespace icsneo; + +class A2BEncoderDecoderTest : public ::testing::Test { +protected: + void SetUp() override { + + report = [](APIEvent::Type, APIEvent::Severity) { + // Unless caught by the test, the packetizer should not throw errors + EXPECT_TRUE(false); + }; + packetizer.emplace([this](APIEvent::Type t, APIEvent::Severity s) { + report(t, s); + }); + packetEncoder.emplace([this](APIEvent::Type t, APIEvent::Severity s) { + report(t, s); + }); + packetDecoder.emplace([this](APIEvent::Type t, APIEvent::Severity s) { + report(t, s); + }); + + A2BPCMSample initialSample = 10 << 8; + constructTest( + msg1, + msg1Encoded, + {1,1,0,0,0,0}, + 1, + true, + false, + false, + false, + false, + 0, + 3, + initialSample + ); + + initialSample = (29 << 16) | (10 << 8); + constructTest( + msg2, + msg2Encoded, + {1,0,9,0,0,0}, + 1, + false, + true, + false, + false, + true, + 0, + 1, + initialSample + ); + + initialSample = (29 << 16) | (10 << 8); + constructTest( + msg3, + msg3Encoded, + {2,0,6,0,0xAA,0xFF}, + 2, + false, + false, + true, + true, + false, + 0xFFAA, + 3, + initialSample + ); + + constructTest( + msg4, + msg4Encoded, + {4,0,6,0,0xAA,0xFF}, + 4, + false, + false, + true, + true, + false, + 0xFFAA, + 0, + initialSample + ); + } + + void constructTest( + std::shared_ptr& testMsg, + std::vector& testMsgEncoded, + std::array encodedHeader, + uint8_t numChannels, + bool channelSize16, + bool monitor, + bool txmsg, + bool errIndicator, + bool syncFrame, + uint16_t rfu2, + uint32_t numFrames, + A2BPCMSample initialSample + ) + { + + testMsg = std::make_shared( + (uint8_t)(channelSize16 ? 16 : 24), + (uint8_t)(channelSize16 ? 2 : 4), + numChannels + ); + + auto addSample = [&]( + uint8_t channel, + A2BMessage::A2BDirection dir + ) { + testMsg->addSample(std::move(initialSample), dir, channel); + + for(size_t i = 0; i < static_cast(testMsg->getBytesPerSample()); i++) { + testMsgEncoded.push_back(static_cast(initialSample >> (i * 8))); + } + + initialSample++; + }; + + testMsg->network = Network(Network::NetID::A2B1); + testMsg->channelSize16 = channelSize16; + testMsg->monitor = monitor; + testMsg->txmsg = txmsg; + testMsg->errIndicator = errIndicator; + testMsg->syncFrame = syncFrame; + testMsg->rfu2 = rfu2; + + testMsgEncoded.reserve( + HardwareA2BPacket::coreMiniMessageHeaderSize + numFrames * static_cast(testMsg->getBytesPerSample()) + ); + + testMsgEncoded.insert( + testMsgEncoded.end(), + encodedHeader.begin(), + encodedHeader.end() + ); // Insert header + appendCoreMiniHeaderOffset(testMsgEncoded); + + for(unsigned int frame = 0; frame < numFrames; frame++) { + for(uint8_t channel = 0; channel < testMsg->getNumChannels(); channel++) { + + addSample(channel, A2BMessage::A2BDirection::DownStream); + addSample(channel, A2BMessage::A2BDirection::UpStream); + + } + } + } + + void appendCoreMiniHeaderOffset(std::vector& buf) { + for( + size_t i = 0; + i < (HardwareA2BPacket::coreMiniMessageHeaderSize - HardwareA2BPacket::a2bHeaderSize); + i++ + ) { + buf.push_back(0); + } + } + + void decrementEncodedMessageSize(std::vector& bytestream) { + if(bytestream.size() < 6) + return; + + uint16_t size = (bytestream[3] << 8) | bytestream[2]; + + if(size == 0) + return; + + size--; + + bytestream[2] = (uint8_t)size; + bytestream[3] = (uint8_t)(size >> 8); + } + + void testMessage(std::shared_ptr& msg, std::vector& msgEncoded) { + std::vector bytestream; + std::vector rawPacketBytes; + std::shared_ptr decodeMsg; + std::shared_ptr decodeA2BMsg; + + EXPECT_TRUE(packetEncoder->encode(*packetizer, bytestream, msg)); + rawPacketBytes = std::vector(bytestream.begin() + 6, bytestream.end()); + + EXPECT_EQ(rawPacketBytes, msgEncoded); + decrementEncodedMessageSize(bytestream); + + EXPECT_TRUE(packetizer->input(bytestream)); + auto packets = packetizer->output(); + if(packets.empty()) { EXPECT_TRUE(false);} + EXPECT_TRUE(packetDecoder->decode(decodeMsg, packets.back())); + decodeA2BMsg = std::static_pointer_cast(decodeMsg); + + EXPECT_EQ(*msg, *decodeA2BMsg); + } + + device_eventhandler_t report; + std::optional packetEncoder; + std::optional packetizer; + std::optional packetDecoder; + + std::shared_ptr msg1; + std::vector msg1Encoded; + + std::shared_ptr msg2; + std::vector msg2Encoded; + + std::shared_ptr msg3; + std::vector msg3Encoded; + + std::shared_ptr msg4; + std::vector msg4Encoded; +}; + +TEST_F(A2BEncoderDecoderTest, ChannelSize16SingleChannel) { + testMessage(msg1, msg1Encoded); +} + +TEST_F(A2BEncoderDecoderTest, ChannelSize24MultiChannel) { + testMessage(msg2, msg2Encoded); +} + +TEST_F(A2BEncoderDecoderTest, ChannelSize24MultiChannelMultiFrame) { + testMessage(msg3, msg3Encoded); +} + +TEST_F(A2BEncoderDecoderTest, NoFrames) { + testMessage(msg4, msg4Encoded); +}