Initial re-upload of spice2x-24-08-24

This commit is contained in:
2024-08-28 11:10:34 -04:00
commit caa9e02285
1181 changed files with 380065 additions and 0 deletions

8
external/stepmaniax-sdk/.editorconfig vendored Normal file
View File

@@ -0,0 +1,8 @@
root=true
[*]
indent_style = space
[*.{cpp,cs,h}]
indent_size = 4

5
external/stepmaniax-sdk/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.vs
*.user
build
obj
out

33
external/stepmaniax-sdk/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,33 @@
set(SMXSDK_SOURCES
"sdk/Windows/Helpers.cpp"
"sdk/Windows/Helpers.h"
"sdk/Windows/SMX.cpp"
"sdk/Windows/SMXBuildVersion.h"
"sdk/Windows/SMXConfigPacket.cpp"
"sdk/Windows/SMXConfigPacket.h"
"sdk/Windows/SMXDevice.cpp"
"sdk/Windows/SMXDevice.h"
"sdk/Windows/SMXDeviceConnection.cpp"
"sdk/Windows/SMXDeviceConnection.h"
"sdk/Windows/SMXDeviceSearch.cpp"
"sdk/Windows/SMXDeviceSearch.h"
"sdk/Windows/SMXDeviceSearchThreaded.cpp"
"sdk/Windows/SMXDeviceSearchThreaded.h"
"sdk/Windows/SMXGif.cpp"
"sdk/Windows/SMXGif.h"
"sdk/Windows/SMXHelperThread.cpp"
"sdk/Windows/SMXHelperThread.h"
"sdk/Windows/SMXManager.cpp"
"sdk/Windows/SMXManager.h"
"sdk/Windows/SMXPanelAnimation.cpp"
"sdk/Windows/SMXPanelAnimation.h"
"sdk/Windows/SMXPanelAnimationUpload.cpp"
"sdk/Windows/SMXPanelAnimationUpload.h"
"sdk/Windows/SMXThread.cpp"
"sdk/Windows/SMXThread.h"
)
add_library(smx STATIC ${SMXSDK_SOURCES})
target_link_libraries(smx PUBLIC hid)
target_include_directories(smx INTERFACE "sdk")

22
external/stepmaniax-sdk/LICENSE.txt vendored Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2017 Step Revolution LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

315
external/stepmaniax-sdk/sdk/SMX.h vendored Normal file
View File

@@ -0,0 +1,315 @@
#ifndef SMX_H
#define SMX_H
#include <stdint.h>
#include <stddef.h> // for offsetof
#define SMX_API extern "C"
struct SMXInfo;
struct SMXConfig;
enum SensorTestMode : uint32_t;
enum PanelTestMode : uint32_t;
enum SMXUpdateCallbackReason : uint32_t;
struct SMXSensorTestModeData;
// All functions are nonblocking. Getters will return the most recent state. Setters will
// return immediately and do their work in the background. No functions return errors, and
// setting data on a pad which isn't connected will have no effect.
// Initialize, and start searching for devices.
//
// UpdateCallback will be called when something happens: connection or disconnection, inputs
// changed, configuration updated, test data updated, etc. It doesn't specify what's changed,
// and the user should check all state that it's interested in.
//
// This is called asynchronously from a helper thread, so the receiver must be thread-safe.
typedef void SMXUpdateCallback(int pad, SMXUpdateCallbackReason reason, void *pUser);
SMX_API void SMX_Start(SMXUpdateCallback UpdateCallback, void *pUser);
// Shut down and disconnect from all devices. This will wait for any user callbacks to complete,
// and no user callbacks will be called after this returns. This must not be called from within
// the update callback.
SMX_API void SMX_Stop();
// Set a function to receive diagnostic logs. By default, logs are written to stdout.
// This can be called before SMX_Start, so it affects any logs sent during initialization.
typedef void SMXLogCallback(const char *log);
SMX_API void SMX_SetLogCallback(SMXLogCallback callback);
// Get info about a pad. Use this to detect which pads are currently connected.
SMX_API void SMX_GetInfo(int pad, SMXInfo *info);
// Get a mask of the currently pressed panels.
SMX_API uint16_t SMX_GetInputState(int pad);
// (deprecated) Equivalent to SMX_SetLights2(lightsData, 864).
SMX_API void SMX_SetLights(const char lightData[864]);
// Update the lights. Both pads are always updated together. lightData is a list of 8-bit RGB
// colors, one for each LED.
//
// lightDataSize is the number of bytes in lightsData. This should be 1350 (2 pads * 9 panels *
// 25 lights * 3 RGB colors). For backwards-compatibility, this can also be 864.
//
// Each panel has lights in the following order:
//
// 00 01 02 03
// 16 17 18
// 04 05 06 07
// 19 20 21
// 08 09 10 11
// 22 23 24
// 12 13 14 15
//
// Panels are in the following order:
//
// 012 9AB
// 345 CDE
// 678 F01
//
// With 18 panels, 25 LEDs per panel and 3 bytes per LED, each light update has 1350 bytes of data.
//
// Lights will update at up to 30 FPS. If lights data is sent more quickly, a best effort will be
// made to send the most recent lights data available, but the panels won't update more quickly.
//
// The panels will return to automatic lighting if no lights are received for a while, so applications
// controlling lights should send light updates continually, even if the lights aren't changing.
//
// For backwards compatibility, if lightDataSize is 864, the old 4x4-only order is used,
// which simply omits lights 16-24.
SMX_API void SMX_SetLights2(const char *lightData, int lightDataSize);
// By default, the panels light automatically when stepped on. If a lights command is sent by
// the application, this stops happening to allow the application to fully control lighting.
// If no lights update is received for a few seconds, automatic lighting is reenabled by the
// panels.
//
// SMX_ReenableAutoLights can be called to immediately reenable auto-lighting, without waiting
// for the timeout period to elapse. Games don't need to call this, since the panels will return
// to auto-lighting mode automatically after a brief period of no updates.
SMX_API void SMX_ReenableAutoLights();
// Get the current controller's configuration.
//
// Return true if a configuration is available. If false is returned, no panel is connected
// and no data will be set.
SMX_API bool SMX_GetConfig(int pad, SMXConfig *config);
// Update the current controller's configuration. This doesn't block, and the new configuration will
// be sent in the background. SMX_GetConfig will return the new configuration as soon as this call
// returns, without waiting for it to actually be sent to the controller.
SMX_API void SMX_SetConfig(int pad, const SMXConfig *config);
// Reset a pad to its original configuration.
SMX_API void SMX_FactoryReset(int pad);
// Request an immediate panel recalibration. This is normally not necessary, but can be helpful
// for diagnostics.
SMX_API void SMX_ForceRecalibration(int pad);
// Set a sensor test mode and request test data. This is used by the configuration tool.
SMX_API void SMX_SetTestMode(int pad, SensorTestMode mode);
SMX_API bool SMX_GetTestData(int pad, SMXSensorTestModeData *data);
// Set a panel test mode. These only appear as debug lighting on the panel and don't
// return data to us. Lights can't be updated while a panel test mode is active.
// This applies to all connected pads.
SMX_API void SMX_SetPanelTestMode(PanelTestMode mode);
// Return the build version of the DLL, which is based on the git tag at build time. This
// is only intended for diagnostic logging, and it's also the version we show in SMXConfig.
SMX_API const char *SMX_Version();
// General info about a connected controller. This can be retrieved with SMX_GetInfo.
struct SMXInfo
{
// True if we're fully connected to this controller. If this is false, the other
// fields won't be set.
bool m_bConnected = false;
// This device's serial number. This can be used to distinguish devices from each
// other if more than one is connected. This is a null-terminated string instead
// of a C++ string for C# marshalling.
char m_Serial[33];
// This device's firmware version.
uint16_t m_iFirmwareVersion;
};
enum SMXUpdateCallbackReason : uint32_t {
// This is called when a generic state change happens: connection or disconnection, inputs changed,
// test data updated, etc. It doesn't specify what's changed. We simply check the whole state.
SMXUpdateCallback_Updated,
// This is called when SMX_FactoryReset completes, indicating that SMX_GetConfig will now return
// the reset configuration.
SMXUpdateCallback_FactoryResetCommandComplete
};
// Bits for SMXConfig::flags.
enum SMXConfigFlags {
// If set, panels will use the pressed animation when pressed, and stepColor
// is ignored. If unset, panels will be lit solid using stepColor.
// masterVersion >= 4. Previous versions always use stepColor.
PlatformFlags_AutoLightingUsePressedAnimations = 1 << 0,
// If set, panels are using FSRs, otherwise load cells.
PlatformFlags_FSR = 1 << 1,
};
#pragma pack(push, 1)
struct packed_sensor_settings_t {
// Load cell thresholds:
uint8_t loadCellLowThreshold;
uint8_t loadCellHighThreshold;
// FSR thresholds:
uint8_t fsrLowThreshold[4];
uint8_t fsrHighThreshold[4];
uint16_t combinedLowThreshold;
uint16_t combinedHighThreshold;
// This must be left unchanged.
uint16_t reserved;
};
static_assert(sizeof(packed_sensor_settings_t) == 16, "Incorrect packed_sensor_settings_t size");
// The configuration for a connected controller. This can be retrieved with SMX_GetConfig
// and modified with SMX_SetConfig.
//
// The order and packing of this struct corresponds to the configuration packet sent to
// the master controller, so it must not be changed.
struct SMXConfig
{
// The firmware version of the master controller. Where supported (version 2 and up), this
// will always read back the firmware version. This will default to 0xFF on version 1, and
// we'll always write 0xFF here so it doesn't change on that firmware version.
//
// We don't need this since we can read the "I" command which also reports the version, but
// this allows panels to also know the master version.
uint8_t masterVersion = 0xFF;
// The version of this config packet. This can be used by the firmware to know which values
// have been filled in. Any values not filled in will always be 0xFF, which can be tested
// for, but that doesn't work for values where 0xFF is a valid value. This value is unrelated
// to the firmware version, and just indicates which fields in this packet have been set.
// Note that we don't need to increase this any time we add a field, only when it's important
// that we be able to tell if a field is set or not.
//
// Versions:
// - 0xFF: This is a config packet from before configVersion was added.
// - 0x00: configVersion added
// - 0x02: panelThreshold0Low through panelThreshold8High added
// - 0x03: debounceDelayMs added
uint8_t configVersion = 0x05;
// Packed flags (masterVersion >= 4).
uint8_t flags = 0;
// Panel thresholds are labelled by their numpad position, eg. Panel8 is up.
// If m_iFirmwareVersion is 1, Panel7 corresponds to all of up, down, left and
// right, and Panel2 corresponds to UpLeft, UpRight, DownLeft and DownRight. For
// later firmware versions, each panel is configured independently.
//
// Setting a value to 0xFF disables that threshold.
// These are internal tunables and should be left unchanged.
uint16_t debounceNodelayMilliseconds = 0;
uint16_t debounceDelayMilliseconds = 0;
uint16_t panelDebounceMicroseconds = 4000;
uint8_t autoCalibrationMaxDeviation = 100;
uint8_t badSensorMinimumDelaySeconds = 15;
uint16_t autoCalibrationAveragesPerUpdate = 60;
uint16_t autoCalibrationSamplesPerAverage = 500;
// The maximum tare value to calibrate to (except on startup).
uint16_t autoCalibrationMaxTare = 0xFFFF;
// Which sensors on each panel to enable. This can be used to disable sensors that
// we know aren't populated. This is packed, with four sensors on two pads per byte:
// enabledSensors[0] & 1 is the first sensor on the first pad, and so on.
uint8_t enabledSensors[5];
// How long the master controller will wait for a lights command before assuming the
// game has gone away and resume auto-lights. This is in 128ms units.
uint8_t autoLightsTimeout = 1000/128; // 1 second
// The color to use for each panel when auto-lighting in master mode. This doesn't
// apply when the pads are in autonomous lighting mode (no master), since they don't
// store any configuration by themselves. These colors should be scaled to the 0-170
// range.
uint8_t stepColor[3*9];
// The default color to set the platform LED strip to.
uint8_t platformStripColor[3];
// Which panels to enable auto-lighting for. Disabled panels will be unlit.
// 0x01 = panel 0, 0x02 = panel 1, 0x04 = panel 2, etc. This only affects
// the master controller's built-in auto lighting and not lights data send
// from the SDK.
uint16_t autoLightPanelMask = 0xFFFF;
// The rotation of the panel, where 0 is the standard rotation, 1 means the panel is
// rotated right 90 degrees, 2 is rotated 180 degrees, and 3 is rotated 270 degrees.
// This value is unused.
uint8_t panelRotation;
// Per-panel sensor settings:
packed_sensor_settings_t panelSettings[9];
// These are internal tunables and should be left unchanged.
uint8_t preDetailsDelayMilliseconds = 5;
// Pad the struct to 250 bytes. This keeps this struct size from changing
// as we add fields, so the ABI doesn't change. Applications should leave
// any data in here unchanged when calling SMX_SetConfig.
uint8_t padding[49];
};
#pragma pack(pop)
static_assert(offsetof(SMXConfig, padding) == 201, "Incorrect padding alignment");
static_assert(sizeof(SMXConfig) == 250, "Expected 250 bytes");
// The values (except for Off) correspond with the protocol and must not be changed.
enum SensorTestMode : uint32_t {
SensorTestMode_Off = 0,
// Return the raw, uncalibrated value of each sensor.
SensorTestMode_UncalibratedValues = '0',
// Return the calibrated value of each sensor.
SensorTestMode_CalibratedValues = '1',
// Return the sensor noise value.
SensorTestMode_Noise = '2',
// Return the sensor tare value.
SensorTestMode_Tare = '3',
};
// Data for the current SensorTestMode. The interpretation of sensorLevel depends on the mode.
struct SMXSensorTestModeData
{
// If false, sensorLevel[n][*] is zero because we didn't receive a response from that panel.
bool bHaveDataFromPanel[9];
int16_t sensorLevel[9][4];
bool bBadSensorInput[9][4];
// The DIP switch settings on each panel. This is used for diagnostics displays.
int iDIPSwitchPerPanel[9];
// Bad sensor selection jumper indication for each panel.
bool iBadJumper[9][4];
};
// The values also correspond with the protocol and must not be changed.
// These are panel-side diagnostics modes.
enum PanelTestMode : uint32_t {
PanelTestMode_Off = '0',
PanelTestMode_PressureTest = '1',
};
#endif

View File

