Import for release.

master
Glenn Maynard 7 years ago
parent 364b82e27c
commit 57379383c0
  1. 8
      .editorconfig
  2. 5
      .gitignore
  3. 22
      LICENSE.txt
  4. 5
      README.md
  5. 37
      SMX.sln
  6. BIN
      docs/icon.png
  7. 157
      docs/index.html
  8. BIN
      docs/logo.png
  9. 26
      docs/style.css
  10. 96
      sample/SMXSample.cpp
  11. 103
      sample/SMXSample.vcxproj
  12. 14
      sample/SMXSample.vcxproj.filters
  13. 244
      sdk/SMX.h
  14. 285
      sdk/Windows/Helpers.cpp
  15. 104
      sdk/Windows/Helpers.h
  16. 65
      sdk/Windows/SMX.cpp
  17. 132
      sdk/Windows/SMX.vcxproj
  18. 61
      sdk/Windows/SMX.vcxproj.filters
  19. 494
      sdk/Windows/SMXDevice.cpp
  20. 132
      sdk/Windows/SMXDevice.h
  21. 378
      sdk/Windows/SMXDeviceConnection.cpp
  22. 122
      sdk/Windows/SMXDeviceConnection.h
  23. 155
      sdk/Windows/SMXDeviceSearch.cpp
  24. 33
      sdk/Windows/SMXDeviceSearch.h
  25. 97
      sdk/Windows/SMXDeviceSearchThreaded.cpp
  26. 46
      sdk/Windows/SMXDeviceSearchThreaded.h
  27. 76
      sdk/Windows/SMXHelperThread.cpp
  28. 46
      sdk/Windows/SMXHelperThread.h
  29. 452
      sdk/Windows/SMXManager.cpp
  30. 76
      sdk/Windows/SMXManager.h
  31. 1
      smx-config/.gitignore
  32. 6
      smx-config/App.config
  33. 9
      smx-config/App.xaml
  34. 32
      smx-config/App.xaml.cs
  35. 137
      smx-config/ConfigPresets.cs
  36. 281
      smx-config/CurrentSMXDevice.cs
  37. 289
      smx-config/DiagnosticsWidgets.cs
  38. 203
      smx-config/DoubleSlider.cs
  39. 204
      smx-config/Helpers.cs
  40. 798
      smx-config/MainWindow.xaml
  41. 86
      smx-config/MainWindow.xaml.cs
  42. 55
      smx-config/Properties/AssemblyInfo.cs
  43. 63
      smx-config/Properties/Resources.Designer.cs
  44. 120
      smx-config/Properties/Resources.resx
  45. 30
      smx-config/Properties/Settings.Designer.cs
  46. 7
      smx-config/Properties/Settings.settings
  47. BIN
      smx-config/Resources/DIP.png
  48. BIN
      smx-config/Resources/pad_cardinal.png
  49. BIN
      smx-config/Resources/pad_center.png
  50. BIN
      smx-config/Resources/pad_diagonal.png
  51. BIN
      smx-config/Resources/pad_down.png
  52. BIN
      smx-config/Resources/pad_down_left.png
  53. BIN
      smx-config/Resources/pad_down_right.png
  54. BIN
      smx-config/Resources/pad_left.png
  55. BIN
      smx-config/Resources/pad_right.png
  56. BIN
      smx-config/Resources/pad_up.png
  57. BIN
      smx-config/Resources/pad_up_left.png
  58. BIN
      smx-config/Resources/pad_up_right.png
  59. BIN
      smx-config/Resources/window icon.ico
  60. BIN
      smx-config/Resources/window icon.png
  61. 296
      smx-config/SMX.cs
  62. 200
      smx-config/SMXConfig.csproj
  63. 921
      smx-config/Widgets.cs
  64. 1
      smx-config/installer/.gitignore
  65. 106
      smx-config/installer/SMX.nsi
  66. 3
      smx-config/installer/build.bat
  67. BIN
      smx-config/window icon.ico

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

5
.gitignore vendored

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

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

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

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@ -0,0 +1,157 @@
<html>
<link rel=stylesheet href=style.css>
<link rel="icon" href="icon.png" type="image/png">
<img src=logo.png style="width: 80%; display: block; margin-left: auto; margin-right: auto;">
<h2>Introduction to the StepManiaX SDK</h2>
The StepManiaX SDK supports C++ development for the <a href=https://stepmaniax.com/>StepManiaX dance platform</a>.
<h2>Usage</h2>
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 <code>SMX.h</code>.
<p>
See <code>sample</code> for a sample application.
<p>
Up to two controllers are supported. <code>SMX_GetInfo</code> can be used to check which
controllers are connected. Each <code>pad</code> argument to API calls can be 0 for the
player 1 pad, or 1 for the player 2 pad.
<h2>HID support</h2>
The platform can be used as a regular USB HID input device, which works in any game
that supports input remapping.
<p>
However, applications using this SDK to control the panels directly should ignore the
HID interface, and instead use <code>SMX_GetInputState</code> to retrieve the input state.
<h2>Platform lights</h2>
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.
<p>
See <code>SMX_SetLights</code>.
<h2>Platform configuration</h2>
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.
<p>
<ul>
<li>
<b>enabledSensors</b>
<p>
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.
<p>
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.
<p>
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.
</li>
</ul>
<h2>Reference</h2>
<h3 class=ref>void SMX_Start(SMXUpdateCallback UpdateCallback, void *pUser);</h3>
Initialize, and start searching for devices.
<p>
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.
<p>
This is called asynchronously from a helper thread, so the receiver must be thread-safe.
<h3 class=ref>void SMX_Stop();</h3>
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.
<h3 class=ref>void SMX_SetLogCallback(SMXLogCallback callback);</h3>
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.
<h3 class=ref>void SMX_GetInfo(int pad, SMXInfo *info);</h3>
Get info about a pad. Use this to detect which pads are currently connected.
<h3 class=ref>uint16_t SMX_GetInputState(int pad);</h3>
Get a mask of the currently pressed panels.
<h3 class=ref>void SMX_SetLights(const char lightsData[864]);</h3>
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:
<p>
<pre>
0123
4567
89AB
CDEF
</pre>
<p>
Panels are in the following order:
<p>
<pre>
012 9AB
345 CDE
678 F01
</pre>
With 18 panels, 16 LEDs per panel and 3 bytes per LED, each light update has 864 bytes of data.
<p>
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.
<p>
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.
<h3 class=ref>void SMX_ReenableAutoLights();</h3>
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.
<p>
<code>SMX_ReenableAutoLights</code> 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.
<h3 class=ref>void SMX_GetConfig(int pad, SMXConfig *config);</h3>
Get the current controller's configuration.
<h3 class=ref>void SMX_SetConfig(int pad, const SMXConfig *config);</h3>
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.
<h3 class=ref>void SMX_FactoryReset(int pad);</h3>
Reset a pad to its original configuration.
<h3 class=ref>void SMX_ForceRecalibration(int pad);</h3>
Request an immediate panel recalibration. This is normally not necessary, but can be helpful
for diagnostics.
<h3 class=ref>
void SMX_SetTestMode(int pad, SensorTestMode mode);
<br>
bool SMX_GetTestData(int pad, SMXSensorTestModeData *data);
</h3>
Set a panel test mode and request test data. This is used by the configuration tool.

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

@ -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;
}

@ -0,0 +1,96 @@
#include <stdio.h>
#include <windows.h>
#include "SMX.h"
#include <memory>
#include <string>
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;
}

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{8861D665-FD49-4EFD-92C3-F4B8548AFD23}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>SMXSample</RootNamespace>
<WindowsTargetPlatformVersion>10.0.16299.0</WindowsTargetPlatformVersion>
<ProjectName>SMXSample</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<WholeProgramOptimization>false</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
<OutDir>$(TargetDir)../out/</OutDir>
<IntDir>$(SolutionDir)/build/$(ProjectName)/$(Configuration)/</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(TargetDir)../out/</OutDir>
<IntDir>$(SolutionDir)/build/$(ProjectName)/$(Configuration)/</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level4</WarningLevel>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<DisableSpecificWarnings>4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<AdditionalIncludeDirectories>..\sdk</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(SolutionDir)/out/$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<PrecompiledHeader>
</PrecompiledHeader>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<DisableSpecificWarnings>4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<AdditionalIncludeDirectories>..\sdk</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(SolutionDir)/out/$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="SMXSample.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\sdk\Windows\SMX.vcxproj">
<Project>{c5fc0823-9896-4b7c-bfe1-b60db671a462}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="SMXSample.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

