/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce::midi_ci::detail { Parser::Status Responder::processCompleteMessage (BufferOutput& output, ump::BytesOnGroup message, Span listeners) { auto status = Parser::Status::noError; const auto parsed = Parser::parse (output.getMuid(), message.bytes, &status); if (! parsed.has_value()) return Parser::Status::malformed; class Output : public ResponderOutput { public: Output (BufferOutput& o, Message::Header h, uint8_t g) : innerOutput (o), header (h), group (g) {} MUID getMuid() const override { return innerOutput.getMuid(); } Message::Header getIncomingHeader() const override { return header; } uint8_t getIncomingGroup() const override { return group; } std::vector& getOutputBuffer() override { return innerOutput.getOutputBuffer(); } void send (uint8_t g) override { innerOutput.send (g); } private: BufferOutput& innerOutput; Message::Header header; uint8_t group{}; }; Output responderOutput { output, parsed->header, message.group }; if (status != Parser::Status::noError) { switch (status) { case Parser::Status::collidingMUID: { const Message::Header header { ChannelInGroup::wholeBlock, MessageMeta::Meta::subID2, MessageMeta::implementationVersion, output.getMuid(), MUID::getBroadcast() }; const Message::InvalidateMUID body { output.getMuid() }; MessageTypeUtils::send (responderOutput, responderOutput.getIncomingGroup(), header, body); break; } case Parser::Status::unrecognisedMessage: MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x01 }); break; case Parser::Status::reservedVersion: MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x02 }); break; case Parser::Status::malformed: MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x41 }); break; case Parser::Status::mismatchedMUID: case Parser::Status::noError: break; } return status; } for (auto* listener : listeners) if (listener != nullptr && listener->tryRespond (responderOutput, *parsed)) return Parser::Status::noError; MessageTypeUtils::BaseCaseDelegate base; if (base.tryRespond (responderOutput, *parsed)) return Parser::Status::noError; return Parser::Status::unrecognisedMessage; } //============================================================================== //============================================================================== #if JUCE_UNIT_TESTS class ResponderTests : public UnitTest { public: ResponderTests() : UnitTest ("Responder", UnitTestCategories::midi) {} void runTest() override { auto random = getRandom(); std::vector outgoing; const auto makeOutput = [&] { struct Output : public BufferOutput { Output (Random& r, std::vector& b) : muid (MUID::makeRandom (r)), buf (b) {} MUID getMuid() const override { return muid; } std::vector& getOutputBuffer() override { return buf; } void send (uint8_t) override { sent.push_back (buf); } MUID muid; std::vector& buf; std::vector> sent; }; return Output { random, outgoing }; }; beginTest ("An endpoint message with a matching MUID provokes an endpoint response"); { constexpr auto version = MessageMeta::implementationVersion; auto output = makeOutput(); const auto initialMUID = output.getMuid(); const auto bytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* endpoint message */ 0x72, /* version */ version, /* source MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* destination MUID */ (initialMUID.get() >> 0x00) & 0x7f, /* ... */ (initialMUID.get() >> 0x07) & 0x7f, /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, /* ... */ (initialMUID.get() >> 0x15) & 0x7f, /* status, product instance ID */ 0x00); const Message::Parsed expectedInput { Message::Header { ChannelInGroup::wholeBlock, std::byte { 0x72 }, version, MUID::makeUnchecked (0x80c101), initialMUID }, Message::EndpointInquiry { std::byte { 0x00 } } }; EndpointResponderListener listener; processCompleteMessage (output, { 0, bytes }, listener); expect (listener == SilentResponderListener (expectedInput)); const auto expectedOutputBytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* endpoint reply */ 0x73, /* version */ version, /* source MUID */ (initialMUID.get() >> 0x00) & 0x7f, /* ... */ (initialMUID.get() >> 0x07) & 0x7f, /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, /* ... */ (initialMUID.get() >> 0x15) & 0x7f, /* destination MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* status */ 0x00, /* 16-bit length of following data */ 0x04, /* ... */ 0x00, /* info */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* ... */ 0x00); expect (rangesEqual (output.sent.front(), expectedOutputBytes)); } beginTest ("An endpoint message directed at a different MUID does not provoke a response"); { const auto destMUID = MUID::makeRandom (random); constexpr auto version = MessageMeta::implementationVersion; const auto bytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* endpoint message */ 0x72, /* version */ version, /* source MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* destination MUID */ (destMUID.get() >> 0x00) & 0x7f, /* ... */ (destMUID.get() >> 0x07) & 0x7f, /* ... */ (destMUID.get() >> 0x0e) & 0x7f, /* ... */ (destMUID.get() >> 0x15) & 0x7f, /* status, product instance ID */ 0x00); auto output = makeOutput(); EndpointResponderListener listener; processCompleteMessage (output, { 0, bytes }, listener); expect (listener == SilentResponderListener()); expect (output.sent.empty()); } beginTest ("If the listener fails to compose an endpoint response, a NAK is emitted"); { auto output = makeOutput(); const auto initialMUID = output.getMuid(); SilentResponderListener listener; constexpr auto version = MessageMeta::implementationVersion; const auto bytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* endpoint message */ 0x72, /* version */ version, /* source MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* destination MUID */ (initialMUID.get() >> 0x00) & 0x7f, /* ... */ (initialMUID.get() >> 0x07) & 0x7f, /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, /* ... */ (initialMUID.get() >> 0x15) & 0x7f, /* status, product instance ID */ 0x00); processCompleteMessage (output, { 0, bytes }, listener); const auto expectedOutputBytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* nak */ 0x7f, /* version */ version, /* source MUID */ (initialMUID.get() >> 0x00) & 0x7f, /* ... */ (initialMUID.get() >> 0x07) & 0x7f, /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, /* ... */ (initialMUID.get() >> 0x15) & 0x7f, /* destination MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* original transaction sub-id #2 */ 0x72, /* nak status code */ 0x00, /* nak status data */ 0x00, /* details */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* message text length */ 0x00, /* ... */ 0x00); expect (rangesEqual (output.sent.front(), expectedOutputBytes)); } beginTest ("If a message is sent with reserved bits set in the Message Format Version, a NAK is emitted"); { auto output = makeOutput(); const auto initialMUID = output.getMuid(); const auto bytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* endpoint message */ 0x72, /* version, reserved bit set */ 0x12, /* source MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* destination MUID */ (initialMUID.get() >> 0x00) & 0x7f, /* ... */ (initialMUID.get() >> 0x07) & 0x7f, /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, /* ... */ (initialMUID.get() >> 0x15) & 0x7f, /* status, product instance ID */ 0x00); SilentResponderListener listener; processCompleteMessage (output, { 0, bytes }, listener); expect (listener == SilentResponderListener{}); const auto expectedOutputBytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* nak */ 0x7f, /* version */ MessageMeta::implementationVersion, /* source MUID */ (initialMUID.get() >> 0x00) & 0x7f, /* ... */ (initialMUID.get() >> 0x07) & 0x7f, /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, /* ... */ (initialMUID.get() >> 0x15) & 0x7f, /* destination MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* original transaction sub-id #2 */ 0x72, /* nak status code */ 0x02, /* nak status data */ 0x00, /* details */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* message text length */ 0x00, /* ... */ 0x00); expect (rangesEqual (output.sent.front(), expectedOutputBytes)); } beginTest ("If the message body is malformed, a NAK with a status of 0x41 is emitted"); { const auto sourceMUID = MUID::makeRandom (random); Message::Header header; header.deviceID = ChannelInGroup::wholeBlock; header.category = std::byte { 0x7e }; header.version = MessageMeta::implementationVersion; header.source = sourceMUID; header.destination = MUID::getBroadcast(); Message::InvalidateMUID invalidate; invalidate.target = MUID::makeRandom (random); std::vector message; Marshalling::Writer { message } (header, invalidate); // Remove a byte from the end of the message message.pop_back(); auto output = makeOutput(); const auto ourMUID = output.getMuid(); SilentResponderListener listener; processCompleteMessage (output, { 0, message }, listener); const auto expectedOutputBytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* nak */ 0x7f, /* version */ MessageMeta::implementationVersion, /* source MUID */ (ourMUID.get() >> 0x00) & 0x7f, /* ... */ (ourMUID.get() >> 0x07) & 0x7f, /* ... */ (ourMUID.get() >> 0x0e) & 0x7f, /* ... */ (ourMUID.get() >> 0x15) & 0x7f, /* destination MUID */ (sourceMUID.get() >> 0x00) & 0x7f, /* ... */ (sourceMUID.get() >> 0x07) & 0x7f, /* ... */ (sourceMUID.get() >> 0x0e) & 0x7f, /* ... */ (sourceMUID.get() >> 0x15) & 0x7f, /* original transaction sub-id #2 */ 0x7e, /* nak status code */ 0x41, /* nak status data */ 0x00, /* details */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* message text length */ 0x00, /* ... */ 0x00); expect (rangesEqual (output.sent.front(), expectedOutputBytes)); } beginTest ("If an unrecognised message is received, a NAK with a status of 0x01 is emitted"); { const auto sourceMUID = MUID::makeRandom (random); Message::Header header; header.deviceID = ChannelInGroup::wholeBlock; header.category = std::byte { 0x50 }; // reserved header.version = MessageMeta::implementationVersion; header.source = sourceMUID; header.destination = MUID::getBroadcast(); std::vector message; Marshalling::Writer { message } (header); message.emplace_back(); auto output = makeOutput(); const auto ourMUID = output.getMuid(); SilentResponderListener listener; processCompleteMessage (output, { 0, message }, listener); const auto expectedOutputBytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* nak */ 0x7f, /* version */ MessageMeta::implementationVersion, /* source MUID */ (ourMUID.get() >> 0x00) & 0x7f, /* ... */ (ourMUID.get() >> 0x07) & 0x7f, /* ... */ (ourMUID.get() >> 0x0e) & 0x7f, /* ... */ (ourMUID.get() >> 0x15) & 0x7f, /* destination MUID */ (sourceMUID.get() >> 0x00) & 0x7f, /* ... */ (sourceMUID.get() >> 0x07) & 0x7f, /* ... */ (sourceMUID.get() >> 0x0e) & 0x7f, /* ... */ (sourceMUID.get() >> 0x15) & 0x7f, /* original transaction sub-id #2 */ 0x50, /* nak status code */ 0x01, /* nak status data */ 0x00, /* details */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* message text length */ 0x00, /* ... */ 0x00); expect (rangesEqual (output.sent.front(), expectedOutputBytes)); } } private: template static std::array makeByteArray (Ts&&... ts) { jassert (((0 <= (int) ts && (int) ts <= std::numeric_limits::max()) && ...)); return { std::byte (ts)... }; } struct SilentResponderListener : public ResponderDelegate { SilentResponderListener() = default; explicit SilentResponderListener (const Message::Parsed& p) : parsed (p) {} bool tryRespond (ResponderOutput&, const Message::Parsed& p) override { parsed = p; return false; } // Returning false indicates that the message was not handled bool operator== (const SilentResponderListener& other) const { return parsed == other.parsed; } bool operator!= (const SilentResponderListener& other) const { return ! operator== (other); } std::optional parsed; }; struct EndpointResponderListener : public SilentResponderListener { bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override { parsed = message; if (std::holds_alternative (message.body)) { std::array data{}; Message::EndpointInquiryResponse response; response.status = std::byte{}; response.data = data; MessageTypeUtils::send (output, output.getIncomingGroup(), output.getReplyHeader (std::byte { 0x73 }), response); return true; } return SilentResponderListener::tryRespond (output, message); } using SilentResponderListener::operator==, SilentResponderListener::operator!=; }; struct OutputCallback { void operator() (Span bytes) { output = std::vector (bytes.begin(), bytes.end()); } std::vector output; }; template static bool rangesEqual (A&& a, B&& b) { using std::begin, std::end; return std::equal (begin (a), end (a), begin (b), end (b)); } static Parser::Status processCompleteMessage (BufferOutput& output, ump::BytesOnGroup message, ResponderDelegate& listener) { ResponderDelegate* const listeners[] { &listener }; return Responder::processCompleteMessage (output, message, listeners); } }; static ResponderTests responderTests; #endif } // namespace juce::midi_ci::detail