/* ============================================================================== This file is part of the JUCE framework. Copyright (c) Raw Material Software Limited JUCE is an open source framework subject to commercial or open source licensing. By downloading, installing, or using the JUCE framework, or combining the JUCE framework with any other source code, object code, content or any other copyrightable work, you agree to the terms of the JUCE End User Licence Agreement, and all incorporated terms including the JUCE Privacy Policy and the JUCE Website Terms of Service, as applicable, which will bind you. If you do not agree to the terms of these agreements, we will not license the JUCE framework to you, and you must discontinue the installation or download process and cease use of the JUCE framework. JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ JUCE Privacy Policy: https://juce.com/juce-privacy-policy JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ Or: You may also use this code under the terms of the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.en.html THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR 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