@ -0,0 +1,244 @@
#ifndef SMX_H
#define SMX_H
#include <stdint.h>
#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

@ -0,0 +1,285 @@
#include "Helpers.h"
#include <windows.h>
#include <algorithm>
using namespace std;
using namespace SMX;
namespace {
function<void(const string &log)> g_LogCallback = [](const string &log) {
printf("%6.3f: %s\n", GetMonotonicTime(), log.c_str());
};
};
void SMX::Log(string s)
{
g_LogCallback(s);
}
void SMX::SetLogCallback(function<void(const string &log)> callback)
{
g_LogCallback = callback;
}
const DWORD MS_VC_EXCEPTION = 0x406D1388;
#pragma pack(push,8)
typedef struct tagTHREADNAME_INFO
{
DWORD dwType; // Must be 0x1000.
LPCSTR szName; // Pointer to name (in user addr space).
DWORD dwThreadID; // Thread ID (-1=caller thread).
DWORD dwFlags; // Reserved for future use, must be zero.
} THREADNAME_INFO;
#pragma pack(pop)
void SMX::SetThreadName(DWORD iThreadId, const string &name)
{
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; i<iNumBytes; i++)
{
unsigned val = pData[i];
s += ssprintf("%02x", val);
}
return s;
}
string SMX::BinaryToHex(const string &sString)
{
return BinaryToHex(sString.data(), sString.size());
}
bool SMX::GetRandomBytes(void *pData, int iBytes)
{
HCRYPTPROV hCryptProvider = 0;
if (!CryptAcquireContext(&hCryptProvider, NULL, MS_DEF_PROV, PROV_RSA_FULL, (CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET)) &&
!CryptAcquireContext(&hCryptProvider, NULL, MS_DEF_PROV, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET | CRYPT_NEWKEYSET))
return 0;
bool bSuccess = !!CryptGenRandom(hCryptProvider, iBytes, (uint8_t *) pData);
CryptReleaseContext(hCryptProvider, 0);
return bSuccess;
}
// Monotonic timer code from https://stackoverflow.com/questions/24330496.
// Why is this hard?
//
// This code has backwards compatibility to XP, but we only officially support and
// test back to Windows 7, so that code path isn't tested.
typedef struct _KSYSTEM_TIME {
ULONG LowPart;
LONG High1Time;
LONG High2Time;
} KSYSTEM_TIME;
#define KUSER_SHARED_DATA 0x7ffe0000
#define InterruptTime ((KSYSTEM_TIME volatile*)(KUSER_SHARED_DATA + 0x08))
#define InterruptTimeBias ((ULONGLONG volatile*)(KUSER_SHARED_DATA + 0x3b0))
namespace {
LONGLONG ReadInterruptTime()
{
// Reading the InterruptTime from KUSER_SHARED_DATA is much better than
// using GetTickCount() because it doesn't wrap, and is even a little quicker.
// This works on all Windows NT versions (NT4 and up).
LONG timeHigh;
ULONG timeLow;
do {
timeHigh = InterruptTime->High1Time;
timeLow = InterruptTime->LowPart;
} while (timeHigh != InterruptTime->High2Time);
LONGLONG now = ((LONGLONG)timeHigh << 32) + timeLow;
static LONGLONG d = now;
return now - d;
}
LONGLONG ScaleQpc(LONGLONG qpc)
{
// We do the actual scaling in fixed-point rather than floating, to make sure
// that we don't violate monotonicity due to rounding errors. There's no
// need to cache QueryPerformanceFrequency().
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
double fraction = 10000000/double(frequency.QuadPart);
LONGLONG denom = 1024;
LONGLONG numer = max(1LL, (LONGLONG)(fraction*denom + 0.5));
return qpc * numer / denom;
}
ULONGLONG ReadUnbiasedQpc()
{
// We remove the suspend bias added to QueryPerformanceCounter results by
// subtracting the interrupt time bias, which is not strictly speaking legal,
// but the units are correct and I think it's impossible for the resulting
// "unbiased QPC" value to go backwards.
LONGLONG interruptTimeBias, qpc;
do {
interruptTimeBias = *InterruptTimeBias;
LARGE_INTEGER counter;
QueryPerformanceCounter(&counter);
qpc = counter.QuadPart;
} while (interruptTimeBias != *InterruptTimeBias);
static std::pair<LONGLONG,LONGLONG> d(qpc, interruptTimeBias);
return ScaleQpc(qpc - d.first) - (interruptTimeBias - d.second);
}
bool Win7OrLater()
{
static int iWin7OrLater = -1;
if(iWin7OrLater != -1)
return bool(iWin7OrLater);
OSVERSIONINFOW ver = { 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);
}

@ -0,0 +1,104 @@
#ifndef HELPERS_H
#define HELPERS_H
#include <string>
#include <stdarg.h>
#include <windows.h>
#include <functional>
#include <memory>
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<void(const string &log)> callback);
void SetThreadName(DWORD iThreadId, const string &name);
void StripCrnl(wstring &s);
wstring GetErrorString(int err);
string vssprintf(const char *szFormat, va_list argList);
string ssprintf(const char *fmt, ...);
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<Class> &pSelf): m_pSelf(GetPointers(pSelf, this)) { }
// const weak_ptr<Class> m_pSelf;
// };
//
// shared_ptr<Class> obj;
// new Class(obj);
//
// For a more convenient way to invoke this, see CreateObj() below.
template<typename T>
weak_ptr<T> GetPointers(shared_ptr<T> &pSharedPtr, T *pObj)
{
pSharedPtr.reset(pObj);
return pSharedPtr;
}
// Create a class that retains a weak reference to itself, returning a shared_ptr.
template<typename T, class... Args>
shared_ptr<T> CreateObj(Args&&... args)
{
shared_ptr<typename T> pResult;
new T(pResult, std::forward<Args>(args)...);
return dynamic_pointer_cast<T>(pResult);
}
class AutoCloseHandle
{
public:
AutoCloseHandle(HANDLE h);
~AutoCloseHandle();
HANDLE value() const { return handle; }
private:
AutoCloseHandle(const AutoCloseHandle &rhs);
AutoCloseHandle &operator=(const AutoCloseHandle &rhs);
HANDLE handle;
};
class Mutex
{
public:
Mutex();
~Mutex();
void Lock();
void Unlock();
void AssertNotLockedByCurrentThread();
void AssertLockedByCurrentThread();
private:
HANDLE m_hLock = INVALID_HANDLE_VALUE;
DWORD m_iLockedByThread = 0;
};
// A local lock helper for Mutex.
class LockMutex
{
public:
LockMutex(Mutex &mutex);
~LockMutex();
private:
Mutex &m_Mutex;
};
}
#endif

