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

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