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