@ -0,0 +1,65 @@
// This implements the public API.
#include <windows.h>
#include <memory>
#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<SMXManager> 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<SMXManager>(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(); }

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\SMX.h" />
<ClInclude Include="Helpers.h" />
<ClInclude Include="SMXDevice.h" />
<ClInclude Include="SMXDeviceConnection.h" />
<ClInclude Include="SMXDeviceSearch.h" />
<ClInclude Include="SMXDeviceSearchThreaded.h" />
<ClInclude Include="SMXHelperThread.h" />
<ClInclude Include="SMXManager.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Helpers.cpp" />
<ClCompile Include="SMX.cpp" />
<ClCompile Include="SMXDevice.cpp" />
<ClCompile Include="SMXDeviceConnection.cpp" />
<ClCompile Include="SMXDeviceSearch.cpp" />
<ClCompile Include="SMXDeviceSearchThreaded.cpp" />
<ClCompile Include="SMXHelperThread.cpp" />
<ClCompile Include="SMXManager.cpp" />
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{C5FC0823-9896-4B7C-BFE1-B60DB671A462}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>SMX</RootNamespace>
<WindowsTargetPlatformVersion>10.0.16299.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<WholeProgramOptimization>false</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(SolutionDir)out\</OutDir>
<IntDir>$(SolutionDir)/build/$(ProjectName)/$(Configuration)/</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(SolutionDir)out\</OutDir>
<IntDir>$(SolutionDir)/build/$(ProjectName)/$(Configuration)/</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level4</WarningLevel>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;SMX_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MinimalRebuild>false</MinimalRebuild>
<DisableSpecificWarnings>4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<RandomizedBaseAddress>false</RandomizedBaseAddress>
<AdditionalDependencies>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)</AdditionalDependencies>
<OutputFile>$(SolutionDir)/out/$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<PrecompiledHeader>
</PrecompiledHeader>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;SMX_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<WholeProgramOptimization>false</WholeProgramOptimization>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<DisableSpecificWarnings>4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings)</DisableSpecificWarnings>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<RandomizedBaseAddress>false</RandomizedBaseAddress>
<AdditionalDependencies>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)</AdditionalDependencies>
<OutputFile>$(SolutionDir)/out/$(TargetName)$(TargetExt)</OutputFile>
</Link>
<CustomBuildStep>
<Command>
</Command>
</CustomBuildStep>
<CustomBuildStep>
<Message>
</Message>
</CustomBuildStep>
<CustomBuildStep>
<Inputs>
</Inputs>
</CustomBuildStep>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\SMX.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXDevice.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXDeviceConnection.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXDeviceSearch.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXDeviceSearchThreaded.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXHelperThread.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXManager.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="Helpers.h">
<Filter>Source Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="SMX.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXDevice.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXDeviceConnection.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXDeviceSearch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXDeviceSearchThreaded.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXHelperThread.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Helpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

@ -0,0 +1,494 @@
#include "SMXDevice.h"
#include "../SMX.h"
#include "Helpers.h"
#include "SMXDeviceConnection.h"
#include "SMXDeviceSearch.h"
#include <windows.h>
#include <memory>
#include <vector>
#include <map>
using namespace std;
using namespace SMX;
// Extract test data for panel iPanel.
static void ReadDataForPanel(const vector<uint16_t> &data, int iPanel, void *pOut, int iOutSize)
{
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<SMXDevice> SMX::SMXDevice::Create(shared_ptr<AutoCloseHandle> hEvent, Mutex &lock)
{
return CreateObj<SMXDevice>(hEvent, lock);
}
SMX::SMXDevice::SMXDevice(shared_ptr<SMXDevice> &pSelf, shared_ptr<AutoCloseHandle> hEvent, Mutex &lock):
m_pSelf(GetPointers(pSelf, this)),
m_hEvent(hEvent),
m_Lock(lock)
{
m_pConnection = SMXDeviceConnection::Create();
}
SMX::SMXDevice::~SMXDevice()
{
}
bool SMX::SMXDevice::OpenDeviceHandle(shared_ptr<AutoCloseHandle> pHandle, wstring &sError)
{
m_Lock.AssertLockedByCurrentThread();
return m_pConnection->Open(pHandle, sError);
}
void SMX::SMXDevice::CloseDevice()
{
m_Lock.AssertLockedByCurrentThread();
m_pConnection->Close();
m_bHaveConfig = false;
m_bSendConfig = false;
CallUpdateCallback(SMXUpdateCallback_Updated);
}
shared_ptr<AutoCloseHandle> SMX::SMXDevice::GetDeviceHandle() const
{
return m_pConnection->GetDeviceHandle();
}
void SMX::SMXDevice::SetUpdateCallback(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback)
{
LockMutex Lock(m_Lock);
m_pUpdateCallback = pCallback;
}
bool SMX::SMXDevice::IsConnected() const
{
m_Lock.AssertNotLockedByCurrentThread();
// Don't expose the device as connected until we've read the current configuration.
LockMutex Lock(m_Lock);
return IsConnectedLocked();
}
bool SMX::SMXDevice::IsConnectedLocked() const
{
m_Lock.AssertLockedByCurrentThread();
return m_pConnection->IsConnectedWithDeviceInfo() && m_bHaveConfig;
}
void SMX::SMXDevice::SendCommand(string cmd, function<void()> pComplete)
{
LockMutex Lock(m_Lock);
SendCommandLocked(cmd, pComplete);
}
void SMX::SMXDevice::SendCommandLocked(string cmd, function<void()> 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<uint16_t> 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);
}

