| @@ -153,6 +153,7 @@ struct BlockSerialNumber | |||
| bool hasPrefix (const char* prefix) const noexcept { return memcmp (serial, prefix, 3) == 0; } | |||
| }; | |||
| //============================================================================== | |||
| /** Structure for the version number | |||
| @tags{Blocks} | |||
| @@ -161,8 +162,15 @@ struct VersionNumber | |||
| { | |||
| uint8 version[21] = {}; | |||
| uint8 length = 0; | |||
| juce::String asString() const | |||
| { | |||
| return juce::String (reinterpret_cast<const char*> (version), | |||
| std::min (sizeof (version), static_cast<size_t> (length))); | |||
| } | |||
| }; | |||
| //============================================================================== | |||
| /** Structure for the block name | |||
| @tags{Blocks} | |||
| @@ -171,8 +179,17 @@ struct BlockName | |||
| { | |||
| uint8 name[33] = {}; | |||
| uint8 length = 0; | |||
| bool isValid() const { return length > 0; } | |||
| juce::String asString() const | |||
| { | |||
| return juce::String (reinterpret_cast<const char*> (name), | |||
| std::min (sizeof (name), static_cast<size_t> (length))); | |||
| } | |||
| }; | |||
| //============================================================================== | |||
| /** Structure for the device status | |||
| @tags{Blocks} | |||
| @@ -185,6 +202,7 @@ struct DeviceStatus | |||
| BatteryCharging batteryCharging; | |||
| }; | |||
| //============================================================================== | |||
| /** Structure for the device connection | |||
| @tags{Blocks} | |||
| @@ -195,6 +213,7 @@ struct DeviceConnection | |||
| ConnectorPort port1, port2; | |||
| }; | |||
| //============================================================================== | |||
| /** Structure for the device version | |||
| @tags{Blocks} | |||
| @@ -205,6 +224,7 @@ struct DeviceVersion | |||
| VersionNumber version; | |||
| }; | |||
| //============================================================================== | |||
| /** Structure used for the device name | |||
| @tags{Blocks} | |||
| @@ -0,0 +1,101 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| The code included in this file is provided under the terms of the ISC license | |||
| http://www.isc.org/downloads/software-support-policy/isc-license. Permission | |||
| To use, copy, modify, and/or distribute this software for any purpose with or | |||
| without fee is hereby granted provided that the above copyright notice and | |||
| this permission notice appear in all copies. | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| namespace juce | |||
| { | |||
| namespace | |||
| { | |||
| struct PortIOStats | |||
| { | |||
| PortIOStats (const char* nm) : name (nm) {} | |||
| const char* const name; | |||
| int byteCount = 0; | |||
| int messageCount = 0; | |||
| int bytesPerSec = 0; | |||
| int largestMessageBytes = 0; | |||
| int lastMessageBytes = 0; | |||
| void update (double elapsedSec) | |||
| { | |||
| if (byteCount > 0) | |||
| { | |||
| bytesPerSec = (int) (byteCount / elapsedSec); | |||
| byteCount = 0; | |||
| juce::Logger::writeToLog (getString()); | |||
| } | |||
| } | |||
| juce::String getString() const | |||
| { | |||
| return juce::String (name) + ": " | |||
| + "count=" + juce::String (messageCount).paddedRight (' ', 7) | |||
| + "rate=" + (juce::String (bytesPerSec / 1024.0f, 1) + " Kb/sec").paddedRight (' ', 11) | |||
| + "largest=" + (juce::String (largestMessageBytes) + " bytes").paddedRight (' ', 11) | |||
| + "last=" + (juce::String (lastMessageBytes) + " bytes").paddedRight (' ', 11); | |||
| } | |||
| void registerMessage (int numBytes) noexcept | |||
| { | |||
| byteCount += numBytes; | |||
| ++messageCount; | |||
| lastMessageBytes = numBytes; | |||
| largestMessageBytes = juce::jmax (largestMessageBytes, numBytes); | |||
| } | |||
| }; | |||
| static PortIOStats inputStats { "Input" }, outputStats { "Output" }; | |||
| static uint32 startTime = 0; | |||
| static inline void resetOnSecondBoundary() | |||
| { | |||
| auto now = juce::Time::getMillisecondCounter(); | |||
| double elapsedSec = (now - startTime) / 1000.0; | |||
| if (elapsedSec >= 1.0) | |||
| { | |||
| inputStats.update (elapsedSec); | |||
| outputStats.update (elapsedSec); | |||
| startTime = now; | |||
| } | |||
| } | |||
| static inline void registerBytesOut (int numBytes) | |||
| { | |||
| outputStats.registerMessage (numBytes); | |||
| resetOnSecondBoundary(); | |||
| } | |||
| static inline void registerBytesIn (int numBytes) | |||
| { | |||
| inputStats.registerMessage (numBytes); | |||
| resetOnSecondBoundary(); | |||
| } | |||
| } | |||
| juce::String getMidiIOStats() | |||
| { | |||
| return inputStats.getString() + " " + outputStats.getString(); | |||
| } | |||
| } // namespace juce | |||
| @@ -0,0 +1,561 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| The code included in this file is provided under the terms of the ISC license | |||
| http://www.isc.org/downloads/software-support-policy/isc-license. Permission | |||
| To use, copy, modify, and/or distribute this software for any purpose with or | |||
| without fee is hereby granted provided that the above copyright notice and | |||
| this permission notice appear in all copies. | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| namespace juce | |||
| { | |||
| namespace | |||
| { | |||
| static Block::Timestamp deviceTimestampToHost (uint32 timestamp) noexcept | |||
| { | |||
| return static_cast<Block::Timestamp> (timestamp); | |||
| } | |||
| } | |||
| template <typename Detector> | |||
| struct ConnectedDeviceGroup : private juce::AsyncUpdater, | |||
| private juce::Timer | |||
| { | |||
| //============================================================================== | |||
| ConnectedDeviceGroup (Detector& d, const juce::String& name, PhysicalTopologySource::DeviceConnection* connection) | |||
| : detector (d), deviceName (name), deviceConnection (connection) | |||
| { | |||
| deviceConnection->handleMessageFromDevice = [this] (const void* data, size_t dataSize) | |||
| { | |||
| this->handleIncomingMessage (data, dataSize); | |||
| }; | |||
| startTimer (200); | |||
| sendTopologyRequest(); | |||
| } | |||
| bool isStillConnected (const juce::StringArray& detectedDevices) const noexcept | |||
| { | |||
| return detectedDevices.contains (deviceName) | |||
| && ! failedToGetTopology(); | |||
| } | |||
| int getIndexFromDeviceID (Block::UID uid) const noexcept | |||
| { | |||
| for (auto& d : currentDeviceInfo) | |||
| if (d.uid == uid) | |||
| return d.index; | |||
| return -1; | |||
| } | |||
| const DeviceInfo* getDeviceInfoFromUID (Block::UID uid) const noexcept | |||
| { | |||
| for (auto& d : currentDeviceInfo) | |||
| if (d.uid == uid) | |||
| return &d; | |||
| return nullptr; | |||
| } | |||
| const BlocksProtocol::DeviceStatus* getLastStatus (Block::UID deviceID) const noexcept | |||
| { | |||
| for (auto&& status : currentTopologyDevices) | |||
| if (getBlockUIDFromSerialNumber (status.serialNumber) == deviceID) | |||
| return &status; | |||
| return nullptr; | |||
| } | |||
| void notifyBlockIsRestarting (Block::UID deviceID) | |||
| { | |||
| forceApiDisconnected (deviceID); | |||
| } | |||
| //============================================================================== | |||
| // The following methods will be called by the HostPacketDecoder: | |||
| void beginTopology (int numDevices, int numConnections) | |||
| { | |||
| incomingTopologyDevices.clearQuick(); | |||
| incomingTopologyDevices.ensureStorageAllocated (numDevices); | |||
| incomingTopologyConnections.clearQuick(); | |||
| incomingTopologyConnections.ensureStorageAllocated (numConnections); | |||
| } | |||
| void extendTopology (int numDevices, int numConnections) | |||
| { | |||
| incomingTopologyDevices.ensureStorageAllocated (incomingTopologyDevices.size() + numDevices); | |||
| incomingTopologyConnections.ensureStorageAllocated (incomingTopologyConnections.size() + numConnections); | |||
| } | |||
| void handleTopologyDevice (BlocksProtocol::DeviceStatus status) | |||
| { | |||
| incomingTopologyDevices.add (status); | |||
| } | |||
| void handleTopologyConnection (BlocksProtocol::DeviceConnection connection) | |||
| { | |||
| incomingTopologyConnections.add (connection); | |||
| } | |||
| void endTopology() | |||
| { | |||
| currentDeviceInfo = getArrayOfDeviceInfo (incomingTopologyDevices); | |||
| currentDeviceConnections = getArrayOfConnections (incomingTopologyConnections); | |||
| currentTopologyDevices = incomingTopologyDevices; | |||
| lastTopologyReceiveTime = juce::Time::getCurrentTime(); | |||
| const int numRemoved = blockPings.removeIf ([this] (auto& ping) | |||
| { | |||
| for (auto& info : currentDeviceInfo) | |||
| if (info.uid == ping.blockUID) | |||
| return false; | |||
| LOG_CONNECTIVITY ("API Disconnected by topology update " << ping.blockUID); | |||
| return true; | |||
| }); | |||
| if (numRemoved > 0) | |||
| detector.handleTopologyChange(); | |||
| } | |||
| void handleVersion (BlocksProtocol::DeviceVersion version) | |||
| { | |||
| for (auto& d : currentDeviceInfo) | |||
| if (d.index == version.index && version.version.length > 1) | |||
| d.version = version.version; | |||
| } | |||
| void handleName (BlocksProtocol::DeviceName name) | |||
| { | |||
| for (auto& d : currentDeviceInfo) | |||
| if (d.index == name.index && name.name.length > 1) | |||
| d.name = name.name; | |||
| } | |||
| void handleControlButtonUpDown (BlocksProtocol::TopologyIndex deviceIndex, uint32 timestamp, | |||
| BlocksProtocol::ControlButtonID buttonID, bool isDown) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| detector.handleButtonChange (deviceID, deviceTimestampToHost (timestamp), buttonID.get(), isDown); | |||
| } | |||
| void handleCustomMessage (BlocksProtocol::TopologyIndex deviceIndex, uint32 timestamp, const int32* data) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| detector.handleCustomMessage (deviceID, deviceTimestampToHost (timestamp), data); | |||
| } | |||
| void handleTouchChange (BlocksProtocol::TopologyIndex deviceIndex, | |||
| uint32 timestamp, | |||
| BlocksProtocol::TouchIndex touchIndex, | |||
| BlocksProtocol::TouchPosition position, | |||
| BlocksProtocol::TouchVelocity velocity, | |||
| bool isStart, bool isEnd) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| { | |||
| TouchSurface::Touch touch; | |||
| touch.index = (int) touchIndex.get(); | |||
| touch.x = position.x.toUnipolarFloat(); | |||
| touch.y = position.y.toUnipolarFloat(); | |||
| touch.z = position.z.toUnipolarFloat(); | |||
| touch.xVelocity = velocity.vx.toBipolarFloat(); | |||
| touch.yVelocity = velocity.vy.toBipolarFloat(); | |||
| touch.zVelocity = velocity.vz.toBipolarFloat(); | |||
| touch.eventTimestamp = deviceTimestampToHost (timestamp); | |||
| touch.isTouchStart = isStart; | |||
| touch.isTouchEnd = isEnd; | |||
| touch.blockUID = deviceID; | |||
| setTouchStartPosition (touch); | |||
| detector.handleTouchChange (deviceID, touch); | |||
| } | |||
| } | |||
| void setTouchStartPosition (TouchSurface::Touch& touch) | |||
| { | |||
| auto& startPos = touchStartPositions.getValue (touch); | |||
| if (touch.isTouchStart) | |||
| startPos = { touch.x, touch.y }; | |||
| touch.startX = startPos.x; | |||
| touch.startY = startPos.y; | |||
| } | |||
| void handlePacketACK (BlocksProtocol::TopologyIndex deviceIndex, | |||
| BlocksProtocol::PacketCounter counter) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| { | |||
| detector.handleSharedDataACK (deviceID, counter); | |||
| updateApiPing (deviceID); | |||
| } | |||
| } | |||
| void handleFirmwareUpdateACK (BlocksProtocol::TopologyIndex deviceIndex, | |||
| BlocksProtocol::FirmwareUpdateACKCode resultCode, | |||
| BlocksProtocol::FirmwareUpdateACKDetail resultDetail) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| { | |||
| detector.handleFirmwareUpdateACK (deviceID, (uint8) resultCode.get(), (uint32) resultDetail.get()); | |||
| updateApiPing (deviceID); | |||
| } | |||
| } | |||
| void handleConfigUpdateMessage (BlocksProtocol::TopologyIndex deviceIndex, | |||
| int32 item, int32 value, int32 min, int32 max) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| detector.handleConfigUpdateMessage (deviceID, item, value, min, max); | |||
| } | |||
| void handleConfigSetMessage (BlocksProtocol::TopologyIndex deviceIndex, | |||
| int32 item, int32 value) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| detector.handleConfigSetMessage (deviceID, item, value); | |||
| } | |||
| void handleConfigFactorySyncEndMessage (BlocksProtocol::TopologyIndex deviceIndex) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| detector.handleConfigFactorySyncEndMessage (deviceID); | |||
| } | |||
| void handleConfigFactorySyncResetMessage (BlocksProtocol::TopologyIndex deviceIndex) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| detector.handleConfigFactorySyncResetMessage (deviceID); | |||
| } | |||
| void handleLogMessage (BlocksProtocol::TopologyIndex deviceIndex, const String& message) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| detector.handleLogMessage (deviceID, message); | |||
| } | |||
| //============================================================================== | |||
| template <typename PacketBuilder> | |||
| bool sendMessageToDevice (const PacketBuilder& builder) const | |||
| { | |||
| if (deviceConnection->sendMessageToDevice (builder.getData(), (size_t) builder.size())) | |||
| { | |||
| #if DUMP_BANDWIDTH_STATS | |||
| registerBytesOut (builder.size()); | |||
| #endif | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| PhysicalTopologySource::DeviceConnection* getDeviceConnection() | |||
| { | |||
| return deviceConnection.get(); | |||
| } | |||
| juce::Array<DeviceInfo> getCurrentDeviceInfo() | |||
| { | |||
| auto blocks = currentDeviceInfo; | |||
| blocks.removeIf ([this] (DeviceInfo& info) { return ! isApiConnected (info.uid); }); | |||
| return blocks; | |||
| } | |||
| juce::Array<BlockDeviceConnection> getCurrentDeviceConnections() | |||
| { | |||
| auto connections = currentDeviceConnections; | |||
| connections.removeIf ([this] (BlockDeviceConnection& c) { return ! isApiConnected (c.device1) || ! isApiConnected (c.device2); }); | |||
| return connections; | |||
| } | |||
| Detector& detector; | |||
| juce::String deviceName; | |||
| static constexpr double pingTimeoutSeconds = 6.0; | |||
| private: | |||
| //============================================================================== | |||
| juce::Array<DeviceInfo> currentDeviceInfo; | |||
| juce::Array<BlockDeviceConnection> currentDeviceConnections; | |||
| std::unique_ptr<PhysicalTopologySource::DeviceConnection> deviceConnection; | |||
| juce::Array<BlocksProtocol::DeviceStatus> incomingTopologyDevices, currentTopologyDevices; | |||
| juce::Array<BlocksProtocol::DeviceConnection> incomingTopologyConnections; | |||
| juce::CriticalSection incomingPacketLock; | |||
| juce::Array<juce::MemoryBlock> incomingPackets; | |||
| struct TouchStart { float x, y; }; | |||
| TouchList<TouchStart> touchStartPositions; | |||
| //============================================================================== | |||
| juce::Time lastTopologyRequestTime, lastTopologyReceiveTime; | |||
| int numTopologyRequestsSent = 0; | |||
| void scheduleNewTopologyRequest() | |||
| { | |||
| numTopologyRequestsSent = 0; | |||
| lastTopologyReceiveTime = juce::Time(); | |||
| lastTopologyRequestTime = juce::Time::getCurrentTime(); | |||
| } | |||
| void sendTopologyRequest() | |||
| { | |||
| ++numTopologyRequestsSent; | |||
| lastTopologyRequestTime = juce::Time::getCurrentTime(); | |||
| sendCommandMessage (0, BlocksProtocol::requestTopologyMessage); | |||
| } | |||
| void timerCallback() override | |||
| { | |||
| const auto now = juce::Time::getCurrentTime(); | |||
| if ((now > lastTopologyReceiveTime + juce::RelativeTime::seconds (30.0)) | |||
| && now > lastTopologyRequestTime + juce::RelativeTime::seconds (1.0) | |||
| && numTopologyRequestsSent < 4) | |||
| sendTopologyRequest(); | |||
| checkApiTimeouts (now); | |||
| startApiModeOnConnectedBlocks(); | |||
| } | |||
| bool failedToGetTopology() const noexcept | |||
| { | |||
| return numTopologyRequestsSent > 4 && lastTopologyReceiveTime == juce::Time(); | |||
| } | |||
| bool sendCommandMessage (BlocksProtocol::TopologyIndex deviceIndex, uint32 commandID) const | |||
| { | |||
| BlocksProtocol::HostPacketBuilder<64> p; | |||
| p.writePacketSysexHeaderBytes (deviceIndex); | |||
| p.deviceControlMessage (commandID); | |||
| p.writePacketSysexFooter(); | |||
| return sendMessageToDevice (p); | |||
| } | |||
| //============================================================================== | |||
| struct BlockPingTime | |||
| { | |||
| Block::UID blockUID; | |||
| juce::Time lastPing; | |||
| }; | |||
| juce::Array<BlockPingTime> blockPings; | |||
| void updateApiPing (Block::UID uid) | |||
| { | |||
| const auto now = juce::Time::getCurrentTime(); | |||
| if (auto* ping = getPing (uid)) | |||
| { | |||
| LOG_PING ("Ping: " << uid << " " << now.formatted ("%Mm %Ss")); | |||
| ping->lastPing = now; | |||
| } | |||
| else | |||
| { | |||
| LOG_CONNECTIVITY ("API Connected " << uid); | |||
| blockPings.add ({ uid, now }); | |||
| detector.handleTopologyChange(); | |||
| } | |||
| } | |||
| BlockPingTime* getPing (Block::UID uid) | |||
| { | |||
| for (auto& ping : blockPings) | |||
| if (uid == ping.blockUID) | |||
| return &ping; | |||
| return nullptr; | |||
| } | |||
| void removeDeviceInfo (Block::UID uid) | |||
| { | |||
| currentDeviceInfo.removeIf ([uid] (DeviceInfo& info) { return uid == info.uid; }); | |||
| } | |||
| bool isApiConnected (Block::UID uid) | |||
| { | |||
| return getPing (uid) != nullptr; | |||
| } | |||
| void forceApiDisconnected (Block::UID uid) | |||
| { | |||
| if (isApiConnected (uid)) | |||
| { | |||
| // Clear all known API connections and broadcast an empty topology, | |||
| // as DNA blocks connected to the restarting block may be offline. | |||
| LOG_CONNECTIVITY ("API Disconnected " << uid << ", re-probing topology"); | |||
| currentDeviceInfo.clearQuick(); | |||
| blockPings.clearQuick(); | |||
| detector.handleTopologyChange(); | |||
| scheduleNewTopologyRequest(); | |||
| } | |||
| } | |||
| void checkApiTimeouts (juce::Time now) | |||
| { | |||
| const auto timedOut = [this, now] (BlockPingTime& ping) | |||
| { | |||
| if (ping.lastPing >= now - juce::RelativeTime::seconds (pingTimeoutSeconds)) | |||
| return false; | |||
| LOG_CONNECTIVITY ("Ping timeout: " << ping.blockUID); | |||
| removeDeviceInfo (ping.blockUID); | |||
| return true; | |||
| }; | |||
| if (blockPings.removeIf (timedOut) > 0) | |||
| { | |||
| scheduleNewTopologyRequest(); | |||
| detector.handleTopologyChange(); | |||
| } | |||
| } | |||
| void startApiModeOnConnectedBlocks() | |||
| { | |||
| for (auto& info : currentDeviceInfo) | |||
| { | |||
| if (! isApiConnected (info.uid)) | |||
| { | |||
| LOG_CONNECTIVITY ("API Try " << info.uid); | |||
| sendCommandMessage (info.index, BlocksProtocol::endAPIMode); | |||
| sendCommandMessage (info.index, BlocksProtocol::beginAPIMode); | |||
| } | |||
| } | |||
| } | |||
| //============================================================================== | |||
| Block::UID getDeviceIDFromIndex (BlocksProtocol::TopologyIndex index) const noexcept | |||
| { | |||
| for (auto& d : currentDeviceInfo) | |||
| if (d.index == index) | |||
| return d.uid; | |||
| return {}; | |||
| } | |||
| Block::UID getDeviceIDFromMessageIndex (BlocksProtocol::TopologyIndex index) noexcept | |||
| { | |||
| const auto uid = getDeviceIDFromIndex (index); | |||
| // re-request topology if we get an event from an unknown block | |||
| if (uid == Block::UID()) | |||
| scheduleNewTopologyRequest(); | |||
| return uid; | |||
| } | |||
| juce::Array<BlockDeviceConnection> getArrayOfConnections (const juce::Array<BlocksProtocol::DeviceConnection>& connections) | |||
| { | |||
| juce::Array<BlockDeviceConnection> result; | |||
| for (auto&& c : connections) | |||
| { | |||
| BlockDeviceConnection dc; | |||
| dc.device1 = getDeviceIDFromIndex (c.device1); | |||
| dc.device2 = getDeviceIDFromIndex (c.device2); | |||
| if (dc.device1 <= 0 || dc.device2 <= 0) | |||
| continue; | |||
| dc.connectionPortOnDevice1 = convertConnectionPort (dc.device1, c.port1); | |||
| dc.connectionPortOnDevice2 = convertConnectionPort (dc.device2, c.port2); | |||
| result.add (dc); | |||
| } | |||
| return result; | |||
| } | |||
| Block::ConnectionPort convertConnectionPort (Block::UID uid, BlocksProtocol::ConnectorPort p) noexcept | |||
| { | |||
| if (auto* info = getDeviceInfoFromUID (uid)) | |||
| return BlocksProtocol::BlockDataSheet (info->serial).convertPortIndexToConnectorPort (p); | |||
| jassertfalse; | |||
| return { Block::ConnectionPort::DeviceEdge::north, 0 }; | |||
| } | |||
| //============================================================================== | |||
| void handleIncomingMessage (const void* data, size_t dataSize) | |||
| { | |||
| juce::MemoryBlock mb (data, dataSize); | |||
| { | |||
| const juce::ScopedLock sl (incomingPacketLock); | |||
| incomingPackets.add (std::move (mb)); | |||
| } | |||
| triggerAsyncUpdate(); | |||
| #if DUMP_BANDWIDTH_STATS | |||
| registerBytesIn ((int) dataSize); | |||
| #endif | |||
| } | |||
| void handleAsyncUpdate() override | |||
| { | |||
| juce::Array<juce::MemoryBlock> packets; | |||
| packets.ensureStorageAllocated (32); | |||
| { | |||
| const juce::ScopedLock sl (incomingPacketLock); | |||
| incomingPackets.swapWith (packets); | |||
| } | |||
| for (auto& packet : packets) | |||
| { | |||
| auto data = static_cast<const uint8*> (packet.getData()); | |||
| BlocksProtocol::HostPacketDecoder<ConnectedDeviceGroup> | |||
| ::processNextPacket (*this, *data, data + 1, (int) packet.getSize() - 1); | |||
| } | |||
| } | |||
| //============================================================================== | |||
| static juce::Array<DeviceInfo> getArrayOfDeviceInfo (const juce::Array<BlocksProtocol::DeviceStatus>& devices) | |||
| { | |||
| juce::Array<DeviceInfo> result; | |||
| bool isFirst = true; // TODO: First block not always master block! Assumption violated. | |||
| for (auto& device : devices) | |||
| { | |||
| BlocksProtocol::VersionNumber version; | |||
| BlocksProtocol::BlockName name; | |||
| result.add ({ getBlockUIDFromSerialNumber (device.serialNumber), | |||
| device.index, | |||
| device.serialNumber, | |||
| version, | |||
| name, | |||
| isFirst }); | |||
| isFirst = false; | |||
| } | |||
| return result; | |||
| } | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConnectedDeviceGroup) | |||
| }; | |||
| } // namespace juce | |||
| @@ -0,0 +1,698 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| The code included in this file is provided under the terms of the ISC license | |||
| http://www.isc.org/downloads/software-support-policy/isc-license. Permission | |||
| To use, copy, modify, and/or distribute this software for any purpose with or | |||
| without fee is hereby granted provided that the above copyright notice and | |||
| this permission notice appear in all copies. | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| namespace juce | |||
| { | |||
| namespace | |||
| { | |||
| static bool containsBlockWithUID (const Block::Array& blocks, Block::UID uid) noexcept | |||
| { | |||
| for (auto&& block : blocks) | |||
| if (block->uid == uid) | |||
| return true; | |||
| return false; | |||
| } | |||
| static bool versionNumberChanged (const DeviceInfo& device, juce::String version) noexcept | |||
| { | |||
| auto deviceVersion = device.version.asString(); | |||
| return deviceVersion != version && deviceVersion.isNotEmpty(); | |||
| } | |||
| static void setVersionNumberForBlock (const DeviceInfo& deviceInfo, Block& block) noexcept | |||
| { | |||
| jassert (deviceInfo.uid == block.uid); | |||
| block.versionNumber = deviceInfo.version.asString(); | |||
| } | |||
| static void setNameForBlock (const DeviceInfo& deviceInfo, Block& block) | |||
| { | |||
| jassert (deviceInfo.uid == block.uid); | |||
| block.name = deviceInfo.name.asString(); | |||
| } | |||
| //============================================================================== | |||
| #if DUMP_TOPOLOGY | |||
| static juce::String idToSerialNum (const BlockTopology& topology, Block::UID uid) | |||
| { | |||
| for (auto* b : topology.blocks) | |||
| if (b->uid == uid) | |||
| return b->serialNumber; | |||
| return "???"; | |||
| } | |||
| static juce::String portEdgeToString (Block::ConnectionPort port) | |||
| { | |||
| switch (port.edge) | |||
| { | |||
| case Block::ConnectionPort::DeviceEdge::north: return "north"; | |||
| case Block::ConnectionPort::DeviceEdge::south: return "south"; | |||
| case Block::ConnectionPort::DeviceEdge::east: return "east"; | |||
| case Block::ConnectionPort::DeviceEdge::west: return "west"; | |||
| } | |||
| return {}; | |||
| } | |||
| static juce::String portToString (Block::ConnectionPort port) | |||
| { | |||
| return portEdgeToString (port) + "_" + juce::String (port.index); | |||
| } | |||
| static void dumpTopology (const BlockTopology& topology) | |||
| { | |||
| MemoryOutputStream m; | |||
| m << "=============================================================================" << newLine | |||
| << "Topology: " << topology.blocks.size() << " device(s)" << newLine | |||
| << newLine; | |||
| int index = 0; | |||
| for (auto block : topology.blocks) | |||
| { | |||
| m << "Device " << index++ << (block->isMasterBlock() ? ": (MASTER)" : ":") << newLine; | |||
| m << " Description: " << block->getDeviceDescription() << newLine | |||
| << " Serial: " << block->serialNumber << newLine; | |||
| if (auto bi = BlockImpl<Detector>::getFrom (*block)) | |||
| m << " Short address: " << (int) bi->getDeviceIndex() << newLine; | |||
| m << " Battery level: " + juce::String (juce::roundToInt (100.0f * block->getBatteryLevel())) + "%" << newLine | |||
| << " Battery charging: " + juce::String (block->isBatteryCharging() ? "y" : "n") << newLine | |||
| << " Width: " << block->getWidth() << newLine | |||
| << " Height: " << block->getHeight() << newLine | |||
| << " Millimeters per unit: " << block->getMillimetersPerUnit() << newLine | |||
| << newLine; | |||
| } | |||
| for (auto& connection : topology.connections) | |||
| { | |||
| m << idToSerialNum (topology, connection.device1) | |||
| << ":" << portToString (connection.connectionPortOnDevice1) | |||
| << " <-> " | |||
| << idToSerialNum (topology, connection.device2) | |||
| << ":" << portToString (connection.connectionPortOnDevice2) << newLine; | |||
| } | |||
| m << "=============================================================================" << newLine; | |||
| Logger::outputDebugString (m.toString()); | |||
| } | |||
| #endif | |||
| } | |||
| //============================================================================== | |||
| /** This is the main singleton object that keeps track of connected blocks */ | |||
| struct Detector : public juce::ReferenceCountedObject, | |||
| private juce::Timer | |||
| { | |||
| using BlockImpl = BlockImplementation<Detector>; | |||
| Detector() : defaultDetector (new MIDIDeviceDetector()), deviceDetector (*defaultDetector) | |||
| { | |||
| startTimer (10); | |||
| } | |||
| Detector (PhysicalTopologySource::DeviceDetector& dd) : deviceDetector (dd) | |||
| { | |||
| startTimer (10); | |||
| } | |||
| ~Detector() | |||
| { | |||
| jassert (activeTopologySources.isEmpty()); | |||
| } | |||
| using Ptr = juce::ReferenceCountedObjectPtr<Detector>; | |||
| static Detector::Ptr getDefaultDetector() | |||
| { | |||
| auto& d = getDefaultDetectorPointer(); | |||
| if (d == nullptr) | |||
| d = new Detector(); | |||
| return d; | |||
| } | |||
| static Detector::Ptr& getDefaultDetectorPointer() | |||
| { | |||
| static Detector::Ptr defaultDetector; | |||
| return defaultDetector; | |||
| } | |||
| void detach (PhysicalTopologySource* pts) | |||
| { | |||
| activeTopologySources.removeAllInstancesOf (pts); | |||
| if (activeTopologySources.isEmpty()) | |||
| { | |||
| for (auto& b : currentTopology.blocks) | |||
| if (auto bi = BlockImpl::getFrom (*b)) | |||
| bi->sendCommandMessage (BlocksProtocol::endAPIMode); | |||
| currentTopology = {}; | |||
| lastTopology = {}; | |||
| auto& d = getDefaultDetectorPointer(); | |||
| if (d != nullptr && d->getReferenceCount() == 2) | |||
| getDefaultDetectorPointer() = nullptr; | |||
| } | |||
| } | |||
| bool isConnected (Block::UID deviceID) const noexcept | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED // This method must only be called from the message thread! | |||
| for (auto&& b : currentTopology.blocks) | |||
| if (b->uid == deviceID) | |||
| return true; | |||
| return false; | |||
| } | |||
| const BlocksProtocol::DeviceStatus* getLastStatus (Block::UID deviceID) const noexcept | |||
| { | |||
| for (auto d : connectedDeviceGroups) | |||
| if (auto status = d->getLastStatus (deviceID)) | |||
| return status; | |||
| return nullptr; | |||
| } | |||
| void handleTopologyChange() | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| { | |||
| juce::Array<DeviceInfo> newDeviceInfo; | |||
| juce::Array<BlockDeviceConnection> newDeviceConnections; | |||
| for (auto d : connectedDeviceGroups) | |||
| { | |||
| newDeviceInfo.addArray (d->getCurrentDeviceInfo()); | |||
| newDeviceConnections.addArray (d->getCurrentDeviceConnections()); | |||
| } | |||
| for (int i = currentTopology.blocks.size(); --i >= 0;) | |||
| { | |||
| auto currentBlock = currentTopology.blocks.getUnchecked (i); | |||
| auto newDeviceIter = std::find_if (newDeviceInfo.begin(), newDeviceInfo.end(), | |||
| [&] (DeviceInfo& info) { return info.uid == currentBlock->uid; }); | |||
| auto* blockImpl = BlockImpl::getFrom (*currentBlock); | |||
| if (newDeviceIter == newDeviceInfo.end()) | |||
| { | |||
| if (blockImpl != nullptr) | |||
| blockImpl->markDisconnected(); | |||
| disconnectedBlocks.addIfNotAlreadyThere (currentTopology.blocks.removeAndReturn (i).get()); | |||
| } | |||
| else | |||
| { | |||
| if (blockImpl != nullptr && blockImpl->wasPowerCycled()) | |||
| { | |||
| blockImpl->resetPowerCycleFlag(); | |||
| blockImpl->markReconnected (*newDeviceIter); | |||
| } | |||
| updateCurrentBlockInfo (currentBlock, *newDeviceIter); | |||
| } | |||
| } | |||
| static const int maxBlocksToSave = 100; | |||
| if (disconnectedBlocks.size() > maxBlocksToSave) | |||
| disconnectedBlocks.removeRange (0, 2 * (disconnectedBlocks.size() - maxBlocksToSave)); | |||
| for (auto& info : newDeviceInfo) | |||
| if (info.serial.isValid() && ! containsBlockWithUID (currentTopology.blocks, getBlockUIDFromSerialNumber (info.serial))) | |||
| addBlock (info); | |||
| currentTopology.connections.swapWith (newDeviceConnections); | |||
| } | |||
| broadcastTopology(); | |||
| } | |||
| void notifyBlockIsRestarting (Block::UID deviceID) | |||
| { | |||
| for (auto& group : connectedDeviceGroups) | |||
| group->notifyBlockIsRestarting (deviceID); | |||
| } | |||
| void handleSharedDataACK (Block::UID deviceID, uint32 packetCounter) const | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| if (auto* bi = getBlockImplementationWithUID (deviceID)) | |||
| bi->handleSharedDataACK (packetCounter); | |||
| } | |||
| void handleFirmwareUpdateACK (Block::UID deviceID, uint8 resultCode, uint32 resultDetail) | |||
| { | |||
| if (auto* bi = getBlockImplementationWithUID (deviceID)) | |||
| bi->handleFirmwareUpdateACK (resultCode, resultDetail); | |||
| } | |||
| void handleConfigUpdateMessage (Block::UID deviceID, int32 item, int32 value, int32 min, int32 max) | |||
| { | |||
| if (auto* bi = getBlockImplementationWithUID (deviceID)) | |||
| bi->handleConfigUpdateMessage (item, value, min, max); | |||
| } | |||
| void notifyBlockOfConfigChange (BlockImpl& bi, uint32 item) | |||
| { | |||
| if (auto configChangedCallback = bi.configChangedCallback) | |||
| { | |||
| if (item >= bi.getMaxConfigIndex()) | |||
| configChangedCallback (bi, {}, item); | |||
| else | |||
| configChangedCallback (bi, bi.getLocalConfigMetaData (item), item); | |||
| } | |||
| } | |||
| void handleConfigSetMessage (Block::UID deviceID, int32 item, int32 value) | |||
| { | |||
| if (auto* bi = getBlockImplementationWithUID (deviceID)) | |||
| { | |||
| bi->handleConfigSetMessage (item, value); | |||
| notifyBlockOfConfigChange (*bi, uint32 (item)); | |||
| } | |||
| } | |||
| void handleConfigFactorySyncEndMessage (Block::UID deviceID) | |||
| { | |||
| if (auto* bi = getBlockImplementationWithUID (deviceID)) | |||
| notifyBlockOfConfigChange (*bi, bi->getMaxConfigIndex()); | |||
| } | |||
| void handleConfigFactorySyncResetMessage (Block::UID deviceID) | |||
| { | |||
| if (auto* bi = getBlockImplementationWithUID (deviceID)) | |||
| bi->resetConfigListActiveStatus(); | |||
| } | |||
| void handleLogMessage (Block::UID deviceID, const String& message) const | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| if (auto* bi = getBlockImplementationWithUID (deviceID)) | |||
| bi->handleLogMessage (message); | |||
| } | |||
| void handleButtonChange (Block::UID deviceID, Block::Timestamp timestamp, uint32 buttonIndex, bool isDown) const | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| if (auto* bi = getBlockImplementationWithUID (deviceID)) | |||
| { | |||
| bi->pingFromDevice(); | |||
| if (isPositiveAndBelow (buttonIndex, bi->getButtons().size())) | |||
| if (auto* cbi = dynamic_cast<BlockImpl::ControlButtonImplementation*> (bi->getButtons().getUnchecked (int (buttonIndex)))) | |||
| cbi->broadcastButtonChange (timestamp, bi->modelData.buttons[(int) buttonIndex].type, isDown); | |||
| } | |||
| } | |||
| void handleTouchChange (Block::UID deviceID, const TouchSurface::Touch& touchEvent) | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| auto block = currentTopology.getBlockWithUID (deviceID); | |||
| if (block != nullptr) | |||
| { | |||
| if (auto* surface = dynamic_cast<BlockImpl::TouchSurfaceImplementation*> (block->getTouchSurface())) | |||
| { | |||
| TouchSurface::Touch scaledEvent (touchEvent); | |||
| scaledEvent.x *= block->getWidth(); | |||
| scaledEvent.y *= block->getHeight(); | |||
| scaledEvent.startX *= block->getWidth(); | |||
| scaledEvent.startY *= block->getHeight(); | |||
| surface->broadcastTouchChange (scaledEvent); | |||
| } | |||
| } | |||
| } | |||
| void cancelAllActiveTouches() noexcept | |||
| { | |||
| for (auto& block : currentTopology.blocks) | |||
| if (auto* surface = block->getTouchSurface()) | |||
| surface->cancelAllActiveTouches(); | |||
| } | |||
| void handleCustomMessage (Block::UID deviceID, Block::Timestamp timestamp, const int32* data) | |||
| { | |||
| if (auto* bi = getBlockImplementationWithUID (deviceID)) | |||
| bi->handleCustomMessage (timestamp, data); | |||
| } | |||
| //============================================================================== | |||
| int getIndexFromDeviceID (Block::UID deviceID) const noexcept | |||
| { | |||
| for (auto* c : connectedDeviceGroups) | |||
| { | |||
| auto index = c->getIndexFromDeviceID (deviceID); | |||
| if (index >= 0) | |||
| return index; | |||
| } | |||
| return -1; | |||
| } | |||
| template <typename PacketBuilder> | |||
| bool sendMessageToDevice (Block::UID deviceID, const PacketBuilder& builder) const | |||
| { | |||
| for (auto* c : connectedDeviceGroups) | |||
| if (c->getIndexFromDeviceID (deviceID) >= 0) | |||
| return c->sendMessageToDevice (builder); | |||
| return false; | |||
| } | |||
| static Detector* getFrom (Block& b) noexcept | |||
| { | |||
| if (auto* bi = BlockImpl::getFrom (b)) | |||
| return (bi->detector); | |||
| jassertfalse; | |||
| return nullptr; | |||
| } | |||
| PhysicalTopologySource::DeviceConnection* getDeviceConnectionFor (const Block& b) | |||
| { | |||
| for (const auto& d : connectedDeviceGroups) | |||
| { | |||
| for (const auto& info : d->getCurrentDeviceInfo()) | |||
| { | |||
| if (info.uid == b.uid) | |||
| return d->getDeviceConnection(); | |||
| } | |||
| } | |||
| return nullptr; | |||
| } | |||
| const PhysicalTopologySource::DeviceConnection* getDeviceConnectionFor (const Block& b) const | |||
| { | |||
| for (const auto& d : connectedDeviceGroups) | |||
| { | |||
| for (const auto& info : d->getCurrentDeviceInfo()) | |||
| { | |||
| if (info.uid == b.uid) | |||
| return d->getDeviceConnection(); | |||
| } | |||
| } | |||
| return nullptr; | |||
| } | |||
| std::unique_ptr<MIDIDeviceDetector> defaultDetector; | |||
| PhysicalTopologySource::DeviceDetector& deviceDetector; | |||
| juce::Array<PhysicalTopologySource*> activeTopologySources; | |||
| BlockTopology currentTopology, lastTopology; | |||
| juce::ReferenceCountedArray<Block, CriticalSection> disconnectedBlocks; | |||
| private: | |||
| void timerCallback() override | |||
| { | |||
| startTimer (1500); | |||
| auto detectedDevices = deviceDetector.scanForDevices(); | |||
| handleDevicesRemoved (detectedDevices); | |||
| handleDevicesAdded (detectedDevices); | |||
| } | |||
| void handleDevicesRemoved (const juce::StringArray& detectedDevices) | |||
| { | |||
| bool anyDevicesRemoved = false; | |||
| for (int i = connectedDeviceGroups.size(); --i >= 0;) | |||
| { | |||
| if (! connectedDeviceGroups.getUnchecked(i)->isStillConnected (detectedDevices)) | |||
| { | |||
| connectedDeviceGroups.remove (i); | |||
| anyDevicesRemoved = true; | |||
| } | |||
| } | |||
| if (anyDevicesRemoved) | |||
| handleTopologyChange(); | |||
| } | |||
| void handleDevicesAdded (const juce::StringArray& detectedDevices) | |||
| { | |||
| for (const auto& devName : detectedDevices) | |||
| { | |||
| if (! hasDeviceFor (devName)) | |||
| { | |||
| if (auto d = deviceDetector.openDevice (detectedDevices.indexOf (devName))) | |||
| { | |||
| connectedDeviceGroups.add (new ConnectedDeviceGroup<Detector> (*this, devName, d)); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| bool hasDeviceFor (const juce::String& devName) const | |||
| { | |||
| for (auto d : connectedDeviceGroups) | |||
| if (d->deviceName == devName) | |||
| return true; | |||
| return false; | |||
| } | |||
| void addBlock (DeviceInfo info) | |||
| { | |||
| if (! reactivateBlockIfKnown (info)) | |||
| addNewBlock (info); | |||
| } | |||
| bool reactivateBlockIfKnown (DeviceInfo info) | |||
| { | |||
| const auto uid = getBlockUIDFromSerialNumber (info.serial); | |||
| for (int i = disconnectedBlocks.size(); --i >= 0;) | |||
| { | |||
| if (uid != disconnectedBlocks.getUnchecked (i)->uid) | |||
| continue; | |||
| auto block = disconnectedBlocks.removeAndReturn (i); | |||
| if (auto* blockImpl = BlockImpl::getFrom (*block)) | |||
| { | |||
| blockImpl->markReconnected (info); | |||
| currentTopology.blocks.add (block); | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| void addNewBlock (DeviceInfo info) | |||
| { | |||
| currentTopology.blocks.add (new BlockImpl (info.serial, *this, info.version, | |||
| info.name, info.isMaster)); | |||
| } | |||
| void updateCurrentBlockInfo (Block::Ptr blockToUpdate, DeviceInfo& updatedInfo) | |||
| { | |||
| jassert (updatedInfo.uid == blockToUpdate->uid); | |||
| if (versionNumberChanged (updatedInfo, blockToUpdate->versionNumber)) | |||
| setVersionNumberForBlock (updatedInfo, *blockToUpdate); | |||
| if (updatedInfo.name.isValid()) | |||
| setNameForBlock (updatedInfo, *blockToUpdate); | |||
| if (updatedInfo.isMaster != blockToUpdate->isMasterBlock()) | |||
| BlockImpl::getFrom (*blockToUpdate)->setToMaster (updatedInfo.isMaster); | |||
| } | |||
| BlockImpl* getBlockImplementationWithUID (Block::UID deviceID) const noexcept | |||
| { | |||
| if (auto&& block = currentTopology.getBlockWithUID (deviceID)) | |||
| return BlockImpl::getFrom (*block); | |||
| return nullptr; | |||
| } | |||
| juce::OwnedArray<ConnectedDeviceGroup<Detector>> connectedDeviceGroups; | |||
| //============================================================================== | |||
| /** This is a friend of the BlocksImplementation that will scan and set the | |||
| physical positions of the blocks */ | |||
| struct BlocksTraverser | |||
| { | |||
| void traverseBlockArray (const BlockTopology& topology) | |||
| { | |||
| juce::Array<Block::UID> visited; | |||
| for (auto& block : topology.blocks) | |||
| { | |||
| if (block->isMasterBlock() && ! visited.contains (block->uid)) | |||
| { | |||
| if (auto* bi = dynamic_cast<BlockImpl*> (block)) | |||
| { | |||
| bi->masterUID = {}; | |||
| bi->position = {}; | |||
| bi->rotation = 0; | |||
| } | |||
| layoutNeighbours (*block, topology, block->uid, visited); | |||
| } | |||
| } | |||
| } | |||
| // returns the distance from corner clockwise | |||
| int getUnitForIndex (Block::Ptr block, Block::ConnectionPort::DeviceEdge edge, int index) | |||
| { | |||
| if (block->getType() == Block::seaboardBlock) | |||
| { | |||
| if (edge == Block::ConnectionPort::DeviceEdge::north) | |||
| { | |||
| if (index == 0) return 1; | |||
| if (index == 1) return 4; | |||
| } | |||
| else if (edge != Block::ConnectionPort::DeviceEdge::south) | |||
| { | |||
| return 1; | |||
| } | |||
| } | |||
| if (edge == Block::ConnectionPort::DeviceEdge::south) | |||
| return block->getWidth() - (index + 1); | |||
| if (edge == Block::ConnectionPort::DeviceEdge::west) | |||
| return block->getHeight() - (index + 1); | |||
| return index; | |||
| } | |||
| // returns how often north needs to rotate by 90 degrees | |||
| int getRotationForEdge (Block::ConnectionPort::DeviceEdge edge) | |||
| { | |||
| switch (edge) | |||
| { | |||
| case Block::ConnectionPort::DeviceEdge::north: return 0; | |||
| case Block::ConnectionPort::DeviceEdge::east: return 1; | |||
| case Block::ConnectionPort::DeviceEdge::south: return 2; | |||
| case Block::ConnectionPort::DeviceEdge::west: return 3; | |||
| } | |||
| jassertfalse; | |||
| return 0; | |||
| } | |||
| void layoutNeighbours (Block::Ptr block, const BlockTopology& topology, | |||
| Block::UID masterUid, juce::Array<Block::UID>& visited) | |||
| { | |||
| visited.add (block->uid); | |||
| for (auto& connection : topology.connections) | |||
| { | |||
| if ((connection.device1 == block->uid && ! visited.contains (connection.device2)) | |||
| || (connection.device2 == block->uid && ! visited.contains (connection.device1))) | |||
| { | |||
| const auto theirUid = connection.device1 == block->uid ? connection.device2 : connection.device1; | |||
| const auto neighbourPtr = topology.getBlockWithUID (theirUid); | |||
| if (auto* neighbour = dynamic_cast<BlockImpl*> (neighbourPtr.get())) | |||
| { | |||
| const auto myBounds = block->getBlockAreaWithinLayout(); | |||
| const auto& myPort = connection.device1 == block->uid ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2; | |||
| const auto& theirPort = connection.device1 == block->uid ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1; | |||
| const auto myOffset = getUnitForIndex (block, myPort.edge, myPort.index); | |||
| const auto theirOffset = getUnitForIndex (neighbourPtr, theirPort.edge, theirPort.index); | |||
| neighbour->masterUID = masterUid; | |||
| neighbour->rotation = (2 + block->getRotation() | |||
| + getRotationForEdge (myPort.edge) | |||
| - getRotationForEdge (theirPort.edge)) % 4; | |||
| Point<int> delta; | |||
| const auto theirBounds = neighbour->getBlockAreaWithinLayout(); | |||
| switch ((block->getRotation() + getRotationForEdge (myPort.edge)) % 4) | |||
| { | |||
| case 0: // over me | |||
| delta = { myOffset - (theirBounds.getWidth() - (theirOffset + 1)), -theirBounds.getHeight() }; | |||
| break; | |||
| case 1: // right of me | |||
| delta = { myBounds.getWidth(), myOffset - (theirBounds.getHeight() - (theirOffset + 1)) }; | |||
| break; | |||
| case 2: // under me | |||
| delta = { (myBounds.getWidth() - (myOffset + 1)) - theirOffset, myBounds.getHeight() }; | |||
| break; | |||
| case 3: // left of me | |||
| delta = { -theirBounds.getWidth(), (myBounds.getHeight() - (myOffset + 1)) - theirOffset }; | |||
| break; | |||
| } | |||
| neighbour->position = myBounds.getPosition() + delta; | |||
| } | |||
| layoutNeighbours (neighbourPtr, topology, masterUid, visited); | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| void broadcastTopology() | |||
| { | |||
| if (currentTopology != lastTopology) | |||
| { | |||
| lastTopology = currentTopology; | |||
| BlocksTraverser traverser; | |||
| traverser.traverseBlockArray (currentTopology); | |||
| for (auto* d : activeTopologySources) | |||
| d->listeners.call ([] (TopologySource::Listener& l) { l.topologyChanged(); }); | |||
| #if DUMP_TOPOLOGY | |||
| dumpTopology (lastTopology); | |||
| #endif | |||
| } | |||
| } | |||
| JUCE_DECLARE_WEAK_REFERENCEABLE (Detector) | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Detector) | |||
| }; | |||
| } // namespace juce | |||
| @@ -0,0 +1,59 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| The code included in this file is provided under the terms of the ISC license | |||
| http://www.isc.org/downloads/software-support-policy/isc-license. Permission | |||
| To use, copy, modify, and/or distribute this software for any purpose with or | |||
| without fee is hereby granted provided that the above copyright notice and | |||
| this permission notice appear in all copies. | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| namespace juce | |||
| { | |||
| struct PhysicalTopologySource::DetectorHolder : private juce::Timer | |||
| { | |||
| DetectorHolder (PhysicalTopologySource& pts) | |||
| : topologySource (pts), | |||
| detector (Detector::getDefaultDetector()) | |||
| { | |||
| startTimerHz (30); | |||
| } | |||
| DetectorHolder (PhysicalTopologySource& pts, DeviceDetector& dd) | |||
| : topologySource (pts), | |||
| detector (new Detector (dd)) | |||
| { | |||
| startTimerHz (30); | |||
| } | |||
| void timerCallback() override | |||
| { | |||
| if (! topologySource.hasOwnServiceTimer()) | |||
| handleTimerTick(); | |||
| } | |||
| void handleTimerTick() | |||
| { | |||
| for (auto& b : detector->currentTopology.blocks) | |||
| if (auto bi = BlockImplementation<Detector>::getFrom (*b)) | |||
| bi->handleTimerTick(); | |||
| } | |||
| PhysicalTopologySource& topologySource; | |||
| Detector::Ptr detector; | |||
| }; | |||
| } // namespace juce | |||
| @@ -0,0 +1,43 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| The code included in this file is provided under the terms of the ISC license | |||
| http://www.isc.org/downloads/software-support-policy/isc-license. Permission | |||
| To use, copy, modify, and/or distribute this software for any purpose with or | |||
| without fee is hereby granted provided that the above copyright notice and | |||
| this permission notice appear in all copies. | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| namespace juce | |||
| { | |||
| struct DeviceInfo | |||
| { | |||
| // VS2015 requires a constructor to avoid aggregate initialization | |||
| DeviceInfo (Block::UID buid, BlocksProtocol::TopologyIndex tidx, BlocksProtocol::BlockSerialNumber s, | |||
| BlocksProtocol::VersionNumber v, BlocksProtocol::BlockName n, bool master = false) | |||
| : uid (buid), index (tidx), serial (s), version (v), name (n), isMaster (master) | |||
| { | |||
| } | |||
| Block::UID uid {}; | |||
| BlocksProtocol::TopologyIndex index; | |||
| BlocksProtocol::BlockSerialNumber serial; | |||
| BlocksProtocol::VersionNumber version; | |||
| BlocksProtocol::BlockName name; | |||
| bool isMaster {}; | |||
| }; | |||
| } // namespace juce | |||
| @@ -0,0 +1,142 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| The code included in this file is provided under the terms of the ISC license | |||
| http://www.isc.org/downloads/software-support-policy/isc-license. Permission | |||
| To use, copy, modify, and/or distribute this software for any purpose with or | |||
| without fee is hereby granted provided that the above copyright notice and | |||
| this permission notice appear in all copies. | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| namespace juce | |||
| { | |||
| struct MIDIDeviceDetector : public PhysicalTopologySource::DeviceDetector | |||
| { | |||
| MIDIDeviceDetector() {} | |||
| juce::StringArray scanForDevices() override | |||
| { | |||
| juce::StringArray result; | |||
| for (auto& pair : findDevices()) | |||
| result.add (pair.inputName + " & " + pair.outputName); | |||
| return result; | |||
| } | |||
| PhysicalTopologySource::DeviceConnection* openDevice (int index) override | |||
| { | |||
| auto pair = findDevices()[index]; | |||
| if (pair.inputIndex >= 0 && pair.outputIndex >= 0) | |||
| { | |||
| std::unique_ptr<MIDIDeviceConnection> dev (new MIDIDeviceConnection()); | |||
| if (dev->lockAgainstOtherProcesses (pair.inputName, pair.outputName)) | |||
| { | |||
| lockedFromOutside = false; | |||
| dev->midiInput.reset (juce::MidiInput::openDevice (pair.inputIndex, dev.get())); | |||
| dev->midiOutput.reset (juce::MidiOutput::openDevice (pair.outputIndex)); | |||
| if (dev->midiInput != nullptr) | |||
| { | |||
| dev->midiInput->start(); | |||
| return dev.release(); | |||
| } | |||
| } | |||
| else | |||
| { | |||
| lockedFromOutside = true; | |||
| } | |||
| } | |||
| return nullptr; | |||
| } | |||
| bool isLockedFromOutside() const override | |||
| { | |||
| return lockedFromOutside && ! findDevices().isEmpty(); | |||
| } | |||
| static bool isBlocksMidiDeviceName (const juce::String& name) | |||
| { | |||
| return name.indexOf (" BLOCK") > 0 || name.indexOf (" Block") > 0; | |||
| } | |||
| static String cleanBlocksDeviceName (juce::String name) | |||
| { | |||
| name = name.trim(); | |||
| if (name.endsWith (" IN)")) | |||
| return name.dropLastCharacters (4); | |||
| if (name.endsWith (" OUT)")) | |||
| return name.dropLastCharacters (5); | |||
| const int openBracketPosition = name.lastIndexOfChar ('['); | |||
| if (openBracketPosition != -1 && name.endsWith ("]")) | |||
| return name.dropLastCharacters (name.length() - openBracketPosition); | |||
| return name; | |||
| } | |||
| struct MidiInputOutputPair | |||
| { | |||
| juce::String outputName, inputName; | |||
| int outputIndex = -1, inputIndex = -1; | |||
| }; | |||
| static juce::Array<MidiInputOutputPair> findDevices() | |||
| { | |||
| juce::Array<MidiInputOutputPair> result; | |||
| auto midiInputs = juce::MidiInput::getDevices(); | |||
| auto midiOutputs = juce::MidiOutput::getDevices(); | |||
| for (int j = 0; j < midiInputs.size(); ++j) | |||
| { | |||
| if (isBlocksMidiDeviceName (midiInputs[j])) | |||
| { | |||
| MidiInputOutputPair pair; | |||
| pair.inputName = midiInputs[j]; | |||
| pair.inputIndex = j; | |||
| String cleanedInputName = cleanBlocksDeviceName (pair.inputName); | |||
| for (int i = 0; i < midiOutputs.size(); ++i) | |||
| { | |||
| if (cleanBlocksDeviceName (midiOutputs[i]) == cleanedInputName) | |||
| { | |||
| pair.outputName = midiOutputs[i]; | |||
| pair.outputIndex = i; | |||
| break; | |||
| } | |||
| } | |||
| result.add (pair); | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| private: | |||
| bool lockedFromOutside = true; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MIDIDeviceDetector) | |||
| }; | |||
| } // namespace juce | |||
| @@ -0,0 +1,114 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| The code included in this file is provided under the terms of the ISC license | |||
| http://www.isc.org/downloads/software-support-policy/isc-license. Permission | |||
| To use, copy, modify, and/or distribute this software for any purpose with or | |||
| without fee is hereby granted provided that the above copyright notice and | |||
| this permission notice appear in all copies. | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| namespace juce | |||
| { | |||
| struct MIDIDeviceConnection : public PhysicalTopologySource::DeviceConnection, | |||
| public juce::MidiInputCallback | |||
| { | |||
| MIDIDeviceConnection() {} | |||
| ~MIDIDeviceConnection() | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| listeners.call ([this] (Listener& l) { l.connectionBeingDeleted (*this); }); | |||
| if (midiInput != nullptr) | |||
| midiInput->stop(); | |||
| if (interprocessLock != nullptr) | |||
| interprocessLock->exit(); | |||
| } | |||
| bool lockAgainstOtherProcesses (const String& midiInName, const String& midiOutName) | |||
| { | |||
| interprocessLock.reset (new juce::InterProcessLock ("blocks_sdk_" | |||
| + File::createLegalFileName (midiInName) | |||
| + "_" + File::createLegalFileName (midiOutName))); | |||
| if (interprocessLock->enter (500)) | |||
| return true; | |||
| interprocessLock = nullptr; | |||
| return false; | |||
| } | |||
| struct Listener | |||
| { | |||
| virtual ~Listener() {} | |||
| virtual void handleIncomingMidiMessage (const juce::MidiMessage& message) = 0; | |||
| virtual void connectionBeingDeleted (const MIDIDeviceConnection&) = 0; | |||
| }; | |||
| void addListener (Listener* l) | |||
| { | |||
| listeners.add (l); | |||
| } | |||
| void removeListener (Listener* l) | |||
| { | |||
| listeners.remove (l); | |||
| } | |||
| bool sendMessageToDevice (const void* data, size_t dataSize) override | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED // This method must only be called from the message thread! | |||
| jassert (dataSize > sizeof (BlocksProtocol::roliSysexHeader) + 2); | |||
| jassert (memcmp (data, BlocksProtocol::roliSysexHeader, sizeof (BlocksProtocol::roliSysexHeader)) == 0); | |||
| jassert (static_cast<const uint8*> (data)[dataSize - 1] == 0xf7); | |||
| if (midiOutput != nullptr) | |||
| { | |||
| midiOutput->sendMessageNow (juce::MidiMessage (data, (int) dataSize)); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| void handleIncomingMidiMessage (juce::MidiInput*, const juce::MidiMessage& message) override | |||
| { | |||
| const auto data = message.getRawData(); | |||
| const int dataSize = message.getRawDataSize(); | |||
| const int bodySize = dataSize - (int) (sizeof (BlocksProtocol::roliSysexHeader) + 1); | |||
| if (bodySize > 0 && memcmp (data, BlocksProtocol::roliSysexHeader, sizeof (BlocksProtocol::roliSysexHeader)) == 0) | |||
| if (handleMessageFromDevice != nullptr) | |||
| handleMessageFromDevice (data + sizeof (BlocksProtocol::roliSysexHeader), (size_t) bodySize); | |||
| listeners.call ([&] (Listener& l) { l.handleIncomingMidiMessage (message); }); | |||
| } | |||
| std::unique_ptr<juce::MidiInput> midiInput; | |||
| std::unique_ptr<juce::MidiOutput> midiOutput; | |||
| private: | |||
| juce::ListenerList<Listener> listeners; | |||
| std::unique_ptr<juce::InterProcessLock> interprocessLock; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MIDIDeviceConnection) | |||
| }; | |||
| } // namespace juce | |||
| @@ -88,7 +88,7 @@ protected: | |||
| private: | |||
| //========================================================================== | |||
| DeviceDetector* customDetector = nullptr; | |||
| struct Internal; | |||
| friend struct Detector; | |||
| struct DetectorHolder; | |||
| std::unique_ptr<DetectorHolder> detector; | |||