@@ -0,0 +1,341 @@
#include "Helpers.h"
#include <windows.h>
#include <wincrypt.h>
#include <algorithm>
#include <stdexcept>
using namespace std;
using namespace SMX;
namespace {
function<void(const string &log)> g_LogCallback = [](const string &log) {
printf("%6.3f: %s\n", GetMonotonicTime(), log.c_str());
};
};
void SMX::Log(string s)
{
g_LogCallback(s);
}
void SMX::Log(wstring s)
{
Log(WideStringToUTF8(s));
}
void SMX::SetLogCallback(function<void(const string &log)> callback)
{
g_LogCallback = callback;
}
const DWORD MS_VC_EXCEPTION = 0x406D1388;
#pragma pack(push,8)
typedef struct tagTHREADNAME_INFO
{
DWORD dwType; // Must be 0x1000.
LPCSTR szName; // Pointer to name (in user addr space).
DWORD dwThreadID; // Thread ID (-1=caller thread).
DWORD dwFlags; // Reserved for future use, must be zero.
} THREADNAME_INFO;
#pragma pack(pop)
void SMX::SetThreadName(DWORD iThreadId, const string &name)
{
#ifdef _MSC_VER
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = name.c_str();
info.dwThreadID = iThreadId;
info.dwFlags = 0;
#pragma warning(push)
#pragma warning(disable: 6320 6322)
__try{
RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
}
#pragma warning(pop)
#endif //_MSC_VER
}
void SMX::StripCrnl(wstring &s)
{
while(s.size() && (s[s.size()-1] == '\r' || s[s.size()-1] == '\n'))
s.erase(s.size()-1);
}
wstring SMX::GetErrorString(int err)
{
wchar_t buf[1024] = L"";
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, 0, err, 0, buf, sizeof(buf), NULL);
// Fix badly formatted strings returned by FORMAT_MESSAGE_FROM_SYSTEM.
wstring sResult = buf;
StripCrnl(sResult);
return sResult;
}
string SMX::vssprintf(const char *szFormat, va_list argList)
{
int iChars = vsnprintf(NULL, 0, szFormat, argList);
if(iChars == -1)
return string("Error formatting string: ") + szFormat;
string sStr;
sStr.resize(iChars+1);
vsnprintf((char *) sStr.data(), iChars+1, szFormat, argList);
sStr.resize(iChars);
return sStr;
}
string SMX::ssprintf(const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
return vssprintf(fmt, va);
}
wstring SMX::wvssprintf(const wchar_t *szFormat, va_list argList)
{
int iChars = _vsnwprintf(NULL, 0, szFormat, argList);
if(iChars == -1)
return wstring(L"Error formatting string: ") + szFormat;
wstring sStr;
sStr.resize(iChars+1);
_vsnwprintf((wchar_t *) sStr.data(), iChars+1, szFormat, argList);
sStr.resize(iChars);
return sStr;
}
wstring SMX::wssprintf(const wchar_t *fmt, ...)
{
va_list va;
va_start(va, fmt);
return wvssprintf(fmt, va);
}
string SMX::BinaryToHex(const void *pData_, int iNumBytes)
{
const unsigned char *pData = (const unsigned char *) pData_;
string s;
for(int i=0; i<iNumBytes; i++)
{
unsigned val = pData[i];
s += ssprintf("%02x", val);
}
return s;
}
string SMX::BinaryToHex(const string &sString)
{
return BinaryToHex(sString.data(), sString.size());
}
bool SMX::GetRandomBytes(void *pData, int iBytes)
{
HCRYPTPROV hCryptProvider = 0;
if (!CryptAcquireContext(&hCryptProvider, NULL, MS_DEF_PROV, PROV_RSA_FULL, (CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET)) &&
!CryptAcquireContext(&hCryptProvider, NULL, MS_DEF_PROV, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET | CRYPT_NEWKEYSET))
return 0;
bool bSuccess = !!CryptGenRandom(hCryptProvider, iBytes, (uint8_t *) pData);
CryptReleaseContext(hCryptProvider, 0);
return bSuccess;
}
// Monotonic timer code from https://stackoverflow.com/questions/24330496.
// Why is this hard?
//
// This code has backwards compatibility to XP, but we only officially support and
// test back to Windows 7, so that code path isn't tested.
typedef struct _KSYSTEM_TIME {
ULONG LowPart;
LONG High1Time;
LONG High2Time;
} KSYSTEM_TIME;
#define KUSER_SHARED_DATA 0x7ffe0000
#define InterruptTime ((KSYSTEM_TIME volatile*)(KUSER_SHARED_DATA + 0x08))
#define InterruptTimeBias ((ULONGLONG volatile*)(KUSER_SHARED_DATA + 0x3b0))
namespace {
LONGLONG ReadInterruptTime()
{
// Reading the InterruptTime from KUSER_SHARED_DATA is much better than
// using GetTickCount() because it doesn't wrap, and is even a little quicker.
// This works on all Windows NT versions (NT4 and up).
LONG timeHigh;
ULONG timeLow;
do {
timeHigh = InterruptTime->High1Time;
timeLow = InterruptTime->LowPart;
} while (timeHigh != InterruptTime->High2Time);
LONGLONG now = ((LONGLONG)timeHigh << 32) + timeLow;
static LONGLONG d = now;
return now - d;
}
LONGLONG ScaleQpc(LONGLONG qpc)
{
// We do the actual scaling in fixed-point rather than floating, to make sure
// that we don't violate monotonicity due to rounding errors. There's no
// need to cache QueryPerformanceFrequency().
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
double fraction = 10000000/double(frequency.QuadPart);
LONGLONG denom = 1024;
LONGLONG numer = max(1LL, (LONGLONG)(fraction*denom + 0.5));
return qpc * numer / denom;
}
ULONGLONG ReadUnbiasedQpc()
{
// We remove the suspend bias added to QueryPerformanceCounter results by
// subtracting the interrupt time bias, which is not strictly speaking legal,
// but the units are correct and I think it's impossible for the resulting
// "unbiased QPC" value to go backwards.
LONGLONG interruptTimeBias, qpc;
do {
interruptTimeBias = *InterruptTimeBias;
LARGE_INTEGER counter;
QueryPerformanceCounter(&counter);
qpc = counter.QuadPart;
} while (interruptTimeBias != static_cast<LONGLONG>(*InterruptTimeBias));
static std::pair<LONGLONG,LONGLONG> d(qpc, interruptTimeBias);
return ScaleQpc(qpc - d.first) - (interruptTimeBias - d.second);
}
bool Win7OrLater()
{
static int iWin7OrLater = -1;
if(iWin7OrLater != -1)
return bool(iWin7OrLater);
OSVERSIONINFOW ver = {};
ver.dwOSVersionInfoSize = sizeof(OSVERSIONINFOW);
GetVersionExW(&ver);
iWin7OrLater = (ver.dwMajorVersion > 6 || (ver.dwMajorVersion == 6 && ver.dwMinorVersion >= 1));
return bool(iWin7OrLater);
}
}
/// getMonotonicTime() returns the time elapsed since the application's first
/// call to getMonotonicTime(), in 100ns units. The values returned are
/// guaranteed to be monotonic. The time ticks in 15ms resolution and advances
/// during suspend on XP and Vista, but we manage to avoid this on Windows 7
/// and 8, which also use a high-precision timer. The time does not wrap after
/// 49 days.
double SMX::GetMonotonicTime()
{
// On Windows XP and earlier, QueryPerformanceCounter is not monotonic so we
// steer well clear of it; on Vista, it's just a bit slow.
uint64_t iTime = Win7OrLater()? ReadUnbiasedQpc() : ReadInterruptTime();
return iTime / 10000000.0;
}
void SMX::GenerateRandom(void *pOut, int iSize)
{
// These calls shouldn't fail.
HCRYPTPROV cryptProv;
if(!CryptAcquireContextW(&cryptProv, nullptr,
L"Microsoft Base Cryptographic Provider v1.0",
PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
throw runtime_error("CryptAcquireContext error");
if(!CryptGenRandom(cryptProv, iSize, (BYTE *) pOut))
throw runtime_error("CryptGenRandom error");
if(!CryptReleaseContext(cryptProv, 0))
throw runtime_error("CryptReleaseContext error");
}
string SMX::WideStringToUTF8(wstring s)
{
if(s.empty())
return "";
int iBytes = WideCharToMultiByte( CP_ACP, 0, s.data(), s.size(), NULL, 0, NULL, FALSE );
string ret;
ret.resize(iBytes);
WideCharToMultiByte( CP_ACP, 0, s.data(), s.size(), (char *) ret.data(), iBytes, NULL, FALSE );
return ret;
}
const char *SMX::CreateError(string error)
{
// Store the string in a static so it doesn't get deallocated.
static string buf;
buf = error;
return buf.c_str();
}
SMX::AutoCloseHandle::AutoCloseHandle(HANDLE h)
{
handle = h;
}
SMX::AutoCloseHandle::~AutoCloseHandle()
{
if(handle != INVALID_HANDLE_VALUE)
CloseHandle(handle);
}
SMX::Mutex::Mutex()
{
m_hLock = CreateMutex(NULL, false, NULL);
}
SMX::Mutex::~Mutex()
{
CloseHandle(m_hLock);
}
void SMX::Mutex::Lock()
{
WaitForSingleObject(m_hLock, INFINITE);
m_iLockedByThread = GetCurrentThreadId();
}
void SMX::Mutex::Unlock()
{
m_iLockedByThread = 0;
ReleaseMutex(m_hLock);
}
void SMX::Mutex::AssertNotLockedByCurrentThread()
{
if(m_iLockedByThread == GetCurrentThreadId())
throw runtime_error("Expected to not be locked");
}
void SMX::Mutex::AssertLockedByCurrentThread()
{
if(m_iLockedByThread != GetCurrentThreadId())
throw runtime_error("Expected to be locked");
}
SMX::LockMutex::LockMutex(SMX::Mutex &mutex):
m_Mutex(mutex)
{
m_Mutex.AssertNotLockedByCurrentThread();
m_Mutex.Lock();
}
SMX::LockMutex::~LockMutex()
{
m_Mutex.AssertLockedByCurrentThread();
m_Mutex.Unlock();
}
// This is a helper to let the config tool open a window, which has no freopen.
// This isn't exposed in SMX.h.
// extern "C" __declspec(dllexport) void SMX_Internal_OpenConsole()
// {
// AllocConsole();
// freopen("CONOUT$","wb", stdout);
// freopen("CONOUT$","wb", stderr);
// }

View File

@@ -0,0 +1,152 @@
#ifndef HELPERS_H
#define HELPERS_H
#include <string>
#include <stdarg.h>
#include <windows.h>
#include <functional>
#include <memory>
#include <vector>
using namespace std;
namespace SMX
{
void Log(string s);
void Log(wstring s);
// Set a function to receive logs written by SMX::Log. By default, logs are written
// to stdout.
void SetLogCallback(function<void(const string &log)> callback);
void SetThreadName(DWORD iThreadId, const string &name);
void StripCrnl(wstring &s);
wstring GetErrorString(int err);
string vssprintf(const char *szFormat, va_list argList);
string ssprintf(const char *fmt, ...);
wstring wvssprintf(const wchar_t *szFormat, va_list argList);
wstring wssprintf(const wchar_t *fmt, ...);
string BinaryToHex(const void *pData_, int iNumBytes);
string BinaryToHex(const string &sString);
bool GetRandomBytes(void *pData, int iBytes);
double GetMonotonicTime();
void GenerateRandom(void *pOut, int iSize);
string WideStringToUTF8(wstring s);
// Create a char* string that will be valid until the next call to CreateError.
// This is used to return error messages to the caller.
const char *CreateError(string error);
#define arraylen(a) (sizeof(a) / sizeof((a)[0]))
// In order to be able to use smart pointers to fully manage an object, we need to get
// a shared_ptr to pass around, but also store a weak_ptr in the object itself. This
// lets the object create shared_ptrs for itself as needed, without keeping itself from
// being deallocated.
//
// This helper allows this pattern:
//
// struct Class
// {
// Class(shared_ptr<Class> &pSelf): m_pSelf(GetPointers(pSelf, this)) { }
// const weak_ptr<Class> m_pSelf;
// };
//
// shared_ptr<Class> obj;
// new Class(obj);
//
// For a more convenient way to invoke this, see CreateObj() below.
template<typename T>
weak_ptr<T> GetPointers(shared_ptr<T> &pSharedPtr, T *pObj)
{
pSharedPtr.reset(pObj);
return pSharedPtr;
}
// Create a class that retains a weak reference to itself, returning a shared_ptr.
template<typename T, class... Args>
shared_ptr<T> CreateObj(Args&&... args)
{
shared_ptr<T> pResult;
new T(pResult, std::forward<Args>(args)...);
return dynamic_pointer_cast<T>(pResult);
}
class AutoCloseHandle
{
public:
AutoCloseHandle(HANDLE h);
~AutoCloseHandle();
HANDLE value() const { return handle; }
private:
AutoCloseHandle(const AutoCloseHandle &rhs);
AutoCloseHandle &operator=(const AutoCloseHandle &rhs);
HANDLE handle;
};
class Mutex
{
public:
Mutex();
~Mutex();
void Lock();
void Unlock();
void AssertNotLockedByCurrentThread();
void AssertLockedByCurrentThread();
private:
HANDLE m_hLock = INVALID_HANDLE_VALUE;
DWORD m_iLockedByThread = 0;
};
// A local lock helper for Mutex.
class LockMutex
{
public:
LockMutex(Mutex &mutex);
~LockMutex();
private:
Mutex &m_Mutex;
};
class Event
{
public:
Event(Mutex &lock):
m_Lock(lock)
{
m_hEvent = make_shared<AutoCloseHandle>(CreateEvent(NULL, false, false, NULL));
}
void Set()
{
SetEvent(m_hEvent->value());
}
// Unlock m_Lock, wait up to iDelayMilliseconds for the event to be set,
// then lock m_Lock. If iDelayMilliseconds is -1, wait forever.
void Wait(int iDelayMilliseconds)
{
if(iDelayMilliseconds == -1)
iDelayMilliseconds = INFINITE;
m_Lock.AssertLockedByCurrentThread();
m_Lock.Unlock();
vector<HANDLE> aHandles = { m_hEvent->value() };
WaitForSingleObjectEx(m_hEvent->value(), iDelayMilliseconds, true);
m_Lock.Lock();
}
private:
shared_ptr<SMX::AutoCloseHandle> m_hEvent;
Mutex &m_Lock;
};
}
#endif

View File

@@ -0,0 +1,124 @@
// This implements the public API.
#include <windows.h>
#include <memory>
#include "../SMX.h"
#include "SMXManager.h"
#include "SMXDevice.h"
#include "SMXBuildVersion.h"
#include "SMXPanelAnimation.h" // for SMX_LightsAnimation_SetAuto
using namespace std;
using namespace SMX;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
// DLL interface:
SMX_API void SMX_Start(SMXUpdateCallback callback, void *pUser)
{
if(SMXManager::g_pSMX != NULL)
return;
// The C++ interface takes a std::function, which doesn't need a user pointer. We add
// one for the C interface for convenience.
auto UpdateCallback = [callback, pUser](int pad, SMXUpdateCallbackReason reason) {
callback(pad, reason, pUser);
};
// Log(ssprintf("Struct sizes (native): %i %i %i\n", sizeof(SMXConfig), sizeof(SMXInfo), sizeof(SMXSensorTestModeData)));
SMXManager::g_pSMX = make_shared<SMXManager>(UpdateCallback);
}
SMX_API void SMX_Stop()
{
// If lights animation is running, shut it down first.
SMX_LightsAnimation_SetAuto(false);
SMXManager::g_pSMX.reset();
}
SMX_API void SMX_SetLogCallback(SMXLogCallback callback)
{
// Wrap the C callback with a C++ one.
SMX::SetLogCallback([callback](const string &log) {
callback(log.c_str());
});
}
SMX_API bool SMX_GetConfig(int pad, SMXConfig *config) { return SMXManager::g_pSMX->GetDevice(pad)->GetConfig(*config); }
SMX_API void SMX_SetConfig(int pad, const SMXConfig *config) { SMXManager::g_pSMX->GetDevice(pad)->SetConfig(*config); }
SMX_API void SMX_GetInfo(int pad, SMXInfo *info) { SMXManager::g_pSMX->GetDevice(pad)->GetInfo(*info); }
SMX_API uint16_t SMX_GetInputState(int pad) { return SMXManager::g_pSMX->GetDevice(pad)->GetInputState(); }
SMX_API void SMX_FactoryReset(int pad) { SMXManager::g_pSMX->GetDevice(pad)->FactoryReset(); }
SMX_API void SMX_ForceRecalibration(int pad) { SMXManager::g_pSMX->GetDevice(pad)->ForceRecalibration(); }
SMX_API void SMX_SetTestMode(int pad, SensorTestMode mode) { SMXManager::g_pSMX->GetDevice(pad)->SetSensorTestMode(mode); }
SMX_API bool SMX_GetTestData(int pad, SMXSensorTestModeData *data) { return SMXManager::g_pSMX->GetDevice(pad)->GetTestData(*data); }
SMX_API void SMX_SetPanelTestMode(PanelTestMode mode) { SMXManager::g_pSMX->SetPanelTestMode(mode); }
SMX_API void SMX_SetLights(const char lightData[864])
{
SMX_SetLights2(lightData, 864);
}
SMX_API void SMX_SetLights2(const char *lightData, int lightDataSize)
{
// The lightData into data per pad depending on whether we've been
// given 16 or 25 lights of data.
string lights[2];
const int BytesPerPad16 = 9*16*3;
const int BytesPerPad25 = 9*25*3;
if(lightDataSize == 2*BytesPerPad16)
{
lights[0] = string(lightData, BytesPerPad16);
lights[1] = string(lightData + BytesPerPad16, BytesPerPad16);
}
else if(lightDataSize == 2*BytesPerPad25)
{
lights[0] = string(lightData, BytesPerPad25);
lights[1] = string(lightData + BytesPerPad25, BytesPerPad25);
}
else
{
Log(ssprintf("SMX_SetLights2: lightDataSize is invalid (must be %i or %i)\n",
2*BytesPerPad16, 2*BytesPerPad25));
return;
}
SMXManager::g_pSMX->SetLights(lights);
// If we're running auto animations, stop them when we get an API call to set lights.
SMXAutoPanelAnimations::TemporaryStopAnimating();
}
// This is internal for SMXConfig. These lights aren't meant to be animated.
SMX_API void SMX_SetPlatformLights(const char lightData[88*3], int lightDataSize)
{
if(lightDataSize != 88*3)
{
Log(ssprintf("SMX_SetPlatformLights: lightDataSize is invalid (must be %i)\n",
88*3));
return;
}
string lights[2];
lights[0] = string(lightData, 44*3);
lights[1] = string(lightData + 44*3, 44*3);
SMXManager::g_pSMX->SetPlatformLights(lights);
}
SMX_API void SMX_ReenableAutoLights() { SMXManager::g_pSMX->ReenableAutoLights(); }
SMX_API const char *SMX_Version() { return SMX_BUILD_VERSION; }
// These aren't exposed in the public API, since they're only used internally.
SMX_API void SMX_SetOnlySendLightsOnChange(bool value) { SMXManager::g_pSMX->SetOnlySendLightsOnChange(value); }
SMX_API void SMX_SetSerialNumbers() { SMXManager::g_pSMX->SetSerialNumbers(); }

View File

@@ -0,0 +1,9 @@
// This file is auto-generated by update-build-version.bat in the SDK when building the release DLL
// in spice2x, a fixed value will be used as the SDK is staticaly linked
#ifndef SMXBuildVersion_h
#define SMXBuildVersion_h
#define SMX_BUILD_VERSION "00000001"
#endif

View File

@@ -0,0 +1,170 @@
#include "SMXConfigPacket.h"
#include <stdint.h>
#include <stddef.h>
#include <cstring>
// The config packet format changed in version 5. This handles compatibility with
// the old configuration packet. The config packet in SMX.h matches the new format.
//
#pragma pack(push, 1)
struct OldSMXConfig
{
uint8_t unused1 = 0xFF, unused2 = 0xFF;
uint8_t unused3 = 0xFF, unused4 = 0xFF;
uint8_t unused5 = 0xFF, unused6 = 0xFF;
uint16_t masterDebounceMilliseconds = 0;
uint8_t panelThreshold7Low = 0xFF, panelThreshold7High = 0xFF; // was "cardinal"
uint8_t panelThreshold4Low = 0xFF, panelThreshold4High = 0xFF; // was "center"
uint8_t panelThreshold2Low = 0xFF, panelThreshold2High = 0xFF; // was "corner"
uint16_t panelDebounceMicroseconds = 4000;
uint16_t autoCalibrationPeriodMilliseconds = 1000;
uint8_t autoCalibrationMaxDeviation = 100;
uint8_t badSensorMinimumDelaySeconds = 15;
uint16_t autoCalibrationAveragesPerUpdate = 60;
uint8_t unused7 = 0xFF, unused8 = 0xFF;
uint8_t panelThreshold1Low = 0xFF, panelThreshold1High = 0xFF; // was "up"
uint8_t enabledSensors[5];
uint8_t autoLightsTimeout = 1000/128; // 1 second
uint8_t stepColor[3*9];
uint8_t panelRotation;
uint16_t autoCalibrationSamplesPerAverage = 500;
uint8_t masterVersion = 0xFF;
uint8_t configVersion = 0x03;
uint8_t unused9[10];
uint8_t panelThreshold0Low, panelThreshold0High;
uint8_t panelThreshold3Low, panelThreshold3High;
uint8_t panelThreshold5Low, panelThreshold5High;
uint8_t panelThreshold6Low, panelThreshold6High;
uint8_t panelThreshold8Low, panelThreshold8High;
uint16_t debounceDelayMilliseconds = 0;
uint8_t padding[164];
};
#pragma pack(pop)
static_assert(offsetof(OldSMXConfig, padding) == 86, "Incorrect padding alignment");
static_assert(sizeof(OldSMXConfig) == 250, "Expected 250 bytes");
void ConvertToNewConfig(const vector<uint8_t> &oldConfigData, SMXConfig &newConfig)
{
// Copy data in its order within OldSMXConfig. This lets us easily stop at each
// known packet version. Any fields that aren't present in oldConfigData will be
// left at their default values in SMXConfig.
const OldSMXConfig &oldConfig = (OldSMXConfig &) *oldConfigData.data();
newConfig.debounceNodelayMilliseconds = oldConfig.masterDebounceMilliseconds;
newConfig.panelSettings[7].loadCellLowThreshold = oldConfig.panelThreshold7Low;
newConfig.panelSettings[4].loadCellLowThreshold = oldConfig.panelThreshold4Low;
newConfig.panelSettings[2].loadCellLowThreshold = oldConfig.panelThreshold2Low;
newConfig.panelSettings[7].loadCellHighThreshold = oldConfig.panelThreshold7High;
newConfig.panelSettings[4].loadCellHighThreshold = oldConfig.panelThreshold4High;
newConfig.panelSettings[2].loadCellHighThreshold = oldConfig.panelThreshold2High;
newConfig.panelDebounceMicroseconds = oldConfig.panelDebounceMicroseconds;
newConfig.autoCalibrationMaxDeviation = oldConfig.autoCalibrationMaxDeviation;
newConfig.badSensorMinimumDelaySeconds = oldConfig.badSensorMinimumDelaySeconds;
newConfig.autoCalibrationAveragesPerUpdate = oldConfig.autoCalibrationAveragesPerUpdate;
newConfig.panelSettings[1].loadCellLowThreshold = oldConfig.panelThreshold1Low;
newConfig.panelSettings[1].loadCellHighThreshold = oldConfig.panelThreshold1High;
memcpy(newConfig.enabledSensors, oldConfig.enabledSensors, sizeof(newConfig.enabledSensors));
newConfig.autoLightsTimeout = oldConfig.autoLightsTimeout;
memcpy(newConfig.stepColor, oldConfig.stepColor, sizeof(newConfig.stepColor));
newConfig.panelRotation = oldConfig.panelRotation;
newConfig.autoCalibrationSamplesPerAverage = oldConfig.autoCalibrationSamplesPerAverage;
if(oldConfig.configVersion == 0xFF)
return;
newConfig.masterVersion = oldConfig.masterVersion;
newConfig.configVersion = oldConfig.configVersion;
if(oldConfig.configVersion < 2)
return;
newConfig.panelSettings[0].loadCellLowThreshold = oldConfig.panelThreshold0Low;
newConfig.panelSettings[3].loadCellLowThreshold = oldConfig.panelThreshold3Low;
newConfig.panelSettings[5].loadCellLowThreshold = oldConfig.panelThreshold5Low;
newConfig.panelSettings[6].loadCellLowThreshold = oldConfig.panelThreshold6Low;
newConfig.panelSettings[8].loadCellLowThreshold = oldConfig.panelThreshold8Low;
newConfig.panelSettings[0].loadCellHighThreshold = oldConfig.panelThreshold0High;
newConfig.panelSettings[3].loadCellHighThreshold = oldConfig.panelThreshold3High;
newConfig.panelSettings[5].loadCellHighThreshold = oldConfig.panelThreshold5High;
newConfig.panelSettings[6].loadCellHighThreshold = oldConfig.panelThreshold6High;
newConfig.panelSettings[8].loadCellHighThreshold = oldConfig.panelThreshold8High;
if(oldConfig.configVersion < 3)
return;
newConfig.debounceDelayMilliseconds = oldConfig.debounceDelayMilliseconds;
}
// oldConfigData contains the data we're replacing. Any fields that exist in the old
// config format and not the new one will be left unchanged.
void ConvertToOldConfig(const SMXConfig &newConfig, vector<uint8_t> &oldConfigData)
{
OldSMXConfig &oldConfig = (OldSMXConfig &) *oldConfigData.data();
// We don't need to check configVersion here. It's safe to set all fields in
// the output config packet. If oldConfigData isn't 128 bytes, extend it.
if(oldConfigData.size() < 128)
oldConfigData.resize(128, 0xFF);
oldConfig.masterDebounceMilliseconds = newConfig.debounceNodelayMilliseconds;
oldConfig.panelThreshold7Low = newConfig.panelSettings[7].loadCellLowThreshold;
oldConfig.panelThreshold4Low = newConfig.panelSettings[4].loadCellLowThreshold;
oldConfig.panelThreshold2Low = newConfig.panelSettings[2].loadCellLowThreshold;
oldConfig.panelThreshold7High = newConfig.panelSettings[7].loadCellHighThreshold;
oldConfig.panelThreshold4High = newConfig.panelSettings[4].loadCellHighThreshold;
oldConfig.panelThreshold2High = newConfig.panelSettings[2].loadCellHighThreshold;
oldConfig.panelDebounceMicroseconds = newConfig.panelDebounceMicroseconds;
oldConfig.autoCalibrationMaxDeviation = newConfig.autoCalibrationMaxDeviation;
oldConfig.badSensorMinimumDelaySeconds = newConfig.badSensorMinimumDelaySeconds;
oldConfig.autoCalibrationAveragesPerUpdate = newConfig.autoCalibrationAveragesPerUpdate;
oldConfig.panelThreshold1Low = newConfig.panelSettings[1].loadCellLowThreshold;
oldConfig.panelThreshold1High = newConfig.panelSettings[1].loadCellHighThreshold;
memcpy(oldConfig.enabledSensors, newConfig.enabledSensors, sizeof(newConfig.enabledSensors));
oldConfig.autoLightsTimeout = newConfig.autoLightsTimeout;
memcpy(oldConfig.stepColor, newConfig.stepColor, sizeof(newConfig.stepColor));
oldConfig.panelRotation = newConfig.panelRotation;
oldConfig.autoCalibrationSamplesPerAverage = newConfig.autoCalibrationSamplesPerAverage;
oldConfig.masterVersion = newConfig.masterVersion;
oldConfig.configVersion= newConfig.configVersion;
oldConfig.panelThreshold0Low = newConfig.panelSettings[0].loadCellLowThreshold;
oldConfig.panelThreshold3Low = newConfig.panelSettings[3].loadCellLowThreshold;
oldConfig.panelThreshold5Low = newConfig.panelSettings[5].loadCellLowThreshold;
oldConfig.panelThreshold6Low = newConfig.panelSettings[6].loadCellLowThreshold;
oldConfig.panelThreshold8Low = newConfig.panelSettings[8].loadCellLowThreshold;
oldConfig.panelThreshold0High = newConfig.panelSettings[0].loadCellHighThreshold;
oldConfig.panelThreshold3High = newConfig.panelSettings[3].loadCellHighThreshold;
oldConfig.panelThreshold5High = newConfig.panelSettings[5].loadCellHighThreshold;
oldConfig.panelThreshold6High = newConfig.panelSettings[6].loadCellHighThreshold;
oldConfig.panelThreshold8High = newConfig.panelSettings[8].loadCellHighThreshold;
oldConfig.debounceDelayMilliseconds = newConfig.debounceDelayMilliseconds;
}

View File

@@ -0,0 +1,12 @@
#ifndef SMXConfigPacket_h
#define SMXConfigPacket_h
#include <vector>
using namespace std;
#include "../SMX.h"
void ConvertToNewConfig(const vector<uint8_t> &oldConfig, SMXConfig &newConfig);
void ConvertToOldConfig(const SMXConfig &newConfig, vector<uint8_t> &oldConfigData);
#endif

View File

@@ -0,0 +1,607 @@
#include "SMXDevice.h"
#include "../SMX.h"
#include "Helpers.h"
#include "SMXDeviceConnection.h"
#include "SMXDeviceSearch.h"
#include "SMXConfigPacket.h"
#include <windows.h>
#include <memory>
#include <vector>
#include <map>
using namespace std;
using namespace SMX;
// Extract test data for panel iPanel.
static void ReadDataForPanel(const vector<uint16_t> &data, int iPanel, void *pOut, int iOutSize)
{
size_t m_iBit = 0;
uint8_t *p = (uint8_t *) pOut;
// Read each byte.
for(int i = 0; i < iOutSize; ++i)
{
// Read each bit in this byte.
uint8_t result = 0;
for(int j = 0; j < 8; ++j)
{
bool bit = false;
if(m_iBit < data.size())
{
bit = data[m_iBit] & (1 << iPanel);
m_iBit++;
}
result |= bit << j;
}
*p++ = result;
}
}
shared_ptr<SMXDevice> SMX::SMXDevice::Create(shared_ptr<AutoCloseHandle> hEvent, Mutex &lock)
{
return CreateObj<SMXDevice>(hEvent, lock);
}
SMX::SMXDevice::SMXDevice(shared_ptr<SMXDevice> &pSelf, shared_ptr<AutoCloseHandle> hEvent, Mutex &lock):
m_hEvent(hEvent),
m_Lock(lock),
m_pSelf(GetPointers(pSelf, this))
{
m_pConnection = SMXDeviceConnection::Create();
}
SMX::SMXDevice::~SMXDevice()
{
}
bool SMX::SMXDevice::OpenDeviceHandle(shared_ptr<AutoCloseHandle> pHandle, wstring &sError)
{
m_Lock.AssertLockedByCurrentThread();
return m_pConnection->Open(pHandle, sError);
}
void SMX::SMXDevice::CloseDevice()
{
m_Lock.AssertLockedByCurrentThread();
m_pConnection->Close();
m_bHaveConfig = false;
m_bSendConfig = false;
m_bSendingConfig = false;
m_bWaitingForConfigResponse = false;
CallUpdateCallback(SMXUpdateCallback_Updated);
}
shared_ptr<AutoCloseHandle> SMX::SMXDevice::GetDeviceHandle() const
{
return m_pConnection->GetDeviceHandle();
}
void SMX::SMXDevice::SetUpdateCallback(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback)
{
LockMutex Lock(m_Lock);
m_pUpdateCallback = pCallback;
}
bool SMX::SMXDevice::IsConnected() const
{
m_Lock.AssertNotLockedByCurrentThread();
// Don't expose the device as connected until we've read the current configuration.
LockMutex Lock(m_Lock);
return IsConnectedLocked();
}
bool SMX::SMXDevice::IsConnectedLocked() const
{
m_Lock.AssertLockedByCurrentThread();
return m_pConnection->IsConnectedWithDeviceInfo() && m_bHaveConfig;
}
void SMX::SMXDevice::SendCommand(string cmd, function<void(string response)> pComplete)
{
LockMutex Lock(m_Lock);
SendCommandLocked(cmd, pComplete);
}
void SMX::SMXDevice::SendCommandLocked(string cmd, function<void(string response)> pComplete)
{
m_Lock.AssertLockedByCurrentThread();
if(!m_pConnection->IsConnected())
{
// If we're not connected, just call pComplete.
if(pComplete)
pComplete("");
return;
}
// This call is nonblocking, so it's safe to do this in the UI thread.
m_pConnection->SendCommand(cmd, pComplete);
// Wake up the communications thread to send the message.
if(m_hEvent)
SetEvent(m_hEvent->value());
}
void SMX::SMXDevice::GetInfo(SMXInfo &info)
{
LockMutex Lock(m_Lock);
GetInfoLocked(info);
}
void SMX::SMXDevice::GetInfoLocked(SMXInfo &info)
{
m_Lock.AssertLockedByCurrentThread();
info = SMXInfo();
info.m_bConnected = IsConnectedLocked();
if(!info.m_bConnected)
return;
// Copy fields from the low-level device info to the high-level struct.
// These are kept separate because the interface depends on the format
// of SMXInfo, but it doesn't care about anything inside SMXDeviceConnection.
SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo();
memcpy(info.m_Serial, deviceInfo.m_Serial, sizeof(info.m_Serial));
info.m_iFirmwareVersion = deviceInfo.m_iFirmwareVersion;
}
bool SMX::SMXDevice::IsPlayer2Locked() const
{
m_Lock.AssertLockedByCurrentThread();
if(!IsConnectedLocked())
return false;
return m_pConnection->GetDeviceInfo().m_bP2;
}
bool SMX::SMXDevice::GetConfig(SMXConfig &configOut)
{
LockMutex Lock(m_Lock);
return GetConfigLocked(configOut);
}
bool SMX::SMXDevice::GetConfigLocked(SMXConfig &configOut)
{
m_Lock.AssertLockedByCurrentThread();
// If SetConfig was called to write a new configuration but we haven't sent it
// yet, return it instead of the configuration we read alst, so GetConfig
// immediately after SetConfig returns the value the caller expects set.
if(m_bSendConfig)
configOut = wanted_config;
else
configOut = config;
return m_bHaveConfig;
}
void SMX::SMXDevice::SetConfig(const SMXConfig &newConfig)
{
LockMutex Lock(m_Lock);
wanted_config = newConfig;
m_bSendConfig = true;
}
uint16_t SMX::SMXDevice::GetInputState() const
{
LockMutex Lock(m_Lock);
return m_pConnection->GetInputState();
}
void SMX::SMXDevice::FactoryReset()
{
// Send a factory reset command, and then read the new configuration.
LockMutex Lock(m_Lock);
SendCommandLocked("f\n");
SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo();
SendCommandLocked(deviceInfo.m_iFirmwareVersion >= 5? "G":"g\n",
[&](string response) {
// We now have the new configuration.
m_Lock.AssertLockedByCurrentThread();
if(deviceInfo.m_iFirmwareVersion >= 5)
{
// Factory reset resets the platform strip color saved to the configuration, but doesn't
// apply it to the lights. Do that now.
string sLightCommand;
sLightCommand.push_back('L');
sLightCommand.push_back(0); // LED strip index (always 0)
sLightCommand.push_back(44); // number of LEDs to set
// config.platformStripColor[0];
for(int i = 0; i < 44; ++i)
{
sLightCommand += config.platformStripColor[0];
sLightCommand += config.platformStripColor[1];
sLightCommand += config.platformStripColor[2];
}
SendCommandLocked(sLightCommand);
}
CallUpdateCallback(SMXUpdateCallback_FactoryResetCommandComplete);
});
}
void SMX::SMXDevice::ForceRecalibration()
{
LockMutex Lock(m_Lock);
SendCommandLocked("C\n");
}
void SMX::SMXDevice::SetSensorTestMode(SensorTestMode mode)
{
LockMutex Lock(m_Lock);
m_SensorTestMode = mode;
}
bool SMX::SMXDevice::GetTestData(SMXSensorTestModeData &data)
{
LockMutex Lock(m_Lock);
// Stop if we haven't read test mode data yet.
if(!m_HaveSensorTestModeData)
return false;
data = m_SensorTestData;
return true;
}
void SMX::SMXDevice::CallUpdateCallback(SMXUpdateCallbackReason reason)
{
m_Lock.AssertLockedByCurrentThread();
if(!m_pUpdateCallback)
return;
SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo();
m_pUpdateCallback(deviceInfo.m_bP2? 1:0, reason);
}
void SMX::SMXDevice::HandlePackets()
{
m_Lock.AssertLockedByCurrentThread();
while(1)
{
string buf;
if(!m_pConnection->ReadPacket(buf))
break;
if(buf.empty())
continue;
switch(buf[0])
{
case 'y':
HandleSensorTestDataResponse(buf);
break;
// 'g' is sent by firmware versions 1-4. Version 5 and newer send 'G', to ensure
// older code doesn't misinterpret the modified config packet format.
case 'g':
case 'G':
{
// This command reads back the configuration we wrote with 'w', or the defaults if
// we haven't written any.
if(buf.size() < 2)
{
Log("Communication error: invalid configuration packet");
continue;
}
size_t iSize = (uint8_t) buf[1];
if(buf.size() < iSize+2)
{
Log("Communication error: invalid configuration packet");
continue;
}
// Store the raw config data in rawConfig. For V1-4 firmwares, this is the
// old config format.
rawConfig.resize(iSize);
memcpy(rawConfig.data(), buf.data()+2, min(static_cast<size_t>(iSize), sizeof(config)));
if(buf[0] == 'g')
{
// Convert the old config format to the new one, so the rest of the SDK and
// user code doesn't need to deal with multiple formats.
ConvertToNewConfig(rawConfig, config);
}
else
{
// This is the new config format. Copy it directly into config.
memcpy(&config, buf.data()+2, min(iSize, sizeof(config)));
}
m_bHaveConfig = true;
buf.erase(buf.begin(), buf.begin()+iSize+2);
// Log(ssprintf("Read back configuration: %i bytes, first byte %i", iSize, buf[2]));
CallUpdateCallback(SMXUpdateCallback_Updated);
break;
}
}
}
}
// If m_bSendConfig is true, send the configuration to the pad. Note that while the game
// always sends its configuration, so the pad is configured according to the game's configuration,
// we only change the configuration if the user changes something so we don't overwrite
// his configuration.
void SMX::SMXDevice::SendConfig()
{
m_Lock.AssertLockedByCurrentThread();
if(!m_pConnection->IsConnected() || !m_bSendConfig || m_bSendingConfig)
return;
// We can't update the configuration until we've received the device's previous
// configuration.
if(!m_bHaveConfig)
return;
// If we're still waiting for a previous configuration to read back, don't send
// another yet.
if(m_bWaitingForConfigResponse)
return;
// Rate limit updating the configuration, to prevent excess EEPROM wear. This is just
// a safeguard in case applications try to change the configuration in realtime. If we've
// written the configuration recently, stop. We'll write the most recent configuration
// once enough time has passed. This is hidden to the application, since GetConfig returns
// wanted_config if it's set.
// const float fTimeBetweenConfigUpdates = 1.0f;
double fNow = SMX::GetMonotonicTime();
if(m_fDelayConfigUpdatesUntil > fNow)
return;
m_fDelayConfigUpdatesUntil = fNow + 1.0f;
SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo();
// Write configuration command. This is "w" in versions 1-4, and "W" in versions 5
// and newer.
string sData = ssprintf(deviceInfo.m_iFirmwareVersion >= 5? "W":"w");
// Append the config packet.
if(deviceInfo.m_iFirmwareVersion < 5)
{
// Convert wanted_config to the old configuration format.
vector<uint8_t> outputConfig = rawConfig;
ConvertToOldConfig(wanted_config, outputConfig);
uint8_t iSize = (uint8_t) outputConfig.size();
sData.append((char *) &iSize, sizeof(iSize));
sData.append((char *) outputConfig.data(), outputConfig.size());
}
else
{
int8_t iSize = sizeof(SMXConfig);
sData.append((char *) &iSize, sizeof(iSize));
sData.append((char *) &wanted_config, sizeof(wanted_config));
}
// Don't send another config packet until this one finishes, so if we get a bunch of
// SetConfig calls quickly we won't spam the device, which can get slow.
m_bSendingConfig = true;
SendCommandLocked(sData, [&](string response) {
m_bSendingConfig = false;
});
m_bSendConfig = false;
// Assume the configuration is what we just sent, so calls to GetConfig will
// continue to return it. Otherwise, they'd return the old values until the
// command below completes.
config = wanted_config;
// Don't send another configuration packet until we receive the response to the above
// command. If we're sending updates quickly (eg. dragging the color slider), we can
// send multiple updates before we get a response.
m_bWaitingForConfigResponse = true;
// After we write the configuration, read back the updated configuration to
// verify it. This command is "g" in versions 1-4, and "G" in versions 5 and
// newer.
SendCommandLocked(
deviceInfo.m_iFirmwareVersion >= 5? "G":"g\n", [this](string response) {
m_bWaitingForConfigResponse = false;
});
}
void SMX::SMXDevice::Update(wstring &sError)
{
m_Lock.AssertLockedByCurrentThread();
if(!m_pConnection->IsConnected())
return;
CheckActive();
SendConfig();
UpdateSensorTestMode();
{
uint16_t iOldState = m_pConnection->GetInputState();
// Process any received packets, and start sending any waiting packets.
m_pConnection->Update(sError);
if(!sError.empty())
return;
// If the inputs changed from packets we just processed, call the update callback.
if(iOldState != m_pConnection->GetInputState())
CallUpdateCallback(SMXUpdateCallback_Updated);
}
HandlePackets();
}
void SMX::SMXDevice::CheckActive()
{
m_Lock.AssertLockedByCurrentThread();
// If there's no connected device, or we've already activated it, we have nothing to do.
if(!m_pConnection->IsConnectedWithDeviceInfo() || m_pConnection->GetActive())
return;
m_pConnection->SetActive(true);
SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo();
// Read the current configuration. The device will return a "g" or "G" response
// containing its current SMXConfig.
SendCommandLocked(deviceInfo.m_iFirmwareVersion >= 5? "G":"g\n");
}
// Check if we need to request test mode data.
void SMX::SMXDevice::UpdateSensorTestMode()
{
m_Lock.AssertLockedByCurrentThread();
if(m_SensorTestMode == SensorTestMode_Off)
return;
// Request sensor data from the master. Don't send this if we have a request outstanding
// already.
uint32_t now = GetTickCount();
if(m_WaitingForSensorTestModeResponse != SensorTestMode_Off)
{
// This request should be quick. If we haven't received a response in a long
// time, assume the request wasn't received.
if(now - m_SentSensorTestModeRequestAtTicks < 2000)
return;
}
// Send the request.
m_WaitingForSensorTestModeResponse = m_SensorTestMode;
m_SentSensorTestModeRequestAtTicks = now;
SendCommandLocked(ssprintf("y%c\n", m_SensorTestMode));
}
// Handle a response to UpdateTestMode.
void SMX::SMXDevice::HandleSensorTestDataResponse(const string &sReadBuffer)
{
m_Lock.AssertLockedByCurrentThread();
// "y" is a response to our "y" query. This is binary data, with the format:
// yAB......
// where A is our original query mode (currently '0' or '1'), and B is the number
// of bits from each panel in the response. Each bit is encoded as a 16-bit int,
// with each int having the response bits from each panel.
if(sReadBuffer.size() < 3)
return;
// If we don't have the whole packet yet, wait.
size_t iSize = sReadBuffer[2] * 2;
if(sReadBuffer.size() < iSize + 3)
return;
SensorTestMode iMode = (SensorTestMode) sReadBuffer[1];
// Copy off the data and remove it from the serial buffer.
vector<uint16_t> data;
for(size_t i = 3; i < iSize + 3; i += 2)
{
uint16_t iValue =
(uint8_t(sReadBuffer[i+1]) << 8) |
(uint8_t(sReadBuffer[i+0]) << 0);
data.push_back(iValue);
}
if(m_WaitingForSensorTestModeResponse == SensorTestMode_Off)
{
Log("Ignoring unexpected sensor data request. It may have been sent by another application.");
return;
}
if(iMode != m_WaitingForSensorTestModeResponse)
{
Log(ssprintf("Ignoring unexpected sensor data request (got %i, expected %i)", iMode, m_WaitingForSensorTestModeResponse));
return;
}
m_WaitingForSensorTestModeResponse = SensorTestMode_Off;
// We match m_WaitingForSensorTestModeResponse, which is the sensor request we most
// recently sent. If we don't match g_SensorTestMode, then the sensor mode was changed
// while a request was in the air. Just ignore the response.
if(iMode != m_SensorTestMode)
return;
#pragma pack(push,1)
struct detail_data {
uint8_t sig1:1; // always 0
uint8_t sig2:1; // always 1
uint8_t sig3:1; // always 0
uint8_t bad_sensor_0:1;
uint8_t bad_sensor_1:1;
uint8_t bad_sensor_2:1;
uint8_t bad_sensor_3:1;
uint8_t dummy:1;
int16_t sensors[4];
uint8_t dip:4;
uint8_t bad_sensor_dip_0:1;
uint8_t bad_sensor_dip_1:1;
uint8_t bad_sensor_dip_2:1;
uint8_t bad_sensor_dip_3:1;
};
#pragma pack(pop)
m_HaveSensorTestModeData = true;
SMXSensorTestModeData &output = m_SensorTestData;
bool bLastHaveDataFromPanel[9];
memcpy(bLastHaveDataFromPanel, output.bHaveDataFromPanel, sizeof(output.bHaveDataFromPanel));
memset(output.bHaveDataFromPanel, 0, sizeof(output.bHaveDataFromPanel));
memset(output.sensorLevel, 0, sizeof(output.sensorLevel));
memset(output.bBadSensorInput, 0, sizeof(output.bBadSensorInput));
memset(output.iDIPSwitchPerPanel, 0, sizeof(output.iDIPSwitchPerPanel));
memset(output.iBadJumper, 0, sizeof(output.iBadJumper));
for(int iPanel = 0; iPanel < 9; ++iPanel)
{
// Decode the response from this panel.
detail_data pad_data;
ReadDataForPanel(data, iPanel, &pad_data, sizeof(pad_data));
// Check the header. This is always 0 1 0, to identify it as a response, and not as random
// steps from the player.
if(pad_data.sig1 != 0 || pad_data.sig2 != 1 || pad_data.sig3 != 0)
{
if(bLastHaveDataFromPanel[iPanel])
Log(ssprintf("No data from panel %i (%02x %02x %02x)", iPanel, pad_data.sig1, pad_data.sig2, pad_data.sig3));
output.bHaveDataFromPanel[iPanel] = false;
continue;
}
output.bHaveDataFromPanel[iPanel] = true;
// These bits are true if that sensor's most recent reading is invalid.
output.bBadSensorInput[iPanel][0] = pad_data.bad_sensor_0;
output.bBadSensorInput[iPanel][1] = pad_data.bad_sensor_1;
output.bBadSensorInput[iPanel][2] = pad_data.bad_sensor_2;
output.bBadSensorInput[iPanel][3] = pad_data.bad_sensor_3;
output.iDIPSwitchPerPanel[iPanel] = pad_data.dip;
output.iBadJumper[iPanel][0] = pad_data.bad_sensor_dip_0;
output.iBadJumper[iPanel][1] = pad_data.bad_sensor_dip_1;
output.iBadJumper[iPanel][2] = pad_data.bad_sensor_dip_2;
output.iBadJumper[iPanel][3] = pad_data.bad_sensor_dip_3;
for(int iSensor = 0; iSensor < 4; ++iSensor)
output.sensorLevel[iPanel][iSensor] = pad_data.sensors[iSensor];
}
CallUpdateCallback(SMXUpdateCallback_Updated);
}

View File

@@ -0,0 +1,136 @@
#ifndef SMXDevice_h
#define SMXDevice_h
#include <windows.h>
#include <memory>
#include <functional>
using namespace std;
#include "Helpers.h"
#include "../SMX.h"
namespace SMX
{
class SMXDeviceConnection;
// The high-level interface to a single controller. This is managed by SMXManager, and uses SMXDeviceConnection
// for low-level USB communication.
class SMXDevice
{
public:
// Create an SMXDevice.
//
// lock is our serialization mutex. This is shared across SMXManager and all SMXDevices.
//
// hEvent is signalled when we have new packets to be sent, to wake the communications thread. The
// device handle opened with OpenPort must also be monitored, to check when packets have been received
// (or successfully sent).
static shared_ptr<SMXDevice> Create(shared_ptr<SMX::AutoCloseHandle> hEvent, SMX::Mutex &lock);
SMXDevice(shared_ptr<SMXDevice> &pSelf, shared_ptr<SMX::AutoCloseHandle> hEvent, SMX::Mutex &lock);
~SMXDevice();
bool OpenDeviceHandle(shared_ptr<SMX::AutoCloseHandle> pHandle, wstring &sError);
void CloseDevice();
shared_ptr<SMX::AutoCloseHandle> GetDeviceHandle() const;
// Set a function to be called when something changes on the device. This allows efficiently
// detecting when a panel is pressed or other changes happen on the device.
void SetUpdateCallback(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback);
// Return true if we're connected.
bool IsConnected() const;
// Send a raw command.
void SendCommand(string sCmd, function<void(string response)> pComplete=nullptr);
void SendCommandLocked(string sCmd, function<void(string response)> pComplete=nullptr);
// Get basic info about the device.
void GetInfo(SMXInfo &info);
void GetInfoLocked(SMXInfo &info); // used by SMXManager
// Return true if this device is configured as player 2.
bool IsPlayer2Locked() const; // used by SMXManager
// Get the configuration of the connected device (or the most recently read configuration if
// we're not connected).
bool GetConfig(SMXConfig &configOut);
bool GetConfigLocked(SMXConfig &configOut);
// Set the configuration of the connected device.
//
// This is asynchronous and returns immediately.
void SetConfig(const SMXConfig &newConfig);
// Return a mask of the panels currently pressed.
uint16_t GetInputState() const;
// Reset the configuration data to what the device used when it was first flashed.
// GetConfig() will continue to return the previous configuration until this command
// completes, which is signalled by a SMXUpdateCallback_FactoryResetCommandComplete callback.
void FactoryReset();
// Force immediate fast recalibration. This is the same calibration that happens at
// boot. This is only used for diagnostics, and the panels will normally auto-calibrate
// on their own.
void ForceRecalibration();
// Set the test mode of the connected device.
//
// This is asynchronous and returns immediately.
void SetSensorTestMode(SensorTestMode mode);
// Return the most recent test data we've received from the pad. Return false if we haven't
// received test data since changing the test mode (or if we're not in a test mode).
bool GetTestData(SMXSensorTestModeData &data);
// Internal:
// Update this device, processing received packets and sending any outbound packets.
// m_Lock must be held.
//
// sError will be set on a communications error. The owner must close the device.
void Update(wstring &sError);
private:
shared_ptr<SMX::AutoCloseHandle> m_hEvent;
SMX::Mutex &m_Lock;
function<void(int PadNumber, SMXUpdateCallbackReason reason)> m_pUpdateCallback;
weak_ptr<SMXDevice> m_pSelf;
shared_ptr<SMXDeviceConnection> m_pConnection;
// The configuration we've read from the device. m_bHaveConfig is true if we've received
// a configuration from the device since we've connected to it.
SMXConfig config;
vector<uint8_t> rawConfig;
bool m_bHaveConfig = false;
double m_fDelayConfigUpdatesUntil = 0;
// This is the configuration the user has set, if he's changed anything. We send this to
// the device if m_bSendConfig is true. Once we send it once, m_bSendConfig is cleared, and
// if we see a different configuration from the device again we won't re-send this.
SMXConfig wanted_config;
bool m_bSendConfig = false;
bool m_bSendingConfig = false;
bool m_bWaitingForConfigResponse = false;
void CallUpdateCallback(SMXUpdateCallbackReason reason);
void HandlePackets();
void SendConfig();
void CheckActive();
bool IsConnectedLocked() const;
// Test/diagnostics mode handling.
void UpdateSensorTestMode();
void HandleSensorTestDataResponse(const string &sReadBuffer);
SensorTestMode m_WaitingForSensorTestModeResponse = SensorTestMode_Off;
SensorTestMode m_SensorTestMode = SensorTestMode_Off;
bool m_HaveSensorTestModeData = false;
SMXSensorTestModeData m_SensorTestData;
uint32_t m_SentSensorTestModeRequestAtTicks = 0;
};
}
#endif

View File

@@ -0,0 +1,440 @@
#include "SMXDeviceConnection.h"
#include "Helpers.h"
#include <string>
#include <memory>
using namespace std;
using namespace SMX;
extern "C"
{
#include <hidsdi.h>
}
#include <setupapi.h>
SMX::SMXDeviceConnection::PendingCommandPacket::PendingCommandPacket()
{
}
SMXDeviceConnection::PendingCommand::PendingCommand()
{
memset(&m_Overlapped, 0, sizeof(m_Overlapped));
}
shared_ptr<SMX::SMXDeviceConnection> SMXDeviceConnection::Create()
{
return CreateObj<SMXDeviceConnection>();
}
SMX::SMXDeviceConnection::SMXDeviceConnection(shared_ptr<SMXDeviceConnection> &pSelf):
m_pSelf(GetPointers(pSelf, this))
{
memset(&overlapped_read, 0, sizeof(overlapped_read));
}
SMX::SMXDeviceConnection::~SMXDeviceConnection()
{
Close();
}
bool SMX::SMXDeviceConnection::Open(shared_ptr<AutoCloseHandle> DeviceHandle, wstring &sError)
{
m_hDevice = DeviceHandle;
if(!HidD_SetNumInputBuffers(DeviceHandle->value(), 512))
Log(ssprintf("Error: HidD_SetNumInputBuffers: %ls", GetErrorString(GetLastError()).c_str()));
// Begin the first async read.
BeginAsyncRead(sError);
// Request device info.
RequestDeviceInfo([&](string response) {
Log(ssprintf("Received device info. Master version: %i, P%i", m_DeviceInfo.m_iFirmwareVersion, m_DeviceInfo.m_bP2+1));
m_bGotInfo = true;
});
return true;
}
void SMX::SMXDeviceConnection::Close()
{
Log("Closing device");
if(m_hDevice)
CancelIo(m_hDevice->value());
// If we're being closed while a command was in progress, call its completion
// callback, so it's guaranteed to always be called.
if(m_pCurrentCommand && m_pCurrentCommand->m_pComplete)
m_pCurrentCommand->m_pComplete("");
// If any commands were queued with completion callbacks, call their completion
// callbacks.
for(auto &pendingCommand: m_aPendingCommands)
{
if(pendingCommand->m_pComplete)
pendingCommand->m_pComplete("");
}
m_hDevice.reset();
m_sReadBuffers.clear();
m_aPendingCommands.clear();
memset(&overlapped_read, 0, sizeof(overlapped_read));
m_bActive = false;
m_bGotInfo = false;
m_pCurrentCommand = nullptr;
m_iInputState = 0;
}
void SMX::SMXDeviceConnection::SetActive(bool bActive)
{
if(m_bActive == bActive)
return;
m_bActive = bActive;
}
void SMX::SMXDeviceConnection::Update(wstring &sError)
{
if(!sError.empty())
return;
if(m_hDevice == nullptr)
{
sError = L"Device not open";
return;
}
// A read packet can allow us to initiate a write, so check reads before writes.
CheckReads(sError);
CheckWrites(sError);
}
bool SMX::SMXDeviceConnection::ReadPacket(string &out)
{
if(m_sReadBuffers.empty())
return false;
out = m_sReadBuffers.front();
m_sReadBuffers.pop_front();
return true;
}
void SMX::SMXDeviceConnection::CheckReads(wstring &error)
{
if(m_pCurrentCommand)
{
// See if this command timed out. This doesn't happen often, so this is
// mostly just a failsafe. The controller takes a moment to initialize on
// startup, so we use a large enough timeout that this doesn't trigger on
// every connection.
double fSecondsAgo = SMX::GetMonotonicTime() - m_pCurrentCommand->m_fSentAt;
if(fSecondsAgo > 2.0f)
{
// If we didn't get a response in this long, we're not going to. Retry the
// command by cancelling its I/O and moving it back to the command queue.
//
// if we were delayed and the response is in the queue, we'll get out of sync
Log("Command timed out. Retrying...");
CancelIoEx(m_hDevice->value(), &m_pCurrentCommand->m_Overlapped);
// Block until the cancellation completes. This should happen quickly.
DWORD unused;
GetOverlappedResult(m_hDevice->value(), &m_pCurrentCommand->m_Overlapped, &unused, true);
m_aPendingCommands.push_front(m_pCurrentCommand);
m_pCurrentCommand = nullptr;
Log("Command requeued");
}
}
DWORD bytes;
int result = GetOverlappedResult(m_hDevice->value(), &overlapped_read, &bytes, FALSE);
if(result == 0)
{
int windows_error = GetLastError();
if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE)
error = wstring(L"Error reading device: ") + GetErrorString(windows_error).c_str();
return;
}
HandleUsbPacket(string(overlapped_read_buffer, bytes));
// Start the next read.
BeginAsyncRead(error);
}
void SMX::SMXDeviceConnection::HandleUsbPacket(const string &buf)
{
if(buf.empty())
return;
// Log(ssprintf("Read: %s", BinaryToHex(buf).c_str()));
int iReportId = buf[0];
switch(iReportId)
{
case 3:
// Input state. We could also read this as a normal HID button change.
m_iInputState = ((buf[2] & 0xFF) << 8) |
((buf[1] & 0xFF) << 0);
// Log(ssprintf("Input state: %x (%x %x)\n", m_iInputState, buf[2], buf[1]));
break;
case 6:
// A HID serial packet.
if(buf.size() < 3)
return;
int cmd = buf[1];
#define PACKET_FLAG_START_OF_COMMAND 0x04
#define PACKET_FLAG_END_OF_COMMAND 0x01
#define PACKET_FLAG_HOST_CMD_FINISHED 0x02
#define PACKET_FLAG_DEVICE_INFO 0x80
size_t bytes = buf[2];
if(3 + bytes > buf.size())
{
Log("Communication error: oversized packet (ignored)");
return;
}
string sPacket( buf.begin()+3, buf.begin()+3+bytes );
if(cmd & PACKET_FLAG_DEVICE_INFO)
{
// This is a response to RequestDeviceInfo. Since any application can send this,
// we ignore the packet if we didn't request it, since it might be requested for
// a different program.
if(m_pCurrentCommand == nullptr || !m_pCurrentCommand->m_bIsDeviceInfoCommand)
break;
// We're little endian and the device is too, so we can just match the struct.
// We're depending on correct padding.
struct data_info_packet
{
char cmd; // always 'I'
uint8_t packet_size; // not used
char player; // '0' for P1, '1' for P2:
char unused2;
uint8_t serial[16];
uint16_t firmware_version;
char unused3; // always '\n'
};
// The packet contains data_info_packet. The packet is actually one byte smaller
// due to a padding byte added (it contains 23 bytes of data but the struct is
// 24 bytes). Resize to be sure.
sPacket.resize(sizeof(data_info_packet));
// Convert the info packet from the wire protocol to our friendlier API.
const data_info_packet *packet = (data_info_packet *) sPacket.data();
m_DeviceInfo.m_bP2 = packet->player == '1';
m_DeviceInfo.m_iFirmwareVersion = packet->firmware_version;
// The serial is binary in this packet. Hex format it, which is the same thing
// we'll get if we read the USB serial number (eg. HidD_GetSerialNumberString).
string sHexSerial = BinaryToHex(packet->serial, 16);
memcpy(m_DeviceInfo.m_Serial, sHexSerial.c_str(), 33);
if(m_pCurrentCommand->m_pComplete)
m_pCurrentCommand->m_pComplete(sPacket);
m_pCurrentCommand = nullptr;
break;
}
// If we're not active, ignore all packets other than device info. This is always false
// while we're in Open() waiting for the device info response.
if(!m_bActive)
break;
if(cmd & PACKET_FLAG_START_OF_COMMAND && !m_sCurrentReadBuffer.empty())
{
// When we get a start packet, the read buffer should already be empty. If
// it isn't, we got a command that didn't end with an END_OF_COMMAND packet,
// and something is wrong. This shouldn't happen, so warn about it and recover
// by clearing the junk in the buffer.
Log(ssprintf("Got PACKET_FLAG_START_OF_COMMAND, but we had %i bytes in the read buffer",
m_sCurrentReadBuffer.size()));
m_sCurrentReadBuffer.clear();
}
m_sCurrentReadBuffer.append(sPacket);
// Note that if PACKET_FLAG_HOST_CMD_FINISHED is set, PACKET_FLAG_END_OF_COMMAND
// will always also be set.
if(cmd & PACKET_FLAG_HOST_CMD_FINISHED)
{
// This tells us that a command we wrote to the device has finished executing, and
// it's safe to start writing another.
if(m_pCurrentCommand && m_pCurrentCommand->m_pComplete)
m_pCurrentCommand->m_pComplete(m_sCurrentReadBuffer);
m_pCurrentCommand = nullptr;
}
if(cmd & PACKET_FLAG_END_OF_COMMAND)
{
if(!m_sCurrentReadBuffer.empty())
m_sReadBuffers.push_back(m_sCurrentReadBuffer);
m_sCurrentReadBuffer.clear();
}
break;
}
}
void SMX::SMXDeviceConnection::BeginAsyncRead(wstring &error)
{
while(1)
{
// Our read buffer is 64 bytes. The HID input packet is much smaller than that,
// but Windows pads packets to the maximum size of any HID report, and the HID
// serial packet is 64 bytes, so we'll get 64 bytes even for 3-byte input packets.
// If this didn't happen, we'd have to be smarter about pulling data out of the
// read buffer.
DWORD bytes;
memset(overlapped_read_buffer, 0, sizeof(overlapped_read_buffer));
if(!ReadFile(m_hDevice->value(), overlapped_read_buffer, sizeof(overlapped_read_buffer), &bytes, &overlapped_read))
{
int windows_error = GetLastError();
if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE)
error = wstring(L"Error reading device: ") + GetErrorString(windows_error).c_str();
return;
}
// The async read finished synchronously. This just means that there was already data waiting.
// Handle the result, and loop to try to start the next async read again.
HandleUsbPacket(string(overlapped_read_buffer, bytes));
}
}
void SMX::SMXDeviceConnection::CheckWrites(wstring &error)
{
if(m_pCurrentCommand)
{
// A command is in progress. See if its writes have completed.
if(m_pCurrentCommand->m_bWriting)
{
DWORD bytes;
int iResult = GetOverlappedResult(m_hDevice->value(), &m_pCurrentCommand->m_Overlapped, &bytes, FALSE);
if(iResult == 0)
{
int windows_error = GetLastError();
if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE)
error = wstring(L"Error writing to device: ") + GetErrorString(windows_error).c_str();
return;
}
m_pCurrentCommand->m_bWriting = false;
}
// Don't clear m_pCurrentCommand here. It'll stay set until we get a PACKET_FLAG_HOST_CMD_FINISHED
// packet from the device, which tells us it's ready to receive another command.
//
// Don't send packets if there's a command in progress.
return;
}
// Stop if we have nothing to do.
if(m_aPendingCommands.empty())
return;
// Send the next command.
shared_ptr<PendingCommand> pPendingCommand = m_aPendingCommands.front();
// Record the time. We can use this for timeouts.
pPendingCommand->m_fSentAt = SMX::GetMonotonicTime();
for(shared_ptr<PendingCommandPacket> &pPacket: pPendingCommand->m_Packets)
{
// In theory the API allows this to return success if the write completed successfully without needing to
// be async, like reads can. However, this can't really happen (the write always needs to go to the device
// first, unlike reads which might already be buffered), and there's no way to test it if we implement that,
// so this assumes all writes are async.
DWORD unused;
// Log(ssprintf("Write: %s", BinaryToHex(pPacket->sData).c_str()));
if(!WriteFile(m_hDevice->value(), pPacket->sData.data(), pPacket->sData.size(), &unused, &pPendingCommand->m_Overlapped))
{
int windows_error = GetLastError();
if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE)
{
error = wstring(L"Error writing to device: ") + GetErrorString(windows_error).c_str();
return;
}
}
}
pPendingCommand->m_bWriting = true;
// Remove this command and store it in m_pCurrentCommand, and we'll stop sending data until the command finishes.
m_pCurrentCommand = pPendingCommand;
m_aPendingCommands.pop_front();
}
// Request device info. This is the same as sending an 'i' command, but we can send it safely
// at any time, even if another application is talking to the device, so we can do this during
// enumeration.
void SMX::SMXDeviceConnection::RequestDeviceInfo(function<void(string response)> pComplete)
{
shared_ptr<PendingCommand> pPendingCommand = make_shared<PendingCommand>();
pPendingCommand->m_pComplete = pComplete;
pPendingCommand->m_bIsDeviceInfoCommand = true;
shared_ptr<PendingCommandPacket> pCommandPacket = make_shared<PendingCommandPacket>();
string sPacket({
5, // report ID
(char) (uint8_t) PACKET_FLAG_DEVICE_INFO, // flags
(char) 0, // bytes in packet
});
sPacket.resize(64, 0);
pCommandPacket->sData = sPacket;
pPendingCommand->m_Packets.push_back(pCommandPacket);
m_aPendingCommands.push_back(pPendingCommand);
}
void SMX::SMXDeviceConnection::SendCommand(const string &cmd, function<void(string response)> pComplete)
{
shared_ptr<PendingCommand> pPendingCommand = make_shared<PendingCommand>();
pPendingCommand->m_pComplete = pComplete;
// Send the command in packets. We allow sending zero-length packets here
// for testing purposes.
size_t i = 0;
do {
shared_ptr<PendingCommandPacket> pCommandPacket = make_shared<PendingCommandPacket>();
int iFlags = 0;
size_t iPacketSize = min(cmd.size() - i, static_cast<size_t>(61));
bool bFirstPacket = (i == 0);
if(bFirstPacket)
iFlags |= PACKET_FLAG_START_OF_COMMAND;
bool bLastPacket = (i + iPacketSize == cmd.size());
if(bLastPacket)
iFlags |= PACKET_FLAG_END_OF_COMMAND;
string sPacket({
5, // report ID
(char) iFlags,
(char) iPacketSize, // bytes in packet
});
sPacket.append(cmd.begin() + i, cmd.begin() + i + iPacketSize);
sPacket.resize(64, 0);
pCommandPacket->sData = sPacket;
pPendingCommand->m_Packets.push_back(pCommandPacket);
i += iPacketSize;
}
while(i < cmd.size());
m_aPendingCommands.push_back(pPendingCommand);
}