@ -0,0 +1,132 @@
#ifndef SMXDevice_h
#define SMXDevice_h
#include <windows.h>
#include <memory>
#include <functional>
using namespace std;
#include "Helpers.h"
#include "../SMX.h"
namespace SMX
{
class SMXDeviceConnection;
// The high-level interface to a single controller. This is managed by SMXManager, and uses SMXDeviceConnection
// for low-level USB communication.
class SMXDevice
{
public:
// Create an SMXDevice.
//
// lock is our serialization mutex. This is shared across SMXManager and all SMXDevices.
//
// hEvent is signalled when we have new packets to be sent, to wake the communications thread. The
// device handle opened with OpenPort must also be monitored, to check when packets have been received
// (or successfully sent).
static shared_ptr<SMXDevice> Create(shared_ptr<SMX::AutoCloseHandle> hEvent, SMX::Mutex &lock);
SMXDevice(shared_ptr<SMXDevice> &pSelf, shared_ptr<SMX::AutoCloseHandle> hEvent, SMX::Mutex &lock);
~SMXDevice();
bool OpenDeviceHandle(shared_ptr<SMX::AutoCloseHandle> pHandle, wstring &sError);
void CloseDevice();
shared_ptr<SMX::AutoCloseHandle> GetDeviceHandle() const;
// Set a function to be called when something changes on the device. This allows efficiently
// detecting when a panel is pressed or other changes happen on the device.
void SetUpdateCallback(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback);
// Return true if we're connected.
bool IsConnected() const;
// Send a raw command.
void SendCommand(string sCmd, function<void()> pComplete=nullptr);
void SendCommandLocked(string sCmd, function<void()> 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<SMX::AutoCloseHandle> m_hEvent;
SMX::Mutex &m_Lock;
function<void(int PadNumber, SMXUpdateCallbackReason reason)> m_pUpdateCallback;
weak_ptr<SMXDevice> m_pSelf;
shared_ptr<SMXDeviceConnection> m_pConnection;
// The configuration we've read from the device. m_bHaveConfig is true if we've received
// a configuration from the device since we've connected to it.
SMXConfig config;
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

@ -0,0 +1,378 @@
#include "SMXDeviceConnection.h"
#include "Helpers.h"
#include <string>
#include <memory>
using namespace std;
using namespace SMX;
#include <hidsdi.h>
#include <SetupAPI.h>
SMX::SMXDeviceConnection::PendingCommandPacket::PendingCommandPacket()
{
memset(&m_OverlappedWrite, 0, sizeof(m_OverlappedWrite));
}
shared_ptr<SMX::SMXDeviceConnection> SMXDeviceConnection::Create()
{
return CreateObj<SMXDeviceConnection>();
}
SMX::SMXDeviceConnection::SMXDeviceConnection(shared_ptr<SMXDeviceConnection> &pSelf):
m_pSelf(GetPointers(pSelf, this))
{
memset(&overlapped_read, 0, sizeof(overlapped_read));
}
SMX::SMXDeviceConnection::~SMXDeviceConnection()
{
Close();
}
bool SMX::SMXDeviceConnection::Open(shared_ptr<AutoCloseHandle> DeviceHandle, wstring &sError)
{
m_hDevice = DeviceHandle;
if(!HidD_SetNumInputBuffers(DeviceHandle->value(), 512))
Log(ssprintf("Error: HidD_SetNumInputBuffers: %ls", GetErrorString(GetLastError()).c_str()));
// Begin the first async read.
BeginAsyncRead(sError);
// Request device info.
RequestDeviceInfo([&] {
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<PendingCommandPacket> 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<PendingCommand> pPendingCommand = m_aPendingCommands.front();
for(shared_ptr<PendingCommandPacket> &pPacket: pPendingCommand->m_Packets)
{
// In theory the API allows this to return success if the write completed successfully without needing to
// be async, like reads can. However, this can't really happen (the write always needs to go to the device
// first, unlike reads which might already be buffered), and there's no way to test it if we implement that,
// so this assumes all writes are async.
DWORD unused;
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<void()> pComplete)
{
shared_ptr<PendingCommand> pPendingCommand = make_shared<PendingCommand>();
pPendingCommand->m_pComplete = pComplete;
pPendingCommand->m_bIsDeviceInfoCommand = true;
shared_ptr<PendingCommandPacket> pCommandPacket = make_shared<PendingCommandPacket>();
string sPacket({
5, // report ID
(char) (uint8_t) PACKET_FLAG_DEVICE_INFO, // flags
(char) 0, // bytes in packet
});
sPacket.resize(64, 0);
pCommandPacket->sData = sPacket;
pPendingCommand->m_Packets.push_back(pCommandPacket);
m_aPendingCommands.push_back(pPendingCommand);
}
void SMX::SMXDeviceConnection::SendCommand(const string &cmd, function<void()> pComplete)
{
shared_ptr<PendingCommand> pPendingCommand = make_shared<PendingCommand>();
pPendingCommand->m_pComplete = pComplete;
// Send the command in packets. We allow sending zero-length packets here
// for testing purposes.
int i = 0;
do {
shared_ptr<PendingCommandPacket> pCommandPacket = make_shared<PendingCommandPacket>();
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);
}

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

@ -0,0 +1,155 @@
#include "SMXDeviceSearch.h"
#include "SMXDeviceConnection.h"
#include "Helpers.h"
#include <string>
#include <memory>
#include <set>
using namespace std;
using namespace SMX;
#include <hidsdi.h>
#include <SetupAPI.h>
// Return all USB HID device paths. This doesn't open the device to filter just our devices.
static set<wstring> GetAllHIDDevicePaths(wstring &error)
{
HDEVINFO DeviceInfoSet = NULL;
GUID HidGuid;
HidD_GetHidGuid(&HidGuid);
DeviceInfoSet = SetupDiGetClassDevs(&HidGuid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if(DeviceInfoSet == NULL)
return {};
set<wstring> paths;
SP_DEVICE_INTERFACE_DATA DeviceInterfaceData;
DeviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for(DWORD iIndex = 0;
SetupDiEnumDeviceInterfaces(DeviceInfoSet, NULL, &HidGuid, iIndex, &DeviceInterfaceData);
iIndex++)
{
DWORD iSize;
if(!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<AutoCloseHandle> 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<AutoCloseHandle>(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<shared_ptr<AutoCloseHandle>> SMX::SMXDeviceSearch::GetDevices(wstring &error)
{
set<wstring> aDevicePaths = GetAllHIDDevicePaths(error);
// Remove any entries in m_Devices that are no longer in the list.
for(wstring sPath: m_setLastDevicePaths)
{
if(aDevicePaths.find(sPath) != aDevicePaths.end())
continue;
Log(ssprintf("Device removed: %ls", sPath.c_str()));
m_Devices.erase(sPath);
}
// Check for new entries.
for(wstring sPath: aDevicePaths)
{
// Only look at devices that weren't in the list last time. OpenUSBDevice has
// to open the device and causes requests to be sent to it.
if(m_setLastDevicePaths.find(sPath) != m_setLastDevicePaths.end())
continue;
// This will return NULL if this isn't our device.
shared_ptr<AutoCloseHandle> hDevice = OpenUSBDevice(sPath.c_str(), error);
if(hDevice == nullptr)
continue;
Log(ssprintf("Device added: %ls", sPath.c_str()));
m_Devices[sPath] = hDevice;
}
m_setLastDevicePaths = aDevicePaths;
vector<shared_ptr<AutoCloseHandle>> aDevices;
for(auto it: m_Devices)
aDevices.push_back(it.second);
return aDevices;
}
void SMX::SMXDeviceSearch::DeviceWasClosed(shared_ptr<AutoCloseHandle> pDevice)
{
map<wstring, shared_ptr<AutoCloseHandle>> aDevices;
for(auto it: m_Devices)
{
if(it.second == pDevice)
{
m_setLastDevicePaths.erase(it.first);
}
else
{
aDevices[it.first] = it.second;
}
}
m_Devices = aDevices;
}

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

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

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

@ -0,0 +1,76 @@
#include "SMXHelperThread.h"
#include <windows.h>
using namespace SMX;
SMX::SMXHelperThread::SMXHelperThread(const string &sThreadName)
{
m_hEvent = make_shared<AutoCloseHandle>(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<function<void()>> funcs;
swap(m_FunctionsToCall, funcs);
// If we're shutting down and have no more functions to call, stop.
if(funcs.empty() && m_bShutdown)
break;
// Unlock while we call the queued functions.
m_Lock.Unlock();
for(auto &func: funcs)
func();
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<void()> func)
{
m_Lock.AssertNotLockedByCurrentThread();
// Add func to the list, and poke the event to wake up the thread if needed.
m_Lock.Lock();
m_FunctionsToCall.push_back(func);
SetEvent(m_hEvent->value());
m_Lock.Unlock();
}

@ -0,0 +1,46 @@
#ifndef SMXHelperThread_h
#define SMXHelperThread_h
#include "Helpers.h"
#include <functional>
#include <vector>
#include <memory>
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<void()> 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<SMX::AutoCloseHandle> m_hEvent;
bool m_bShutdown = false;
HANDLE m_hThread = INVALID_HANDLE_VALUE;
vector<function<void()>> m_FunctionsToCall;
};
}
#endif

@ -0,0 +1,452 @@
#include "SMXManager.h"
#include "SMXDevice.h"
#include "SMXDeviceConnection.h"
#include "SMXDeviceSearchThreaded.h"
#include "Helpers.h"
#include <windows.h>
#include <memory>
using namespace std;
using namespace SMX;
namespace {
Mutex g_Lock;
}
SMX::SMXManager::SMXManager(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback):
m_UserCallbackThread("SMXUserCallbackThread")
{
// Raise the priority of the user callback thread, since we don't want input
// events to be preempted by other things and reduce timing accuracy.
m_UserCallbackThread.SetHighPriority(true);
m_hEvent = make_shared<AutoCloseHandle>(CreateEvent(NULL, false, false, NULL));
m_pSMXDeviceSearchThreaded = make_shared<SMXDeviceSearchThreaded>();
// Create the SMXDevices. We don't create these as we connect, we just reuse the same
// ones.
for(int i = 0; i < 2; ++i)
{
shared_ptr<SMXDevice> pDevice = SMXDevice::Create(m_hEvent, g_Lock);
m_pDevices.push_back(pDevice);
}
// The callback we send to SMXDeviceConnection will be called from our thread. Wrap
// it so it's called from UserCallbackThread instead.
auto pCallbackInThread = [this, pCallback](int PadNumber, SMXUpdateCallbackReason reason) {
m_UserCallbackThread.RunInThread([pCallback, PadNumber, reason]() {
pCallback(PadNumber, reason);
});
};
// Set the update callbacks. Do this before starting the thread, to avoid race conditions.
for(int pad = 0; pad < 2; ++pad)
m_pDevices[pad]->SetUpdateCallback(pCallbackInThread);
// Start the thread.
DWORD id;
m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &id);
SMX::SetThreadName(id, "SMXManager");
// Raise the priority of the I/O thread, since we don't want input
// events to be preempted by other things and reduce timing accuracy.
SetThreadPriority( m_hThread, THREAD_PRIORITY_HIGHEST );
}
SMX::SMXManager::~SMXManager()
{
// Shut down the thread, if it's still running.
Shutdown();
}
shared_ptr<SMXDevice> SMX::SMXManager::GetDevice(int pad)
{
return m_pDevices[pad];
}
void SMX::SMXManager::Shutdown()
{
g_Lock.AssertNotLockedByCurrentThread();
// Make sure we're not being called from within m_UserCallbackThread, since that'll
// deadlock when we shut down m_UserCallbackThread.
if(m_UserCallbackThread.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<SMXDevice> pDevice: m_pDevices)
{
wstring sError;
pDevice->Update(sError);
if(!sError.empty())
{
Log(ssprintf("Device error: %ls", sError.c_str()));
// Tell m_pDeviceList that the device was closed, so it'll discard the device
// and notice if a new device shows up on the same path.
m_pSMXDeviceSearchThreaded->DeviceWasClosed(pDevice->GetDeviceHandle());
pDevice->CloseDevice();
}
}
// Devices may have finished initializing, so see if we need to update the ordering.
CorrectDeviceOrder();
// Make a list of handles for WaitForMultipleObjectsEx.
vector<HANDLE> aHandles = { m_hEvent->value() };
for(shared_ptr<SMXDevice> pDevice: m_pDevices)
{
shared_ptr<AutoCloseHandle> pHandle = pDevice->GetDeviceHandle();
if(pHandle)
aHandles.push_back(pHandle->value());
}
// See how long we should block waiting for I/O. If we have any scheduled lights commands,
// wait until the next command should be sent, otherwise wait for a second.
int iDelayMS = 1000;
if(!m_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<shared_ptr<AutoCloseHandle>> apDevices = m_pSMXDeviceSearchThreaded->GetDevices();
// Check each device that we've found. This will include ones we already have open.
for(shared_ptr<AutoCloseHandle> pHandle: apDevices)
{
// See if this device is already open. If it is, we don't need to do anything with it.
bool bAlreadyOpen = false;
for(shared_ptr<SMXDevice> pDevice: m_pDevices)
{
if(pDevice->GetDeviceHandle() == pHandle)
bAlreadyOpen = true;
}
if(bAlreadyOpen)
continue;
// Find an open device slot.
shared_ptr<SMXDevice> pDeviceToOpen;
for(shared_ptr<SMXDevice> pDevice: m_pDevices)
{
// Note that we check whether the device has a handle rather than calling IsConnected, since
// devices aren't actually considered connected until they've read the configuration.
if(pDevice->GetDeviceHandle() == NULL)
{
pDeviceToOpen = pDevice;
break;
}
}
if(pDeviceToOpen == nullptr)
{
// All device slots are used. Are there more than two devices plugged in?
Log("Error: No available slots for device. Are more than two devices connected?");
break;
}
// Open the device in this slot.
Log("Opening SMX device");
wstring sError;
pDeviceToOpen->OpenDeviceHandle(pHandle, sError);
if(!sError.empty())
Log(ssprintf("Error opening device: %ls", sError.c_str()));
}
}

@ -0,0 +1,76 @@
#ifndef SMXManager_h
#define SMXManager_h
#include <windows.h>
#include <memory>
#include <vector>
#include <functional>
using namespace std;
#include "Helpers.h"
#include "../SMX.h"
#include "SMXHelperThread.h"
namespace SMX {
class SMXDevice;
class SMXDeviceSearchThreaded;
struct SMXControllerState
{
// True
bool m_bConnected[2];
// Pressed panels for player 1 and player 2:
uint16_t m_Inputs[2];
};
// This implements the main thread that controller communication and device searching
// happens in, finding and opening devices, and running device updates.
//
// Connected controllers can be accessed with GetDevice(),
// This also abstracts controller numbers. GetDevice(SMX_PadNumber_1) will return the
// first device that connected,
class SMXManager
{
public:
// pCallback is a function to be called when something changes on any device. This allows
// efficiently detecting when a panel is pressed or other changes happen.
SMXManager(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback);
~SMXManager();
void Shutdown();
shared_ptr<SMXDevice> GetDevice(int pad);
void SetLights(const string &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<SMX::AutoCloseHandle> m_hEvent;
shared_ptr<SMXDeviceSearchThreaded> m_pSMXDeviceSearchThreaded;
bool m_bShutdown = false;
vector<shared_ptr<SMXDevice>> m_pDevices;
// We make user callbacks asynchronously in this thread, to avoid any locking or timing
// issues that could occur by calling them in our I/O thread.
SMXHelperThread m_UserCallbackThread;
// A list of queued lights commands to send to the controllers. This is always sorted
// by iTimeToSend.
struct PendingCommand
{
PendingCommand(float fTime): fTimeToSend(fTime) { }
double fTimeToSend = 0;
string sPadCommand[2];
};
vector<PendingCommand> m_aPendingCommands;
double m_fDelayLightCommandsUntil = 0;
};
}
#endif

@ -0,0 +1 @@
bin

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
</configuration>

@ -0,0 +1,9 @@
<Application x:Class="smx_config.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:smx_config"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>

@ -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;
}
}
}

@ -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);
}
}
}

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

@ -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;
}
}
}

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

@ -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<TSource>(this IEnumerable<TSource> first, IEnumerable<TSource> 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<byte[]> parts = new LinkedList<byte[]>();
};
// 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());
}
};
}

