The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2372 lines
126KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce::midi_ci
  19. {
  20. class Device::Impl
  21. {
  22. template <typename This>
  23. static auto getProfileHostImpl (This& t) { return t.profileHost.has_value() ? &*t.profileHost : nullptr; }
  24. template <typename This>
  25. static auto getPropertyHostImpl (This& t) { return t.propertyHost.has_value() ? &*t.propertyHost : nullptr; }
  26. public:
  27. explicit Impl (const Options& opt)
  28. : options (getValidated (opt)),
  29. muid (getReallyRandomMuid())
  30. {
  31. if (options.getFeatures().isProfileConfigurationSupported())
  32. profileHost.emplace (options.getFunctionBlock(), profileDelegate, concreteBufferOutput);
  33. if (options.getFeatures().isPropertyExchangeSupported())
  34. propertyHost.emplace (options.getFunctionBlock(), propertyDelegate, concreteBufferOutput, cacheProvider);
  35. outgoing.reserve (options.getMaxSysExSize());
  36. }
  37. ~Impl()
  38. {
  39. if (concreteBufferOutput.hasSentMuid())
  40. {
  41. detail::MessageTypeUtils::send (concreteBufferOutput,
  42. options.getFunctionBlock().firstGroup,
  43. MUID::getBroadcast(),
  44. ChannelInGroup::wholeBlock,
  45. Message::InvalidateMUID { muid });
  46. }
  47. }
  48. void sendDiscovery()
  49. {
  50. {
  51. const auto aboutToRemove = std::move (discovered);
  52. for (const auto& pair : aboutToRemove)
  53. listeners.call ([&] (auto& l) { l.deviceRemoved (pair.first); });
  54. }
  55. const Message::Header header
  56. {
  57. ChannelInGroup::wholeBlock,
  58. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  59. detail::MessageMeta::implementationVersion,
  60. muid,
  61. MUID::getBroadcast(),
  62. };
  63. jassert (options.getOutputs().size() < 128);
  64. for (size_t i = 0; i < options.getOutputs().size(); ++i)
  65. {
  66. const Message::Discovery discovery
  67. {
  68. options.getDeviceInfo(),
  69. options.getFeatures().getSupportedCapabilities(),
  70. uint32_t (options.getMaxSysExSize()),
  71. std::byte (i % 128),
  72. };
  73. outgoing.clear();
  74. detail::Marshalling::Writer { outgoing } (header, discovery);
  75. options.getOutputs()[i]->processMessage ({ options.getFunctionBlock().firstGroup, outgoing });
  76. }
  77. }
  78. void sendEndpointInquiry (MUID destination, Message::EndpointInquiry endpoint)
  79. {
  80. detail::MessageTypeUtils::send (concreteBufferOutput,
  81. options.getFunctionBlock().firstGroup,
  82. destination,
  83. ChannelInGroup::wholeBlock,
  84. endpoint);
  85. }
  86. void sendProfileInquiry (MUID receiver, ChannelInGroup address)
  87. {
  88. if (! supportsProfiles (receiver))
  89. return;
  90. detail::MessageTypeUtils::send (concreteBufferOutput,
  91. options.getFunctionBlock().firstGroup,
  92. receiver,
  93. address,
  94. Message::ProfileInquiry{});
  95. }
  96. void sendProfileDetailsInquiry (MUID receiver, ChannelInGroup address, Profile profile, std::byte target)
  97. {
  98. if (! supportsProfiles (receiver))
  99. return;
  100. detail::MessageTypeUtils::send (concreteBufferOutput,
  101. options.getFunctionBlock().firstGroup,
  102. receiver,
  103. address,
  104. Message::ProfileDetails { profile, target });
  105. }
  106. void sendProfileSpecificData (MUID receiver, ChannelInGroup address, Profile profile, Span<const std::byte> data)
  107. {
  108. if (! supportsProfiles (receiver))
  109. return;
  110. detail::MessageTypeUtils::send (concreteBufferOutput,
  111. options.getFunctionBlock().firstGroup,
  112. receiver,
  113. address,
  114. Message::ProfileSpecificData { profile, data });
  115. }
  116. void sendProfileEnablement (MUID m, ChannelInGroup address, Profile profile, int numChannels)
  117. {
  118. if (! supportsProfiles (m))
  119. return;
  120. // There are only 256 channels on a UMP endpoint, so requesting more probably doesn't make sense!
  121. jassert (numChannels <= 256);
  122. if (numChannels > 0)
  123. {
  124. const auto channelsToSend = address == ChannelInGroup::wholeBlock || address == ChannelInGroup::wholeGroup
  125. ? 0
  126. : numChannels;
  127. detail::MessageTypeUtils::send (concreteBufferOutput,
  128. options.getFunctionBlock().firstGroup,
  129. m,
  130. address,
  131. Message::ProfileOn { profile, (uint16_t) channelsToSend });
  132. }
  133. else
  134. {
  135. detail::MessageTypeUtils::send (concreteBufferOutput,
  136. options.getFunctionBlock().firstGroup,
  137. m,
  138. address,
  139. Message::ProfileOff { profile });
  140. }
  141. }
  142. void sendPropertyCapabilitiesInquiry (MUID m)
  143. {
  144. if (! supportsProperties (m))
  145. return;
  146. detail::MessageTypeUtils::send (concreteBufferOutput,
  147. options.getFunctionBlock().firstGroup,
  148. m,
  149. ChannelInGroup::wholeBlock,
  150. Message::PropertyExchangeCapabilities { std::byte { propertyDelegate.getNumSimultaneousRequestsSupported() }, {}, {} });
  151. }
  152. ErasedScopeGuard sendPropertyGetInquiry (MUID m,
  153. const PropertyRequestHeader& propertyHeader,
  154. std::function<void (const PropertyExchangeResult&)> callback)
  155. {
  156. const auto iter = discovered.find (m);
  157. if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported())
  158. return {};
  159. auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(),
  160. std::move (callback),
  161. detail::PropertyHostUtils::getTerminator (concreteBufferOutput, options.getFunctionBlock(), m));
  162. if (! primed.isValid())
  163. return {};
  164. detail::MessageTypeUtils::send (concreteBufferOutput,
  165. options.getFunctionBlock().firstGroup,
  166. m,
  167. ChannelInGroup::wholeBlock,
  168. Message::PropertyGetData { { primed.id, Encodings::jsonTo7BitText (propertyHeader.toVarCondensed()) } });
  169. return std::move (primed.token);
  170. }
  171. void sendPropertySetInquiry (MUID m,
  172. const PropertyRequestHeader& propertyHeader,
  173. Span<const std::byte> propertyBody,
  174. std::function<void (const PropertyExchangeResult&)> callback)
  175. {
  176. const auto iter = discovered.find (m);
  177. if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported())
  178. return;
  179. const auto encoded = Encodings::tryEncode (propertyBody, propertyHeader.mutualEncoding);
  180. if (! encoded.has_value())
  181. {
  182. NullCheckedInvocation::invoke (callback, PropertyExchangeResult { PropertyExchangeResult::Error::invalidPayload });
  183. return;
  184. }
  185. auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(),
  186. std::move (callback),
  187. detail::PropertyHostUtils::getTerminator (concreteBufferOutput, options.getFunctionBlock(), m));
  188. if (! primed.isValid())
  189. return;
  190. detail::PropertyHostUtils::send (concreteBufferOutput,
  191. options.getFunctionBlock().firstGroup,
  192. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  193. m,
  194. primed.id,
  195. Encodings::jsonTo7BitText (propertyHeader.toVarCondensed()),
  196. *encoded,
  197. cacheProvider.getMaxSysexSizeForMuid (m));
  198. }
  199. void sendPropertySubscriptionStart (MUID m,
  200. const PropertySubscriptionHeader& header,
  201. std::function<void (const PropertyExchangeResult&)> cb)
  202. {
  203. const auto resource = header.resource;
  204. auto wrappedCallback = [this, m, resource, callback = std::move (cb)] (const PropertyExchangeResult& result)
  205. {
  206. if (! result.getError().has_value())
  207. {
  208. const auto foundMuid = discovered.find (m);
  209. if (foundMuid != discovered.end())
  210. {
  211. const auto parsed = result.getHeaderAsSubscriptionHeader();
  212. // The responder should have given us a subscription ID so that we can reference the original subscription
  213. // whenever we get updates in the future, or if we want to end the subscription.
  214. jassert (parsed.subscribeId.isNotEmpty());
  215. const auto emplaceResult = foundMuid->second.subscriptions.insert ({ parsed.subscribeId, resource });
  216. // If this fails, the device gave us a subscribeId that it was already using for another subscription.
  217. jassertquiet (emplaceResult.second);
  218. }
  219. }
  220. NullCheckedInvocation::invoke (callback, result);
  221. };
  222. inquirePropertySubscribe (m, header, std::move (wrappedCallback));
  223. }
  224. void sendPropertySubscriptionEnd (MUID m,
  225. const String& subscribeId,
  226. std::function<void (const PropertyExchangeResult&)> cb)
  227. {
  228. const auto iter = discovered.find (m);
  229. if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported())
  230. {
  231. // Trying to send a subscription message to a device that doesn't exist (maybe it got removed), or
  232. // that doesn't support property exchange.
  233. jassertfalse;
  234. return;
  235. }
  236. if (iter->second.subscriptions.count ({ subscribeId, {} }) == 0)
  237. {
  238. // Trying to end a subscription that doesn't exist - perhaps it already ended.
  239. jassertfalse;
  240. return;
  241. }
  242. auto wrappedCallback = [this, m, subscribeId, callback = std::move (cb)] (const PropertyExchangeResult& result)
  243. {
  244. if (! result.getError().has_value())
  245. {
  246. const auto foundMuid = discovered.find (m);
  247. if (foundMuid != discovered.end())
  248. foundMuid->second.subscriptions.erase ({ subscribeId, {} });
  249. }
  250. NullCheckedInvocation::invoke (callback, result);
  251. };
  252. PropertySubscriptionHeader header;
  253. header.subscribeId = subscribeId;
  254. header.command = PropertySubscriptionCommand::end;
  255. inquirePropertySubscribe (m, header, std::move (wrappedCallback));
  256. }
  257. std::vector<Subscription> getOngoingSubscriptionsForMuid (MUID m) const
  258. {
  259. const auto iter = discovered.find (m);
  260. if (iter == discovered.end())
  261. return {};
  262. std::vector<Subscription> result;
  263. result.reserve (iter->second.subscriptions.size());
  264. for (const auto& [subscribeId, resource] : iter->second.subscriptions)
  265. result.push_back ({ subscribeId, resource });
  266. return result;
  267. }
  268. int countOngoingPropertyTransactions() const
  269. {
  270. return std::accumulate (discovered.begin(),
  271. discovered.end(),
  272. 0,
  273. [] (auto acc, const auto& pair)
  274. {
  275. return acc + pair.second.initiatorPropertyCaches.countOngoingTransactions();
  276. });
  277. }
  278. void processMessage (ump::BytesOnGroup msg)
  279. {
  280. // Queried before the property host to unconditionally register capabilities of property exchange hosts.
  281. FirstListener firstListener { this };
  282. LastListener lastListener { this };
  283. ResponderDelegate* const l[] { &firstListener,
  284. getProfileHostImpl (*this),
  285. getPropertyHostImpl (*this),
  286. &lastListener };
  287. const auto status = detail::Responder::processCompleteMessage (concreteBufferOutput, msg, l);
  288. if (status == Parser::Status::collidingMUID)
  289. {
  290. muid = getReallyRandomMuid();
  291. concreteBufferOutput.resetSentMuid();
  292. sendDiscovery();
  293. }
  294. }
  295. void addListener (Listener& l)
  296. {
  297. listeners.add (&l);
  298. }
  299. void removeListener (Listener& l)
  300. {
  301. listeners.remove (&l);
  302. }
  303. std::vector<MUID> getDiscoveredMuids() const
  304. {
  305. std::vector<MUID> result (discovered.size(), MUID::makeUnchecked (0));
  306. std::transform (discovered.begin(), discovered.end(), result.begin(), [] (const auto& p) { return p.first; });
  307. return result;
  308. }
  309. std::optional<Message::Discovery> getDiscoveryInfoForMuid (MUID m) const
  310. {
  311. const auto iter = discovered.find (m);
  312. return iter != discovered.end()
  313. ? std::optional<Message::Discovery> (iter->second.discovery)
  314. : std::nullopt;
  315. }
  316. std::optional<int> getNumPropertyExchangeRequestsSupportedForMuid (MUID m) const
  317. {
  318. const auto iter = discovered.find (m);
  319. return iter != discovered.end()
  320. ? std::optional<int> ((int) iter->second.propertyExchangeResponse->numSimultaneousRequestsSupported)
  321. : std::nullopt;
  322. }
  323. const ChannelProfileStates* getProfileStateForMuid (MUID m, ChannelAddress address) const
  324. {
  325. const auto iter = discovered.find (m);
  326. return iter != discovered.end() ? iter->second.profileStates.getStateForDestination (address) : nullptr;
  327. }
  328. var getResourceListForMuid (MUID x) const
  329. {
  330. const auto iter = discovered.find (x);
  331. return iter != discovered.end() ? iter->second.resourceList : var();
  332. }
  333. var getDeviceInfoForMuid (MUID x) const
  334. {
  335. const auto iter = discovered.find (x);
  336. return iter != discovered.end() ? iter->second.deviceInfo : var();
  337. }
  338. var getChannelListForMuid (MUID x) const
  339. {
  340. const auto iter = discovered.find (x);
  341. return iter != discovered.end() ? iter->second.channelList : var();
  342. }
  343. MUID getMuid() const { return muid; }
  344. Options getOptions() const { return options; }
  345. ProfileHost* getProfileHost() { return getProfileHostImpl (*this); }
  346. const ProfileHost* getProfileHost() const { return getProfileHostImpl (*this); }
  347. PropertyHost* getPropertyHost() { return getPropertyHostImpl (*this); }
  348. const PropertyHost* getPropertyHost() const { return getPropertyHostImpl (*this); }
  349. private:
  350. class FirstListener : public ResponderDelegate
  351. {
  352. public:
  353. explicit FirstListener (Impl* d) : device (d) {}
  354. bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override
  355. {
  356. detail::MessageTypeUtils::visit (message, Visitor { device, &output });
  357. return false;
  358. }
  359. private:
  360. class Visitor : public detail::MessageTypeUtils::MessageVisitor
  361. {
  362. public:
  363. Visitor (Impl* d, ResponderOutput* o)
  364. : device (d), output (o) {}
  365. void visit (const Message::PropertyExchangeCapabilities& caps) const override { visitImpl (caps); }
  366. void visit (const Message::PropertyExchangeCapabilitiesResponse& caps) const override { visitImpl (caps); }
  367. using MessageVisitor::visit;
  368. private:
  369. template <typename Body>
  370. void visitImpl (const Body& t) const
  371. {
  372. const auto responderMUID = output->getIncomingHeader().source;
  373. const auto iter = device->discovered.find (responderMUID);
  374. if (iter == device->discovered.end())
  375. return;
  376. iter->second.propertyExchangeResponse = Message::PropertyExchangeCapabilitiesResponse { t.numSimultaneousRequestsSupported,
  377. t.majorVersion,
  378. t.minorVersion };
  379. }
  380. Impl* device = nullptr;
  381. ResponderOutput* output = nullptr;
  382. };
  383. Impl* device = nullptr;
  384. };
  385. class LastListener : public ResponderDelegate
  386. {
  387. public:
  388. explicit LastListener (Impl* d) : device (d) {}
  389. bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override
  390. {
  391. bool result = false;
  392. detail::MessageTypeUtils::visit (message, Visitor { device, &output, &result });
  393. return result;
  394. }
  395. private:
  396. class Visitor : public detail::MessageTypeUtils::MessageVisitor
  397. {
  398. public:
  399. Visitor (Impl* d, ResponderOutput* o, bool* b)
  400. : device (d), output (o), handled (b) {}
  401. void visit (const Message::Discovery& x) const override { visitImpl (x); }
  402. void visit (const Message::DiscoveryResponse& x) const override { visitImpl (x); }
  403. void visit (const Message::InvalidateMUID& x) const override { visitImpl (x); }
  404. void visit (const Message::EndpointInquiry& x) const override { visitImpl (x); }
  405. void visit (const Message::EndpointInquiryResponse& x) const override { visitImpl (x); }
  406. void visit (const Message::NAK& x) const override { visitImpl (x); }
  407. void visit (const Message::ProfileInquiryResponse& x) const override { visitImpl (x); }
  408. void visit (const Message::ProfileAdded& x) const override { visitImpl (x); }
  409. void visit (const Message::ProfileRemoved& x) const override { visitImpl (x); }
  410. void visit (const Message::ProfileEnabledReport& x) const override { visitImpl (x); }
  411. void visit (const Message::ProfileDisabledReport& x) const override { visitImpl (x); }
  412. void visit (const Message::ProfileDetailsResponse& x) const override { visitImpl (x); }
  413. void visit (const Message::ProfileSpecificData& x) const override { visitImpl (x); }
  414. void visit (const Message::PropertyExchangeCapabilitiesResponse& x) const override { visitImpl (x); }
  415. void visit (const Message::PropertyGetDataResponse& x) const override { visitImpl (x); }
  416. void visit (const Message::PropertySetDataResponse& x) const override { visitImpl (x); }
  417. void visit (const Message::PropertySubscribe& x) const override { visitImpl (x); }
  418. void visit (const Message::PropertySubscribeResponse& x) const override { visitImpl (x); }
  419. void visit (const Message::PropertyNotify& x) const override { visitImpl (x); }
  420. using MessageVisitor::visit;
  421. private:
  422. template <typename Body>
  423. void visitImpl (const Body& body) const { *handled = messageReceived (body); }
  424. bool messageReceived (const Message::Discovery& body) const
  425. {
  426. const auto replyPath = uint8_t (output->getIncomingHeader().version) >= 0x02 ? body.outputPathID : std::byte { 0x00 };
  427. detail::MessageTypeUtils::send (*output, Message::DiscoveryResponse
  428. {
  429. device->options.getDeviceInfo(),
  430. device->options.getFeatures().getSupportedCapabilities(),
  431. uint32_t (device->options.getMaxSysExSize()),
  432. replyPath,
  433. device->options.getFunctionBlock().identifier,
  434. });
  435. // TODO(reuk) rather than sending a new discovery inquiry, we should store the details from the incoming message
  436. const auto iter = device->discovered.find (output->getIncomingHeader().source);
  437. if (iter == device->discovered.end())
  438. {
  439. const auto initiator = output->getIncomingHeader().source;
  440. device->discovered.emplace (initiator, Discovered { body });
  441. device->listeners.call ([&] (auto& l) { l.deviceAdded (initiator); });
  442. device->sendEndpointInquiry (initiator, Message::EndpointInquiry { std::byte{} });
  443. }
  444. return true;
  445. }
  446. bool messageReceived (const Message::DiscoveryResponse& response) const
  447. {
  448. const auto responderMUID = output->getIncomingHeader().source;
  449. const auto iter = device->discovered.find (responderMUID);
  450. if (iter != device->discovered.end())
  451. {
  452. device->discovered.erase (iter);
  453. device->listeners.call ([&] (auto& l) { l.deviceRemoved (responderMUID); });
  454. const Message::Header header
  455. {
  456. ChannelInGroup::wholeBlock,
  457. detail::MessageMeta::Meta<Message::InvalidateMUID>::subID2,
  458. detail::MessageMeta::implementationVersion,
  459. device->muid,
  460. MUID::getBroadcast(),
  461. };
  462. detail::MessageTypeUtils::send (*output, output->getIncomingGroup(), header, Message::InvalidateMUID { responderMUID });
  463. }
  464. else
  465. {
  466. const Message::Discovery discovery { response.device,
  467. response.capabilities,
  468. response.maximumSysexSize,
  469. response.outputPathID };
  470. device->discovered.emplace (responderMUID, Discovered { discovery });
  471. device->listeners.call ([&] (auto& l) { l.deviceAdded (responderMUID); });
  472. device->sendEndpointInquiry (output->getIncomingHeader().source, Message::EndpointInquiry { std::byte{} });
  473. }
  474. return true;
  475. }
  476. bool messageReceived (const Message::InvalidateMUID& invalidate) const
  477. {
  478. const auto targetMuid = invalidate.target;
  479. const auto iter = device->discovered.find (targetMuid);
  480. if (iter != device->discovered.end())
  481. {
  482. device->discovered.erase (iter);
  483. device->listeners.call ([&] (auto& l) { l.deviceRemoved (targetMuid); });
  484. }
  485. if (invalidate.target != device->muid)
  486. return false;
  487. device->muid = getReallyRandomMuid();
  488. device->concreteBufferOutput.resetSentMuid();
  489. device->sendDiscovery();
  490. return true;
  491. }
  492. bool messageReceived (const Message::EndpointInquiry& endpoint) const
  493. {
  494. // Only status 0 is defined at time of writing
  495. if (endpoint.status == std::byte{})
  496. {
  497. const auto& id = device->options.getProductInstanceId();
  498. const auto length = std::distance (id.begin(), std::find (id.begin(), id.end(), 0));
  499. if (length <= 0)
  500. return false;
  501. Message::EndpointInquiryResponse response;
  502. response.status = endpoint.status;
  503. response.data = Span<const std::byte> (reinterpret_cast<const std::byte*> (id.data()), (size_t) length);
  504. detail::MessageTypeUtils::send (*output, response);
  505. return true;
  506. }
  507. return false;
  508. }
  509. bool messageReceived (const Message::EndpointInquiryResponse& endpoint) const
  510. {
  511. const auto responderMUID = output->getIncomingHeader().source;
  512. const auto iter = device->discovered.find (responderMUID);
  513. if (iter == device->discovered.end())
  514. return false; // Got an endpoint response for a device we haven't discovered
  515. device->listeners.call ([&] (auto& l) { l.endpointReceived (responderMUID, endpoint); });
  516. return true;
  517. }
  518. bool messageReceived (const Message::NAK& nak) const
  519. {
  520. const auto responderMUID = output->getIncomingHeader().source;
  521. device->listeners.call ([&] (auto& l) { l.messageNotAcknowledged (responderMUID, nak); });
  522. return true;
  523. }
  524. bool messageReceived (const Message::ProfileInquiryResponse& response) const
  525. {
  526. const auto responderMUID = output->getIncomingHeader().source;
  527. const auto iter = device->discovered.find (responderMUID);
  528. if (iter == device->discovered.end())
  529. return false;
  530. const auto destination = output->getIncomingHeader().deviceID;
  531. auto* state = iter->second.profileStates.getStateForDestination (output->getChannelAddress());
  532. if (state == nullptr)
  533. return false;
  534. ChannelProfileStates newState;
  535. for (auto& enabled : response.enabledProfiles)
  536. newState.set (enabled, { 1, 1 });
  537. for (auto& disabled : response.disabledProfiles)
  538. newState.set (disabled, { 1, 0 });
  539. *state = newState;
  540. device->listeners.call ([&] (auto& l) { l.profileStateReceived (responderMUID, destination); });
  541. return true;
  542. }
  543. bool messageReceived (const Message::ProfileAdded& added) const
  544. {
  545. const auto responderMUID = output->getIncomingHeader().source;
  546. const auto iter = device->discovered.find (responderMUID);
  547. if (iter == device->discovered.end())
  548. return false;
  549. const auto address = output->getChannelAddress();
  550. auto* state = iter->second.profileStates.getStateForDestination (address);
  551. if (state == nullptr)
  552. return false;
  553. state->set (added.profile, { 1, 0 });
  554. device->listeners.call ([&] (auto& l) { l.profilePresenceChanged (responderMUID, address.getChannel(), added.profile, true); });
  555. return true;
  556. }
  557. bool messageReceived (const Message::ProfileRemoved& removed) const
  558. {
  559. const auto responderMUID = output->getIncomingHeader().source;
  560. const auto iter = device->discovered.find (responderMUID);
  561. if (iter == device->discovered.end())
  562. return false;
  563. const auto address = output->getChannelAddress();
  564. auto* state = iter->second.profileStates.getStateForDestination (address);
  565. if (state == nullptr)
  566. return false;
  567. state->erase (removed.profile);
  568. device->listeners.call ([&] (auto& l) { l.profilePresenceChanged (responderMUID, address.getChannel(), removed.profile, false); });
  569. return true;
  570. }
  571. bool messageReceived (const Message::ProfileEnabledReport& x) const
  572. {
  573. const auto responderMUID = output->getIncomingHeader().source;
  574. const auto iter = device->discovered.find (responderMUID);
  575. if (iter == device->discovered.end())
  576. return false;
  577. const auto address = output->getChannelAddress();
  578. auto* state = iter->second.profileStates.getStateForDestination (address);
  579. if (state == nullptr)
  580. return false;
  581. const auto numChannels = jmax ((uint16_t) 1, x.numChannels);
  582. state->set (x.profile, { state->get (x.profile).supported, numChannels });
  583. device->listeners.call ([&] (auto& l) { l.profileEnablementChanged (responderMUID, address.getChannel(), x.profile, numChannels); });
  584. return true;
  585. }
  586. bool messageReceived (const Message::ProfileDisabledReport& x) const
  587. {
  588. const auto responderMUID = output->getIncomingHeader().source;
  589. const auto iter = device->discovered.find (responderMUID);
  590. if (iter == device->discovered.end())
  591. return false;
  592. const auto address = output->getChannelAddress();
  593. auto* state = iter->second.profileStates.getStateForDestination (address);
  594. if (state == nullptr)
  595. return false;
  596. state->set (x.profile, { state->get (x.profile).supported, 0 });
  597. device->listeners.call ([&] (auto& l) { l.profileEnablementChanged (responderMUID, address.getChannel(), x.profile, 0); });
  598. return true;
  599. }
  600. bool messageReceived (const Message::ProfileDetailsResponse& response) const
  601. {
  602. const auto responderMUID = output->getIncomingHeader().source;
  603. const auto destination = output->getIncomingHeader().deviceID;
  604. device->listeners.call ([&] (auto& l) { l.profileDetailsReceived (responderMUID, destination, response.profile, response.target, response.data); });
  605. return true;
  606. }
  607. bool messageReceived (const Message::ProfileSpecificData& data) const
  608. {
  609. const auto responderMUID = output->getIncomingHeader().source;
  610. const auto destination = output->getIncomingHeader().deviceID;
  611. device->listeners.call ([&] (auto& l) { l.profileSpecificDataReceived (responderMUID, destination, data.profile, data.data); });
  612. return true;
  613. }
  614. bool messageReceived (const Message::PropertyExchangeCapabilitiesResponse&) const
  615. {
  616. const auto source = output->getIncomingHeader().source;
  617. const auto iter = device->discovered.find (source);
  618. constexpr auto hasResource = [] (var obj, auto resource)
  619. {
  620. if (auto* array = obj.getArray())
  621. for (const auto& item : *array)
  622. if (item.isObject() && item.getProperty ("resource", {}) == var (resource))
  623. return true;
  624. return false;
  625. };
  626. const auto transaction = device->ongoingTransactions.emplace (device->ongoingTransactions.end());
  627. const auto onResourceListReceived = [this, iter, source, hasResource, transaction] (const PropertyExchangeResult& result)
  628. {
  629. const auto validateResponse = [] (const PropertyExchangeResult& r)
  630. {
  631. const auto parsed = r.getHeaderAsReplyHeader();
  632. return ! r.getError().has_value()
  633. && parsed.mediaType == PropertySubscriptionHeader().mediaType
  634. && parsed.status == 200;
  635. };
  636. const auto allDone = [this, source, transaction]
  637. {
  638. device->ongoingTransactions.erase (transaction);
  639. device->listeners.call ([source] (auto& l) { l.propertyExchangeCapabilitiesReceived (source); });
  640. };
  641. if (! validateResponse (result))
  642. {
  643. jassertfalse;
  644. allDone();
  645. return;
  646. }
  647. const auto bodyAsObj = Encodings::jsonFrom7BitText (result.getBody());
  648. iter->second.resourceList = bodyAsObj;
  649. const auto onChannelListReceived = [iter, allDone, validateResponse] (const PropertyExchangeResult& r)
  650. {
  651. if (validateResponse (r))
  652. iter->second.channelList = Encodings::jsonFrom7BitText (r.getBody());
  653. allDone();
  654. return;
  655. };
  656. const auto getChannelList = [this, bodyAsObj, source, allDone, hasResource, onChannelListReceived, transaction]
  657. {
  658. if (hasResource (bodyAsObj, "ChannelList"))
  659. {
  660. PropertyRequestHeader header;
  661. header.resource = "ChannelList";
  662. *transaction = device->sendPropertyGetInquiry (source, header, onChannelListReceived);
  663. return;
  664. }
  665. allDone();
  666. return;
  667. };
  668. if (hasResource (bodyAsObj, "DeviceInfo"))
  669. {
  670. PropertyRequestHeader header;
  671. header.resource = "DeviceInfo";
  672. *transaction = device->sendPropertyGetInquiry (source,
  673. header,
  674. [iter, getChannelList, validateResponse] (const PropertyExchangeResult& r)
  675. {
  676. if (validateResponse (r))
  677. iter->second.deviceInfo = Encodings::jsonFrom7BitText (r.getBody());
  678. getChannelList();
  679. });
  680. return;
  681. }
  682. return getChannelList();
  683. };
  684. PropertyRequestHeader header;
  685. header.resource = "ResourceList";
  686. *transaction = device->sendPropertyGetInquiry (source, header, onResourceListReceived);
  687. return true;
  688. }
  689. bool handlePropertyDataResponse (const Message::DynamicSizePropertyExchange& response) const
  690. {
  691. const auto responderMUID = output->getIncomingHeader().source;
  692. const auto iter = device->discovered.find (responderMUID);
  693. if (iter == device->discovered.end())
  694. return false;
  695. iter->second.initiatorPropertyCaches.addChunk (response.requestID, response);
  696. return true;
  697. }
  698. bool messageReceived (const Message::PropertyGetDataResponse& response) const
  699. {
  700. handlePropertyDataResponse (response);
  701. return true;
  702. }
  703. bool messageReceived (const Message::PropertySetDataResponse& response) const
  704. {
  705. handlePropertyDataResponse (Message::DynamicSizePropertyExchange { response.requestID,
  706. response.header,
  707. 1,
  708. 1,
  709. {} });
  710. return true;
  711. }
  712. bool messageReceived (const Message::PropertySubscribe& subscription) const
  713. {
  714. const auto responderMUID = output->getIncomingHeader().source;
  715. const auto iter = device->discovered.find (responderMUID);
  716. if (iter == device->discovered.end())
  717. return false;
  718. const auto request = subscription.requestID;
  719. const auto source = output->getIncomingHeader().source;
  720. const auto jsonHeader = Encodings::jsonFrom7BitText (subscription.header);
  721. const auto typedHeader = PropertySubscriptionHeader::parseCondensed (jsonHeader);
  722. const auto subscribeId = typedHeader.subscribeId;
  723. const auto callback = [this, request, source, subscribeId] (const PropertyExchangeResult& result)
  724. {
  725. if (result.getError().has_value())
  726. return;
  727. PropertySubscriptionData data;
  728. data.header = result.getHeaderAsSubscriptionHeader();
  729. data.body = result.getBody();
  730. if (data.header.command == PropertySubscriptionCommand::end)
  731. {
  732. const auto foundMuid = device->discovered.find (source);
  733. if (foundMuid != device->discovered.end())
  734. foundMuid->second.subscriptions.erase ({ data.header.subscribeId, {} });
  735. }
  736. if (data.header.command != PropertySubscriptionCommand::start)
  737. device->listeners.call ([source, &data] (auto& l) { l.propertySubscriptionDataReceived (source, data); });
  738. PropertyReplyHeader header;
  739. const auto headerBytes = Encodings::jsonTo7BitText (header.toVarCondensed());
  740. detail::MessageTypeUtils::send (device->concreteBufferOutput,
  741. device->options.getFunctionBlock().firstGroup,
  742. source,
  743. ChannelInGroup::wholeBlock,
  744. Message::PropertySubscribeResponse { { request, headerBytes, 1, 1, {} } });
  745. };
  746. // Subscription events may be sent at any time by the responder, so there may not be
  747. // an existing transaction ID for new subscription messages.
  748. iter->second.responderPropertyCaches.primeCache (device->propertyDelegate.getNumSimultaneousRequestsSupported(),
  749. callback,
  750. subscription.requestID);
  751. iter->second.responderPropertyCaches.addChunk (subscription.requestID, subscription);
  752. return true;
  753. }
  754. bool messageReceived (const Message::PropertySubscribeResponse& response) const
  755. {
  756. handlePropertyDataResponse (response);
  757. return true;
  758. }
  759. bool messageReceived (const Message::PropertyNotify& notify) const
  760. {
  761. const auto responderMUID = output->getIncomingHeader().source;
  762. const auto iter = device->discovered.find (responderMUID);
  763. if (iter == device->discovered.end())
  764. return false;
  765. iter->second.initiatorPropertyCaches.notify (notify.requestID, notify.header);
  766. iter->second.responderPropertyCaches.notify (notify.requestID, notify.header);
  767. return true;
  768. }
  769. Impl* device = nullptr;
  770. ResponderOutput* output = nullptr;
  771. bool* handled = nullptr;
  772. };
  773. Impl* device = nullptr;
  774. };
  775. struct Discovered
  776. {
  777. explicit Discovered (Message::Discovery r) : discovery (r) {}
  778. Message::Discovery discovery;
  779. std::optional<Message::PropertyExchangeCapabilitiesResponse> propertyExchangeResponse;
  780. BlockProfileStates profileStates;
  781. InitiatorPropertyExchangeCache initiatorPropertyCaches;
  782. ResponderPropertyExchangeCache responderPropertyCaches;
  783. var resourceList, deviceInfo, channelList;
  784. std::set<Subscription> subscriptions; ///< subscribeIds of subscriptions that we initiated
  785. };
  786. class ConcreteBufferOutput : public BufferOutput
  787. {
  788. public:
  789. explicit ConcreteBufferOutput (Impl& d) : device (d) {}
  790. MUID getMuid() const override { return device.muid; }
  791. std::vector<std::byte>& getOutputBuffer() override { return device.outgoing; }
  792. void send (uint8_t group) override
  793. {
  794. sentMuid = true;
  795. for (auto* o : device.options.getOutputs())
  796. o->processMessage ({ group, getOutputBuffer() });
  797. }
  798. bool hasSentMuid() const { return sentMuid; }
  799. void resetSentMuid() { sentMuid = false; }
  800. private:
  801. Impl& device;
  802. bool sentMuid = false;
  803. };
  804. class CacheProviderImpl : public CacheProvider
  805. {
  806. public:
  807. explicit CacheProviderImpl (Impl& d) : device (d) {}
  808. std::set<MUID> getDiscoveredMuids() const override
  809. {
  810. std::set<MUID> result;
  811. for (const auto& d : device.discovered)
  812. result.insert (d.first);
  813. return result;
  814. }
  815. InitiatorPropertyExchangeCache* getCacheForMuidAsInitiator (MUID m) override
  816. {
  817. const auto iter = device.discovered.find (m);
  818. return iter != device.discovered.end() ? &iter->second.initiatorPropertyCaches : nullptr;
  819. }
  820. ResponderPropertyExchangeCache* getCacheForMuidAsResponder (MUID m) override
  821. {
  822. const auto iter = device.discovered.find (m);
  823. return iter != device.discovered.end() ? &iter->second.responderPropertyCaches : nullptr;
  824. }
  825. int getMaxSysexSizeForMuid (MUID m) const override
  826. {
  827. constexpr auto defaultResult = 1 << 16;
  828. const auto iter = device.discovered.find (m);
  829. return iter != device.discovered.end() ? jmin (defaultResult, (int) iter->second.discovery.maximumSysexSize) : defaultResult;
  830. }
  831. private:
  832. Impl& device;
  833. };
  834. class ProfileDelegateImpl : public ProfileDelegate
  835. {
  836. public:
  837. explicit ProfileDelegateImpl (Impl& d) : device (d) {}
  838. void profileEnablementRequested (MUID x, ProfileAtAddress profileAtAddress, int numChannels, bool enabled) override
  839. {
  840. if (auto* d = device.options.getProfileDelegate())
  841. return d->profileEnablementRequested (x, profileAtAddress, numChannels, enabled);
  842. if (! device.profileHost.has_value())
  843. return;
  844. device.profileHost->setProfileEnablement (profileAtAddress, enabled ? jmax (1, numChannels) : 0);
  845. }
  846. private:
  847. Impl& device;
  848. };
  849. class PropertyDelegateImpl : public PropertyDelegate
  850. {
  851. public:
  852. explicit PropertyDelegateImpl (Impl& d) : device (d) {}
  853. uint8_t getNumSimultaneousRequestsSupported() const override
  854. {
  855. if (auto* d = device.options.getPropertyDelegate())
  856. return d->getNumSimultaneousRequestsSupported();
  857. return 127;
  858. }
  859. PropertyReplyData propertyGetDataRequested (MUID m, const PropertyRequestHeader& header) override
  860. {
  861. if (auto* d = device.options.getPropertyDelegate())
  862. return d->propertyGetDataRequested (m, header);
  863. PropertyReplyData result;
  864. result.header.status = 404; // Resource not found, do not retry
  865. result.header.message = TRANS ("Handling for \"Inquiry: Get Property Data\" is not implemented.");
  866. return result;
  867. }
  868. PropertyReplyHeader propertySetDataRequested (MUID m, const PropertyRequestData& data) override
  869. {
  870. if (auto* d = device.options.getPropertyDelegate())
  871. return d->propertySetDataRequested (m, data);
  872. PropertyReplyHeader result;
  873. result.status = 404; // Resource not found, do not retry
  874. result.message = TRANS ("Handling for \"Inquiry: Set Property Data\" is not implemented.");
  875. return result;
  876. }
  877. bool subscriptionStartRequested (MUID m, const PropertySubscriptionHeader& data) override
  878. {
  879. if (auto* d = device.options.getPropertyDelegate())
  880. return d->subscriptionStartRequested (m, data);
  881. return false;
  882. }
  883. void subscriptionDidStart (MUID m, const String& id, const PropertySubscriptionHeader& data) override
  884. {
  885. if (auto* d = device.options.getPropertyDelegate())
  886. d->subscriptionDidStart (m, id, data);
  887. }
  888. void subscriptionWillEnd (MUID m, const ci::Subscription& subscription) override
  889. {
  890. if (auto* d = device.options.getPropertyDelegate())
  891. d->subscriptionWillEnd (m, subscription);
  892. }
  893. private:
  894. Impl& device;
  895. };
  896. static MUID getReallyRandomMuid()
  897. {
  898. Random random;
  899. random.setSeedRandomly();
  900. return MUID::makeRandom (random);
  901. }
  902. static DeviceOptions getValidated (DeviceOptions opt)
  903. {
  904. opt = opt.withMaxSysExSize (jmax ((size_t) 128, opt.getMaxSysExSize()));
  905. if (opt.getFeatures().isPropertyExchangeSupported())
  906. opt = opt.withMaxSysExSize (jmax ((size_t) 512, opt.getMaxSysExSize()));
  907. opt = opt.withFeatures (opt.getFeatures().withProcessInquirySupported (false));
  908. // You'll need to provide some outputs if you want the device to talk to the outside world!
  909. jassert (! opt.getOutputs().empty());
  910. return opt;
  911. }
  912. template <typename Member>
  913. bool supportsFlag (MUID m, Member member) const
  914. {
  915. const auto iter = discovered.find (m);
  916. return iter != discovered.end() && (Features (iter->second.discovery.capabilities).*member)();
  917. }
  918. bool supportsProfiles (MUID m) const
  919. {
  920. return supportsFlag (m, &Features::isProfileConfigurationSupported);
  921. }
  922. bool supportsProperties (MUID m) const
  923. {
  924. return supportsFlag (m, &Features::isPropertyExchangeSupported);
  925. }
  926. void inquirePropertySubscribe (MUID m,
  927. const PropertySubscriptionHeader& header,
  928. std::function<void (const PropertyExchangeResult&)> cb)
  929. {
  930. const auto iter = discovered.find (m);
  931. if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported())
  932. {
  933. // Trying to send a subscription message to a device that doesn't exist (maybe it got removed), or
  934. // that doesn't support property exchange.
  935. jassertfalse;
  936. return;
  937. }
  938. auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(),
  939. std::move (cb),
  940. detail::PropertyHostUtils::getTerminator (concreteBufferOutput, options.getFunctionBlock(), m));
  941. if (! primed.isValid())
  942. return;
  943. // TODO(reuk) this isn't ideal, make subscription/request handling more robust
  944. primed.token.release();
  945. detail::PropertyHostUtils::send (concreteBufferOutput,
  946. options.getFunctionBlock().firstGroup,
  947. detail::MessageMeta::Meta<Message::PropertySubscribe>::subID2,
  948. m,
  949. primed.id,
  950. Encodings::jsonTo7BitText (header.toVarCondensed()),
  951. {},
  952. cacheProvider.getMaxSysexSizeForMuid (m));
  953. }
  954. DeviceOptions options;
  955. MUID muid;
  956. std::vector<std::byte> outgoing;
  957. std::map<MUID, Discovered> discovered;
  958. ListenerList<Listener> listeners;
  959. ConcreteBufferOutput concreteBufferOutput { *this };
  960. CacheProviderImpl cacheProvider { *this };
  961. ProfileDelegateImpl profileDelegate { *this };
  962. PropertyDelegateImpl propertyDelegate { *this };
  963. std::optional<ProfileHost> profileHost;
  964. std::optional<PropertyHost> propertyHost;
  965. std::list<ErasedScopeGuard> ongoingTransactions;
  966. };
  967. //==============================================================================
  968. Device::Device (const Options& opt) : pimpl (std::make_unique<Impl> (opt)) {}
  969. Device::~Device() = default;
  970. Device::Device (Device&&) noexcept = default;
  971. Device& Device::operator= (Device&&) noexcept = default;
  972. void Device::processMessage (ump::BytesOnGroup msg) { pimpl->processMessage (msg); }
  973. void Device::sendDiscovery() { pimpl->sendDiscovery(); }
  974. void Device::sendEndpointInquiry (MUID destination, Message::EndpointInquiry endpoint) { pimpl->sendEndpointInquiry (destination, endpoint); }
  975. void Device::sendProfileInquiry (MUID destination, ChannelInGroup address) { pimpl->sendProfileInquiry (destination, address); }
  976. void Device::sendProfileDetailsInquiry (MUID destination, ChannelInGroup address, Profile profile, std::byte target)
  977. {
  978. pimpl->sendProfileDetailsInquiry (destination, address, profile, target);
  979. }
  980. void Device::sendProfileSpecificData (MUID destination, ChannelInGroup address, Profile profile, Span<const std::byte> data)
  981. {
  982. pimpl->sendProfileSpecificData (destination, address, profile, data);
  983. }
  984. void Device::sendProfileEnablement (MUID destination, ChannelInGroup address, Profile profile, int numChannels)
  985. {
  986. pimpl->sendProfileEnablement (destination, address, profile, numChannels);
  987. }
  988. void Device::sendPropertyCapabilitiesInquiry (MUID destination)
  989. {
  990. pimpl->sendPropertyCapabilitiesInquiry (destination);
  991. }
  992. ErasedScopeGuard Device::sendPropertyGetInquiry (MUID destination,
  993. const PropertyRequestHeader& header,
  994. std::function<void (const PropertyExchangeResult&)> onResult)
  995. {
  996. return pimpl->sendPropertyGetInquiry (destination, header, std::move (onResult));
  997. }
  998. void Device::sendPropertySetInquiry (MUID destination,
  999. const PropertyRequestHeader& header,
  1000. Span<const std::byte> body,
  1001. std::function<void (const PropertyExchangeResult&)> onResult)
  1002. {
  1003. pimpl->sendPropertySetInquiry (destination, header, body, std::move (onResult));
  1004. }
  1005. void Device::sendPropertySubscriptionStart (MUID destination,
  1006. const PropertySubscriptionHeader& header,
  1007. std::function<void (const PropertyExchangeResult&)> onResult)
  1008. {
  1009. pimpl->sendPropertySubscriptionStart (destination, header, std::move (onResult));
  1010. }
  1011. void Device::sendPropertySubscriptionEnd (MUID destination,
  1012. const String& subscribeId,
  1013. std::function<void (const PropertyExchangeResult&)> onResult)
  1014. {
  1015. pimpl->sendPropertySubscriptionEnd (destination, subscribeId, std::move (onResult));
  1016. }
  1017. std::vector<Subscription> Device::getOngoingSubscriptionsForMuid (MUID m) const { return pimpl->getOngoingSubscriptionsForMuid (m); }
  1018. int Device::countOngoingPropertyTransactions() const { return pimpl->countOngoingPropertyTransactions(); }
  1019. void Device::addListener (Listener& l) { pimpl->addListener (l); }
  1020. void Device::removeListener (Listener& l) { pimpl->removeListener (l); }
  1021. MUID Device::getMuid() const { return pimpl->getMuid(); }
  1022. DeviceOptions Device::getOptions() const { return pimpl->getOptions(); }
  1023. std::vector<MUID> Device::getDiscoveredMuids() const { return pimpl->getDiscoveredMuids(); }
  1024. const ProfileHost* Device::getProfileHost() const { return pimpl->getProfileHost(); }
  1025. ProfileHost* Device::getProfileHost() { return pimpl->getProfileHost(); }
  1026. const PropertyHost* Device::getPropertyHost() const { return pimpl->getPropertyHost(); }
  1027. PropertyHost* Device::getPropertyHost() { return pimpl->getPropertyHost(); }
  1028. std::optional<Message::Discovery> Device::getDiscoveryInfoForMuid (MUID m) const { return pimpl->getDiscoveryInfoForMuid (m); }
  1029. const ChannelProfileStates* Device::getProfileStateForMuid (MUID m, ChannelAddress address) const { return pimpl->getProfileStateForMuid (m, address); }
  1030. std::optional<int> Device::getNumPropertyExchangeRequestsSupportedForMuid (MUID m) const
  1031. {
  1032. return pimpl->getNumPropertyExchangeRequestsSupportedForMuid (m);
  1033. }
  1034. var Device::getResourceListForMuid (MUID x) const { return pimpl->getResourceListForMuid (x); }
  1035. var Device::getDeviceInfoForMuid (MUID x) const { return pimpl->getDeviceInfoForMuid (x); }
  1036. var Device::getChannelListForMuid (MUID x) const { return pimpl->getChannelListForMuid (x); }
  1037. //==============================================================================
  1038. //==============================================================================
  1039. #if JUCE_UNIT_TESTS
  1040. class DeviceTests : public UnitTest
  1041. {
  1042. public:
  1043. DeviceTests() : UnitTest ("Device", UnitTestCategories::midi) {}
  1044. void runTest() override
  1045. {
  1046. auto random = getRandom();
  1047. struct GroupOutput
  1048. {
  1049. uint8_t group;
  1050. std::vector<std::byte> bytes;
  1051. bool operator== (const GroupOutput& other) const
  1052. {
  1053. const auto tie = [] (const auto& x) { return std::tie (x.group, x.bytes); };
  1054. return tie (*this) == tie (other);
  1055. }
  1056. bool operator!= (const GroupOutput& other) const { return ! operator== (other); }
  1057. };
  1058. struct Output : public DeviceMessageHandler
  1059. {
  1060. void processMessage (ump::BytesOnGroup msg) override
  1061. {
  1062. messages.push_back ({ msg.group, std::vector<std::byte> (msg.bytes.begin(), msg.bytes.end()) });
  1063. }
  1064. std::vector<GroupOutput> messages;
  1065. };
  1066. const ump::DeviceInfo deviceInfo { { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 } },
  1067. { std::byte { 0x11 }, std::byte { 0x12 } },
  1068. { std::byte { 0x21 }, std::byte { 0x22 } },
  1069. { std::byte { 0x31 }, std::byte { 0x32 }, std::byte { 0x33 }, std::byte { 0x34 } } };
  1070. FunctionBlock functionBlock;
  1071. beginTest ("When receiving Discovery from a MUID that matches the Device MUID, reply with InvalidateMUID and initiate discovery");
  1072. {
  1073. Output output;
  1074. const auto options = DeviceOptions().withOutputs ({ &output })
  1075. .withFunctionBlock (functionBlock)
  1076. .withDeviceInfo (deviceInfo)
  1077. .withMaxSysExSize (512);
  1078. Device device { options };
  1079. const auto commonMUID = device.getMuid();
  1080. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1081. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1082. detail::MessageMeta::implementationVersion,
  1083. commonMUID,
  1084. MUID::getBroadcast() },
  1085. Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } },
  1086. { std::byte { 0x15 }, std::byte { 0x16 } },
  1087. { std::byte { 0x25 }, std::byte { 0x26 } },
  1088. { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } },
  1089. std::byte{},
  1090. 1024,
  1091. std::byte{} }) });
  1092. expect (device.getMuid() != commonMUID);
  1093. const std::vector<GroupOutput> responses
  1094. {
  1095. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1096. detail::MessageMeta::Meta<Message::InvalidateMUID>::subID2,
  1097. detail::MessageMeta::implementationVersion,
  1098. commonMUID,
  1099. MUID::getBroadcast() },
  1100. Message::InvalidateMUID { commonMUID }) },
  1101. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1102. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1103. detail::MessageMeta::implementationVersion,
  1104. device.getMuid(),
  1105. MUID::getBroadcast() },
  1106. Message::Discovery { deviceInfo, std::byte{}, 512, std::byte{} }) },
  1107. };
  1108. expect (output.messages == responses);
  1109. }
  1110. beginTest ("When receiving Discovery from a MUID that does not match the Device MUID, reply with DiscoveryResponse and EndpointInquiry");
  1111. {
  1112. Output output;
  1113. const auto options = DeviceOptions().withOutputs ({ &output })
  1114. .withFunctionBlock (functionBlock)
  1115. .withDeviceInfo (deviceInfo)
  1116. .withMaxSysExSize (512);
  1117. Device device { options };
  1118. const auto responderMUID = device.getMuid();
  1119. const auto initiatorMUID = MUID::makeRandom (random);
  1120. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1121. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1122. detail::MessageMeta::implementationVersion,
  1123. initiatorMUID,
  1124. MUID::getBroadcast() },
  1125. Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } },
  1126. { std::byte { 0x15 }, std::byte { 0x16 } },
  1127. { std::byte { 0x25 }, std::byte { 0x26 } },
  1128. { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } },
  1129. std::byte{},
  1130. 1024,
  1131. std::byte{} }) });
  1132. expect (device.getMuid() == responderMUID);
  1133. const std::vector<GroupOutput> responses
  1134. {
  1135. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1136. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1137. detail::MessageMeta::implementationVersion,
  1138. responderMUID,
  1139. initiatorMUID },
  1140. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) },
  1141. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1142. detail::MessageMeta::Meta<Message::EndpointInquiry>::subID2,
  1143. detail::MessageMeta::implementationVersion,
  1144. responderMUID,
  1145. initiatorMUID },
  1146. Message::EndpointInquiry { std::byte{} }) },
  1147. };
  1148. expect (output.messages == responses);
  1149. }
  1150. beginTest ("Sending a V1 discovery message notifies the listener");
  1151. {
  1152. Output output;
  1153. const auto options = DeviceOptions().withOutputs ({ &output })
  1154. .withFunctionBlock (functionBlock)
  1155. .withDeviceInfo (deviceInfo)
  1156. .withMaxSysExSize (512);
  1157. Device device { options };
  1158. const auto responderMUID = device.getMuid();
  1159. const auto initiatorMUID = MUID::makeRandom (random);
  1160. constexpr uint8_t version = 0x01;
  1161. auto bytes = getMessageBytes ({ ChannelInGroup::wholeBlock,
  1162. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1163. std::byte { version },
  1164. initiatorMUID,
  1165. MUID::getBroadcast() },
  1166. Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } },
  1167. { std::byte { 0x15 }, std::byte { 0x16 } },
  1168. { std::byte { 0x25 }, std::byte { 0x26 } },
  1169. { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } },
  1170. std::byte{},
  1171. 1024,
  1172. std::byte{} });
  1173. // V1 message doesn't have an output path
  1174. bytes.pop_back();
  1175. device.processMessage ({ 0, bytes });
  1176. expect (device.getMuid() == responderMUID);
  1177. const std::vector<GroupOutput> responses
  1178. {
  1179. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1180. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1181. detail::MessageMeta::implementationVersion,
  1182. responderMUID,
  1183. initiatorMUID },
  1184. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) },
  1185. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1186. detail::MessageMeta::Meta<Message::EndpointInquiry>::subID2,
  1187. detail::MessageMeta::implementationVersion,
  1188. responderMUID,
  1189. initiatorMUID },
  1190. Message::EndpointInquiry { std::byte{} }) },
  1191. };
  1192. expect (output.messages == responses);
  1193. }
  1194. beginTest ("Sending a V2 discovery message notifies the input listener");
  1195. {
  1196. constexpr std::byte outputPathID { 5 };
  1197. const auto initiatorMUID = MUID::makeRandom (random);
  1198. constexpr std::byte version { 0x02 };
  1199. Output output;
  1200. const auto options = DeviceOptions().withOutputs ({ &output })
  1201. .withFunctionBlock (functionBlock)
  1202. .withDeviceInfo (deviceInfo)
  1203. .withMaxSysExSize (512);
  1204. Device device { options };
  1205. const auto responderMUID = device.getMuid();
  1206. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1207. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1208. version,
  1209. initiatorMUID,
  1210. MUID::getBroadcast() },
  1211. Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } },
  1212. { std::byte { 0x15 }, std::byte { 0x16 } },
  1213. { std::byte { 0x25 }, std::byte { 0x26 } },
  1214. { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } },
  1215. std::byte{},
  1216. 1024,
  1217. outputPathID }) });
  1218. expect (device.getMuid() == responderMUID);
  1219. const std::vector<GroupOutput> responses
  1220. {
  1221. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1222. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1223. detail::MessageMeta::implementationVersion,
  1224. responderMUID,
  1225. initiatorMUID },
  1226. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, outputPathID, std::byte { 0x7f } }) },
  1227. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1228. detail::MessageMeta::Meta<Message::EndpointInquiry>::subID2,
  1229. detail::MessageMeta::implementationVersion,
  1230. responderMUID,
  1231. initiatorMUID },
  1232. Message::EndpointInquiry { std::byte{} }) },
  1233. };
  1234. expect (output.messages == responses);
  1235. }
  1236. beginTest ("Sending a discovery message with a future version notifies the input listener and ignores trailing fields");
  1237. {
  1238. constexpr std::byte outputPathID { 10 };
  1239. const auto initiatorMUID = MUID::makeRandom (random);
  1240. constexpr std::byte version { 0x03 };
  1241. Output output;
  1242. const auto options = DeviceOptions().withOutputs ({ &output })
  1243. .withFunctionBlock (functionBlock)
  1244. .withDeviceInfo (deviceInfo)
  1245. .withMaxSysExSize (512);
  1246. Device device { options };
  1247. const auto responderMUID = device.getMuid();
  1248. auto bytes = getMessageBytes ({ ChannelInGroup::wholeBlock,
  1249. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1250. version,
  1251. initiatorMUID,
  1252. MUID::getBroadcast() },
  1253. Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } },
  1254. { std::byte { 0x15 }, std::byte { 0x16 } },
  1255. { std::byte { 0x25 }, std::byte { 0x26 } },
  1256. { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } },
  1257. std::byte{},
  1258. 1024,
  1259. outputPathID });
  1260. // Future versions might have more trailing bytes
  1261. bytes.insert (bytes.end(), { std::byte{}, std::byte{} });
  1262. device.processMessage ({ 0, bytes });
  1263. expect (device.getMuid() == responderMUID);
  1264. const std::vector<GroupOutput> responses
  1265. {
  1266. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1267. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1268. detail::MessageMeta::implementationVersion,
  1269. responderMUID,
  1270. initiatorMUID },
  1271. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, outputPathID, std::byte { 0x7f } }) },
  1272. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1273. detail::MessageMeta::Meta<Message::EndpointInquiry>::subID2,
  1274. detail::MessageMeta::implementationVersion,
  1275. responderMUID,
  1276. initiatorMUID },
  1277. Message::EndpointInquiry { std::byte{} }) },
  1278. };
  1279. expect (output.messages == responses);
  1280. }
  1281. beginTest ("When receiving an InvalidateMUID that matches the Device MUID, initiate discovery using a new MUID");
  1282. {
  1283. Output output;
  1284. const auto options = DeviceOptions().withOutputs ({ &output })
  1285. .withFunctionBlock (functionBlock)
  1286. .withDeviceInfo (deviceInfo)
  1287. .withMaxSysExSize (512);
  1288. Device device { options };
  1289. const auto deviceMUID = device.getMuid();
  1290. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1291. detail::MessageMeta::Meta<Message::InvalidateMUID>::subID2,
  1292. detail::MessageMeta::implementationVersion,
  1293. MUID::makeRandom (random),
  1294. MUID::getBroadcast() },
  1295. Message::InvalidateMUID { deviceMUID }) });
  1296. expect (device.getMuid() != deviceMUID);
  1297. expect (Parser::parse (MUID::makeRandom (random), output.messages.front().bytes) == Message::Parsed { { ChannelInGroup::wholeBlock,
  1298. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1299. detail::MessageMeta::implementationVersion,
  1300. device.getMuid(),
  1301. MUID::getBroadcast() },
  1302. Message::Discovery { deviceInfo,
  1303. {},
  1304. 512,
  1305. {} } });
  1306. }
  1307. struct Listener : public DeviceListener
  1308. {
  1309. void deviceAdded (MUID x) override { added .push_back (x); }
  1310. void deviceRemoved (MUID x) override { removed.push_back (x); }
  1311. std::vector<MUID> added, removed;
  1312. };
  1313. beginTest ("When receiving a DiscoveryResponse, update the set of known devices, notify outputs, and request endpoint info");
  1314. {
  1315. Listener delegate;
  1316. Output output;
  1317. const auto options = DeviceOptions().withOutputs ({ &output })
  1318. .withFunctionBlock (functionBlock)
  1319. .withDeviceInfo (deviceInfo)
  1320. .withMaxSysExSize (512);
  1321. Device device { options };
  1322. device.addListener (delegate);
  1323. expect (device.getDiscoveredMuids().empty());
  1324. const auto deviceMUID = device.getMuid();
  1325. const auto responderMUID = MUID::makeRandom (random);
  1326. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1327. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1328. detail::MessageMeta::implementationVersion,
  1329. responderMUID,
  1330. deviceMUID },
  1331. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) });
  1332. expect (device.getDiscoveredMuids() == std::vector { responderMUID });
  1333. expect (delegate.added == std::vector { responderMUID });
  1334. std::vector<GroupOutput> responses
  1335. {
  1336. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1337. detail::MessageMeta::Meta<Message::EndpointInquiry>::subID2,
  1338. detail::MessageMeta::implementationVersion,
  1339. deviceMUID,
  1340. responderMUID },
  1341. Message::EndpointInquiry { std::byte{} }) },
  1342. };
  1343. expect (output.messages == responses);
  1344. beginTest ("When receiving a DiscoveryResponse with a MUID that matches a known device, invalidate that MUID");
  1345. {
  1346. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1347. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1348. detail::MessageMeta::implementationVersion,
  1349. responderMUID,
  1350. deviceMUID },
  1351. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) });
  1352. expect (device.getDiscoveredMuids().empty());
  1353. expect (delegate.removed == std::vector { responderMUID });
  1354. responses.push_back ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1355. detail::MessageMeta::Meta<Message::InvalidateMUID>::subID2,
  1356. detail::MessageMeta::implementationVersion,
  1357. deviceMUID,
  1358. MUID::getBroadcast() },
  1359. Message::InvalidateMUID { responderMUID }) });
  1360. expect (output.messages == responses);
  1361. }
  1362. }
  1363. beginTest ("After receiving an EndpointResponse, the listener is notified");
  1364. {
  1365. static constexpr std::byte dataBytes[] { std::byte { 0x01 }, std::byte { 0x7f }, std::byte { 0x41 } };
  1366. struct EndpointListener : public DeviceListener
  1367. {
  1368. EndpointListener (UnitTest& t, Device& d) : test (t), device (d) {}
  1369. void endpointReceived (MUID, Message::EndpointInquiryResponse) override { called = true; }
  1370. UnitTest& test;
  1371. Device& device;
  1372. bool called = false;
  1373. };
  1374. Output output;
  1375. const auto options = DeviceOptions().withOutputs ({ &output })
  1376. .withFunctionBlock (functionBlock)
  1377. .withDeviceInfo (deviceInfo)
  1378. .withMaxSysExSize (512);
  1379. Device device { options };
  1380. EndpointListener delegate { *this, device };
  1381. device.addListener (delegate);
  1382. const auto responderMUID = MUID::makeRandom (random);
  1383. const auto deviceMUID = device.getMuid();
  1384. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1385. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1386. detail::MessageMeta::implementationVersion,
  1387. responderMUID,
  1388. deviceMUID },
  1389. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) });
  1390. expect (! delegate.called);
  1391. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1392. detail::MessageMeta::Meta<Message::EndpointInquiryResponse>::subID2,
  1393. detail::MessageMeta::implementationVersion,
  1394. responderMUID,
  1395. deviceMUID },
  1396. Message::EndpointInquiryResponse { std::byte{}, dataBytes }) });
  1397. expect (delegate.called);
  1398. }
  1399. beginTest ("If a device has not previously acted as a responder, modifying profiles does not emit events");
  1400. {
  1401. Output output;
  1402. const auto options = DeviceOptions().withOutputs ({ &output })
  1403. .withFunctionBlock (functionBlock)
  1404. .withDeviceInfo (deviceInfo)
  1405. .withMaxSysExSize (512)
  1406. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1407. Device device { options };
  1408. expect (device.getProfileHost() != nullptr);
  1409. const Profile profile { std::byte { 0x01 },
  1410. std::byte { 0x02 },
  1411. std::byte { 0x03 },
  1412. std::byte { 0x04 },
  1413. std::byte { 0x05 } };
  1414. device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1415. expect (output.messages.empty());
  1416. beginTest ("The device reports profiles accurately");
  1417. {
  1418. const auto inquiryMUID = MUID::makeRandom (random);
  1419. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1420. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1421. detail::MessageMeta::implementationVersion,
  1422. inquiryMUID,
  1423. device.getMuid() },
  1424. Message::ProfileInquiry{}) });
  1425. const Profile disabledProfiles[] { profile };
  1426. expect (output.messages.size() == 1);
  1427. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1428. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1429. detail::MessageMeta::implementationVersion,
  1430. device.getMuid(),
  1431. inquiryMUID },
  1432. Message::ProfileInquiryResponse { {}, disabledProfiles }));
  1433. }
  1434. beginTest ("If a device has previously acted as a responder to profile inquiry, then modifying profiles emits events");
  1435. {
  1436. device.getProfileHost()->setProfileEnablement ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, 1);
  1437. expect (output.messages.size() == 2);
  1438. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1439. detail::MessageMeta::Meta<Message::ProfileEnabledReport>::subID2,
  1440. detail::MessageMeta::implementationVersion,
  1441. device.getMuid(),
  1442. MUID::getBroadcast() },
  1443. Message::ProfileEnabledReport { profile, 0 }));
  1444. }
  1445. }
  1446. beginTest ("If a device receives a details inquiry message addressed to an unsupported profile, a NAK with a code of 0x04 is emitted");
  1447. {
  1448. Output output;
  1449. const auto options = DeviceOptions().withOutputs ({ &output })
  1450. .withFunctionBlock (functionBlock)
  1451. .withDeviceInfo (deviceInfo)
  1452. .withMaxSysExSize (512)
  1453. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1454. Device device { options };
  1455. expect (device.getProfileHost() != nullptr);
  1456. const auto inquiryMUID = MUID::makeRandom (random);
  1457. const Profile profile { std::byte { 0x01 },
  1458. std::byte { 0x02 },
  1459. std::byte { 0x03 },
  1460. std::byte { 0x04 },
  1461. std::byte { 0x05 } };
  1462. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1463. detail::MessageMeta::Meta<Message::ProfileDetails>::subID2,
  1464. detail::MessageMeta::implementationVersion,
  1465. inquiryMUID,
  1466. device.getMuid() },
  1467. Message::ProfileDetails { profile, std::byte { 0x02 } }) });
  1468. expect (output.messages.size() == 1);
  1469. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1470. detail::MessageMeta::Meta<Message::NAK>::subID2,
  1471. detail::MessageMeta::implementationVersion,
  1472. device.getMuid(),
  1473. inquiryMUID },
  1474. Message::NAK { detail::MessageMeta::Meta<Message::ProfileDetails>::subID2,
  1475. std::byte { 0x04 },
  1476. {},
  1477. {},
  1478. {} }));
  1479. }
  1480. beginTest ("If a device receives a set profile on and enables the profile, profile enabled report is emitted");
  1481. {
  1482. // Note: if there's no explicit profile delegate, the device will toggle profiles as requested.
  1483. Output output;
  1484. const auto options = DeviceOptions().withOutputs ({ &output })
  1485. .withFunctionBlock (functionBlock)
  1486. .withDeviceInfo (deviceInfo)
  1487. .withMaxSysExSize (512)
  1488. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1489. Device device { options };
  1490. expect (device.getProfileHost() != nullptr);
  1491. const Profile profile { std::byte { 0x01 },
  1492. std::byte { 0x02 },
  1493. std::byte { 0x03 },
  1494. std::byte { 0x04 },
  1495. std::byte { 0x05 } };
  1496. device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1497. const auto inquiryMUID = MUID::makeRandom (random);
  1498. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1499. detail::MessageMeta::Meta<Message::ProfileOn>::subID2,
  1500. detail::MessageMeta::implementationVersion,
  1501. inquiryMUID,
  1502. device.getMuid() },
  1503. Message::ProfileOn { profile, 0 }) });
  1504. expect (output.messages.size() == 1);
  1505. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1506. detail::MessageMeta::Meta<Message::ProfileEnabledReport>::subID2,
  1507. detail::MessageMeta::implementationVersion,
  1508. device.getMuid(),
  1509. MUID::getBroadcast() },
  1510. Message::ProfileEnabledReport { profile, 0 }));
  1511. }
  1512. struct DoNothingProfileDelegate : public ProfileDelegate
  1513. {
  1514. void profileEnablementRequested (MUID, ProfileAtAddress, int, bool) override {}
  1515. };
  1516. beginTest ("If a device receives a set profile on but then doesn't enable the profile, profile disabled report is emitted");
  1517. {
  1518. DoNothingProfileDelegate delegate;
  1519. Output output;
  1520. const auto options = DeviceOptions().withOutputs ({ &output })
  1521. .withFunctionBlock (functionBlock)
  1522. .withDeviceInfo (deviceInfo)
  1523. .withMaxSysExSize (512)
  1524. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true))
  1525. .withProfileDelegate (&delegate);
  1526. Device device { options };
  1527. expect (device.getProfileHost() != nullptr);
  1528. const Profile profile { std::byte { 0x01 },
  1529. std::byte { 0x02 },
  1530. std::byte { 0x03 },
  1531. std::byte { 0x04 },
  1532. std::byte { 0x05 } };
  1533. device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1534. const auto inquiryMUID = MUID::makeRandom (random);
  1535. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1536. detail::MessageMeta::Meta<Message::ProfileOn>::subID2,
  1537. detail::MessageMeta::implementationVersion,
  1538. inquiryMUID,
  1539. device.getMuid() },
  1540. Message::ProfileOn { profile, 1 }) });
  1541. expect (output.messages.size() == 1);
  1542. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1543. detail::MessageMeta::Meta<Message::ProfileDisabledReport>::subID2,
  1544. detail::MessageMeta::implementationVersion,
  1545. device.getMuid(),
  1546. MUID::getBroadcast() },
  1547. Message::ProfileDisabledReport { profile, {} }));
  1548. }
  1549. beginTest ("If a device receives a set profile on for an unsupported profile, NAK is emitted");
  1550. {
  1551. Output output;
  1552. const auto options = DeviceOptions().withOutputs ({ &output })
  1553. .withFunctionBlock (functionBlock)
  1554. .withDeviceInfo (deviceInfo)
  1555. .withMaxSysExSize (512)
  1556. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1557. Device device { options };
  1558. expect (device.getProfileHost() != nullptr);
  1559. const Profile profile { std::byte { 0x01 },
  1560. std::byte { 0x02 },
  1561. std::byte { 0x03 },
  1562. std::byte { 0x04 },
  1563. std::byte { 0x05 } };
  1564. const auto inquiryMUID = MUID::makeRandom (random);
  1565. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1566. detail::MessageMeta::Meta<Message::ProfileOn>::subID2,
  1567. detail::MessageMeta::implementationVersion,
  1568. inquiryMUID,
  1569. device.getMuid() },
  1570. Message::ProfileOn { profile, 1 }) });
  1571. expect (output.messages.size() == 1);
  1572. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1573. detail::MessageMeta::Meta<Message::NAK>::subID2,
  1574. detail::MessageMeta::implementationVersion,
  1575. device.getMuid(),
  1576. inquiryMUID },
  1577. Message::NAK { detail::MessageMeta::Meta<Message::ProfileOn>::subID2,
  1578. {},
  1579. {},
  1580. {},
  1581. {} }));
  1582. }
  1583. beginTest ("If a device receives a set profile off and disables the profile, profile disabled report is emitted");
  1584. {
  1585. // Note: if there's no explicit profile delegate, the device will toggle profiles as requested.
  1586. Output output;
  1587. const auto options = DeviceOptions().withOutputs ({ &output })
  1588. .withFunctionBlock (functionBlock)
  1589. .withDeviceInfo (deviceInfo)
  1590. .withMaxSysExSize (512)
  1591. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1592. Device device { options };
  1593. expect (device.getProfileHost() != nullptr);
  1594. const Profile profile { std::byte { 0x01 },
  1595. std::byte { 0x02 },
  1596. std::byte { 0x03 },
  1597. std::byte { 0x04 },
  1598. std::byte { 0x05 } };
  1599. device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1600. device.getProfileHost()->setProfileEnablement ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, 0);
  1601. const auto inquiryMUID = MUID::makeRandom (random);
  1602. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1603. detail::MessageMeta::Meta<Message::ProfileOff>::subID2,
  1604. detail::MessageMeta::implementationVersion,
  1605. inquiryMUID,
  1606. device.getMuid() },
  1607. Message::ProfileOff { profile }) });
  1608. expect (output.messages.size() == 1);
  1609. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1610. detail::MessageMeta::Meta<Message::ProfileDisabledReport>::subID2,
  1611. detail::MessageMeta::implementationVersion,
  1612. device.getMuid(),
  1613. MUID::getBroadcast() },
  1614. Message::ProfileDisabledReport { profile, {} }));
  1615. }
  1616. beginTest ("If a device receives a set profile off but then doesn't disable the profile, profile enabled report is emitted");
  1617. {
  1618. Output output;
  1619. DoNothingProfileDelegate delegate;
  1620. const auto options = DeviceOptions().withOutputs ({ &output })
  1621. .withFunctionBlock (functionBlock)
  1622. .withDeviceInfo (deviceInfo)
  1623. .withMaxSysExSize (512)
  1624. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true))
  1625. .withProfileDelegate (&delegate);
  1626. Device device { options };
  1627. expect (device.getProfileHost() != nullptr);
  1628. const Profile profile { std::byte { 0x01 },
  1629. std::byte { 0x02 },
  1630. std::byte { 0x03 },
  1631. std::byte { 0x04 },
  1632. std::byte { 0x05 } };
  1633. device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1634. device.getProfileHost()->setProfileEnablement ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, 1);
  1635. const auto inquiryMUID = MUID::makeRandom (random);
  1636. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1637. detail::MessageMeta::Meta<Message::ProfileOff>::subID2,
  1638. detail::MessageMeta::implementationVersion,
  1639. inquiryMUID,
  1640. device.getMuid() },
  1641. Message::ProfileOff { profile }) });
  1642. expect (output.messages.size() == 1);
  1643. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1644. detail::MessageMeta::Meta<Message::ProfileEnabledReport>::subID2,
  1645. detail::MessageMeta::implementationVersion,
  1646. device.getMuid(),
  1647. MUID::getBroadcast() },
  1648. Message::ProfileEnabledReport { profile, 0 }));
  1649. }
  1650. beginTest ("If a device receives a set profile off for an unsupported profile, NAK is emitted");
  1651. {
  1652. Output output;
  1653. const auto options = DeviceOptions().withOutputs ({ &output })
  1654. .withFunctionBlock (functionBlock)
  1655. .withDeviceInfo (deviceInfo)
  1656. .withMaxSysExSize (512)
  1657. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1658. Device device { options };
  1659. expect (device.getProfileHost() != nullptr);
  1660. const Profile profile { std::byte { 0x01 },
  1661. std::byte { 0x02 },
  1662. std::byte { 0x03 },
  1663. std::byte { 0x04 },
  1664. std::byte { 0x05 } };
  1665. const auto inquiryMUID = MUID::makeRandom (random);
  1666. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1667. detail::MessageMeta::Meta<Message::ProfileOff>::subID2,
  1668. detail::MessageMeta::implementationVersion,
  1669. inquiryMUID,
  1670. device.getMuid() },
  1671. Message::ProfileOff { profile }) });
  1672. expect (output.messages.size() == 1);
  1673. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1674. detail::MessageMeta::Meta<Message::NAK>::subID2,
  1675. detail::MessageMeta::implementationVersion,
  1676. device.getMuid(),
  1677. inquiryMUID },
  1678. Message::NAK { detail::MessageMeta::Meta<Message::ProfileOff>::subID2,
  1679. {},
  1680. {},
  1681. {},
  1682. {} }));
  1683. }
  1684. const FunctionBlock realBlock { std::byte{}, 0, 3 };
  1685. beginTest ("If a device receives a profile inquiry addressed to a channel, that channel's profiles are emitted");
  1686. {
  1687. Output output;
  1688. const auto options = DeviceOptions().withOutputs ({ &output })
  1689. .withFunctionBlock (realBlock)
  1690. .withDeviceInfo (deviceInfo)
  1691. .withMaxSysExSize (512)
  1692. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1693. Device device { options };
  1694. auto& profileHost = *device.getProfileHost();
  1695. Profile channel0Profile { std::byte { 0x01 } };
  1696. Profile channel1Profile { std::byte { 0x02 } };
  1697. profileHost.addProfile ({ channel0Profile, ChannelAddress{}.withChannel (ChannelInGroup::channel0) });
  1698. profileHost.addProfile ({ channel1Profile, ChannelAddress{}.withChannel (ChannelInGroup::channel1) });
  1699. const auto inquiryMUID = MUID::makeRandom (random);
  1700. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::channel0,
  1701. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1702. detail::MessageMeta::implementationVersion,
  1703. inquiryMUID,
  1704. device.getMuid() },
  1705. Message::ProfileInquiry{}) });
  1706. const Profile channel0Profiles[] { channel0Profile };
  1707. const Profile channel1Profiles[] { channel1Profile };
  1708. expect (output.messages.size() == 1);
  1709. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::channel0,
  1710. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1711. detail::MessageMeta::implementationVersion,
  1712. device.getMuid(),
  1713. inquiryMUID },
  1714. Message::ProfileInquiryResponse { {}, channel0Profiles }));
  1715. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::channel2,
  1716. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1717. detail::MessageMeta::implementationVersion,
  1718. inquiryMUID,
  1719. device.getMuid() },
  1720. Message::ProfileInquiry{}) });
  1721. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::channel2,
  1722. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1723. detail::MessageMeta::implementationVersion,
  1724. device.getMuid(),
  1725. inquiryMUID },
  1726. Message::ProfileInquiryResponse { {}, {} }));
  1727. Profile group0Profile { std::byte { 0x05 } };
  1728. Profile group1Profile { std::byte { 0x06 } };
  1729. const Profile group0Profiles[] { group0Profile };
  1730. const Profile group1Profiles[] { group1Profile };
  1731. beginTest ("If a device receives a profile inquiry addressed to a group, that group's profiles are emitted");
  1732. {
  1733. profileHost.addProfile ({ group0Profile, ChannelAddress{}.withGroup (0).withChannel (ChannelInGroup::wholeGroup) });
  1734. profileHost.addProfile ({ group1Profile, ChannelAddress{}.withGroup (1).withChannel (ChannelInGroup::wholeGroup) });
  1735. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeGroup,
  1736. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1737. detail::MessageMeta::implementationVersion,
  1738. inquiryMUID,
  1739. device.getMuid() },
  1740. Message::ProfileInquiry{}) });
  1741. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeGroup,
  1742. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1743. detail::MessageMeta::implementationVersion,
  1744. device.getMuid(),
  1745. inquiryMUID },
  1746. Message::ProfileInquiryResponse { {}, group0Profiles }));
  1747. device.processMessage ({ 2, getMessageBytes ({ ChannelInGroup::wholeGroup,
  1748. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1749. detail::MessageMeta::implementationVersion,
  1750. inquiryMUID,
  1751. device.getMuid() },
  1752. Message::ProfileInquiry{}) });
  1753. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeGroup,
  1754. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1755. detail::MessageMeta::implementationVersion,
  1756. device.getMuid(),
  1757. inquiryMUID },
  1758. Message::ProfileInquiryResponse { {}, {} }));
  1759. }
  1760. beginTest ("If a device receives a profile inquiry addressed to a block, the profiles for member channels, then member groups, then the block are emitted");
  1761. {
  1762. Profile blockProfile { std::byte { 0x0a } };
  1763. profileHost.addProfile ({ blockProfile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1764. output.messages.clear();
  1765. device.processMessage ({ 1, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1766. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1767. detail::MessageMeta::implementationVersion,
  1768. inquiryMUID,
  1769. device.getMuid() },
  1770. Message::ProfileInquiry{}) });
  1771. const Profile blockProfiles[] { blockProfile };
  1772. expect (output.messages == std::vector<GroupOutput> { { 0, getMessageBytes ({ ChannelInGroup::channel0,
  1773. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1774. detail::MessageMeta::implementationVersion,
  1775. device.getMuid(),
  1776. inquiryMUID },
  1777. Message::ProfileInquiryResponse { {}, channel0Profiles }) },
  1778. { 0, getMessageBytes ({ ChannelInGroup::channel1,
  1779. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1780. detail::MessageMeta::implementationVersion,
  1781. device.getMuid(),
  1782. inquiryMUID },
  1783. Message::ProfileInquiryResponse { {}, channel1Profiles }) },
  1784. { 0, getMessageBytes ({ ChannelInGroup::wholeGroup,
  1785. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1786. detail::MessageMeta::implementationVersion,
  1787. device.getMuid(),
  1788. inquiryMUID },
  1789. Message::ProfileInquiryResponse { {}, group0Profiles }) },
  1790. { 1, getMessageBytes ({ ChannelInGroup::wholeGroup,
  1791. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1792. detail::MessageMeta::implementationVersion,
  1793. device.getMuid(),
  1794. inquiryMUID },
  1795. Message::ProfileInquiryResponse { {}, group1Profiles }) },
  1796. { 1, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1797. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1798. detail::MessageMeta::implementationVersion,
  1799. device.getMuid(),
  1800. inquiryMUID },
  1801. Message::ProfileInquiryResponse { {}, blockProfiles }) } });
  1802. }
  1803. }
  1804. // Property exchange
  1805. {
  1806. const auto inquiryMUID = MUID::makeRandom (random);
  1807. struct Delegate : public PropertyDelegate
  1808. {
  1809. uint8_t getNumSimultaneousRequestsSupported() const override { return 1; }
  1810. PropertyReplyData propertyGetDataRequested (MUID, const PropertyRequestHeader&) override { return {}; }
  1811. PropertyReplyHeader propertySetDataRequested (MUID, const PropertyRequestData&) override { return {}; }
  1812. bool subscriptionStartRequested (MUID, const PropertySubscriptionHeader&) override { return true; }
  1813. void subscriptionDidStart (MUID, const String&, const PropertySubscriptionHeader&) override {}
  1814. void subscriptionWillEnd (MUID, const Subscription&) override {}
  1815. };
  1816. Delegate delegate;
  1817. Output output;
  1818. const auto options = DeviceOptions().withOutputs ({ &output })
  1819. .withFunctionBlock (realBlock)
  1820. .withDeviceInfo (deviceInfo)
  1821. .withMaxSysExSize (512)
  1822. .withFeatures (DeviceFeatures{}.withPropertyExchangeSupported (true))
  1823. .withPropertyDelegate (&delegate);
  1824. Device device { options };
  1825. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1826. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1827. detail::MessageMeta::implementationVersion,
  1828. inquiryMUID,
  1829. MUID::getBroadcast() },
  1830. Message::Discovery { {}, DeviceFeatures{}.withPropertyExchangeSupported (true).getSupportedCapabilities(), 512, {} }) });
  1831. expect (output.messages.size() == 2);
  1832. output.messages.clear();
  1833. beginTest ("If a device receives too many concurrent property exchange requests, it responds with a retry status code.");
  1834. {
  1835. auto obj = std::make_unique<DynamicObject>();
  1836. obj->setProperty ("resource", "X-CustomProp");
  1837. const auto header = Encodings::jsonTo7BitText (obj.release());
  1838. for (const auto& requestID : { std::byte { 0 }, std::byte { 1 } })
  1839. {
  1840. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1841. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  1842. detail::MessageMeta::implementationVersion,
  1843. inquiryMUID,
  1844. device.getMuid() },
  1845. Message::PropertySetData { { requestID, header, 0, 1, {} } }) });
  1846. }
  1847. expect (output.messages.size() == 1);
  1848. const auto parsed = Parser::parse (output.messages.back().bytes);
  1849. expect (parsed.has_value());
  1850. expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock,
  1851. detail::MessageMeta::Meta<Message::PropertySetDataResponse>::subID2,
  1852. detail::MessageMeta::implementationVersion,
  1853. device.getMuid(),
  1854. inquiryMUID });
  1855. auto* body = std::get_if<Message::PropertySetDataResponse> (&parsed->body);
  1856. expect (body != nullptr);
  1857. expect (body->requestID == std::byte { 1 });
  1858. auto replyHeader = Encodings::jsonFrom7BitText (body->header);
  1859. expect (replyHeader.getProperty ("status", "") == var (343));
  1860. }
  1861. // Terminate ongoing message
  1862. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1863. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  1864. detail::MessageMeta::implementationVersion,
  1865. inquiryMUID,
  1866. device.getMuid() },
  1867. Message::PropertySetData { { {}, {}, 0, 0, {} } }) });
  1868. output.messages.clear();
  1869. beginTest ("If a device receives an unexpectedly-terminated request, it responds with an error status code.");
  1870. {
  1871. auto obj = std::make_unique<DynamicObject>();
  1872. obj->setProperty ("resource", "X-CustomProp");
  1873. const auto header = Encodings::jsonTo7BitText (obj.release());
  1874. const std::byte requestID { 3 };
  1875. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1876. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  1877. detail::MessageMeta::implementationVersion,
  1878. inquiryMUID,
  1879. device.getMuid() },
  1880. Message::PropertySetData { { requestID, header, 2, 1, {} } }) });
  1881. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1882. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  1883. detail::MessageMeta::implementationVersion,
  1884. inquiryMUID,
  1885. device.getMuid() },
  1886. Message::PropertySetData { { requestID, header, 2, 0, {} } }) });
  1887. expect (output.messages.size() == 1);
  1888. const auto parsed = Parser::parse (output.messages.back().bytes);
  1889. expect (parsed.has_value());
  1890. expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock,
  1891. detail::MessageMeta::Meta<Message::PropertySetDataResponse>::subID2,
  1892. detail::MessageMeta::implementationVersion,
  1893. device.getMuid(),
  1894. inquiryMUID });
  1895. auto* body = std::get_if<Message::PropertySetDataResponse> (&parsed->body);
  1896. expect (body != nullptr);
  1897. expect (body->requestID == requestID);
  1898. auto replyHeader = Encodings::jsonFrom7BitText (body->header);
  1899. expect (replyHeader.getProperty ("status", "") == var (400));
  1900. }
  1901. output.messages.clear();
  1902. beginTest ("If a request is terminated via notify, the device responds with an error status code.");
  1903. {
  1904. auto obj = std::make_unique<DynamicObject>();
  1905. obj->setProperty ("resource", "X-CustomProp");
  1906. const auto header = Encodings::jsonTo7BitText (obj.release());
  1907. const std::byte requestID { 100 };
  1908. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1909. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  1910. detail::MessageMeta::implementationVersion,
  1911. inquiryMUID,
  1912. device.getMuid() },
  1913. Message::PropertySetData { { requestID, header, 2, 1, {} } }) });
  1914. auto notifyHeader = std::make_unique<DynamicObject>();
  1915. notifyHeader->setProperty ("status", 144);
  1916. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1917. detail::MessageMeta::Meta<Message::PropertyNotify>::subID2,
  1918. detail::MessageMeta::implementationVersion,
  1919. inquiryMUID,
  1920. device.getMuid() },
  1921. Message::PropertyNotify { { requestID, Encodings::jsonTo7BitText (notifyHeader.release()), 1, 1, {} } }) });
  1922. expect (output.messages.size() == 1);
  1923. const auto parsed = Parser::parse (output.messages.back().bytes);
  1924. expect (parsed.has_value());
  1925. expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock,
  1926. detail::MessageMeta::Meta<Message::PropertySetDataResponse>::subID2,
  1927. detail::MessageMeta::implementationVersion,
  1928. device.getMuid(),
  1929. inquiryMUID });
  1930. auto* body = std::get_if<Message::PropertySetDataResponse> (&parsed->body);
  1931. expect (body != nullptr);
  1932. expect (body->requestID == requestID);
  1933. auto replyHeader = Encodings::jsonFrom7BitText (body->header);
  1934. expect (replyHeader.getProperty ("status", "") == var (400));
  1935. }
  1936. }
  1937. }
  1938. private:
  1939. template <typename Msg>
  1940. static std::vector<std::byte> getMessageBytes (const Message::Header& header, const Msg& body)
  1941. {
  1942. std::vector<std::byte> bytes;
  1943. detail::Marshalling::Writer { bytes } (header, body);
  1944. return bytes;
  1945. }
  1946. };
  1947. static DeviceTests deviceTests;
  1948. #endif
  1949. } // namespace juce::midi_ci