View File

@@ -0,0 +1,131 @@
#ifndef SMXDevice_H
#define SMXDevice_H
#include <windows.h>
#include <vector>
#include <memory>
#include <string>
#include <list>
#include <functional>
using namespace std;
#include "Helpers.h"
namespace SMX
{
struct SMXDeviceInfo
{
// If true, this controller is set to player 2.
bool m_bP2 = false;
// This device's serial number.
char m_Serial[33];
// This device's firmware version (normally 1).
uint16_t m_iFirmwareVersion;
};
// Low-level SMX device handling.
class SMXDeviceConnection
{
public:
static shared_ptr<SMXDeviceConnection> Create();
SMXDeviceConnection(shared_ptr<SMXDeviceConnection> &pSelf);
~SMXDeviceConnection();
bool Open(shared_ptr<AutoCloseHandle> DeviceHandle, wstring &error);
void Close();
// Get the device handle opened by Open(), or NULL if we're not open.
shared_ptr<AutoCloseHandle> GetDeviceHandle() const { return m_hDevice; }
void Update(wstring &sError);
// Devices are inactive by default, and will just read device info and then idle. We'll
// process input state packets, but we won't send any commands to the device or process
// any commands from it. It's safe to have a device open but inactive if it's being used
// by another application.
void SetActive(bool bActive);
bool GetActive() const { return m_bActive; }
bool IsConnected() const { return m_hDevice != nullptr; }
bool IsConnectedWithDeviceInfo() const { return m_hDevice != nullptr && m_bGotInfo; }
SMXDeviceInfo GetDeviceInfo() const { return m_DeviceInfo; }
// Read from the read buffer. This only returns data that we've already read, so there aren't
// any errors to report here.
bool ReadPacket(string &out);
// Send a command. This must be a single complete command: partial writes and multiple
// commands in a call aren't allowed.
void SendCommand(const string &cmd, function<void(string response)> pComplete=nullptr);
uint16_t GetInputState() const { return m_iInputState; }
private:
void RequestDeviceInfo(function<void(string response)> pComplete = nullptr);
void CheckReads(wstring &error);
void BeginAsyncRead(wstring &error);
void CheckWrites(wstring &error);
void HandleUsbPacket(const string &buf);
weak_ptr<SMXDeviceConnection> m_pSelf;
shared_ptr<AutoCloseHandle> m_hDevice;
bool m_bActive = false;
// After we open a device, we request basic info. Once we get it, this is set to true.
bool m_bGotInfo = false;
list<string> m_sReadBuffers;
string m_sCurrentReadBuffer;
struct PendingCommandPacket {
PendingCommandPacket();
string sData;
};
// Commands that are waiting to be sent:
struct PendingCommand {
PendingCommand();
list<shared_ptr<PendingCommandPacket>> m_Packets;
// The overlapped struct for writing this command's packets. m_bWriting is true
// if we're waiting for the write to complete.
OVERLAPPED m_Overlapped;
bool m_bWriting = false;
// This is only called if m_bWaitForResponse if true. Otherwise, we send the command
// and forget about it. If the command has a response, it'll be in buf.
function<void(string response)> m_pComplete;
// If true, once we send this command we won't send any other commands until we get
// a response.
bool m_bIsDeviceInfoCommand = false;
// The SMX::GetMonotonicTime when we started sending this command.
double m_fSentAt = 0;
};
list<shared_ptr<PendingCommand>> m_aPendingCommands;
// If set, we've sent a command out of m_aPendingCommands and we're waiting for a response. We
// can't send another command until the previous one has completed.
shared_ptr<PendingCommand> m_pCurrentCommand = nullptr;
// We always have a read in progress.
OVERLAPPED overlapped_read;
char overlapped_read_buffer[64];
uint16_t m_iInputState = 0;
// The current device info. We retrieve this when we connect.
SMXDeviceInfo m_DeviceInfo;
};
}
#endif