@ -0,0 +1,798 @@
<Window x:Class="smx_config.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:clr="clr-namespace:System;assembly=mscorlib"
xmlns:controls="clr-namespace:smx_config"
mc:Ignorable="d"
x:Name="root"
Title="StepManiaX Platform Settings"
Icon="Resources/window icon.png"
Height="700" Width="525" ResizeMode="CanMinimize">
<Window.Resources>
<clr:String x:Key="HighPresetDescription" xml:space="preserve">Lighter steps will activate the arrows.
Use if small children are having difficulty pressing the arrows.</clr:String>
<clr:String x:Key="NormalPresetDescription" xml:space="preserve">This is the recommended setting.</clr:String>
<clr:String x:Key="LowPresetDescription" xml:space="preserve">More force is required to activate the arrows.
Use if the platform is too sensitive.</clr:String>
<FontFamily x:Key="WarningMarkFont">Segoe UI Black</FontFamily>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<SolidColorBrush x:Key="DoubleSlider.Static.Foreground" Color="#FFE5E5E5"/>
<SolidColorBrush x:Key="DoubleSlider.Static.Background" Color="#FFF0F0F0"/>
<SolidColorBrush x:Key="DoubleSlider.Static.Border" Color="#FFACACAC"/>
<SolidColorBrush x:Key="DoubleSlider.MouseOver.Background" Color="#FFDCECFC"/>
<SolidColorBrush x:Key="DoubleSlider.MouseOver.Border" Color="#FF7Eb4EA"/>
<SolidColorBrush x:Key="DoubleSlider.Pressed.Background" Color="#FFDAECFC"/>
<SolidColorBrush x:Key="DoubleSlider.Pressed.Border" Color="#FF569DE5"/>
<Style x:Key="DoubleSliderThumb" TargetType="{x:Type Thumb}">
<Setter Property="Height" Value="18"/>
<Setter Property="Width" Value="11"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center"
Height="18" Width="11">
<Path x:Name="grip" Data="M 0,0 C0,0 11,0 11,0 11,0 11,18 11,18 11,18 0,18 0,18 0,18 0,0 0,0 z" Fill="{StaticResource DoubleSlider.Static.Background}" Stretch="Fill" SnapsToDevicePixels="True" Stroke="{StaticResource DoubleSlider.Static.Border}" StrokeThickness="1" UseLayoutRounding="True" VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource DoubleSlider.MouseOver.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource DoubleSlider.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsDragging" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource DoubleSlider.Pressed.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource DoubleSlider.Pressed.Border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="DoubleSliderMiddleThumb" TargetType="{x:Type Thumb}">
<Grid x:Name="grip" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<Border x:Name="Border" Background="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
<Border x:Name="Fill" Background="{TemplateBinding Background}" Margin="1"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" TargetName="Fill" Value="{StaticResource DoubleSlider.MouseOver.Background}"/>
<Setter Property="Background" TargetName="Border" Value="{StaticResource DoubleSlider.MouseOver.Border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style x:Key="DoubleSliderRepeatButton" TargetType="{x:Type RepeatButton}">
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Focusable" Value="false"/>
<Setter Property="IsTabStop" Value="false"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Rectangle Fill="#00FFFFFF" Height="{TemplateBinding Height}" Width="{TemplateBinding Width}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="DoubleSlider" TargetType="{x:Type controls:DoubleSlider}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:DoubleSlider}">
<Grid>
<!-- The underlying bar: -->
<Border BorderBrush="#FFD6D6D6"
BorderThickness="1"
Background="#FFE7EAEA"
Height="4.0" Margin="5,0" VerticalAlignment="center"
/>
<!-- These hidden buttons handle moving the slider when clicking outside of a handle. -->
<RepeatButton x:Name="PART_DecreaseButton" Style="{StaticResource DoubleSliderRepeatButton}"/>
<RepeatButton x:Name="PART_IncreaseButton" Style="{StaticResource DoubleSliderRepeatButton}"/>
<Thumb x:Name="PART_UpperThumb" Style="{StaticResource DoubleSliderThumb}" />
<Thumb x:Name="PART_LowerThumb" Style="{StaticResource DoubleSliderThumb}" />
<!-- The connecting bar: -->
<Thumb x:Name="PART_Middle"
Template="{StaticResource DoubleSliderMiddleThumb}"
Width="Auto"
Height="10"
Foreground="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"
Stylus.IsPressAndHoldEnabled="false"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Visibility" TargetName="PART_UpperThumb" Value="Hidden"/>
<Setter Property="Visibility" TargetName="PART_LowerThumb" Value="Hidden"/>
<Setter Property="Visibility" TargetName="PART_Middle" Value="Hidden"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type controls:ThresholdSlider}">
<Setter Property="Focusable" Value="false"/>
<Setter Property="AdvancedModeEnabled" Value="{Binding ElementName=AdvancedModeEnabledCheckbox, Path=AdvancedModeEnabled, Mode=OneWay}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:ThresholdSlider}">
<StackPanel Margin="0,0,0,0" Orientation="Horizontal" HorizontalAlignment="Center">
<Image
Margin="0,0,0,0"
Height="28" Width="28"
HorizontalAlignment="Left" VerticalAlignment="Top"
Source="{TemplateBinding Icon}"
/>
<Label Margin="5,0,0,0" x:Name="LowerValue" Content="0" Width="30" HorizontalContentAlignment="Right"
HorizontalAlignment="Left" VerticalAlignment="Top"
/>
<controls:DoubleSlider x:Name="Slider"
Margin="32,5,0,0"
Minimum="20" Maximum="200" MinimumDistance="5"
LowerValue="20" UpperValue="35"
VerticalAlignment="Top"
Width="240"
Focusable="False"
Style="{DynamicResource DoubleSlider}"
/>
<Label Margin="11,0,0,0" x:Name="UpperValue" Content="0" Width="30" HorizontalContentAlignment="Center"
HorizontalAlignment="Left" VerticalAlignment="Top"
/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type controls:PresetButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:PresetButton}">
<Button x:Name="PART_Button" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<StackPanel Orientation="Horizontal">
<Label x:Name="Checkbox" Padding="0" VerticalAlignment="Center">✔</Label>
<Label Content="{TemplateBinding Label}"></Label>
</StackPanel>
</Button>
<ControlTemplate.Triggers>
<!-- Show the checkmark if the configuration matches this preset. -->
<Trigger Property="Selected" Value="False">
<Setter Property="Visibility" TargetName="Checkbox" Value="Collapsed" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type controls:PresetWidget}">
<Setter Property="Focusable" Value="false" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:PresetWidget}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<controls:PresetButton
Width="70"
Padding="5,2" Margin="5"
Label="{TemplateBinding Label}"
Type="{TemplateBinding Type}"
/>
<TextBlock Width="350" VerticalAlignment="Center"
TextAlignment="Center" xml:space="preserve" Text="{TemplateBinding Description}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Change this to any pure hue i.e. no more than 2 rgb components set and at least 1 set to FF -->
<Color x:Key="CurrentColor">#00FF00</Color>
<!-- http://stackoverflow.com/a/32514853/136829 -->
<LinearGradientBrush x:Key="HueBrush" StartPoint="0,0" EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FF0000" Offset="0.000" />
<GradientStop Color="#FFFF00" Offset="0.167" />
<GradientStop Color="#00FF00" Offset="0.333" />
<GradientStop Color="#00FFFF" Offset="0.5" />
<GradientStop Color="#0000FF" Offset="0.667" />
<GradientStop Color="#FF00FF" Offset="0.833" />
<GradientStop Color="#FF0000" Offset="1.000" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<Style x:Key="PanelButton" TargetType="{x:Type controls:PanelButton}">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Width" Value="25" />
<Setter Property="FocusVisualStyle">
<Setter.Value>
<Style>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
<Setter Property="Background" Value="#FFDDDDDD"/>
<Setter Property="BorderBrush" Value="#FF707070"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:PanelButton}">
<Border x:Name="border" BorderBrush="#000000" BorderThickness="1" SnapsToDevicePixels="true">
<ContentPresenter
SnapsToDevicePixels="true"
Focusable="False"
HorizontalAlignment="Center" VerticalAlignment="Center"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="False">
<Setter Property="Background" TargetName="border" Value="#FFFF0000"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" TargetName="border" Value="#FF00FF00"/>
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter Property="Background" TargetName="border" Value="#FFC4E5F6"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF2C628B"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type controls:PanelColorButton}">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Width" Value="25" />
<Setter Property="AllowDrop" Value="True" />
<Setter Property="Focusable" Value="false"/>
<Setter Property="Background" Value="#FFDDDDDD"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="BorderBrush" Value="#FF707070"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:PanelColorButton}">
<Border x:Name="border"
Background="{TemplateBinding PanelColor}"
BorderBrush="#000000" BorderThickness="1" SnapsToDevicePixels="true">
<ContentPresenter
SnapsToDevicePixels="true"
Focusable="False"
HorizontalAlignment="Center" VerticalAlignment="Center"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter Property="BorderBrush" TargetName="border" Value="#FF000000"/>
<Setter Property="BorderThickness" TargetName="border" Value="2"/>
</Trigger>
<Trigger Property="IsSelected" Value="false">
<Setter Property="BorderBrush" TargetName="border" Value="#FF808080"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type controls:ColorPicker}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:ColorPicker}">
<StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Label Content="Step color" Width="80" HorizontalContentAlignment="Center" />
<Grid Width="400" Height="20">
<DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Rectangle DockPanel.Dock="Left" Fill="#FFFFFFFF" Width="25"
Stroke="Black" StrokeThickness="1" SnapsToDevicePixels="True" />
<Rectangle DockPanel.Dock="Left" Fill="{DynamicResource HueBrush}"
Stroke="Black" StrokeThickness="1" SnapsToDevicePixels="True" />
</DockPanel>
<controls:ColorPickerSlider x:Name="HueSlider" Minimum="-14" Maximum="359"
Focusable="false"
VerticalAlignment="Center" SmallChange="1" LargeChange="10"
Margin="7 0 0 0"
IsMoveToPointEnabled="True" IsSnapToTickEnabled="True"/>
</Grid>
</StackPanel>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type controls:PanelSelector}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:PanelSelector}">
<Grid Background="#FFE5E5E5" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,10,0,-1">
<controls:PanelButton controls:PanelButton.Button="7" x:Name="EnablePanel7" Content="↖" Style="{StaticResource PanelButton}" Margin="-60,-50,0,0" />
<controls:PanelButton controls:PanelButton.Button="8" x:Name="EnablePanel8" Content="↑" Style="{StaticResource PanelButton}" Margin="0,-50,0,0" />
<controls:PanelButton controls:PanelButton.Button="9" x:Name="EnablePanel9" Content="↗" Style="{StaticResource PanelButton}" Margin="60,-50,0,0" />
<controls:PanelButton controls:PanelButton.Button="4" x:Name="EnablePanel4" Content="←" Style="{StaticResource PanelButton}" Margin="-60,0,0,0" />
<controls:PanelButton controls:PanelButton.Button="5" x:Name="EnablePanel5" Content="☐" Style="{StaticResource PanelButton}" Margin="0,0,0,0" />
<controls:PanelButton controls:PanelButton.Button="6" x:Name="EnablePanel6" Content="→" Style="{StaticResource PanelButton}" Margin="60,0,0,0" />
<controls:PanelButton controls:PanelButton.Button="1" x:Name="EnablePanel1" Content="↙" Style="{StaticResource PanelButton}" Margin="-60,50,0,0" />
<controls:PanelButton controls:PanelButton.Button="2" x:Name="EnablePanel2" Content="↓" Style="{StaticResource PanelButton}" Margin="0,50,0,0" />
<controls:PanelButton controls:PanelButton.Button="3" x:Name="EnablePanel3" Content="↘" Style="{StaticResource PanelButton}" Margin="60,50,0,0" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type controls:DiagnosticsPanelButton}">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
<Setter Property="Focusable" Value="false"/>
<Setter Property="Background" Value="#FFDDDDDD"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="BorderBrush" Value="#FF707070"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:DiagnosticsPanelButton}">
<Border x:Name="border"
Background="#FFA0A0A0"
BorderBrush="#000000" BorderThickness="1" SnapsToDevicePixels="true">
<Grid
HorizontalAlignment="Center" VerticalAlignment="Center"
>
<TextBlock
x:Name="WarningIcon"
Text="!"
Visibility="{TemplateBinding Warning, Converter={StaticResource BooleanToVisibilityConverter}}"
Foreground="#FFFF4040"
Margin="0,-5,0,0"
FontSize="40"
FontFamily="{StaticResource WarningMarkFont}"
/>
<ContentPresenter
SnapsToDevicePixels="true"
Focusable="False"
HorizontalAlignment="Center" VerticalAlignment="Center"
/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<!-- Highlight on press: -->
<Trigger Property="Pressed" Value="true">
<Setter Property="Background" TargetName="border" Value="#FF20FF20"/>
</Trigger>
<!-- Show which panel is selected for displaying diagnostics: -->
<Trigger Property="IsSelected" Value="true">
<Setter Property="BorderBrush" TargetName="border" Value="#FF000000"/>
<Setter Property="BorderThickness" TargetName="border" Value="2"/>
</Trigger>
<Trigger Property="IsSelected" Value="false">
<Setter Property="BorderBrush" TargetName="border" Value="#FF808080"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type controls:LevelBar}">
<Setter Property="Width" Value="35" />
<Setter Property="Height" Value="200" />
<Setter Property="Focusable" Value="false"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:LevelBar}">
<Grid>
<!-- Black backing: -->
<Rectangle
x:Name="Back"
Width="35" Height="200" Fill="#FF000000"
HorizontalAlignment="Center"
Stroke="Black" StrokeThickness="1"
/>
<!-- Filled portion: -->
<Rectangle
x:Name="Fill"
Margin="0,0,0,1"
Width="33" Height="100" Fill="#FF00FF00"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type controls:DiagnosticsControl}">
<Setter Property="Focusable" Value="false"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:DiagnosticsControl}">
<Grid Background="#FFE5E5E5">
<TextBlock HorizontalAlignment="Center"
xml:space="preserve" FontSize="16" Margin="0,15,0,0">Platform diagnostics</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top">
<Grid x:Name="P1Diagnostics"
Margin="0,50,0,0"
Background="#FFE5E5E5" Width="200" Height="200">
<controls:DiagnosticsPanelButton Panel="0" Margin="-120,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="1" Margin="0,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="2" Margin="120,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="3" Margin="-120,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="4" Margin="0,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="5" Margin="120,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="6" Margin="-120,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="7" Margin="0,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="8" Margin="120,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
</Grid>
<Grid x:Name="P2Diagnostics" Margin="0,50,0,0"
Background="#FFE5E5E5" Width="200" Height="200"
HorizontalAlignment="Center" VerticalAlignment="Top">
<controls:DiagnosticsPanelButton Panel="9" Margin="-120,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="10" Margin="0,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="11" Margin="120,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="12" Margin="-120,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="13" Margin="0,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="14" Margin="120,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="15" Margin="-120,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="16" Margin="0,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<controls:DiagnosticsPanelButton Panel="17" Margin="120,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
</Grid>
</StackPanel>
<StackPanel Orientation="Vertical"
Margin="50,300,0,0"
Width="200"
HorizontalAlignment="Left" VerticalAlignment="Top">
<DockPanel x:Name="SensorBarPanel" HorizontalAlignment="Center">
<ComboBox
x:Name="DiagnosticMode" DockPanel.Dock="Top"
Margin="0,0,0,5"
HorizontalAlignment="Left" SelectedIndex="0" Width="120">
<ListBoxItem Content="Calibrated values"/>
<ListBoxItem Content="Raw values"/>
<ListBoxItem Content="Noise"/>
<ListBoxItem Content="Tare"/>
</ComboBox>
<StackPanel DockPanel.Dock="Left" Orientation="Vertical">
<controls:LevelBar x:Name="SensorBar1" Margin="0,0,5,0"/>
<Label x:Name="SensorBarLevel1" HorizontalAlignment="Center" Content="xxx"/>
</StackPanel>
<StackPanel DockPanel.Dock="Left" Orientation="Vertical">
<controls:LevelBar x:Name="SensorBar2" Margin="0,0,5,0"/>
<Label x:Name="SensorBarLevel2" HorizontalAlignment="Center" Content="xxx"/>
</StackPanel>
<StackPanel DockPanel.Dock="Left" Orientation="Vertical">
<controls:LevelBar x:Name="SensorBar3" Margin="0,0,5,0"/>
<Label x:Name="SensorBarLevel3" HorizontalAlignment="Center" Content="xxx"/>
</StackPanel>
<StackPanel DockPanel.Dock="Left" Orientation="Vertical">
<controls:LevelBar x:Name="SensorBar4" Margin="0,0,0,0"/>
<Label x:Name="SensorBarLevel4" HorizontalAlignment="Center" Content="xxx"/>
</StackPanel>
</DockPanel>
<DockPanel
x:Name="NoResponseFromPanel"
Width="200"
HorizontalAlignment="Center"
Margin="0,-20,0,0"
>
<TextBlock
HorizontalAlignment="Stretch" VerticalAlignment="Center"
Foreground="#FFFF0000" FontSize="20"
FontFamily="{StaticResource WarningMarkFont}"
Text="!"
/>
<TextBlock
HorizontalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" TextAlignment="Center"
Text="No data received from this panel."
/>
</DockPanel>
<DockPanel
x:Name="NoResponseFromSensors"
Width="200"
>
<TextBlock
HorizontalAlignment="Stretch" VerticalAlignment="Center"
Foreground="#FFFF0000" FontSize="20"
FontFamily="{StaticResource WarningMarkFont}"
Text="!"
/>
<TextBlock
HorizontalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" TextAlignment="Center"
Text="Some sensors on this panel aren't responding correctly."
/>
</DockPanel>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="10,10,0,0"
>
<Button
x:Name="Recalibrate" Content="Recalibrate"
HorizontalAlignment="Left" VerticalAlignment="Top"
Padding="8,4"
/>
<Button
x:Name="LightAll" Content="Light panels"
HorizontalAlignment="Left" VerticalAlignment="Top"
Padding="8,4"
Margin="10,0,0,0"
/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Vertical"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="300,350,0,0"
>
<TextBlock Text="Current DIP"/>
<controls:FrameImage
Image="Resources/DIP.png"
x:Name="CurrentDIP"
FramesX="16"
Frame="1"
Width="60"
Height="100"
/>
</StackPanel>
<StackPanel Orientation="Vertical"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="400,350,0,0"
>
<TextBlock Text="Expected DIP"/>
<controls:FrameImage
Image="Resources/DIP.png"
x:Name="ExpectedDIP"
FramesX="16"
Frame="1"
Width="60"
Height="100"
/>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- P1, P2 labels in the top right: -->
<Style x:Key="EnabledIcon" TargetType="Label">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="true">
<Setter Property="Foreground" Value="#000000"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="#A0A0A0"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<!-- A list of which pads are connected, overlapping the tab bar in the top right. -->
<Grid x:Name="ConnectedPads" Background="#DDD">
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Right" Orientation="Horizontal">
<Label Content="Connected:" FontSize="10"/>
<Label x:Name="P1Connected" Style="{StaticResource EnabledIcon}" Content="P1" Margin="1,0,4,0" FontSize="10"/>
<Label x:Name="P2Connected" Style="{StaticResource EnabledIcon}" Content="P2" Margin="0,0,4,0" FontSize="10"/>
</StackPanel>
</Grid>
<Grid x:Name="Searching" Visibility="Hidden" Background="#DDD">
<Label Content="Searching for controller..."
HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="33.333"/>
</Grid>
<TabControl x:Name="Main" Margin="0,0,0,0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<TabItem Header="Settings">
<Grid Background="#FFE5E5E5" RenderTransformOrigin="0.5,0.5">
<StackPanel Margin="0,0,0,0" VerticalAlignment="Top">
<TextBlock HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top"
TextAlignment="Center"
xml:space="preserve" FontSize="16">Panel sensitivity</TextBlock>
<controls:PresetWidget Type="high" Label="High"
Description="{StaticResource HighPresetDescription}"
Margin="0,5,0,10"
/>
<controls:PresetWidget controls:PresetWidget.Type="normal" Label="Medium"
Description="{StaticResource NormalPresetDescription}"
Margin="0,5,0,10"
/>
<controls:PresetWidget controls:PresetWidget.Type="low" Label="Low"
Description="{StaticResource LowPresetDescription}"
Margin="0,5,0,10"
/>
<Separator Margin="0,10,0,4" />
<TextBlock HorizontalAlignment="Center" Margin="0,15,0,0" VerticalAlignment="Top"
TextAlignment="Center"
xml:space="preserve" FontSize="16">Panel colors</TextBlock>
<TextBlock DockPanel.Dock="Top" HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top"
TextAlignment="Center"
xml:space="preserve"
>Set the color each arrow lights when pressed.</TextBlock>
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<!-- The duplication here could be factored out, but with WPF it's not really worth it: -->
<Grid x:Name="PanelColorP1" Background="#FFE5E5E5" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Top">
<controls:PanelColorButton Panel="0" Content="↖" Margin="-60,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="1" Content="↑" Margin="0,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="2" Content="↗" Margin="60,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="3" Content="←" Margin="-60,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="4" Content="☐" Margin="0,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="5" Content="→" Margin="60,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="6" Content="↙" Margin="-60,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="7" Content="↓" Margin="0,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="8" Content="↘" Margin="60,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
</Grid>
<Grid x:Name="PanelColorP2" Background="#FFE5E5E5" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Top">
<controls:PanelColorButton Panel="9" Content="↖" Margin="-60,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="10" Content="↑" Margin="0,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="11" Content="↗" Margin="60,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="12" Content="←" Margin="-60,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="13" Content="☐" Margin="0,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="14" Content="→" Margin="60,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="15" Content="↙" Margin="-60,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="16" Content="↓" Margin="0,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
<controls:PanelColorButton Panel="17" Content="↘" Margin="60,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" />
</Grid>
</StackPanel>
<controls:ColorPicker
x:Name="AutoLightsColor" Focusable="false"
controls:ColorPicker.SelectedPanel="0"
/>
<Button x:Name="SetAllPanelsToCurrentColor" Content="Set all panels to this color" HorizontalAlignment="Center" Padding="6,2" Margin="0,6,0,0" />
</StackPanel>
</Grid>
</TabItem>
<TabItem Header="Sensitivity">
<Grid Background="#FFE5E5E5" RenderTransformOrigin="0.5,0.5">
<DockPanel Margin="0,0,0,0" VerticalAlignment="Stretch">
<TextBlock DockPanel.Dock="Top" HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top"
TextAlignment="Center"
xml:space="preserve" FontSize="16">Custom sensitivity</TextBlock>
<TextBlock DockPanel.Dock="Top" HorizontalAlignment="Center" Margin="0,0,0,10" VerticalAlignment="Top"
xml:space="preserve"
TextAlignment="Center"
>Set the force required to trigger each arrow.</TextBlock>
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="up-left" controls:ThresholdSlider.Icon="Resources/pad_up_left.png" Margin="0,8,0,0" />
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="up" controls:ThresholdSlider.Icon="Resources/pad_up.png" Margin="0,8,0,0" />
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="up-right" controls:ThresholdSlider.Icon="Resources/pad_up_right.png" Margin="0,8,0,0" />
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="left" controls:ThresholdSlider.Icon="Resources/pad_left.png" Margin="0,8,0,0" />
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="center" controls:ThresholdSlider.Icon="Resources/pad_center.png" Margin="0,8,0,0" />
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="right" controls:ThresholdSlider.Icon="Resources/pad_right.png" Margin="0,8,0,0" />
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="down-left" controls:ThresholdSlider.Icon="Resources/pad_down_left.png" Margin="0,8,0,0" />
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="down" controls:ThresholdSlider.Icon="Resources/pad_down.png" Margin="0,8,0,0" />
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="down-right" controls:ThresholdSlider.Icon="Resources/pad_down_right.png" Margin="0,8,0,0" />
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="cardinal" controls:ThresholdSlider.Icon="Resources/pad_cardinal.png" Margin="0,8,0,0" />
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="corner" controls:ThresholdSlider.Icon="Resources/pad_diagonal.png" Margin="0,8,0,0" />
<TextBlock DockPanel.Dock="Top" HorizontalAlignment="Center" Margin="0,25,0,0" VerticalAlignment="Top"
xml:space="preserve"
TextAlignment="Center"
>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.</TextBlock>
<DockPanel DockPanel.Dock="Bottom"
VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Margin="0,0,5,0">
<Label Content="Presets:" VerticalAlignment="Center" />
<controls:PresetButton Width="70" Padding="5,2" Margin="5" Label="Low" Type="low" />
<controls:PresetButton Width="70" Padding="5,2" Margin="5" Label="Normal" Type="normal" />
<controls:PresetButton Width="70" Padding="5,2" Margin="5" Label="High" Type="high" />
<controls:AdvancedThresholdViewCheckbox
x:Name="AdvancedModeEnabledCheckbox"
DockPanel.Dock="Right" Content="Advanced view"
VerticalAlignment="Center" HorizontalAlignment="Right"
IsChecked="{Binding Path=AdvancedModeEnabled, Mode=TwoWay, RelativeSource={RelativeSource Self}}"
/>
</DockPanel>
</DockPanel>
</Grid>
</TabItem>
<TabItem Header="Advanced">
<StackPanel Background="#FFE5E5E5">
<TextBlock HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top"
TextAlignment="Center"
xml:space="preserve" FontSize="16">Active panels</TextBlock>
<TextBlock xml:space="preserve" HorizontalAlignment="Center" Margin="0,0,0,0" TextAlignment="Center">Select which directions have sensors, and deselect panels that aren't in use.
Input will be disabled from deselected panels.</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<controls:PanelSelector Focusable="false" />
</StackPanel>
<Separator Margin="0,10,0,10" />
<TextBlock HorizontalAlignment="Center"
xml:space="preserve" FontSize="16">Reset all settings</TextBlock>
<Button Content="Factory reset" Width="140" Margin="0 10 0 0" Padding="0 4" Click="FactoryReset_Click"/>
</StackPanel>
</TabItem>
<TabItem Header="Diagnostics">
<StackPanel Background="#FFE5E5E5">
<controls:DiagnosticsControl x:Name="Diagnostics" />
</StackPanel>
</TabItem>
</TabControl>
</Grid>
</Window>

