705 lines
26 KiB
C++
705 lines
26 KiB
C++
|
#include "SMXManager.h"
|
||
|
#include "SMXDevice.h"
|
||
|
#include "SMXDeviceConnection.h"
|
||
|
#include "SMXDeviceSearchThreaded.h"
|
||
|
#include "Helpers.h"
|
||
|
|
||
|
#include <windows.h>
|
||
|
#include <memory>
|
||
|
#include <stdexcept>
|
||
|
using namespace std;
|
||
|
using namespace SMX;
|
||
|
|
||
|
namespace {
|
||
|
Mutex g_Lock;
|
||
|
}
|
||
|
|
||
|
shared_ptr<SMXManager> SMXManager::g_pSMX;
|
||
|
|
||
|
SMX::SMXManager::SMXManager(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback):
|
||
|
m_UserCallbackThread("SMXUserCallbackThread")
|
||
|
{
|
||
|
// Raise the priority of the user callback thread, since we don't want input
|
||
|
// events to be preempted by other things and reduce timing accuracy.
|
||
|
m_UserCallbackThread.SetHighPriority(true);
|
||
|
m_hEvent = make_shared<AutoCloseHandle>(CreateEvent(NULL, false, false, NULL));
|
||
|
m_pSMXDeviceSearchThreaded = make_shared<SMXDeviceSearchThreaded>();
|
||
|
|
||
|
// Create the SMXDevices. We don't create these as we connect, we just reuse the same
|
||
|
// ones.
|
||
|
for(int i = 0; i < 2; ++i)
|
||
|
{
|
||
|
shared_ptr<SMXDevice> pDevice = SMXDevice::Create(m_hEvent, g_Lock);
|
||
|
m_pDevices.push_back(pDevice);
|
||
|
}
|
||
|
|
||
|
// The callback we send to SMXDeviceConnection will be called from our thread. Wrap
|
||
|
// it so it's called from UserCallbackThread instead.
|
||
|
auto pCallbackInThread = [this, pCallback](int PadNumber, SMXUpdateCallbackReason reason) {
|
||
|
m_UserCallbackThread.RunInThread([pCallback, PadNumber, reason]() {
|
||
|
pCallback(PadNumber, reason);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
// Set the update callbacks. Do this before starting the thread, to avoid race conditions.
|
||
|
for(int pad = 0; pad < 2; ++pad)
|
||
|
m_pDevices[pad]->SetUpdateCallback(pCallbackInThread);
|
||
|
|
||
|
// Start the thread.
|
||
|
DWORD id;
|
||
|
m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &id);
|
||
|
SMX::SetThreadName(id, "SMXManager");
|
||
|
|
||
|
// Raise the priority of the I/O thread, since we don't want input
|
||
|
// events to be preempted by other things and reduce timing accuracy.
|
||
|
SetThreadPriority( m_hThread, THREAD_PRIORITY_HIGHEST );
|
||
|
}
|
||
|
|
||
|
SMX::SMXManager::~SMXManager()
|
||
|
{
|
||
|
// Shut down the thread, if it's still running.
|
||
|
Shutdown();
|
||
|
}
|
||
|
|
||
|
shared_ptr<SMXDevice> SMX::SMXManager::GetDevice(int pad)
|
||
|
{
|
||
|
return m_pDevices[pad];
|
||
|
}
|
||
|
|
||
|
void SMX::SMXManager::Shutdown()
|
||
|
{
|
||
|
g_Lock.AssertNotLockedByCurrentThread();
|
||
|
|
||
|
// Make sure we're not being called from within m_UserCallbackThread, since that'll
|
||
|
// deadlock when we shut down m_UserCallbackThread.
|
||
|
if(m_UserCallbackThread.IsCurrentThread())
|
||
|
throw runtime_error("SMX::SMXManager::Shutdown must not be called from an SMX callback");
|
||
|
|
||
|
// Shut down the thread we make user callbacks from.
|
||
|
m_UserCallbackThread.Shutdown();
|
||
|
|
||
|
// Shut down the device search thread.
|
||
|
m_pSMXDeviceSearchThreaded->Shutdown();
|
||
|
|
||
|
if(m_hThread == INVALID_HANDLE_VALUE)
|
||
|
return;
|
||
|
|
||
|
// Tell the thread to shut down, and wait for it before returning.
|
||
|
m_bShutdown = true;
|
||
|
SetEvent(m_hEvent->value());
|
||
|
|
||
|
WaitForSingleObject(m_hThread, INFINITE);
|
||
|
m_hThread = INVALID_HANDLE_VALUE;
|
||
|
}
|
||
|
|
||
|
DWORD WINAPI SMX::SMXManager::ThreadMainStart(void *self_)
|
||
|
{
|
||
|
SMXManager *self = (SMXManager *) self_;
|
||
|
self->ThreadMain();
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// When we connect to a device, we don't know whether it's P1 or P2, since we get that
|
||
|
// info from the device after we connect to it. If we have a P2 device in SMX_PadNumber_1
|
||
|
// or a P1 device in SMX_PadNumber_2, swap the two.
|
||
|
void SMX::SMXManager::CorrectDeviceOrder()
|
||
|
{
|
||
|
// We're still holding the lock from when we updated the devices, so the application
|
||
|
// won't see the devices out of order before we do this.
|
||
|
g_Lock.AssertLockedByCurrentThread();
|
||
|
|
||
|
SMXInfo info[2];
|
||
|
m_pDevices[0]->GetInfoLocked(info[0]);
|
||
|
m_pDevices[1]->GetInfoLocked(info[1]);
|
||
|
|
||
|
// If we have two P1s or two P2s, the pads are misconfigured and we'll just leave the order alone.
|
||
|
bool Player2[2] = {
|
||
|
m_pDevices[0]->IsPlayer2Locked(),
|
||
|
m_pDevices[1]->IsPlayer2Locked(),
|
||
|
};
|
||
|
if(info[0].m_bConnected && info[1].m_bConnected && Player2[0] == Player2[1])
|
||
|
return;
|
||
|
|
||
|
bool bP1NeedsSwap = info[0].m_bConnected && Player2[0];
|
||
|
bool bP2NeedsSwap = info[1].m_bConnected && !Player2[1];
|
||
|
if(bP1NeedsSwap || bP2NeedsSwap)
|
||
|
swap(m_pDevices[0], m_pDevices[1]);
|
||
|
}
|
||
|
|
||
|
void SMX::SMXManager::ThreadMain()
|
||
|
{
|
||
|
g_Lock.Lock();
|
||
|
|
||
|
while(!m_bShutdown)
|
||
|
{
|
||
|
// If there are any lights commands to be sent, send them now. Do this before callig Update(),
|
||
|
// since this actually just queues commands, which are actually handled in Update.
|
||
|
SendLightUpdates();
|
||
|
|
||
|
// Send panel test mode commands if needed.
|
||
|
UpdatePanelTestMode();
|
||
|
|
||
|
// See if there are any new devices.
|
||
|
AttemptConnections();
|
||
|
|
||
|
// Update all connected devices.
|
||
|
for(shared_ptr<SMXDevice> pDevice: m_pDevices)
|
||
|
{
|
||
|
wstring sError;
|
||
|
pDevice->Update(sError);
|
||
|
|
||
|
if(!sError.empty())
|
||
|
{
|
||
|
Log(ssprintf("Device error: %ls", sError.c_str()));
|
||
|
|
||
|
// Tell m_pDeviceList that the device was closed, so it'll discard the device
|
||
|
// and notice if a new device shows up on the same path.
|
||
|
m_pSMXDeviceSearchThreaded->DeviceWasClosed(pDevice->GetDeviceHandle());
|
||
|
pDevice->CloseDevice();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Devices may have finished initializing, so see if we need to update the ordering.
|
||
|
CorrectDeviceOrder();
|
||
|
|
||
|
// Make a list of handles for WaitForMultipleObjectsEx.
|
||
|
vector<HANDLE> aHandles = { m_hEvent->value() };
|
||
|
for(shared_ptr<SMXDevice> pDevice: m_pDevices)
|
||
|
{
|
||
|
shared_ptr<AutoCloseHandle> pHandle = pDevice->GetDeviceHandle();
|
||
|
if(pHandle)
|
||
|
aHandles.push_back(pHandle->value());
|
||
|
}
|
||
|
|
||
|
// See how long we should block waiting for I/O. If we have any scheduled lights commands,
|
||
|
// wait until the next command should be sent, otherwise wait for a second.
|
||
|
int iDelayMS = 1000;
|
||
|
if(!m_aPendingLightsCommands.empty())
|
||
|
{
|
||
|
double fSendIn = m_aPendingLightsCommands[0].fTimeToSend - GetMonotonicTime();
|
||
|
|
||
|
// Add 1ms to the delay time. We're using a high resolution timer, but
|
||
|
// WaitForMultipleObjectsEx only has 1ms resolution, so this keeps us from
|
||
|
// repeatedly waking up slightly too early.
|
||
|
iDelayMS = int(fSendIn * 1000) + 1;
|
||
|
iDelayMS = max(0, iDelayMS);
|
||
|
}
|
||
|
|
||
|
// Wait until there's something to do for a connected device, or delay briefly if we're
|
||
|
// not connected to anything. Unlock while we block. Devices are only ever opened or
|
||
|
// closed from within this thread, so the handles won't go away while we're waiting on
|
||
|
// them.
|
||
|
g_Lock.Unlock();
|
||
|
WaitForMultipleObjectsEx(aHandles.size(), aHandles.data(), false, iDelayMS, true);
|
||
|
g_Lock.Lock();
|
||
|
}
|
||
|
g_Lock.Unlock();
|
||
|
}
|
||
|
|
||
|
// Lights are updated with two commands. The top two rows of LEDs in each panel are
|
||
|
// updated by the first command, and the bottom two rows are updated by the second
|
||
|
// command. We need to send the two commands in order. The panel won't update lights
|
||
|
// until both commands have been received, so we don't flicker the partial top update
|
||
|
// before the bottom update is received.
|
||
|
//
|
||
|
// A complete update can be performed at up to 30 FPS, but we actually update at 60
|
||
|
// FPS, alternating between updating the top and bottom half.
|
||
|
//
|
||
|
// This interlacing is performed to reduce the amount of work the panels and master
|
||
|
// controller need to do on each update. This improves timing accuracy, since less
|
||
|
// time is taken by each update.
|
||
|
//
|
||
|
// The order of lights is:
|
||
|
//
|
||
|
// 0123 0123 0123
|
||
|
// 4567 4567 4567
|
||
|
// 89AB 89AB 89AB
|
||
|
// CDEF CDEF CDEF
|
||
|
//
|
||
|
// 0123 0123 0123
|
||
|
// 4567 4567 4567
|
||
|
// 89AB 89AB 89AB
|
||
|
// CDEF CDEF CDEF
|
||
|
//
|
||
|
// 0123 0123 0123
|
||
|
// 4567 4567 4567
|
||
|
// 89AB 89AB 89AB
|
||
|
// CDEF CDEF CDEF
|
||
|
//
|
||
|
// with panels left-to-right, top-to-bottom. The first packet sends all 0123 and 4567
|
||
|
// lights, and the second packet sends 78AB and CDEF.
|
||
|
//
|
||
|
// We hide these details from the API to simplify things for the user:
|
||
|
//
|
||
|
// - The user sends us a complete lights set. This should be sent at (up to) 30Hz.
|
||
|
// If we get lights data too quickly, we'll always complete the one we started before
|
||
|
// sending the next.
|
||
|
// - We don't limit to exactly 30Hz to prevent phase issues where a 60 FPS game is
|
||
|
// coming in and out of phase with our timer. To avoid this, we limit to 40Hz.
|
||
|
// - When we have new lights data to send, we send the first half right away, wait
|
||
|
// 16ms (60Hz), then send the second half, which is the pacing the device expects.
|
||
|
// - If we get a new lights update in between the two lights commands, we won't split
|
||
|
// the lights. The two lights commands will always come from the same update, so
|
||
|
// we don't get weird interlacing effects.
|
||
|
// - If SMX_ReenableAutoLights is called between the two commands, we need to guarantee
|
||
|
// that we don't send the second lights commands, since that may re-disable auto lights.
|
||
|
// - If we have two pads, the lights update is for both pads and we'll send both commands
|
||
|
// for both pads at the same time, so both pads update lights simultaneously.
|
||
|
void SMX::SMXManager::SetLights(const string sPanelLights[2])
|
||
|
{
|
||
|
g_Lock.AssertNotLockedByCurrentThread();
|
||
|
LockMutex L(g_Lock);
|
||
|
|
||
|
// Don't send lights when a panel test mode is active.
|
||
|
if(m_PanelTestMode != PanelTestMode_Off)
|
||
|
return;
|
||
|
|
||
|
// If m_bOnlySendLightsOnChange is true, only send lights commands if the lights have
|
||
|
// actually changed. This is only used for internal testing, and the controllers normally
|
||
|
// expect to receive regular lights updates, even if the lights aren't actually changing.
|
||
|
if(m_bOnlySendLightsOnChange)
|
||
|
{
|
||
|
static string sLastPanelLights[2];
|
||
|
if(sPanelLights[0] == sLastPanelLights[0] && sPanelLights[1] == sLastPanelLights[1])
|
||
|
{
|
||
|
Log("no change");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
sLastPanelLights[0] = sPanelLights[0];
|
||
|
sLastPanelLights[1] = sPanelLights[1];
|
||
|
}
|
||
|
|
||
|
// Separate top and bottom lights commands.
|
||
|
//
|
||
|
// sPanelLights[iPad] is
|
||
|
//
|
||
|
// 0123 0123 0123
|
||
|
// 4567 4567 4567
|
||
|
// 89AB 89AB 89AB
|
||
|
// CDEF CDEF CDEF
|
||
|
//
|
||
|
// 0123 0123 0123
|
||
|
// 4567 4567 4567
|
||
|
// 89AB 89AB 89AB
|
||
|
// CDEF CDEF CDEF
|
||
|
//
|
||
|
// 0123 0123 0123
|
||
|
// 4567 4567 4567
|
||
|
// 89AB 89AB 89AB
|
||
|
// CDEF CDEF CDEF
|
||
|
//
|
||
|
// If we're on a 25-light device, we have an additional grid of 3x3 LEDs:
|
||
|
//
|
||
|
//
|
||
|
// x x x x
|
||
|
// 0 1 2
|
||
|
// x x x x
|
||
|
// 3 4 5
|
||
|
// x x x x
|
||
|
// 6 7 8
|
||
|
// x x x x
|
||
|
//
|
||
|
// Set sLightsCommand[iPad][0] to include 0123 4567, [1] to 89AB CDEF,
|
||
|
// and [2] to the 3x3 grid.
|
||
|
string sLightCommands[3][2]; // sLightCommands[command][pad]
|
||
|
|
||
|
// Read the linearly arranged color data we've been given and split it into top and
|
||
|
// bottom commands for each pad.
|
||
|
for(int iPad = 0; iPad < 2; ++iPad)
|
||
|
{
|
||
|
// If there's no data for this pad, leave the command empty.
|
||
|
string sLightsDataForPad = sPanelLights[iPad];
|
||
|
if(sLightsDataForPad.empty())
|
||
|
continue;
|
||
|
|
||
|
// Sanity check the lights data. For 4x4 lights, it should have 9*4*4*3 bytes of
|
||
|
// data: RGB for each of 4x4 LEDs on 9 panels. For 25-light panels there should
|
||
|
// be 4x4+3x3 (25) lights of data.
|
||
|
size_t LightSize4x4 = 9*4*4*3;
|
||
|
size_t LightSize25 = 9*5*5*3;
|
||
|
if(sLightsDataForPad.size() != LightSize4x4 && sLightsDataForPad.size() != LightSize25)
|
||
|
{
|
||
|
Log(ssprintf("SetLights: Lights data should be %i or %i bytes, received %i",
|
||
|
LightSize4x4, LightSize25, sLightsDataForPad.size()));
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// If we've been given 16 lights, pad to 25.
|
||
|
if(sLightsDataForPad.size() == LightSize4x4)
|
||
|
sLightsDataForPad.append(LightSize25 - LightSize4x4, '\0');
|
||
|
|
||
|
// Lights are sent in three commands:
|
||
|
//
|
||
|
// 4: the 3x3 inner grid
|
||
|
// 2: the top 4x2 lights
|
||
|
// 3: the bottom 4x2 lights
|
||
|
//
|
||
|
// Command 4 is only used by firmware version 4+.
|
||
|
//
|
||
|
// Always send all three commands if the firmware expects it, even if we've
|
||
|
// been given 4x4 data.
|
||
|
sLightCommands[0][iPad] = "4";
|
||
|
sLightCommands[1][iPad] = "2";
|
||
|
sLightCommands[2][iPad] = "3";
|
||
|
int iNextInputByte = 0;
|
||
|
auto scaleLight = [](uint8_t iColor) {
|
||
|
// Apply color scaling. Values over about 170 don't make the LEDs any brighter, so this
|
||
|
// gives better contrast and draws less power.
|
||
|
return uint8_t(iColor * 0.6666f);
|
||
|
};
|
||
|
for(int iPanel = 0; iPanel < 9; ++iPanel)
|
||
|
{
|
||
|
// Create the 2 and 3 commands.
|
||
|
for(int iByte = 0; iByte < 4*4*3; ++iByte)
|
||
|
{
|
||
|
uint8_t iColor = sLightsDataForPad[iNextInputByte++];
|
||
|
iColor = scaleLight(iColor);
|
||
|
|
||
|
int iCommandIndex = iByte < 4*2*3? 1:2;
|
||
|
sLightCommands[iCommandIndex][iPad].append(1, iColor);
|
||
|
}
|
||
|
|
||
|
// Create the 4 command.
|
||
|
for(int iByte = 0; iByte < 3*3*3; ++iByte)
|
||
|
{
|
||
|
uint8_t iColor = sLightsDataForPad[iNextInputByte++];
|
||
|
iColor = scaleLight(iColor);
|
||
|
sLightCommands[0][iPad].append(1, iColor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sLightCommands[0][iPad].push_back('\n');
|
||
|
sLightCommands[1][iPad].push_back('\n');
|
||
|
sLightCommands[2][iPad].push_back('\n');
|
||
|
}
|
||
|
|
||
|
// Each update adds one entry to m_aPendingLightsCommands for each lights command.
|
||
|
//
|
||
|
// If there are at least as many entries in m_aPendingLightsCommands as there are
|
||
|
// commands to send, then lights updates are happening faster than they can be sent
|
||
|
// to the pad. If that happens, replace the existing commands rather than adding
|
||
|
// new ones.
|
||
|
//
|
||
|
// Make sure we always finish a lights update once we start it, so if we receive lights
|
||
|
// updates very quickly we won't just keep sending the first half and never finish one.
|
||
|
// Otherwise, we'll update with the newest data we have available.
|
||
|
//
|
||
|
// Note that m_aPendingLightsCommands contains the update for both pads, to guarantee
|
||
|
// we always send light updates for both pads together and they never end up out of
|
||
|
// phase.
|
||
|
if(m_aPendingLightsCommands.size() < 3)
|
||
|
{
|
||
|
// There's a subtle but important difference between command timing in
|
||
|
// firmware version 4 compared to earlier versions:
|
||
|
//
|
||
|
// Earlier firmwares would process host commands as soon as they're received.
|
||
|
// Because of this, we have to wait before sending the '3' command to give
|
||
|
// the master controller time to finish sending the '2' command to panels.
|
||
|
// If we don't do this everything will still work, but the master will block
|
||
|
// while processing the second command waiting for panel data to finish sending
|
||
|
// since the TX queue will be full. If this happens it isn't processing HID
|
||
|
// data, which reduces input timing accuracy.
|
||
|
//
|
||
|
// Firmware version 4 won't process a host command if there's data still being
|
||
|
// sent to the panels. It'll wait until the data is flushed. This means that
|
||
|
// we can queue all three lights commands at once, and just send them as fast
|
||
|
// as the host acknowledges them. The second command will sit around on the
|
||
|
// master controller's buffer until it finishes sending the first command to
|
||
|
// the panels, then the third command will do the same.
|
||
|
//
|
||
|
// This change is only needed due to the larger amount of data sent in 25-light
|
||
|
// mode. Since we're spending more time sending data from the master to the
|
||
|
// panels, the timing requirements are tighter. Doing it in the same manual-delay
|
||
|
// fashion causes too much latency and makes it harder to maintain 30 FPS.
|
||
|
//
|
||
|
// If two controllers are connected, they should either both be 4+ or not. We
|
||
|
// don't handle the case where they're different and both timings are needed.
|
||
|
double fNow = GetMonotonicTime();
|
||
|
double fSendCommandAt = max(fNow, m_fDelayLightCommandsUntil);
|
||
|
double fCommandTimes[3] = { fNow, fNow, fNow };
|
||
|
|
||
|
bool masterIsV4 = false;
|
||
|
bool anyMasterConnected = false;
|
||
|
for(int iPad = 0; iPad < 2; ++iPad)
|
||
|
{
|
||
|
SMXConfig config;
|
||
|
if(!m_pDevices[iPad]->GetConfigLocked(config))
|
||
|
continue;
|
||
|
|
||
|
anyMasterConnected = true;
|
||
|
if(config.masterVersion >= 4)
|
||
|
masterIsV4 = true;
|
||
|
}
|
||
|
|
||
|
// If we don't have the config yet, the master is in the process of connecting, so don't
|
||
|
// queue lights.
|
||
|
if(!anyMasterConnected)
|
||
|
return;
|
||
|
|
||
|
// If we're on master firmware < 4, set delay times. For 4+, just queue commands.
|
||
|
// We don't need to set fCommandTimes[0] since the '4' packet won't be sent.
|
||
|
if(!masterIsV4)
|
||
|
{
|
||
|
const double fDelayBetweenLightsCommands = 1/60.0;
|
||
|
fCommandTimes[1] = fSendCommandAt;
|
||
|
fCommandTimes[2] = fCommandTimes[1] + fDelayBetweenLightsCommands;
|
||
|
}
|
||
|
|
||
|
// Update m_fDelayLightCommandsUntil, so we know when the next
|
||
|
// lights command can be sent.
|
||
|
m_fDelayLightCommandsUntil = fSendCommandAt + 1/30.0f;
|
||
|
|
||
|
// Add three commands to the list, scheduled at fFirstCommandTime and fSecondCommandTime.
|
||
|
m_aPendingLightsCommands.push_back(PendingCommand(fCommandTimes[0]));
|
||
|
m_aPendingLightsCommands.push_back(PendingCommand(fCommandTimes[1]));
|
||
|
m_aPendingLightsCommands.push_back(PendingCommand(fCommandTimes[2]));
|
||
|
}
|
||
|
|
||
|
// Set the pad commands.
|
||
|
for(int iPad = 0; iPad < 2; ++iPad)
|
||
|
{
|
||
|
// If the command for this pad is empty, leave any existing pad command alone.
|
||
|
if(sLightCommands[0][iPad].empty())
|
||
|
continue;
|
||
|
|
||
|
SMXConfig config;
|
||
|
if(!m_pDevices[iPad]->GetConfigLocked(config))
|
||
|
continue;
|
||
|
|
||
|
// If this pad is firmware version 4, send the 4 command. Otherwise, leave the 4 command
|
||
|
// empty and no command will be sent.
|
||
|
PendingCommand *pPending4Commands = &m_aPendingLightsCommands[m_aPendingLightsCommands.size()-3]; // 3
|
||
|
if(config.masterVersion >= 4)
|
||
|
pPending4Commands->sPadCommand[iPad] = sLightCommands[0][iPad];
|
||
|
else
|
||
|
pPending4Commands->sPadCommand[iPad] = "";
|
||
|
|
||
|
PendingCommand *pPending2Commands = &m_aPendingLightsCommands[m_aPendingLightsCommands.size()-2]; // 2
|
||
|
pPending2Commands->sPadCommand[iPad] = sLightCommands[1][iPad];
|
||
|
|
||
|
PendingCommand *pPending3Commands = &m_aPendingLightsCommands[m_aPendingLightsCommands.size()-1]; // 3
|
||
|
pPending3Commands->sPadCommand[iPad] = sLightCommands[2][iPad];
|
||
|
}
|
||
|
|
||
|
// Wake up the I/O thread if it's blocking on WaitForMultipleObjectsEx.
|
||
|
SetEvent(m_hEvent->value());
|
||
|
}
|
||
|
|
||
|
void SMX::SMXManager::SetPlatformLights(const string sPanelLights[2])
|
||
|
{
|
||
|
g_Lock.AssertNotLockedByCurrentThread();
|
||
|
LockMutex L(g_Lock);
|
||
|
|
||
|
// Read the linearly arranged color data we've been given and split it into top and
|
||
|
// bottom commands for each pad.
|
||
|
for(int iPad = 0; iPad < 2; ++iPad)
|
||
|
{
|
||
|
// If there's no data for this pad, skip it.
|
||
|
string sLightsDataForPad = sPanelLights[iPad];
|
||
|
if(sLightsDataForPad.empty())
|
||
|
continue;
|
||
|
|
||
|
if(sLightsDataForPad.size() != 44*3)
|
||
|
{
|
||
|
Log(ssprintf("SetPlatformLights: Platform lights data should be %i bytes, received %i",
|
||
|
44*3, sLightsDataForPad.size()));
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// If this master doesn't support this, skip it.
|
||
|
SMXConfig config;
|
||
|
if(!m_pDevices[iPad]->GetConfigLocked(config))
|
||
|
continue;
|
||
|
if(config.masterVersion < 4)
|
||
|
continue;
|
||
|
|
||
|
string sLightCommand;
|
||
|
sLightCommand.push_back('L');
|
||
|
sLightCommand.push_back(0); // LED strip index (always 0)
|
||
|
sLightCommand.push_back(44); // number of LEDs to set
|
||
|
sLightCommand += sLightsDataForPad;
|
||
|
|
||
|
m_pDevices[iPad]->SendCommandLocked(sLightCommand);
|
||
|
}
|
||
|
|
||
|
// Wake up the I/O thread if it's blocking on WaitForMultipleObjectsEx.
|
||
|
SetEvent(m_hEvent->value());
|
||
|
}
|
||
|
|
||
|
void SMX::SMXManager::ReenableAutoLights()
|
||
|
{
|
||
|
g_Lock.AssertNotLockedByCurrentThread();
|
||
|
LockMutex L(g_Lock);
|
||
|
|
||
|
// Clear any pending lights commands, so we don't re-disable auto-lighting by sending a
|
||
|
// lights command after we enable it. If we've sent the first half of a lights update
|
||
|
// and this causes us to not send the second half, the controller will just discard it.
|
||
|
m_aPendingLightsCommands.clear();
|
||
|
for(int iPad = 0; iPad < 2; ++iPad)
|
||
|
m_pDevices[iPad]->SendCommandLocked(string("S 1\n", 4));
|
||
|
}
|
||
|
|
||
|
// Check to see if we should send any commands in m_aPendingLightsCommands.
|
||
|
void SMX::SMXManager::SendLightUpdates()
|
||
|
{
|
||
|
g_Lock.AssertLockedByCurrentThread();
|
||
|
|
||
|
// If previous lights commands are being sent, wait for them to complete before
|
||
|
// queueing more.
|
||
|
if(m_iLightsCommandsInProgress > 0)
|
||
|
return;
|
||
|
|
||
|
// If we have more than one command queued, we can queue several of them if we're
|
||
|
// before fTimeToSend. For the V4 pads that require more commands, this lets us queue
|
||
|
// the whole lights update at once. V3 pads require us to time commands, so we can't
|
||
|
// spam both lights commands at once, which is handled by fTimeToSend.
|
||
|
while( !m_aPendingLightsCommands.empty() )
|
||
|
{
|
||
|
// Send the lights command for each pad. If either pad isn't connected, this won't do
|
||
|
// anything.
|
||
|
const PendingCommand &command = m_aPendingLightsCommands[0];
|
||
|
|
||
|
// See if it's time to send this command.
|
||
|
if(command.fTimeToSend > GetMonotonicTime())
|
||
|
break;
|
||
|
|
||
|
for(int iPad = 0; iPad < 2; ++iPad)
|
||
|
{
|
||
|
if(!command.sPadCommand[iPad].empty())
|
||
|
{
|
||
|
// Count the number of commands we've queued. We won't send any more until
|
||
|
// this reaches 0 and all queued commands were sent.
|
||
|
m_iLightsCommandsInProgress++;
|
||
|
|
||
|
// The completion callback is guaranteed to always be called, even if the controller
|
||
|
// disconnects and the command wasn't sent.
|
||
|
m_pDevices[iPad]->SendCommandLocked(command.sPadCommand[iPad], [this, iPad](string response) {
|
||
|
g_Lock.AssertLockedByCurrentThread();
|
||
|
m_iLightsCommandsInProgress--;
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Remove the command we've sent.
|
||
|
m_aPendingLightsCommands.erase(m_aPendingLightsCommands.begin(), m_aPendingLightsCommands.begin()+1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void SMX::SMXManager::SetPanelTestMode(PanelTestMode mode)
|
||
|
{
|
||
|
g_Lock.AssertNotLockedByCurrentThread();
|
||
|
LockMutex Lock(g_Lock);
|
||
|
m_PanelTestMode = mode;
|
||
|
}
|
||
|
|
||
|
void SMX::SMXManager::UpdatePanelTestMode()
|
||
|
{
|
||
|
// If the test mode has changed, send the new test mode.
|
||
|
//
|
||
|
// When the test mode is enabled, send the test mode again periodically, or it'll time
|
||
|
// out on the master and be turned off. Don't repeat the PanelTestMode_Off command.
|
||
|
g_Lock.AssertLockedByCurrentThread();
|
||
|
uint32_t now = GetTickCount();
|
||
|
if(m_PanelTestMode == m_LastSentPanelTestMode &&
|
||
|
(m_PanelTestMode == PanelTestMode_Off || now - m_SentPanelTestModeAtTicks < 1000))
|
||
|
return;
|
||
|
|
||
|
// When we first send the test mode command (not for repeats), turn off lights.
|
||
|
if(m_LastSentPanelTestMode == PanelTestMode_Off)
|
||
|
{
|
||
|
// The 'l' command used to set lights, but it's now only used to turn lights off
|
||
|
// for cases like this.
|
||
|
string sData = "l";
|
||
|
sData.append(108, 0);
|
||
|
sData += "\n";
|
||
|
for(int iPad = 0; iPad < 2; ++iPad)
|
||
|
m_pDevices[iPad]->SendCommandLocked(sData);
|
||
|
}
|
||
|
|
||
|
m_SentPanelTestModeAtTicks = now;
|
||
|
m_LastSentPanelTestMode = m_PanelTestMode;
|
||
|
for(int iPad = 0; iPad < 2; ++iPad)
|
||
|
m_pDevices[iPad]->SendCommandLocked(ssprintf("t %c\n", m_PanelTestMode));
|
||
|
}
|
||
|
|
||
|
// Assign a serial number to master controllers if one isn't already assigned. This
|
||
|
// will have no effect if a serial is already set.
|
||
|
//
|
||
|
// We just assign a random number. The serial number will be used as the USB serial
|
||
|
// number, and can be queried in SMXInfo.
|
||
|
void SMX::SMXManager::SetSerialNumbers()
|
||
|
{
|
||
|
g_Lock.AssertNotLockedByCurrentThread();
|
||
|
LockMutex L(g_Lock);
|
||
|
|
||
|
m_aPendingLightsCommands.clear();
|
||
|
for(int iPad = 0; iPad < 2; ++iPad)
|
||
|
{
|
||
|
string sData = "s";
|
||
|
uint8_t serial[16];
|
||
|
SMX::GenerateRandom(serial, sizeof(serial));
|
||
|
sData.append((char *) serial, sizeof(serial));
|
||
|
sData.append(1, '\n');
|
||
|
|
||
|
m_pDevices[iPad]->SendCommandLocked(sData);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void SMX::SMXManager::RunInHelperThread(function<void()> func)
|
||
|
{
|
||
|
m_UserCallbackThread.RunInThread(func);
|
||
|
}
|
||
|
|
||
|
// See if there are any new devices to connect to.
|
||
|
void SMX::SMXManager::AttemptConnections()
|
||
|
{
|
||
|
g_Lock.AssertLockedByCurrentThread();
|
||
|
|
||
|
vector<shared_ptr<AutoCloseHandle>> apDevices = m_pSMXDeviceSearchThreaded->GetDevices();
|
||
|
|
||
|
// Check each device that we've found. This will include ones we already have open.
|
||
|
for(shared_ptr<AutoCloseHandle> pHandle: apDevices)
|
||
|
{
|
||
|
// See if this device is already open. If it is, we don't need to do anything with it.
|
||
|
bool bAlreadyOpen = false;
|
||
|
for(shared_ptr<SMXDevice> pDevice: m_pDevices)
|
||
|
{
|
||
|
if(pDevice->GetDeviceHandle() == pHandle)
|
||
|
bAlreadyOpen = true;
|
||
|
}
|
||
|
if(bAlreadyOpen)
|
||
|
continue;
|
||
|
|
||
|
// Find an open device slot.
|
||
|
shared_ptr<SMXDevice> pDeviceToOpen;
|
||
|
for(shared_ptr<SMXDevice> pDevice: m_pDevices)
|
||
|
{
|
||
|
// Note that we check whether the device has a handle rather than calling IsConnected, since
|
||
|
// devices aren't actually considered connected until they've read the configuration.
|
||
|
if(pDevice->GetDeviceHandle() == NULL)
|
||
|
{
|
||
|
pDeviceToOpen = pDevice;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(pDeviceToOpen == nullptr)
|
||
|
{
|
||
|
// All device slots are used. Are there more than two devices plugged in?
|
||
|
Log("Error: No available slots for device. Are more than two devices connected?");
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Open the device in this slot.
|
||
|
Log("Opening SMX device");
|
||
|
wstring sError;
|
||
|
pDeviceToOpen->OpenDeviceHandle(pHandle, sError);
|
||
|
if(!sError.empty())
|
||
|
Log(ssprintf("Error opening device: %ls", sError.c_str()));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|