View File

@@ -0,0 +1,179 @@
#include "SMXDeviceSearch.h"
#include "SMXDeviceConnection.h"
#include "Helpers.h"
#include <string>
#include <memory>
#include <set>
using namespace std;
using namespace SMX;
extern "C"
{
#include <hidsdi.h>
}
#include <setupapi.h>
// Return all USB HID device paths. This doesn't open the device to filter just our devices.
static set<wstring> GetAllHIDDevicePaths(wstring &error)
{
HDEVINFO DeviceInfoSet = NULL;
GUID HidGuid;
HidD_GetHidGuid(&HidGuid);
DeviceInfoSet = SetupDiGetClassDevs(&HidGuid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if(DeviceInfoSet == NULL)
return {};
set<wstring> paths;
SP_DEVICE_INTERFACE_DATA DeviceInterfaceData;
DeviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for(DWORD iIndex = 0;
SetupDiEnumDeviceInterfaces(DeviceInfoSet, NULL, &HidGuid, iIndex, &DeviceInterfaceData);
iIndex++)
{
DWORD iSize;
if(!SetupDiGetDeviceInterfaceDetailW(DeviceInfoSet, &DeviceInterfaceData, NULL, 0, &iSize, NULL))
{
// This call normally fails with ERROR_INSUFFICIENT_BUFFER.
int iError = GetLastError();
if(iError != ERROR_INSUFFICIENT_BUFFER)
{
Log(wssprintf(L"SetupDiGetDeviceInterfaceDetail failed: %ls", GetErrorString(iError).c_str()));
continue;
}
}
PSP_DEVICE_INTERFACE_DETAIL_DATA_W DeviceInterfaceDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA_W) new uint8_t[iSize];
DeviceInterfaceDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);
SP_DEVINFO_DATA DeviceInfoData;
ZeroMemory(&DeviceInfoData, sizeof(SP_DEVINFO_DATA));
DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
if(!SetupDiGetDeviceInterfaceDetailW(DeviceInfoSet, &DeviceInterfaceData, DeviceInterfaceDetailData, iSize, NULL, &DeviceInfoData))
{
Log(wssprintf(L"SetupDiGetDeviceInterfaceDetail failed: %ls", GetErrorString(GetLastError()).c_str()));
delete[] (uint8_t *)DeviceInterfaceDetailData;
continue;
}
paths.insert(DeviceInterfaceDetailData->DevicePath);
delete[] (uint8_t *)DeviceInterfaceDetailData;
}
SetupDiDestroyDeviceInfoList(DeviceInfoSet);
return paths;
}
static shared_ptr<AutoCloseHandle> OpenUSBDevice(LPCWSTR DevicePath, wstring &error)
{
// Log(ssprintf("Opening device: %ls", DevicePath));
HANDLE OpenDevice = CreateFileW(
DevicePath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL
);
if(OpenDevice == INVALID_HANDLE_VALUE)
{
// Many unrelated devices will fail to open, so don't return this as an error.
Log(wssprintf(L"Error opening device %ls: %ls", DevicePath, GetErrorString(GetLastError()).c_str()));
return nullptr;
}
auto result = make_shared<AutoCloseHandle>(OpenDevice);
// Get the HID attributes to check the IDs.
HIDD_ATTRIBUTES HidAttributes;
HidAttributes.Size = sizeof(HidAttributes);
if(!HidD_GetAttributes(result->value(), &HidAttributes))
{
Log(ssprintf("Error opening device %ls: HidD_GetAttributes failed", DevicePath));
error = L"HidD_GetAttributes failed";
return nullptr;
}
if(HidAttributes.VendorID != 0x2341 || HidAttributes.ProductID != 0x8037)
{
Log(ssprintf("Device %ls: not our device (ID %04x:%04x)", DevicePath, HidAttributes.VendorID, HidAttributes.ProductID));
return nullptr;
}
// Since we're using the default Arduino IDs, check the product name to make sure
// this isn't some other Arduino device.
WCHAR ProductName[255];
ZeroMemory(ProductName, sizeof(ProductName));
if(!HidD_GetProductString(result->value(), ProductName, 255))
{
Log(ssprintf("Error opening device %ls: HidD_GetProductString failed", DevicePath));
return nullptr;
}
if(wstring(ProductName) != L"StepManiaX")
{
Log(ssprintf("Device %ls: not our device (%ls)", DevicePath, ProductName));
return nullptr;
}
return result;
}
vector<shared_ptr<AutoCloseHandle>> SMX::SMXDeviceSearch::GetDevices(wstring &error)
{
set<wstring> aDevicePaths = GetAllHIDDevicePaths(error);
// Remove any entries in m_Devices that are no longer in the list.
for(wstring sPath: m_setLastDevicePaths)
{
if(aDevicePaths.find(sPath) != aDevicePaths.end())
continue;
Log(ssprintf("Device removed: %ls", sPath.c_str()));
m_Devices.erase(sPath);
}
// Check for new entries.
for(wstring sPath: aDevicePaths)
{
// Only look at devices that weren't in the list last time. OpenUSBDevice has
// to open the device and causes requests to be sent to it.
if(m_setLastDevicePaths.find(sPath) != m_setLastDevicePaths.end())
continue;
// This will return NULL if this isn't our device.
shared_ptr<AutoCloseHandle> hDevice = OpenUSBDevice(sPath.c_str(), error);
if(hDevice == nullptr)
continue;
Log(ssprintf("Device added: %ls", sPath.c_str()));
m_Devices[sPath] = hDevice;
}
m_setLastDevicePaths = aDevicePaths;
vector<shared_ptr<AutoCloseHandle>> aDevices;
for(auto it: m_Devices)
aDevices.push_back(it.second);
return aDevices;
}
void SMX::SMXDeviceSearch::DeviceWasClosed(shared_ptr<AutoCloseHandle> pDevice)
{
map<wstring, shared_ptr<AutoCloseHandle>> aDevices;
for(auto it: m_Devices)
{
if(it.second == pDevice)
{
m_setLastDevicePaths.erase(it.first);
}
else
{
aDevices[it.first] = it.second;
}
}
m_Devices = aDevices;
}

