diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c4ae9de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root=true + +[*] +indent_style = space + +[*.{cpp,cs,h}] +indent_size = 4 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..677b8b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vs +*.user +build +obj +out diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..16ecdac --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2017 Step Revolution LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..557d760 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +This is the SDK for StepManiaX platform development. + +See [the StepManiaX website](https://stepmaniax.com) and the [documentation](https://steprevolution.github.io/stepmaniax-sdk/) +for info. + diff --git a/SMX.sln b/SMX.sln new file mode 100644 index 0000000..87244e4 --- /dev/null +++ b/SMX.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2009 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SMX", "sdk\Windows\SMX.vcxproj", "{C5FC0823-9896-4B7C-BFE1-B60DB671A462}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SMXSample", "sample\SMXSample.vcxproj", "{8861D665-FD49-4EFD-92C3-F4B8548AFD23}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMXConfig", "smx-config\SMXConfig.csproj", "{B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C5FC0823-9896-4B7C-BFE1-B60DB671A462}.Debug|x86.ActiveCfg = Debug|Win32 + {C5FC0823-9896-4B7C-BFE1-B60DB671A462}.Debug|x86.Build.0 = Debug|Win32 + {C5FC0823-9896-4B7C-BFE1-B60DB671A462}.Release|x86.ActiveCfg = Release|Win32 + {C5FC0823-9896-4B7C-BFE1-B60DB671A462}.Release|x86.Build.0 = Release|Win32 + {8861D665-FD49-4EFD-92C3-F4B8548AFD23}.Debug|x86.ActiveCfg = Debug|Win32 + {8861D665-FD49-4EFD-92C3-F4B8548AFD23}.Debug|x86.Build.0 = Debug|Win32 + {8861D665-FD49-4EFD-92C3-F4B8548AFD23}.Release|x86.ActiveCfg = Release|Win32 + {8861D665-FD49-4EFD-92C3-F4B8548AFD23}.Release|x86.Build.0 = Release|Win32 + {B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}.Debug|x86.ActiveCfg = Debug|x86 + {B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}.Debug|x86.Build.0 = Debug|x86 + {B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}.Release|x86.ActiveCfg = Release|x86 + {B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8DAB67F2-430F-43CC-853E-03B5A24F9806} + EndGlobalSection +EndGlobal diff --git a/docs/icon.png b/docs/icon.png new file mode 100644 index 0000000..63d7ac3 Binary files /dev/null and b/docs/icon.png differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..2319607 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,157 @@ + + + + + + +

Introduction to the StepManiaX SDK

+ +The StepManiaX SDK supports C++ development for the StepManiaX dance platform. + +

Usage

+ +You can either build the solution and link the resulting SMX.dll to your application, +or import the source project and add it to your Visual Studio solution. The SDK +interface is SMX.h. +

+See sample for a sample application. +

+Up to two controllers are supported. SMX_GetInfo can be used to check which +controllers are connected. Each pad argument to API calls can be 0 for the +player 1 pad, or 1 for the player 2 pad. + +

HID support

+ +The platform can be used as a regular USB HID input device, which works in any game +that supports input remapping. +

+However, applications using this SDK to control the panels directly should ignore the +HID interface, and instead use SMX_GetInputState to retrieve the input state. + +

Platform lights

+ +The platform can have up to nine panels. Each panel has a grid of 4x4 RGB LEDs, which can +be individually controlled at up to 30 FPS. +

+See SMX_SetLights. + +

Platform configuration

+ +The platform configuration can be read and modified using SMX_GetConfig and SMX_SetConfig. +Most of the platform configuration doesn't need to be accessed by applications, since +users can use the SMXConfig application to manage their platform. +

+

+ +

Reference

+ +

void SMX_Start(SMXUpdateCallback UpdateCallback, void *pUser);

+ +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. + +

void SMX_Stop();

+ +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. + +

void SMX_SetLogCallback(SMXLogCallback callback);

+ +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. + +

void SMX_GetInfo(int pad, SMXInfo *info);

+ +Get info about a pad. Use this to detect which pads are currently connected. + +

uint16_t SMX_GetInputState(int pad);

+ +Get a mask of the currently pressed panels. + +

void SMX_SetLights(const char lightsData[864]);

+Update the lights. Both pads are always updated together. lightsData is a list of 8-bit RGB +colors, one for each LED. Each panel has lights in the following order: +

+

+0123
+4567
+89AB
+CDEF
+
+

+Panels are in the following order: +

+

+012 9AB
+345 CDE
+678 F01
+
+ +With 18 panels, 16 LEDs per panel and 3 bytes per LED, each light update has 864 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. + +

void SMX_ReenableAutoLights();

+ +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. + +

void SMX_GetConfig(int pad, SMXConfig *config);

+ +Get the current controller's configuration. + +

void SMX_SetConfig(int pad, const 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. + +

void SMX_FactoryReset(int pad);

+ +Reset a pad to its original configuration. + +

void SMX_ForceRecalibration(int pad);

+ +Request an immediate panel recalibration. This is normally not necessary, but can be helpful +for diagnostics. + +

+ void SMX_SetTestMode(int pad, SensorTestMode mode); +
+ bool SMX_GetTestData(int pad, SMXSensorTestModeData *data); +

+ +Set a panel test mode and request test data. This is used by the configuration tool. + + diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..4f9eb64 Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..f11d570 --- /dev/null +++ b/docs/style.css @@ -0,0 +1,26 @@ +body { + padding: 1em; + max-width: 1000px; + font-family: "Segoe UI",Helvetica,Arial,sans-serif; + margin-left: auto; + margin-right: auto; + line-height: 1.5; +} + +h1 { + font-size: 2em; +} +h1, h2 { + border-bottom: 1px solid #eeeeee; + padding-bottom: .3em; +} + +h3.ref { + display: block; + font-family: monospace; + background-color: #008080; + color: #FFFFFF; + padding: .5em; + + +} diff --git a/sample/SMXSample.cpp b/sample/SMXSample.cpp new file mode 100644 index 0000000..f25a81c --- /dev/null +++ b/sample/SMXSample.cpp @@ -0,0 +1,96 @@ +#include +#include +#include "SMX.h" +#include +#include +using namespace std; + +class InputSample +{ +public: + InputSample() + { + // Set a logging callback. This can be called before SMX_Start. + // SMX_SetLogCallback( SMXLogCallback ); + + // Start scanning. The update callback will be called when devices connect or + // disconnect or panels are pressed or released. This callback will be called + // from a thread. + SMX_Start( SMXStateChangedCallback, this ); + } + + static void SMXStateChangedCallback(int pad, SMXUpdateCallbackReason reason, void *pUser) + { + InputSample *pSelf = (InputSample *) pUser; + pSelf->SMXStateChanged( pad, reason ); + } + + static void SMXLogCallback(const char *log) + { + printf("-> %s\n", log); + } + + void SMXStateChanged(int pad, SMXUpdateCallbackReason reason) + { + printf("Device %i state changed: %04x\n", pad, SMX_GetInputState(pad)); + + } + + int iPanelToLight = 0; + void SetLights() + { + string sLightsData; + auto addColor = [&sLightsData](uint8_t r, uint8_t g, uint8_t b) + { + sLightsData.append( 1, r ); + sLightsData.append( 1, g ); + sLightsData.append( 1, b ); + }; + for( int iPad = 0; iPad < 2; ++iPad ) + { + for( int iPanel = 0; iPanel < 9; ++iPanel ) + { + bool bLight = iPanel == iPanelToLight && iPad == 0; + if( !bLight ) + { + for( int iLED = 0; iLED < 16; ++iLED ) + addColor( 0, 0, 0 ); + continue; + } + addColor( 0xFF, 0, 0 ); + addColor( 0xFF, 0, 0 ); + addColor( 0xFF, 0, 0 ); + addColor( 0xFF, 0, 0 ); + addColor( 0, 0xFF, 0 ); + addColor( 0, 0xFF, 0 ); + addColor( 0, 0xFF, 0 ); + addColor( 0, 0xFF, 0 ); + addColor( 0, 0, 0xFF ); + addColor( 0, 0, 0xFF ); + addColor( 0, 0, 0xFF ); + addColor( 0, 0, 0xFF ); + addColor( 0xFF, 0xFF, 0 ); + addColor( 0xFF, 0xFF, 0 ); + addColor( 0xFF, 0xFF, 0 ); + addColor( 0xFF, 0xFF, 0 ); + } + } + + SMX_SetLights( sLightsData.data() ); + } +}; + +int main() +{ + InputSample demo; + + // Loop forever for this sample. + while(1) + { + Sleep(500); + demo.SetLights(); + } + + return 0; +} + diff --git a/sample/SMXSample.vcxproj b/sample/SMXSample.vcxproj new file mode 100644 index 0000000..4a43efc --- /dev/null +++ b/sample/SMXSample.vcxproj @@ -0,0 +1,103 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + {8861D665-FD49-4EFD-92C3-F4B8548AFD23} + Win32Proj + SMXSample + 10.0.16299.0 + SMXSample + + + + Application + true + v141 + Unicode + + + Application + false + v141 + false + Unicode + + + + + + + + + + + + + + + true + $(TargetDir)../out/ + $(SolutionDir)/build/$(ProjectName)/$(Configuration)/ + + + false + $(TargetDir)../out/ + $(SolutionDir)/build/$(ProjectName)/$(Configuration)/ + + + + + + Level4 + Disabled + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + 4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings) + ..\sdk + + + Console + true + $(SolutionDir)/out/$(TargetName)$(TargetExt) + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + 4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings) + ..\sdk + + + Console + true + true + true + $(SolutionDir)/out/$(TargetName)$(TargetExt) + + + + + + + + {c5fc0823-9896-4b7c-bfe1-b60db671a462} + + + + + + \ No newline at end of file diff --git a/sample/SMXSample.vcxproj.filters b/sample/SMXSample.vcxproj.filters new file mode 100644 index 0000000..32430b1 --- /dev/null +++ b/sample/SMXSample.vcxproj.filters @@ -0,0 +1,14 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + + + Source Files + + + \ No newline at end of file diff --git a/sdk/SMX.h b/sdk/SMX.h new file mode 100644 index 0000000..7ca2efd --- /dev/null +++ b/sdk/SMX.h @@ -0,0 +1,244 @@ +#ifndef SMX_H +#define SMX_H + +#include + +#ifdef SMX_EXPORTS +#define SMX_API __declspec(dllexport) +#else +#define SMX_API __declspec(dllimport) +#endif + +struct SMXInfo; +struct SMXConfig; +enum SensorTestMode; +enum SMXUpdateCallbackReason; +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); +extern "C" 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. +extern "C" 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); +extern "C" SMX_API void SMX_SetLogCallback(SMXLogCallback callback); + +// Get info about a pad. Use this to detect which pads are currently connected. +extern "C" SMX_API void SMX_GetInfo(int pad, SMXInfo *info); + +// Get a mask of the currently pressed panels. +extern "C" SMX_API uint16_t SMX_GetInputState(int pad); + +// Update the lights. Both pads are always updated together. lightsData is a list of 8-bit RGB +// colors, one for each LED. Each panel has lights in the following order: +// +// 0123 +// 4567 +// 89AB +// CDEF +// +// Panels are in the following order: +// +// 012 9AB +// 345 CDE +// 678 F01 +// +// With 18 panels, 16 LEDs per panel and 3 bytes per LED, each light update has 864 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. +extern "C" SMX_API void SMX_SetLights(const char lightsData[864]); + +// 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. +extern "C" SMX_API void SMX_ReenableAutoLights(); + +// Get the current controller's configuration. +extern "C" SMX_API void 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. +extern "C" SMX_API void SMX_SetConfig(int pad, const SMXConfig *config); + +// Reset a pad to its original configuration. +extern "C" SMX_API void SMX_FactoryReset(int pad); + +// Request an immediate panel recalibration. This is normally not necessary, but can be helpful +// for diagnostics. +extern "C" SMX_API void SMX_ForceRecalibration(int pad); + +// Set a panel test mode and request test data. This is used by the configuration tool. +extern "C" SMX_API void SMX_SetTestMode(int pad, SensorTestMode mode); +extern "C" SMX_API bool SMX_GetTestData(int pad, SMXSensorTestModeData *data); + +// 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 { + // 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 +}; + +// 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 +{ + // These fields are unused and must be left at their existing values. + uint8_t unused1 = 0xFF, unused2 = 0xFF; + uint8_t unused3 = 0xFF, unused4 = 0xFF; + uint8_t unused5 = 0xFF, unused6 = 0xFF; + + // 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. + 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" + + // These are internal tunables and should be left unchanged. + 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" + + // 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 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; + + // This is an internal tunable that should be left unchanged. + uint16_t autoCalibrationSamplesPerAverage = 500; + + // 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 + uint8_t configVersion = 0x02; + + // The remaining thresholds (configVersion >= 2). + uint8_t unused9[10]; + uint8_t panelThreshold0Low, panelThreshold0High; + uint8_t panelThreshold3Low, panelThreshold3High; + uint8_t panelThreshold5Low, panelThreshold5High; + uint8_t panelThreshold6Low, panelThreshold6High; + uint8_t panelThreshold8Low, panelThreshold8High; +}; +static_assert(sizeof(SMXConfig) == 84, "Expected 84 bytes"); + +// The values (except for Off) correspond with the protocol and must not be changed. +enum SensorTestMode { + 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]; +}; + +#endif diff --git a/sdk/Windows/Helpers.cpp b/sdk/Windows/Helpers.cpp new file mode 100644 index 0000000..6141a24 --- /dev/null +++ b/sdk/Windows/Helpers.cpp @@ -0,0 +1,285 @@ +#include "Helpers.h" +#include +#include +using namespace std; +using namespace SMX; + +namespace { + function g_LogCallback = [](const string &log) { + printf("%6.3f: %s\n", GetMonotonicTime(), log.c_str()); + }; +}; + +void SMX::Log(string s) +{ + g_LogCallback(s); +} + +void SMX::SetLogCallback(function 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) +{ + + 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) +} + +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""; + FormatMessage(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) +{ + string sStr; + + char *pBuf = NULL; + int iChars = 128; + int iUsed = 0; + int iTry = 0; + + do + { + // Grow more than linearly (e.g. 512, 1536, 3072, etc) + iChars += iTry * 1024; + delete[] pBuf; + pBuf = new char[iChars]; + iUsed = vsnprintf(pBuf, iChars-1, szFormat, argList); + ++iTry; + } while(iUsed < 0); + + // assign whatever we managed to format + sStr.assign(pBuf, iUsed); + + delete[] pBuf; + + return sStr; +} + +string SMX::ssprintf(const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + return vssprintf(fmt, va); +} + +string SMX::BinaryToHex(const void *pData_, int iNumBytes) +{ + const unsigned char *pData = (const unsigned char *) pData_; + string s; + for(int i=0; iHigh1Time; + 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 != *InterruptTimeBias); + static std::pair 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 = { sizeof(OSVERSIONINFOW), }; + GetVersionEx(&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; +} + +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 exception("Expected to not be locked"); +} + +void SMX::Mutex::AssertLockedByCurrentThread() +{ + if(m_iLockedByThread != GetCurrentThreadId()) + throw exception("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); +} diff --git a/sdk/Windows/Helpers.h b/sdk/Windows/Helpers.h new file mode 100644 index 0000000..f6875af --- /dev/null +++ b/sdk/Windows/Helpers.h @@ -0,0 +1,104 @@ +#ifndef HELPERS_H +#define HELPERS_H + +#include +#include +#include +#include +#include +using namespace std; + +namespace SMX +{ +void Log(string s); + +// Set a function to receive logs written by SMX::Log. By default, logs are written +// to stdout. +void SetLogCallback(function 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, ...); +string BinaryToHex(const void *pData_, int iNumBytes); +string BinaryToHex(const string &sString); +bool GetRandomBytes(void *pData, int iBytes); +double GetMonotonicTime(); + +// 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 &pSelf): m_pSelf(GetPointers(pSelf, this)) { } +// const weak_ptr m_pSelf; +// }; +// +// shared_ptr obj; +// new Class(obj); +// +// For a more convenient way to invoke this, see CreateObj() below. + +template +weak_ptr GetPointers(shared_ptr &pSharedPtr, T *pObj) +{ + pSharedPtr.reset(pObj); + return pSharedPtr; +} + +// Create a class that retains a weak reference to itself, returning a shared_ptr. +template +shared_ptr CreateObj(Args&&... args) +{ + shared_ptr pResult; + new T(pResult, std::forward(args)...); + return dynamic_pointer_cast(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; +}; +} + +#endif diff --git a/sdk/Windows/SMX.cpp b/sdk/Windows/SMX.cpp new file mode 100644 index 0000000..dd35a6f --- /dev/null +++ b/sdk/Windows/SMX.cpp @@ -0,0 +1,65 @@ +// This implements the public API. + +#include +#include + +#include "../SMX.h" +#include "SMXManager.h" +#include "SMXDevice.h" +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; +} + +static shared_ptr g_pSMX; + +// DLL interface: +SMX_API void SMX_Start(SMXUpdateCallback callback, void *pUser) +{ + if(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))); + g_pSMX = make_shared(UpdateCallback); +} + +SMX_API void SMX_Stop() +{ + 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 void SMX_GetConfig(int pad, SMXConfig *config) { g_pSMX->GetDevice(pad)->GetConfig(*config); } +SMX_API void SMX_SetConfig(int pad, const SMXConfig *config) { g_pSMX->GetDevice(pad)->SetConfig(*config); } +SMX_API void SMX_GetInfo(int pad, SMXInfo *info) { g_pSMX->GetDevice(pad)->GetInfo(*info); } +SMX_API uint16_t SMX_GetInputState(int pad) { return g_pSMX->GetDevice(pad)->GetInputState(); } +SMX_API void SMX_FactoryReset(int pad) { g_pSMX->GetDevice(pad)->FactoryReset(); } +SMX_API void SMX_ForceRecalibration(int pad) { g_pSMX->GetDevice(pad)->ForceRecalibration(); } +SMX_API void SMX_SetTestMode(int pad, SensorTestMode mode) { g_pSMX->GetDevice(pad)->SetSensorTestMode((SensorTestMode) mode); } +SMX_API bool SMX_GetTestData(int pad, SMXSensorTestModeData *data) { return g_pSMX->GetDevice(pad)->GetTestData(*data); } +SMX_API void SMX_SetLights(const char lightsData[864]) { g_pSMX->SetLights(string(lightsData, 864)); } +SMX_API void SMX_ReenableAutoLights() { g_pSMX->ReenableAutoLights(); } diff --git a/sdk/Windows/SMX.vcxproj b/sdk/Windows/SMX.vcxproj new file mode 100644 index 0000000..cfd449d --- /dev/null +++ b/sdk/Windows/SMX.vcxproj @@ -0,0 +1,132 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + + + + + + + + + + + + + + + + + + + + + {C5FC0823-9896-4B7C-BFE1-B60DB671A462} + Win32Proj + SMX + 10.0.16299.0 + + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + false + v141 + false + Unicode + + + + + + + + + + + + + + + false + $(SolutionDir)out\ + $(SolutionDir)/build/$(ProjectName)/$(Configuration)/ + + + false + $(SolutionDir)out\ + $(SolutionDir)/build/$(ProjectName)/$(Configuration)/ + + + + + + Level4 + Disabled + _DEBUG;_WINDOWS;_USRDLL;SMX_EXPORTS;%(PreprocessorDefinitions) + false + 4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings) + ProgramDatabase + + + Windows + true + false + hid.lib;setupapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + $(SolutionDir)/out/$(TargetName)$(TargetExt) + + + + + Level4 + + + MaxSpeed + true + true + NDEBUG;_WINDOWS;_USRDLL;SMX_EXPORTS;%(PreprocessorDefinitions) + false + true + 4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings) + + + Windows + true + true + true + false + hid.lib;setupapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + $(SolutionDir)/out/$(TargetName)$(TargetExt) + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sdk/Windows/SMX.vcxproj.filters b/sdk/Windows/SMX.vcxproj.filters new file mode 100644 index 0000000..7b343b7 --- /dev/null +++ b/sdk/Windows/SMX.vcxproj.filters @@ -0,0 +1,61 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/sdk/Windows/SMXDevice.cpp b/sdk/Windows/SMXDevice.cpp new file mode 100644 index 0000000..d35bf78 --- /dev/null +++ b/sdk/Windows/SMXDevice.cpp @@ -0,0 +1,494 @@ +#include "SMXDevice.h" + +#include "../SMX.h" +#include "Helpers.h" +#include "SMXDeviceConnection.h" +#include "SMXDeviceSearch.h" +#include +#include +#include +#include +using namespace std; +using namespace SMX; + +// Extract test data for panel iPanel. +static void ReadDataForPanel(const vector &data, int iPanel, void *pOut, int iOutSize) +{ + int 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 SMX::SMXDevice::Create(shared_ptr hEvent, Mutex &lock) +{ + return CreateObj(hEvent, lock); +} + +SMX::SMXDevice::SMXDevice(shared_ptr &pSelf, shared_ptr hEvent, Mutex &lock): + m_pSelf(GetPointers(pSelf, this)), + m_hEvent(hEvent), + m_Lock(lock) +{ + m_pConnection = SMXDeviceConnection::Create(); +} + +SMX::SMXDevice::~SMXDevice() +{ +} + +bool SMX::SMXDevice::OpenDeviceHandle(shared_ptr 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; + + CallUpdateCallback(SMXUpdateCallback_Updated); +} + +shared_ptr SMX::SMXDevice::GetDeviceHandle() const +{ + return m_pConnection->GetDeviceHandle(); +} + +void SMX::SMXDevice::SetUpdateCallback(function 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 pComplete) +{ + LockMutex Lock(m_Lock); + SendCommandLocked(cmd, pComplete); +} + +void SMX::SMXDevice::SendCommandLocked(string cmd, function pComplete) +{ + m_Lock.AssertLockedByCurrentThread(); + + // This call is nonblocking, so it's safe to do this in the UI thread. + if(m_pConnection->IsConnected()) + { + 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); + + // 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"); + SendCommandLocked("g\n", [&] { + // We now have the new configuration. + m_Lock.AssertLockedByCurrentThread(); + 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; + + 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; + } + uint8_t iSize = buf[1]; + if(buf.size() < iSize+2) + { + Log("Communication error: invalid configuration packet"); + continue; + } + + // Copy in the configuration. + // Log(ssprintf("Read back configuration: %i bytes, first byte %i", iSize, buf[2])); + memcpy(&config, buf.data()+2, min(iSize, sizeof(config))); + m_bHaveConfig = true; + buf.erase(buf.begin(), buf.begin()+iSize+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; + + // Write configuration command: + string sData = ssprintf("w"); + 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, [&] { + 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; + + // After we write the configuration, read back the updated configuration to + // verify it. + SendCommandLocked("g\n"); +} + +void SMX::SMXDevice::Update(wstring &sError) +{ + m_Lock.AssertLockedByCurrentThread(); + + if(!m_pConnection->IsConnected()) + return; + + CheckActive(); + SendConfig(); + UpdateTestMode(); + + { + 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); + + // Reset panels. + SendCommandLocked("R\n"); + + // Read the current configuration. The device will return a "g" response containing + // its current SMXConfig. + SendCommandLocked("g\n"); +} + +// Check if we need to request test mode data. +void SMX::SMXDevice::UpdateTestMode() +{ + 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. + uint8_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 data; + for(int 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 dummy2:4; + }; +#pragma pack(pop) + + m_HaveSensorTestModeData = true; + SMXSensorTestModeData &output = m_SensorTestData; + 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)); + + 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) + { + // Log(ssprintf("Invalid data: %i %i %i", sig1, sig2, 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; + + for(int iSensor = 0; iSensor < 4; ++iSensor) + output.sensorLevel[iPanel][iSensor] = pad_data.sensors[iSensor]; + } + + CallUpdateCallback(SMXUpdateCallback_Updated); +} diff --git a/sdk/Windows/SMXDevice.h b/sdk/Windows/SMXDevice.h new file mode 100644 index 0000000..bdb6e8c --- /dev/null +++ b/sdk/Windows/SMXDevice.h @@ -0,0 +1,132 @@ +#ifndef SMXDevice_h +#define SMXDevice_h + +#include +#include +#include +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 Create(shared_ptr hEvent, SMX::Mutex &lock); + SMXDevice(shared_ptr &pSelf, shared_ptr hEvent, SMX::Mutex &lock); + ~SMXDevice(); + + bool OpenDeviceHandle(shared_ptr pHandle, wstring &sError); + void CloseDevice(); + shared_ptr 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 pCallback); + + // Return true if we're connected. + bool IsConnected() const; + + // Send a raw command. + void SendCommand(string sCmd, function pComplete=nullptr); + void SendCommandLocked(string sCmd, function 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); + + // 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 m_hEvent; + SMX::Mutex &m_Lock; + + function m_pUpdateCallback; + weak_ptr m_pSelf; + + shared_ptr 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; + bool m_bHaveConfig = false; + + // 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; + + void CallUpdateCallback(SMXUpdateCallbackReason reason); + void HandlePackets(); + + void SendConfig(); + void CheckActive(); + bool IsConnectedLocked() const; + + // Test/diagnostics mode handling. + void UpdateTestMode(); + 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 diff --git a/sdk/Windows/SMXDeviceConnection.cpp b/sdk/Windows/SMXDeviceConnection.cpp new file mode 100644 index 0000000..7ec4842 --- /dev/null +++ b/sdk/Windows/SMXDeviceConnection.cpp @@ -0,0 +1,378 @@ +#include "SMXDeviceConnection.h" +#include "Helpers.h" + +#include +#include +using namespace std; +using namespace SMX; + +#include +#include + +SMX::SMXDeviceConnection::PendingCommandPacket::PendingCommandPacket() +{ + memset(&m_OverlappedWrite, 0, sizeof(m_OverlappedWrite)); +} + +shared_ptr SMXDeviceConnection::Create() +{ + return CreateObj(); +} + +SMX::SMXDeviceConnection::SMXDeviceConnection(shared_ptr &pSelf): + m_pSelf(GetPointers(pSelf, this)) +{ + memset(&overlapped_read, 0, sizeof(overlapped_read)); +} + +SMX::SMXDeviceConnection::~SMXDeviceConnection() +{ + Close(); +} + +bool SMX::SMXDeviceConnection::Open(shared_ptr 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([&] { + 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()); + + 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) +{ + 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\n", 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 + + int 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(); + 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; + + m_sCurrentReadBuffer.append(sPacket); + + if(cmd & PACKET_FLAG_END_OF_COMMAND) + { + if(!m_sCurrentReadBuffer.empty()) + m_sReadBuffers.push_back(m_sCurrentReadBuffer); + m_sCurrentReadBuffer.clear(); + } + + 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_pCurrentCommand = nullptr; + } + + 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, sizeof(overlapped_read_buffer), 0); + 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 && !m_pCurrentCommand->m_Packets.empty()) + { + // A command is in progress. See if any writes have completed. + while(!m_pCurrentCommand->m_Packets.empty()) + { + shared_ptr pFirstPacket = m_pCurrentCommand->m_Packets.front(); + + DWORD bytes; + int iResult = GetOverlappedResult(m_hDevice->value(), &pFirstPacket->m_OverlappedWrite, &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_Packets.pop_front(); + } + + + // 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. + if(m_pCurrentCommand) + return; + + // Stop if we have nothing to do. + if(m_aPendingCommands.empty()) + return; + + // Send the next command. + shared_ptr pPendingCommand = m_aPendingCommands.front(); + + for(shared_ptr &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; + if(!WriteFile(m_hDevice->value(), pPacket->sData.data(), pPacket->sData.size(), &unused, &pPacket->m_OverlappedWrite)) + { + 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; + } + } + } + + // 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 pComplete) +{ + shared_ptr pPendingCommand = make_shared(); + pPendingCommand->m_pComplete = pComplete; + pPendingCommand->m_bIsDeviceInfoCommand = true; + + shared_ptr pCommandPacket = make_shared(); + + 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 pComplete) +{ + shared_ptr pPendingCommand = make_shared(); + pPendingCommand->m_pComplete = pComplete; + + // Send the command in packets. We allow sending zero-length packets here + // for testing purposes. + int i = 0; + do { + shared_ptr pCommandPacket = make_shared(); + + int iFlags = 0; + int iPacketSize = min(cmd.size() - i, 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); +} diff --git a/sdk/Windows/SMXDeviceConnection.h b/sdk/Windows/SMXDeviceConnection.h new file mode 100644 index 0000000..003b3a5 --- /dev/null +++ b/sdk/Windows/SMXDeviceConnection.h @@ -0,0 +1,122 @@ +#ifndef SMXDevice_H +#define SMXDevice_H + +#include +#include +#include +#include +#include +#include +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 Create(); + SMXDeviceConnection(shared_ptr &pSelf); + ~SMXDeviceConnection(); + + bool Open(shared_ptr DeviceHandle, wstring &error); + + void Close(); + + // Get the device handle opened by Open(), or NULL if we're not open. + shared_ptr 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 pComplete=nullptr); + + uint16_t GetInputState() const { return m_iInputState; } + +private: + void RequestDeviceInfo(function pComplete = nullptr); + + void CheckReads(wstring &error); + void BeginAsyncRead(wstring &error); + void CheckWrites(wstring &error); + void HandleUsbPacket(const string &buf); + + weak_ptr m_pSelf; + shared_ptr 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 m_sReadBuffers; + string m_sCurrentReadBuffer; + + struct PendingCommandPacket { + PendingCommandPacket(); + + string sData; + OVERLAPPED m_OverlappedWrite; + }; + + // Commands that are waiting to be sent: + struct PendingCommand { + list> m_Packets; + + // This is only called if m_bWaitForResponse if true. Otherwise, we send the command + // and forget about it. + function 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; + }; + list> 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 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 diff --git a/sdk/Windows/SMXDeviceSearch.cpp b/sdk/Windows/SMXDeviceSearch.cpp new file mode 100644 index 0000000..c6e5f3d --- /dev/null +++ b/sdk/Windows/SMXDeviceSearch.cpp @@ -0,0 +1,155 @@ +#include "SMXDeviceSearch.h" + +#include "SMXDeviceConnection.h" +#include "Helpers.h" + +#include +#include +#include +using namespace std; +using namespace SMX; + +#include +#include + +// Return all USB HID device paths. This doesn't open the device to filter just our devices. +static set 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 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(!SetupDiGetDeviceInterfaceDetail(DeviceInfoSet, &DeviceInterfaceData, NULL, 0, &iSize, NULL)) + { + // This call normally fails with ERROR_INSUFFICIENT_BUFFER. + int iError = GetLastError(); + if(iError != ERROR_INSUFFICIENT_BUFFER) + { + // Helpers::FormatWindowsError(error); + continue; + } + } + + PSP_DEVICE_INTERFACE_DETAIL_DATA DeviceInterfaceDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA) alloca(iSize); + DeviceInterfaceDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); + + SP_DEVINFO_DATA DeviceInfoData; + ZeroMemory(&DeviceInfoData, sizeof(SP_DEVINFO_DATA)); + DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); + if(!SetupDiGetDeviceInterfaceDetail(DeviceInfoSet, &DeviceInterfaceData, DeviceInterfaceDetailData, iSize, NULL, &DeviceInfoData)) + continue; + + paths.insert(DeviceInterfaceDetailData->DevicePath); + } + + SetupDiDestroyDeviceInfoList(DeviceInfoSet); + + return paths; +} + +static shared_ptr OpenUSBDevice(LPCTSTR DevicePath, wstring &error) +{ + HANDLE OpenDevice = CreateFile( + 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) + return nullptr; + + auto result = make_shared(OpenDevice); + + // Get the HID attributes to check the IDs. + HIDD_ATTRIBUTES HidAttributes; + HidAttributes.Size = sizeof(HidAttributes); + if(!HidD_GetAttributes(result->value(), &HidAttributes)) + { + error = L"HidD_GetAttributes failed"; + return nullptr; + } + + if(HidAttributes.VendorID != 0x2341 || HidAttributes.ProductID != 0x8037) + 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)) + return nullptr; + + if(wstring(ProductName) != L"StepManiaX") + return nullptr; + + return result; +} + +vector> SMX::SMXDeviceSearch::GetDevices(wstring &error) +{ + set 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 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> aDevices; + for(auto it: m_Devices) + aDevices.push_back(it.second); + + return aDevices; +} + +void SMX::SMXDeviceSearch::DeviceWasClosed(shared_ptr pDevice) +{ + map> aDevices; + for(auto it: m_Devices) + { + if(it.second == pDevice) + { + m_setLastDevicePaths.erase(it.first); + } + else + { + aDevices[it.first] = it.second; + } + } + m_Devices = aDevices; +} diff --git a/sdk/Windows/SMXDeviceSearch.h b/sdk/Windows/SMXDeviceSearch.h new file mode 100644 index 0000000..3b72bdd --- /dev/null +++ b/sdk/Windows/SMXDeviceSearch.h @@ -0,0 +1,33 @@ +#ifndef SMXDeviceSearch_h +#define SMXDeviceSearch_h + +#include +#include +#include +#include +#include +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> 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 pDevice); + +private: + set m_setLastDevicePaths; + map> m_Devices; +}; +} + +#endif diff --git a/sdk/Windows/SMXDeviceSearchThreaded.cpp b/sdk/Windows/SMXDeviceSearchThreaded.cpp new file mode 100644 index 0000000..97a08f2 --- /dev/null +++ b/sdk/Windows/SMXDeviceSearchThreaded.cpp @@ -0,0 +1,97 @@ +#include "SMXDeviceSearchThreaded.h" +#include "SMXDeviceSearch.h" +#include "SMXDeviceConnection.h" + +#include +#include +using namespace std; +using namespace SMX; + +SMX::SMXDeviceSearchThreaded::SMXDeviceSearchThreaded() +{ + m_hEvent = make_shared(CreateEvent(NULL, false, false, NULL)); + m_pDeviceList = make_shared(); + + // 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> 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 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> SMX::SMXDeviceSearchThreaded::GetDevices() +{ + // Lock to make a copy of the device list. + m_Lock.Lock(); + vector> apResult = m_apDevices; + m_Lock.Unlock(); + return apResult; +} diff --git a/sdk/Windows/SMXDeviceSearchThreaded.h b/sdk/Windows/SMXDeviceSearchThreaded.h new file mode 100644 index 0000000..de4031b --- /dev/null +++ b/sdk/Windows/SMXDeviceSearchThreaded.h @@ -0,0 +1,46 @@ +#ifndef SMXDeviceSearchThreaded_h +#define SMXDeviceSearchThreaded_h + +#include "Helpers.h" +#include +#include +#include +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> GetDevices(); + void DeviceWasClosed(shared_ptr 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 m_pDeviceList; + shared_ptr m_hEvent; + vector> m_apDevices; + vector> m_apClosedDevices; + bool m_bShutdown = false; + HANDLE m_hThread = INVALID_HANDLE_VALUE; +}; +} + +#endif diff --git a/sdk/Windows/SMXHelperThread.cpp b/sdk/Windows/SMXHelperThread.cpp new file mode 100644 index 0000000..bf3bd85 --- /dev/null +++ b/sdk/Windows/SMXHelperThread.cpp @@ -0,0 +1,76 @@ +#include "SMXHelperThread.h" + +#include +using namespace SMX; + +SMX::SMXHelperThread::SMXHelperThread(const string &sThreadName) +{ + m_hEvent = make_shared(CreateEvent(NULL, false, false, NULL)); + + // Start the thread. + m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &m_iThreadId); + SMX::SetThreadName(m_iThreadId, sThreadName); +} + +SMX::SMXHelperThread::~SMXHelperThread() +{ +} + +void SMX::SMXHelperThread::SetHighPriority(bool bHighPriority) +{ + SetThreadPriority( m_hThread, THREAD_PRIORITY_HIGHEST ); +} + +DWORD WINAPI SMX::SMXHelperThread::ThreadMainStart(void *self_) +{ + SMXHelperThread *self = (SMXHelperThread *) self_; + self->ThreadMain(); + return 0; +} + +void SMX::SMXHelperThread::ThreadMain() +{ + m_Lock.Lock(); + while(true) + { + vector> 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(); + + WaitForSingleObjectEx(m_hEvent->value(), 250, true); + m_Lock.Lock(); + } + m_Lock.Unlock(); +} + +void SMX::SMXHelperThread::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; +} + +void SMX::SMXHelperThread::RunInThread(function 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); + SetEvent(m_hEvent->value()); + m_Lock.Unlock(); +} diff --git a/sdk/Windows/SMXHelperThread.h b/sdk/Windows/SMXHelperThread.h new file mode 100644 index 0000000..ceeffa1 --- /dev/null +++ b/sdk/Windows/SMXHelperThread.h @@ -0,0 +1,46 @@ +#ifndef SMXHelperThread_h +#define SMXHelperThread_h + +#include "Helpers.h" + +#include +#include +#include +using namespace std; + +namespace SMX +{ +class SMXHelperThread +{ +public: + SMXHelperThread(const string &sThreadName); + ~SMXHelperThread(); + + // Raise the priority of the helper thread. + void SetHighPriority(bool bHighPriority); + + // Shut down the thread. Any calls queued by RunInThread will complete before + // this returns. + void Shutdown(); + + // Call func asynchronously from the helper thread. + void RunInThread(function func); + + // Return the Win32 thread ID, or INVALID_HANDLE_VALUE if the thread has been + // shut down. + DWORD GetThreadId() const { return m_iThreadId; } + +private: + static DWORD WINAPI ThreadMainStart(void *self_); + void ThreadMain(); + + DWORD m_iThreadId = 0; + SMX::Mutex m_Lock; + shared_ptr m_hEvent; + bool m_bShutdown = false; + HANDLE m_hThread = INVALID_HANDLE_VALUE; + vector> m_FunctionsToCall; +}; +} + +#endif diff --git a/sdk/Windows/SMXManager.cpp b/sdk/Windows/SMXManager.cpp new file mode 100644 index 0000000..10676ed --- /dev/null +++ b/sdk/Windows/SMXManager.cpp @@ -0,0 +1,452 @@ +#include "SMXManager.h" +#include "SMXDevice.h" +#include "SMXDeviceConnection.h" +#include "SMXDeviceSearchThreaded.h" +#include "Helpers.h" + +#include +#include +using namespace std; +using namespace SMX; + +namespace { + Mutex g_Lock; +} + +SMX::SMXManager::SMXManager(function 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(CreateEvent(NULL, false, false, NULL)); + m_pSMXDeviceSearchThreaded = make_shared(); + + // 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 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 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.GetThreadId() == GetCurrentThreadId()) + 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(); + + // See if there are any new devices. + AttemptConnections(); + + // Update all connected devices. + for(shared_ptr 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 aHandles = { m_hEvent->value() }; + for(shared_ptr pDevice: m_pDevices) + { + shared_ptr 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_aPendingCommands.empty()) + { + double fSendIn = m_aPendingCommands[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 &sLightData) +{ + g_Lock.AssertNotLockedByCurrentThread(); + LockMutex L(g_Lock); + + // Sanity check the lights data. It should have 18*16*3 bytes of data: RGB for each of 4x4 + // LEDs on 18 panels. + if(sLightData.size() != 2*3*3*16*3) + { + Log(ssprintf("SetLights: Lights data should be %i bytes, received %i", 2*3*3*16*3, sLightData.size())); + return; + } + + // Split the lights data into P1 and P2. + string sPanelLights[2]; + sPanelLights[0] = sLightData.substr(0, 9*16*3); + sPanelLights[1] = sLightData.substr(9*16*3); + + // 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 + // + // Set sLightsCommand[iPad][0] to include 0123 4567, and [1] to 89AB CDEF. + string sLightCommands[2][2]; // sLightCommands[command][pad] + + auto addByte = [&sLightCommands](int iPanel, int iByte, uint8_t iColor) { + // If iPanel is 0-8, this is for pad 0. For 9-17, it's for pad 1. + // If the color byte within the panel is in the top half, it's the first + // command, otherwise it's the second command. + int iPad = iPanel < 9? 0:1; + int iCommandIndex = iByte < 4*2*3? 0:1; + sLightCommands[iCommandIndex][iPad].append(1, iColor); + }; + + // Read the linearly arranged color data we've been given and split it into top and + // bottom commands for each pad. + int iNextInputByte = 0; + for(int iPanel = 0; iPanel < 18; ++iPanel) + { + for(int iByte = 0; iByte < 4*4*3; ++iByte) + { + uint8_t iColor = sLightData[iNextInputByte++]; + addByte(iPanel, iByte, iColor); + } + } + + for(int iPad = 0; iPad < 2; ++iPad) + { + for(int iCommand = 0; iCommand < 2; ++iCommand) + { + string &sCommand = sLightCommands[iCommand][iPad]; + + // Apply color scaling. Values over about 170 don't make the LEDs any brighter, so this + // gives better contrast and draws less power. + for(char &c: sCommand) + c = char(uint8_t(c) * 0.6666f); + + // Add the command byte. + sCommand.insert(sCommand.begin(), 1, iCommand == 0? '2':'3'); + sCommand.push_back('\n'); + } + } + + // Each update adds two entries to m_aPendingCommands, one for the top half and one + // for the lower half. + // + // If there's one entry in the list, we've already sent the first half of a previous update, + // and the remaining entry is the second half. We'll leave it in place so we always finish + // an update once we start it, and add this update after it. + // + // If there are two entries in the list, then it's an existing update that we haven't sent yet. + // If there are three entries, we added an update after a partial update. In either case, the + // last two commands in the list are a complete lights update, and we'll just update it in-place. + // + // This way, we'll 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. + if(m_aPendingCommands.size() <= 1) + { + static const double fDelayBetweenLightsCommands = 1/60.0; + + double fNow = GetMonotonicTime(); + double fSendCommandAt = max(fNow, m_fDelayLightCommandsUntil); + float fFirstCommandTime = fSendCommandAt; + float fSecondCommandTime = fFirstCommandTime + fDelayBetweenLightsCommands; + + // Update m_fDelayLightCommandsUntil, so we know when the next + m_fDelayLightCommandsUntil = fSecondCommandTime + fDelayBetweenLightsCommands; + + // Add two commands to the list, scheduled at fFirstCommandTime and fSecondCommandTime. + m_aPendingCommands.push_back(PendingCommand(fFirstCommandTime)); + m_aPendingCommands.push_back(PendingCommand(fSecondCommandTime)); + // Log(ssprintf("Scheduled commands at %f and %f", fFirstCommandTime, fSecondCommandTime)); + + // Wake up the I/O thread if it's blocking on WaitForMultipleObjectsEx. + SetEvent(m_hEvent->value()); + } + + // Set the pad commands. + PendingCommand *pPendingCommands[2]; + pPendingCommands[0] = &m_aPendingCommands[m_aPendingCommands.size()-2]; + pPendingCommands[1] = &m_aPendingCommands[m_aPendingCommands.size()-1]; + + pPendingCommands[0]->sPadCommand[0] = sLightCommands[0][0]; + pPendingCommands[0]->sPadCommand[1] = sLightCommands[0][1]; + pPendingCommands[1]->sPadCommand[0] = sLightCommands[1][0]; + pPendingCommands[1]->sPadCommand[1] = sLightCommands[1][1]; +} + +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_aPendingCommands.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_aPendingCommands. +void SMX::SMXManager::SendLightUpdates() +{ + g_Lock.AssertLockedByCurrentThread(); + if(m_aPendingCommands.empty()) + return; + + const PendingCommand &command = m_aPendingCommands[0]; + + // See if it's time to send the next command. We only need to look at the first + // command, since these are always sorted. + if(command.fTimeToSend > GetMonotonicTime()) + return; + + // Send the lights command for each pad. If either pad isn't connected, this won't do + // anything. + for(int iPad = 0; iPad < 2; ++iPad) + m_pDevices[iPad]->SendCommandLocked(command.sPadCommand[iPad]); + + // Remove the command we've sent. + m_aPendingCommands.erase(m_aPendingCommands.begin(), m_aPendingCommands.begin()+1); +} + +// See if there are any new devices to connect to. +void SMX::SMXManager::AttemptConnections() +{ + g_Lock.AssertLockedByCurrentThread(); + + vector> apDevices = m_pSMXDeviceSearchThreaded->GetDevices(); + + // Check each device that we've found. This will include ones we already have open. + for(shared_ptr 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 pDevice: m_pDevices) + { + if(pDevice->GetDeviceHandle() == pHandle) + bAlreadyOpen = true; + } + if(bAlreadyOpen) + continue; + + // Find an open device slot. + shared_ptr pDeviceToOpen; + for(shared_ptr 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())); + } +} + + + diff --git a/sdk/Windows/SMXManager.h b/sdk/Windows/SMXManager.h new file mode 100644 index 0000000..cf079f3 --- /dev/null +++ b/sdk/Windows/SMXManager.h @@ -0,0 +1,76 @@ +#ifndef SMXManager_h +#define SMXManager_h + +#include +#include +#include +#include +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: + // 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 pCallback); + ~SMXManager(); + + void Shutdown(); + shared_ptr GetDevice(int pad); + void SetLights(const string &sLightData); + void ReenableAutoLights(); + +private: + static DWORD WINAPI ThreadMainStart(void *self_); + void ThreadMain(); + void AttemptConnections(); + void CorrectDeviceOrder(); + void SendLightUpdates(); + + HANDLE m_hThread = INVALID_HANDLE_VALUE; + shared_ptr m_hEvent; + shared_ptr m_pSMXDeviceSearchThreaded; + bool m_bShutdown = false; + vector> 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 m_aPendingCommands; + double m_fDelayLightCommandsUntil = 0; +}; +} + +#endif diff --git a/smx-config/.gitignore b/smx-config/.gitignore new file mode 100644 index 0000000..ba077a4 --- /dev/null +++ b/smx-config/.gitignore @@ -0,0 +1 @@ +bin diff --git a/smx-config/App.config b/smx-config/App.config new file mode 100644 index 0000000..737ed23 --- /dev/null +++ b/smx-config/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/smx-config/App.xaml b/smx-config/App.xaml new file mode 100644 index 0000000..aaa2d48 --- /dev/null +++ b/smx-config/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/smx-config/App.xaml.cs b/smx-config/App.xaml.cs new file mode 100644 index 0000000..105c0ee --- /dev/null +++ b/smx-config/App.xaml.cs @@ -0,0 +1,32 @@ +using System; +using System.Windows; +using System.Runtime.InteropServices; + +namespace smx_config +{ + public partial class App: Application + { + [DllImport("Kernel32")] + private static extern void AllocConsole(); + [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] + private static extern void SMX_Internal_OpenConsole(); + + App() + { + if(Helpers.GetDebug()) + SMX_Internal_OpenConsole(); + CurrentSMXDevice.singleton = new CurrentSMXDevice(); + } + + protected override void OnExit(ExitEventArgs e) + { + base.OnExit(e); + + // Shut down cleanly, to make sure we don't run any threaded callbacks during shutdown. + Console.WriteLine("Application exiting"); + CurrentSMXDevice.singleton.Shutdown(); + CurrentSMXDevice.singleton = null; + } + + } +} diff --git a/smx-config/ConfigPresets.cs b/smx-config/ConfigPresets.cs new file mode 100644 index 0000000..fa2227c --- /dev/null +++ b/smx-config/ConfigPresets.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace smx_config +{ + class ConfigPresets + { + static private string[] Presets = { "low", "normal", "high" }; + + static public string GetPreset(SMX.SMXConfig config) + { + // If the config we're comparing against is a V1 config, that means the per-panel thresholds + // weren't set and those fields are ignored (or not supported by) by the firmware. Sync those + // thresholds to the unified thresholds before comparing. That way, we'll correctly match up + // each preset regardless of what happens to be in the unused per-panel threshold fields. + // If we don't do this, we won't recognize that the default preset is active because unused + // fields won't match up. + if(config.configVersion == 0xFF || config.configVersion < 2) + SyncUnifiedThresholds(ref config); + + foreach(string Preset in Presets) + { + SMX.SMXConfig PresetConfig = config; + SetPreset(Preset, ref PresetConfig); + if(SamePreset(config, PresetConfig)) + return Preset; + } + return ""; + } + + // Return true if the config matches, only comparing values that we set in presets. + static private bool SamePreset(SMX.SMXConfig config1, SMX.SMXConfig config2) + { + // These aren't arrays for compatibility reasons. + if( config1.panelThreshold0High != config2.panelThreshold0High || + config1.panelThreshold1High != config2.panelThreshold1High || + config1.panelThreshold2High != config2.panelThreshold2High || + config1.panelThreshold3High != config2.panelThreshold3High || + config1.panelThreshold4High != config2.panelThreshold4High || + config1.panelThreshold5High != config2.panelThreshold5High || + config1.panelThreshold6High != config2.panelThreshold6High || + config1.panelThreshold7High != config2.panelThreshold7High || + config1.panelThreshold8High != config2.panelThreshold8High) + return false; + if( config1.panelThreshold0Low != config2.panelThreshold0Low || + config1.panelThreshold1Low != config2.panelThreshold1Low || + config1.panelThreshold2Low != config2.panelThreshold2Low || + config1.panelThreshold3Low != config2.panelThreshold3Low || + config1.panelThreshold4Low != config2.panelThreshold4Low || + config1.panelThreshold5Low != config2.panelThreshold5Low || + config1.panelThreshold6Low != config2.panelThreshold6Low || + config1.panelThreshold7Low != config2.panelThreshold7Low || + config1.panelThreshold8Low != config2.panelThreshold8Low) + return false; + return true; + } + + static public void SetPreset(string name, ref SMX.SMXConfig config) + { + switch(name) + { + case "low": SetLowPreset(ref config); return; + case "normal": SetNormalPreset(ref config); return; + case "high": SetHighPreset(ref config); return; + } + } + + static private void SetHighPreset(ref SMX.SMXConfig config) + { + config.panelThreshold7Low = // cardinal + config.panelThreshold1Low = // up + config.panelThreshold2Low = 20; // corner + config.panelThreshold7High = // cardinal + config.panelThreshold1High = // up + config.panelThreshold2High = 25; // corner + + config.panelThreshold4Low = 20; // center + config.panelThreshold4High = 30; + SyncUnifiedThresholds(ref config); + } + + static private void SetNormalPreset(ref SMX.SMXConfig config) + { + config.panelThreshold7Low = // cardinal + config.panelThreshold1Low = // up + config.panelThreshold2Low = 33; // corner + config.panelThreshold7High = // cardinal + config.panelThreshold1High = // up + config.panelThreshold2High = 42; // corner + + config.panelThreshold4Low = 35; // center + config.panelThreshold4High = 60; + SyncUnifiedThresholds(ref config); + } + + static private void SetLowPreset(ref SMX.SMXConfig config) + { + config.panelThreshold7Low = // cardinal + config.panelThreshold1Low = // up + config.panelThreshold2Low = 70; // corner + config.panelThreshold7High = // cardinal + config.panelThreshold1High = // up + config.panelThreshold2High = 80; // corner + + config.panelThreshold4Low = 100; // center + config.panelThreshold4High = 120; + SyncUnifiedThresholds(ref config); + } + + // The simplified configuration scheme sets thresholds for up, center, cardinal directions + // and corners. Rev1 firmware uses those only. Copy cardinal directions (down) to the + // other cardinal directions (except for up, which already had its own setting) and corners + // to the other corners. + static public void SyncUnifiedThresholds(ref SMX.SMXConfig config) + { + // left = right = down (cardinal) + config.panelThreshold3Low = config.panelThreshold5Low = config.panelThreshold7Low; + config.panelThreshold3High = config.panelThreshold5High = config.panelThreshold7High; + + // UL = DL = DR = UR (corners) + config.panelThreshold0Low = config.panelThreshold6Low = config.panelThreshold8Low = config.panelThreshold2Low; + config.panelThreshold0High = config.panelThreshold6High = config.panelThreshold8High = config.panelThreshold2High; + } + + // Return true if the panel thresholds are already synced, so SyncUnifiedThresholds would + // have no effect. + static public bool AreUnifiedThresholdsSynced(SMX.SMXConfig config) + { + SMX.SMXConfig config2 = config; + SyncUnifiedThresholds(ref config2); + return SamePreset(config, config2); + } + } +} diff --git a/smx-config/CurrentSMXDevice.cs b/smx-config/CurrentSMXDevice.cs new file mode 100644 index 0000000..744e8d7 --- /dev/null +++ b/smx-config/CurrentSMXDevice.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; +using System.Windows.Controls; +using System.ComponentModel; + +namespace smx_config +{ + // The state and configuration of a pad. + public struct LoadFromConfigDelegateArgsPerController + { + public SMX.SMXInfo info; + public SMX.SMXConfig config; + public SMX.SMXSensorTestModeData test_data; + + // The panels that are activated. Note that to receive notifications from OnConfigChange + // when inputs change state, set RefreshOnInputChange to true. Otherwise, this field will + // be filled in but notifications won't be sent due to only inputs changing. + public bool[] inputs; + } + + public struct LoadFromConfigDelegateArgs + { + // This indicates which fields changed since the last call. + public bool ConfigurationChanged, InputChanged, TestDataChanged; + + // Data for each of two controllers: + public LoadFromConfigDelegateArgsPerController[] controller; + + // For convenience, this is the index in controller of the first connected controller. + // If no controllers are connected, this is 0. + public int FirstController; + + // The control that changed the configuration (passed to FireConfigurationChanged). + public object source; + }; + + // This class tracks the device we're currently configuring, and runs a callback when + // it changes. + class CurrentSMXDevice + { + public static CurrentSMXDevice singleton; + + // This is fired when FireConfigurationChanged is called, and when the current device + // changes. + public delegate void ConfigurationChangedDelegate(LoadFromConfigDelegateArgs args); + public event ConfigurationChangedDelegate ConfigurationChanged; + + private bool[] WasConnected = new bool[2] { false, false }; + private bool[][] LastInputs = new bool[2][]; + private SMX.SMXSensorTestModeData[] LastTestData = new SMX.SMXSensorTestModeData[2]; + private Dispatcher MainDispatcher; + + public CurrentSMXDevice() + { + // Grab the main thread's dispatcher, so we can invoke into it. + MainDispatcher = Dispatcher.CurrentDispatcher; + + // Set our update callback. This will be called when something happens: connection or disconnection, + // inputs changed, configuration updated, test data updated, etc. It doesn't specify what's changed, + // we simply check the whole state. + SMX.SMX.Start(delegate(int PadNumber, SMX.SMX.SMXUpdateCallbackReason reason) { + // Console.WriteLine("... " + reason); + // This is called from a thread, with SMX's internal mutex locked. We must not call into SMX + // or do anything with the UI from here. Just queue an update back into the UI thread. + MainDispatcher.InvokeAsync(delegate() { + switch(reason) + { + case SMX.SMX.SMXUpdateCallbackReason.Updated: + CheckForChanges(); + break; + case SMX.SMX.SMXUpdateCallbackReason.FactoryResetCommandComplete: + Console.WriteLine("SMX_FactoryResetCommandComplete"); + FireConfigurationChanged(null); + break; + } + }); + }); + } + + public void Shutdown() + { + SMX.SMX.SetUpdateCallback(null); + SMX.SMX.Stop(); + } + + private void CheckForChanges() + { + LoadFromConfigDelegateArgs args = GetState(); + + // Mark which parts have changed. + // + // For configuration, we only check for connection state changes. Actual configuration + // changes are fired by controls via FireConfigurationChanged. + for(int pad = 0; pad < 2; ++pad) + { + LoadFromConfigDelegateArgsPerController controller = args.controller[pad]; + if(WasConnected[pad] != controller.info.connected) + { + args.ConfigurationChanged = true; + WasConnected[pad] = controller.info.connected; + } + + if(LastInputs[pad] == null || !Enumerable.SequenceEqual(controller.inputs, LastInputs[pad])) + { + args.InputChanged = true; + LastInputs[pad] = controller.inputs; + } + + if(!controller.test_data.Equals(LastTestData[pad])) + { + args.TestDataChanged = true; + LastTestData[pad] = controller.test_data; + } + } + + // Only fire the delegate if something has actually changed. + if(args.ConfigurationChanged || args.InputChanged || args.TestDataChanged) + ConfigurationChanged?.Invoke(args); + } + + public void FireConfigurationChanged(object source) + { + LoadFromConfigDelegateArgs args = GetState(); + args.ConfigurationChanged = true; + args.source = source; + ConfigurationChanged?.Invoke(args); + } + + public LoadFromConfigDelegateArgs GetState() + { + LoadFromConfigDelegateArgs args = new LoadFromConfigDelegateArgs(); + args.FirstController = -1; + args.controller = new LoadFromConfigDelegateArgsPerController[2]; + + for(int pad = 0; pad < 2; ++pad) + { + LoadFromConfigDelegateArgsPerController controller; + controller.test_data = new SMX.SMXSensorTestModeData(); + + // Expand the inputs mask to an array. + UInt16 Inputs = SMX.SMX.GetInputState(pad); + controller.inputs = new bool[9]; + for(int i = 0; i < 9; ++i) + controller.inputs[i] = (Inputs & (1 << i)) != 0; + SMX.SMX.GetInfo(pad, out controller.info); + SMX.SMX.GetConfig(pad, out controller.config); + SMX.SMX.GetTestData(pad, out controller.test_data); + args.controller[pad] = controller; + + // If this is the first connected controller, set FirstController. + if(controller.info.connected && args.FirstController == -1) + args.FirstController = pad; + } + + if(args.FirstController == -1) + args.FirstController = 0; + + return args; + } + + } + + // Call a delegate on configuration change. Configuration changes are notified by calling + // FireConfigurationChanged. Listeners won't receive notifications for changes that they + // fired themselves. + public class OnConfigChange + { + public delegate void LoadFromConfigDelegate(LoadFromConfigDelegateArgs args); + private readonly Control Owner; + private readonly LoadFromConfigDelegate Callback; + private bool _RefreshOnInputChange = false; + + // If set to true, the callback will be invoked on input changes in addition to configuration + // changes. This can cause the callback to be run at any time, such as while the user is + // interacting with the control. + public bool RefreshOnInputChange { + get { return _RefreshOnInputChange; } + set {_RefreshOnInputChange = value; } + } + + private bool _RefreshOnTestDataChange = false; + + // Like RefreshOnInputChange, but enables callbacks when test data changes. + public bool RefreshOnTestDataChange { + get { return _RefreshOnTestDataChange; } + set { _RefreshOnTestDataChange = value; } + } + + // Owner is the Control that we're calling. This callback will be disable when the + // control is unloaded, and we won't call it if it's the same control that fired + // the change via FireConfigurationChanged. + // + // In addition, the callback is called when the control is Loaded, to load the initial + // state. + public OnConfigChange(Control owner, LoadFromConfigDelegate callback) + { + Owner = owner; + Callback = callback; + + Owner.Loaded += delegate(object sender, RoutedEventArgs e) + { + if(CurrentSMXDevice.singleton != null) + CurrentSMXDevice.singleton.ConfigurationChanged += ConfigurationChanged; + Refresh(); + }; + + Owner.Unloaded += delegate(object sender, RoutedEventArgs e) + { + if(CurrentSMXDevice.singleton != null) + CurrentSMXDevice.singleton.ConfigurationChanged -= ConfigurationChanged; + }; + } + + private void ConfigurationChanged(LoadFromConfigDelegateArgs args) + { + if(args.ConfigurationChanged || + (RefreshOnInputChange && args.InputChanged) || + (RefreshOnTestDataChange && args.TestDataChanged)) + { + Callback(args); + } + } + + private void Refresh() + { + if(CurrentSMXDevice.singleton != null) + Callback(CurrentSMXDevice.singleton.GetState()); + } + }; + + + public class OnInputChange + { + public delegate void LoadFromConfigDelegate(LoadFromConfigDelegateArgs args); + private readonly Control Owner; + private readonly LoadFromConfigDelegate Callback; + + // Owner is the Control that we're calling. This callback will be disable when the + // control is unloaded, and we won't call it if it's the same control that fired + // the change via FireConfigurationChanged. + // + // In addition, the callback is called when the control is Loaded, to load the initial + // state. + public OnInputChange(Control owner, LoadFromConfigDelegate callback) + { + Owner = owner; + Callback = callback; + + // This is available when the application is running, but will be null in the XAML designer. + if(CurrentSMXDevice.singleton == null) + return; + + Owner.Loaded += delegate(object sender, RoutedEventArgs e) + { + CurrentSMXDevice.singleton.ConfigurationChanged += ConfigurationChanged; + Refresh(); + }; + + Owner.Unloaded += delegate(object sender, RoutedEventArgs e) + { + CurrentSMXDevice.singleton.ConfigurationChanged -= ConfigurationChanged; + }; + } + + private void ConfigurationChanged(LoadFromConfigDelegateArgs args) + { + Callback(args); + } + + private void Refresh() + { + if(CurrentSMXDevice.singleton != null) + Callback(CurrentSMXDevice.singleton.GetState()); + } + }; +} diff --git a/smx-config/DiagnosticsWidgets.cs b/smx-config/DiagnosticsWidgets.cs new file mode 100644 index 0000000..8f6e6c5 --- /dev/null +++ b/smx-config/DiagnosticsWidgets.cs @@ -0,0 +1,289 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; +using System.Windows.Input; + +namespace smx_config +{ + public class DiagnosticsPanelButton: PanelSelectButton + { + // True if this panel is being pressed. + public static readonly DependencyProperty PressedProperty = DependencyProperty.Register("Pressed", + typeof(bool), typeof(DiagnosticsPanelButton), new FrameworkPropertyMetadata(false)); + + public bool Pressed { + get { return (bool) GetValue(PressedProperty); } + set { SetValue(PressedProperty, value); } + } + + // True if a warning icon should be displayed for this panel. + public static readonly DependencyProperty WarningProperty = DependencyProperty.Register("Warning", + typeof(bool), typeof(DiagnosticsPanelButton), new FrameworkPropertyMetadata(false)); + + public bool Warning { + get { return (bool) GetValue(WarningProperty); } + set { SetValue(WarningProperty, value); } + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + OnConfigChange onConfigChange; + onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { + int SelectedPad = Panel < 9? 0:1; + int PanelIndex = Panel % 9; + Pressed = args.controller[SelectedPad].inputs[PanelIndex]; + + Warning = !args.controller[SelectedPad].test_data.bHaveDataFromPanel[PanelIndex] || + args.controller[SelectedPad].test_data.AnySensorsOnPanelNotResponding(PanelIndex); + + }); + onConfigChange.RefreshOnInputChange = true; + onConfigChange.RefreshOnTestDataChange = true; + } + + protected override void OnClick() + { + base.OnClick(); + + // Select this panel. + Console.WriteLine(SelectedPanel + " -> " + Panel); + SelectedPanel = Panel; + + CurrentSMXDevice.singleton.FireConfigurationChanged(this); + } + } + + public class LevelBar: Control + { + public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", + typeof(double), typeof(LevelBar), new FrameworkPropertyMetadata(0.5, ValueChangedCallback)); + + public double Value { + get { return (double) GetValue(ValueProperty); } + set { SetValue(ValueProperty, value); } + } + + public static readonly DependencyProperty ErrorProperty = DependencyProperty.Register("Error", + typeof(bool), typeof(LevelBar), new FrameworkPropertyMetadata(false, ValueChangedCallback)); + + public bool Error { + get { return (bool) GetValue(ErrorProperty); } + set { SetValue(ErrorProperty, value); } + } + + private Rectangle Fill, Back; + + private static void ValueChangedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args) + { + LevelBar self = target as LevelBar; + self.Refresh(); + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + Fill = Template.FindName("Fill", this) as Rectangle; + Back = Template.FindName("Back", this) as Rectangle; + Refresh(); + } + + private void Refresh() + { + // If Error is true, fill the bar red. + double FillHeight = Error? 1:Value; + Fill.Height = Math.Round(Math.Max(FillHeight, 0) * (Back.Height - 2)); + + if(Error) + { + Fill.Fill = new SolidColorBrush(Color.FromRgb(255,0,0)); + } + else + { + // Scale from green (#FF0000) to yellow (#FFFF00) as we go from 0 to .4. + double ColorValue = Value / 0.4; + Byte Yellow = (Byte) (Math.Max(0, Math.Min(255, ColorValue * 255)) ); + Fill.Fill = new SolidColorBrush(Color.FromRgb(255,Yellow,0)); + } + } + } + + public class DiagnosticsControl: Control + { + // Which panel is currently selected: + public static readonly DependencyProperty SelectedPanelProperty = DependencyProperty.Register("SelectedPanel", + typeof(int), typeof(DiagnosticsControl), new FrameworkPropertyMetadata(0)); + + public int SelectedPanel { + get { return (int) this.GetValue(SelectedPanelProperty); } + set { this.SetValue(SelectedPanelProperty, value); } + } + + private LevelBar[] LevelBars; + private Label[] LevelBarText; + private ComboBox DiagnosticMode; + private FrameImage CurrentDIP; + private FrameImage ExpectedDIP; + private FrameworkElement NoResponseFromPanel; + private FrameworkElement NoResponseFromSensors; + private FrameworkElement P1Diagnostics, P2Diagnostics; + + public delegate void ShowAllLightsEvent(bool on); + public event ShowAllLightsEvent SetShowAllLights; + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + LevelBars = new LevelBar[4]; + LevelBars[0] = Template.FindName("SensorBar1", this) as LevelBar; + LevelBars[1] = Template.FindName("SensorBar2", this) as LevelBar; + LevelBars[2] = Template.FindName("SensorBar3", this) as LevelBar; + LevelBars[3] = Template.FindName("SensorBar4", this) as LevelBar; + + LevelBarText = new Label[4]; + LevelBarText[0] = Template.FindName("SensorBarLevel1", this) as Label; + LevelBarText[1] = Template.FindName("SensorBarLevel2", this) as Label; + LevelBarText[2] = Template.FindName("SensorBarLevel3", this) as Label; + LevelBarText[3] = Template.FindName("SensorBarLevel4", this) as Label; + + DiagnosticMode = Template.FindName("DiagnosticMode", this) as ComboBox; + CurrentDIP = Template.FindName("CurrentDIP", this) as FrameImage; + ExpectedDIP = Template.FindName("ExpectedDIP", this) as FrameImage; + NoResponseFromPanel = Template.FindName("NoResponseFromPanel", this) as FrameworkElement; + NoResponseFromSensors = Template.FindName("NoResponseFromSensors", this) as FrameworkElement; + P1Diagnostics = Template.FindName("P1Diagnostics", this) as FrameworkElement; + P2Diagnostics = Template.FindName("P2Diagnostics", this) as FrameworkElement; + + // Only show the mode dropdown in debug mode. In regular use, just show calibrated values. + DiagnosticMode.Visibility = Helpers.GetDebug()? Visibility.Visible:Visibility.Collapsed; + + Button Recalibrate = Template.FindName("Recalibrate", this) as Button; + Recalibrate.Click += delegate(object sender, RoutedEventArgs e) + { + for(int pad = 0; pad < 2; ++pad) + SMX.SMX.ForceRecalibration(pad); + }; + + Button LightAll = Template.FindName("LightAll", this) as Button; + LightAll.PreviewMouseDown += delegate(object sender, MouseButtonEventArgs e) + { + SetShowAllLights?.Invoke(true); + }; + LightAll.PreviewMouseUp += delegate(object sender, MouseButtonEventArgs e) + { + SetShowAllLights?.Invoke(false); + }; + + // Update the test mode when the dropdown is changed. + DiagnosticMode.AddHandler(ComboBox.SelectionChangedEvent, new RoutedEventHandler(delegate(object sender, RoutedEventArgs e) + { + for(int pad = 0; pad < 2; ++pad) + SMX.SMX.SetTestMode(pad, GetTestMode()); + })); + + OnConfigChange onConfigChange; + onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { + Refresh(args); + }); + onConfigChange.RefreshOnTestDataChange = true; + + Loaded += delegate(object sender, RoutedEventArgs e) + { + for(int pad = 0; pad < 2; ++pad) + SMX.SMX.SetTestMode(pad, GetTestMode()); + }; + + Unloaded += delegate(object sender, RoutedEventArgs e) + { + for(int pad = 0; pad < 2; ++pad) + SMX.SMX.SetTestMode(pad, SMX.SMX.SensorTestMode.Off); + }; + } + + private SMX.SMX.SensorTestMode GetTestMode() + { + switch(DiagnosticMode.SelectedIndex) + { + case 0: return SMX.SMX.SensorTestMode.CalibratedValues; + case 1: return SMX.SMX.SensorTestMode.UncalibratedValues; + case 2: return SMX.SMX.SensorTestMode.Noise; + case 3: + default: return SMX.SMX.SensorTestMode.Tare; + } + } + + private void Refresh(LoadFromConfigDelegateArgs args) + { + P1Diagnostics.Visibility = args.controller[0].info.connected? Visibility.Visible:Visibility.Collapsed; + P2Diagnostics.Visibility = args.controller[1].info.connected? Visibility.Visible:Visibility.Collapsed; + + // Update the displayed DIP switch icons. + int SelectedPad = SelectedPanel < 9? 0:1; + int PanelIndex = SelectedPanel % 9; + int dip = args.controller[SelectedPad].test_data.iDIPSwitchPerPanel[PanelIndex]; + CurrentDIP.Frame = dip; + ExpectedDIP.Frame = PanelIndex; + + // Show or hide the sensor error text. + bool AnySensorsNotResponding = false; + if(args.controller[SelectedPad].test_data.bHaveDataFromPanel[PanelIndex]) + AnySensorsNotResponding = args.controller[SelectedPad].test_data.AnySensorsOnPanelNotResponding(PanelIndex); + NoResponseFromSensors.Visibility = AnySensorsNotResponding? Visibility.Visible:Visibility.Collapsed; + + // Update the level bar from the test mode data for the selected panel. + for(int sensor = 0; sensor < 4; ++sensor) + { + Int16 value = args.controller[SelectedPad].test_data.sensorLevel[PanelIndex*4+sensor]; + + if(GetTestMode() == SMX.SMX.SensorTestMode.Noise) + { + // In noise mode, we receive standard deviation values squared. Display the square + // root, since the panels don't do this for us. This makes the numbers different + // than the configured value (square it to convert back), but without this we display + // a bunch of 4 and 5-digit numbers that are too hard to read. + value = (Int16) Math.Sqrt(value); + } + + LevelBarText[sensor].Visibility = Visibility.Visible; + if(!args.controller[SelectedPad].test_data.bHaveDataFromPanel[PanelIndex]) + { + LevelBars[sensor].Value = 0; + LevelBarText[sensor].Visibility = Visibility.Hidden; + LevelBarText[sensor].Content = "-"; + LevelBars[sensor].Error = false; + } + else if(args.controller[SelectedPad].test_data.bBadSensorInput[PanelIndex*4+sensor]) + { + LevelBars[sensor].Value = 0; + LevelBarText[sensor].Content = "!"; + LevelBars[sensor].Error = true; + } + else + { + // Very slightly negative values happen due to noise. They don't indicate a + // problem, but they're confusing in the UI, so clamp them away. + if(value < 0 && value >= -10) + value = 0; + + LevelBars[sensor].Value = value / 500.0; + LevelBarText[sensor].Content = value; + LevelBars[sensor].Error = false; + } + } + + if(!args.controller[SelectedPad].test_data.bHaveDataFromPanel[PanelIndex]) + { + NoResponseFromPanel.Visibility = Visibility.Visible; + NoResponseFromSensors.Visibility = Visibility.Collapsed; + return; + } + + NoResponseFromPanel.Visibility = Visibility.Collapsed; + } + } +} diff --git a/smx-config/DoubleSlider.cs b/smx-config/DoubleSlider.cs new file mode 100644 index 0000000..1f2a0a7 --- /dev/null +++ b/smx-config/DoubleSlider.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; + +namespace smx_config +{ + // A slider with two handles, and a handle connecting them. Dragging the handle drags both + // of the sliders. + class DoubleSlider: Control + { + public delegate void ValueChangedDelegate(DoubleSlider slider); + public event ValueChangedDelegate ValueChanged; + + // The minimum value for either knob. + public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", + typeof(double), typeof(DoubleSlider), new FrameworkPropertyMetadata(0.0)); + + public double Minimum { + get { return (double) GetValue(MinimumProperty); } + set { SetValue(MinimumProperty, value); } + } + + // The maximum value for either knob. + public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", + typeof(double), typeof(DoubleSlider), new FrameworkPropertyMetadata(20.0)); + + public double Maximum { + get { return (double) GetValue(MaximumProperty); } + set { SetValue(MaximumProperty, value); } + } + + // The minimum distance between the two values. + public static readonly DependencyProperty MinimumDistanceProperty = DependencyProperty.Register("MinimumDistance", + typeof(double), typeof(DoubleSlider), new FrameworkPropertyMetadata(0.0)); + + public double MinimumDistance { + get { return (double) GetValue(MinimumDistanceProperty); } + set { SetValue(MinimumDistanceProperty, value); } + } + + // Clamp value between minimum and maximum. + private double CoerceValueToLimits(double value) + { + return Math.Min(Math.Max(value, Minimum), Maximum); + } + + // Note that we only clamp LowerValue and UpperValue to the min/max values. We don't + // clamp them to each other or to MinimumDistance here, since that complicates setting + // properties a lot. We only clamp to those when the user manipulates the control, not + // when we set values directly. + private static object LowerValueCoerceValueCallback(DependencyObject target, object valueObject) + { + DoubleSlider slider = target as DoubleSlider; + double value = (double)valueObject; + value = slider.CoerceValueToLimits(value); + return value; + } + + private static object UpperValueCoerceValueCallback(DependencyObject target, object valueObject) + { + DoubleSlider slider = target as DoubleSlider; + double value = (double)valueObject; + value = slider.CoerceValueToLimits(value); + return value; + } + + private static void SliderValueChangedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args) + { + DoubleSlider slider = target as DoubleSlider; + if(slider.ValueChanged != null) + slider.ValueChanged.Invoke(slider); + } + + public static readonly DependencyProperty LowerValueProperty = DependencyProperty.Register("LowerValue", + typeof(double), typeof(DoubleSlider), + new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsArrange, SliderValueChangedCallback, LowerValueCoerceValueCallback)); + + public double LowerValue { + get { return (double) GetValue(LowerValueProperty); } + set { SetValue(LowerValueProperty, value); } + } + + public static readonly DependencyProperty UpperValueProperty = DependencyProperty.Register("UpperValue", + typeof(double), typeof(DoubleSlider), + new FrameworkPropertyMetadata(15.0, FrameworkPropertyMetadataOptions.AffectsArrange, SliderValueChangedCallback, UpperValueCoerceValueCallback)); + + public double UpperValue { + get { return (double) GetValue(UpperValueProperty); } + set { SetValue(UpperValueProperty, value); } + } + + private Thumb Middle; + + Thumb UpperThumb; + Thumb LowerThumb; + + private RepeatButton DecreaseButton; + private RepeatButton IncreaseButton; + + protected override Size ArrangeOverride(Size arrangeSize) + { + arrangeSize = base.ArrangeOverride(arrangeSize); + + // Figure out the X position of the upper and lower thumbs. Note that we have to provide + // our width to GetValueToSize, since ActualWidth isn't available yet. + double valueToSize = GetValueToSize(arrangeSize.Width); + double UpperPointX = (UpperValue-Minimum) * valueToSize; + double LowerPointX = (LowerValue-Minimum) * valueToSize; + + // Move the upper and lower handles out by this much, and extend this middle. This + // makes the middle handle bigger. + double OffsetOutwards = 5; + Middle.Arrange(new Rect(LowerPointX-OffsetOutwards-1, 0, + Math.Max(1, UpperPointX-LowerPointX+OffsetOutwards*2+2), arrangeSize.Height)); + + // Right-align the lower thumb and left-align the upper thumb. + LowerThumb.Arrange(new Rect(LowerPointX-LowerThumb.Width-OffsetOutwards, 0, LowerThumb.Width, arrangeSize.Height)); + UpperThumb.Arrange(new Rect(UpperPointX +OffsetOutwards, 0, UpperThumb.Width, arrangeSize.Height)); + + DecreaseButton.Arrange(new Rect(0, 0, Math.Max(1, LowerPointX), Math.Max(1, arrangeSize.Height))); + IncreaseButton.Arrange(new Rect(UpperPointX, 0, Math.Max(1, arrangeSize.Width - UpperPointX), arrangeSize.Height)); + return arrangeSize; + } + + private void MoveValue(double delta) + { + if(delta > 0) + { + // If this increase will be clamped when changing the upper value, reduce it + // so it clamps the lower value too. This way, the distance between the upper + // and lower value stays the same. + delta = Math.Min(delta, Maximum - UpperValue); + UpperValue += delta; + LowerValue += delta; + } + else + { + delta *= -1; + delta = Math.Min(delta, LowerValue - Minimum); + LowerValue -= delta; + UpperValue -= delta; + } + } + + private double GetValueToSize() + { + return GetValueToSize(this.ActualWidth); + } + + private double GetValueToSize(double width) + { + double Range = Maximum - Minimum; + return Math.Max(0.0, (width - UpperThumb.RenderSize.Width) / Range); + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + LowerThumb = GetTemplateChild("PART_LowerThumb") as Thumb; + UpperThumb = GetTemplateChild("PART_UpperThumb") as Thumb; + Middle = GetTemplateChild("PART_Middle") as Thumb; + DecreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton; + IncreaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton; + DecreaseButton.Click += delegate(object sender, RoutedEventArgs e) { MoveValue(-1); }; + IncreaseButton.Click += delegate(object sender, RoutedEventArgs e) { MoveValue(+1); }; + + LowerThumb.DragDelta += delegate(object sender, DragDeltaEventArgs e) + { + double sizeToValue = 1 / GetValueToSize(); + + double NewValue = LowerValue + e.HorizontalChange * sizeToValue; + NewValue = Math.Min(NewValue, UpperValue - MinimumDistance); + LowerValue = NewValue; + }; + + UpperThumb.DragDelta += delegate(object sender, DragDeltaEventArgs e) + { + double sizeToValue = 1 / GetValueToSize(); + double NewValue = UpperValue + e.HorizontalChange * sizeToValue; + NewValue = Math.Max(NewValue, LowerValue + MinimumDistance); + UpperValue = NewValue; + }; + + Middle.DragDelta += delegate(object sender, DragDeltaEventArgs e) + { + // Convert the pixel delta to a value change. + double sizeToValue = 1 / GetValueToSize(); + Console.WriteLine("drag: " + e.HorizontalChange + ", " + sizeToValue + ", " + e.HorizontalChange * sizeToValue); + MoveValue(e.HorizontalChange * sizeToValue); + }; + + InvalidateArrange(); + } + } +} diff --git a/smx-config/Helpers.cs b/smx-config/Helpers.cs new file mode 100644 index 0000000..656d66d --- /dev/null +++ b/smx-config/Helpers.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Media; +using System.Windows.Threading; + +namespace smx_config +{ + static class Helpers + { + // Return true if we're in debug mode. + public static bool GetDebug() + { + foreach(string arg in Environment.GetCommandLineArgs()) + { + if(arg == "-d") + return true; + } + return false; + } + + // Work around Enumerable.SequenceEqual not checking if the arrays are null. + public static bool SequenceEqual(this IEnumerable first, IEnumerable second) + { + if(first == second) + return true; + if(first == null || second == null) + return false; + return Enumerable.SequenceEqual(first, second); + } + + public static Color ColorFromFloatRGB(double r, double g, double b) + { + byte R = (byte) Math.Max(0, Math.Min(255, r * 255)); + byte G = (byte) Math.Max(0, Math.Min(255, g * 255)); + byte B = (byte) Math.Max(0, Math.Min(255, b * 255)); + return Color.FromRgb(R, G, B); + } + + public static Color FromHSV(double H, double S, double V) + { + H = H % 360; + S = Math.Max(0, Math.Min(1, S)); + V = Math.Max(0, Math.Min(1, V)); + if(H < 0) + H += 360; + H /= 60; + + if( S < 0.0001f ) + return ColorFromFloatRGB(V, V, V); + + double C = V * S; + double X = C * (1 - Math.Abs((H % 2) - 1)); + + Color ret; + switch( (int) Math.Round(Math.Floor(H)) ) + { + case 0: ret = ColorFromFloatRGB(C, X, 0); break; + case 1: ret = ColorFromFloatRGB(X, C, 0); break; + case 2: ret = ColorFromFloatRGB(0, C, X); break; + case 3: ret = ColorFromFloatRGB(0, X, C); break; + case 4: ret = ColorFromFloatRGB(X, 0, C); break; + default: ret = ColorFromFloatRGB(C, 0, X); break; + } + + ret -= ColorFromFloatRGB(C-V, C-V, C-V); + return ret; + } + + public static void ToHSV(Color c, out double h, out double s, out double v) + { + h = s = v = 0; + if( c.R == 0 && c.G == 0 && c.B == 0 ) + return; + + double r = c.R / 255.0; + double g = c.G / 255.0; + double b = c.B / 255.0; + + double m = Math.Min(Math.Min(r, g), b); + double M = Math.Max(Math.Max(r, g), b); + double C = M - m; + if( Math.Abs(r-g) < 0.0001f && Math.Abs(g-b) < 0.0001f ) // grey + h = 0; + else if( Math.Abs(r-M) < 0.0001f ) // M == R + h = ((g - b)/C) % 6; + else if( Math.Abs(g-M) < 0.0001f ) // M == G + h = (b - r)/C + 2; + else // M == B + h = (r - g)/C + 4; + + h *= 60; + if( h < 0 ) + h += 360; + + s = C / M; + v = M; + } + } + + // This class just makes it easier to assemble binary command packets. + public class CommandBuffer + { + public void Write(string s) + { + char[] buf = s.ToCharArray(); + byte[] data = new byte[buf.Length]; + for(int i = 0; i < buf.Length; ++i) + data[i] = (byte) buf[i]; + Write(data); + } + public void Write(byte[] s) { parts.AddLast(s); } + public void Write(byte b) { Write(new byte[] { b }); } + public void Write(char b) { Write((byte) b); } + + public byte[] Get() + { + int length = 0; + foreach(byte[] part in parts) + length += part.Length; + + byte[] result = new byte[length]; + int next = 0; + foreach(byte[] part in parts) + { + Buffer.BlockCopy(part, 0, result, next, part.Length); + next += part.Length; + } + return result; + } + + private LinkedList parts = new LinkedList(); + }; + + // When enabled, periodically set all lights to the current auto-lighting color. This + // is enabled while manipulating the step color slider. + class ShowAutoLightsColor + { + private DispatcherTimer LightsTimer; + + public ShowAutoLightsColor() + { + LightsTimer = new DispatcherTimer(); + + // Run at 30fps. + LightsTimer.Interval = new TimeSpan(0,0,0,0, 1000 / 33); + + LightsTimer.Tick += delegate(object sender, EventArgs e) + { + if(!LightsTimer.IsEnabled) + return; + + AutoLightsColorRefreshColor(); + }; + } + + public void Start() + { + // To show the current color, send a lights command periodically. If we stop sending + // this for a while the controller will return to auto-lights, which we won't want to + // happen until AutoLightsColorEnd is called. + if(LightsTimer.IsEnabled) + return; + + // Don't wait for an interval to send the first update. + //AutoLightsColorRefreshColor(); + + LightsTimer.Start(); + } + + public void Stop() + { + LightsTimer.Stop(); + + // Reenable auto-lights immediately, without waiting for lights to time out. + SMX.SMX.ReenableAutoLights(); + } + + private void AutoLightsColorRefreshColor() + { + byte[] lights = new byte[864]; + CommandBuffer cmd = new CommandBuffer(); + + for(int pad = 0; pad < 2; ++pad) + { + SMX.SMXConfig config; + if(!SMX.SMX.GetConfig(pad, out config)) + continue; + + byte[] color = config.stepColor; + for( int iPanel = 0; iPanel < 9; ++iPanel ) + { + for( int i = 0; i < 16; ++i ) + { + cmd.Write( color[iPanel*3+0] ); + cmd.Write( color[iPanel*3+1] ); + cmd.Write( color[iPanel*3+2] ); + } + } + } + SMX.SMX.SetLights(cmd.Get()); + } + }; +} diff --git a/smx-config/MainWindow.xaml b/smx-config/MainWindow.xaml new file mode 100644 index 0000000..280725f --- /dev/null +++ b/smx-config/MainWindow.xaml @@ -0,0 +1,798 @@ + + + Lighter steps will activate the arrows. +Use if small children are having difficulty pressing the arrows. + This is the recommended setting. + More force is required to activate the arrows. +Use if the platform is too sensitive. + + Segoe UI Black + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #00FF00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Panel sensitivity + + + + + + + + Panel colors + + Set the color each arrow lights when pressed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +