| @@ -153,6 +153,7 @@ struct BlockSerialNumber | |||||
| bool hasPrefix (const char* prefix) const noexcept { return memcmp (serial, prefix, 3) == 0; } | bool hasPrefix (const char* prefix) const noexcept { return memcmp (serial, prefix, 3) == 0; } | ||||
| }; | }; | ||||
| //============================================================================== | |||||
| /** Structure for the version number | /** Structure for the version number | ||||
| @tags{Blocks} | @tags{Blocks} | ||||
| @@ -161,8 +162,15 @@ struct VersionNumber | |||||
| { | { | ||||
| uint8 version[21] = {}; | uint8 version[21] = {}; | ||||
| uint8 length = 0; | 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 | /** Structure for the block name | ||||
| @tags{Blocks} | @tags{Blocks} | ||||
| @@ -171,8 +179,17 @@ struct BlockName | |||||
| { | { | ||||
| uint8 name[33] = {}; | uint8 name[33] = {}; | ||||
| uint8 length = 0; | 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 | /** Structure for the device status | ||||
| @tags{Blocks} | @tags{Blocks} | ||||
| @@ -185,6 +202,7 @@ struct DeviceStatus | |||||
| BatteryCharging batteryCharging; | BatteryCharging batteryCharging; | ||||
| }; | }; | ||||
| //============================================================================== | |||||
| /** Structure for the device connection | /** Structure for the device connection | ||||
| @tags{Blocks} | @tags{Blocks} | ||||
| @@ -195,6 +213,7 @@ struct DeviceConnection | |||||
| ConnectorPort port1, port2; | ConnectorPort port1, port2; | ||||
| }; | }; | ||||
| //============================================================================== | |||||
| /** Structure for the device version | /** Structure for the device version | ||||
| @tags{Blocks} | @tags{Blocks} | ||||
| @@ -205,6 +224,7 @@ struct DeviceVersion | |||||
| VersionNumber version; | VersionNumber version; | ||||
| }; | }; | ||||
| //============================================================================== | |||||
| /** Structure used for the device name | /** Structure used for the device name | ||||
| @tags{Blocks} | @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: | private: | ||||
| //========================================================================== | //========================================================================== | ||||
| DeviceDetector* customDetector = nullptr; | DeviceDetector* customDetector = nullptr; | ||||
| struct Internal; | |||||
| friend struct Detector; | |||||
| struct DetectorHolder; | struct DetectorHolder; | ||||
| std::unique_ptr<DetectorHolder> detector; | std::unique_ptr<DetectorHolder> detector; | ||||