View File

@@ -0,0 +1,33 @@
#ifndef SMXDeviceSearch_h
#define SMXDeviceSearch_h
#include <memory>
#include <string>
#include <vector>
#include <set>
#include <map>
using namespace std;
#include "Helpers.h"
namespace SMX {
class SMXDeviceSearch
{
public:
// Return a list of connected devices. If the same device stays connected and this
// is called multiple times, the same handle will be returned.
vector<shared_ptr<AutoCloseHandle>> GetDevices(wstring &error);
// After a device is opened and then closed, tell this class that the device was closed.
// We'll discard our record of it, so we'll notice a new device plugged in on the same
// path.
void DeviceWasClosed(shared_ptr<AutoCloseHandle> pDevice);
private:
set<wstring> m_setLastDevicePaths;
map<wstring, shared_ptr<AutoCloseHandle>> m_Devices;
};
}
#endif

View File

@@ -0,0 +1,97 @@
#include "SMXDeviceSearchThreaded.h"
#include "SMXDeviceSearch.h"
#include "SMXDeviceConnection.h"
#include <windows.h>
#include <memory>
using namespace std;
using namespace SMX;
SMX::SMXDeviceSearchThreaded::SMXDeviceSearchThreaded()
{
m_hEvent = make_shared<AutoCloseHandle>(CreateEvent(NULL, false, false, NULL));
m_pDeviceList = make_shared<SMXDeviceSearch>();
// Start the thread.
DWORD id;
m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &id);
SMX::SetThreadName(id, "SMXDeviceSearch");
}
SMX::SMXDeviceSearchThreaded::~SMXDeviceSearchThreaded()
{
// Shut down the thread, if it's still running.
Shutdown();
}
void SMX::SMXDeviceSearchThreaded::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::SMXDeviceSearchThreaded::ThreadMainStart(void *self_)
{
SMXDeviceSearchThreaded *self = (SMXDeviceSearchThreaded *) self_;
self->ThreadMain();
return 0;
}
void SMX::SMXDeviceSearchThreaded::UpdateDeviceList()
{
m_Lock.AssertNotLockedByCurrentThread();
// Tell m_pDeviceList about closed devices, so it knows that any device on the
// same path is new.
m_Lock.Lock();
for(auto pDevice: m_apClosedDevices)
m_pDeviceList->DeviceWasClosed(pDevice);
m_apClosedDevices.clear();
m_Lock.Unlock();
// Get the current device list.
wstring sError;
vector<shared_ptr<AutoCloseHandle>> apDevices = m_pDeviceList->GetDevices(sError);
if(!sError.empty())
{
Log(ssprintf("Error listing USB devices: %ls", sError.c_str()));
return;
}
// Update the device list returned by GetDevices.
m_Lock.Lock();
m_apDevices = apDevices;
m_Lock.Unlock();
}
void SMX::SMXDeviceSearchThreaded::ThreadMain()
{
while(!m_bShutdown)
{
UpdateDeviceList();
WaitForSingleObjectEx(m_hEvent->value(), 250, true);
}
}
void SMX::SMXDeviceSearchThreaded::DeviceWasClosed(shared_ptr<AutoCloseHandle> pDevice)
{
// Add pDevice to the list of closed devices. We'll call m_pDeviceList->DeviceWasClosed
// on these from the scanning thread.
m_apClosedDevices.push_back(pDevice);
}
vector<shared_ptr<AutoCloseHandle>> SMX::SMXDeviceSearchThreaded::GetDevices()
{
// Lock to make a copy of the device list.
m_Lock.Lock();
vector<shared_ptr<AutoCloseHandle>> apResult = m_apDevices;
m_Lock.Unlock();
return apResult;
}

View File

@@ -0,0 +1,46 @@
#ifndef SMXDeviceSearchThreaded_h
#define SMXDeviceSearchThreaded_h
#include "Helpers.h"
#include <windows.h>
#include <memory>
#include <vector>
using namespace std;
namespace SMX {
class SMXDeviceSearch;
// This is a wrapper around SMXDeviceSearch which performs USB scanning in a thread.
// It's free on Win10, but takes a while on Windows 7 (about 8ms), so running it on
// a separate thread prevents random timing errors when reading HID updates.
class SMXDeviceSearchThreaded
{
public:
SMXDeviceSearchThreaded();
~SMXDeviceSearchThreaded();
// The same interface as SMXDeviceSearch:
vector<shared_ptr<SMX::AutoCloseHandle>> GetDevices();
void DeviceWasClosed(shared_ptr<SMX::AutoCloseHandle> pDevice);
// Synchronously shut down the thread.
void Shutdown();
private:
void UpdateDeviceList();
static DWORD WINAPI ThreadMainStart(void *self_);
void ThreadMain();
SMX::Mutex m_Lock;
shared_ptr<SMXDeviceSearch> m_pDeviceList;
shared_ptr<SMX::AutoCloseHandle> m_hEvent;
vector<shared_ptr<SMX::AutoCloseHandle>> m_apDevices;
vector<shared_ptr<SMX::AutoCloseHandle>> m_apClosedDevices;
bool m_bShutdown = false;
HANDLE m_hThread = INVALID_HANDLE_VALUE;
};
}
#endif

