/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2016 - ROLI Ltd. Permission is granted to use this software 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. THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ----------------------------------------------------------------------------- To release a closed-source product which uses other parts of JUCE not licensed under the ISC terms, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ #define JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED \ jassert (juce::MessageManager::getInstance()->currentThreadHasLockedMessageManager()); #if DUMP_BANDWIDTH_STATS 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(); } #endif //============================================================================== struct PhysicalTopologySource::Internal { struct Detector; struct BlockImplementation; struct ControlButtonImplementation; struct RotaryDialImplementation; struct TouchSurfaceImplementation; struct LEDGridImplementation; struct LEDRowImplementation; //============================================================================== struct MIDIDeviceConnection : public DeviceConnection, public juce::MidiInputCallback { MIDIDeviceConnection() {} ~MIDIDeviceConnection() { JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED listeners.call (&Listener::connectionBeingDeleted, *this); midiInput->stop(); } 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 (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::handleIncomingMidiMessage, message); } std::unique_ptr midiInput; std::unique_ptr midiOutput; private: juce::ListenerList listeners; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MIDIDeviceConnection) }; struct MIDIDeviceDetector : public DeviceDetector { MIDIDeviceDetector() {} juce::StringArray scanForDevices() override { juce::StringArray result; for (auto& pair : findDevices()) result.add (pair.inputName + " & " + pair.outputName); return result; } DeviceConnection* openDevice (int index) override { auto pair = findDevices()[index]; if (pair.inputIndex >= 0 && pair.outputIndex >= 0) { std::unique_ptr dev (new MIDIDeviceConnection()); 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(); } } return nullptr; } static bool isBlocksMidiDeviceName (const juce::String& name) { return name.indexOf (" BLOCK") > 0 || name.indexOf (" Block") > 0; } struct MidiInputOutputPair { juce::String outputName, inputName; int outputIndex = -1, inputIndex = -1; }; static juce::Array findDevices() { juce::Array 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; for (int i = 0; i < midiOutputs.size(); ++i) { if (midiOutputs[i].trim() == pair.inputName.trim()) { pair.outputName = midiOutputs[i]; pair.outputIndex = i; break; } } result.add (pair); } } return result; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MIDIDeviceDetector) }; //============================================================================== struct DeviceInfo { Block::UID uid; BlocksProtocol::TopologyIndex index; BlocksProtocol::BlockSerialNumber serial; bool isMaster; }; static Block::Timestamp deviceTimestampToHost (uint32 timestamp) noexcept { return static_cast (timestamp); } static juce::Array getArrayOfDeviceInfo (const juce::Array& devices) { juce::Array result; bool isFirst = true; for (auto& device : devices) { result.add ({ getBlockUIDFromSerialNumber (device.serialNumber), device.index, device.serialNumber, isFirst }); isFirst = false; } return result; } static bool containsBlockWithUID (const juce::Array& devices, Block::UID uid) noexcept { for (auto&& d : devices) if (d.uid == uid) return true; return false; } static bool containsBlockWithUID (const Block::Array& blocks, Block::UID uid) noexcept { for (auto&& block : blocks) if (block->uid == uid) return true; return false; } //============================================================================== struct ConnectedDeviceGroup : private juce::AsyncUpdater, private juce::Timer { ConnectedDeviceGroup (Detector& d, const juce::String& name, DeviceConnection* connection) : detector (d), deviceName (name), deviceConnection (connection) { lastGlobalPingTime = juce::Time::getCurrentTime(); 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() && lastGlobalPingTime > juce::Time::getCurrentTime() - juce::RelativeTime::seconds (pingTimeoutSeconds); } Block::UID getDeviceIDFromIndex (BlocksProtocol::TopologyIndex index) const noexcept { for (auto& d : currentDeviceInfo) if (d.index == index) return d.uid; return {}; } int getIndexFromDeviceID (Block::UID uid) const noexcept { for (auto& d : currentDeviceInfo) if (d.uid == uid) return d.index; return -1; } 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; } //============================================================================== juce::Time lastTopologyRequestTime, lastTopologyReceiveTime; int numTopologyRequestsSent = 0; void sendTopologyRequest() { ++numTopologyRequestsSent; lastTopologyRequestTime = juce::Time::getCurrentTime(); sendCommandMessage (0, BlocksProtocol::requestTopologyMessage); } void scheduleNewTopologyRequest() { numTopologyRequestsSent = 0; lastTopologyReceiveTime = juce::Time(); } bool failedToGetTopology() const noexcept { return numTopologyRequestsSent > 4 && lastTopologyReceiveTime == juce::Time(); } bool hasAnyBlockStoppedPinging() const noexcept { auto now = juce::Time::getCurrentTime(); for (auto& ping : blockPings) if (ping.lastPing < now - juce::RelativeTime::seconds (pingTimeoutSeconds)) return true; return false; } void timerCallback() override { auto now = juce::Time::getCurrentTime(); if ((now > lastTopologyReceiveTime + juce::RelativeTime::seconds (30.0) || hasAnyBlockStoppedPinging()) && now > lastTopologyRequestTime + juce::RelativeTime::seconds (1.0) && numTopologyRequestsSent < 4) sendTopologyRequest(); } //============================================================================== // The following methods will be called by the DeviceToHostPacketDecoder: void beginTopology (int numDevices, int numConnections) { incomingTopologyDevices.clearQuick(); incomingTopologyDevices.ensureStorageAllocated (numDevices); incomingTopologyConnections.clearQuick(); incomingTopologyConnections.ensureStorageAllocated (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; currentTopologyConnections = incomingTopologyConnections; detector.handleTopologyChange(); lastTopologyReceiveTime = juce::Time::getCurrentTime(); blockPings.clear(); } 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 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); } //============================================================================== template 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; } bool sendCommandMessage (BlocksProtocol::TopologyIndex deviceIndex, uint32 commandID) const { BlocksProtocol::HostPacketBuilder<64> p; p.writePacketSysexHeaderBytes (deviceIndex); p.deviceControlMessage (commandID); p.writePacketSysexFooter(); return sendMessageToDevice (p); } bool broadcastCommandMessage (uint32 commandID) const { return sendCommandMessage (BlocksProtocol::topologyIndexForBroadcast, commandID); } DeviceConnection* getDeviceConnection() { return deviceConnection.get(); } Detector& detector; juce::String deviceName; juce::Array currentDeviceInfo; juce::Array currentDeviceConnections; static constexpr double pingTimeoutSeconds = 6.0; private: //============================================================================== std::unique_ptr deviceConnection; juce::Array incomingTopologyDevices, currentTopologyDevices; juce::Array incomingTopologyConnections, currentTopologyConnections; juce::CriticalSection incomingPacketLock; juce::Array incomingPackets; struct TouchStart { float x, y; }; TouchList touchStartPositions; juce::Time lastGlobalPingTime; struct BlockPingTime { Block::UID blockUID; juce::Time lastPing; }; juce::Array blockPings; Block::UID getDeviceIDFromMessageIndex (BlocksProtocol::TopologyIndex index) noexcept { auto uid = getDeviceIDFromIndex (index); if (uid == Block::UID()) { scheduleNewTopologyRequest(); // force a re-request of the topology when we // get an event from a block that we don't know about } else { auto now = juce::Time::getCurrentTime(); for (auto& ping : blockPings) { if (ping.blockUID == uid) { ping.lastPing = now; return uid; } } blockPings.add ({ uid, now }); } return uid; } juce::Array getArrayOfConnections (const juce::Array& connections) { juce::Array result; for (auto&& c : connections) { BlockDeviceConnection dc; dc.device1 = getDeviceIDFromIndex (c.device1); dc.device2 = getDeviceIDFromIndex (c.device2); 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 packets; packets.ensureStorageAllocated (32); { const juce::ScopedLock sl (incomingPacketLock); incomingPackets.swapWith (packets); } for (auto& packet : packets) { lastGlobalPingTime = juce::Time::getCurrentTime(); auto data = static_cast (packet.getData()); BlocksProtocol::HostPacketDecoder ::processNextPacket (*this, *data, data + 1, (int) packet.getSize() - 1); } } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConnectedDeviceGroup) }; //============================================================================== /** This is the main singleton object that keeps track of connected blocks */ struct Detector : public juce::ReferenceCountedObject, private juce::Timer { Detector() : defaultDetector (new MIDIDeviceDetector()), deviceDetector (*defaultDetector) { startTimer (10); } Detector (DeviceDetector& dd) : deviceDetector (dd) { startTimer (10); } ~Detector() { jassert (activeTopologySources.isEmpty()); jassert (activeControlButtons.isEmpty()); } using Ptr = juce::ReferenceCountedObjectPtr; 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 = BlockImplementation::getFrom (*b)) bi->sendCommandMessage (BlocksProtocol::endAPIMode); currentTopology = {}; 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 newDeviceInfo; juce::Array newDeviceConnections; for (auto d : connectedDeviceGroups) { newDeviceInfo.addArray (d->currentDeviceInfo); newDeviceConnections.addArray (d->currentDeviceConnections); } for (int i = currentTopology.blocks.size(); --i >= 0;) { auto block = currentTopology.blocks.getUnchecked(i); if (! containsBlockWithUID (newDeviceInfo, block->uid)) { if (auto bi = BlockImplementation::getFrom (*block)) bi->invalidate(); currentTopology.blocks.remove (i); } } for (auto& info : newDeviceInfo) if (info.serial.isValid()) if (! containsBlockWithUID (currentTopology.blocks, getBlockUIDFromSerialNumber (info.serial))) currentTopology.blocks.add (new BlockImplementation (info.serial, *this, info.isMaster)); currentTopology.connections.swapWith (newDeviceConnections); } for (auto d : activeTopologySources) d->listeners.call (&TopologySource::Listener::topologyChanged); #if DUMP_TOPOLOGY dumpTopology (currentTopology); #endif } void handleSharedDataACK (Block::UID deviceID, uint32 packetCounter) const { JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED for (auto&& b : currentTopology.blocks) if (b->uid == deviceID) if (auto bi = BlockImplementation::getFrom (*b)) bi->handleSharedDataACK (packetCounter); } void handleButtonChange (Block::UID deviceID, Block::Timestamp timestamp, uint32 buttonIndex, bool isDown) const { JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED for (auto b : activeControlButtons) { if (b->block.uid == deviceID) { if (auto bi = BlockImplementation::getFrom (b->block)) { bi->pingFromDevice(); if (buttonIndex < (uint32) bi->modelData.buttons.size()) b->broadcastButtonChange (timestamp, bi->modelData.buttons[(int) buttonIndex].type, isDown); } } } } void handleTouchChange (Block::UID deviceID, const TouchSurface::Touch& touchEvent) { JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED for (auto t : activeTouchSurfaces) { if (t->block.uid == deviceID) { TouchSurface::Touch scaledEvent (touchEvent); scaledEvent.x *= t->block.getWidth(); scaledEvent.y *= t->block.getHeight(); scaledEvent.startX *= t->block.getWidth(); scaledEvent.startY *= t->block.getHeight(); t->broadcastTouchChange (scaledEvent); } } } void cancelAllActiveTouches() noexcept { for (auto surface : activeTouchSurfaces) surface->cancelAllActiveTouches(); } //============================================================================== int getIndexFromDeviceID (Block::UID deviceID) const noexcept { for (auto c : connectedDeviceGroups) { const int index = c->getIndexFromDeviceID (deviceID); if (index >= 0) return index; } return -1; } template 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 = BlockImplementation::getFrom (b)) return &(bi->detector); jassertfalse; return nullptr; } DeviceConnection* getDeviceConnectionFor (const Block& b) { for (const auto& d : connectedDeviceGroups) { for (const auto& info : d->currentDeviceInfo) { if (info.uid == b.uid) return d->getDeviceConnection(); } } return nullptr; } std::unique_ptr defaultDetector; DeviceDetector& deviceDetector; juce::Array activeTopologySources; juce::Array activeControlButtons; juce::Array activeTouchSurfaces; BlockTopology currentTopology; 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) { bool anyDevicesAdded = false; for (const auto& devName : detectedDevices) { if (! hasDeviceFor (devName)) { if (auto d = deviceDetector.openDevice (detectedDevices.indexOf (devName))) { connectedDeviceGroups.add (new ConnectedDeviceGroup (*this, devName, d)); anyDevicesAdded = true; } } } if (anyDevicesAdded) handleTopologyChange(); } bool hasDeviceFor (const juce::String& devName) const { for (auto d : connectedDeviceGroups) if (d->deviceName == devName) return true; return false; } juce::OwnedArray connectedDeviceGroups; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Detector) }; //============================================================================== struct BlockImplementation : public Block, private MIDIDeviceConnection::Listener { BlockImplementation (const BlocksProtocol::BlockSerialNumber& serial, Detector& detectorToUse, bool master) : Block (juce::String ((const char*) serial.serial, sizeof (serial.serial))), modelData (serial), remoteHeap (modelData.programAndHeapSize), detector (detectorToUse), isMaster (master) { sendCommandMessage (BlocksProtocol::beginAPIMode); if (modelData.hasTouchSurface) touchSurface.reset (new TouchSurfaceImplementation (*this)); int i = 0; for (auto b : modelData.buttons) controlButtons.add (new ControlButtonImplementation (*this, i++, b)); if (modelData.lightGridWidth > 0 && modelData.lightGridHeight > 0) ledGrid.reset (new LEDGridImplementation (*this)); for (auto s : modelData.statusLEDs) statusLights.add (new StatusLightImplementation (*this, s)); if (modelData.numLEDRowLEDs > 0) ledRow.reset (new LEDRowImplementation (*this)); listenerToMidiConnection = dynamic_cast (detector.getDeviceConnectionFor (*this)); if (listenerToMidiConnection != nullptr) listenerToMidiConnection->addListener (this); } ~BlockImplementation() { if (listenerToMidiConnection != nullptr) listenerToMidiConnection->removeListener (this); } void invalidate() { isStillConnected = false; } Type getType() const override { return modelData.apiType; } juce::String getDeviceDescription() const override { return modelData.description; } int getWidth() const override { return modelData.widthUnits; } int getHeight() const override { return modelData.heightUnits; } float getMillimetersPerUnit() const override { return 47.0f; } bool isHardwareBlock() const override { return true; } juce::Array getPorts() const override { return modelData.ports; } bool isConnected() const override { return isStillConnected && detector.isConnected (uid); } bool isMasterBlock() const override { return isMaster; } TouchSurface* getTouchSurface() const override { return touchSurface.get(); } LEDGrid* getLEDGrid() const override { return ledGrid.get(); } LEDRow* getLEDRow() const override { return ledRow.get(); } juce::Array getButtons() const override { juce::Array result; result.addArray (controlButtons); return result; } juce::Array getStatusLights() const override { juce::Array result; result.addArray (statusLights); return result; } float getBatteryLevel() const override { if (auto status = detector.getLastStatus (uid)) return status->batteryLevel.toUnipolarFloat(); return 0.0f; } bool isBatteryCharging() const override { if (auto status = detector.getLastStatus (uid)) return status->batteryCharging.get() != 0; return false; } bool supportsGraphics() const override { return false; } int getDeviceIndex() const noexcept { return isConnected() ? detector.getIndexFromDeviceID (uid) : -1; } template bool sendMessageToDevice (const PacketBuilder& builder) { lastMessageSendTime = juce::Time::getCurrentTime(); return detector.sendMessageToDevice (uid, builder); } bool sendCommandMessage (uint32 commandID) { int index = getDeviceIndex(); if (index < 0) return false; BlocksProtocol::HostPacketBuilder<64> p; p.writePacketSysexHeaderBytes ((BlocksProtocol::TopologyIndex) index); p.deviceControlMessage (commandID); p.writePacketSysexFooter(); return sendMessageToDevice (p); } static BlockImplementation* getFrom (Block& b) noexcept { if (auto bi = dynamic_cast (&b)) return bi; jassertfalse; return nullptr; } bool isControlBlock() const { auto type = getType(); return type == Block::Type::liveBlock || type == Block::Type::loopBlock || type == Block::Type::developerControlBlock; } //============================================================================== void clearProgramAndData() { programSize = 0; remoteHeap.clear(); } void setProgram (const void* compiledCode, size_t codeSize) { clearProgramAndData(); setDataBytes (0, compiledCode, codeSize); programSize = (uint32) codeSize; } void setDataByte (size_t offset, uint8 value) { remoteHeap.setByte (programSize + offset, value); } void setDataBytes (size_t offset, const void* newData, size_t num) { remoteHeap.setBytes (programSize + offset, static_cast (newData), num); } void setDataBits (uint32 startBit, uint32 numBits, uint32 value) { remoteHeap.setBits (programSize * 8 + startBit, numBits, value); } uint8 getDataByte (size_t offset) { return remoteHeap.getByte (programSize + offset); } void sendProgramEvent (const LEDGrid::ProgramEventMessage& message) { static_assert (sizeof (LEDGrid::ProgramEventMessage::values) == 4 * BlocksProtocol::numProgramMessageInts, "Need to keep the internal and external messages structures the same"); if (remoteHeap.isProgramLoaded()) { auto index = getDeviceIndex(); if (index >= 0) { BlocksProtocol::HostPacketBuilder<128> p; p.writePacketSysexHeaderBytes ((BlocksProtocol::TopologyIndex) index); if (p.addProgramEventMessage (message.values)) { p.writePacketSysexFooter(); sendMessageToDevice (p); } } else { jassertfalse; } } } void saveProgramAsDefault() { sendCommandMessage (BlocksProtocol::saveProgramAsDefault); } void handleSharedDataACK (uint32 packetCounter) noexcept { pingFromDevice(); remoteHeap.handleACKFromDevice (*this, packetCounter); } void pingFromDevice() { lastMessageReceiveTime = juce::Time::getCurrentTime(); } void addDataInputPortListener (DataInputPortListener* listener) override { Block::addDataInputPortListener (listener); if (auto midiInput = getMidiInput()) midiInput->start(); } void sendMessage (const void* message, size_t messageSize) override { if (auto midiOutput = getMidiOutput()) midiOutput->sendMessageNow ({ message, (int) messageSize }); } void handleTimerTick() { if (++resetMessagesSent < 3) { if (resetMessagesSent == 1) sendCommandMessage (BlocksProtocol::endAPIMode); sendCommandMessage (BlocksProtocol::beginAPIMode); return; } if (ledGrid != nullptr) if (auto renderer = ledGrid->getRenderer()) renderer->renderLEDGrid (*ledGrid); remoteHeap.sendChanges (*this); if (lastMessageSendTime < juce::Time::getCurrentTime() - juce::RelativeTime::milliseconds (pingIntervalMs)) sendCommandMessage (BlocksProtocol::ping); } //============================================================================== std::unique_ptr touchSurface; juce::OwnedArray controlButtons; std::unique_ptr ledGrid; std::unique_ptr ledRow; juce::OwnedArray statusLights; BlocksProtocol::BlockDataSheet modelData; MIDIDeviceConnection* listenerToMidiConnection = nullptr; static constexpr int pingIntervalMs = 400; static constexpr uint32 maxBlockSize = BlocksProtocol::padBlockProgramAndHeapSize; static constexpr uint32 maxPacketCounter = BlocksProtocol::PacketCounter::maxValue; static constexpr uint32 maxPacketSize = 200; using PacketBuilder = BlocksProtocol::HostPacketBuilder; using RemoteHeapType = littlefoot::LittleFootRemoteHeap; RemoteHeapType remoteHeap; uint32 programSize = 0; Detector& detector; juce::Time lastMessageSendTime, lastMessageReceiveTime; private: uint32 resetMessagesSent = 0; bool isStillConnected = true; bool isMaster = false; const juce::MidiInput* getMidiInput() const { if (auto c = dynamic_cast (detector.getDeviceConnectionFor (*this))) return c->midiInput.get(); jassertfalse; return nullptr; } juce::MidiInput* getMidiInput() { return const_cast (static_cast(*this).getMidiInput()); } const juce::MidiOutput* getMidiOutput() const { if (auto c = dynamic_cast (detector.getDeviceConnectionFor (*this))) return c->midiOutput.get(); jassertfalse; return nullptr; } juce::MidiOutput* getMidiOutput() { return const_cast (static_cast(*this).getMidiOutput()); } void handleIncomingMidiMessage (const juce::MidiMessage& message) override { dataInputPortListeners.call (&Block::DataInputPortListener::handleIncomingDataPortMessage, *this, message.getRawData(), (size_t) message.getRawDataSize()); } void connectionBeingDeleted (const MIDIDeviceConnection& c) override { jassert (listenerToMidiConnection == &c); juce::ignoreUnused (c); listenerToMidiConnection->removeListener (this); listenerToMidiConnection = nullptr; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlockImplementation) }; //============================================================================== struct LEDRowImplementation : public LEDRow { LEDRowImplementation (BlockImplementation& b) : LEDRow (b), blockImpl (b) { loadProgramOntoBlock(); } /* Data format: 0: 10 x 5-6-5 bits for button LED RGBs 20: 15 x 5-6-5 bits for LED row colours 50: 1 x 5-6-5 bits for LED row overlay colour */ static constexpr uint32 totalDataSize = 256; //============================================================================== void setButtonColour (uint32 index, LEDColour colour) { if (index < 10) write565Colour (16 * index, colour); } int getNumLEDs() const override { return blockImpl.modelData.numLEDRowLEDs; } void setLEDColour (int index, LEDColour colour) override { if ((uint32) index < 15u) write565Colour (20 * 8 + 16 * (uint32) index, colour); } void setOverlayColour (LEDColour colour) override { write565Colour (50 * 8, colour); } void resetOverlayColour() override { write565Colour (50 * 8, {}); } private: void loadProgramOntoBlock() { littlefoot::Compiler compiler; compiler.addNativeFunctions (PhysicalTopologySource::getStandardLittleFootFunctions()); auto err = compiler.compile (getLittleFootProgram(), totalDataSize); if (err.failed()) { DBG (err.getErrorMessage()); jassertfalse; return; } blockImpl.setProgram (compiler.compiledObjectCode.begin(), (size_t) compiler.compiledObjectCode.size()); } void write565Colour (uint32 bitIndex, LEDColour colour) { blockImpl.setDataBits (bitIndex, 5, colour.getRed() >> 3); blockImpl.setDataBits (bitIndex + 5, 6, colour.getGreen() >> 2); blockImpl.setDataBits (bitIndex + 11, 5, colour.getBlue() >> 3); } static const char* getLittleFootProgram() noexcept { return R"littlefoot( int getColour (int bitIndex) { return makeARGB (255, getHeapBits (bitIndex, 5) << 3, getHeapBits (bitIndex + 5, 6) << 2, getHeapBits (bitIndex + 11, 5) << 3); } int getButtonColour (int index) { return getColour (16 * index); } int getLEDColour (int index) { if (getHeapInt (50)) return getColour (50 * 8); return getColour (20 * 8 + 16 * index); } void repaint() { for (int x = 0; x < 15; ++x) setLED (x, 0, getLEDColour (x)); for (int i = 0; i < 10; ++i) setLED (i, 1, getButtonColour (i)); } void handleMessage (int p1, int p2) {} )littlefoot"; } BlockImplementation& blockImpl; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LEDRowImplementation) }; //============================================================================== struct TouchSurfaceImplementation : public TouchSurface, private juce::Timer { TouchSurfaceImplementation (BlockImplementation& b) : TouchSurface (b), blockImpl (b) { if (auto det = Detector::getFrom (block)) det->activeTouchSurfaces.add (this); startTimer (500); } ~TouchSurfaceImplementation() { if (auto det = Detector::getFrom (block)) det->activeTouchSurfaces.removeFirstMatchingValue (this); } int getNumberOfKeywaves() const noexcept override { return blockImpl.modelData.numKeywaves; } void broadcastTouchChange (const TouchSurface::Touch& touchEvent) { auto& status = touches.getValue (touchEvent); // Fake a touch end if we receive a duplicate touch-start with no preceding touch-end (ie: comms error) if (touchEvent.isTouchStart && status.isActive) killTouch (touchEvent, status, juce::Time::getMillisecondCounter()); // Fake a touch start if we receive an unexpected event with no matching start event. (ie: comms error) if (! touchEvent.isTouchStart && ! status.isActive) { TouchSurface::Touch t (touchEvent); t.isTouchStart = true; t.isTouchEnd = false; if (t.zVelocity <= 0) t.zVelocity = status.lastStrikePressure; if (t.zVelocity <= 0) t.zVelocity = t.z; if (t.zVelocity <= 0) t.zVelocity = 0.9f; listeners.call (&TouchSurface::Listener::touchChanged, *this, t); } // Normal handling: status.lastEventTime = juce::Time::getMillisecondCounter(); status.isActive = ! touchEvent.isTouchEnd; if (touchEvent.isTouchStart) status.lastStrikePressure = touchEvent.zVelocity; listeners.call (&TouchSurface::Listener::touchChanged, *this, touchEvent); } void timerCallback() override { // Find touches that seem to have become stuck, and fake a touch-end for them.. static const uint32 touchTimeOutMs = 40; for (auto& t : touches) { auto& status = t.value; auto now = juce::Time::getMillisecondCounter(); if (status.isActive && now > status.lastEventTime + touchTimeOutMs) killTouch (t.touch, status, now); } } struct TouchStatus { uint32 lastEventTime = 0; float lastStrikePressure = 0; bool isActive = false; }; void killTouch (const TouchSurface::Touch& touch, TouchStatus& status, uint32 timeStamp) noexcept { jassert (status.isActive); TouchSurface::Touch killTouch (touch); killTouch.z = 0; killTouch.xVelocity = 0; killTouch.yVelocity = 0; killTouch.zVelocity = -1.0f; killTouch.eventTimestamp = timeStamp; killTouch.isTouchStart = false; killTouch.isTouchEnd = true; listeners.call (&TouchSurface::Listener::touchChanged, *this, killTouch); status.isActive = false; } void cancelAllActiveTouches() noexcept override { const auto now = juce::Time::getMillisecondCounter(); for (auto& t : touches) if (t.value.isActive) killTouch (t.touch, t.value, now); touches.clear(); } BlockImplementation& blockImpl; TouchList touches; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TouchSurfaceImplementation) }; //============================================================================== struct ControlButtonImplementation : public ControlButton { ControlButtonImplementation (BlockImplementation& b, int index, BlocksProtocol::BlockDataSheet::ButtonInfo info) : ControlButton (b), blockImpl (b), buttonInfo (info), buttonIndex (index) { if (auto det = Detector::getFrom (block)) det->activeControlButtons.add (this); } ~ControlButtonImplementation() { if (auto det = Detector::getFrom (block)) det->activeControlButtons.removeFirstMatchingValue (this); } ButtonFunction getType() const override { return buttonInfo.type; } juce::String getName() const override { return BlocksProtocol::getButtonNameForFunction (buttonInfo.type); } float getPositionX() const override { return buttonInfo.x; } float getPositionY() const override { return buttonInfo.y; } bool hasLight() const override { return blockImpl.isControlBlock(); } bool setLightColour (LEDColour colour) override { if (hasLight()) { if (auto row = blockImpl.ledRow.get()) { row->setButtonColour ((uint32) buttonIndex, colour); return true; } } return false; } void broadcastButtonChange (Block::Timestamp timestamp, ControlButton::ButtonFunction button, bool isDown) { if (button == buttonInfo.type) { if (wasDown == isDown) sendButtonChangeToListeners (timestamp, ! isDown); sendButtonChangeToListeners (timestamp, isDown); wasDown = isDown; } } void sendButtonChangeToListeners (Block::Timestamp timestamp, bool isDown) { if (isDown) listeners.call (&ControlButton::Listener::buttonPressed, *this, timestamp); else listeners.call (&ControlButton::Listener::buttonReleased, *this, timestamp); } BlockImplementation& blockImpl; BlocksProtocol::BlockDataSheet::ButtonInfo buttonInfo; int buttonIndex; bool wasDown = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlButtonImplementation) }; //============================================================================== struct StatusLightImplementation : public StatusLight { StatusLightImplementation (Block& b, BlocksProtocol::BlockDataSheet::StatusLEDInfo i) : StatusLight (b), info (i) { } juce::String getName() const override { return info.name; } bool setColour (LEDColour newColour) override { // XXX TODO! juce::ignoreUnused (newColour); return false; } BlocksProtocol::BlockDataSheet::StatusLEDInfo info; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StatusLightImplementation) }; //============================================================================== struct LEDGridImplementation : public LEDGrid { LEDGridImplementation (BlockImplementation& b) : LEDGrid (b), blockImpl (b) { } int getNumColumns() const override { return blockImpl.modelData.lightGridWidth; } int getNumRows() const override { return blockImpl.modelData.lightGridHeight; } juce::Result setProgram (Program* newProgram) override { if (program.get() != newProgram) { program.reset (newProgram); if (program != nullptr) { littlefoot::Compiler compiler; compiler.addNativeFunctions (PhysicalTopologySource::getStandardLittleFootFunctions()); auto err = compiler.compile (newProgram->getLittleFootProgram(), newProgram->getHeapSize()); if (err.failed()) return err; DBG ("Compiled littlefoot program, size = " << (int) compiler.compiledObjectCode.size() << " bytes"); blockImpl.setProgram (compiler.compiledObjectCode.begin(), (size_t) compiler.compiledObjectCode.size()); } else { blockImpl.clearProgramAndData(); } } else { jassertfalse; } return juce::Result::ok(); } Program* getProgram() const override { return program.get(); } void sendProgramEvent (const ProgramEventMessage& m) override { blockImpl.sendProgramEvent (m); } void saveProgramAsDefault() override { blockImpl.saveProgramAsDefault(); } void setDataByte (size_t offset, uint8 value) override { blockImpl.setDataByte (offset, value); } void setDataBytes (size_t offset, const void* data, size_t num) override { blockImpl.setDataBytes (offset, data, num); } void setDataBits (uint32 startBit, uint32 numBits, uint32 value) override { blockImpl.setDataBits (startBit, numBits, value); } uint8 getDataByte (size_t offset) override { return blockImpl.getDataByte (offset); } BlockImplementation& blockImpl; std::unique_ptr program; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LEDGridImplementation) }; //============================================================================== #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 = BlockImplementation::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 }; //============================================================================== struct PhysicalTopologySource::DetectorHolder : private juce::Timer { DetectorHolder (PhysicalTopologySource& pts) : topologySource (pts), detector (Internal::Detector::getDefaultDetector()) { startTimerHz (30); } DetectorHolder (PhysicalTopologySource& pts, DeviceDetector& dd) : topologySource (pts), detector (new Internal::Detector (dd)) { startTimerHz (30); } void timerCallback() override { if (! topologySource.hasOwnServiceTimer()) handleTimerTick(); } void handleTimerTick() { for (auto& b : detector->currentTopology.blocks) if (auto bi = Internal::BlockImplementation::getFrom (*b)) bi->handleTimerTick(); } PhysicalTopologySource& topologySource; Internal::Detector::Ptr detector; }; //============================================================================== PhysicalTopologySource::PhysicalTopologySource() : detector (new DetectorHolder (*this)) { detector->detector->activeTopologySources.add (this); } PhysicalTopologySource::PhysicalTopologySource (DeviceDetector& detectorToUse) : detector (new DetectorHolder (*this, detectorToUse)) { detector->detector->activeTopologySources.add (this); } PhysicalTopologySource::~PhysicalTopologySource() { detector->detector->detach (this); detector = nullptr; } BlockTopology PhysicalTopologySource::getCurrentTopology() const { JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED // This method must only be called from the message thread! return detector->detector->currentTopology; } void PhysicalTopologySource::cancelAllActiveTouches() noexcept { detector->detector->cancelAllActiveTouches(); } bool PhysicalTopologySource::hasOwnServiceTimer() const { return false; } void PhysicalTopologySource::handleTimerTick() { detector->handleTimerTick(); } PhysicalTopologySource::DeviceConnection::DeviceConnection() {} PhysicalTopologySource::DeviceConnection::~DeviceConnection() {} PhysicalTopologySource::DeviceDetector::DeviceDetector() {} PhysicalTopologySource::DeviceDetector::~DeviceDetector() {} const char* const* PhysicalTopologySource::getStandardLittleFootFunctions() noexcept { return BlocksProtocol::ledProgramLittleFootFunctions; } static bool blocksMatch (const Block::Array& list1, const Block::Array& list2) noexcept { if (list1.size() != list2.size()) return false; for (auto&& b : list1) if (! list2.contains (b)) return false; return true; } bool BlockTopology::operator== (const BlockTopology& other) const noexcept { return connections == other.connections && blocksMatch (blocks, other.blocks); } bool BlockTopology::operator!= (const BlockTopology& other) const noexcept { return ! operator== (other); } bool BlockDeviceConnection::operator== (const BlockDeviceConnection& other) const noexcept { return (device1 == other.device1 && device2 == other.device2 && connectionPortOnDevice1 == other.connectionPortOnDevice1 && connectionPortOnDevice2 == other.connectionPortOnDevice2) || (device1 == other.device2 && device2 == other.device1 && connectionPortOnDevice1 == other.connectionPortOnDevice2 && connectionPortOnDevice2 == other.connectionPortOnDevice1); } bool BlockDeviceConnection::operator!= (const BlockDeviceConnection& other) const noexcept { return ! operator== (other); }