| @@ -27,7 +27,7 @@ static Block::UID getBlockUIDFromSerialNumber (const uint8* serial) noexcept | |||
| { | |||
| Block::UID n = {}; | |||
| for (int i = 0; i < (int) sizeof (BlocksProtocol::BlockSerialNumber); ++i) | |||
| for (int i = 0; i < int (BlocksProtocol::BlockSerialNumber::maxLength); ++i) | |||
| n += n * 127 + serial[i]; | |||
| return n; | |||
| @@ -35,15 +35,15 @@ static Block::UID getBlockUIDFromSerialNumber (const uint8* serial) noexcept | |||
| static Block::UID getBlockUIDFromSerialNumber (const BlocksProtocol::BlockSerialNumber& serial) noexcept | |||
| { | |||
| return getBlockUIDFromSerialNumber (serial.serial); | |||
| return getBlockUIDFromSerialNumber (serial.data); | |||
| } | |||
| static Block::UID getBlockUIDFromSerialNumber (const juce::String& serial) noexcept | |||
| { | |||
| if (serial.length() < (int) sizeof (BlocksProtocol::BlockSerialNumber)) | |||
| if (serial.length() < int (BlocksProtocol::BlockSerialNumber::maxLength)) | |||
| { | |||
| jassertfalse; | |||
| return getBlockUIDFromSerialNumber (serial.paddedRight ('0', sizeof (BlocksProtocol::BlockSerialNumber))); | |||
| return getBlockUIDFromSerialNumber (serial.paddedRight ('0', BlocksProtocol::BlockSerialNumber::maxLength)); | |||
| } | |||
| return getBlockUIDFromSerialNumber ((const uint8*) serial.toRawUTF8()); | |||
| @@ -107,6 +107,13 @@ public: | |||
| /** Returns true if this block is connected and active. */ | |||
| virtual bool isConnected() const = 0; | |||
| /** Returns the time this block object was connected to the topology. | |||
| Only valid when isConnected == true. | |||
| @see isConnected | |||
| */ | |||
| virtual Time getConnectionTime() const = 0; | |||
| /** Returns true if this block is directly connected to the application, | |||
| as opposed to only being connected to a different block via a connection port. | |||
| @@ -123,36 +123,6 @@ using BatteryCharging = IntegerWithBitSize<1>; | |||
| */ | |||
| using ConnectorPort = IntegerWithBitSize<5>; | |||
| //============================================================================== | |||
| /** Structure describing a block's serial number | |||
| @tags{Blocks} | |||
| */ | |||
| struct BlockSerialNumber | |||
| { | |||
| uint8 serial[16]; | |||
| bool isValid() const noexcept | |||
| { | |||
| for (auto c : serial) | |||
| if (c == 0) | |||
| return false; | |||
| return isAnyControlBlock() || isPadBlock() || isSeaboardBlock(); | |||
| } | |||
| bool isPadBlock() const noexcept { return hasPrefix ("LPB") || hasPrefix ("LPM"); } | |||
| bool isLiveBlock() const noexcept { return hasPrefix ("LIC"); } | |||
| bool isLoopBlock() const noexcept { return hasPrefix ("LOC"); } | |||
| bool isDevCtrlBlock() const noexcept { return hasPrefix ("DCB"); } | |||
| bool isTouchBlock() const noexcept { return hasPrefix ("TCB"); } | |||
| bool isSeaboardBlock() const noexcept { return hasPrefix ("SBB"); } | |||
| bool isAnyControlBlock() const noexcept { return isLiveBlock() || isLoopBlock() || isDevCtrlBlock() || isTouchBlock(); } | |||
| bool hasPrefix (const char* prefix) const noexcept { return memcmp (serial, prefix, 3) == 0; } | |||
| }; | |||
| //============================================================================== | |||
| /** Structure for generic block data | |||
| @@ -171,6 +141,11 @@ struct BlockStringData | |||
| return length > 0; | |||
| } | |||
| juce::String asString() const | |||
| { | |||
| return juce::String ((const char*) data, length); | |||
| } | |||
| bool operator== (const BlockStringData& other) const | |||
| { | |||
| if (length != other.length) | |||
| @@ -192,6 +167,34 @@ struct BlockStringData | |||
| using VersionNumber = BlockStringData<21>; | |||
| using BlockName = BlockStringData<33>; | |||
| //============================================================================== | |||
| /** Structure describing a block's serial number | |||
| @tags{Blocks} | |||
| */ | |||
| struct BlockSerialNumber : public BlockStringData<16> | |||
| { | |||
| bool isValid() const noexcept | |||
| { | |||
| for (auto c : data) | |||
| if (c == 0) | |||
| return false; | |||
| return isAnyControlBlock() || isPadBlock() || isSeaboardBlock(); | |||
| } | |||
| bool isPadBlock() const noexcept { return hasPrefix ("LPB") || hasPrefix ("LPM"); } | |||
| bool isLiveBlock() const noexcept { return hasPrefix ("LIC"); } | |||
| bool isLoopBlock() const noexcept { return hasPrefix ("LOC"); } | |||
| bool isDevCtrlBlock() const noexcept { return hasPrefix ("DCB"); } | |||
| bool isTouchBlock() const noexcept { return hasPrefix ("TCB"); } | |||
| bool isSeaboardBlock() const noexcept { return hasPrefix ("SBB"); } | |||
| bool isAnyControlBlock() const noexcept { return isLiveBlock() || isLoopBlock() || isDevCtrlBlock() || isTouchBlock(); } | |||
| bool hasPrefix (const char* prefix) const noexcept { return memcmp (data, prefix, 3) == 0; } | |||
| }; | |||
| //============================================================================== | |||
| /** Structure for the device status | |||
| @@ -214,6 +217,25 @@ struct DeviceConnection | |||
| { | |||
| TopologyIndex device1, device2; | |||
| ConnectorPort port1, port2; | |||
| bool operator== (const DeviceConnection& other) const | |||
| { | |||
| return isEqual (other); | |||
| } | |||
| bool operator!= (const DeviceConnection& other) const | |||
| { | |||
| return ! isEqual (other); | |||
| } | |||
| private: | |||
| bool isEqual (const DeviceConnection& other) const | |||
| { | |||
| return device1 == other.device1 | |||
| && device2 == other.device2 | |||
| && port1 == other.port1 | |||
| && port2 == other.port2; | |||
| } | |||
| }; | |||
| //============================================================================== | |||
| @@ -445,7 +467,7 @@ static constexpr uint32 controlBlockStackSize = 800; | |||
| enum BitSizes | |||
| { | |||
| topologyMessageHeader = MessageType::bits + ProtocolVersion::bits + DeviceCount::bits + ConnectionCount::bits, | |||
| topologyDeviceInfo = sizeof (BlockSerialNumber) * 7 + BatteryLevel::bits + BatteryCharging::bits, | |||
| topologyDeviceInfo = BlockSerialNumber::maxLength * 7 + BatteryLevel::bits + BatteryCharging::bits, | |||
| topologyConnectionInfo = topologyIndexBits + ConnectorPort::bits + topologyIndexBits + ConnectorPort::bits, | |||
| typeDeviceAndTime = MessageType::bits + PacketTimestampOffset::bits, | |||
| @@ -164,8 +164,11 @@ struct HostPacketDecoder | |||
| { | |||
| DeviceStatus status; | |||
| for (uint32 i = 0; i < sizeof (BlockSerialNumber); ++i) | |||
| status.serialNumber.serial[i] = (uint8) reader.readBits (7); | |||
| for (uint32 i = 0; i < BlockSerialNumber::maxLength; ++i) | |||
| { | |||
| status.serialNumber.data[i] = (uint8) reader.readBits (7); | |||
| ++status.serialNumber.length; | |||
| } | |||
| status.index = (TopologyIndex) reader.readBits (topologyIndexBits); | |||
| status.batteryLevel = reader.read<BatteryLevel>(); | |||
| @@ -34,19 +34,16 @@ public: | |||
| struct LEDGridImplementation; | |||
| struct LEDRowImplementation; | |||
| BlockImplementation (const BlocksProtocol::BlockSerialNumber& serial, | |||
| Detector& detectorToUse, | |||
| BlocksProtocol::VersionNumber version, | |||
| BlocksProtocol::BlockName blockName, | |||
| bool isMasterBlock) | |||
| : Block (juce::String ((const char*) serial.serial, sizeof (serial.serial)), | |||
| juce::String ((const char*) version.data, version.length), | |||
| juce::String ((const char*) blockName.data, blockName.length)), | |||
| modelData (serial), | |||
| BlockImplementation (Detector& detectorToUse, const DeviceInfo& deviceInfo) | |||
| : Block (deviceInfo.serial.asString(), | |||
| deviceInfo.version.asString(), | |||
| deviceInfo.name.asString()), | |||
| modelData (deviceInfo.serial), | |||
| remoteHeap (modelData.programAndHeapSize), | |||
| detector (&detectorToUse), | |||
| isMaster (isMasterBlock) | |||
| detector (&detectorToUse) | |||
| { | |||
| markReconnected (deviceInfo); | |||
| if (modelData.hasTouchSurface) | |||
| touchSurface.reset (new TouchSurfaceImplementation (*this)); | |||
| @@ -77,13 +74,25 @@ public: | |||
| { | |||
| if (auto surface = dynamic_cast<TouchSurfaceImplementation*> (touchSurface.get())) | |||
| surface->disableTouchSurface(); | |||
| connectionTime = Time(); | |||
| } | |||
| void markReconnected (const DeviceInfo& deviceInfo) | |||
| { | |||
| versionNumber = asString (deviceInfo.version); | |||
| name = asString (deviceInfo.name); | |||
| if (wasPowerCycled()) | |||
| resetPowerCycleFlag(); | |||
| if (connectionTime == Time()) | |||
| connectionTime = Time::getCurrentTime(); | |||
| versionNumber = deviceInfo.version.asString(); | |||
| name = deviceInfo.name.asString(); | |||
| isMaster = deviceInfo.isMaster; | |||
| masterUID = deviceInfo.masterUid; | |||
| batteryCharging = deviceInfo.batteryCharging; | |||
| batteryLevel = deviceInfo.batteryLevel; | |||
| topologyIndex = deviceInfo.index; | |||
| setProgram (nullptr); | |||
| remoteHeap.resetDeviceStateToUnknown(); | |||
| @@ -120,6 +129,7 @@ public: | |||
| bool isHardwareBlock() const override { return true; } | |||
| juce::Array<Block::ConnectionPort> getPorts() const override { return modelData.ports; } | |||
| bool isConnected() const override { return detector && detector->isConnected (uid); } | |||
| juce::Time getConnectionTime() const override { return connectionTime; } | |||
| bool isMasterBlock() const override { return isMaster; } | |||
| Block::UID getConnectedMasterUID() const override { return masterUID; } | |||
| int getRotation() const override { return rotation; } | |||
| @@ -159,24 +169,12 @@ public: | |||
| float getBatteryLevel() const override | |||
| { | |||
| if (detector == nullptr) | |||
| return 0.0f; | |||
| if (auto status = detector->getLastStatus (uid)) | |||
| return status->batteryLevel.toUnipolarFloat(); | |||
| return 0.0f; | |||
| return batteryLevel.toUnipolarFloat(); | |||
| } | |||
| bool isBatteryCharging() const override | |||
| { | |||
| if (detector == nullptr) | |||
| return false; | |||
| if (auto status = detector->getLastStatus (uid)) | |||
| return status->batteryCharging.get() != 0; | |||
| return false; | |||
| return batteryCharging.get() > 0; | |||
| } | |||
| bool supportsGraphics() const override | |||
| @@ -186,10 +184,7 @@ public: | |||
| int getDeviceIndex() const noexcept | |||
| { | |||
| if (detector == nullptr) | |||
| return -1; | |||
| return isConnected() ? detector->getIndexFromDeviceID (uid) : -1; | |||
| return isConnected() ? topologyIndex : -1; | |||
| } | |||
| template <typename PacketBuilder> | |||
| @@ -220,10 +215,15 @@ public: | |||
| programEventListeners.call ([&] (ProgramEventListener& l) { l.handleProgramEvent (*this, m); }); | |||
| } | |||
| static BlockImplementation* getFrom (Block* b) noexcept | |||
| { | |||
| jassert (dynamic_cast<BlockImplementation*> (b) != nullptr); | |||
| return dynamic_cast<BlockImplementation*> (b); | |||
| } | |||
| static BlockImplementation* getFrom (Block& b) noexcept | |||
| { | |||
| jassert (dynamic_cast<BlockImplementation*> (&b) != nullptr); | |||
| return dynamic_cast<BlockImplementation*> (&b); | |||
| return getFrom (&b); | |||
| } | |||
| //============================================================================== | |||
| @@ -584,6 +584,13 @@ private: | |||
| bool isMaster = false; | |||
| Block::UID masterUID = {}; | |||
| BlocksProtocol::BatteryLevel batteryLevel {}; | |||
| BlocksProtocol::BatteryCharging batteryCharging {}; | |||
| BlocksProtocol::TopologyIndex topologyIndex {}; | |||
| Time connectionTime {}; | |||
| std::pair<int, int> position; | |||
| int rotation = 0; | |||
| friend Detector; | |||
| @@ -29,55 +29,6 @@ namespace | |||
| { | |||
| return static_cast<Block::Timestamp> (timestamp); | |||
| } | |||
| template <typename V> | |||
| static int removeUnusedBlocksFromMap (std::map<Block::UID, V>& mapToClean, const juce::Array<DeviceInfo>& currentDevices) | |||
| { | |||
| int numRemoved = 0; | |||
| for (auto iter = mapToClean.begin(); iter != mapToClean.end();) | |||
| { | |||
| bool found = false; | |||
| for (auto& info : currentDevices) | |||
| { | |||
| if (info.uid == iter->first) | |||
| { | |||
| found = true; | |||
| break; | |||
| } | |||
| } | |||
| if (found) | |||
| { | |||
| ++iter; | |||
| } | |||
| else | |||
| { | |||
| mapToClean.erase (iter++); | |||
| ++numRemoved; | |||
| } | |||
| } | |||
| return numRemoved; | |||
| } | |||
| /* Returns true new element added to map or value of existing key changed */ | |||
| template <typename V> | |||
| static bool insertOrAssign (std::map<Block::UID, V>& map, const Block::UID& key, const V& value) | |||
| { | |||
| const auto result = map.emplace (std::make_pair (key, value)); | |||
| if (! result.second) | |||
| { | |||
| if (result.first->second == value) | |||
| return false; | |||
| result.first->second = value; | |||
| } | |||
| return true; | |||
| } | |||
| } | |||
| template <typename Detector> | |||
| @@ -88,8 +39,6 @@ struct ConnectedDeviceGroup : private juce::AsyncUpdater, | |||
| ConnectedDeviceGroup (Detector& d, const juce::String& name, PhysicalTopologySource::DeviceConnection* connection) | |||
| : detector (d), deviceName (name), deviceConnection (connection) | |||
| { | |||
| if (auto midiDeviceConnection = static_cast<MIDIDeviceConnection*> (deviceConnection.get())) | |||
| { | |||
| depreciatedVersionReader = std::make_unique<DepreciatedVersionReader> (*midiDeviceConnection); | |||
| @@ -106,30 +55,23 @@ struct ConnectedDeviceGroup : private juce::AsyncUpdater, | |||
| sendTopologyRequest(); | |||
| } | |||
| bool isStillConnected (const juce::StringArray& detectedDevices) const noexcept | |||
| ~ConnectedDeviceGroup() | |||
| { | |||
| return detectedDevices.contains (deviceName) && ! failedToGetTopology(); | |||
| for (const auto& device : currentDeviceInfo) | |||
| detector.handleDeviceRemoved (device); | |||
| } | |||
| int getIndexFromDeviceID (Block::UID uid) const noexcept | |||
| bool isStillConnected (const juce::StringArray& detectedDevices) const noexcept | |||
| { | |||
| for (auto& d : currentDeviceInfo) | |||
| if (d.uid == uid) | |||
| return d.index; | |||
| return -1; | |||
| return detectedDevices.contains (deviceName) && ! failedToGetTopology(); | |||
| } | |||
| const BlocksProtocol::DeviceStatus* getLastStatus (Block::UID deviceID) const noexcept | |||
| bool contains (Block::UID uid) | |||
| { | |||
| for (auto&& status : currentTopologyDevices) | |||
| if (getBlockUIDFromSerialNumber (status.serialNumber) == deviceID) | |||
| return &status; | |||
| return nullptr; | |||
| return getIndexFromDeviceID (uid) >= 0; | |||
| } | |||
| void notifyBlockIsRestarting (Block::UID deviceID) | |||
| void handleBlockRestarting (Block::UID deviceID) | |||
| { | |||
| forceApiDisconnected (deviceID); | |||
| } | |||
| @@ -164,63 +106,52 @@ struct ConnectedDeviceGroup : private juce::AsyncUpdater, | |||
| { | |||
| lastTopologyReceiveTime = juce::Time::getCurrentTime(); | |||
| if (incomingTopologyDevices.isEmpty()) | |||
| if (incomingTopologyDevices.isEmpty() | |||
| || incomingTopologyConnections.size() < incomingTopologyDevices.size() - 1) | |||
| { | |||
| jassertfalse; | |||
| LOG_CONNECTIVITY ("Invalid topology or device list received."); | |||
| LOG_CONNECTIVITY ("Device size : " << incomingTopologyDevices.size()); | |||
| LOG_CONNECTIVITY ("Connections size : " << incomingTopologyConnections.size()); | |||
| scheduleNewTopologyRequest(); | |||
| return; | |||
| } | |||
| currentTopologyDevices.swapWith (incomingTopologyDevices); | |||
| currentTopologyConnections.swapWith (incomingTopologyConnections); | |||
| incomingTopologyDevices.clearQuick(); | |||
| incomingTopologyConnections.clearQuick(); | |||
| refreshCurrentDeviceInfo(); | |||
| refreshCurrentDeviceConnections(); | |||
| removeUnusedBlocksFromMap (versionNumbers, currentDeviceInfo); | |||
| removeUnusedBlocksFromMap (blockNames, currentDeviceInfo); | |||
| LOG_CONNECTIVITY ("Valid topology received"); | |||
| removePingForDisconnectedBlocks(); | |||
| detector.handleTopologyChange(); | |||
| updateCurrentDeviceList(); | |||
| updateCurrentDeviceConnections(); | |||
| } | |||
| void handleVersion (BlocksProtocol::DeviceVersion version) | |||
| { | |||
| const auto uid = getDeviceIDFromIndex (version.index); | |||
| if (uid == Block::UID() || version.version.length <= 1) | |||
| return; | |||
| setVersion (uid, version.version); | |||
| setVersion (version.index, version.version); | |||
| } | |||
| void handleName (BlocksProtocol::DeviceName name) | |||
| { | |||
| const auto uid = getDeviceIDFromIndex (name.index); | |||
| if (uid == Block::UID() || name.name.length <= 1) | |||
| if (name.name.length <= 1) | |||
| return; | |||
| if (insertOrAssign (blockNames, uid, name.name)) | |||
| if (const auto info = getDeviceInfoFromIndex (name.index)) | |||
| { | |||
| refreshCurrentDeviceInfo(); | |||
| detector.handleTopologyChange(); | |||
| if (info->name == name.name) | |||
| return; | |||
| info->name = name.name; | |||
| detector.handleDeviceUpdated (*info); | |||
| } | |||
| } | |||
| void handleControlButtonUpDown (BlocksProtocol::TopologyIndex deviceIndex, uint32 timestamp, | |||
| BlocksProtocol::ControlButtonID buttonID, bool isDown) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| if (auto deviceID = getDeviceIDFromIndex (deviceIndex)) | |||
| detector.handleButtonChange (deviceID, deviceTimestampToHost (timestamp), buttonID.get(), isDown); | |||
| } | |||
| void handleCustomMessage (BlocksProtocol::TopologyIndex deviceIndex, uint32 timestamp, const int32* data) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| if (auto deviceID = getDeviceIDFromIndex (deviceIndex)) | |||
| detector.handleCustomMessage (deviceID, deviceTimestampToHost (timestamp), data); | |||
| } | |||
| @@ -231,7 +162,7 @@ struct ConnectedDeviceGroup : private juce::AsyncUpdater, | |||
| BlocksProtocol::TouchVelocity velocity, | |||
| bool isStart, bool isEnd) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| if (auto deviceID = getDeviceIDFromIndex (deviceIndex)) | |||
| { | |||
| TouchSurface::Touch touch; | |||
| @@ -267,7 +198,7 @@ struct ConnectedDeviceGroup : private juce::AsyncUpdater, | |||
| void handlePacketACK (BlocksProtocol::TopologyIndex deviceIndex, | |||
| BlocksProtocol::PacketCounter counter) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| if (auto deviceID = getDeviceIDFromIndex (deviceIndex)) | |||
| { | |||
| detector.handleSharedDataACK (deviceID, counter); | |||
| updateApiPing (deviceID); | |||
| @@ -278,7 +209,7 @@ struct ConnectedDeviceGroup : private juce::AsyncUpdater, | |||
| BlocksProtocol::FirmwareUpdateACKCode resultCode, | |||
| BlocksProtocol::FirmwareUpdateACKDetail resultDetail) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| if (auto deviceID = getDeviceIDFromIndex (deviceIndex)) | |||
| { | |||
| detector.handleFirmwareUpdateACK (deviceID, (uint8) resultCode.get(), (uint32) resultDetail.get()); | |||
| updateApiPing (deviceID); | |||
| @@ -288,32 +219,32 @@ struct ConnectedDeviceGroup : private juce::AsyncUpdater, | |||
| void handleConfigUpdateMessage (BlocksProtocol::TopologyIndex deviceIndex, | |||
| int32 item, int32 value, int32 min, int32 max) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| if (auto deviceID = getDeviceIDFromIndex (deviceIndex)) | |||
| detector.handleConfigUpdateMessage (deviceID, item, value, min, max); | |||
| } | |||
| void handleConfigSetMessage (BlocksProtocol::TopologyIndex deviceIndex, | |||
| int32 item, int32 value) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| if (auto deviceID = getDeviceIDFromIndex (deviceIndex)) | |||
| detector.handleConfigSetMessage (deviceID, item, value); | |||
| } | |||
| void handleConfigFactorySyncEndMessage (BlocksProtocol::TopologyIndex deviceIndex) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| if (auto deviceID = getDeviceIDFromIndex (deviceIndex)) | |||
| detector.handleConfigFactorySyncEndMessage (deviceID); | |||
| } | |||
| void handleConfigFactorySyncResetMessage (BlocksProtocol::TopologyIndex deviceIndex) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| if (auto deviceID = getDeviceIDFromIndex (deviceIndex)) | |||
| detector.handleConfigFactorySyncResetMessage (deviceID); | |||
| } | |||
| void handleLogMessage (BlocksProtocol::TopologyIndex deviceIndex, const String& message) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| if (auto deviceID = getDeviceIDFromIndex (deviceIndex)) | |||
| detector.handleLogMessage (deviceID, message); | |||
| } | |||
| @@ -337,17 +268,14 @@ struct ConnectedDeviceGroup : private juce::AsyncUpdater, | |||
| 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); }); | |||
| juce::Array<BlockDeviceConnection> connections; | |||
| for (const auto& connection : currentDeviceConnections) | |||
| if (isApiConnected (getDeviceIDFromIndex (connection.device1)) && isApiConnected (getDeviceIDFromIndex (connection.device2))) | |||
| connections.add (getBlockDeviceConnection (connection)); | |||
| return connections; | |||
| } | |||
| @@ -359,18 +287,14 @@ struct ConnectedDeviceGroup : private juce::AsyncUpdater, | |||
| private: | |||
| //============================================================================== | |||
| juce::Array<DeviceInfo> currentDeviceInfo; | |||
| juce::Array<BlockDeviceConnection> currentDeviceConnections; | |||
| std::unique_ptr<PhysicalTopologySource::DeviceConnection> deviceConnection; | |||
| juce::Array<BlocksProtocol::DeviceStatus> incomingTopologyDevices; | |||
| juce::Array<BlocksProtocol::DeviceConnection> incomingTopologyConnections, currentDeviceConnections; | |||
| juce::Array<BlocksProtocol::DeviceStatus> incomingTopologyDevices, currentTopologyDevices; | |||
| juce::Array<BlocksProtocol::DeviceConnection> incomingTopologyConnections, currentTopologyConnections; | |||
| std::unique_ptr<PhysicalTopologySource::DeviceConnection> deviceConnection; | |||
| juce::CriticalSection incomingPacketLock; | |||
| juce::Array<juce::MemoryBlock> incomingPackets; | |||
| std::map<Block::UID, BlocksProtocol::VersionNumber> versionNumbers; | |||
| std::map<Block::UID, BlocksProtocol::BlockName> blockNames; | |||
| std::unique_ptr<DepreciatedVersionReader> depreciatedVersionReader; | |||
| struct TouchStart { float x, y; }; | |||
| @@ -378,6 +302,21 @@ private: | |||
| Block::UID masterBlock = 0; | |||
| //============================================================================== | |||
| 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(); | |||
| requestMasterBlockVersionIfNeeded(); | |||
| } | |||
| //============================================================================== | |||
| void setMidiMessageCallback() | |||
| { | |||
| @@ -387,12 +326,57 @@ private: | |||
| }; | |||
| } | |||
| 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); | |||
| } | |||
| } | |||
| bool sendCommandMessage (BlocksProtocol::TopologyIndex deviceIndex, uint32 commandID) const | |||
| { | |||
| BlocksProtocol::HostPacketBuilder<64> p; | |||
| p.writePacketSysexHeaderBytes (deviceIndex); | |||
| p.deviceControlMessage (commandID); | |||
| p.writePacketSysexFooter(); | |||
| return sendMessageToDevice (p); | |||
| } | |||
| //============================================================================== | |||
| juce::Time lastTopologyRequestTime, lastTopologyReceiveTime; | |||
| int numTopologyRequestsSent = 0; | |||
| void scheduleNewTopologyRequest() | |||
| { | |||
| LOG_CONNECTIVITY ("Topology Request Scheduled"); | |||
| numTopologyRequestsSent = 0; | |||
| lastTopologyReceiveTime = juce::Time(); | |||
| lastTopologyRequestTime = juce::Time::getCurrentTime(); | |||
| @@ -405,34 +389,11 @@ private: | |||
| 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(); | |||
| requestMasterBlockVersionIfNeeded(); | |||
| } | |||
| 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); | |||
| } | |||
| //============================================================================== | |||
| void requestMasterBlockVersionIfNeeded() | |||
| { | |||
| @@ -442,18 +403,31 @@ private: | |||
| const auto masterVersion = depreciatedVersionReader->getVersionNumber(); | |||
| if (masterVersion.isNotEmpty()) | |||
| setVersion (masterBlock, masterVersion); | |||
| { | |||
| const auto masterIndex = getIndexFromDeviceID (masterBlock); | |||
| if (masterIndex >= 0) | |||
| setVersion (BlocksProtocol::TopologyIndex (masterIndex), masterVersion); | |||
| else | |||
| jassertfalse; | |||
| } | |||
| } | |||
| void setVersion (const Block::UID uid, const BlocksProtocol::VersionNumber versionNumber) | |||
| void setVersion (const BlocksProtocol::TopologyIndex index, const BlocksProtocol::VersionNumber versionNumber) | |||
| { | |||
| if (uid == masterBlock) | |||
| depreciatedVersionReader.reset(); | |||
| if (versionNumber.length <= 1) | |||
| return; | |||
| if (insertOrAssign (versionNumbers, uid, versionNumber)) | |||
| if (const auto info = getDeviceInfoFromIndex (index)) | |||
| { | |||
| refreshCurrentDeviceInfo(); | |||
| detector.handleTopologyChange(); | |||
| if (info->version == versionNumber) | |||
| return; | |||
| if (info->uid == masterBlock) | |||
| depreciatedVersionReader.reset(); | |||
| info->version = versionNumber; | |||
| detector.handleDeviceUpdated (*info); | |||
| } | |||
| } | |||
| @@ -467,6 +441,31 @@ private: | |||
| juce::Array<BlockPingTime> blockPings; | |||
| BlockPingTime* getPing (Block::UID uid) | |||
| { | |||
| for (auto& ping : blockPings) | |||
| if (uid == ping.blockUID) | |||
| return &ping; | |||
| return nullptr; | |||
| } | |||
| void removePing (Block::UID uid) | |||
| { | |||
| const auto remove = [uid] (const BlockPingTime& ping) | |||
| { | |||
| if (uid == ping.blockUID) | |||
| { | |||
| LOG_CONNECTIVITY ("API Disconnected by topology update " << ping.blockUID); | |||
| return true; | |||
| } | |||
| return false; | |||
| }; | |||
| blockPings.removeIf (remove); | |||
| } | |||
| void updateApiPing (Block::UID uid) | |||
| { | |||
| const auto now = juce::Time::getCurrentTime(); | |||
| @@ -480,22 +479,10 @@ private: | |||
| { | |||
| LOG_CONNECTIVITY ("API Connected " << uid); | |||
| blockPings.add ({ uid, now, 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; }); | |||
| if (const auto info = getDeviceInfoFromUID (uid)) | |||
| detector.handleDeviceAdded (*info); | |||
| } | |||
| } | |||
| bool isApiConnected (Block::UID uid) | |||
| @@ -503,55 +490,35 @@ private: | |||
| return getPing (uid) != nullptr; | |||
| } | |||
| void forceApiDisconnected (Block::UID uid) | |||
| 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(); | |||
| blockNames.clear(); | |||
| versionNumbers.clear(); | |||
| detector.handleTopologyChange(); | |||
| scheduleNewTopologyRequest(); | |||
| } | |||
| } | |||
| Array<Block::UID> toRemove; | |||
| void checkApiTimeouts (juce::Time now) | |||
| { | |||
| const auto timedOut = [this, now] (BlockPingTime& ping) | |||
| { | |||
| if (ping.lastPing >= now - juce::RelativeTime::seconds (pingTimeoutSeconds)) | |||
| return false; | |||
| for (const auto& ping : blockPings) | |||
| toRemove.add (ping.blockUID); | |||
| LOG_CONNECTIVITY ("Ping timeout: " << ping.blockUID); | |||
| removeDeviceInfo (ping.blockUID); | |||
| return true; | |||
| }; | |||
| for (const auto& uid : toRemove) | |||
| removeDevice (uid); | |||
| if (blockPings.removeIf (timedOut) > 0) | |||
| { | |||
| scheduleNewTopologyRequest(); | |||
| detector.handleTopologyChange(); | |||
| } | |||
| scheduleNewTopologyRequest(); | |||
| } | |||
| /* Returns true is ping was removed */ | |||
| void removePingForDisconnectedBlocks() | |||
| void checkApiTimeouts (juce::Time now) | |||
| { | |||
| const auto removed = [this] (auto& ping) | |||
| { | |||
| for (auto& info : currentDeviceInfo) | |||
| if (info.uid == ping.blockUID) | |||
| return false; | |||
| Array<Block::UID> toRemove; | |||
| LOG_CONNECTIVITY ("API Disconnected by topology update " << ping.blockUID); | |||
| return true; | |||
| }; | |||
| for (const auto& ping : blockPings) | |||
| { | |||
| if (ping.lastPing < now - juce::RelativeTime::seconds (pingTimeoutSeconds)) | |||
| { | |||
| LOG_CONNECTIVITY ("Ping timeout: " << ping.blockUID); | |||
| toRemove.add (ping.blockUID); | |||
| scheduleNewTopologyRequest(); | |||
| } | |||
| } | |||
| blockPings.removeIf (removed); | |||
| for (const auto& uid : toRemove) | |||
| removeDevice (uid); | |||
| } | |||
| void startApiModeOnConnectedBlocks() | |||
| @@ -568,151 +535,160 @@ private: | |||
| } | |||
| //============================================================================== | |||
| void checkVersionNumberTimeouts() | |||
| Block::UID getDeviceIDFromIndex (BlocksProtocol::TopologyIndex index) noexcept | |||
| { | |||
| for (const auto& device : currentDeviceInfo) | |||
| { | |||
| const auto version = versionNumbers.find (device.uid); | |||
| if (device.index == index) | |||
| return device.uid; | |||
| if (version == versionNumbers.end()) | |||
| { | |||
| auto* ping = getPing (device.uid); | |||
| scheduleNewTopologyRequest(); | |||
| return {}; | |||
| } | |||
| } | |||
| } | |||
| int getIndexFromDeviceID (Block::UID uid) const noexcept | |||
| { | |||
| for (auto& d : currentDeviceInfo) | |||
| if (d.uid == uid) | |||
| return d.index; | |||
| return -1; | |||
| } | |||
| //============================================================================== | |||
| Block::UID getDeviceIDFromIndex (BlocksProtocol::TopologyIndex index) const noexcept | |||
| DeviceInfo* getDeviceInfoFromUID (Block::UID uid) const noexcept | |||
| { | |||
| for (auto& d : currentDeviceInfo) | |||
| if (d.index == index) | |||
| return d.uid; | |||
| if (d.uid == uid) | |||
| return &d; | |||
| return {}; | |||
| return nullptr; | |||
| } | |||
| Block::UID getDeviceIDFromMessageIndex (BlocksProtocol::TopologyIndex index) noexcept | |||
| DeviceInfo* getDeviceInfoFromIndex (BlocksProtocol::TopologyIndex index) const noexcept | |||
| { | |||
| const auto uid = getDeviceIDFromIndex (index); | |||
| for (auto& d : currentDeviceInfo) | |||
| if (d.index == index) | |||
| return &d; | |||
| // re-request topology if we get an event from an unknown block | |||
| if (uid == Block::UID()) | |||
| scheduleNewTopologyRequest(); | |||
| return nullptr; | |||
| } | |||
| return uid; | |||
| void removeDeviceInfo (Block::UID uid) | |||
| { | |||
| currentDeviceInfo.removeIf ([uid] (const DeviceInfo& info) { return info.uid == uid; }); | |||
| } | |||
| //============================================================================== | |||
| void handleIncomingMessage (const void* data, size_t dataSize) | |||
| const DeviceStatus* getIncomingDeviceStatus (BlockSerialNumber serialNumber) const | |||
| { | |||
| juce::MemoryBlock mb (data, dataSize); | |||
| for (auto& device : incomingTopologyDevices) | |||
| if (device.serialNumber == serialNumber) | |||
| return &device; | |||
| { | |||
| const juce::ScopedLock sl (incomingPacketLock); | |||
| incomingPackets.add (std::move (mb)); | |||
| } | |||
| return nullptr; | |||
| } | |||
| triggerAsyncUpdate(); | |||
| //============================================================================== | |||
| void removeDevice (Block::UID uid) | |||
| { | |||
| if (const auto info = getDeviceInfoFromUID (uid)) | |||
| detector.handleDeviceRemoved (*info); | |||
| #if DUMP_BANDWIDTH_STATS | |||
| registerBytesIn ((int) dataSize); | |||
| #endif | |||
| removeDeviceInfo (uid); | |||
| removePing (uid); | |||
| } | |||
| void handleAsyncUpdate() override | |||
| void updateCurrentDeviceList() | |||
| { | |||
| juce::Array<juce::MemoryBlock> packets; | |||
| packets.ensureStorageAllocated (32); | |||
| Array<Block::UID> toRemove; | |||
| //Update known devices | |||
| for (int i = currentDeviceInfo.size(); --i >= 0; ) | |||
| { | |||
| const juce::ScopedLock sl (incomingPacketLock); | |||
| incomingPackets.swapWith (packets); | |||
| } | |||
| auto& currentDevice = currentDeviceInfo.getReference (i); | |||
| for (auto& packet : packets) | |||
| { | |||
| auto data = static_cast<const uint8*> (packet.getData()); | |||
| if (const auto newStatus = getIncomingDeviceStatus (currentDevice.serial)) | |||
| { | |||
| if (currentDevice.index != newStatus->index) | |||
| { | |||
| currentDevice.index = newStatus->index; | |||
| detector.handleIndexChanged (currentDevice.uid, currentDevice.index); | |||
| } | |||
| BlocksProtocol::HostPacketDecoder<ConnectedDeviceGroup> | |||
| ::processNextPacket (*this, *data, data + 1, (int) packet.getSize() - 1); | |||
| } | |||
| } | |||
| if (currentDevice.batteryCharging != newStatus->batteryCharging) | |||
| { | |||
| currentDevice.batteryCharging = newStatus->batteryCharging; | |||
| detector.handleBatteryChargingChanged (currentDevice.uid, currentDevice.batteryCharging); | |||
| } | |||
| //============================================================================== | |||
| BlocksProtocol::VersionNumber getVersionNumber (Block::UID uid) | |||
| { | |||
| const auto version = versionNumbers.find (uid); | |||
| return version == versionNumbers.end() ? BlocksProtocol::VersionNumber() : version->second; | |||
| } | |||
| if (currentDevice.batteryLevel != newStatus->batteryLevel) | |||
| { | |||
| currentDevice.batteryLevel = newStatus->batteryLevel; | |||
| detector.handleBatteryLevelChanged (currentDevice.uid, currentDevice.batteryLevel); | |||
| } | |||
| } | |||
| else | |||
| { | |||
| toRemove.add (currentDevice.uid); | |||
| } | |||
| } | |||
| BlocksProtocol::BlockName getName (Block::UID uid) | |||
| { | |||
| const auto name = blockNames.find (uid); | |||
| return name == blockNames.end() ? BlocksProtocol::BlockName() : name->second; | |||
| } | |||
| for (const auto& uid : toRemove) | |||
| removeDevice (uid); | |||
| //============================================================================== | |||
| const DeviceInfo* getDeviceInfoFromUID (Block::UID uid) const noexcept | |||
| { | |||
| for (auto& d : currentDeviceInfo) | |||
| if (d.uid == uid) | |||
| return &d; | |||
| //Add new devices | |||
| for (const auto& device : incomingTopologyDevices) | |||
| { | |||
| const auto uid = getBlockUIDFromSerialNumber (device.serialNumber); | |||
| return nullptr; | |||
| if (! getDeviceInfoFromUID (uid)) | |||
| { | |||
| // For backwards compatibility we assume the first device we see in a group is the master and won't change | |||
| if (masterBlock == 0) | |||
| masterBlock = uid; | |||
| currentDeviceInfo.add ({ uid, | |||
| device.index, | |||
| device.serialNumber, | |||
| BlocksProtocol::VersionNumber(), | |||
| BlocksProtocol::BlockName(), | |||
| device.batteryLevel, | |||
| device.batteryCharging, | |||
| masterBlock }); | |||
| } | |||
| } | |||
| } | |||
| //============================================================================== | |||
| Block::ConnectionPort convertConnectionPort (Block::UID uid, BlocksProtocol::ConnectorPort p) noexcept | |||
| { | |||
| if (auto* info = getDeviceInfoFromUID (uid)) | |||
| return BlocksProtocol::BlockDataSheet (info->serial).convertPortIndexToConnectorPort (p); | |||
| jassertfalse; | |||
| jassertfalse; | |||
| return { Block::ConnectionPort::DeviceEdge::north, 0 }; | |||
| } | |||
| //============================================================================== | |||
| void refreshCurrentDeviceInfo() | |||
| BlockDeviceConnection getBlockDeviceConnection (const BlocksProtocol::DeviceConnection& connection) | |||
| { | |||
| currentDeviceInfo.clearQuick(); | |||
| BlockDeviceConnection dc; | |||
| for (auto& device : currentTopologyDevices) | |||
| { | |||
| const auto uid = getBlockUIDFromSerialNumber (device.serialNumber); | |||
| const auto version = getVersionNumber (uid); | |||
| const auto name = getName (uid); | |||
| dc.device1 = getDeviceIDFromIndex (connection.device1); | |||
| dc.device2 = getDeviceIDFromIndex (connection.device2); | |||
| // For backwards compatibility we assume the first device we see in a group is the master and won't change | |||
| if (masterBlock == 0) | |||
| masterBlock = uid; | |||
| if (dc.device1 <= 0 || dc.device2 <= 0) | |||
| jassertfalse; | |||
| currentDeviceInfo.add ({ uid, | |||
| device.index, | |||
| device.serialNumber, | |||
| version, | |||
| name, | |||
| masterBlock == uid }); | |||
| } | |||
| dc.connectionPortOnDevice1 = convertConnectionPort (dc.device1, connection.port1); | |||
| dc.connectionPortOnDevice2 = convertConnectionPort (dc.device2, connection.port2); | |||
| return dc; | |||
| } | |||
| void refreshCurrentDeviceConnections() | |||
| void updateCurrentDeviceConnections() | |||
| { | |||
| currentDeviceConnections.clearQuick(); | |||
| currentDeviceConnections.swapWith (incomingTopologyConnections); | |||
| for (auto&& c : currentTopologyConnections) | |||
| { | |||
| 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); | |||
| currentDeviceConnections.add (dc); | |||
| } | |||
| detector.handleConnectionsChanged(); | |||
| } | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConnectedDeviceGroup) | |||
| @@ -55,8 +55,8 @@ public: | |||
| for (size_t i = 1; i < numFirmwareApps; ++i) | |||
| { | |||
| const BlocksVersion highest { asString (highestVersion) }; | |||
| const BlocksVersion test { asString ( result[i]) }; | |||
| const BlocksVersion highest { highestVersion.asString() }; | |||
| const BlocksVersion test { result[i].asString() }; | |||
| if (highest < test) | |||
| highestVersion = result[i]; | |||
| @@ -80,10 +80,21 @@ private: | |||
| { | |||
| static constexpr size_t requestSize { 8 }; | |||
| static constexpr uint8 requests[][requestSize] = {{ 0xf0, 0x00, 0x21, 0x10, 0x47, 0x03, 0x00, 0xf7 }, // Main App | |||
| { 0xf0, 0x00, 0x21, 0x10, 0x47, 0x03, 0x01, 0xf7 }, // Stm32 | |||
| { 0xf0, 0x00, 0x21, 0x10, 0x47, 0x03, 0x03, 0xf7 }}; // Bootloader | |||
| { 0xf0, 0x00, 0x21, 0x10, 0x47, 0x03, 0x03, 0xf7 }, // Bootloader | |||
| { 0xf0, 0x00, 0x21, 0x10, 0x47, 0x03, 0x01, 0xf7 }}; // Stm32 | |||
| deviceConnection.sendMessageToDevice (&requests[currentRequest.get()][0], requestSize); | |||
| static const BlocksVersion depreciatedVersion ("0.3.0"); | |||
| if (currentRequest.get() == numFirmwareApps - 1 | |||
| && (BlocksVersion (result[0].asString()) >= depreciatedVersion | |||
| || BlocksVersion (result[1].asString()) >= depreciatedVersion)) | |||
| { | |||
| stopTimer(); | |||
| } | |||
| else | |||
| { | |||
| deviceConnection.sendMessageToDevice (&requests[currentRequest.get()][0], requestSize); | |||
| } | |||
| } | |||
| //============================================================================== | |||
| @@ -23,40 +23,11 @@ | |||
| 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 = asString (device.version); | |||
| return deviceVersion != version && deviceVersion.isNotEmpty(); | |||
| } | |||
| static void setVersionNumberForBlock (const DeviceInfo& deviceInfo, Block& block) noexcept | |||
| { | |||
| jassert (deviceInfo.uid == block.uid); | |||
| block.versionNumber = asString (deviceInfo.version); | |||
| } | |||
| static void setNameForBlock (const DeviceInfo& deviceInfo, Block& block) | |||
| { | |||
| jassert (deviceInfo.uid == block.uid); | |||
| block.name = asString (deviceInfo.name); | |||
| } | |||
| } | |||
| //============================================================================== | |||
| /** This is the main singleton object that keeps track of connected blocks */ | |||
| struct Detector : public juce::ReferenceCountedObject, | |||
| private juce::Timer | |||
| private juce::Timer, | |||
| private juce::AsyncUpdater | |||
| { | |||
| using BlockImpl = BlockImplementation<Detector>; | |||
| @@ -100,11 +71,10 @@ struct Detector : public juce::ReferenceCountedObject, | |||
| if (activeTopologySources.isEmpty()) | |||
| { | |||
| for (auto& b : currentTopology.blocks) | |||
| if (auto bi = BlockImpl::getFrom (*b)) | |||
| if (auto bi = BlockImpl::getFrom (b)) | |||
| bi->sendCommandMessage (BlocksProtocol::endAPIMode); | |||
| currentTopology = {}; | |||
| lastTopology = {}; | |||
| auto& d = getDefaultDetectorPointer(); | |||
| @@ -124,76 +94,127 @@ struct Detector : public juce::ReferenceCountedObject, | |||
| return false; | |||
| } | |||
| const BlocksProtocol::DeviceStatus* getLastStatus (Block::UID deviceID) const noexcept | |||
| void handleDeviceAdded (const DeviceInfo& info) | |||
| { | |||
| for (auto d : connectedDeviceGroups) | |||
| if (auto status = d->getLastStatus (deviceID)) | |||
| return status; | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| return nullptr; | |||
| } | |||
| const auto blockWasRemoved = containsBlockWithUID (blocksToRemove, info.uid); | |||
| const auto knownBlock = std::find_if (previouslySeenBlocks.begin(), previouslySeenBlocks.end(), | |||
| [uid = info.uid] (Block::Ptr block) { return uid == block->uid; }); | |||
| void handleTopologyChange() | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| Block::Ptr block; | |||
| if (knownBlock != previouslySeenBlocks.end()) | |||
| { | |||
| juce::Array<DeviceInfo> newDeviceInfo; | |||
| juce::Array<BlockDeviceConnection> newDeviceConnections; | |||
| block = *knownBlock; | |||
| for (auto d : connectedDeviceGroups) | |||
| if (auto* blockImpl = BlockImpl::getFrom (*block)) | |||
| { | |||
| newDeviceInfo.addArray (d->getCurrentDeviceInfo()); | |||
| newDeviceConnections.addArray (d->getCurrentDeviceConnections()); | |||
| blockImpl->markReconnected (info); | |||
| previouslySeenBlocks.removeObject (block); | |||
| } | |||
| } | |||
| else | |||
| { | |||
| block = new BlockImpl (*this, info); | |||
| } | |||
| for (int i = currentTopology.blocks.size(); --i >= 0;) | |||
| { | |||
| auto currentBlock = currentTopology.blocks.getUnchecked (i); | |||
| currentTopology.blocks.addIfNotAlreadyThere (block); | |||
| auto newDeviceIter = std::find_if (newDeviceInfo.begin(), newDeviceInfo.end(), | |||
| [&] (DeviceInfo& info) { return info.uid == currentBlock->uid; }); | |||
| if (blockWasRemoved) | |||
| { | |||
| blocksToUpdate.addIfNotAlreadyThere (block); | |||
| blocksToAdd.removeObject (block); | |||
| } | |||
| else | |||
| { | |||
| blocksToAdd.addIfNotAlreadyThere (block); | |||
| blocksToUpdate.removeObject (block); | |||
| } | |||
| auto* blockImpl = BlockImpl::getFrom (*currentBlock); | |||
| blocksToRemove.removeObject (block); | |||
| if (newDeviceIter == newDeviceInfo.end()) | |||
| { | |||
| if (blockImpl != nullptr) | |||
| blockImpl->markDisconnected(); | |||
| triggerAsyncUpdate(); | |||
| } | |||
| disconnectedBlocks.addIfNotAlreadyThere (currentTopology.blocks.removeAndReturn (i).get()); | |||
| } | |||
| else | |||
| { | |||
| if (blockImpl != nullptr && blockImpl->wasPowerCycled()) | |||
| { | |||
| blockImpl->resetPowerCycleFlag(); | |||
| blockImpl->markReconnected (*newDeviceIter); | |||
| } | |||
| void handleDeviceRemoved (const DeviceInfo& info) | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| updateCurrentBlockInfo (currentBlock, *newDeviceIter); | |||
| } | |||
| } | |||
| const auto blockIt = std::find_if (currentTopology.blocks.begin(), currentTopology.blocks.end(), | |||
| [uid = info.uid] (Block::Ptr block) { return uid == block->uid; }); | |||
| if (blockIt != currentTopology.blocks.end()) | |||
| { | |||
| const auto block = *blockIt; | |||
| if (auto blockImpl = BlockImpl::getFrom (block)) | |||
| blockImpl->markDisconnected(); | |||
| currentTopology.blocks.removeObject (block); | |||
| previouslySeenBlocks.addIfNotAlreadyThere (block); | |||
| blocksToRemove.addIfNotAlreadyThere (block); | |||
| blocksToUpdate.removeObject (block); | |||
| blocksToAdd.removeObject (block); | |||
| triggerAsyncUpdate(); | |||
| } | |||
| } | |||
| static const int maxBlocksToSave = 100; | |||
| void handleConnectionsChanged() | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| triggerAsyncUpdate(); | |||
| } | |||
| void handleDeviceUpdated (const DeviceInfo& info) | |||
| { | |||
| if (containsBlockWithUID (blocksToRemove, info.uid)) | |||
| return; | |||
| if (disconnectedBlocks.size() > maxBlocksToSave) | |||
| disconnectedBlocks.removeRange (0, 2 * (disconnectedBlocks.size() - maxBlocksToSave)); | |||
| const auto blockIt = std::find_if (currentTopology.blocks.begin(), currentTopology.blocks.end(), | |||
| [uid = info.uid] (Block::Ptr block) { return uid == block->uid; }); | |||
| for (auto& info : newDeviceInfo) | |||
| if (info.serial.isValid() && ! containsBlockWithUID (currentTopology.blocks, getBlockUIDFromSerialNumber (info.serial))) | |||
| addBlock (info); | |||
| if (blockIt != currentTopology.blocks.end()) | |||
| { | |||
| const auto block = *blockIt; | |||
| if (auto blockImpl = BlockImpl::getFrom (block)) | |||
| blockImpl->markReconnected (info); | |||
| currentTopology.connections.swapWith (newDeviceConnections); | |||
| if (! containsBlockWithUID (blocksToAdd, info.uid)) | |||
| { | |||
| blocksToUpdate.addIfNotAlreadyThere (block); | |||
| triggerAsyncUpdate(); | |||
| } | |||
| } | |||
| } | |||
| void handleBatteryChargingChanged (Block::UID deviceID, const BlocksProtocol::BatteryCharging isCharging) | |||
| { | |||
| if (auto block = currentTopology.getBlockWithUID (deviceID)) | |||
| if (auto blockImpl = BlockImpl::getFrom (*block)) | |||
| blockImpl->batteryCharging = isCharging; | |||
| } | |||
| void handleBatteryLevelChanged (Block::UID deviceID, const BlocksProtocol::BatteryLevel batteryLevel) | |||
| { | |||
| if (auto block = currentTopology.getBlockWithUID (deviceID)) | |||
| if (auto blockImpl = BlockImpl::getFrom (*block)) | |||
| blockImpl->batteryLevel = batteryLevel; | |||
| } | |||
| broadcastTopology(); | |||
| void handleIndexChanged (Block::UID deviceID, const BlocksProtocol::TopologyIndex index) | |||
| { | |||
| if (auto block = currentTopology.getBlockWithUID (deviceID)) | |||
| if (auto blockImpl = BlockImpl::getFrom (*block)) | |||
| blockImpl->topologyIndex = index; | |||
| } | |||
| void notifyBlockIsRestarting (Block::UID deviceID) | |||
| { | |||
| for (auto& group : connectedDeviceGroups) | |||
| group->notifyBlockIsRestarting (deviceID); | |||
| group->handleBlockRestarting (deviceID); | |||
| } | |||
| void handleSharedDataACK (Block::UID deviceID, uint32 packetCounter) const | |||
| @@ -305,24 +326,11 @@ struct Detector : public juce::ReferenceCountedObject, | |||
| } | |||
| //============================================================================== | |||
| 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) | |||
| if (c->contains (deviceID)) | |||
| return c->sendMessageToDevice (builder); | |||
| return false; | |||
| @@ -341,11 +349,8 @@ struct Detector : public juce::ReferenceCountedObject, | |||
| { | |||
| for (const auto& d : connectedDeviceGroups) | |||
| { | |||
| for (const auto& info : d->getCurrentDeviceInfo()) | |||
| { | |||
| if (info.uid == b.uid) | |||
| return d->getDeviceConnection(); | |||
| } | |||
| if (d->contains (b.uid)) | |||
| return d->getDeviceConnection(); | |||
| } | |||
| return nullptr; | |||
| @@ -355,11 +360,8 @@ struct Detector : public juce::ReferenceCountedObject, | |||
| { | |||
| for (const auto& d : connectedDeviceGroups) | |||
| { | |||
| for (const auto& info : d->getCurrentDeviceInfo()) | |||
| { | |||
| if (info.uid == b.uid) | |||
| return d->getDeviceConnection(); | |||
| } | |||
| if (d->contains (b.uid)) | |||
| return d->getDeviceConnection(); | |||
| } | |||
| return nullptr; | |||
| @@ -370,10 +372,11 @@ struct Detector : public juce::ReferenceCountedObject, | |||
| juce::Array<PhysicalTopologySource*> activeTopologySources; | |||
| BlockTopology currentTopology, lastTopology; | |||
| juce::ReferenceCountedArray<Block, CriticalSection> disconnectedBlocks; | |||
| BlockTopology currentTopology; | |||
| private: | |||
| Block::Array previouslySeenBlocks, blocksToAdd, blocksToRemove, blocksToUpdate; | |||
| void timerCallback() override | |||
| { | |||
| startTimer (1500); | |||
| @@ -384,21 +387,20 @@ private: | |||
| handleDevicesAdded (detectedDevices); | |||
| } | |||
| void handleDevicesRemoved (const juce::StringArray& detectedDevices) | |||
| bool containsBlockWithUID (const juce::Block::Array& blocks, juce::Block::UID uid) | |||
| { | |||
| bool anyDevicesRemoved = false; | |||
| for (const auto block : blocks) | |||
| if (block->uid == uid) | |||
| return true; | |||
| return false; | |||
| } | |||
| void handleDevicesRemoved (const juce::StringArray& detectedDevices) | |||
| { | |||
| 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) | |||
| @@ -424,57 +426,9 @@ private: | |||
| 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.isNotEmpty()) | |||
| 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)) | |||
| if (auto block = currentTopology.getBlockWithUID (deviceID)) | |||
| return BlockImpl::getFrom (*block); | |||
| return nullptr; | |||
| @@ -484,31 +438,41 @@ private: | |||
| //============================================================================== | |||
| /** This is a friend of the BlocksImplementation that will scan and set the | |||
| physical positions of the blocks */ | |||
| struct BlocksTraverser | |||
| physical positions of the blocks. | |||
| Returns an array of blocks that were updated. | |||
| */ | |||
| struct BlocksLayoutTraverser | |||
| { | |||
| void traverseBlockArray (const BlockTopology& topology) | |||
| static Block::Array updateBlocks (const BlockTopology& topology) | |||
| { | |||
| Block::Array updated; | |||
| juce::Array<Block::UID> visited; | |||
| for (auto& block : topology.blocks) | |||
| { | |||
| if (block->isMasterBlock() && ! visited.contains (block->uid)) | |||
| { | |||
| if (auto* bi = dynamic_cast<BlockImpl*> (block)) | |||
| if (auto* bi = BlockImpl::getFrom (block)) | |||
| { | |||
| bi->masterUID = {}; | |||
| bi->position = {}; | |||
| bi->rotation = 0; | |||
| if (bi->rotation != 0 || bi->position.first != 0 || bi->position.second != 0) | |||
| { | |||
| bi->rotation = 0; | |||
| bi->position = {}; | |||
| updated.add (block); | |||
| } | |||
| } | |||
| layoutNeighbours (*block, topology, block->uid, visited); | |||
| layoutNeighbours (*block, topology, visited, updated); | |||
| } | |||
| } | |||
| return updated; | |||
| } | |||
| private: | |||
| // returns the distance from corner clockwise | |||
| int getUnitForIndex (Block::Ptr block, Block::ConnectionPort::DeviceEdge edge, int index) | |||
| static int getUnitForIndex (Block::Ptr block, Block::ConnectionPort::DeviceEdge edge, int index) | |||
| { | |||
| if (block->getType() == Block::seaboardBlock) | |||
| { | |||
| @@ -533,7 +497,7 @@ private: | |||
| } | |||
| // returns how often north needs to rotate by 90 degrees | |||
| int getRotationForEdge (Block::ConnectionPort::DeviceEdge edge) | |||
| static int getRotationForEdge (Block::ConnectionPort::DeviceEdge edge) | |||
| { | |||
| switch (edge) | |||
| { | |||
| @@ -547,8 +511,10 @@ private: | |||
| return 0; | |||
| } | |||
| void layoutNeighbours (Block::Ptr block, const BlockTopology& topology, | |||
| Block::UID masterUid, juce::Array<Block::UID>& visited) | |||
| static void layoutNeighbours (const Block::Ptr block, | |||
| const BlockTopology& topology, | |||
| juce::Array<Block::UID>& visited, | |||
| Block::Array& updated) | |||
| { | |||
| visited.add (block->uid); | |||
| @@ -568,10 +534,17 @@ private: | |||
| 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; | |||
| { | |||
| const auto neighbourRotation = (2 + block->getRotation() | |||
| + getRotationForEdge (myPort.edge) | |||
| - getRotationForEdge (theirPort.edge)) % 4; | |||
| if (neighbour->rotation != neighbourRotation) | |||
| { | |||
| neighbour->rotation = neighbourRotation; | |||
| updated.addIfNotAlreadyThere (neighbourPtr); | |||
| } | |||
| } | |||
| std::pair<int, int> delta; | |||
| const auto theirBounds = neighbour->getBlockAreaWithinLayout(); | |||
| @@ -592,10 +565,22 @@ private: | |||
| break; | |||
| } | |||
| neighbour->position = { myBounds.x + delta.first, myBounds.y + delta.second }; | |||
| } | |||
| { | |||
| const auto neighbourX = myBounds.x + delta.first; | |||
| const auto neighbourY = myBounds.y + delta.second; | |||
| layoutNeighbours (neighbourPtr, topology, masterUid, visited); | |||
| if (neighbour->position.first != neighbourX | |||
| || neighbour->position.second != neighbourY) | |||
| { | |||
| neighbour->position.first = neighbourX; | |||
| neighbour->position.second = neighbourY; | |||
| updated.addIfNotAlreadyThere (neighbourPtr); | |||
| } | |||
| } | |||
| layoutNeighbours (neighbourPtr, topology, visited, updated); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -674,22 +659,64 @@ private: | |||
| #endif | |||
| //============================================================================== | |||
| void broadcastTopology() | |||
| void updateBlockPositions() | |||
| { | |||
| if (currentTopology != lastTopology) | |||
| const auto updated = BlocksLayoutTraverser::updateBlocks (currentTopology); | |||
| for (const auto block : updated) | |||
| { | |||
| lastTopology = currentTopology; | |||
| if (containsBlockWithUID (blocksToAdd, block->uid) || containsBlockWithUID (blocksToRemove, block->uid)) | |||
| continue; | |||
| BlocksTraverser traverser; | |||
| traverser.traverseBlockArray (currentTopology); | |||
| blocksToUpdate.addIfNotAlreadyThere (block); | |||
| } | |||
| } | |||
| for (auto* d : activeTopologySources) | |||
| d->listeners.call ([] (TopologySource::Listener& l) { l.topologyChanged(); }); | |||
| void updateBlockConnections() | |||
| { | |||
| currentTopology.connections.clearQuick(); | |||
| for (auto d : connectedDeviceGroups) | |||
| currentTopology.connections.addArray (d->getCurrentDeviceConnections()); | |||
| } | |||
| void handleAsyncUpdate() override | |||
| { | |||
| updateBlockConnections(); | |||
| updateBlockPositions(); | |||
| for (auto* d : activeTopologySources) | |||
| { | |||
| for (const auto block : blocksToAdd) | |||
| d->listeners.call ([&block] (TopologySource::Listener& l) { l.blockAdded (block); }); | |||
| for (const auto block : blocksToRemove) | |||
| d->listeners.call ([&block] (TopologySource::Listener& l) { l.blockRemoved (block); }); | |||
| for (const auto block : blocksToUpdate) | |||
| d->listeners.call ([&block] (TopologySource::Listener& l) { l.blockUpdated (block); }); | |||
| } | |||
| const auto topologyChanged = blocksToAdd.size() > 0 || blocksToRemove.size() > 0 || blocksToUpdate.size() > 0; | |||
| if (topologyChanged) | |||
| { | |||
| #if DUMP_TOPOLOGY | |||
| dumpTopology (lastTopology); | |||
| dumpTopology (currentTopology); | |||
| #endif | |||
| for (auto* d : activeTopologySources) | |||
| d->listeners.call ([] (TopologySource::Listener& l) { l.topologyChanged(); }); | |||
| } | |||
| blocksToUpdate.clear(); | |||
| blocksToAdd.clear(); | |||
| blocksToRemove.clear(); | |||
| static const int maxBlocksToSave = 100; | |||
| if (previouslySeenBlocks.size() > maxBlocksToSave) | |||
| previouslySeenBlocks.removeRange (0, 2 * (previouslySeenBlocks.size() - maxBlocksToSave)); | |||
| } | |||
| //============================================================================== | |||
| @@ -26,9 +26,13 @@ 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) | |||
| DeviceInfo (Block::UID buid, BlocksProtocol::TopologyIndex tidx, | |||
| BlocksProtocol::BlockSerialNumber s, BlocksProtocol::VersionNumber v, | |||
| BlocksProtocol::BlockName n, BlocksProtocol::BatteryLevel level, | |||
| BlocksProtocol::BatteryCharging charging, Block::UID master) | |||
| : uid (buid), index (tidx), serial (s), version (v), name (n), | |||
| batteryLevel (level), batteryCharging (charging), masterUid (master), | |||
| isMaster (uid == master) | |||
| { | |||
| } | |||
| @@ -37,6 +41,9 @@ struct DeviceInfo | |||
| BlocksProtocol::BlockSerialNumber serial; | |||
| BlocksProtocol::VersionNumber version; | |||
| BlocksProtocol::BlockName name; | |||
| BlocksProtocol::BatteryLevel batteryLevel; | |||
| BlocksProtocol::BatteryCharging batteryCharging; | |||
| Block::UID masterUid; | |||
| bool isMaster {}; | |||
| }; | |||
| @@ -47,17 +47,6 @@ | |||
| #include "internal/juce_BandwidthStatsLogger.cpp" | |||
| #endif | |||
| namespace | |||
| { | |||
| /** Helper function to create juce::String from BlockStringData */ | |||
| template <size_t MaxSize> | |||
| juce::String asString (juce::BlocksProtocol::BlockStringData<MaxSize> blockString) | |||
| { | |||
| return { reinterpret_cast<const char*> (blockString.data), | |||
| juce::jmin (sizeof (blockString.data), static_cast<size_t> (blockString.length))}; | |||
| } | |||
| } | |||
| #include "internal/juce_MidiDeviceConnection.cpp" | |||
| #include "internal/juce_MIDIDeviceDetector.cpp" | |||
| #include "internal/juce_DeviceInfo.cpp" | |||
| @@ -48,7 +48,20 @@ public: | |||
| struct Listener | |||
| { | |||
| virtual ~Listener() = default; | |||
| virtual void topologyChanged() = 0; | |||
| /** Called for any change in topology - devices changed, connections changed, etc. */ | |||
| virtual void topologyChanged() {} | |||
| /** Called when a new block is added to the topology. */ | |||
| virtual void blockAdded (const Block::Ptr) {} | |||
| /** Called when a block is removed from the topology. */ | |||
| virtual void blockRemoved (const Block::Ptr) {} | |||
| /** Called when a known block is updated. | |||
| This could be becasue details have been reveived asyncroniously. E.g. Block name. | |||
| */ | |||
| virtual void blockUpdated (const Block::Ptr) {} | |||
| }; | |||
| void addListener (Listener* l) { listeners.add (l); } | |||