View File

@@ -0,0 +1,545 @@
#include "SMXGif.h"
#include <stdint.h>
#include <string>
#include <vector>
using namespace std;
// This is a simple animated GIF decoder. It always decodes to RGBA color,
// discarding palettes, and decodes the whole file at once.
class GIFError: public exception { };
struct Palette
{
SMXGif::Color color[256];
};
void SMXGif::GIFImage::Init(int width_, int height_)
{
width = width_;
height = height_;
image.resize(width * height);
}
void SMXGif::GIFImage::Clear(const Color &color)
{
for(int y = 0; y < height; ++y)
for(int x = 0; x < width; ++x)
get(x,y) = color;
}
void SMXGif::GIFImage::CropImage(SMXGif::GIFImage &dst, int crop_left, int crop_top, int crop_width, int crop_height) const
{
dst.Init(crop_width, crop_height);
for(int y = 0; y < crop_height; ++y)
{
for(int x = 0; x < crop_width; ++x)
dst.get(x,y) = get(x + crop_left, y + crop_top);
}
}
void SMXGif::GIFImage::Blit(SMXGif::GIFImage &src, int dst_left, int dst_top, int dst_width, int dst_height)
{
for(int y = 0; y < dst_height; ++y)
{
for(int x = 0; x < dst_width; ++x)
get(x + dst_left, y + dst_top) = src.get(x, y);
}
}
bool SMXGif::GIFImage::operator==(const GIFImage &rhs) const
{
return
width == rhs.width &&
height == rhs.height &&
image == rhs.image;
}
class DataStream
{
public:
DataStream(const string &data_):
data(data_)
{
}
uint8_t ReadByte()
{
if(pos >= data.size())
throw GIFError();
uint8_t result = data[pos];
pos++;
return result;
}
uint16_t ReadLE16()
{
uint8_t byte1 = ReadByte();
uint8_t byte2 = ReadByte();
return byte1 | (byte2 << 8);
}
void ReadBytes(string &s, int count)
{
s.clear();
while(count--)
s.push_back(ReadByte());
}
void skip(int bytes)
{
pos += bytes;
}
private:
const string &data;
uint32_t pos = 0;
};
class LWZStream
{
public:
LWZStream(DataStream &stream_):
stream(stream_)
{
}
// Read one LZW code from the input data.
uint32_t ReadLZWCode(uint32_t bit_count)
{
while(bits_in_buffer < bit_count)
{
if(bytes_remaining == 0)
{
// Read the next block's byte count.
bytes_remaining = stream.ReadByte();
if(bytes_remaining == 0)
throw GIFError();
}
// Shift in another 8 bits into the end of self.bits.
bits |= stream.ReadByte() << bits_in_buffer;
bits_in_buffer += 8;
bytes_remaining -= 1;
}
// Shift out bit_count worth of data from the end.
uint32_t result = bits & ((1 << bit_count) - 1);
bits >>= bit_count;
bits_in_buffer -= bit_count;
return result;
}
// Skip the rest of the LZW data.
void Flush()
{
stream.skip(bytes_remaining);
bytes_remaining = 0;
// If there are any blocks past the end of data, skip them.
while(1)
{
uint8_t blocksize = stream.ReadByte();
stream.skip(blocksize);
if(bytes_remaining == 0)
break;
}
}
private:
DataStream &stream;
uint32_t bits = 0;
int bytes_remaining = 0;
uint32_t bits_in_buffer = 0;
};
struct LWZDecoder
{
LWZDecoder(DataStream &stream):
lzw_stream(LWZStream(stream))
{
// Each frame has a single bits field.
code_bits = stream.ReadByte();
}
string DecodeImage();
private:
uint16_t code_bits;
LWZStream lzw_stream;
};
static const int GIFBITS = 12;
string LWZDecoder::DecodeImage()
{
uint32_t dictionary_bits = code_bits + 1;
int prev_code1 = -1;
int prev_code2 = -1;
uint32_t clear = 1 << code_bits;
uint32_t end = clear + 1;
uint32_t next_free_slot = clear + 2;
vector<pair<int,int>> dictionary;
dictionary.resize(1 << GIFBITS);
// We append to this buffer as we decode data, then append the data in reverse
// order.
string append_buffer;
string result;
while(1)
{
// Flush append_buffer.
for(int i = append_buffer.size() - 1; i >= 0; --i)
result.push_back(append_buffer[i]);
append_buffer.clear();
uint32_t code1 = lzw_stream.ReadLZWCode(dictionary_bits);
// printf("%02x");
if(code1 == end)
break;
if(code1 == clear)
{
// Clear the dictionary and reset.
dictionary_bits = code_bits + 1;
next_free_slot = clear + 2;
prev_code1 = -1;
prev_code2 = -1;
continue;
}
uint32_t code2;
if(code1 < next_free_slot)
code2 = code1;
else if(code1 == next_free_slot && prev_code2 != -1)
{
append_buffer.push_back(prev_code2);
code2 = prev_code1;
}
else
throw GIFError();
// Walk through the linked list of codes in the dictionary and append.
while(code2 >= clear + 2)
{
uint8_t append_char = dictionary[code2].first;
code2 = dictionary[code2].second;
append_buffer.push_back(append_char);
}
append_buffer.push_back(code2);
// If we're already at the last free slot, the dictionary is full and can't be expanded.
if(next_free_slot < static_cast<uint32_t>(1 << dictionary_bits))
{
// If we have any free dictionary slots, save.
if(prev_code1 != -1)
{
dictionary[next_free_slot] = make_pair(code2, prev_code1);
next_free_slot += 1;
}
// If we've just filled the last dictionary slot, expand the dictionary size if possible.
if(next_free_slot >= static_cast<uint32_t>(1 << dictionary_bits) && dictionary_bits < GIFBITS)
dictionary_bits += 1;
}
prev_code1 = code1;
prev_code2 = code2;
}
// Skip any remaining data in this block.
lzw_stream.Flush();
return result;
}
struct GlobalGIFData
{
int width = 0, height = 0;
int background_index = -1;
bool use_transparency = false;
int transparency_index = -1;
int duration = 0;
int disposal_method = 0;
bool have_global_palette = false;
Palette palette;
};
class GIFDecoder
{
public:
GIFDecoder(DataStream &stream_):
stream(stream_)
{
}
void ReadAllFrames(vector<SMXGif::SMXGifFrame> &frames);
private:
bool ReadPacket(string &packet);
Palette ReadPalette(int palette_size);
void DecodeImage(GlobalGIFData global_data, SMXGif::GIFImage &out);
DataStream &stream;
SMXGif::GIFImage image;
int frame;
};
// Read a palette with size colors.
//
// This is a simple string, with 4 RGBA bytes per color.
Palette GIFDecoder::ReadPalette(int palette_size)
{
Palette result;
for(int i = 0; i < palette_size; ++i)
{
result.color[i].color[0] = stream.ReadByte(); // R
result.color[i].color[1] = stream.ReadByte(); // G
result.color[i].color[2] = stream.ReadByte(); // B
result.color[i].color[3] = 0xFF;
}
return result;
}
bool GIFDecoder::ReadPacket(string &packet)
{
uint8_t packet_size = stream.ReadByte();
if(packet_size == 0)
return false;
stream.ReadBytes(packet, packet_size);
return true;
}
void GIFDecoder::ReadAllFrames(vector<SMXGif::SMXGifFrame> &frames)
{
string header;
stream.ReadBytes(header, 6);
if(header != "GIF87a" && header != "GIF89a")
throw GIFError();
GlobalGIFData global_data;
global_data.width = stream.ReadLE16();
global_data.height = stream.ReadLE16();
image.Init(global_data.width, global_data.height);
// Ignore the aspect ratio field. (Supporting pixel aspect ratios in a format
// this rudimentary was almost ambitious of them...)
uint8_t global_flags = stream.ReadByte();
global_data.background_index = stream.ReadByte();
// Ignore the aspect ratio field. (Supporting pixel aspect ratios in a format
// this rudimentary was almost ambitious of them...)
stream.ReadByte();
// Decode global_flags.
uint8_t global_palette_size = global_flags & 0x7;
global_data.have_global_palette = (global_flags >> 7) & 0x1;
// If there's no global palette, leave it empty.
if(global_data.have_global_palette)
global_data.palette = ReadPalette(1 << (global_palette_size + 1));
frame = 0;
// Save a copy of global data, so we can restore it after each frame.
GlobalGIFData saved_global_data = global_data;
// Decode all packets.
while(1)
{
uint8_t packet_type = stream.ReadByte();
if(packet_type == 0x21)
{
// Extension packet
uint8_t extension_type = stream.ReadByte();
if(extension_type == 0xF9)
{
string packet;
if(!ReadPacket(packet))
throw GIFError();
DataStream packet_buf(packet);
// Graphics control extension
uint8_t gce_flags = packet_buf.ReadByte();
global_data.duration = packet_buf.ReadLE16();
global_data.transparency_index = packet_buf.ReadByte();
global_data.use_transparency = bool(gce_flags & 1);
global_data.disposal_method = (gce_flags >> 2) & 0xF;
if(!global_data.use_transparency)
global_data.transparency_index = -1;
}
// Read any remaining packets in this extension packet.
while(1)
{
string packet;
if(!ReadPacket(packet))
break;
}
}
else if(packet_type == 0x2C)
{
// Image data
SMXGif::GIFImage frame_image;
DecodeImage(global_data, frame_image);
SMXGif::SMXGifFrame gif_frame;
gif_frame.width = global_data.width;
gif_frame.height = global_data.height;
gif_frame.milliseconds = global_data.duration * 10;
gif_frame.frame = frame_image;
// If this frame is identical to the previous one, just extend the previous frame.
if(!frames.empty() && gif_frame.frame == frames.back().frame)
{
frames.back().milliseconds += gif_frame.milliseconds;
continue;
}
frames.push_back(gif_frame);
frame++;
// Reset GCE (frame-specific) data.
global_data = saved_global_data;
}
else if(packet_type == 0x3B)
{
// EOF
return;
}
else
throw GIFError();
}
}
// Decode a single GIF image into out, leaving this->image ready for
// the next frame (with this frame's dispose applied).
void GIFDecoder::DecodeImage(GlobalGIFData global_data, SMXGif::GIFImage &out)
{
uint16_t block_left = stream.ReadLE16();
uint16_t block_top = stream.ReadLE16();
uint16_t block_width = stream.ReadLE16();
uint16_t block_height = stream.ReadLE16();
uint8_t local_flags = stream.ReadByte();
// area = (block_left, block_top, block_left + block_width, block_top + block_height)
// Extract flags:
uint8_t have_local_palette = (local_flags >> 7) & 1;
// bool interlaced = (local_flags >> 6) & 1;
uint8_t local_palette_size = (local_flags >> 0) & 0x7;
// print 'Interlaced:', interlaced
// We don't support interlaced GIFs right now.
// assert interlaced == 0
// If this frame has a local palette, use it. Otherwise, use the global palette.
Palette active_palette = global_data.palette;
if(have_local_palette)
active_palette = ReadPalette(1 << (local_palette_size + 1));
if(!global_data.have_global_palette && !have_local_palette)
{
// We have no palette. This is an invalid file.
throw GIFError();
}
if(frame == 0)
{
// On the first frame, clear the buffer. If we have a transparency index,
// clear to transparent. Otherwise, clear to the background color.
if(global_data.transparency_index != -1)
image.Clear(SMXGif::Color(0,0,0,0));
else
image.Clear(active_palette.color[global_data.background_index]);
}
// Decode the compressed image data.
LWZDecoder decoder(stream);
string decompressed_data = decoder.DecodeImage();
if(decompressed_data.size() < block_width*block_height)
throw GIFError();
// Save the region to restore after decoding.
SMXGif::GIFImage dispose;
if(global_data.disposal_method <= 1)
{
// No disposal.
}
else if(global_data.disposal_method == 2)
{
// Clear the region to a background color afterwards.
dispose.Init(block_width, block_height);
if(global_data.transparency_index != -1)
dispose.Clear(SMXGif::Color(0,0,0,0));
else
{
uint8_t palette_idx = global_data.background_index;
dispose.Clear(active_palette.color[palette_idx]);
}
}
else if(global_data.disposal_method == 3)
{
// Restore the previous frame afterwards.
image.CropImage(dispose, block_left, block_top, block_width, block_height);
}
else
{
// Unknown disposal method
}
int pos = 0;
for(int y = block_top; y < block_top + block_height; ++y)
{
for(int x = block_left; x < block_left + block_width; ++x)
{
uint8_t palette_idx = decompressed_data[pos];
pos++;
if(palette_idx == global_data.transparency_index)
{
// If this pixel is transparent, leave the existing color in place.
}
else
{
image.get(x,y) = active_palette.color[palette_idx];
}
}
}
// Copy the image before we run dispose.
out = image;
// Restore the dispose area.
if(dispose.width != 0)
image.Blit(dispose, block_left, block_top, block_width, block_height);
}
bool SMXGif::DecodeGIF(string buf, vector<SMXGif::SMXGifFrame> &frames)
{
DataStream stream(buf);
GIFDecoder gif(stream);
try {
gif.ReadAllFrames(frames);
} catch(GIFError &) {
// We don't return error strings for this, just success or failure.
return false;
}
return true;
}

View File

@@ -0,0 +1,73 @@
#ifndef SMXGif_h
#define SMXGif_h
#include <stdint.h>
#include <string>
#include <vector>
#include <cstring>
// This is a simple internal GIF decoder. It's only meant to be used by
// SMXConfig.
namespace SMXGif
{
struct Color
{
uint8_t color[4];
Color()
{
memset(color, 0, sizeof(color));
}
Color(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
{
color[0] = r;
color[1] = g;
color[2] = b;
color[3] = a;
}
bool operator==(const Color &rhs) const
{
return !memcmp(color, rhs.color, sizeof(color));
}
};
struct GIFImage
{
int width = 0, height = 0;
void Init(int width, int height);
Color get(int x, int y) const { return image[y*width+x]; }
Color &get(int x, int y) { return image[y*width+x]; }
// Clear to a solid color.
void Clear(const Color &color);
// Copy a rectangle from this image into dst.
void CropImage(GIFImage &dst, int crop_left, int crop_top, int crop_width, int crop_height) const;
// Copy src into a rectangle in this image.
void Blit(GIFImage &src, int dst_left, int dst_top, int dst_width, int dst_height);
bool operator==(const GIFImage &rhs) const;
private:
std::vector<Color> image;
};
struct SMXGifFrame
{
int width = 0, height = 0;
// GIF images have a delay in 10ms units. We use 1ms for clarity.
int milliseconds = 0;
GIFImage frame;
};
// Decode a GIF into a list of frames.
bool DecodeGIF(std::string buf, std::vector<SMXGifFrame> &frames);
}
void gif_test();
#endif

View File

@@ -0,0 +1,42 @@
#include "SMXHelperThread.h"
using namespace SMX;
SMX::SMXHelperThread::SMXHelperThread(const string &sThreadName):
SMXThread(m_Lock)
{
Start(sThreadName);
}
void SMX::SMXHelperThread::ThreadMain()
{
m_Lock.Lock();
while(true)
{
vector<function<void()>> funcs;
swap(m_FunctionsToCall, funcs);
// If we're shutting down and have no more functions to call, stop.
if(funcs.empty() && m_bShutdown)
break;
// Unlock while we call the queued functions.
m_Lock.Unlock();
for(auto &func: funcs)
func();
m_Lock.Lock();
m_Event.Wait(250);
}
m_Lock.Unlock();
}
void SMX::SMXHelperThread::RunInThread(function<void()> func)
{
m_Lock.AssertNotLockedByCurrentThread();
// Add func to the list, and poke the event to wake up the thread if needed.
m_Lock.Lock();
m_FunctionsToCall.push_back(func);
m_Event.Set();
m_Lock.Unlock();
}

View File

@@ -0,0 +1,31 @@
#ifndef SMXHelperThread_h
#define SMXHelperThread_h
#include "Helpers.h"
#include "SMXThread.h"
#include <functional>
#include <vector>
#include <memory>
using namespace std;
namespace SMX
{
class SMXHelperThread: public SMXThread
{
public:
SMXHelperThread(const string &sThreadName);
// Call func asynchronously from the helper thread.
void RunInThread(function<void()> func);
private:
void ThreadMain();
// Helper threads use their independent lock.
SMX::Mutex m_Lock;
vector<function<void()>> m_FunctionsToCall;
};
}
#endif

View 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()));
}
}

View File

@@ -0,0 +1,96 @@
#ifndef SMXManager_h
#define SMXManager_h
#include <windows.h>
#include <memory>
#include <vector>
#include <functional>
using namespace std;
#include "Helpers.h"
#include "../SMX.h"
#include "SMXHelperThread.h"
namespace SMX {
class SMXDevice;
class SMXDeviceSearchThreaded;
struct SMXControllerState
{
// True
bool m_bConnected[2];
// Pressed panels for player 1 and player 2:
uint16_t m_Inputs[2];
};
// This implements the main thread that controller communication and device searching
// happens in, finding and opening devices, and running device updates.
//
// Connected controllers can be accessed with GetDevice(),
// This also abstracts controller numbers. GetDevice(SMX_PadNumber_1) will return the
// first device that connected,
class SMXManager
{
public:
// Our singleton:
static shared_ptr<SMXManager> g_pSMX;
// pCallback is a function to be called when something changes on any device. This allows
// efficiently detecting when a panel is pressed or other changes happen.
SMXManager(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback);
~SMXManager();
void Shutdown();
shared_ptr<SMXDevice> GetDevice(int pad);
void SetLights(const string sLights[2]);
void SetPlatformLights(const string sLights[2]);
void ReenableAutoLights();
void SetPanelTestMode(PanelTestMode mode);
void SetSerialNumbers();
void SetOnlySendLightsOnChange(bool value) { m_bOnlySendLightsOnChange = value; }
// Run a function in the user callback thread.
void RunInHelperThread(function<void()> func);
private:
static DWORD WINAPI ThreadMainStart(void *self_);
void ThreadMain();
void AttemptConnections();
void CorrectDeviceOrder();
void SendLightUpdates();
HANDLE m_hThread = INVALID_HANDLE_VALUE;
shared_ptr<SMX::AutoCloseHandle> m_hEvent;
shared_ptr<SMXDeviceSearchThreaded> m_pSMXDeviceSearchThreaded;
bool m_bShutdown = false;
vector<shared_ptr<SMXDevice>> m_pDevices;
// We make user callbacks asynchronously in this thread, to avoid any locking or timing
// issues that could occur by calling them in our I/O thread.
SMXHelperThread m_UserCallbackThread;
// A list of queued lights commands to send to the controllers. This is always sorted
// by iTimeToSend.
struct PendingCommand
{
PendingCommand(float fTime): fTimeToSend(fTime) { }
double fTimeToSend = 0;
string sPadCommand[2];
};
vector<PendingCommand> m_aPendingLightsCommands;
int m_iLightsCommandsInProgress = 0;
double m_fDelayLightCommandsUntil = 0;
// Panel test mode. This is separate from the sensor test mode (pressure display),
// which is handled in SMXDevice.
void UpdatePanelTestMode();
uint32_t m_SentPanelTestModeAtTicks = 0;
PanelTestMode m_PanelTestMode = PanelTestMode_Off;
PanelTestMode m_LastSentPanelTestMode = PanelTestMode_Off;
bool m_bOnlySendLightsOnChange = false;
};
}
#endif

