|
- /*
- ==============================================================================
-
- 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<ResponderDelegate* const> 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<std::byte>& 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<Message::InvalidateMUID>::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<std::byte> outgoing;
-
- const auto makeOutput = [&]
- {
- struct Output : public BufferOutput
- {
- Output (Random& r, std::vector<std::byte>& b)
- : muid (MUID::makeRandom (r)), buf (b) {}
-
- MUID getMuid() const override { return muid; }
- std::vector<std::byte>& getOutputBuffer() override { return buf; }
- void send (uint8_t) override { sent.push_back (buf); }
-
- MUID muid;
- std::vector<std::byte>& buf;
- std::vector<std::vector<std::byte>> 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<std::byte> 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<std::byte> 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 <typename... Ts>
- static std::array<std::byte, sizeof... (Ts)> makeByteArray (Ts&&... ts)
- {
- jassert (((0 <= (int) ts && (int) ts <= std::numeric_limits<uint8_t>::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<Message::Parsed> parsed;
- };
-
- struct EndpointResponderListener : public SilentResponderListener
- {
- bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override
- {
- parsed = message;
-
- if (std::holds_alternative<Message::EndpointInquiry> (message.body))
- {
- std::array<std::byte, 4> 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<const std::byte> bytes)
- {
- output = std::vector<std::byte> (bytes.begin(), bytes.end());
- }
-
- std::vector<std::byte> output;
- };
-
- template <typename A, typename B>
- 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
|