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.
+
+
+
+ enabledSensors
+
+ Each platform can have up to nine panels in any configuration, but most devices have
+ a smaller number of panels installed. If an application wants to adapt its UI to the
+ user's panel configuration, see enabledSensors to detect which sensors are enabled.
+
+ Each panel has four sensors, and if a panel is disabled, all four of its panels will be
+ disabled. Disabling individual sensors is possible, but removing individual sensors
+ reduces the performance of the pad and isn't recommended.
+
+ Note that this indicates which panels the player is using for input. Other panels may
+ still have lights support, and the application should always send lights data for all
+ possible panels even if it's not being used for input.
+
+
+
+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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Custom sensitivity
+ Set the force required to trigger each arrow.
+
+
+
+
+
+
+
+
+
+
+
+ Each slider sets the amount of weight necessary for
+a panel to activate and deactivate.
+
+A panel will activate when it reaches the right side of the
+slider, and deactivate when it reaches the left side of the slider.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Active panels
+ Select which directions have sensors, and deselect panels that aren't in use.
+
+Input will be disabled from deselected panels.
+
+
+
+
+
+
+ Reset all settings
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/smx-config/MainWindow.xaml.cs b/smx-config/MainWindow.xaml.cs
new file mode 100644
index 0000000..bc72ba8
--- /dev/null
+++ b/smx-config/MainWindow.xaml.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Windows;
+
+namespace smx_config
+{
+ public partial class MainWindow: Window
+ {
+ OnConfigChange onConfigChange;
+ ShowAutoLightsColor showAutoLightsColor = new ShowAutoLightsColor();
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ onConfigChange = new OnConfigChange(this, delegate(LoadFromConfigDelegateArgs args) {
+ LoadUIFromConfig(args);
+ });
+ }
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ AutoLightsColor.StartedDragging += delegate() { showAutoLightsColor.Start(); };
+ AutoLightsColor.StoppedDragging += delegate() { showAutoLightsColor.Stop(); };
+ AutoLightsColor.StoppedDragging += delegate() { showAutoLightsColor.Stop(); };
+
+ // This doesn't happen at the same time AutoLightsColor is used, since they're on different tabs.
+ Diagnostics.SetShowAllLights += delegate(bool on)
+ {
+ if(on)
+ showAutoLightsColor.Start();
+ else
+ showAutoLightsColor.Stop();
+ };
+
+ SetAllPanelsToCurrentColor.Click += delegate(object sender, RoutedEventArgs e)
+ {
+ int SelectedPanel = AutoLightsColor.SelectedPanel % 9;
+ int SelectedPad = AutoLightsColor.SelectedPanel < 9? 0:1;
+
+ // Get the color of the selected pad.
+ SMX.SMXConfig copyFromConfig;
+ if(!SMX.SMX.GetConfig(SelectedPad, out copyFromConfig))
+ return;
+
+ for(int pad = 0; pad < 2; ++pad)
+ {
+ SMX.SMXConfig config;
+ if(!SMX.SMX.GetConfig(pad, out config))
+ continue;
+
+ // Set all stepColors to the color of the selected panel.
+ for(int i = 0; i < 9; ++i)
+ {
+ config.stepColor[i*3+0] = copyFromConfig.stepColor[SelectedPanel*3+0];
+ config.stepColor[i*3+1] = copyFromConfig.stepColor[SelectedPanel*3+1];
+ config.stepColor[i*3+2] = copyFromConfig.stepColor[SelectedPanel*3+2];
+ }
+
+ SMX.SMX.SetConfig(pad, config);
+ }
+ CurrentSMXDevice.singleton.FireConfigurationChanged(null);
+ };
+ }
+
+ private void LoadUIFromConfig(LoadFromConfigDelegateArgs args)
+ {
+ bool EitherControllerConnected = args.controller[0].info.connected || args.controller[1].info.connected;
+ Main.Visibility = EitherControllerConnected? Visibility.Visible:Visibility.Hidden;
+ Searching.Visibility = EitherControllerConnected? Visibility.Hidden:Visibility.Visible;
+ ConnectedPads.Visibility = EitherControllerConnected? Visibility.Visible:Visibility.Hidden;
+ P1Connected.IsEnabled = args.controller[0].info.connected;
+ P2Connected.IsEnabled = args.controller[1].info.connected;
+ PanelColorP1.Visibility = args.controller[0].info.connected? Visibility.Visible:Visibility.Collapsed;
+ PanelColorP2.Visibility = args.controller[1].info.connected? Visibility.Visible:Visibility.Collapsed;
+ }
+
+ private void FactoryReset_Click(object sender, RoutedEventArgs e)
+ {
+ for(int pad = 0; pad < 2; ++pad)
+ SMX.SMX.FactoryReset(pad);
+ CurrentSMXDevice.singleton.FireConfigurationChanged(null);
+ }
+ }
+}
diff --git a/smx-config/Properties/AssemblyInfo.cs b/smx-config/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..cf18978
--- /dev/null
+++ b/smx-config/Properties/AssemblyInfo.cs
@@ -0,0 +1,55 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("smx-config")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("smx-config")]
+[assembly: AssemblyCopyright("© 2017")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//CultureYouAreCodingWith in your .csproj file
+//inside a . For example, if you are using US english
+//in your source files, set the to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/smx-config/Properties/Resources.Designer.cs b/smx-config/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..f462ca3
--- /dev/null
+++ b/smx-config/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace smx_config.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("smx_config.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/smx-config/Properties/Resources.resx b/smx-config/Properties/Resources.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/smx-config/Properties/Resources.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/smx-config/Properties/Settings.Designer.cs b/smx-config/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..90d3470
--- /dev/null
+++ b/smx-config/Properties/Settings.Designer.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace smx_config.Properties
+{
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+ internal sealed partial class Settings: global::System.Configuration.ApplicationSettingsBase
+ {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default
+ {
+ get
+ {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/smx-config/Properties/Settings.settings b/smx-config/Properties/Settings.settings
new file mode 100644
index 0000000..033d7a5
--- /dev/null
+++ b/smx-config/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/smx-config/Resources/DIP.png b/smx-config/Resources/DIP.png
new file mode 100644
index 0000000..b04c077
Binary files /dev/null and b/smx-config/Resources/DIP.png differ
diff --git a/smx-config/Resources/pad_cardinal.png b/smx-config/Resources/pad_cardinal.png
new file mode 100644
index 0000000..b01bc65
Binary files /dev/null and b/smx-config/Resources/pad_cardinal.png differ
diff --git a/smx-config/Resources/pad_center.png b/smx-config/Resources/pad_center.png
new file mode 100644
index 0000000..16cdcf5
Binary files /dev/null and b/smx-config/Resources/pad_center.png differ
diff --git a/smx-config/Resources/pad_diagonal.png b/smx-config/Resources/pad_diagonal.png
new file mode 100644
index 0000000..84429bf
Binary files /dev/null and b/smx-config/Resources/pad_diagonal.png differ
diff --git a/smx-config/Resources/pad_down.png b/smx-config/Resources/pad_down.png
new file mode 100644
index 0000000..65737d7
Binary files /dev/null and b/smx-config/Resources/pad_down.png differ
diff --git a/smx-config/Resources/pad_down_left.png b/smx-config/Resources/pad_down_left.png
new file mode 100644
index 0000000..dbb6e6e
Binary files /dev/null and b/smx-config/Resources/pad_down_left.png differ
diff --git a/smx-config/Resources/pad_down_right.png b/smx-config/Resources/pad_down_right.png
new file mode 100644
index 0000000..3a3c685
Binary files /dev/null and b/smx-config/Resources/pad_down_right.png differ
diff --git a/smx-config/Resources/pad_left.png b/smx-config/Resources/pad_left.png
new file mode 100644
index 0000000..44ce663
Binary files /dev/null and b/smx-config/Resources/pad_left.png differ
diff --git a/smx-config/Resources/pad_right.png b/smx-config/Resources/pad_right.png
new file mode 100644
index 0000000..3b8b39f
Binary files /dev/null and b/smx-config/Resources/pad_right.png differ
diff --git a/smx-config/Resources/pad_up.png b/smx-config/Resources/pad_up.png
new file mode 100644
index 0000000..a758467
Binary files /dev/null and b/smx-config/Resources/pad_up.png differ
diff --git a/smx-config/Resources/pad_up_left.png b/smx-config/Resources/pad_up_left.png
new file mode 100644
index 0000000..971a081
Binary files /dev/null and b/smx-config/Resources/pad_up_left.png differ
diff --git a/smx-config/Resources/pad_up_right.png b/smx-config/Resources/pad_up_right.png
new file mode 100644
index 0000000..f13ab10
Binary files /dev/null and b/smx-config/Resources/pad_up_right.png differ
diff --git a/smx-config/Resources/window icon.ico b/smx-config/Resources/window icon.ico
new file mode 100644
index 0000000..febd6de
Binary files /dev/null and b/smx-config/Resources/window icon.ico differ
diff --git a/smx-config/Resources/window icon.png b/smx-config/Resources/window icon.png
new file mode 100644
index 0000000..63d7ac3
Binary files /dev/null and b/smx-config/Resources/window icon.png differ
diff --git a/smx-config/SMX.cs b/smx-config/SMX.cs
new file mode 100644
index 0000000..fddba65
--- /dev/null
+++ b/smx-config/SMX.cs
@@ -0,0 +1,296 @@
+using System;
+using System.Runtime.InteropServices;
+using smx_config;
+
+// This is a binding to the native SMX.dll.
+namespace SMX
+{
+ [StructLayout(LayoutKind.Sequential, Pack=1)]
+ public struct SMXInfo {
+ [MarshalAs(UnmanagedType.I1)] // work around C# bug: marshals bool as int
+ public bool connected;
+
+ // The 32-byte hex serial number of the device, followed by '\0'.
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 33)]
+ public byte[] m_Serial;
+
+ public Int16 m_iFirmwareVersion;
+
+ // Padding to make this the same size as native, where MSVC adds padding even though
+ // we tell it not to:
+ private Byte dummy;
+ };
+
+ [StructLayout(LayoutKind.Sequential, Pack=1)]
+ public struct SMXConfig {
+ public Byte unused1, unused2;
+ public Byte unused3, unused4;
+ public Byte unused5, unused6;
+ public UInt16 masterDebounceMilliseconds;
+ public Byte panelThreshold7Low, panelThreshold7High; // was "cardinal"
+ public Byte panelThreshold4Low, panelThreshold4High; // was "center"
+ public Byte panelThreshold2Low, panelThreshold2High; // was "corner"
+ public UInt16 panelDebounceMicroseconds;
+ public UInt16 autoCalibrationPeriodMilliseconds;
+ public Byte autoCalibrationMaxDeviation;
+ public Byte badSensorMinimumDelaySeconds;
+ public UInt16 autoCalibrationAveragesPerUpdate;
+ public Byte unused7, unused8;
+ public Byte panelThreshold1Low, panelThreshold1High; // was "up"
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
+ public Byte[] enabledSensors;
+
+ public Byte autoLightsTimeout;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3*9)]
+ public Byte[] stepColor;
+
+ public Byte panelRotation;
+ public UInt16 autoCalibrationSamplesPerAverage;
+ public Byte masterVersion;
+ public Byte configVersion;
+
+ // The remaining thresholds (configVersion >= 2).
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
+ public Byte[] unused9;
+ public Byte panelThreshold0Low, panelThreshold0High;
+ public Byte panelThreshold3Low, panelThreshold3High;
+ public Byte panelThreshold5Low, panelThreshold5High;
+ public Byte panelThreshold6Low, panelThreshold6High;
+ public Byte panelThreshold8Low, panelThreshold8High;
+
+ // enabledSensors is a mask of which panels are enabled. Return this as an array
+ // for convenience.
+ public bool[] GetEnabledPanels()
+ {
+ return new bool[] {
+ (enabledSensors[0] & 0xF0) != 0,
+ (enabledSensors[0] & 0x0F) != 0,
+ (enabledSensors[1] & 0xF0) != 0,
+ (enabledSensors[1] & 0x0F) != 0,
+ (enabledSensors[2] & 0xF0) != 0,
+ (enabledSensors[2] & 0x0F) != 0,
+ (enabledSensors[3] & 0xF0) != 0,
+ (enabledSensors[3] & 0x0F) != 0,
+ (enabledSensors[4] & 0xF0) != 0,
+ };
+ }
+ };
+
+ public struct SMXSensorTestModeData
+ {
+ // If false, sensorLevel[n][*] is zero because we didn't receive a response from that panel.
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType=UnmanagedType.I1, SizeConst = 9)]
+ public bool[] bHaveDataFromPanel;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 9*4)]
+ public Int16[] sensorLevel;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType=UnmanagedType.I1, SizeConst = 9*4)]
+ public bool[] bBadSensorInput;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]
+ public int[] iDIPSwitchPerPanel;
+
+ public override bool Equals(object obj)
+ {
+ SMXSensorTestModeData other = (SMXSensorTestModeData) obj;
+ return
+ Helpers.SequenceEqual(bHaveDataFromPanel, other.bHaveDataFromPanel) &&
+ Helpers.SequenceEqual(sensorLevel, other.sensorLevel) &&
+ Helpers.SequenceEqual(bBadSensorInput, other.bBadSensorInput) &&
+ Helpers.SequenceEqual(iDIPSwitchPerPanel, other.iDIPSwitchPerPanel);
+ }
+
+ // Dummy override to silence a bad warning. We don't use these in containers to need
+ // a hash code implementation.
+ public override int GetHashCode() { return base.GetHashCode(); }
+
+
+ public bool AnySensorsOnPanelNotResponding(int panel)
+ {
+ if(!bHaveDataFromPanel[panel])
+ return false;
+ for(int sensor = 0; sensor < 4; ++sensor)
+ if(bBadSensorInput[panel*4+sensor])
+ return true;
+
+ return false;
+ }
+ };
+
+ public static class SMX
+ {
+ [DllImport("kernel32", SetLastError=true)]
+ static extern IntPtr LoadLibrary(string lpFileName);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern void SMX_Start(
+ [MarshalAs(UnmanagedType.FunctionPtr)] InternalUpdateCallback callback,
+ IntPtr user);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern void SMX_Stop();
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern void SMX_GetInfo(int pad, out SMXInfo info);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern UInt16 SMX_GetInputState(int pad);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ [return:MarshalAs(UnmanagedType.I1)]
+ private static extern bool SMX_GetConfig(int pad, out SMXConfig config);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern void SMX_SetConfig(int pad, ref SMXConfig config);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern void SMX_FactoryReset(int pad);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern void SMX_ForceRecalibration(int pad);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern void SMX_SetTestMode(int pad, int mode);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern bool SMX_GetTestData(int pad, out SMXSensorTestModeData data);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern bool SMX_SetLights(byte[] buf);
+ [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
+ private static extern bool SMX_ReenableAutoLights();
+
+ // Check if the native DLL is available. This is mostly to avoid exceptions in the designer.
+ private static bool DLLAvailable()
+ {
+ return LoadLibrary("SMX.dll") != IntPtr.Zero;
+ }
+
+ public delegate void UpdateCallback(int PadNumber, SMXUpdateCallbackReason reason);
+
+ // The C API allows a user pointer, but we don't use that here.
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate void InternalUpdateCallback(int PadNumber, int reason, IntPtr user);
+ private static InternalUpdateCallback CurrentUpdateCallback;
+ public static void SetUpdateCallback(UpdateCallback callback)
+ {
+ if(!DLLAvailable()) return;
+
+ }
+
+ public static void Start(UpdateCallback callback)
+ {
+ if(!DLLAvailable()) return;
+
+ // Make a wrapper to convert from the native enum to SMXUpdateCallbackReason.
+ InternalUpdateCallback NewCallback = delegate(int PadNumber, int reason, IntPtr user) {
+ SMXUpdateCallbackReason ReasonEnum = (SMXUpdateCallbackReason) Enum.ToObject(typeof(SMXUpdateCallbackReason), reason);
+ callback(PadNumber, ReasonEnum);
+ };
+ if(callback == null)
+ NewCallback = null;
+
+ SMX_Start(NewCallback, IntPtr.Zero);
+
+ // Keep a reference to the delegate, so it isn't garbage collected. Do this last. Once
+ // we do this the old callback may be collected, so we want to be sure that native code
+ // has already updated the callback.
+ CurrentUpdateCallback = NewCallback;
+
+ Console.WriteLine("Struct sizes (C#): " +
+ Marshal.SizeOf(typeof(SMXConfig)) + " " +
+ Marshal.SizeOf(typeof(SMXInfo)) + " " +
+ Marshal.SizeOf(typeof(SMXSensorTestModeData)));
+ }
+
+ public static void Stop()
+ {
+ if(!DLLAvailable()) return;
+ SMX_Stop();
+ }
+
+ public enum SMXUpdateCallbackReason {
+ Updated,
+ FactoryResetCommandComplete
+ };
+
+ public static void GetInfo(int pad, out SMXInfo info)
+ {
+ if(!DLLAvailable()) {
+ info = new SMXInfo();
+ return;
+ }
+ SMX_GetInfo(pad, out info);
+ }
+
+ public static UInt16 GetInputState(int pad)
+ {
+ if(!DLLAvailable())
+ return 0;
+
+ return SMX_GetInputState(pad);
+ }
+
+ public static bool GetConfig(int pad, out SMXConfig config)
+ {
+ if(!DLLAvailable()) {
+ config = new SMXConfig();
+ config.enabledSensors = new Byte[5];
+ config.stepColor = new Byte[3*9];
+ return false;
+ }
+ return SMX_GetConfig(pad, out config);
+ }
+
+ public static void SetConfig(int pad, SMXConfig config)
+ {
+ if(!DLLAvailable()) return;
+
+ // Always bump the configVersion to the version we support on write.
+ config.configVersion = 2;
+
+ SMX_SetConfig(pad, ref config);
+ }
+
+ public enum SensorTestMode {
+ Off = 0,
+ UncalibratedValues = '0',
+ CalibratedValues = '1',
+ Noise = '2',
+ Tare = '3',
+ };
+
+ public static void SetTestMode(int pad, SensorTestMode mode)
+ {
+ if(!DLLAvailable()) return;
+ SMX_SetTestMode(pad, (int) mode);
+ }
+
+ public static bool GetTestData(int pad, out SMXSensorTestModeData data)
+ {
+ if(!DLLAvailable()) {
+ data = new SMXSensorTestModeData();
+ return false;
+ }
+
+ return SMX_GetTestData(pad, out data);
+ }
+
+ public static void FactoryReset(int pad)
+ {
+ if(!DLLAvailable()) return;
+ SMX_FactoryReset(pad);
+ }
+
+
+ public static void ForceRecalibration(int pad)
+ {
+ if(!DLLAvailable()) return;
+ SMX_ForceRecalibration(pad);
+ }
+
+ public static void SetLights(byte[] buf)
+ {
+ if(!DLLAvailable()) return;
+ SMX_SetLights(buf);
+ }
+
+ public static void ReenableAutoLights()
+ {
+ if(!DLLAvailable()) return;
+ SMX_ReenableAutoLights();
+ }
+ }
+}
diff --git a/smx-config/SMXConfig.csproj b/smx-config/SMXConfig.csproj
new file mode 100644
index 0000000..72ab963
--- /dev/null
+++ b/smx-config/SMXConfig.csproj
@@ -0,0 +1,200 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}
+ WinExe
+ Properties
+ smx_config
+ SMXConfig
+ v4.5.2
+ 512
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 4
+ true
+ publish\
+ true
+ Disk
+ false
+ Foreground
+ 7
+ Days
+ false
+ false
+ true
+ 0
+ 1.0.0.%2a
+ false
+ false
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ true
+ $(SolutionDir)\out\
+ DEBUG;TRACE
+ full
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+ true
+ false
+
+
+ ..\out\
+ TRACE
+ true
+ pdbonly
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+ true
+ default
+ false
+ false
+
+
+ window icon.ico
+
+
+
+
+
+
+
+
+
+
+
+
+ 4.0
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ App.xaml
+ Code
+
+
+ MainWindow.xaml
+ Code
+
+
+
+
+ Code
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ Settings.settings
+ True
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
+
+
+
+
+ False
+ Microsoft .NET Framework 4.5.2 %28x86 and x64%29
+ true
+
+
+ False
+ .NET Framework 3.5 SP1
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {c5fc0823-9896-4b7c-bfe1-b60db671a462}
+ SMX
+
+
+
+
+
\ No newline at end of file
diff --git a/smx-config/Widgets.cs b/smx-config/Widgets.cs
new file mode 100644
index 0000000..8765995
--- /dev/null
+++ b/smx-config/Widgets.cs
@@ -0,0 +1,921 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Media;
+using System.Windows.Input;
+using System.Windows.Data;
+using System.Windows.Shapes;
+using System.Windows.Media.Imaging;
+
+namespace smx_config
+{
+ // The checkbox to enable and disable the advanced per-panel sliders.
+ //
+ // This is always enabled if the thresholds in the configuration are set to different
+ // values. If the user enables us, we'll remember that we were forced on. If the user
+ // disables us, we'll sync the thresholds back up and turn the ForcedOn flag off.
+ public class AdvancedThresholdViewCheckbox: CheckBox
+ {
+ public static readonly DependencyProperty AdvancedModeEnabledProperty = DependencyProperty.Register("AdvancedModeEnabled",
+ typeof(bool), typeof(AdvancedThresholdViewCheckbox), new FrameworkPropertyMetadata(false));
+ public bool AdvancedModeEnabled {
+ get { return (bool) GetValue(AdvancedModeEnabledProperty); }
+ set { SetValue(AdvancedModeEnabledProperty, value); }
+ }
+
+ OnConfigChange onConfigChange;
+
+ // If true, the user enabled advanced view and we should display it even if
+ // the thresholds happen to be synced. If false, we'll only show the advanced
+ // view if we need to because the thresholds aren't synced.
+ bool ForcedOn;
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
+ LoadUIFromConfig(args.controller[args.FirstController].config);
+ });
+ }
+
+ private void LoadUIFromConfig(SMX.SMXConfig config)
+ {
+ // The master version doesn't actually matter, but we use this as a signal that the panels
+ // have a new enough firmware to support this.
+ bool SupportsAdvancedMode = config.masterVersion != 0xFF && config.masterVersion >= 2;
+ Visibility = SupportsAdvancedMode? Visibility.Visible:Visibility.Collapsed;
+
+ // If the thresholds are different, force the checkbox on. This way, if you load the application
+ // with a platform with per-panel thresholds, and change the thresholds to no longer be different,
+ // advanced mode stays forced on. It'll only turn off if you uncheck the box, or if you exit
+ // the application with synced thresholds and then restart it.
+ if(SupportsAdvancedMode && !ConfigPresets.AreUnifiedThresholdsSynced(config))
+ ForcedOn = true;
+
+ // Enable advanced mode if the master says it's supported, and either the user has checked the
+ // box to turn it on or the thresholds are different in the current configuration.
+ AdvancedModeEnabled = SupportsAdvancedMode && ForcedOn;
+ }
+
+ protected override void OnClick()
+ {
+ if(AdvancedModeEnabled)
+ {
+ // Stop forcing advanced mode on, and sync the thresholds so we exit advanced mode.
+ ForcedOn = false;
+
+ for(int pad = 0; pad < 2; ++pad)
+ {
+ SMX.SMXConfig config;
+ if(!SMX.SMX.GetConfig(pad, out config))
+ continue;
+
+ ConfigPresets.SyncUnifiedThresholds(ref config);
+ SMX.SMX.SetConfig(pad, config);
+ }
+ CurrentSMXDevice.singleton.FireConfigurationChanged(this);
+ }
+ else
+ {
+ // Enable advanced mode.
+ ForcedOn = true;
+ }
+
+ // Refresh the UI.
+ LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState();
+ LoadUIFromConfig(args.controller[args.FirstController].config);
+ }
+ }
+
+ // This implements the threshold slider widget for changing an upper/lower threshold pair.
+ public class ThresholdSlider: Control
+ {
+ public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon",
+ typeof(ImageSource), typeof(ThresholdSlider), new FrameworkPropertyMetadata(null));
+
+ public ImageSource Icon {
+ get { return (ImageSource) GetValue(IconProperty); }
+ set { SetValue(IconProperty, value); }
+ }
+
+ public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type",
+ typeof(string), typeof(ThresholdSlider), new FrameworkPropertyMetadata(""));
+
+ public string Type {
+ get { return (string) GetValue(TypeProperty); }
+ set { SetValue(TypeProperty, value); }
+ }
+
+ public static readonly DependencyProperty AdvancedModeEnabledProperty = DependencyProperty.Register("AdvancedModeEnabled",
+ typeof(bool), typeof(ThresholdSlider), new FrameworkPropertyMetadata(false, RefreshAdvancedModeEnabledCallback));
+
+ public bool AdvancedModeEnabled {
+ get { return (bool) GetValue(AdvancedModeEnabledProperty); }
+ set { SetValue(AdvancedModeEnabledProperty, value); }
+ }
+
+ private static void RefreshAdvancedModeEnabledCallback(DependencyObject target, DependencyPropertyChangedEventArgs args)
+ {
+ ThresholdSlider self = target as ThresholdSlider;
+ self.RefreshVisibility();
+ }
+
+ DoubleSlider slider;
+ Label LowerLabel, UpperLabel;
+
+ OnConfigChange onConfigChange;
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ slider = GetTemplateChild("Slider") as DoubleSlider;
+ LowerLabel = GetTemplateChild("LowerValue") as Label;
+ UpperLabel = GetTemplateChild("UpperValue") as Label;
+
+ slider.ValueChanged += delegate(DoubleSlider slider) { SaveToConfig(); };
+
+ onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
+ LoadUIFromConfig(args.controller[args.FirstController].config);
+ });
+ }
+
+ private void SetValueToConfig(ref SMX.SMXConfig config)
+ {
+ byte lower = (byte) slider.LowerValue;
+ byte upper = (byte) slider.UpperValue;
+
+ switch(Type)
+ {
+ case "up-left": config.panelThreshold0Low = lower; config.panelThreshold0High = upper; break;
+ case "up": config.panelThreshold1Low = lower; config.panelThreshold1High = upper; break;
+ case "up-right": config.panelThreshold2Low = lower; config.panelThreshold2High = upper; break;
+ case "left": config.panelThreshold3Low = lower; config.panelThreshold3High = upper; break;
+ case "center": config.panelThreshold4Low = lower; config.panelThreshold4High = upper; break;
+ case "right": config.panelThreshold5Low = lower; config.panelThreshold5High = upper; break;
+ case "down-left": config.panelThreshold6Low = lower; config.panelThreshold6High = upper; break;
+ case "down": config.panelThreshold7Low = lower; config.panelThreshold7High = upper; break;
+ case "down-right": config.panelThreshold8Low = lower; config.panelThreshold8High = upper; break;
+ case "cardinal": config.panelThreshold7Low = lower; config.panelThreshold7High = upper; break;
+ case "corner": config.panelThreshold2Low = lower; config.panelThreshold2High = upper; break;
+ }
+
+ // If we're not in advanced mode, sync the cardinal value to each of the panel values.
+ if(!AdvancedModeEnabled)
+ ConfigPresets.SyncUnifiedThresholds(ref config);
+ }
+
+ private void GetValueFromConfig(SMX.SMXConfig config, out byte lower, out byte upper)
+ {
+ switch(Type)
+ {
+ case "up-left": lower = config.panelThreshold0Low; upper = config.panelThreshold0High; return;
+ case "up": lower = config.panelThreshold1Low; upper = config.panelThreshold1High; return;
+ case "up-right": lower = config.panelThreshold2Low; upper = config.panelThreshold2High; return;
+ case "left": lower = config.panelThreshold3Low; upper = config.panelThreshold3High; return;
+ case "center": lower = config.panelThreshold4Low; upper = config.panelThreshold4High; return;
+ case "right": lower = config.panelThreshold5Low; upper = config.panelThreshold5High; return;
+ case "down-left": lower = config.panelThreshold6Low; upper = config.panelThreshold6High; return;
+ case "down": lower = config.panelThreshold7Low; upper = config.panelThreshold7High; return;
+ case "down-right": lower = config.panelThreshold8Low; upper = config.panelThreshold8High; return;
+ case "cardinal": lower = config.panelThreshold7Low; upper = config.panelThreshold7High; return;
+ case "corner": lower = config.panelThreshold2Low; upper = config.panelThreshold2High; return;
+ default:
+ lower = upper = 0;
+ return;
+ }
+ }
+
+ private void SaveToConfig()
+ {
+ if(UpdatingUI)
+ return;
+
+ // Apply the change and save it to the devices.
+ for(int pad = 0; pad < 2; ++pad)
+ {
+ SMX.SMXConfig config;
+ if(!SMX.SMX.GetConfig(pad, out config))
+ continue;
+
+ SetValueToConfig(ref config);
+ SMX.SMX.SetConfig(pad, config);
+ CurrentSMXDevice.singleton.FireConfigurationChanged(this);
+ }
+ }
+
+ bool UpdatingUI = false;
+ private void LoadUIFromConfig(SMX.SMXConfig config)
+ {
+ // Make sure SaveToConfig doesn't treat these as the user changing values.
+ UpdatingUI = true;
+
+ byte lower, upper;
+ GetValueFromConfig(config, out lower, out upper);
+
+ if(lower == 0xFF)
+ {
+ LowerLabel.Content = "Off";
+ UpperLabel.Content = "";
+ }
+ else
+ {
+ slider.LowerValue = lower;
+ slider.UpperValue = upper;
+ LowerLabel.Content = lower.ToString();
+ UpperLabel.Content = upper.ToString();
+ }
+
+ RefreshVisibility();
+ UpdatingUI = false;
+ }
+
+ void RefreshVisibility()
+ {
+ LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState();
+ SMX.SMXConfig config = args.controller[args.FirstController].config;
+ this.Visibility = ShouldBeDisplayed(config)? Visibility.Visible:Visibility.Collapsed;
+ }
+
+ // Return true if this slider should be displayed. Only display a slider if it affects
+ // at least one panel which is enabled.
+ private bool ShouldBeDisplayed(SMX.SMXConfig config)
+ {
+ bool[] enabledPanels = config.GetEnabledPanels();
+
+ // Up and center are shown in both modes.
+ switch(Type)
+ {
+ case "up-left": return AdvancedModeEnabled && enabledPanels[0];
+ case "up": return enabledPanels[1];
+ case "up-right": return AdvancedModeEnabled && enabledPanels[2];
+ case "left": return AdvancedModeEnabled && enabledPanels[3];
+ case "center": return enabledPanels[4];
+ case "right": return AdvancedModeEnabled && enabledPanels[5];
+ case "down-left": return AdvancedModeEnabled && enabledPanels[6];
+ case "down": return AdvancedModeEnabled && enabledPanels[7];
+ case "down-right": return AdvancedModeEnabled && enabledPanels[8];
+
+ // Show cardinal and corner if at least one panel they affect is enabled.
+ case "cardinal": return !AdvancedModeEnabled && (enabledPanels[3] || enabledPanels[5] || enabledPanels[8]);
+ case "corner": return !AdvancedModeEnabled && (enabledPanels[0] || enabledPanels[2] || enabledPanels[6] || enabledPanels[8]);
+ default: return true;
+ }
+ }
+ }
+
+ // A button that selects a preset, and shows a checkmark if that preset is set.
+ public class PresetButton: Control
+ {
+ public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type",
+ typeof(string), typeof(PresetButton), new FrameworkPropertyMetadata(""));
+ public string Type {
+ get { return (string) GetValue(TypeProperty); }
+ set { SetValue(TypeProperty, value); }
+ }
+
+ public static readonly DependencyProperty SelectedProperty = DependencyProperty.Register("Selected",
+ typeof(bool), typeof(PresetButton), new FrameworkPropertyMetadata(true));
+ public bool Selected {
+ get { return (bool) GetValue(SelectedProperty); }
+ set { SetValue(SelectedProperty, value); }
+ }
+
+ public static readonly DependencyProperty LabelProperty = DependencyProperty.Register("Label",
+ typeof(string), typeof(PresetButton), new FrameworkPropertyMetadata(""));
+ public string Label {
+ get { return (string) GetValue(LabelProperty); }
+ set { SetValue(LabelProperty, value); }
+ }
+
+ private OnConfigChange onConfigChange;
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ Button button = GetTemplateChild("PART_Button") as Button;
+ button.Click += delegate(object sender, RoutedEventArgs e) { Select(); };
+
+ onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
+ string CurrentPreset = ConfigPresets.GetPreset(args.controller[args.FirstController].config);
+ Selected = CurrentPreset == Type;
+ });
+ }
+
+ private void Select()
+ {
+ for(int pad = 0; pad < 2; ++pad)
+ {
+ SMX.SMXConfig config;
+ if(!SMX.SMX.GetConfig(pad, out config))
+ continue;
+
+ ConfigPresets.SetPreset(Type, ref config);
+ Console.WriteLine("PresetButton::Select (" + Type + "): " +
+ config.panelThreshold1Low + ", " + config.panelThreshold4Low + ", " + config.panelThreshold7Low + ", " + config.panelThreshold2Low);
+ SMX.SMX.SetConfig(pad, config);
+ }
+ CurrentSMXDevice.singleton.FireConfigurationChanged(this);
+ }
+ }
+
+ public class PresetWidget: Control
+ {
+ public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type",
+ typeof(string), typeof(PresetWidget), new FrameworkPropertyMetadata(""));
+ public string Type {
+ get { return (string) GetValue(TypeProperty); }
+ set { SetValue(TypeProperty, value); }
+ }
+
+ public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register("Description",
+ typeof(string), typeof(PresetWidget), new FrameworkPropertyMetadata(""));
+ public string Description {
+ get { return (string) GetValue(DescriptionProperty); }
+ set { SetValue(DescriptionProperty, value); }
+ }
+
+ public static readonly DependencyProperty LabelProperty = DependencyProperty.Register("Label",
+ typeof(string), typeof(PresetWidget), new FrameworkPropertyMetadata(""));
+ public string Label {
+ get { return (string) GetValue(LabelProperty); }
+ set { SetValue(LabelProperty, value); }
+ }
+ }
+
+ public class PanelButton: ToggleButton
+ {
+ public static readonly DependencyProperty ButtonProperty = DependencyProperty.RegisterAttached("Button",
+ typeof(string), typeof(PanelButton), new FrameworkPropertyMetadata(null));
+
+ public string Button {
+ get { return (string) this.GetValue(ButtonProperty); }
+ set { this.SetValue(ButtonProperty, value); }
+ }
+
+ protected override void OnIsPressedChanged(DependencyPropertyChangedEventArgs e)
+ {
+ base.OnIsPressedChanged(e);
+ }
+ }
+
+ // A base class for buttons used to select a panel to work with.
+ public class PanelSelectButton: Button
+ {
+ // Which panel this is (P1 0-8, P2 9-17):
+ public static readonly DependencyProperty PanelProperty = DependencyProperty.RegisterAttached("Panel",
+ typeof(int), typeof(PanelSelectButton), new FrameworkPropertyMetadata(0, RefreshIsSelectedCallback));
+
+ public int Panel {
+ get { return (int) this.GetValue(PanelProperty); }
+ set { this.SetValue(PanelProperty, value); }
+ }
+
+ // Which panel is currently selected. If this == Panel, this panel is selected. This is
+ // bound to ColorPicker.SelectedPanel, so changing this changes which panel the picker edits.
+ public static readonly DependencyProperty SelectedPanelProperty = DependencyProperty.RegisterAttached("SelectedPanel",
+ typeof(int), typeof(PanelSelectButton), new FrameworkPropertyMetadata(0, RefreshIsSelectedCallback));
+
+ public int SelectedPanel {
+ get { return (int) this.GetValue(SelectedPanelProperty); }
+ set { this.SetValue(SelectedPanelProperty, value); }
+ }
+
+ // Whether this panel is selected. This is true if Panel == SelectedPanel.
+ public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.RegisterAttached("IsSelected",
+ typeof(bool), typeof(PanelSelectButton), new FrameworkPropertyMetadata(false));
+
+ public bool IsSelected {
+ get { return (bool) this.GetValue(IsSelectedProperty); }
+ set { this.SetValue(IsSelectedProperty, value); }
+ }
+
+ // When Panel or SelectedPanel change, update IsSelected.
+ private static void RefreshIsSelectedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args)
+ {
+ PanelSelectButton self = target as PanelSelectButton;
+ self.RefreshIsSelected();
+ }
+
+ private void RefreshIsSelected()
+ {
+ IsSelected = Panel == SelectedPanel;
+ }
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ RefreshIsSelected();
+ }
+ }
+
+ // This button shows the color configured for that panel, and chooses which color is being
+ // edited by the ColorPicker.
+ public class PanelColorButton: PanelSelectButton
+ {
+ // The color configured for this panel:
+ public static readonly DependencyProperty PanelColorProperty = DependencyProperty.RegisterAttached("PanelColor",
+ typeof(SolidColorBrush), typeof(PanelColorButton), new FrameworkPropertyMetadata(new SolidColorBrush()));
+
+ public SolidColorBrush PanelColor {
+ get { return (SolidColorBrush) this.GetValue(PanelColorProperty); }
+ set { this.SetValue(PanelColorProperty, value); }
+ }
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ OnConfigChange onConfigChange;
+ onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
+ int pad = Panel < 9? 0:1;
+ LoadUIFromConfig(args.controller[pad].config);
+ });
+ }
+
+ protected override void OnClick()
+ {
+ base.OnClick();
+
+ // Select this panel.
+ SelectedPanel = Panel;
+
+ // Fire configuration changed, so the color slider updates to show this panel.
+ CurrentSMXDevice.singleton.FireConfigurationChanged(this);
+ }
+
+ // Set PanelColor. This widget doesn't change the color, it only reflects the current configuration.
+ private void LoadUIFromConfig(SMX.SMXConfig config)
+ {
+ int PanelIndex = Panel % 9;
+
+ // Hide color buttons for disabled panels.
+ bool[] enabledPanels = config.GetEnabledPanels();
+ Visibility = enabledPanels[PanelIndex]? Visibility.Visible:Visibility.Hidden;
+
+ Color rgb = ColorPicker.UnscaleColor(Color.FromRgb(
+ config.stepColor[PanelIndex*3+0],
+ config.stepColor[PanelIndex*3+1],
+ config.stepColor[PanelIndex*3+2]));
+ PanelColor = new SolidColorBrush(rgb);
+ }
+
+ // Return #RRGGBB for the color set on this panel.
+ private string GetColorString()
+ {
+ // WPF's Color.ToString() returns #AARRGGBB, which is just wrong. Alpha is always
+ // last in HTML color codes. We don't need alpha, so just strip it off.
+ return "#" + PanelColor.Color.ToString().Substring(3);
+ }
+
+ // Parse #RRGGBB and return a Color, or white if the string isn't in the correct format.
+ private static Color ParseColorString(string s)
+ {
+ // We only expect "#RRGGBB".
+ if(s.Length != 7 || !s.StartsWith("#"))
+ return Color.FromRgb(255,255,255);
+
+ try {
+ return (Color) ColorConverter.ConvertFromString(s);
+ }
+ catch(System.FormatException)
+ {
+ return Color.FromRgb(255,255,255);
+ }
+ }
+
+ Point MouseDownPosition;
+
+ protected override void OnMouseDown(MouseButtonEventArgs e)
+ {
+ MouseDownPosition = e.GetPosition(null);
+ base.OnMouseDown(e);
+ }
+
+ // Handle initiating drag.
+ protected override void OnMouseMove(MouseEventArgs e)
+ {
+ if(e.LeftButton == MouseButtonState.Pressed)
+ {
+ Point position = e.GetPosition(null);
+
+ // Why do we have to handle drag thresholding manually? This is the platform's job.
+ // If we don't do this, clicks won't work at all.
+ if (Math.Abs(position.X - MouseDownPosition.X) >= SystemParameters.MinimumHorizontalDragDistance ||
+ Math.Abs(position.Y - MouseDownPosition.Y) >= SystemParameters.MinimumVerticalDragDistance)
+ {
+ DragDrop.DoDragDrop(this, GetColorString(), DragDropEffects.Copy);
+ }
+ }
+
+ base.OnMouseMove(e);
+ }
+
+ private bool HandleDrop(DragEventArgs e)
+ {
+ PanelColorButton Button = e.Source as PanelColorButton;
+ if(Button == null)
+ return false;
+
+ // A color is being dropped from another button. Don't just update our color, since
+ // that will just change the button color and not actually apply it.
+ DataObject data = e.Data as DataObject;
+ if(data == null)
+ return false;
+
+ // Parse the color being dragged onto us.
+ Color color = ParseColorString(data.GetData(typeof(string)) as string);
+
+ // Update the panel color.
+ int PanelIndex = Panel % 9;
+ int Pad = Panel < 9? 0:1;
+ SMX.SMXConfig config;
+ if(!SMX.SMX.GetConfig(Pad, out config))
+ return false;
+
+ // Light colors are 8-bit values, but we only use values between 0-170. Higher values
+ // don't make the panel noticeably brighter, and just draw more power.
+ config.stepColor[PanelIndex*3+0] = ColorPicker.ScaleColor(color.R);
+ config.stepColor[PanelIndex*3+1] = ColorPicker.ScaleColor(color.G);
+ config.stepColor[PanelIndex*3+2] = ColorPicker.ScaleColor(color.B);
+
+ SMX.SMX.SetConfig(Pad, config);
+ CurrentSMXDevice.singleton.FireConfigurationChanged(this);
+ return true;
+ }
+
+ protected override void OnDrop(DragEventArgs e)
+ {
+ if(!HandleDrop(e))
+ base.OnDrop(e);
+ }
+ }
+
+ // This is a Slider class with some added helpers.
+ public class Slider2: Slider
+ {
+ public delegate void DragEvent();
+ public event DragEvent StartedDragging, StoppedDragging;
+
+ protected Thumb Thumb;
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ Track track = Template.FindName("PART_Track", this) as Track;
+ Thumb = track.Thumb;
+ }
+
+ // How are there no events for this?
+ protected override void OnThumbDragStarted(DragStartedEventArgs e)
+ {
+ base.OnThumbDragStarted(e);
+ StartedDragging?.Invoke();
+ }
+
+ protected override void OnThumbDragCompleted(DragCompletedEventArgs e)
+ {
+ base.OnThumbDragCompleted(e);
+ StoppedDragging?.Invoke();
+ }
+
+ public Slider2()
+ {
+ // Fix the slider not dragging after clicking outside the thumb.
+ // http://stackoverflow.com/a/30575638/136829
+ bool clickedInSlider = false;
+ MouseMove += delegate(object sender, MouseEventArgs args)
+ {
+ if(args.LeftButton == MouseButtonState.Released || !clickedInSlider || Thumb.IsDragging)
+ return;
+
+ Thumb.RaiseEvent(new MouseButtonEventArgs(args.MouseDevice, args.Timestamp, MouseButton.Left)
+ {
+ RoutedEvent = UIElement.MouseLeftButtonDownEvent,
+ Source = args.Source,
+ });
+ };
+
+ AddHandler(UIElement.PreviewMouseLeftButtonDownEvent, new RoutedEventHandler((sender, args) =>
+ {
+ clickedInSlider = true;
+ }), true);
+
+ AddHandler(UIElement.PreviewMouseLeftButtonUpEvent, new RoutedEventHandler((sender, args) =>
+ {
+ clickedInSlider = false;
+ }), true);
+ }
+ };
+
+ // This is the Slider inside a ColorPicker.
+ public class ColorPickerSlider: Slider2
+ {
+ public ColorPickerSlider()
+ {
+ }
+ };
+
+ public class ColorPicker: Control
+ {
+ // Which panel is currently selected:
+ public static readonly DependencyProperty SelectedPanelProperty = DependencyProperty.Register("SelectedPanel",
+ typeof(int), typeof(ColorPicker), new FrameworkPropertyMetadata(0));
+
+ public int SelectedPanel {
+ get { return (int) this.GetValue(SelectedPanelProperty); }
+ set { this.SetValue(SelectedPanelProperty, value); }
+ }
+
+ ColorPickerSlider HueSlider;
+ public delegate void Event();
+
+ public event Event StartedDragging, StoppedDragging;
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ HueSlider = GetTemplateChild("HueSlider") as ColorPickerSlider;
+ HueSlider.ValueChanged += delegate(object sender, RoutedPropertyChangedEventArgs e) {
+ SaveToConfig();
+ };
+
+ HueSlider.StartedDragging += delegate() { StartedDragging?.Invoke(); };
+ HueSlider.StoppedDragging += delegate() { StoppedDragging?.Invoke(); };
+
+ DoubleCollection ticks = new DoubleCollection();
+ // Add a tick at the minimum value, which is a negative value. This is the
+ // tick for white.
+ ticks.Add(HueSlider.Minimum);
+
+ // Add a tick for 0-359. Don't add 360, since that's the same as 0.
+ for(int i = 0; i < 360; ++i)
+ ticks.Add(i);
+ HueSlider.Ticks = ticks;
+
+ OnConfigChange onConfigChange;
+ onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
+ int pad = SelectedPanel < 9? 0:1;
+ LoadUIFromConfig(args.controller[pad].config);
+ });
+ }
+
+ // Light values are actually in the range 0-170 and not 0-255, since higher values aren't
+ // any brighter and just draw more power. The auto-lighting colors that we're configuring
+ // need to be scaled to this range too, but show full range colors in the UI.
+ readonly static double LightsScaleFactor = 0.666666f;
+ static public Byte ScaleColor(Byte c) { return (Byte) (c * LightsScaleFactor); }
+ static public Byte UnscaleColor(Byte c) { return (Byte) Math.Min(255, c / LightsScaleFactor); }
+
+ static public Color ScaleColor(Color c)
+ {
+ return Color.FromRgb(
+ ColorPicker.ScaleColor(c.R),
+ ColorPicker.ScaleColor(c.G),
+ ColorPicker.ScaleColor(c.B));
+ }
+
+ static public Color UnscaleColor(Color c)
+ {
+ return Color.FromRgb(
+ ColorPicker.UnscaleColor(c.R),
+ ColorPicker.UnscaleColor(c.G),
+ ColorPicker.UnscaleColor(c.B));
+ }
+
+ private void SaveToConfig()
+ {
+ if(UpdatingUI)
+ return;
+
+ // Apply the change and save it to the device.
+ int pad = SelectedPanel < 9? 0:1;
+ SMX.SMXConfig config;
+ if(!SMX.SMX.GetConfig(pad, out config))
+ return;
+
+ Color color = Helpers.FromHSV(HueSlider.Value, 1, 1);
+
+ // If we're set to the minimum value, use white instead.
+ if(HueSlider.Value == HueSlider.Minimum)
+ color = Color.FromRgb(255,255,255);
+
+ // Light colors are 8-bit values, but we only use values between 0-170. Higher values
+ // don't make the panel noticeably brighter, and just draw more power.
+ int PanelIndex = SelectedPanel % 9;
+ config.stepColor[PanelIndex*3+0] = ScaleColor(color.R);
+ config.stepColor[PanelIndex*3+1] = ScaleColor(color.G);
+ config.stepColor[PanelIndex*3+2] = ScaleColor(color.B);
+
+ SMX.SMX.SetConfig(pad, config);
+ CurrentSMXDevice.singleton.FireConfigurationChanged(this);
+ }
+
+ bool UpdatingUI = false;
+ private void LoadUIFromConfig(SMX.SMXConfig config)
+ {
+ // Make sure SaveToConfig doesn't treat these as the user changing values.
+ UpdatingUI = true;
+
+ // Reverse the scaling we applied in SaveToConfig.
+ int PanelIndex = SelectedPanel % 9;
+ Color rgb = Color.FromRgb(
+ UnscaleColor(config.stepColor[PanelIndex*3+0]),
+ UnscaleColor(config.stepColor[PanelIndex*3+1]),
+ UnscaleColor(config.stepColor[PanelIndex*3+2]));
+ double h, s, v;
+ Helpers.ToHSV(rgb, out h, out s, out v);
+
+ // Check for white. Since the conversion through LightsScaleFactor may not round trip
+ // back to exactly #FFFFFF, give some room for error in the value (brightness).
+ if(s <= 0.001 && v >= .90)
+ {
+ // This is white, so set it to the white block at the left edge of the slider.
+ HueSlider.Value = HueSlider.Minimum;
+ }
+ else
+ {
+ HueSlider.Value = h;
+ }
+
+ UpdatingUI = false;
+ }
+ };
+
+ // This widget selects which panels are enabled. We only show one of these for both pads.
+ class PanelSelector: Control
+ {
+ PanelButton[] EnabledPanelButtons;
+ OnConfigChange onConfigChange;
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ int[] PanelToIndex = new int[] {
+ 7, 8, 9,
+ 4, 5, 6,
+ 1, 2, 3,
+ };
+
+ EnabledPanelButtons = new PanelButton[9];
+ for(int i = 0; i < 9; ++i)
+ EnabledPanelButtons[i] = GetTemplateChild("EnablePanel" + PanelToIndex[i]) as PanelButton;
+
+ foreach(PanelButton button in EnabledPanelButtons)
+ button.Click += EnabledPanelButtonClicked;
+
+ onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
+ LoadUIFromConfig(args.controller[args.FirstController].config);
+ });
+ }
+
+ private void LoadUIFromConfig(SMX.SMXConfig config)
+ {
+ // The firmware configuration allows disabling each of the four sensors in a panel
+ // individually, but currently we only have a UI for toggling the whole sensor. Taking
+ // individual sensors out isn't recommended.
+ bool[] enabledPanels = {
+ (config.enabledSensors[0] & 0xF0) != 0,
+ (config.enabledSensors[0] & 0x0F) != 0,
+ (config.enabledSensors[1] & 0xF0) != 0,
+ (config.enabledSensors[1] & 0x0F) != 0,
+ (config.enabledSensors[2] & 0xF0) != 0,
+ (config.enabledSensors[2] & 0x0F) != 0,
+ (config.enabledSensors[3] & 0xF0) != 0,
+ (config.enabledSensors[3] & 0x0F) != 0,
+ (config.enabledSensors[4] & 0xF0) != 0,
+ };
+
+ for(int i = 0; i < 9; ++i)
+ EnabledPanelButtons[i].IsChecked = enabledPanels[i];
+ }
+
+ private int GetIndexFromButton(object sender)
+ {
+ for(int i = 0; i < 9; i++)
+ {
+ if(sender == EnabledPanelButtons[i])
+ return i;
+ }
+
+ return 0;
+ }
+
+ private void EnabledPanelButtonClicked(object sender, EventArgs e)
+ {
+ // One of the panel buttons on the panel toggle UI was clicked. Toggle the
+ // panel.
+ int button = GetIndexFromButton(sender);
+ Console.WriteLine("Clicked " + button);
+
+ // Set the enabled sensor mask on both pads to the state of the UI.
+ for(int pad = 0; pad < 2; ++pad)
+ {
+ SMX.SMXConfig config;
+ if(!SMX.SMX.GetConfig(pad, out config))
+ continue;
+
+ // This could be done algorithmically, but this is clearer.
+ int[] PanelButtonToSensorIndex = {
+ 0, 0, 1, 1, 2, 2, 3, 3, 4
+ };
+ byte[] PanelButtonToSensorMask = {
+ 0xF0, 0x0F,
+ 0xF0, 0x0F,
+ 0xF0, 0x0F,
+ 0xF0, 0x0F,
+ 0xF0,
+ };
+ for(int i = 0; i < 5; ++i)
+ config.enabledSensors[i] = 0;
+
+ for(int Panel = 0; Panel < 9; ++Panel)
+ {
+ int index = PanelButtonToSensorIndex[Panel];
+ byte mask = PanelButtonToSensorMask[Panel];
+ if(EnabledPanelButtons[Panel].IsChecked == true)
+ config.enabledSensors[index] |= (byte) mask;
+ }
+
+ SMX.SMX.SetConfig(pad, config);
+ }
+ }
+ };
+
+ public class FrameImage: Image
+ {
+ // The source image. Changing this after load isn't supported.
+ public static readonly DependencyProperty ImageProperty = DependencyProperty.Register("Image",
+ typeof(BitmapSource), typeof(FrameImage), new FrameworkPropertyMetadata(null, ImageChangedCallback));
+
+ public BitmapSource Image {
+ get { return (BitmapSource) this.GetValue(ImageProperty); }
+ set { this.SetValue(ImageProperty, value); }
+ }
+
+ // Which frame is currently displayed:
+ public static readonly DependencyProperty FrameProperty = DependencyProperty.Register("Frame",
+ typeof(int), typeof(FrameImage), new FrameworkPropertyMetadata(0, FrameChangedCallback));
+
+ public int Frame {
+ get { return (int) this.GetValue(FrameProperty); }
+ set { this.SetValue(FrameProperty, value); }
+ }
+
+ public static readonly DependencyProperty FramesXProperty = DependencyProperty.Register("FramesX",
+ typeof(int), typeof(FrameImage), new FrameworkPropertyMetadata(0, ImageChangedCallback));
+
+ public int FramesX {
+ get { return (int) this.GetValue(FramesXProperty); }
+ set { this.SetValue(FramesXProperty, value); }
+ }
+
+ private static void ImageChangedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args)
+ {
+ FrameImage self = target as FrameImage;
+ self.Load();
+ }
+
+ private static void FrameChangedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args)
+ {
+ FrameImage self = target as FrameImage;
+ self.Refresh();
+ }
+
+ private BitmapSource[] ImageFrames;
+
+ private void Load()
+ {
+ if(Image == null || FramesX == 0)
+ {
+ ImageFrames = null;
+ return;
+ }
+
+ // Split the image into frames.
+ int FrameWidth = Image.PixelWidth / FramesX;
+ int FrameHeight = Image.PixelHeight;
+ ImageFrames = new BitmapSource[FramesX];
+ for(int i = 0; i < FramesX; ++i)
+ ImageFrames[i] = new CroppedBitmap(Image, new Int32Rect(FrameWidth*i, 0, FrameWidth, FrameHeight));
+
+ Refresh();
+ }
+
+ private void Refresh()
+ {
+ if(ImageFrames == null || Frame >= ImageFrames.Length)
+ {
+ this.Source = null;
+ return;
+ }
+
+ this.Source = ImageFrames[Frame];
+ }
+ };
+}
diff --git a/smx-config/installer/.gitignore b/smx-config/installer/.gitignore
new file mode 100644
index 0000000..7d98506
--- /dev/null
+++ b/smx-config/installer/.gitignore
@@ -0,0 +1 @@
+InstallSMXConfig.exe
diff --git a/smx-config/installer/SMX.nsi b/smx-config/installer/SMX.nsi
new file mode 100644
index 0000000..d195dcf
--- /dev/null
+++ b/smx-config/installer/SMX.nsi
@@ -0,0 +1,106 @@
+!include "MUI2.nsh"
+
+Name "StepManiaX Platform"
+OutFile "SMXConfigInstaller.exe"
+
+!define MUI_ICON "..\window icon.ico"
+
+InstallDir "$PROGRAMFILES32\SMXConfig"
+!insertmacro MUI_UNPAGE_CONFIRM
+!insertmacro MUI_UNPAGE_INSTFILES
+;!insertmacro MUI_PAGE_DIRECTORY
+
+Function InstallNetRuntime
+ # Check if .NET 4.5.2 is installed. https://msdn.microsoft.com/en-us/library/hh925568(v=vs.110).aspx
+ Var /Global Net4Version
+ ReadRegDWORD $Net4Version HKLM "SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" "Release"
+ IntCmp $Net4Version 379893 already_installed do_install already_installed
+
+ already_installed:
+ DetailPrint ".NET runtime 4.5.2 already installed."
+ return
+
+ do_install:
+
+ # Download the runtime.
+ NSISdl::download "https://go.microsoft.com/fwlink/?LinkId=397708" "$TEMP\NET452Installer.exe"
+ Var /GLOBAL download_result
+ Pop $download_result
+ DetailPrint "$download_result"
+ StrCmp $download_result success download_successful
+
+ MessageBox MB_OK|MB_ICONEXCLAMATION "The .NET 4.5.2 runtime couldn't be downloaded."
+ return
+
+ download_successful:
+
+ # Run the installer.
+ # We can run this without opening the install dialog like this, but this runtime can take a
+ # while to install and it makes it look like the installation has stalled.
+ # ExecWait '"$TEMP\NET452Installer.exe" /q /norestart /c:"install /q"'
+ ExecWait '"$TEMP\NET452Installer.exe" /passive /norestart /c:"install"'
+FunctionEnd
+
+Function InstallMSVCRuntime
+ # Check if the runtime is already installed.
+ Var /Global MSVCVersion1
+ Var /Global MSVCVersion2
+ GetDLLVersion "$sysdir\MSVCP140.dll" $MSVCVersion1 $MSVCVersion2
+ StrCmp $MSVCVersion1 "" do_install
+
+ DetailPrint "MSVC runtime already installed."
+ return
+
+ do_install:
+
+ DetailPrint "Installing MSVC runtime"
+
+ # Download the runtime.
+ NSISdl::download "https://go.microsoft.com/fwlink/?LinkId=615459" "$TEMP\vcredist_x86.exe"
+ Var /GLOBAL download_result_2
+ Pop $download_result_2
+ DetailPrint "$download_result_2"
+ StrCmp $download_result_2 success download_successful
+
+ MessageBox MB_OK|MB_ICONEXCLAMATION "The MSVC runtime couldn't be downloaded."
+ return
+
+ download_successful:
+
+ # Run the installer.
+ ExecWait '"$TEMP\vcredist_x86.exe" /passive /norestart'
+FunctionEnd
+
+Page directory
+Page instfiles
+
+Section
+ Call InstallNetRuntime
+ Call InstallMSVCRuntime
+
+ SetOutPath $INSTDIR
+ File "..\..\out\SMX.dll"
+ File "..\..\out\SMXConfig.exe"
+
+ CreateShortCut "$SMPROGRAMS\StepManiaX Platform.lnk" "$INSTDIR\SMXConfig.exe"
+ WriteUninstaller $INSTDIR\uninstall.exe
+
+ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\StepManiaX Platform" \
+ "DisplayName" "StepManiaX Platform"
+ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\StepManiaX Platform" \
+ "Publisher" "Step Revolution"
+ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\StepManiaX Platform" \
+ "DisplayIcon" "$INSTDIR\SMXConfig.exe"
+ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\StepManiaX Platform" \
+ "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+SectionEnd
+
+Section "Uninstall"
+ Delete $INSTDIR\SMX.dll
+ Delete $INSTDIR\SMXConfig.exe
+ Delete $INSTDIR\uninstall.exe
+ rmdir $INSTDIR
+ Delete "$SMPROGRAMS\StepManiaX Platform.lnk"
+ DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\StepManiaX Platform"
+SectionEnd
+
diff --git a/smx-config/installer/build.bat b/smx-config/installer/build.bat
new file mode 100644
index 0000000..4212da1
--- /dev/null
+++ b/smx-config/installer/build.bat
@@ -0,0 +1,3 @@
+@echo off
+"C:\Program Files (x86)\NSIS\makensis.exe" SMX.nsi
+pause
diff --git a/smx-config/window icon.ico b/smx-config/window icon.ico
new file mode 100644
index 0000000..febd6de
Binary files /dev/null and b/smx-config/window icon.ico differ