View File

@@ -0,0 +1,520 @@
// Handle playing GIF animations from inside SMXConfig.
//
// This can load two GIF animations, one for when panels are released
// and one for when they're pressed, and play them automatically on the
// pad in the background. Applications that control lights can do more
// sophisticated things with the lights, but this gives an easy way for
// people to create simple animations.
//
// If you're implementing the SDK in a game, you don't need this and should
// use SMX.h instead.
//
// An animation is a single GIF with animations for all panels, in the
// following layout:
//
// 0000|1111|2222
// 0000|1111|2222
// 0000|1111|2222
// 0000|1111|2222
// --------------
// 3333|4444|5555
// 3333|4444|5555
// 3333|4444|5555
// 3333|4444|5555
// --------------
// 6666|7777|8888
// 6666|7777|8888
// 6666|7777|8888
// 6666|7777|8888
// x-------------
//
// The - | regions are ignored and are only there to space out the animation
// to make it easier to view.
//
// The extra bottom row is a flag row and should normally be black. The first
// pixel (bottom-left) optionally marks a loop frame. By default, the animation
// plays all the way through and then loops back to the beginning. If the loop
// frame pixel is white, it marks a frame to loop to instead of the beginning.
// This allows pressed animations to have a separate lead-in and loop.
//
// Each animation is for a single pad. You can load the same animation for both
// pads or use different ones.
#include "SMXPanelAnimation.h"
#include "SMXManager.h"
#include "SMXDevice.h"
#include "SMXThread.h"
#include <cmath>
using namespace std;
using namespace SMX;
namespace {
Mutex g_Lock;
}
#define LIGHTS_PER_PANEL 25
// XXX: go to sleep if there are no pads connected
struct AnimationState
{
SMXPanelAnimation animation;
// Seconds into the animation:
float fTime = 0;
// The currently displayed frame:
size_t iCurrentFrame = 0;
bool bPlaying = false;
double m_fLastUpdateTime = -1;
// Return the current animation frame.
const vector<SMXGif::Color> &GetAnimationFrame() const
{
// If we're not playing, return an empty array. As a sanity check, do this
// if the frame is out of bounds too.
if(!bPlaying || iCurrentFrame >= animation.m_aPanelGraphics.size())
{
static vector<SMXGif::Color> dummy;
return dummy;
}
return animation.m_aPanelGraphics[iCurrentFrame];
}
// Start the animation if it's not playing.
void Play()
{
bPlaying = true;
}
// Stop and disable the animation.
void Stop()
{
bPlaying = false;
Rewind();
}
// Reset to the first frame.
void Rewind()
{
fTime = 0;
iCurrentFrame = 0;
}
// Advance the animation by fSeconds.
void Update()
{
// fSeconds is the time since the last update:
double fNow = SMX::GetMonotonicTime();
double fSeconds = m_fLastUpdateTime == -1? 0: (fNow - m_fLastUpdateTime);
m_fLastUpdateTime = fNow;
if(!bPlaying || animation.m_aPanelGraphics.empty())
return;
// If the current frame is past the end, a new animation was probably
// loaded.
if(iCurrentFrame >= animation.m_aPanelGraphics.size())
Rewind();
// Advance time.
fTime += fSeconds;
// If we're still on this frame, we're done.
float fFrameDuration = animation.m_iFrameDurations[iCurrentFrame];
if(fTime - 0.00001f < fFrameDuration)
return;
// If we've passed the end of the frame, move to the next frame. Don't
// skip frames if we're updating too quickly.
fTime -= fFrameDuration;
if(fTime > 0)
fTime = 0;
// Advance the frame.
iCurrentFrame++;
// If we're at the end of the frame, rewind to the loop frame.
if(iCurrentFrame == animation.m_aPanelGraphics.size())
iCurrentFrame = animation.m_iLoopFrame;
}
};
struct AnimationStateForPad
{
// asLightsData is an array of lights data to send to the pad and graphic
// is an animation graphic. Overlay graphic on top of the lights.
void OverlayLights(char *asLightsData, const vector<SMXGif::Color> &graphic) const
{
// Stop if this graphic isn't loaded or is paused.
if(graphic.empty())
return;
for(size_t i = 0; i < graphic.size(); ++i)
{
if(i >= LIGHTS_PER_PANEL)
return;
// If this color is transparent, leave the released animation alone.
if(graphic[i].color[3] == 0)
continue;
asLightsData[i*3+0] = graphic[i].color[0];
asLightsData[i*3+1] = graphic[i].color[1];
asLightsData[i*3+2] = graphic[i].color[2];
}
}
// Return the command to set the current animation state as pad lights.
string GetLightsCommand(int iPadState, const SMXConfig &config) const
{
g_Lock.AssertLockedByCurrentThread();
// If AutoLightingUsePressedAnimations is set, use lights animations.
// If it's not (the config tool is set to step color), mimic the built-in
// step color behavior instead of using pressed animations. Any released
// animation will always be used.
bool bUsePressedAnimations = config.flags & PlatformFlags_AutoLightingUsePressedAnimations;
const int iBytesPerPanel = LIGHTS_PER_PANEL*3;
const int iTotalLights = 9*iBytesPerPanel;
string result(iTotalLights, 0);
for(int panel = 0; panel < 9; ++panel)
{
// The portion of lights data for this panel:
char *out = &result[panel*iBytesPerPanel];
// Skip this panel if it's not in autoLightPanelMask.
if(!(config.autoLightPanelMask & (1 << panel)))
continue;
// Add the released animation, then overlay the pressed animation if we're pressed.
OverlayLights(out, animations[SMX_LightsType_Released][panel].GetAnimationFrame());
bool bPressed = bool(iPadState & (1 << panel));
if(bPressed && bUsePressedAnimations)
OverlayLights(out, animations[SMX_LightsType_Pressed][panel].GetAnimationFrame());
else if(bPressed && !bUsePressedAnimations)
{
// Light all LEDs on this panel using stepColor.
double LightsScaleFactor = 0.666666f;
const uint8_t *color = &config.stepColor[panel*3];
for(int light = 0; light < LIGHTS_PER_PANEL; ++light)
{
for(int i = 0; i < 3; ++i)
{
// stepColor is scaled to the 0-170 range. Scale it back to the 0-255 range.
// User applications don't need to worry about this since they normally don't
// need to care about stepColor.
uint8_t c = color[i];
c = (uint8_t) lrintf(min(255.0f, static_cast<float>(c / LightsScaleFactor)));
out[light*3+i] = c;
}
}
}
}
return result;
}
// State for both animations on each panel:
AnimationState animations[NUM_SMX_LightsType][9];
};
namespace
{
// Animations and animation states for both pads.
AnimationStateForPad pad_states[2];
}
namespace {
// The X,Y positions of each possible panel.
vector<pair<int,int>> graphic_positions = {
{ 0,0 },
{ 1,0 },
{ 2,0 },
{ 0,1 },
{ 1,1 },
{ 2,1 },
{ 0,2 },
{ 1,2 },
{ 2,2 },
};
// Given a 14x15 graphic frame and a panel number, return an array of 16 colors, containing
// each light in the order it's sent to the master controller.
void ConvertToPanelGraphic16(const SMXGif::GIFImage &src, vector<SMXGif::Color> &dst, int panel)
{
dst.clear();
// The top-left corner for this panel:
int x = graphic_positions[panel].first * 5;
int y = graphic_positions[panel].second * 5;
// Add the 4x4 grid.
for(int dy = 0; dy < 4; ++dy)
for(int dx = 0; dx < 4; ++dx)
dst.push_back(src.get(x+dx, y+dy));
// These animations have no data for the 3x3 grid, so just set them to transparent.
for(int dy = 0; dy < 3; ++dy)
for(int dx = 0; dx < 3; ++dx)
dst.push_back(SMXGif::Color(0,0,0,0));
}
// Given a 23x24 graphic frame and a panel number, return an array of 25 colors, containing
// each light in the order it's sent to the master controller.
void ConvertToPanelGraphic25(const SMXGif::GIFImage &src, vector<SMXGif::Color> &dst, int panel)
{
dst.clear();
// The top-left corner for this panel:
int x = graphic_positions[panel].first * 8;
int y = graphic_positions[panel].second * 8;
// Add the 4x4 grid first.
for(int dy = 0; dy < 4; ++dy)
for(int dx = 0; dx < 4; ++dx)
dst.push_back(src.get(x+dx*2, y+dy*2));
// Add the 3x3 grid.
for(int dy = 0; dy < 3; ++dy)
for(int dx = 0; dx < 3; ++dx)
dst.push_back(src.get(x+dx*2+1, y+dy*2+1));
}
}
// Load an array of animation frames as a panel animation. Each frame must
// be 14x15 or 23x24.
void SMXPanelAnimation::Load(const vector<SMXGif::SMXGifFrame> &frames, int panel)
{
m_aPanelGraphics.clear();
m_iFrameDurations.clear();
m_iLoopFrame = -1;
for(size_t frame_no = 0; frame_no < frames.size(); ++frame_no)
{
const SMXGif::SMXGifFrame &gif_frame = frames[frame_no];
// If the bottom-left pixel is white, this is the loop frame, which marks the
// frame the animation should start at after a loop. This is global to the
// animation, not specific to each panel.
SMXGif::Color marker = gif_frame.frame.get(0, gif_frame.frame.height-1);
if(marker.color[3] == 0xFF && marker.color[0] >= 0x80)
{
// We shouldn't see more than one of these. If we do, use the first.
if(m_iLoopFrame == -1)
m_iLoopFrame = frame_no;
}
// Extract this frame. If the graphic is 14x15 it's a 4x4 animation,
// and if it's 23x24 it's 25-light.
vector<SMXGif::Color> panel_graphic;
if(frames[0].width == 14)
ConvertToPanelGraphic16(gif_frame.frame, panel_graphic, panel);
else
ConvertToPanelGraphic25(gif_frame.frame, panel_graphic, panel);
// GIFs have a very low-resolution duration field, with 10ms units.
// The panels run at 30 FPS internally, or 33 1/3 ms, but GIF can only
// represent 30ms or 40ms. Most applications will probably output 30,
// but snap both 30ms and 40ms to exactly 30 FPS to make sure animations
// that are meant to run at native framerate do.
float seconds;
if(gif_frame.milliseconds == 30 || gif_frame.milliseconds == 40)
seconds = 1 / 30.0f;
else
seconds = gif_frame.milliseconds / 1000.0;
m_aPanelGraphics.push_back(panel_graphic);
m_iFrameDurations.push_back(seconds);
}
// By default, loop back to the first frame.
if(m_iLoopFrame == -1)
m_iLoopFrame = 0;
}
#include "SMXPanelAnimationUpload.h"
// Load a GIF into SMXLoadedPanelAnimations::animations.
bool SMX_LightsAnimation_Load(const char *gif, int size, int pad, SMX_LightsType type, const char **error)
{
// Parse the GIF.
string buf(gif, size);
vector<SMXGif::SMXGifFrame> frames;
if(!SMXGif::DecodeGIF(buf, frames) || frames.empty())
{
*error = "The GIF couldn't be read.";
return false;
}
// Check the dimensions of the image. We only need to check the first, the
// others will always have the same size.
if((frames[0].width != 14 || frames[0].height != 15) && (frames[0].width != 23 || frames[0].height != 24))
{
*error = "The GIF must be 14x15 or 23x24.";
return false;
}
// Load the graphics into SMXPanelAnimations.
SMXPanelAnimation animations[9];
for(int panel = 0; panel < 9; ++panel)
animations[panel].Load(frames, panel);
// Set up the upload for this graphic.
if(!SMX_LightsUpload_PrepareUpload(pad, type, animations, error))
return false;
// Lock while we access pad_states.
g_Lock.AssertNotLockedByCurrentThread();
LockMutex L(g_Lock);
// Commit the animation to pad_states now that we know there are no errors.
for(int panel = 0; panel < 9; ++panel)
{
SMXPanelAnimation &animation = pad_states[pad].animations[type][panel].animation;
animation = animations[panel];
}
return true;
}
namespace
{
double g_fStopAnimatingUntil = -1;
}
void SMXAutoPanelAnimations::TemporaryStopAnimating()
{
// Stop animating for 100ms.
double fStopForSeconds = 1/10.0f;
g_fStopAnimatingUntil = SMX::GetMonotonicTime() + fStopForSeconds;
}
// A thread to handle setting light animations. We do this in a separate
// thread rather than in the SMXManager thread so this can be treated as
// if it's external application thread, and it's making normal threaded
// calls to SetLights.
class PanelAnimationThread: public SMXThread
{
public:
static shared_ptr<PanelAnimationThread> g_pSingleton;
PanelAnimationThread():
SMXThread(g_Lock)
{
Start("SMX light animations");
}
private:
void ThreadMain()
{
m_Lock.Lock();
// Update lights at 30 FPS.
const int iDelayMS = 33;
while(!m_bShutdown)
{
// Check if we've temporarily stopped updating lights.
bool bSkipUpdate = g_fStopAnimatingUntil > SMX::GetMonotonicTime();
// Run a single panel lights update.
if(!bSkipUpdate)
UpdateLights();
// Wait up to 30 FPS, or until we're signalled. We can only be signalled
// if we're shutting down, so we don't need to worry about partial frame
// delays.
m_Event.Wait(iDelayMS);
}
m_Lock.Unlock();
}
// Return lights for the given pad and pad state, using the loaded panel animations.
bool GetCurrentLights(string &asLightsDataOut, int pad, int iPadState)
{
m_Lock.AssertLockedByCurrentThread();
// Get this pad's configuration.
SMXConfig config;
if(!SMXManager::g_pSMX->GetDevice(pad)->GetConfig(config))
return false;
// If this controller handles animation itself, don't handle it here too. It can
// lead to confusing situations if SMXConfig's animations don't match the ones stored
// on the pad.
if(config.masterVersion >= 4)
return false;
AnimationStateForPad &pad_state = pad_states[pad];
// Make sure the correct animations are playing.
for(int panel = 0; panel < 9; ++panel)
{
// The released animation is always playing.
pad_state.animations[SMX_LightsType_Released][panel].Play();
// The pressed animation only plays while the button is pressed,
// and rewind when it's released.
bool bPressed = iPadState & (1 << panel);
if(bPressed)
pad_state.animations[SMX_LightsType_Pressed][panel].Play();
else
pad_state.animations[SMX_LightsType_Pressed][panel].Stop();
}
// Set the current state.
asLightsDataOut = pad_state.GetLightsCommand(iPadState, config);
// Advance animations.
for(int type = 0; type < NUM_SMX_LightsType; ++type)
{
for(int panel = 0; panel < 9; ++panel)
pad_state.animations[type][panel].Update();
}
return true;
}
// Run a single light animation update.
void UpdateLights()
{
string asLightsData[2];
bool bHaveLights = false;
for(int pad = 0; pad < 2; pad++)
{
int iPadState = SMXManager::g_pSMX->GetDevice(pad)->GetInputState();
if(GetCurrentLights(asLightsData[pad], pad, iPadState))
bHaveLights = true;
}
// Update lights.
if(bHaveLights)
SMXManager::g_pSMX->SetLights(asLightsData);
}
};
void SMX_LightsAnimation_SetAuto(bool enable)
{
if(!enable)
{
// If we're turning off, shut down the thread if it's running.
if(PanelAnimationThread::g_pSingleton)
PanelAnimationThread::g_pSingleton->Shutdown();
PanelAnimationThread::g_pSingleton.reset();
return;
}
// Create the animation thread if it's not already running.
if(PanelAnimationThread::g_pSingleton)
return;
PanelAnimationThread::g_pSingleton.reset(new PanelAnimationThread());
}
shared_ptr<PanelAnimationThread> PanelAnimationThread::g_pSingleton;

View File

@@ -0,0 +1,57 @@
#ifndef SMXPanelAnimation_h
#define SMXPanelAnimation_h
#include <vector>
#include "SMXGif.h"
enum SMX_LightsType
{
SMX_LightsType_Released, // animation while panels are released
SMX_LightsType_Pressed, // animation while panel is pressed
NUM_SMX_LightsType,
};
// SMXPanelAnimation holds an animation, with graphics for a single panel.
class SMXPanelAnimation
{
public:
void Load(const std::vector<SMXGif::SMXGifFrame> &frames, int panel);
// The high-level animated GIF frames:
std::vector<std::vector<SMXGif::Color>> m_aPanelGraphics;
// The animation starts on frame 0. When it reaches the end, it loops
// back to this frame.
int m_iLoopFrame = 0;
// The duration of each frame in seconds.
std::vector<float> m_iFrameDurations;
};
namespace SMXAutoPanelAnimations
{
// If SMX_LightsAnimation_SetAuto is active, stop sending animations briefly. This is
// called when lights are set directly, so they don't compete with the animation.
void TemporaryStopAnimating();
}
// For SMX_API:
#include "../SMX.h"
// High-level interface for C# bindings:
//
// Load an animated GIF as a panel animation. pad is the pad this animation is for (0 or 1),
// and type is which animation this is for. Any previously loaded animation will be replaced.
// On error, false is returned and error is set to a plain-text error message which is valid
// until the next call. On success, the animation can be uploaded to the pad if supported using
// SMX_LightsUpload_BeginUpload, or used directly with SMX_LightsAnimation_SetAuto.
SMX_API bool SMX_LightsAnimation_Load(const char *gif, int size, int pad, SMX_LightsType type, const char **error);
// Enable or disable automatically handling lights animations. If enabled, any animations
// loaded with SMX_LightsAnimation_Load will run automatically as long as the SDK is loaded.
// This only has an effect if the platform doesn't handle animations directly. On newer firmware,
// this has no effect (upload the animation to the panel instead).
// XXX: should we automatically disable SMX_SetLights when this is enabled?
SMX_API void SMX_LightsAnimation_SetAuto(bool enable);
#endif

View File

