Initial re-upload of spice2x-24-08-24
This commit is contained in:
704
external/stepmaniax-sdk/sdk/Windows/SMXManager.cpp
vendored
Normal file
704
external/stepmaniax-sdk/sdk/Windows/SMXManager.cpp
vendored
Normal file
@@ -0,0 +1,704 @@
|
||||
#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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user