@ -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);
}
}
}

@ -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
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> 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")]

@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace smx_config.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
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;
}
}
}
}

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

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

@ -0,0 +1,200 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}</ProjectGuid>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>smx_config</RootNamespace>
<AssemblyName>SMXConfig</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>$(SolutionDir)\out\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>..\out\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<LangVersion>default</LangVersion>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>window icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="ConfigPresets.cs" />
<Compile Include="CurrentSMXDevice.cs" />
<Compile Include="DiagnosticsWidgets.cs" />
<Compile Include="DoubleSlider.cs" />
<Compile Include="Helpers.cs" />
<Compile Include="SMX.cs" />
<Compile Include="Widgets.cs" />
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<AppDesigner Include="Properties\" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.5.2">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.5.2 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\pad_cardinal.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\pad_center.png" />
<Resource Include="Resources\pad_diagonal.png" />
<Resource Include="Resources\pad_up.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\window icon.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\DIP.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="window icon.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\pad_down.png" />
<Resource Include="Resources\pad_down_left.png" />
<Resource Include="Resources\pad_down_right.png" />
<Resource Include="Resources\pad_left.png" />
<Resource Include="Resources\pad_right.png" />
<Resource Include="Resources\pad_up_left.png" />
<Resource Include="Resources\pad_up_right.png" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\sdk\Windows\SMX.vcxproj">
<Project>{c5fc0823-9896-4b7c-bfe1-b60db671a462}</Project>
<Name>SMX</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

@ -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<double> 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];
}
};
}

@ -0,0 +1 @@
InstallSMXConfig.exe

@ -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

@ -0,0 +1,3 @@
@echo off
"C:\Program Files (x86)\NSIS\makensis.exe" SMX.nsi
pause

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Loading…
Cancel
Save