Import for release.
8
.editorconfig
Normal file
@ -0,0 +1,8 @@
|
||||
root=true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
|
||||
[*.{cpp,cs,h}]
|
||||
indent_size = 4
|
||||
|
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.vs
|
||||
*.user
|
||||
build
|
||||
obj
|
||||
out
|
22
LICENSE.txt
Normal file
@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Step Revolution LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
5
README.md
Normal file
@ -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.
|
||||
|
37
SMX.sln
Normal file
@ -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
|
BIN
docs/icon.png
Normal file
After Width: | Height: | Size: 25 KiB |
157
docs/index.html
Normal file
@ -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.
|
||||
|
||||
|
BIN
docs/logo.png
Normal file
After Width: | Height: | Size: 416 KiB |
26
docs/style.css
Normal file
@ -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;
|
||||
|
||||
|
||||
}
|
96
sample/SMXSample.cpp
Normal file
@ -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;
|
||||
}
|
||||
|
103
sample/SMXSample.vcxproj
Normal file
@ -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>
|
14
sample/SMXSample.vcxproj.filters
Normal file
@ -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>
|
244
sdk/SMX.h
Normal file
@ -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
|
285
sdk/Windows/Helpers.cpp
Normal file
@ -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);
|
||||
}
|
104
sdk/Windows/Helpers.h
Normal file
@ -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
|
65
sdk/Windows/SMX.cpp
Normal file
@ -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(); }
|
132
sdk/Windows/SMX.vcxproj
Normal file
@ -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>
|
61
sdk/Windows/SMX.vcxproj.filters
Normal file
@ -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>
|
494
sdk/Windows/SMXDevice.cpp
Normal file
@ -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);
|
||||
}
|
132
sdk/Windows/SMXDevice.h
Normal file
@ -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
|
378
sdk/Windows/SMXDeviceConnection.cpp
Normal file
@ -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);
|
||||
}
|
122
sdk/Windows/SMXDeviceConnection.h
Normal file
@ -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
|
155
sdk/Windows/SMXDeviceSearch.cpp
Normal file
@ -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;
|
||||
}
|
33
sdk/Windows/SMXDeviceSearch.h
Normal file
@ -0,0 +1,33 @@
|
||||
#ifndef SMXDeviceSearch_h
|
||||
#define SMXDeviceSearch_h
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <map>
|
||||
using namespace std;
|
||||
|
||||
#include "Helpers.h"
|
||||
|
||||
namespace SMX {
|
||||
|
||||
class SMXDeviceSearch
|
||||
{
|
||||
public:
|
||||
// Return a list of connected devices. If the same device stays connected and this
|
||||
// is called multiple times, the same handle will be returned.
|
||||
vector<shared_ptr<AutoCloseHandle>> GetDevices(wstring &error);
|
||||
|
||||
// After a device is opened and then closed, tell this class that the device was closed.
|
||||
// We'll discard our record of it, so we'll notice a new device plugged in on the same
|
||||
// path.
|
||||
void DeviceWasClosed(shared_ptr<AutoCloseHandle> pDevice);
|
||||
|
||||
private:
|
||||
set<wstring> m_setLastDevicePaths;
|
||||
map<wstring, shared_ptr<AutoCloseHandle>> m_Devices;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
97
sdk/Windows/SMXDeviceSearchThreaded.cpp
Normal file
@ -0,0 +1,97 @@
|
||||
#include "SMXDeviceSearchThreaded.h"
|
||||
#include "SMXDeviceSearch.h"
|
||||
#include "SMXDeviceConnection.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <memory>
|
||||
using namespace std;
|
||||
using namespace SMX;
|
||||
|
||||
SMX::SMXDeviceSearchThreaded::SMXDeviceSearchThreaded()
|
||||
{
|
||||
m_hEvent = make_shared<AutoCloseHandle>(CreateEvent(NULL, false, false, NULL));
|
||||
m_pDeviceList = make_shared<SMXDeviceSearch>();
|
||||
|
||||
// Start the thread.
|
||||
DWORD id;
|
||||
m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &id);
|
||||
SMX::SetThreadName(id, "SMXDeviceSearch");
|
||||
}
|
||||
|
||||
SMX::SMXDeviceSearchThreaded::~SMXDeviceSearchThreaded()
|
||||
{
|
||||
// Shut down the thread, if it's still running.
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void SMX::SMXDeviceSearchThreaded::Shutdown()
|
||||
{
|
||||
if(m_hThread == INVALID_HANDLE_VALUE)
|
||||
return;
|
||||
|
||||
// Tell the thread to shut down, and wait for it before returning.
|
||||
m_bShutdown = true;
|
||||
SetEvent(m_hEvent->value());
|
||||
|
||||
WaitForSingleObject(m_hThread, INFINITE);
|
||||
m_hThread = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
|
||||
DWORD WINAPI SMX::SMXDeviceSearchThreaded::ThreadMainStart(void *self_)
|
||||
{
|
||||
SMXDeviceSearchThreaded *self = (SMXDeviceSearchThreaded *) self_;
|
||||
self->ThreadMain();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SMX::SMXDeviceSearchThreaded::UpdateDeviceList()
|
||||
{
|
||||
m_Lock.AssertNotLockedByCurrentThread();
|
||||
|
||||
// Tell m_pDeviceList about closed devices, so it knows that any device on the
|
||||
// same path is new.
|
||||
m_Lock.Lock();
|
||||
for(auto pDevice: m_apClosedDevices)
|
||||
m_pDeviceList->DeviceWasClosed(pDevice);
|
||||
m_apClosedDevices.clear();
|
||||
m_Lock.Unlock();
|
||||
|
||||
// Get the current device list.
|
||||
wstring sError;
|
||||
vector<shared_ptr<AutoCloseHandle>> apDevices = m_pDeviceList->GetDevices(sError);
|
||||
if(!sError.empty())
|
||||
{
|
||||
Log(ssprintf("Error listing USB devices: %ls", sError.c_str()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the device list returned by GetDevices.
|
||||
m_Lock.Lock();
|
||||
m_apDevices = apDevices;
|
||||
m_Lock.Unlock();
|
||||
}
|
||||
|
||||
void SMX::SMXDeviceSearchThreaded::ThreadMain()
|
||||
{
|
||||
while(!m_bShutdown)
|
||||
{
|
||||
UpdateDeviceList();
|
||||
WaitForSingleObjectEx(m_hEvent->value(), 250, true);
|
||||
}
|
||||
}
|
||||
|
||||
void SMX::SMXDeviceSearchThreaded::DeviceWasClosed(shared_ptr<AutoCloseHandle> pDevice)
|
||||
{
|
||||
// Add pDevice to the list of closed devices. We'll call m_pDeviceList->DeviceWasClosed
|
||||
// on these from the scanning thread.
|
||||
m_apClosedDevices.push_back(pDevice);
|
||||
}
|
||||
|
||||
vector<shared_ptr<AutoCloseHandle>> SMX::SMXDeviceSearchThreaded::GetDevices()
|
||||
{
|
||||
// Lock to make a copy of the device list.
|
||||
m_Lock.Lock();
|
||||
vector<shared_ptr<AutoCloseHandle>> apResult = m_apDevices;
|
||||
m_Lock.Unlock();
|
||||
return apResult;
|
||||
}
|
46
sdk/Windows/SMXDeviceSearchThreaded.h
Normal file
@ -0,0 +1,46 @@
|
||||
#ifndef SMXDeviceSearchThreaded_h
|
||||
#define SMXDeviceSearchThreaded_h
|
||||
|
||||
#include "Helpers.h"
|
||||
#include <windows.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
using namespace std;
|
||||
|
||||
namespace SMX {
|
||||
|
||||
class SMXDeviceSearch;
|
||||
|
||||
// This is a wrapper around SMXDeviceSearch which performs USB scanning in a thread.
|
||||
// It's free on Win10, but takes a while on Windows 7 (about 8ms), so running it on
|
||||
// a separate thread prevents random timing errors when reading HID updates.
|
||||
class SMXDeviceSearchThreaded
|
||||
{
|
||||
public:
|
||||
SMXDeviceSearchThreaded();
|
||||
~SMXDeviceSearchThreaded();
|
||||
|
||||
// The same interface as SMXDeviceSearch:
|
||||
vector<shared_ptr<SMX::AutoCloseHandle>> GetDevices();
|
||||
void DeviceWasClosed(shared_ptr<SMX::AutoCloseHandle> pDevice);
|
||||
|
||||
// Synchronously shut down the thread.
|
||||
void Shutdown();
|
||||
|
||||
private:
|
||||
void UpdateDeviceList();
|
||||
|
||||
static DWORD WINAPI ThreadMainStart(void *self_);
|
||||
void ThreadMain();
|
||||
|
||||
SMX::Mutex m_Lock;
|
||||
shared_ptr<SMXDeviceSearch> m_pDeviceList;
|
||||
shared_ptr<SMX::AutoCloseHandle> m_hEvent;
|
||||
vector<shared_ptr<SMX::AutoCloseHandle>> m_apDevices;
|
||||
vector<shared_ptr<SMX::AutoCloseHandle>> m_apClosedDevices;
|
||||
bool m_bShutdown = false;
|
||||
HANDLE m_hThread = INVALID_HANDLE_VALUE;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
76
sdk/Windows/SMXHelperThread.cpp
Normal file
@ -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();
|
||||
}
|
46
sdk/Windows/SMXHelperThread.h
Normal file
@ -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
|
452
sdk/Windows/SMXManager.cpp
Normal file
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
76
sdk/Windows/SMXManager.h
Normal file
@ -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
|
1
smx-config/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
bin
|
6
smx-config/App.config
Normal file
@ -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>
|
9
smx-config/App.xaml
Normal file
@ -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>
|
32
smx-config/App.xaml.cs
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
137
smx-config/ConfigPresets.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
281
smx-config/CurrentSMXDevice.cs
Normal file
@ -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());
|
||||
}
|
||||
};
|
||||
}
|
289
smx-config/DiagnosticsWidgets.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
203
smx-config/DoubleSlider.cs
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
204
smx-config/Helpers.cs
Normal file
@ -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());
|
||||
}
|
||||
};
|
||||
}
|
798
smx-config/MainWindow.xaml
Normal file
@ -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>
|
86
smx-config/MainWindow.xaml.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
55
smx-config/Properties/AssemblyInfo.cs
Normal file
@ -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")]
|
63
smx-config/Properties/Resources.Designer.cs
generated
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
120
smx-config/Properties/Resources.resx
Normal file
@ -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>
|
30
smx-config/Properties/Settings.Designer.cs
generated
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
smx-config/Properties/Settings.settings
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
|
||||
<Profiles>
|
||||
<Profile Name="(Default)" />
|
||||
</Profiles>
|
||||
<Settings />
|
||||
</SettingsFile>
|
BIN
smx-config/Resources/DIP.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
smx-config/Resources/pad_cardinal.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/pad_center.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/pad_diagonal.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/pad_down.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/pad_down_left.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/pad_down_right.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/pad_left.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/pad_right.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/pad_up.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/pad_up_left.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/pad_up_right.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
smx-config/Resources/window icon.ico
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
smx-config/Resources/window icon.png
Normal file
After Width: | Height: | Size: 25 KiB |
296
smx-config/SMX.cs
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
200
smx-config/SMXConfig.csproj
Normal file
@ -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>
|
921
smx-config/Widgets.cs
Normal file
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
1
smx-config/installer/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
InstallSMXConfig.exe
|
106
smx-config/installer/SMX.nsi
Normal file
@ -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
|
||||
|
3
smx-config/installer/build.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
"C:\Program Files (x86)\NSIS\makensis.exe" SMX.nsi
|
||||
pause
|
BIN
smx-config/window icon.ico
Normal file
After Width: | Height: | Size: 37 KiB |