@@ -0,0 +1,499 @@
#include "SMXPanelAnimationUpload.h"
#include "SMXPanelAnimation.h"
#include "SMXGif.h"
#include "SMXManager.h"
#include "SMXDevice.h"
#include "Helpers.h"
#include <string>
#include <vector>
#include <cmath>
using namespace std;
using namespace SMX;
// This handles setting up commands to upload panel animations to the
// controller.
//
// This is only meant to be used by configuration tools to allow setting
// up animations that work while the pad isn't being controlled by the
// SDK. If you want to control lights for your game, this isn't what
// you want. Use SMX_SetLights instead.
//
// Panel animations are sent to the master controller one panel at a time, and
// each animation can take several commands to upload to fit in the protocol packet
// size. These commands are stateful.
namespace
{
// Panel names for error messages.
static const char *panel_names[] = {
"up-left", "up", "up-right",
"left", "center", "right",
"down-left", "down", "down-right",
};
}
// These structs are the protocol we use to send offline graphics to the pad.
// This isn't related to realtime lighting.
namespace PanelLightGraphic
{
// One 24-bit RGB color:
struct color_t {
uint8_t rgb[3];
};
// 4-bit palette, 15 colors. Our graphics are 4-bit. Color 0xF is transparent,
// so we don't have a palette entry for it.
struct palette_t {
color_t colors[15];
};
// A single 4-bit paletted graphic.
struct graphic_t {
uint8_t data[13];
};
struct panel_animation_data_t
{
// Our graphics and palettes. We can apply either palette to any graphic. Note that
// each graphic is 13 bytes and each palette is 45 bytes.
graphic_t graphics[64];
palette_t palettes[2];
};
struct animation_timing_t
{
// An index into frames[]:
uint8_t loop_animation_frame;
// A list of graphic frames to display, and how long to display them in
// 30 FPS frames. A frame index of 0xFF (or reaching the end) loops.
uint8_t frames[64];
uint8_t delay[64];
};
// Commands to upload data:
#pragma pack(push, 1)
struct upload_packet
{
// 'm' to upload master animation data.
uint8_t cmd = 'm';
// The panel this data is for. If this is 0xFF, it's for the master.
uint8_t panel = 0;
// For master uploads, the animation number to modify. Panels ignore this field.
uint8_t animation_idx = 0;
// True if this is the last upload packet. This lets the firmware know that
// this part of the upload is finished and it can update anything that might
// be affected by it, like resetting lights animations.
bool final_packet = false;
uint16_t offset = 0;
uint8_t size = 0;
uint8_t data[240];
};
#pragma pack(pop)
#pragma pack(push, 1)
struct delay_packet
{
// 'd' to ask the master to delay.
uint8_t cmd = 'd';
// How long to delay:
uint16_t milliseconds = 0;
};
#pragma pack(pop)
// Make sure the packet fits in a command packet.
static_assert(sizeof(upload_packet) <= 0xFF, "");
}
// The GIFs can use variable framerates. The panels update at 30 FPS.
#define FPS 30
// Helpers for converting PanelGraphics to the packed sprite representation
// we give to the pad.
namespace ProtocolHelpers
{
// Return a color's index in palette. If the color isn't found, return 0xFF.
// We can use a dumb linear search here since the graphics are so small.
uint8_t GetColorIndex(const PanelLightGraphic::palette_t &palette, const SMXGif::Color &color)
{
// Transparency is always palette index 15.
if(color.color[3] == 0)
return 15;
for(int idx = 0; idx < 15; ++idx)
{
PanelLightGraphic::color_t pad_color = palette.colors[idx];
if(pad_color.rgb[0] == color.color[0] &&
pad_color.rgb[1] == color.color[1] &&
pad_color.rgb[2] == color.color[2])
return idx;
}
return 0xFF;
}
// Create a palette for an animation.
//
// We're loading from paletted GIFs, but we create a separate small palette
// for each panel's animation, so we don't use the GIF's palette.
bool CreatePalette(const SMXPanelAnimation &animation, PanelLightGraphic::palette_t &palette)
{
int next_color = 0;
for(const auto &panel_graphic: animation.m_aPanelGraphics)
{
for(const SMXGif::Color &color: panel_graphic)
{
// If this color is transparent, leave it out of the palette.
if(color.color[3] == 0)
continue;
// Check if this color is already in the palette.
uint8_t existing_idx = GetColorIndex(palette, color);
if(existing_idx != 0xFF)
continue;
// Return false if we're using too many colors.
if(next_color == 15)
return false;
// Add this color.
PanelLightGraphic::color_t pad_color;
pad_color.rgb[0] = color.color[0];
pad_color.rgb[1] = color.color[1];
pad_color.rgb[2] = color.color[2];
palette.colors[next_color] = pad_color;
next_color++;
}
}
return true;
}
// Return packed paletted graphics for each frame, using a palette created
// with CreatePalette. The palette must have fewer than 16 colors.
void CreatePackedGraphic(const vector<SMXGif::Color> &image, const PanelLightGraphic::palette_t &palette,
PanelLightGraphic::graphic_t &out)
{
int position = 0;
memset(out.data, 0, sizeof(out.data));
for(auto color: image)
{
// Transparency is always palette index 15.
uint8_t palette_idx = GetColorIndex(palette, color);
if(palette_idx == 0xFF)
palette_idx = 0;
// If this is an odd index, put the palette index in the low 4
// bits. Otherwise, put it in the high 4 bits.
if(position & 1)
out.data[position/2] |= (palette_idx & 0x0F) << 0;
else
out.data[position/2] |= (palette_idx & 0x0F) << 4;
position++;
}
}
vector<uint8_t> get_frame_delays(const SMXPanelAnimation &animation)
{
vector<uint8_t> result;
size_t current_frame = 0;
float time_left_in_frame = animation.m_iFrameDurations[0];
result.push_back(0);
while(1)
{
// Advance time by 1/FPS seconds.
time_left_in_frame -= 1.0f / FPS;
result.back()++;
if(time_left_in_frame <= 0.00001f)
{
// We've displayed this frame long enough, so advance to the next frame.
if(current_frame + 1 == animation.m_iFrameDurations.size())
break;
current_frame += 1;
result.push_back(0);
time_left_in_frame += animation.m_iFrameDurations[current_frame];
// If time_left_in_frame is still negative, the animation is too fast.
if(time_left_in_frame < 0.00001)
time_left_in_frame = 0;
}
}
return result;
}
// Create the master data. This just has timing information.
bool CreateMasterAnimationData(SMX_LightsType type,
const SMXPanelAnimation &animation,
PanelLightGraphic::animation_timing_t &animation_timing, const char **error)
{
// Released (idle) animations use frames 0-31, and pressed animations use 32-63.
int first_graphic = type == SMX_LightsType_Released? 0:32;
// Check that we don't have more frames than we can fit in animation_timing.
// This is currently the same as the "too many frames" error below, but if
// we support longer delays (staying on the same graphic for multiple animation_timings)
// or deduping they'd be different.
if(animation.m_aPanelGraphics.size() > arraylen(animation_timing.frames))
{
*error = "The animation is too long.";
return false;
}
memset(&animation_timing.frames[0], 0xFF, sizeof(animation_timing.frames));
for(size_t i = 0; i < animation.m_aPanelGraphics.size(); ++i)
animation_timing.frames[i] = i + first_graphic;
// Set frame delays.
memset(&animation_timing.delay[0], 0, sizeof(animation_timing.delay));
vector<uint8_t> delays = get_frame_delays(animation);
for(size_t i = 0; i < delays.size() && i < 64; ++i)
animation_timing.delay[i] = delays[i];
// These frame numbers are relative to the animation, so don't add first_graphic.
animation_timing.loop_animation_frame = animation.m_iLoopFrame;
return true;
}
// Pack panel graphics.
bool CreatePanelAnimationData(PanelLightGraphic::panel_animation_data_t &panel_data,
int pad, SMX_LightsType type, int panel, const SMXPanelAnimation &animation, const char **error)
{
// We have a single buffer of animation frames for each panel, which we pack
// both the pressed and released frames into. This is the index of the next
// frame.
size_t next_graphic_idx = type == SMX_LightsType_Released? 0:32;
// Create this animation's 4-bit palette.
if(!ProtocolHelpers::CreatePalette(animation, panel_data.palettes[type]))
{
*error = SMX::CreateError(SMX::ssprintf("The %s panel uses too many colors.", panel_names[panel]));
return false;
}
// Create a small 4-bit paletted graphic with the 4-bit palette we created.
// These are the graphics we'll send to the controller.
for(const auto &panel_graphic: animation.m_aPanelGraphics)
{
if(next_graphic_idx > arraylen(panel_data.graphics))
{
*error = "The animation has too many frames.";
return false;
}
ProtocolHelpers::CreatePackedGraphic(panel_graphic, panel_data.palettes[type], panel_data.graphics[next_graphic_idx]);
next_graphic_idx++;
}
// Apply color scaling to the palette, in the same way SMXManager::SetLights does.
// Do this after we've finished creating the graphic, so this is only applied to
// the final result and doesn't affect palettization.
for(PanelLightGraphic::color_t &color: panel_data.palettes[type].colors)
{
for(int i = 0; i < 3; ++i)
color.rgb[i] = uint8_t(color.rgb[i] * 0.6666f);
}
return true;
}
// Create upload packets to upload a block of data.
void CreateUploadPackets(vector<PanelLightGraphic::upload_packet> &packets,
const void *data_block, int start, int size,
uint8_t panel, int animation_idx)
{
const uint8_t *buf = (const uint8_t *) data_block;
for(int offset = 0; offset < size; )
{
PanelLightGraphic::upload_packet packet;
packet.panel = panel;
packet.animation_idx = animation_idx;
packet.offset = start + offset;
int bytes_left = size - offset;
packet.size = min(sizeof(PanelLightGraphic::upload_packet::data), static_cast<size_t>(bytes_left));
memcpy(packet.data, buf, packet.size);
packets.push_back(packet);
offset += packet.size;
buf += packet.size;
}
}
}
namespace LightsUploadData
{
vector<string> commands[2];
}
// Prepare the loaded graphics for upload.
bool SMX_LightsUpload_PrepareUpload(int pad, SMX_LightsType type, const SMXPanelAnimation animations[9], const char **error)
{
// Create master animation data.
PanelLightGraphic::animation_timing_t master_animation_data;
memset(&master_animation_data, 0xFF, sizeof(master_animation_data));
// All animations of each type have the same timing for all panels, since
// they come from the same GIF, so just use the first frame to generate the
// master data.
if(!ProtocolHelpers::CreateMasterAnimationData(type, animations[0], master_animation_data, error))
return false;
// Create panel animation data.
PanelLightGraphic::panel_animation_data_t all_panel_data[9];
memset(&all_panel_data, 0xFF, sizeof(all_panel_data));
for(int panel = 0; panel < 9; ++panel)
{
if(!ProtocolHelpers::CreatePanelAnimationData(all_panel_data[panel], pad, type, panel, animations[panel], error))
return false;
}
// We successfully created the data, so there's nothing else that can fail from
// here on.
//
// A list of the final commands we'll send:
vector<string> &pad_commands = LightsUploadData::commands[pad];
pad_commands.clear();
// Add an upload packet to pad_commands:
auto add_packet_command = [&pad_commands](const PanelLightGraphic::upload_packet &packet) {
string command((char *) &packet, sizeof(packet));
pad_commands.push_back(command);
};
// Add a command to briefly delay the master, to give panels a chance to finish writing to EEPROM.
auto add_delay = [&pad_commands](int milliseconds) {
PanelLightGraphic::delay_packet packet;
packet.milliseconds = milliseconds;
string command((char *) &packet, sizeof(packet));
pad_commands.push_back(command);
};
// Create the packets we'll send, grouped by panel.
vector<PanelLightGraphic::upload_packet> packetsPerPanel[9];
for(int panel = 0; panel < 9; ++panel)
{
// Only upload the panel graphic data and the palette we're changing. If type
// is 0 (SMX_LightsType_Released), we're uploading the first 32 graphics and palette
// 0. If it's 1 (SMX_LightsType_Pressed), we're uploading the second 32 graphics
// and palette 1.
const auto &panel_data_block = all_panel_data[panel];
{
int first_graphic = type == SMX_LightsType_Released? 0:32;
const PanelLightGraphic::graphic_t *graphics = &panel_data_block.graphics[first_graphic];
//int offset = offsetof(PanelLightGraphic::panel_animation_data_t, graphics[first_graphic]);
int offset;
{
PanelLightGraphic::panel_animation_data_t tmp;
intptr_t start = reinterpret_cast<intptr_t>(&tmp);
intptr_t target = reinterpret_cast<intptr_t>(&tmp.graphics[first_graphic]);
offset = static_cast<int>(target-start);
}
ProtocolHelpers::CreateUploadPackets(packetsPerPanel[panel], graphics, offset, sizeof(PanelLightGraphic::graphic_t) * 32, panel, type);
}
{
const PanelLightGraphic::palette_t *palette = &panel_data_block.palettes[type];
//int offset = offsetof(PanelLightGraphic::panel_animation_data_t, palettes[type]);
int offset;
{
PanelLightGraphic::panel_animation_data_t tmp;
intptr_t start = reinterpret_cast<intptr_t>(&tmp);
intptr_t target = reinterpret_cast<intptr_t>(&tmp.palettes[type]);
offset = static_cast<int>(target-start);
}
ProtocolHelpers::CreateUploadPackets(packetsPerPanel[panel], palette, offset, sizeof(PanelLightGraphic::palette_t), panel, type);
}
}
// It takes 3.4ms per byte to write to EEPROM, and we need to avoid writing data
// to any single panel faster than that or data won't be written. However, we're
// writing each data separately to each panel, so we can write data to panel 1, then
// immediately write to panel 2 while panel 1 is busy doing the write. Taking advantage
// of this makes the upload go much faster. Panels will miss commands while they're
// writing data, but we don't care if panel 1 misses a command that's writing to panel
// 2 that it would ignore anyway.
//
// We write the first set of packets for each panel, then explicitly delay long enough
// for them to finish before writing the next set of packets.
while(1)
{
bool added_any_packets = false;
int max_size = 0;
for(int panel = 0; panel < 9; ++panel)
{
// Pull this panel's next packet. It doesn't actually matter what order we
// send the packets in.
// Add the next packet for each panel.
vector<PanelLightGraphic::upload_packet> &packets = packetsPerPanel[panel];
if(packets.empty())
continue;
PanelLightGraphic::upload_packet packet = packets.back();
packets.pop_back();
add_packet_command(packet);
max_size = max(max_size, static_cast<int>(packet.size));
added_any_packets = true;
}
// Delay long enough for the biggest write in this burst to finish. We do this
// by sending a command to the master to tell it to delay synchronously by the
// right amount.
int millisecondsToDelay = lrintf(max_size * 3.4);
add_delay(millisecondsToDelay);
// Stop if there were no more packets to add.
if(!added_any_packets)
break;
}
// Add the master data.
vector<PanelLightGraphic::upload_packet> masterPackets;
ProtocolHelpers::CreateUploadPackets(masterPackets, &master_animation_data, 0, sizeof(master_animation_data), 0xFF, type);
masterPackets.back().final_packet = true;
for(const auto &packet: masterPackets)
add_packet_command(packet);
return true;
}
// Start sending a prepared upload.
//
// The commands to send to upload the data are in LightsUploadData::commands[pad].
void SMX_LightsUpload_BeginUpload(int pad, SMX_LightsUploadCallback pCallback, void *pUser)
{
shared_ptr<SMXDevice> pDevice = SMXManager::g_pSMX->GetDevice(pad);
vector<string> asCommands = LightsUploadData::commands[pad];
int iTotalCommands = static_cast<int>(asCommands.size());
// Queue all commands at once. As each command finishes, our callback
// will be called.
for(int i = 0; i < static_cast<int>(asCommands.size()); ++i)
{
const string &sCommand = asCommands[i];
pDevice->SendCommand(sCommand, [i, iTotalCommands, pCallback, pUser](string response) {
// Command #i has finished being sent.
//
// If this isn't the last command, make sure progress isn't 100.
// Once we send 100%, the callback is no longer valid.
int progress;
if(i != iTotalCommands-1)
progress = min((i*100) / (iTotalCommands - 1), 99);
else
progress = 100;
// We're currently in the SMXManager thread. Call the user thread from
// the user callback thread.
SMXManager::g_pSMX->RunInHelperThread([pCallback, pUser, progress]() {
pCallback(progress, pUser);
});
});
}
}

View File

@@ -0,0 +1,39 @@
#ifndef SMXPanelAnimationUpload_h
#define SMXPanelAnimationUpload_h
#include "SMXPanelAnimation.h"
// For SMX_API:
#include "../SMX.h"
// This is used to upload panel animations to the firmware. This is
// only needed for offline animations. For live animations, either
// use SMX_LightsAnimation_SetAuto, or to control lights directly
// (recommended), use SMX_SetLights. animations[] contains the animations
// to load.
//
// Prepare the currently loaded animations to be stored on the pad.
// Return false with an error message on error.
//
// All LightTypes must be loaded before beginning the upload.
//
// If a lights upload is already in progress, returns an error.
SMX_API bool SMX_LightsUpload_PrepareUpload(int pad, SMX_LightsType type, const SMXPanelAnimation animations[9], const char **error);
typedef void SMX_LightsUploadCallback(int progress, void *pUser);
// After a successful call to SMX_LightsUpload_PrepareUpload, begin uploading data
// to the master controller for the given pad and animation type.
//
// The callback will be called as the upload progresses, with progress values
// from 0-100.
//
// callback will always be called exactly once with a progress value of 100.
// Once the 100% progress is called, the callback won't be accessed, so the
// caller can safely clean up. This will happen even if the pad disconnects
// partway through the upload.
//
// The callback will be called from the user callback helper thread.
SMX_API void SMX_LightsUpload_BeginUpload(int pad, SMX_LightsUploadCallback callback, void *pUser);
#endif

View File

@@ -0,0 +1,50 @@
#include "SMXThread.h"
#include <stdexcept>
using namespace std;
using namespace SMX;
SMXThread::SMXThread(Mutex &lock):
m_Lock(lock),
m_Event(lock)
{
}
void SMX::SMXThread::SetHighPriority(bool bHighPriority)
{
if(m_hThread == INVALID_HANDLE_VALUE)
throw runtime_error("SetHighPriority called while the thread isn't running");
SetThreadPriority(m_hThread, THREAD_PRIORITY_HIGHEST);
}
bool SMX::SMXThread::IsCurrentThread() const
{
return GetCurrentThreadId() == m_iThreadId;
}
void SMXThread::Start(string name)
{
// Start the thread.
m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &m_iThreadId);
SMX::SetThreadName(m_iThreadId, name);
}
void SMXThread::Shutdown()
{
m_Lock.AssertNotLockedByCurrentThread();
// Shut down the thread and wait for it to exit.
m_bShutdown = true;
m_Event.Set();
WaitForSingleObject(m_hThread, INFINITE);
m_hThread = INVALID_HANDLE_VALUE;
}
DWORD WINAPI SMXThread::ThreadMainStart(void *self_)
{
SMXThread *self = (SMXThread *) self_;
self->ThreadMain();
return 0;
}

View File

@@ -0,0 +1,45 @@
#ifndef SMXThread_h
#define SMXThread_h
// A base class for a thread.
#include "Helpers.h"
#include <string>
namespace SMX
{
class SMXThread
{
public:
SMXThread(SMX::Mutex &lock);
// Raise the priority of the thread.
void SetHighPriority(bool bHighPriority);
// Start the thread, giving it a name for debugging.
void Start(std::string name);
// Shut down the thread. This function won't return until the thread
// has been stopped.
void Shutdown();
// Return true if this is the calling thread.
bool IsCurrentThread() const;
// The derived class implements this.
virtual void ThreadMain() = 0;
protected:
static DWORD WINAPI ThreadMainStart(void *self);
SMX::Mutex &m_Lock;
SMX::Event m_Event;
bool m_bShutdown = false;
private:
HANDLE m_hThread = INVALID_HANDLE_VALUE;
DWORD m_iThreadId = 0;
};
}
#endif