@ -0,0 +1,8 @@ |
|||||||
|
root=true |
||||||
|
|
||||||
|
[*] |
||||||
|
indent_style = space |
||||||
|
|
||||||
|
[*.{cpp,cs,h}] |
||||||
|
indent_size = 4 |
||||||
|
|
@ -0,0 +1,5 @@ |
|||||||
|
.vs |
||||||
|
*.user |
||||||
|
build |
||||||
|
obj |
||||||
|
out |
@ -0,0 +1,22 @@ |
|||||||
|
The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2017 Step Revolution LLC |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
|
@ -0,0 +1,5 @@ |
|||||||
|
This is the SDK for StepManiaX platform development. |
||||||
|
|
||||||
|
See [the StepManiaX website](https://stepmaniax.com) and the [documentation](https://steprevolution.github.io/stepmaniax-sdk/) |
||||||
|
for info. |
||||||
|
|
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,157 @@ |
|||||||
|
<html> |
||||||
|
<link rel=stylesheet href=style.css> |
||||||
|
<link rel="icon" href="icon.png" type="image/png"> |
||||||
|
|
||||||
|
<img src=logo.png style="width: 80%; display: block; margin-left: auto; margin-right: auto;"> |
||||||
|
|
||||||
|
<h2>Introduction to the StepManiaX SDK</h2> |
||||||
|
|
||||||
|
The StepManiaX SDK supports C++ development for the <a href=https://stepmaniax.com/>StepManiaX dance platform</a>. |
||||||
|
|
||||||
|
<h2>Usage</h2> |
||||||
|
|
||||||
|
You can either build the solution and link the resulting SMX.dll to your application, |
||||||
|
or import the source project and add it to your Visual Studio solution. The SDK |
||||||
|
interface is <code>SMX.h</code>. |
||||||
|
<p> |
||||||
|
See <code>sample</code> for a sample application. |
||||||
|
<p> |
||||||
|
Up to two controllers are supported. <code>SMX_GetInfo</code> can be used to check which |
||||||
|
controllers are connected. Each <code>pad</code> argument to API calls can be 0 for the |
||||||
|
player 1 pad, or 1 for the player 2 pad. |
||||||
|
|
||||||
|
<h2>HID support</h2> |
||||||
|
|
||||||
|
The platform can be used as a regular USB HID input device, which works in any game |
||||||
|
that supports input remapping. |
||||||
|
<p> |
||||||
|
However, applications using this SDK to control the panels directly should ignore the |
||||||
|
HID interface, and instead use <code>SMX_GetInputState</code> to retrieve the input state. |
||||||
|
|
||||||
|
<h2>Platform lights</h2> |
||||||
|
|
||||||
|
The platform can have up to nine panels. Each panel has a grid of 4x4 RGB LEDs, which can |
||||||
|
be individually controlled at up to 30 FPS. |
||||||
|
<p> |
||||||
|
See <code>SMX_SetLights</code>. |
||||||
|
|
||||||
|
<h2>Platform configuration</h2> |
||||||
|
|
||||||
|
The platform configuration can be read and modified using SMX_GetConfig and SMX_SetConfig. |
||||||
|
Most of the platform configuration doesn't need to be accessed by applications, since |
||||||
|
users can use the SMXConfig application to manage their platform. |
||||||
|
<p> |
||||||
|
<ul> |
||||||
|
<li> |
||||||
|
<b>enabledSensors</b> |
||||||
|
<p> |
||||||
|
Each platform can have up to nine panels in any configuration, but most devices have |
||||||
|
a smaller number of panels installed. If an application wants to adapt its UI to the |
||||||
|
user's panel configuration, see enabledSensors to detect which sensors are enabled. |
||||||
|
<p> |
||||||
|
Each panel has four sensors, and if a panel is disabled, all four of its panels will be |
||||||
|
disabled. Disabling individual sensors is possible, but removing individual sensors |
||||||
|
reduces the performance of the pad and isn't recommended. |
||||||
|
<p> |
||||||
|
Note that this indicates which panels the player is using for input. Other panels may |
||||||
|
still have lights support, and the application should always send lights data for all |
||||||
|
possible panels even if it's not being used for input. |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
<h2>Reference</h2> |
||||||
|
|
||||||
|
<h3 class=ref>void SMX_Start(SMXUpdateCallback UpdateCallback, void *pUser);</h3> |
||||||
|
|
||||||
|
Initialize, and start searching for devices. |
||||||
|
<p> |
||||||
|
UpdateCallback will be called when something happens: connection or disconnection, inputs |
||||||
|
changed, configuration updated, test data updated, etc. It doesn't specify what's changed, |
||||||
|
and the user should check all state that it's interested in. |
||||||
|
<p> |
||||||
|
This is called asynchronously from a helper thread, so the receiver must be thread-safe. |
||||||
|
|
||||||
|
<h3 class=ref>void SMX_Stop();</h3> |
||||||
|
|
||||||
|
Shut down and disconnect from all devices. This will wait for any user callbacks to complete, |
||||||
|
and no user callbacks will be called after this returns. |
||||||
|
|
||||||
|
<h3 class=ref>void SMX_SetLogCallback(SMXLogCallback callback);</h3> |
||||||
|
|
||||||
|
Set a function to receive diagnostic logs. By default, logs are written to stdout. |
||||||
|
This can be called before SMX_Start, so it affects any logs sent during initialization. |
||||||
|
|
||||||
|
<h3 class=ref>void SMX_GetInfo(int pad, SMXInfo *info);</h3> |
||||||
|
|
||||||
|
Get info about a pad. Use this to detect which pads are currently connected. |
||||||
|
|
||||||
|
<h3 class=ref>uint16_t SMX_GetInputState(int pad);</h3> |
||||||
|
|
||||||
|
Get a mask of the currently pressed panels. |
||||||
|
|
||||||
|
<h3 class=ref>void SMX_SetLights(const char lightsData[864]);</h3> |
||||||
|
Update the lights. Both pads are always updated together. lightsData is a list of 8-bit RGB |
||||||
|
colors, one for each LED. Each panel has lights in the following order: |
||||||
|
<p> |
||||||
|
<pre> |
||||||
|
0123 |
||||||
|
4567 |
||||||
|
89AB |
||||||
|
CDEF |
||||||
|
</pre> |
||||||
|
<p> |
||||||
|
Panels are in the following order: |
||||||
|
<p> |
||||||
|
<pre> |
||||||
|
012 9AB |
||||||
|
345 CDE |
||||||
|
678 F01 |
||||||
|
</pre> |
||||||
|
|
||||||
|
With 18 panels, 16 LEDs per panel and 3 bytes per LED, each light update has 864 bytes of data. |
||||||
|
<p> |
||||||
|
Lights will update at up to 30 FPS. If lights data is sent more quickly, a best effort will be |
||||||
|
made to send the most recent lights data available, but the panels won't update more quickly. |
||||||
|
<p> |
||||||
|
The panels will return to automatic lighting if no lights are received for a while, so applications |
||||||
|
controlling lights should send light updates continually, even if the lights aren't changing. |
||||||
|
|
||||||
|
<h3 class=ref>void SMX_ReenableAutoLights();</h3> |
||||||
|
|
||||||
|
By default, the panels light automatically when stepped on. If a lights command is sent by |
||||||
|
the application, this stops happening to allow the application to fully control lighting. |
||||||
|
If no lights update is received for a few seconds, automatic lighting is reenabled by the |
||||||
|
panels. |
||||||
|
<p> |
||||||
|
<code>SMX_ReenableAutoLights</code> can be called to immediately reenable auto-lighting, without waiting |
||||||
|
for the timeout period to elapse. Games don't need to call this, since the panels will return |
||||||
|
to auto-lighting mode automatically after a brief period of no updates. |
||||||
|
|
||||||
|
<h3 class=ref>void SMX_GetConfig(int pad, SMXConfig *config);</h3> |
||||||
|
|
||||||
|
Get the current controller's configuration. |
||||||
|
|
||||||
|
<h3 class=ref>void SMX_SetConfig(int pad, const SMXConfig *config);</h3> |
||||||
|
|
||||||
|
Update the current controller's configuration. This doesn't block, and the new configuration will |
||||||
|
be sent in the background. SMX_GetConfig will return the new configuration as soon as this call |
||||||
|
returns, without waiting for it to actually be sent to the controller. |
||||||
|
|
||||||
|
<h3 class=ref>void SMX_FactoryReset(int pad);</h3> |
||||||
|
|
||||||
|
Reset a pad to its original configuration. |
||||||
|
|
||||||
|
<h3 class=ref>void SMX_ForceRecalibration(int pad);</h3> |
||||||
|
|
||||||
|
Request an immediate panel recalibration. This is normally not necessary, but can be helpful |
||||||
|
for diagnostics. |
||||||
|
|
||||||
|
<h3 class=ref> |
||||||
|
void SMX_SetTestMode(int pad, SensorTestMode mode); |
||||||
|
<br> |
||||||
|
bool SMX_GetTestData(int pad, SMXSensorTestModeData *data); |
||||||
|
</h3> |
||||||
|
|
||||||
|
Set a panel test mode and request test data. This is used by the configuration tool. |
||||||
|
|
||||||
|
|
After Width: | Height: | Size: 416 KiB |
@ -0,0 +1,26 @@ |
|||||||
|
body { |
||||||
|
padding: 1em; |
||||||
|
max-width: 1000px; |
||||||
|
font-family: "Segoe UI",Helvetica,Arial,sans-serif; |
||||||
|
margin-left: auto; |
||||||
|
margin-right: auto; |
||||||
|
line-height: 1.5; |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: 2em; |
||||||
|
} |
||||||
|
h1, h2 { |
||||||
|
border-bottom: 1px solid #eeeeee; |
||||||
|
padding-bottom: .3em; |
||||||
|
} |
||||||
|
|
||||||
|
h3.ref { |
||||||
|
display: block; |
||||||
|
font-family: monospace; |
||||||
|
background-color: #008080; |
||||||
|
color: #FFFFFF; |
||||||
|
padding: .5em; |
||||||
|
|
||||||
|
|
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
#include <stdio.h> |
||||||
|
#include <windows.h> |
||||||
|
#include "SMX.h" |
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
using namespace std; |
||||||
|
|
||||||
|
class InputSample |
||||||
|
{ |
||||||
|
public: |
||||||
|
InputSample() |
||||||
|
{ |
||||||
|
// Set a logging callback. This can be called before SMX_Start.
|
||||||
|
// SMX_SetLogCallback( SMXLogCallback );
|
||||||
|
|
||||||
|
// Start scanning. The update callback will be called when devices connect or
|
||||||
|
// disconnect or panels are pressed or released. This callback will be called
|
||||||
|
// from a thread.
|
||||||
|
SMX_Start( SMXStateChangedCallback, this ); |
||||||
|
} |
||||||
|
|
||||||
|
static void SMXStateChangedCallback(int pad, SMXUpdateCallbackReason reason, void *pUser) |
||||||
|
{ |
||||||
|
InputSample *pSelf = (InputSample *) pUser; |
||||||
|
pSelf->SMXStateChanged( pad, reason ); |
||||||
|
} |
||||||
|
|
||||||
|
static void SMXLogCallback(const char *log) |
||||||
|
{ |
||||||
|
printf("-> %s\n", log); |
||||||
|
} |
||||||
|
|
||||||
|
void SMXStateChanged(int pad, SMXUpdateCallbackReason reason) |
||||||
|
{ |
||||||
|
printf("Device %i state changed: %04x\n", pad, SMX_GetInputState(pad)); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
int iPanelToLight = 0; |
||||||
|
void SetLights() |
||||||
|
{ |
||||||
|
string sLightsData; |
||||||
|
auto addColor = [&sLightsData](uint8_t r, uint8_t g, uint8_t b) |
||||||
|
{ |
||||||
|
sLightsData.append( 1, r ); |
||||||
|
sLightsData.append( 1, g ); |
||||||
|
sLightsData.append( 1, b ); |
||||||
|
}; |
||||||
|
for( int iPad = 0; iPad < 2; ++iPad ) |
||||||
|
{ |
||||||
|
for( int iPanel = 0; iPanel < 9; ++iPanel ) |
||||||
|
{ |
||||||
|
bool bLight = iPanel == iPanelToLight && iPad == 0; |
||||||
|
if( !bLight ) |
||||||
|
{ |
||||||
|
for( int iLED = 0; iLED < 16; ++iLED ) |
||||||
|
addColor( 0, 0, 0 ); |
||||||
|
continue; |
||||||
|
} |
||||||
|
addColor( 0xFF, 0, 0 ); |
||||||
|
addColor( 0xFF, 0, 0 ); |
||||||
|
addColor( 0xFF, 0, 0 ); |
||||||
|
addColor( 0xFF, 0, 0 ); |
||||||
|
addColor( 0, 0xFF, 0 ); |
||||||
|
addColor( 0, 0xFF, 0 ); |
||||||
|
addColor( 0, 0xFF, 0 ); |
||||||
|
addColor( 0, 0xFF, 0 ); |
||||||
|
addColor( 0, 0, 0xFF ); |
||||||
|
addColor( 0, 0, 0xFF ); |
||||||
|
addColor( 0, 0, 0xFF ); |
||||||
|
addColor( 0, 0, 0xFF ); |
||||||
|
addColor( 0xFF, 0xFF, 0 ); |
||||||
|
addColor( 0xFF, 0xFF, 0 ); |
||||||
|
addColor( 0xFF, 0xFF, 0 ); |
||||||
|
addColor( 0xFF, 0xFF, 0 ); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
SMX_SetLights( sLightsData.data() ); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
int main() |
||||||
|
{ |
||||||
|
InputSample demo; |
||||||
|
|
||||||
|
// Loop forever for this sample.
|
||||||
|
while(1) |
||||||
|
{ |
||||||
|
Sleep(500); |
||||||
|
demo.SetLights(); |
||||||
|
} |
||||||
|
|
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
@ -0,0 +1,244 @@ |
|||||||
|
#ifndef SMX_H |
||||||
|
#define SMX_H |
||||||
|
|
||||||
|
#include <stdint.h> |
||||||
|
|
||||||
|
#ifdef SMX_EXPORTS |
||||||
|
#define SMX_API __declspec(dllexport) |
||||||
|
#else |
||||||
|
#define SMX_API __declspec(dllimport) |
||||||
|
#endif |
||||||
|
|
||||||
|
struct SMXInfo; |
||||||
|
struct SMXConfig; |
||||||
|
enum SensorTestMode; |
||||||
|
enum SMXUpdateCallbackReason; |
||||||
|
struct SMXSensorTestModeData; |
||||||
|
|
||||||
|
// All functions are nonblocking. Getters will return the most recent state. Setters will
|
||||||
|
// return immediately and do their work in the background. No functions return errors, and
|
||||||
|
// setting data on a pad which isn't connected will have no effect.
|
||||||
|
|
||||||
|
// Initialize, and start searching for devices.
|
||||||
|
//
|
||||||
|
// UpdateCallback will be called when something happens: connection or disconnection, inputs
|
||||||
|
// changed, configuration updated, test data updated, etc. It doesn't specify what's changed,
|
||||||
|
// and the user should check all state that it's interested in.
|
||||||
|
//
|
||||||
|
// This is called asynchronously from a helper thread, so the receiver must be thread-safe.
|
||||||
|
typedef void SMXUpdateCallback(int pad, SMXUpdateCallbackReason reason, void *pUser); |
||||||
|
extern "C" SMX_API void SMX_Start(SMXUpdateCallback UpdateCallback, void *pUser); |
||||||
|
|
||||||
|
// Shut down and disconnect from all devices. This will wait for any user callbacks to complete,
|
||||||
|
// and no user callbacks will be called after this returns. This must not be called from within
|
||||||
|
// the update callback.
|
||||||
|
extern "C" SMX_API void SMX_Stop(); |
||||||
|
|
||||||
|
// Set a function to receive diagnostic logs. By default, logs are written to stdout.
|
||||||
|
// This can be called before SMX_Start, so it affects any logs sent during initialization.
|
||||||
|
typedef void SMXLogCallback(const char *log); |
||||||
|
extern "C" SMX_API void SMX_SetLogCallback(SMXLogCallback callback); |
||||||
|
|
||||||
|
// Get info about a pad. Use this to detect which pads are currently connected.
|
||||||
|
extern "C" SMX_API void SMX_GetInfo(int pad, SMXInfo *info); |
||||||
|
|
||||||
|
// Get a mask of the currently pressed panels.
|
||||||
|
extern "C" SMX_API uint16_t SMX_GetInputState(int pad); |
||||||
|
|
||||||
|
// Update the lights. Both pads are always updated together. lightsData is a list of 8-bit RGB
|
||||||
|
// colors, one for each LED. Each panel has lights in the following order:
|
||||||
|
//
|
||||||
|
// 0123
|
||||||
|
// 4567
|
||||||
|
// 89AB
|
||||||
|
// CDEF
|
||||||
|
//
|
||||||
|
// Panels are in the following order:
|
||||||
|
//
|
||||||
|
// 012 9AB
|
||||||
|
// 345 CDE
|
||||||
|
// 678 F01
|
||||||
|
//
|
||||||
|
// With 18 panels, 16 LEDs per panel and 3 bytes per LED, each light update has 864 bytes of data.
|
||||||
|
//
|
||||||
|
// Lights will update at up to 30 FPS. If lights data is sent more quickly, a best effort will be
|
||||||
|
// made to send the most recent lights data available, but the panels won't update more quickly.
|
||||||
|
//
|
||||||
|
// The panels will return to automatic lighting if no lights are received for a while, so applications
|
||||||
|
// controlling lights should send light updates continually, even if the lights aren't changing.
|
||||||
|
extern "C" SMX_API void SMX_SetLights(const char lightsData[864]); |
||||||
|
|
||||||
|
// By default, the panels light automatically when stepped on. If a lights command is sent by
|
||||||
|
// the application, this stops happening to allow the application to fully control lighting.
|
||||||
|
// If no lights update is received for a few seconds, automatic lighting is reenabled by the
|
||||||
|
// panels.
|
||||||
|
//
|
||||||
|
// SMX_ReenableAutoLights can be called to immediately reenable auto-lighting, without waiting
|
||||||
|
// for the timeout period to elapse. Games don't need to call this, since the panels will return
|
||||||
|
// to auto-lighting mode automatically after a brief period of no updates.
|
||||||
|
extern "C" SMX_API void SMX_ReenableAutoLights(); |
||||||
|
|
||||||
|
// Get the current controller's configuration.
|
||||||
|
extern "C" SMX_API void SMX_GetConfig(int pad, SMXConfig *config); |
||||||
|
|
||||||
|
// Update the current controller's configuration. This doesn't block, and the new configuration will
|
||||||
|
// be sent in the background. SMX_GetConfig will return the new configuration as soon as this call
|
||||||
|
// returns, without waiting for it to actually be sent to the controller.
|
||||||
|
extern "C" SMX_API void SMX_SetConfig(int pad, const SMXConfig *config); |
||||||
|
|
||||||
|
// Reset a pad to its original configuration.
|
||||||
|
extern "C" SMX_API void SMX_FactoryReset(int pad); |
||||||
|
|
||||||
|
// Request an immediate panel recalibration. This is normally not necessary, but can be helpful
|
||||||
|
// for diagnostics.
|
||||||
|
extern "C" SMX_API void SMX_ForceRecalibration(int pad); |
||||||
|
|
||||||
|
// Set a panel test mode and request test data. This is used by the configuration tool.
|
||||||
|
extern "C" SMX_API void SMX_SetTestMode(int pad, SensorTestMode mode); |
||||||
|
extern "C" SMX_API bool SMX_GetTestData(int pad, SMXSensorTestModeData *data); |
||||||
|
|
||||||
|
// General info about a connected controller. This can be retrieved with SMX_GetInfo.
|
||||||
|
struct SMXInfo |
||||||
|
{ |
||||||
|
// True if we're fully connected to this controller. If this is false, the other
|
||||||
|
// fields won't be set.
|
||||||
|
bool m_bConnected = false; |
||||||
|
|
||||||
|
// This device's serial number. This can be used to distinguish devices from each
|
||||||
|
// other if more than one is connected. This is a null-terminated string instead
|
||||||
|
// of a C++ string for C# marshalling.
|
||||||
|
char m_Serial[33]; |
||||||
|
|
||||||
|
// This device's firmware version.
|
||||||
|
uint16_t m_iFirmwareVersion; |
||||||
|
}; |
||||||
|
|
||||||
|
enum SMXUpdateCallbackReason { |
||||||
|
// This is called when a generic state change happens: connection or disconnection, inputs changed,
|
||||||
|
// test data updated, etc. It doesn't specify what's changed. We simply check the whole state.
|
||||||
|
SMXUpdateCallback_Updated, |
||||||
|
|
||||||
|
// This is called when SMX_FactoryReset completes, indicating that SMX_GetConfig will now return
|
||||||
|
// the reset configuration.
|
||||||
|
SMXUpdateCallback_FactoryResetCommandComplete |
||||||
|
}; |
||||||
|
|
||||||
|
// The configuration for a connected controller. This can be retrieved with SMX_GetConfig
|
||||||
|
// and modified with SMX_SetConfig.
|
||||||
|
//
|
||||||
|
// The order and packing of this struct corresponds to the configuration packet sent to
|
||||||
|
// the master controller, so it must not be changed.
|
||||||
|
struct SMXConfig |
||||||
|
{ |
||||||
|
// These fields are unused and must be left at their existing values.
|
||||||
|
uint8_t unused1 = 0xFF, unused2 = 0xFF; |
||||||
|
uint8_t unused3 = 0xFF, unused4 = 0xFF; |
||||||
|
uint8_t unused5 = 0xFF, unused6 = 0xFF; |
||||||
|
|
||||||
|
// Panel thresholds are labelled by their numpad position, eg. Panel8 is up.
|
||||||
|
// If m_iFirmwareVersion is 1, Panel7 corresponds to all of up, down, left and
|
||||||
|
// right, and Panel2 corresponds to UpLeft, UpRight, DownLeft and DownRight. For
|
||||||
|
// later firmware versions, each panel is configured independently.
|
||||||
|
//
|
||||||
|
// Setting a value to 0xFF disables that threshold.
|
||||||
|
uint16_t masterDebounceMilliseconds = 0; |
||||||
|
uint8_t panelThreshold7Low = 0xFF, panelThreshold7High = 0xFF; // was "cardinal"
|
||||||
|
uint8_t panelThreshold4Low = 0xFF, panelThreshold4High = 0xFF; // was "center"
|
||||||
|
uint8_t panelThreshold2Low = 0xFF, panelThreshold2High = 0xFF; // was "corner"
|
||||||
|
|
||||||
|
// These are internal tunables and should be left unchanged.
|
||||||
|
uint16_t panelDebounceMicroseconds = 4000; |
||||||
|
uint16_t autoCalibrationPeriodMilliseconds = 1000; |
||||||
|
uint8_t autoCalibrationMaxDeviation = 100; |
||||||
|
uint8_t badSensorMinimumDelaySeconds = 15; |
||||||
|
uint16_t autoCalibrationAveragesPerUpdate = 60; |
||||||
|
|
||||||
|
uint8_t unused7 = 0xFF, unused8 = 0xFF; |
||||||
|
|
||||||
|
uint8_t panelThreshold1Low = 0xFF, panelThreshold1High = 0xFF; // was "up"
|
||||||
|
|
||||||
|
// Which sensors on each panel to enable. This can be used to disable sensors that
|
||||||
|
// we know aren't populated. This is packed, with four sensors on two pads per byte:
|
||||||
|
// enabledSensors[0] & 1 is the first sensor on the first pad, and so on.
|
||||||
|
uint8_t enabledSensors[5]; |
||||||
|
|
||||||
|
// How long the master controller will wait for a lights command before assuming the
|
||||||
|
// game has gone away and resume auto-lights. This is in 128ms units.
|
||||||
|
uint8_t autoLightsTimeout = 1000/128; // 1 second
|
||||||
|
|
||||||
|
// The color to use for each panel when auto-lighting in master mode. This doesn't
|
||||||
|
// apply when the pads are in autonomous lighting mode (no master), since they don't
|
||||||
|
// store any configuration by themselves. These colors should be scaled to the 0-170
|
||||||
|
// range.
|
||||||
|
uint8_t stepColor[3*9]; |
||||||
|
|
||||||
|
// The rotation of the panel, where 0 is the standard rotation, 1 means the panel is
|
||||||
|
// rotated right 90 degrees, 2 is rotated 180 degrees, and 3 is rotated 270 degrees.
|
||||||
|
// This value is unused.
|
||||||
|
uint8_t panelRotation; |
||||||
|
|
||||||
|
// This is an internal tunable that should be left unchanged.
|
||||||
|
uint16_t autoCalibrationSamplesPerAverage = 500; |
||||||
|
|
||||||
|
// The firmware version of the master controller. Where supported (version 2 and up), this
|
||||||
|
// will always read back the firmware version. This will default to 0xFF on version 1, and
|
||||||
|
// we'll always write 0xFF here so it doesn't change on that firmware version.
|
||||||
|
//
|
||||||
|
// We don't need this since we can read the "I" command which also reports the version, but
|
||||||
|
// this allows panels to also know the master version.
|
||||||
|
uint8_t masterVersion = 0xFF; |
||||||
|
|
||||||
|
// The version of this config packet. This can be used by the firmware to know which values
|
||||||
|
// have been filled in. Any values not filled in will always be 0xFF, which can be tested
|
||||||
|
// for, but that doesn't work for values where 0xFF is a valid value. This value is unrelated
|
||||||
|
// to the firmware version, and just indicates which fields in this packet have been set.
|
||||||
|
// Note that we don't need to increase this any time we add a field, only when it's important
|
||||||
|
// that we be able to tell if a field is set or not.
|
||||||
|
//
|
||||||
|
// Versions:
|
||||||
|
// - 0xFF: This is a config packet from before configVersion was added.
|
||||||
|
// - 0x00: configVersion added
|
||||||
|
// - 0x02: panelThreshold0Low through panelThreshold8High added
|
||||||
|
uint8_t configVersion = 0x02; |
||||||
|
|
||||||
|
// The remaining thresholds (configVersion >= 2).
|
||||||
|
uint8_t unused9[10]; |
||||||
|
uint8_t panelThreshold0Low, panelThreshold0High; |
||||||
|
uint8_t panelThreshold3Low, panelThreshold3High; |
||||||
|
uint8_t panelThreshold5Low, panelThreshold5High; |
||||||
|
uint8_t panelThreshold6Low, panelThreshold6High; |
||||||
|
uint8_t panelThreshold8Low, panelThreshold8High; |
||||||
|
}; |
||||||
|
static_assert(sizeof(SMXConfig) == 84, "Expected 84 bytes"); |
||||||
|
|
||||||
|
// The values (except for Off) correspond with the protocol and must not be changed.
|
||||||
|
enum SensorTestMode { |
||||||
|
SensorTestMode_Off = 0, |
||||||
|
// Return the raw, uncalibrated value of each sensor.
|
||||||
|
SensorTestMode_UncalibratedValues = '0', |
||||||
|
|
||||||
|
// Return the calibrated value of each sensor.
|
||||||
|
SensorTestMode_CalibratedValues = '1', |
||||||
|
|
||||||
|
// Return the sensor noise value.
|
||||||
|
SensorTestMode_Noise = '2', |
||||||
|
|
||||||
|
// Return the sensor tare value.
|
||||||
|
SensorTestMode_Tare = '3', |
||||||
|
}; |
||||||
|
|
||||||
|
// Data for the current SensorTestMode. The interpretation of sensorLevel depends on the mode.
|
||||||
|
struct SMXSensorTestModeData |
||||||
|
{ |
||||||
|
// If false, sensorLevel[n][*] is zero because we didn't receive a response from that panel.
|
||||||
|
bool bHaveDataFromPanel[9]; |
||||||
|
|
||||||
|
int16_t sensorLevel[9][4]; |
||||||
|
bool bBadSensorInput[9][4]; |
||||||
|
|
||||||
|
// The DIP switch settings on each panel. This is used for diagnostics
|
||||||
|
// displays.
|
||||||
|
int iDIPSwitchPerPanel[9]; |
||||||
|
}; |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,285 @@ |
|||||||
|
#include "Helpers.h" |
||||||
|
#include <windows.h> |
||||||
|
#include <algorithm> |
||||||
|
using namespace std; |
||||||
|
using namespace SMX; |
||||||
|
|
||||||
|
namespace { |
||||||
|
function<void(const string &log)> g_LogCallback = [](const string &log) { |
||||||
|
printf("%6.3f: %s\n", GetMonotonicTime(), log.c_str()); |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
void SMX::Log(string s) |
||||||
|
{ |
||||||
|
g_LogCallback(s); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SetLogCallback(function<void(const string &log)> callback) |
||||||
|
{ |
||||||
|
g_LogCallback = callback; |
||||||
|
} |
||||||
|
|
||||||
|
const DWORD MS_VC_EXCEPTION = 0x406D1388;
|
||||||
|
#pragma pack(push,8) |
||||||
|
typedef struct tagTHREADNAME_INFO
|
||||||
|
{
|
||||||
|
DWORD dwType; // Must be 0x1000.
|
||||||
|
LPCSTR szName; // Pointer to name (in user addr space).
|
||||||
|
DWORD dwThreadID; // Thread ID (-1=caller thread).
|
||||||
|
DWORD dwFlags; // Reserved for future use, must be zero.
|
||||||
|
} THREADNAME_INFO;
|
||||||
|
|
||||||
|
#pragma pack(pop) |
||||||
|
void SMX::SetThreadName(DWORD iThreadId, const string &name) |
||||||
|
{ |
||||||
|
|
||||||
|
THREADNAME_INFO info;
|
||||||
|
info.dwType = 0x1000;
|
||||||
|
info.szName = name.c_str();
|
||||||
|
info.dwThreadID = iThreadId; |
||||||
|
info.dwFlags = 0;
|
||||||
|
#pragma warning(push) |
||||||
|
#pragma warning(disable: 6320 6322) |
||||||
|
__try{
|
||||||
|
RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);
|
||||||
|
}
|
||||||
|
__except (EXCEPTION_EXECUTE_HANDLER) { |
||||||
|
}
|
||||||
|
#pragma warning(pop) |
||||||
|
}
|
||||||
|
|
||||||
|
void SMX::StripCrnl(wstring &s) |
||||||
|
{ |
||||||
|
while(s.size() && (s[s.size()-1] == '\r' || s[s.size()-1] == '\n')) |
||||||
|
s.erase(s.size()-1); |
||||||
|
} |
||||||
|
|
||||||
|
wstring SMX::GetErrorString(int err) |
||||||
|
{ |
||||||
|
wchar_t buf[1024] = L""; |
||||||
|
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, err, 0, buf, sizeof(buf), NULL); |
||||||
|
|
||||||
|
// Fix badly formatted strings returned by FORMAT_MESSAGE_FROM_SYSTEM.
|
||||||
|
wstring sResult = buf; |
||||||
|
StripCrnl(sResult); |
||||||
|
return sResult; |
||||||
|
} |
||||||
|
|
||||||
|
string SMX::vssprintf(const char *szFormat, va_list argList) |
||||||
|
{ |
||||||
|
string sStr; |
||||||
|
|
||||||
|
char *pBuf = NULL; |
||||||
|
int iChars = 128; |
||||||
|
int iUsed = 0; |
||||||
|
int iTry = 0; |
||||||
|
|
||||||
|
do |
||||||
|
{ |
||||||
|
// Grow more than linearly (e.g. 512, 1536, 3072, etc)
|
||||||
|
iChars += iTry * 1024; |
||||||
|
delete[] pBuf; |
||||||
|
pBuf = new char[iChars]; |
||||||
|
iUsed = vsnprintf(pBuf, iChars-1, szFormat, argList); |
||||||
|
++iTry; |
||||||
|
} while(iUsed < 0); |
||||||
|
|
||||||
|
// assign whatever we managed to format
|
||||||
|
sStr.assign(pBuf, iUsed); |
||||||
|
|
||||||
|
delete[] pBuf; |
||||||
|
|
||||||
|
return sStr; |
||||||
|
} |
||||||
|
|
||||||
|
string SMX::ssprintf(const char *fmt, ...) |
||||||
|
{ |
||||||
|
va_list va; |
||||||
|
va_start(va, fmt); |
||||||
|
return vssprintf(fmt, va); |
||||||
|
} |
||||||
|
|
||||||
|
string SMX::BinaryToHex(const void *pData_, int iNumBytes) |
||||||
|
{ |
||||||
|
const unsigned char *pData = (const unsigned char *) pData_; |
||||||
|
string s; |
||||||
|
for(int i=0; i<iNumBytes; i++) |
||||||
|
{ |
||||||
|
unsigned val = pData[i]; |
||||||
|
s += ssprintf("%02x", val); |
||||||
|
} |
||||||
|
return s; |
||||||
|
} |
||||||
|
|
||||||
|
string SMX::BinaryToHex(const string &sString) |
||||||
|
{ |
||||||
|
return BinaryToHex(sString.data(), sString.size()); |
||||||
|
} |
||||||
|
|
||||||
|
bool SMX::GetRandomBytes(void *pData, int iBytes) |
||||||
|
{ |
||||||
|
HCRYPTPROV hCryptProvider = 0; |
||||||
|
if (!CryptAcquireContext(&hCryptProvider, NULL, MS_DEF_PROV, PROV_RSA_FULL, (CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET)) &&
|
||||||
|
!CryptAcquireContext(&hCryptProvider, NULL, MS_DEF_PROV, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET | CRYPT_NEWKEYSET)) |
||||||
|
return 0; |
||||||
|
|
||||||
|
bool bSuccess = !!CryptGenRandom(hCryptProvider, iBytes, (uint8_t *) pData); |
||||||
|
CryptReleaseContext(hCryptProvider, 0); |
||||||
|
return bSuccess; |
||||||
|
} |
||||||
|
|
||||||
|
// Monotonic timer code from https://stackoverflow.com/questions/24330496.
|
||||||
|
// Why is this hard?
|
||||||
|
//
|
||||||
|
// This code has backwards compatibility to XP, but we only officially support and
|
||||||
|
// test back to Windows 7, so that code path isn't tested.
|
||||||
|
typedef struct _KSYSTEM_TIME { |
||||||
|
ULONG LowPart; |
||||||
|
LONG High1Time; |
||||||
|
LONG High2Time; |
||||||
|
} KSYSTEM_TIME; |
||||||
|
#define KUSER_SHARED_DATA 0x7ffe0000 |
||||||
|
#define InterruptTime ((KSYSTEM_TIME volatile*)(KUSER_SHARED_DATA + 0x08)) |
||||||
|
#define InterruptTimeBias ((ULONGLONG volatile*)(KUSER_SHARED_DATA + 0x3b0)) |
||||||
|
|
||||||
|
namespace { |
||||||
|
LONGLONG ReadInterruptTime() |
||||||
|
{ |
||||||
|
// Reading the InterruptTime from KUSER_SHARED_DATA is much better than
|
||||||
|
// using GetTickCount() because it doesn't wrap, and is even a little quicker.
|
||||||
|
// This works on all Windows NT versions (NT4 and up).
|
||||||
|
LONG timeHigh; |
||||||
|
ULONG timeLow; |
||||||
|
do { |
||||||
|
timeHigh = InterruptTime->High1Time; |
||||||
|
timeLow = InterruptTime->LowPart; |
||||||
|
} while (timeHigh != InterruptTime->High2Time); |
||||||
|
LONGLONG now = ((LONGLONG)timeHigh << 32) + timeLow; |
||||||
|
static LONGLONG d = now; |
||||||
|
return now - d; |
||||||
|
} |
||||||
|
|
||||||
|
LONGLONG ScaleQpc(LONGLONG qpc) |
||||||
|
{ |
||||||
|
// We do the actual scaling in fixed-point rather than floating, to make sure
|
||||||
|
// that we don't violate monotonicity due to rounding errors. There's no
|
||||||
|
// need to cache QueryPerformanceFrequency().
|
||||||
|
LARGE_INTEGER frequency; |
||||||
|
QueryPerformanceFrequency(&frequency); |
||||||
|
double fraction = 10000000/double(frequency.QuadPart); |
||||||
|
LONGLONG denom = 1024; |
||||||
|
LONGLONG numer = max(1LL, (LONGLONG)(fraction*denom + 0.5)); |
||||||
|
return qpc * numer / denom; |
||||||
|
} |
||||||
|
|
||||||
|
ULONGLONG ReadUnbiasedQpc() |
||||||
|
{ |
||||||
|
// We remove the suspend bias added to QueryPerformanceCounter results by
|
||||||
|
// subtracting the interrupt time bias, which is not strictly speaking legal,
|
||||||
|
// but the units are correct and I think it's impossible for the resulting
|
||||||
|
// "unbiased QPC" value to go backwards.
|
||||||
|
LONGLONG interruptTimeBias, qpc; |
||||||
|
do { |
||||||
|
interruptTimeBias = *InterruptTimeBias; |
||||||
|
LARGE_INTEGER counter; |
||||||
|
QueryPerformanceCounter(&counter); |
||||||
|
qpc = counter.QuadPart; |
||||||
|
} while (interruptTimeBias != *InterruptTimeBias); |
||||||
|
static std::pair<LONGLONG,LONGLONG> d(qpc, interruptTimeBias); |
||||||
|
return ScaleQpc(qpc - d.first) - (interruptTimeBias - d.second); |
||||||
|
} |
||||||
|
|
||||||
|
bool Win7OrLater() |
||||||
|
{ |
||||||
|
static int iWin7OrLater = -1; |
||||||
|
if(iWin7OrLater != -1) |
||||||
|
return bool(iWin7OrLater); |
||||||
|
|
||||||
|
OSVERSIONINFOW ver = { sizeof(OSVERSIONINFOW), }; |
||||||
|
GetVersionEx(&ver); |
||||||
|
iWin7OrLater = (ver.dwMajorVersion > 6 || (ver.dwMajorVersion == 6 && ver.dwMinorVersion >= 1)); |
||||||
|
return bool(iWin7OrLater); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// getMonotonicTime() returns the time elapsed since the application's first
|
||||||
|
/// call to getMonotonicTime(), in 100ns units. The values returned are
|
||||||
|
/// guaranteed to be monotonic. The time ticks in 15ms resolution and advances
|
||||||
|
/// during suspend on XP and Vista, but we manage to avoid this on Windows 7
|
||||||
|
/// and 8, which also use a high-precision timer. The time does not wrap after
|
||||||
|
/// 49 days.
|
||||||
|
double SMX::GetMonotonicTime() |
||||||
|
{ |
||||||
|
// On Windows XP and earlier, QueryPerformanceCounter is not monotonic so we
|
||||||
|
// steer well clear of it; on Vista, it's just a bit slow.
|
||||||
|
uint64_t iTime = Win7OrLater()? ReadUnbiasedQpc() : ReadInterruptTime(); |
||||||
|
return iTime / 10000000.0; |
||||||
|
} |
||||||
|
|
||||||
|
SMX::AutoCloseHandle::AutoCloseHandle(HANDLE h) |
||||||
|
{ |
||||||
|
handle = h; |
||||||
|
} |
||||||
|
|
||||||
|
SMX::AutoCloseHandle::~AutoCloseHandle() |
||||||
|
{ |
||||||
|
if(handle != INVALID_HANDLE_VALUE) |
||||||
|
CloseHandle(handle); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::Mutex::Mutex() |
||||||
|
{ |
||||||
|
m_hLock = CreateMutex(NULL, false, NULL); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::Mutex::~Mutex() |
||||||
|
{ |
||||||
|
CloseHandle(m_hLock); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::Mutex::Lock() |
||||||
|
{ |
||||||
|
WaitForSingleObject(m_hLock, INFINITE); |
||||||
|
m_iLockedByThread = GetCurrentThreadId(); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::Mutex::Unlock() |
||||||
|
{ |
||||||
|
m_iLockedByThread = 0; |
||||||
|
ReleaseMutex(m_hLock); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::Mutex::AssertNotLockedByCurrentThread() |
||||||
|
{ |
||||||
|
if(m_iLockedByThread == GetCurrentThreadId()) |
||||||
|
throw exception("Expected to not be locked"); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::Mutex::AssertLockedByCurrentThread() |
||||||
|
{ |
||||||
|
if(m_iLockedByThread != GetCurrentThreadId()) |
||||||
|
throw exception("Expected to be locked"); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::LockMutex::LockMutex(SMX::Mutex &mutex): |
||||||
|
m_Mutex(mutex) |
||||||
|
{ |
||||||
|
m_Mutex.AssertNotLockedByCurrentThread(); |
||||||
|
m_Mutex.Lock(); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::LockMutex::~LockMutex() |
||||||
|
{ |
||||||
|
m_Mutex.AssertLockedByCurrentThread(); |
||||||
|
m_Mutex.Unlock(); |
||||||
|
} |
||||||
|
|
||||||
|
// This is a helper to let the config tool open a window, which has no freopen.
|
||||||
|
// This isn't exposed in SMX.h.
|
||||||
|
extern "C" __declspec(dllexport) void SMX_Internal_OpenConsole() |
||||||
|
{ |
||||||
|
AllocConsole(); |
||||||
|
freopen("CONOUT$","wb", stdout); |
||||||
|
freopen("CONOUT$","wb", stderr); |
||||||
|
} |
@ -0,0 +1,104 @@ |
|||||||
|
#ifndef HELPERS_H |
||||||
|
#define HELPERS_H |
||||||
|
|
||||||
|
#include <string> |
||||||
|
#include <stdarg.h> |
||||||
|
#include <windows.h> |
||||||
|
#include <functional> |
||||||
|
#include <memory> |
||||||
|
using namespace std; |
||||||
|
|
||||||
|
namespace SMX |
||||||
|
{ |
||||||
|
void Log(string s); |
||||||
|
|
||||||
|
// Set a function to receive logs written by SMX::Log. By default, logs are written
|
||||||
|
// to stdout.
|
||||||
|
void SetLogCallback(function<void(const string &log)> callback); |
||||||
|
|
||||||
|
void SetThreadName(DWORD iThreadId, const string &name); |
||||||
|
void StripCrnl(wstring &s); |
||||||
|
wstring GetErrorString(int err); |
||||||
|
string vssprintf(const char *szFormat, va_list argList); |
||||||
|
string ssprintf(const char *fmt, ...); |
||||||
|
string BinaryToHex(const void *pData_, int iNumBytes); |
||||||
|
string BinaryToHex(const string &sString); |
||||||
|
bool GetRandomBytes(void *pData, int iBytes); |
||||||
|
double GetMonotonicTime(); |
||||||
|
|
||||||
|
// In order to be able to use smart pointers to fully manage an object, we need to get
|
||||||
|
// a shared_ptr to pass around, but also store a weak_ptr in the object itself. This
|
||||||
|
// lets the object create shared_ptrs for itself as needed, without keeping itself from
|
||||||
|
// being deallocated.
|
||||||
|
//
|
||||||
|
// This helper allows this pattern:
|
||||||
|
//
|
||||||
|
// struct Class
|
||||||
|
// {
|
||||||
|
// Class(shared_ptr<Class> &pSelf): m_pSelf(GetPointers(pSelf, this)) { }
|
||||||
|
// const weak_ptr<Class> m_pSelf;
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// shared_ptr<Class> obj;
|
||||||
|
// new Class(obj);
|
||||||
|
//
|
||||||
|
// For a more convenient way to invoke this, see CreateObj() below.
|
||||||
|
|
||||||
|
template<typename T> |
||||||
|
weak_ptr<T> GetPointers(shared_ptr<T> &pSharedPtr, T *pObj) |
||||||
|
{ |
||||||
|
pSharedPtr.reset(pObj); |
||||||
|
return pSharedPtr; |
||||||
|
} |
||||||
|
|
||||||
|
// Create a class that retains a weak reference to itself, returning a shared_ptr.
|
||||||
|
template<typename T, class... Args> |
||||||
|
shared_ptr<T> CreateObj(Args&&... args) |
||||||
|
{ |
||||||
|
shared_ptr<typename T> pResult; |
||||||
|
new T(pResult, std::forward<Args>(args)...); |
||||||
|
return dynamic_pointer_cast<T>(pResult); |
||||||
|
} |
||||||
|
|
||||||
|
class AutoCloseHandle |
||||||
|
{ |
||||||
|
public: |
||||||
|
AutoCloseHandle(HANDLE h); |
||||||
|
~AutoCloseHandle(); |
||||||
|
HANDLE value() const { return handle; } |
||||||
|
|
||||||
|
private: |
||||||
|
AutoCloseHandle(const AutoCloseHandle &rhs); |
||||||
|
AutoCloseHandle &operator=(const AutoCloseHandle &rhs); |
||||||
|
HANDLE handle; |
||||||
|
}; |
||||||
|
|
||||||
|
class Mutex |
||||||
|
{ |
||||||
|
public: |
||||||
|
Mutex(); |
||||||
|
~Mutex(); |
||||||
|
void Lock(); |
||||||
|
void Unlock(); |
||||||
|
|
||||||
|
void AssertNotLockedByCurrentThread(); |
||||||
|
void AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
private: |
||||||
|
HANDLE m_hLock = INVALID_HANDLE_VALUE; |
||||||
|
DWORD m_iLockedByThread = 0; |
||||||
|
}; |
||||||
|
|
||||||
|
// A local lock helper for Mutex.
|
||||||
|
class LockMutex |
||||||
|
{ |
||||||
|
public: |
||||||
|
LockMutex(Mutex &mutex); |
||||||
|
~LockMutex(); |
||||||
|
|
||||||
|
private: |
||||||
|
Mutex &m_Mutex; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,65 @@ |
|||||||
|
// This implements the public API.
|
||||||
|
|
||||||
|
#include <windows.h> |
||||||
|
#include <memory> |
||||||
|
|
||||||
|
#include "../SMX.h" |
||||||
|
#include "SMXManager.h" |
||||||
|
#include "SMXDevice.h" |
||||||
|
using namespace std; |
||||||
|
using namespace SMX; |
||||||
|
|
||||||
|
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) |
||||||
|
{ |
||||||
|
switch(ul_reason_for_call) |
||||||
|
{ |
||||||
|
case DLL_PROCESS_ATTACH: |
||||||
|
case DLL_THREAD_ATTACH: |
||||||
|
case DLL_THREAD_DETACH: |
||||||
|
case DLL_PROCESS_DETACH: |
||||||
|
break; |
||||||
|
} |
||||||
|
return TRUE; |
||||||
|
} |
||||||
|
|
||||||
|
static shared_ptr<SMXManager> g_pSMX; |
||||||
|
|
||||||
|
// DLL interface:
|
||||||
|
SMX_API void SMX_Start(SMXUpdateCallback callback, void *pUser) |
||||||
|
{ |
||||||
|
if(g_pSMX != NULL) |
||||||
|
return; |
||||||
|
|
||||||
|
// The C++ interface takes a std::function, which doesn't need a user pointer. We add
|
||||||
|
// one for the C interface for convenience.
|
||||||
|
auto UpdateCallback = [callback, pUser](int pad, SMXUpdateCallbackReason reason) { |
||||||
|
callback(pad, reason, pUser); |
||||||
|
}; |
||||||
|
|
||||||
|
// Log(ssprintf("Struct sizes (native): %i %i %i\n", sizeof(SMXConfig), sizeof(SMXInfo), sizeof(SMXSensorTestModeData)));
|
||||||
|
g_pSMX = make_shared<SMXManager>(UpdateCallback); |
||||||
|
} |
||||||
|
|
||||||
|
SMX_API void SMX_Stop() |
||||||
|
{ |
||||||
|
g_pSMX.reset(); |
||||||
|
} |
||||||
|
|
||||||
|
SMX_API void SMX_SetLogCallback(SMXLogCallback callback) |
||||||
|
{ |
||||||
|
// Wrap the C callback with a C++ one.
|
||||||
|
SMX::SetLogCallback([callback](const string &log) { |
||||||
|
callback(log.c_str()); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
SMX_API void SMX_GetConfig(int pad, SMXConfig *config) { g_pSMX->GetDevice(pad)->GetConfig(*config); } |
||||||
|
SMX_API void SMX_SetConfig(int pad, const SMXConfig *config) { g_pSMX->GetDevice(pad)->SetConfig(*config); } |
||||||
|
SMX_API void SMX_GetInfo(int pad, SMXInfo *info) { g_pSMX->GetDevice(pad)->GetInfo(*info); } |
||||||
|
SMX_API uint16_t SMX_GetInputState(int pad) { return g_pSMX->GetDevice(pad)->GetInputState(); } |
||||||
|
SMX_API void SMX_FactoryReset(int pad) { g_pSMX->GetDevice(pad)->FactoryReset(); } |
||||||
|
SMX_API void SMX_ForceRecalibration(int pad) { g_pSMX->GetDevice(pad)->ForceRecalibration(); } |
||||||
|
SMX_API void SMX_SetTestMode(int pad, SensorTestMode mode) { g_pSMX->GetDevice(pad)->SetSensorTestMode((SensorTestMode) mode); } |
||||||
|
SMX_API bool SMX_GetTestData(int pad, SMXSensorTestModeData *data) { return g_pSMX->GetDevice(pad)->GetTestData(*data); } |
||||||
|
SMX_API void SMX_SetLights(const char lightsData[864]) { g_pSMX->SetLights(string(lightsData, 864)); } |
||||||
|
SMX_API void SMX_ReenableAutoLights() { g_pSMX->ReenableAutoLights(); } |
@ -0,0 +1,494 @@ |
|||||||
|
#include "SMXDevice.h" |
||||||
|
|
||||||
|
#include "../SMX.h" |
||||||
|
#include "Helpers.h" |
||||||
|
#include "SMXDeviceConnection.h" |
||||||
|
#include "SMXDeviceSearch.h" |
||||||
|
#include <windows.h> |
||||||
|
#include <memory> |
||||||
|
#include <vector> |
||||||
|
#include <map> |
||||||
|
using namespace std; |
||||||
|
using namespace SMX; |
||||||
|
|
||||||
|
// Extract test data for panel iPanel.
|
||||||
|
static void ReadDataForPanel(const vector<uint16_t> &data, int iPanel, void *pOut, int iOutSize) |
||||||
|
{ |
||||||
|
int m_iBit = 0; |
||||||
|
|
||||||
|
uint8_t *p = (uint8_t *) pOut; |
||||||
|
|
||||||
|
// Read each byte.
|
||||||
|
for(int i = 0; i < iOutSize; ++i) |
||||||
|
{ |
||||||
|
// Read each bit in this byte.
|
||||||
|
uint8_t result = 0; |
||||||
|
for(int j = 0; j < 8; ++j) |
||||||
|
{ |
||||||
|
bool bit = false; |
||||||
|
|
||||||
|
if(m_iBit < data.size()) |
||||||
|
{ |
||||||
|
bit = data[m_iBit] & (1 << iPanel); |
||||||
|
m_iBit++; |
||||||
|
} |
||||||
|
|
||||||
|
result |= bit << j; |
||||||
|
} |
||||||
|
|
||||||
|
*p++ = result; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
shared_ptr<SMXDevice> SMX::SMXDevice::Create(shared_ptr<AutoCloseHandle> hEvent, Mutex &lock) |
||||||
|
{ |
||||||
|
return CreateObj<SMXDevice>(hEvent, lock); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::SMXDevice::SMXDevice(shared_ptr<SMXDevice> &pSelf, shared_ptr<AutoCloseHandle> hEvent, Mutex &lock): |
||||||
|
m_pSelf(GetPointers(pSelf, this)), |
||||||
|
m_hEvent(hEvent), |
||||||
|
m_Lock(lock) |
||||||
|
{ |
||||||
|
m_pConnection = SMXDeviceConnection::Create(); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::SMXDevice::~SMXDevice() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
bool SMX::SMXDevice::OpenDeviceHandle(shared_ptr<AutoCloseHandle> pHandle, wstring &sError) |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
return m_pConnection->Open(pHandle, sError); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::CloseDevice() |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
m_pConnection->Close(); |
||||||
|
m_bHaveConfig = false; |
||||||
|
m_bSendConfig = false; |
||||||
|
|
||||||
|
CallUpdateCallback(SMXUpdateCallback_Updated); |
||||||
|
} |
||||||
|
|
||||||
|
shared_ptr<AutoCloseHandle> SMX::SMXDevice::GetDeviceHandle() const |
||||||
|
{ |
||||||
|
return m_pConnection->GetDeviceHandle(); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::SetUpdateCallback(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback) |
||||||
|
{ |
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
m_pUpdateCallback = pCallback; |
||||||
|
} |
||||||
|
|
||||||
|
bool SMX::SMXDevice::IsConnected() const |
||||||
|
{ |
||||||
|
m_Lock.AssertNotLockedByCurrentThread(); |
||||||
|
|
||||||
|
// Don't expose the device as connected until we've read the current configuration.
|
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
return IsConnectedLocked(); |
||||||
|
} |
||||||
|
|
||||||
|
bool SMX::SMXDevice::IsConnectedLocked() const |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
return m_pConnection->IsConnectedWithDeviceInfo() && m_bHaveConfig; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::SendCommand(string cmd, function<void()> pComplete) |
||||||
|
{ |
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
SendCommandLocked(cmd, pComplete); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::SendCommandLocked(string cmd, function<void()> pComplete) |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
// This call is nonblocking, so it's safe to do this in the UI thread.
|
||||||
|
if(m_pConnection->IsConnected()) |
||||||
|
{ |
||||||
|
m_pConnection->SendCommand(cmd, pComplete); |
||||||
|
|
||||||
|
// Wake up the communications thread to send the message.
|
||||||
|
if(m_hEvent) |
||||||
|
SetEvent(m_hEvent->value()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::GetInfo(SMXInfo &info) |
||||||
|
{ |
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
GetInfoLocked(info); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::GetInfoLocked(SMXInfo &info) |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
info = SMXInfo(); |
||||||
|
info.m_bConnected = IsConnectedLocked(); |
||||||
|
if(!info.m_bConnected) |
||||||
|
return; |
||||||
|
|
||||||
|
// Copy fields from the low-level device info to the high-level struct.
|
||||||
|
// These are kept separate because the interface depends on the format
|
||||||
|
// of SMXInfo, but it doesn't care about anything inside SMXDeviceConnection.
|
||||||
|
SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo(); |
||||||
|
memcpy(info.m_Serial, deviceInfo.m_Serial, sizeof(info.m_Serial)); |
||||||
|
info.m_iFirmwareVersion = deviceInfo.m_iFirmwareVersion; |
||||||
|
} |
||||||
|
|
||||||
|
bool SMX::SMXDevice::IsPlayer2Locked() const |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
if(!IsConnectedLocked()) |
||||||
|
return false; |
||||||
|
|
||||||
|
return m_pConnection->GetDeviceInfo().m_bP2; |
||||||
|
} |
||||||
|
|
||||||
|
bool SMX::SMXDevice::GetConfig(SMXConfig &configOut) |
||||||
|
{ |
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
|
||||||
|
// If SetConfig was called to write a new configuration but we haven't sent it
|
||||||
|
// yet, return it instead of the configuration we read alst, so GetConfig
|
||||||
|
// immediately after SetConfig returns the value the caller expects set.
|
||||||
|
if(m_bSendConfig) |
||||||
|
configOut = wanted_config; |
||||||
|
else |
||||||
|
configOut = config; |
||||||
|
|
||||||
|
return m_bHaveConfig; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::SetConfig(const SMXConfig &newConfig) |
||||||
|
{ |
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
wanted_config = newConfig; |
||||||
|
m_bSendConfig = true; |
||||||
|
} |
||||||
|
|
||||||
|
uint16_t SMX::SMXDevice::GetInputState() const |
||||||
|
{ |
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
return m_pConnection->GetInputState(); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::FactoryReset() |
||||||
|
{ |
||||||
|
// Send a factory reset command, and then read the new configuration.
|
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
SendCommandLocked("f\n"); |
||||||
|
SendCommandLocked("g\n", [&] { |
||||||
|
// We now have the new configuration.
|
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
CallUpdateCallback(SMXUpdateCallback_FactoryResetCommandComplete); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::ForceRecalibration() |
||||||
|
{ |
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
SendCommandLocked("C\n"); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::SetSensorTestMode(SensorTestMode mode) |
||||||
|
{ |
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
m_SensorTestMode = mode; |
||||||
|
} |
||||||
|
|
||||||
|
bool SMX::SMXDevice::GetTestData(SMXSensorTestModeData &data) |
||||||
|
{ |
||||||
|
LockMutex Lock(m_Lock); |
||||||
|
|
||||||
|
// Stop if we haven't read test mode data yet.
|
||||||
|
if(!m_HaveSensorTestModeData) |
||||||
|
return false; |
||||||
|
|
||||||
|
data = m_SensorTestData; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::CallUpdateCallback(SMXUpdateCallbackReason reason) |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
if(!m_pUpdateCallback) |
||||||
|
return; |
||||||
|
|
||||||
|
SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo(); |
||||||
|
m_pUpdateCallback(deviceInfo.m_bP2? 1:0, reason); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::HandlePackets() |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
while(1) |
||||||
|
{ |
||||||
|
string buf; |
||||||
|
if(!m_pConnection->ReadPacket(buf)) |
||||||
|
break; |
||||||
|
if(buf.empty()) |
||||||
|
continue; |
||||||
|
|
||||||
|
switch(buf[0]) |
||||||
|
{ |
||||||
|
case 'y': |
||||||
|
HandleSensorTestDataResponse(buf); |
||||||
|
break; |
||||||
|
|
||||||
|
case 'g': |
||||||
|
{ |
||||||
|
// This command reads back the configuration we wrote with 'w', or the defaults if
|
||||||
|
// we haven't written any.
|
||||||
|
if(buf.size() < 2) |
||||||
|
{ |
||||||
|
Log("Communication error: invalid configuration packet"); |
||||||
|
continue; |
||||||
|
} |
||||||
|
uint8_t iSize = buf[1]; |
||||||
|
if(buf.size() < iSize+2) |
||||||
|
{ |
||||||
|
Log("Communication error: invalid configuration packet"); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// Copy in the configuration.
|
||||||
|
// Log(ssprintf("Read back configuration: %i bytes, first byte %i", iSize, buf[2]));
|
||||||
|
memcpy(&config, buf.data()+2, min(iSize, sizeof(config))); |
||||||
|
m_bHaveConfig = true; |
||||||
|
buf.erase(buf.begin(), buf.begin()+iSize+2); |
||||||
|
|
||||||
|
CallUpdateCallback(SMXUpdateCallback_Updated); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If m_bSendConfig is true, send the configuration to the pad. Note that while the game
|
||||||
|
// always sends its configuration, so the pad is configured according to the game's configuration,
|
||||||
|
// we only change the configuration if the user changes something so we don't overwrite
|
||||||
|
// his configuration.
|
||||||
|
void SMX::SMXDevice::SendConfig() |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
if(!m_pConnection->IsConnected() || !m_bSendConfig || m_bSendingConfig) |
||||||
|
return; |
||||||
|
|
||||||
|
// We can't update the configuration until we've received the device's previous
|
||||||
|
// configuration.
|
||||||
|
if(!m_bHaveConfig) |
||||||
|
return; |
||||||
|
|
||||||
|
// Write configuration command:
|
||||||
|
string sData = ssprintf("w"); |
||||||
|
int8_t iSize = sizeof(SMXConfig); |
||||||
|
sData.append((char *) &iSize, sizeof(iSize)); |
||||||
|
sData.append((char *) &wanted_config, sizeof(wanted_config)); |
||||||
|
|
||||||
|
// Don't send another config packet until this one finishes, so if we get a bunch of
|
||||||
|
// SetConfig calls quickly we won't spam the device, which can get slow.
|
||||||
|
m_bSendingConfig = true; |
||||||
|
SendCommandLocked(sData, [&] { |
||||||
|
m_bSendingConfig = false; |
||||||
|
}); |
||||||
|
m_bSendConfig = false; |
||||||
|
|
||||||
|
// Assume the configuration is what we just sent, so calls to GetConfig will
|
||||||
|
// continue to return it. Otherwise, they'd return the old values until the
|
||||||
|
// command below completes.
|
||||||
|
config = wanted_config; |
||||||
|
|
||||||
|
// After we write the configuration, read back the updated configuration to
|
||||||
|
// verify it.
|
||||||
|
SendCommandLocked("g\n"); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::Update(wstring &sError) |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
if(!m_pConnection->IsConnected()) |
||||||
|
return; |
||||||
|
|
||||||
|
CheckActive(); |
||||||
|
SendConfig(); |
||||||
|
UpdateTestMode(); |
||||||
|
|
||||||
|
{ |
||||||
|
uint16_t iOldState = m_pConnection->GetInputState(); |
||||||
|
|
||||||
|
// Process any received packets, and start sending any waiting packets.
|
||||||
|
m_pConnection->Update(sError); |
||||||
|
if(!sError.empty()) |
||||||
|
return; |
||||||
|
|
||||||
|
// If the inputs changed from packets we just processed, call the update callback.
|
||||||
|
if(iOldState != m_pConnection->GetInputState()) |
||||||
|
CallUpdateCallback(SMXUpdateCallback_Updated); |
||||||
|
} |
||||||
|
|
||||||
|
HandlePackets(); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDevice::CheckActive() |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
// If there's no connected device, or we've already activated it, we have nothing to do.
|
||||||
|
if(!m_pConnection->IsConnectedWithDeviceInfo() || m_pConnection->GetActive()) |
||||||
|
return; |
||||||
|
|
||||||
|
m_pConnection->SetActive(true); |
||||||
|
|
||||||
|
// Reset panels.
|
||||||
|
SendCommandLocked("R\n"); |
||||||
|
|
||||||
|
// Read the current configuration. The device will return a "g" response containing
|
||||||
|
// its current SMXConfig.
|
||||||
|
SendCommandLocked("g\n"); |
||||||
|
} |
||||||
|
|
||||||
|
// Check if we need to request test mode data.
|
||||||
|
void SMX::SMXDevice::UpdateTestMode() |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
if(m_SensorTestMode == SensorTestMode_Off) |
||||||
|
return; |
||||||
|
|
||||||
|
// Request sensor data from the master. Don't send this if we have a request outstanding
|
||||||
|
// already.
|
||||||
|
uint32_t now = GetTickCount(); |
||||||
|
if(m_WaitingForSensorTestModeResponse != SensorTestMode_Off) |
||||||
|
{ |
||||||
|
// This request should be quick. If we haven't received a response in a long
|
||||||
|
// time, assume the request wasn't received.
|
||||||
|
if(now - m_SentSensorTestModeRequestAtTicks < 2000) |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// Send the request.
|
||||||
|
m_WaitingForSensorTestModeResponse = m_SensorTestMode; |
||||||
|
m_SentSensorTestModeRequestAtTicks = now; |
||||||
|
|
||||||
|
SendCommandLocked(ssprintf("y%c\n", m_SensorTestMode)); |
||||||
|
} |
||||||
|
|
||||||
|
// Handle a response to UpdateTestMode.
|
||||||
|
void SMX::SMXDevice::HandleSensorTestDataResponse(const string &sReadBuffer) |
||||||
|
{ |
||||||
|
m_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
// "y" is a response to our "y" query. This is binary data, with the format:
|
||||||
|
// yAB......
|
||||||
|
// where A is our original query mode (currently '0' or '1'), and B is the number
|
||||||
|
// of bits from each panel in the response. Each bit is encoded as a 16-bit int,
|
||||||
|
// with each int having the response bits from each panel.
|
||||||
|
if(sReadBuffer.size() < 3) |
||||||
|
return; |
||||||
|
|
||||||
|
// If we don't have the whole packet yet, wait.
|
||||||
|
uint8_t iSize = sReadBuffer[2] * 2; |
||||||
|
if(sReadBuffer.size() < iSize + 3) |
||||||
|
return; |
||||||
|
|
||||||
|
SensorTestMode iMode = (SensorTestMode) sReadBuffer[1]; |
||||||
|
|
||||||
|
// Copy off the data and remove it from the serial buffer.
|
||||||
|
vector<uint16_t> data; |
||||||
|
for(int i = 3; i < iSize + 3; i += 2) |
||||||
|
{ |
||||||
|
uint16_t iValue = |
||||||
|
(uint8_t(sReadBuffer[i+1]) << 8) | |
||||||
|
(uint8_t(sReadBuffer[i+0]) << 0); |
||||||
|
data.push_back(iValue); |
||||||
|
} |
||||||
|
|
||||||
|
if(m_WaitingForSensorTestModeResponse == SensorTestMode_Off) |
||||||
|
{ |
||||||
|
Log("Ignoring unexpected sensor data request. It may have been sent by another application."); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if(iMode != m_WaitingForSensorTestModeResponse) |
||||||
|
{ |
||||||
|
Log(ssprintf("Ignoring unexpected sensor data request (got %i, expected %i)", iMode, m_WaitingForSensorTestModeResponse)); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
m_WaitingForSensorTestModeResponse = SensorTestMode_Off; |
||||||
|
|
||||||
|
// We match m_WaitingForSensorTestModeResponse, which is the sensor request we most
|
||||||
|
// recently sent. If we don't match g_SensorTestMode, then the sensor mode was changed
|
||||||
|
// while a request was in the air. Just ignore the response.
|
||||||
|
if(iMode != m_SensorTestMode) |
||||||
|
return; |
||||||
|
|
||||||
|
#pragma pack(push,1) |
||||||
|
struct detail_data { |
||||||
|
uint8_t sig1:1; // always 0
|
||||||
|
uint8_t sig2:1; // always 1
|
||||||
|
uint8_t sig3:1; // always 0
|
||||||
|
uint8_t bad_sensor_0:1; |
||||||
|
uint8_t bad_sensor_1:1; |
||||||
|
uint8_t bad_sensor_2:1; |
||||||
|
uint8_t bad_sensor_3:1; |
||||||
|
uint8_t dummy:1; |
||||||
|
|
||||||
|
int16_t sensors[4]; |
||||||
|
|
||||||
|
uint8_t dip:4; |
||||||
|
uint8_t dummy2:4; |
||||||
|
}; |
||||||
|
#pragma pack(pop) |
||||||
|
|
||||||
|
m_HaveSensorTestModeData = true; |
||||||
|
SMXSensorTestModeData &output = m_SensorTestData; |
||||||
|
memset(output.bHaveDataFromPanel, 0, sizeof(output.bHaveDataFromPanel)); |
||||||
|
memset(output.sensorLevel, 0, sizeof(output.sensorLevel)); |
||||||
|
memset(output.bBadSensorInput, 0, sizeof(output.bBadSensorInput)); |
||||||
|
memset(output.iDIPSwitchPerPanel, 0, sizeof(output.iDIPSwitchPerPanel)); |
||||||
|
|
||||||
|
for(int iPanel = 0; iPanel < 9; ++iPanel) |
||||||
|
{ |
||||||
|
// Decode the response from this panel.
|
||||||
|
detail_data pad_data; |
||||||
|
ReadDataForPanel(data, iPanel, &pad_data, sizeof(pad_data)); |
||||||
|
|
||||||
|
// Check the header. This is always 0 1 0, to identify it as a response, and not as random
|
||||||
|
// steps from the player.
|
||||||
|
if(pad_data.sig1 != 0 || pad_data.sig2 != 1 || pad_data.sig3 != 0) |
||||||
|
{ |
||||||
|
// Log(ssprintf("Invalid data: %i %i %i", sig1, sig2, sig3));
|
||||||
|
output.bHaveDataFromPanel[iPanel] = false; |
||||||
|
continue; |
||||||
|
} |
||||||
|
output.bHaveDataFromPanel[iPanel] = true; |
||||||
|
|
||||||
|
// These bits are true if that sensor's most recent reading is invalid.
|
||||||
|
output.bBadSensorInput[iPanel][0] = pad_data.bad_sensor_0; |
||||||
|
output.bBadSensorInput[iPanel][1] = pad_data.bad_sensor_1; |
||||||
|
output.bBadSensorInput[iPanel][2] = pad_data.bad_sensor_2; |
||||||
|
output.bBadSensorInput[iPanel][3] = pad_data.bad_sensor_3; |
||||||
|
output.iDIPSwitchPerPanel[iPanel] = pad_data.dip; |
||||||
|
|
||||||
|
for(int iSensor = 0; iSensor < 4; ++iSensor) |
||||||
|
output.sensorLevel[iPanel][iSensor] = pad_data.sensors[iSensor]; |
||||||
|
} |
||||||
|
|
||||||
|
CallUpdateCallback(SMXUpdateCallback_Updated); |
||||||
|
} |
@ -0,0 +1,132 @@ |
|||||||
|
#ifndef SMXDevice_h |
||||||
|
#define SMXDevice_h |
||||||
|
|
||||||
|
#include <windows.h> |
||||||
|
#include <memory> |
||||||
|
#include <functional> |
||||||
|
using namespace std; |
||||||
|
|
||||||
|
#include "Helpers.h" |
||||||
|
#include "../SMX.h" |
||||||
|
|
||||||
|
namespace SMX |
||||||
|
{ |
||||||
|
class SMXDeviceConnection; |
||||||
|
|
||||||
|
// The high-level interface to a single controller. This is managed by SMXManager, and uses SMXDeviceConnection
|
||||||
|
// for low-level USB communication.
|
||||||
|
class SMXDevice |
||||||
|
{ |
||||||
|
public: |
||||||
|
// Create an SMXDevice.
|
||||||
|
//
|
||||||
|
// lock is our serialization mutex. This is shared across SMXManager and all SMXDevices.
|
||||||
|
//
|
||||||
|
// hEvent is signalled when we have new packets to be sent, to wake the communications thread. The
|
||||||
|
// device handle opened with OpenPort must also be monitored, to check when packets have been received
|
||||||
|
// (or successfully sent).
|
||||||
|
static shared_ptr<SMXDevice> Create(shared_ptr<SMX::AutoCloseHandle> hEvent, SMX::Mutex &lock); |
||||||
|
SMXDevice(shared_ptr<SMXDevice> &pSelf, shared_ptr<SMX::AutoCloseHandle> hEvent, SMX::Mutex &lock); |
||||||
|
~SMXDevice(); |
||||||
|
|
||||||
|
bool OpenDeviceHandle(shared_ptr<SMX::AutoCloseHandle> pHandle, wstring &sError); |
||||||
|
void CloseDevice(); |
||||||
|
shared_ptr<SMX::AutoCloseHandle> GetDeviceHandle() const; |
||||||
|
|
||||||
|
// Set a function to be called when something changes on the device. This allows efficiently
|
||||||
|
// detecting when a panel is pressed or other changes happen on the device.
|
||||||
|
void SetUpdateCallback(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback); |
||||||
|
|
||||||
|
// Return true if we're connected.
|
||||||
|
bool IsConnected() const; |
||||||
|
|
||||||
|
// Send a raw command.
|
||||||
|
void SendCommand(string sCmd, function<void()> pComplete=nullptr); |
||||||
|
void SendCommandLocked(string sCmd, function<void()> pComplete=nullptr); |
||||||
|
|
||||||
|
// Get basic info about the device.
|
||||||
|
void GetInfo(SMXInfo &info); |
||||||
|
void GetInfoLocked(SMXInfo &info); // used by SMXManager
|
||||||
|
|
||||||
|
// Return true if this device is configured as player 2.
|
||||||
|
bool IsPlayer2Locked() const; // used by SMXManager
|
||||||
|
|
||||||
|
// Get the configuration of the connected device (or the most recently read configuration if
|
||||||
|
// we're not connected).
|
||||||
|
bool GetConfig(SMXConfig &configOut); |
||||||
|
|
||||||
|
// Set the configuration of the connected device.
|
||||||
|
//
|
||||||
|
// This is asynchronous and returns immediately.
|
||||||
|
void SetConfig(const SMXConfig &newConfig); |
||||||
|
|
||||||
|
// Return a mask of the panels currently pressed.
|
||||||
|
uint16_t GetInputState() const; |
||||||
|
|
||||||
|
// Reset the configuration data to what the device used when it was first flashed.
|
||||||
|
// GetConfig() will continue to return the previous configuration until this command
|
||||||
|
// completes, which is signalled by a SMXUpdateCallback_FactoryResetCommandComplete callback.
|
||||||
|
void FactoryReset(); |
||||||
|
|
||||||
|
// Force immediate fast recalibration. This is the same calibration that happens at
|
||||||
|
// boot. This is only used for diagnostics, and the panels will normally auto-calibrate
|
||||||
|
// on their own.
|
||||||
|
void ForceRecalibration(); |
||||||
|
|
||||||
|
// Set the test mode of the connected device.
|
||||||
|
//
|
||||||
|
// This is asynchronous and returns immediately.
|
||||||
|
void SetSensorTestMode(SensorTestMode mode); |
||||||
|
|
||||||
|
// Return the most recent test data we've received from the pad. Return false if we haven't
|
||||||
|
// received test data since changing the test mode (or if we're not in a test mode).
|
||||||
|
bool GetTestData(SMXSensorTestModeData &data); |
||||||
|
|
||||||
|
// Internal:
|
||||||
|
|
||||||
|
// Update this device, processing received packets and sending any outbound packets.
|
||||||
|
// m_Lock must be held.
|
||||||
|
//
|
||||||
|
// sError will be set on a communications error. The owner must close the device.
|
||||||
|
void Update(wstring &sError); |
||||||
|
|
||||||
|
private: |
||||||
|
shared_ptr<SMX::AutoCloseHandle> m_hEvent; |
||||||
|
SMX::Mutex &m_Lock; |
||||||
|
|
||||||
|
function<void(int PadNumber, SMXUpdateCallbackReason reason)> m_pUpdateCallback; |
||||||
|
weak_ptr<SMXDevice> m_pSelf; |
||||||
|
|
||||||
|
shared_ptr<SMXDeviceConnection> m_pConnection; |
||||||
|
|
||||||
|
// The configuration we've read from the device. m_bHaveConfig is true if we've received
|
||||||
|
// a configuration from the device since we've connected to it.
|
||||||
|
SMXConfig config; |
||||||
|
bool m_bHaveConfig = false; |
||||||
|
|
||||||
|
// This is the configuration the user has set, if he's changed anything. We send this to
|
||||||
|
// the device if m_bSendConfig is true. Once we send it once, m_bSendConfig is cleared, and
|
||||||
|
// if we see a different configuration from the device again we won't re-send this.
|
||||||
|
SMXConfig wanted_config; |
||||||
|
bool m_bSendConfig = false; |
||||||
|
bool m_bSendingConfig = false; |
||||||
|
|
||||||
|
void CallUpdateCallback(SMXUpdateCallbackReason reason); |
||||||
|
void HandlePackets(); |
||||||
|
|
||||||
|
void SendConfig(); |
||||||
|
void CheckActive(); |
||||||
|
bool IsConnectedLocked() const; |
||||||
|
|
||||||
|
// Test/diagnostics mode handling.
|
||||||
|
void UpdateTestMode(); |
||||||
|
void HandleSensorTestDataResponse(const string &sReadBuffer); |
||||||
|
SensorTestMode m_WaitingForSensorTestModeResponse = SensorTestMode_Off; |
||||||
|
SensorTestMode m_SensorTestMode = SensorTestMode_Off; |
||||||
|
bool m_HaveSensorTestModeData = false; |
||||||
|
SMXSensorTestModeData m_SensorTestData; |
||||||
|
uint32_t m_SentSensorTestModeRequestAtTicks = 0; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,378 @@ |
|||||||
|
#include "SMXDeviceConnection.h" |
||||||
|
#include "Helpers.h" |
||||||
|
|
||||||
|
#include <string> |
||||||
|
#include <memory> |
||||||
|
using namespace std; |
||||||
|
using namespace SMX; |
||||||
|
|
||||||
|
#include <hidsdi.h> |
||||||
|
#include <SetupAPI.h> |
||||||
|
|
||||||
|
SMX::SMXDeviceConnection::PendingCommandPacket::PendingCommandPacket() |
||||||
|
{ |
||||||
|
memset(&m_OverlappedWrite, 0, sizeof(m_OverlappedWrite)); |
||||||
|
} |
||||||
|
|
||||||
|
shared_ptr<SMX::SMXDeviceConnection> SMXDeviceConnection::Create() |
||||||
|
{ |
||||||
|
return CreateObj<SMXDeviceConnection>(); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::SMXDeviceConnection::SMXDeviceConnection(shared_ptr<SMXDeviceConnection> &pSelf): |
||||||
|
m_pSelf(GetPointers(pSelf, this)) |
||||||
|
{ |
||||||
|
memset(&overlapped_read, 0, sizeof(overlapped_read)); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::SMXDeviceConnection::~SMXDeviceConnection() |
||||||
|
{ |
||||||
|
Close(); |
||||||
|
} |
||||||
|
|
||||||
|
bool SMX::SMXDeviceConnection::Open(shared_ptr<AutoCloseHandle> DeviceHandle, wstring &sError) |
||||||
|
{ |
||||||
|
m_hDevice = DeviceHandle; |
||||||
|
|
||||||
|
if(!HidD_SetNumInputBuffers(DeviceHandle->value(), 512)) |
||||||
|
Log(ssprintf("Error: HidD_SetNumInputBuffers: %ls", GetErrorString(GetLastError()).c_str())); |
||||||
|
|
||||||
|
// Begin the first async read.
|
||||||
|
BeginAsyncRead(sError); |
||||||
|
|
||||||
|
// Request device info.
|
||||||
|
RequestDeviceInfo([&] { |
||||||
|
Log(ssprintf("Received device info. Master version: %i, P%i", m_DeviceInfo.m_iFirmwareVersion, m_DeviceInfo.m_bP2+1)); |
||||||
|
m_bGotInfo = true; |
||||||
|
}); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceConnection::Close() |
||||||
|
{ |
||||||
|
Log("Closing device"); |
||||||
|
|
||||||
|
if(m_hDevice) |
||||||
|
CancelIo(m_hDevice->value()); |
||||||
|
|
||||||
|
m_hDevice.reset(); |
||||||
|
m_sReadBuffers.clear(); |
||||||
|
m_aPendingCommands.clear(); |
||||||
|
memset(&overlapped_read, 0, sizeof(overlapped_read)); |
||||||
|
m_bActive = false; |
||||||
|
m_bGotInfo = false; |
||||||
|
m_pCurrentCommand = nullptr; |
||||||
|
m_iInputState = 0; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceConnection::SetActive(bool bActive) |
||||||
|
{ |
||||||
|
if(m_bActive == bActive) |
||||||
|
return; |
||||||
|
|
||||||
|
m_bActive = bActive; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceConnection::Update(wstring &sError) |
||||||
|
{ |
||||||
|
if(!sError.empty()) |
||||||
|
return; |
||||||
|
|
||||||
|
if(m_hDevice == nullptr) |
||||||
|
{ |
||||||
|
sError = L"Device not open"; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// A read packet can allow us to initiate a write, so check reads before writes.
|
||||||
|
CheckReads(sError); |
||||||
|
CheckWrites(sError); |
||||||
|
} |
||||||
|
|
||||||
|
bool SMX::SMXDeviceConnection::ReadPacket(string &out) |
||||||
|
{ |
||||||
|
if(m_sReadBuffers.empty()) |
||||||
|
return false; |
||||||
|
out = m_sReadBuffers.front(); |
||||||
|
m_sReadBuffers.pop_front(); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceConnection::CheckReads(wstring &error) |
||||||
|
{ |
||||||
|
DWORD bytes; |
||||||
|
int result = GetOverlappedResult(m_hDevice->value(), &overlapped_read, &bytes, FALSE); |
||||||
|
if(result == 0) |
||||||
|
{ |
||||||
|
int windows_error = GetLastError(); |
||||||
|
if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE) |
||||||
|
error = wstring(L"Error reading device: ") + GetErrorString(windows_error).c_str(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
HandleUsbPacket(string(overlapped_read_buffer, bytes)); |
||||||
|
|
||||||
|
// Start the next read.
|
||||||
|
BeginAsyncRead(error); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceConnection::HandleUsbPacket(const string &buf) |
||||||
|
{ |
||||||
|
if(buf.empty()) |
||||||
|
return; |
||||||
|
// Log(ssprintf("Read: %s\n", BinaryToHex(buf).c_str()));
|
||||||
|
|
||||||
|
int iReportId = buf[0]; |
||||||
|
switch(iReportId) |
||||||
|
{ |
||||||
|
case 3: |
||||||
|
// Input state. We could also read this as a normal HID button change.
|
||||||
|
m_iInputState = ((buf[2] & 0xFF) << 8) | |
||||||
|
((buf[1] & 0xFF) << 0); |
||||||
|
|
||||||
|
// Log(ssprintf("Input state: %x (%x %x)\n", m_iInputState, buf[2], buf[1]));
|
||||||
|
break; |
||||||
|
|
||||||
|
case 6: |
||||||
|
// A HID serial packet.
|
||||||
|
if(buf.size() < 3) |
||||||
|
return; |
||||||
|
|
||||||
|
int cmd = buf[1]; |
||||||
|
|
||||||
|
#define PACKET_FLAG_START_OF_COMMAND 0x04 |
||||||
|
#define PACKET_FLAG_END_OF_COMMAND 0x01 |
||||||
|
#define PACKET_FLAG_HOST_CMD_FINISHED 0x02 |
||||||
|
#define PACKET_FLAG_DEVICE_INFO 0x80 |
||||||
|
|
||||||
|
int bytes = buf[2]; |
||||||
|
if(3 + bytes > buf.size()) |
||||||
|
{ |
||||||
|
Log("Communication error: oversized packet (ignored)"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
string sPacket( buf.begin()+3, buf.begin()+3+bytes ); |
||||||
|
|
||||||
|
if(cmd & PACKET_FLAG_DEVICE_INFO) |
||||||
|
{ |
||||||
|
// This is a response to RequestDeviceInfo. Since any application can send this,
|
||||||
|
// we ignore the packet if we didn't request it, since it might be requested for
|
||||||
|
// a different program.
|
||||||
|
if(m_pCurrentCommand == nullptr || !m_pCurrentCommand->m_bIsDeviceInfoCommand) |
||||||
|
break; |
||||||
|
|
||||||
|
// We're little endian and the device is too, so we can just match the struct.
|
||||||
|
// We're depending on correct padding.
|
||||||
|
struct data_info_packet |
||||||
|
{ |
||||||
|
char cmd; // always 'I'
|
||||||
|
uint8_t packet_size; // not used
|
||||||
|
char player; // '0' for P1, '1' for P2:
|
||||||
|
char unused2; |
||||||
|
uint8_t serial[16]; |
||||||
|
uint16_t firmware_version; |
||||||
|
char unused3; // always '\n'
|
||||||
|
}; |
||||||
|
|
||||||
|
// The packet contains data_info_packet. The packet is actually one byte smaller
|
||||||
|
// due to a padding byte added (it contains 23 bytes of data but the struct is
|
||||||
|
// 24 bytes). Resize to be sure.
|
||||||
|
sPacket.resize(sizeof(data_info_packet)); |
||||||
|
|
||||||
|
// Convert the info packet from the wire protocol to our friendlier API.
|
||||||
|
const data_info_packet *packet = (data_info_packet *) sPacket.data(); |
||||||
|
m_DeviceInfo.m_bP2 = packet->player == '1'; |
||||||
|
m_DeviceInfo.m_iFirmwareVersion = packet->firmware_version; |
||||||
|
|
||||||
|
// The serial is binary in this packet. Hex format it, which is the same thing
|
||||||
|
// we'll get if we read the USB serial number (eg. HidD_GetSerialNumberString).
|
||||||
|
string sHexSerial = BinaryToHex(packet->serial, 16); |
||||||
|
memcpy(m_DeviceInfo.m_Serial, sHexSerial.c_str(), 33); |
||||||
|
|
||||||
|
if(m_pCurrentCommand->m_pComplete) |
||||||
|
m_pCurrentCommand->m_pComplete(); |
||||||
|
m_pCurrentCommand = nullptr; |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// If we're not active, ignore all packets other than device info. This is always false
|
||||||
|
// while we're in Open() waiting for the device info response.
|
||||||
|
if(!m_bActive) |
||||||
|
break; |
||||||
|
|
||||||
|
m_sCurrentReadBuffer.append(sPacket); |
||||||
|
|
||||||
|
if(cmd & PACKET_FLAG_END_OF_COMMAND) |
||||||
|
{ |
||||||
|
if(!m_sCurrentReadBuffer.empty()) |
||||||
|
m_sReadBuffers.push_back(m_sCurrentReadBuffer); |
||||||
|
m_sCurrentReadBuffer.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
if(cmd & PACKET_FLAG_HOST_CMD_FINISHED) |
||||||
|
{ |
||||||
|
// This tells us that a command we wrote to the device has finished executing, and
|
||||||
|
// it's safe to start writing another.
|
||||||
|
if(m_pCurrentCommand && m_pCurrentCommand->m_pComplete) |
||||||
|
m_pCurrentCommand->m_pComplete(); |
||||||
|
m_pCurrentCommand = nullptr; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceConnection::BeginAsyncRead(wstring &error) |
||||||
|
{ |
||||||
|
while(1) |
||||||
|
{ |
||||||
|
// Our read buffer is 64 bytes. The HID input packet is much smaller than that,
|
||||||
|
// but Windows pads packets to the maximum size of any HID report, and the HID
|
||||||
|
// serial packet is 64 bytes, so we'll get 64 bytes even for 3-byte input packets.
|
||||||
|
// If this didn't happen, we'd have to be smarter about pulling data out of the
|
||||||
|
// read buffer.
|
||||||
|
DWORD bytes; |
||||||
|
memset(overlapped_read_buffer, sizeof(overlapped_read_buffer), 0); |
||||||
|
if(!ReadFile(m_hDevice->value(), overlapped_read_buffer, sizeof(overlapped_read_buffer), &bytes, &overlapped_read)) |
||||||
|
{ |
||||||
|
int windows_error = GetLastError(); |
||||||
|
if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE) |
||||||
|
error = wstring(L"Error reading device: ") + GetErrorString(windows_error).c_str(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// The async read finished synchronously. This just means that there was already data waiting.
|
||||||
|
// Handle the result, and loop to try to start the next async read again.
|
||||||
|
HandleUsbPacket(string(overlapped_read_buffer, bytes)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceConnection::CheckWrites(wstring &error) |
||||||
|
{ |
||||||
|
if(m_pCurrentCommand && !m_pCurrentCommand->m_Packets.empty()) |
||||||
|
{ |
||||||
|
// A command is in progress. See if any writes have completed.
|
||||||
|
while(!m_pCurrentCommand->m_Packets.empty()) |
||||||
|
{ |
||||||
|
shared_ptr<PendingCommandPacket> pFirstPacket = m_pCurrentCommand->m_Packets.front(); |
||||||
|
|
||||||
|
DWORD bytes; |
||||||
|
int iResult = GetOverlappedResult(m_hDevice->value(), &pFirstPacket->m_OverlappedWrite, &bytes, FALSE); |
||||||
|
if(iResult == 0) |
||||||
|
{ |
||||||
|
int windows_error = GetLastError(); |
||||||
|
if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE) |
||||||
|
error = wstring(L"Error writing to device: ") + GetErrorString(windows_error).c_str(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
m_pCurrentCommand->m_Packets.pop_front(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// Don't clear m_pCurrentCommand here. It'll stay set until we get a PACKET_FLAG_HOST_CMD_FINISHED
|
||||||
|
// packet from the device, which tells us it's ready to receive another command.
|
||||||
|
} |
||||||
|
|
||||||
|
// Don't send packets if there's a command in progress.
|
||||||
|
if(m_pCurrentCommand) |
||||||
|
return; |
||||||
|
|
||||||
|
// Stop if we have nothing to do.
|
||||||
|
if(m_aPendingCommands.empty()) |
||||||
|
return; |
||||||
|
|
||||||
|
// Send the next command.
|
||||||
|
shared_ptr<PendingCommand> pPendingCommand = m_aPendingCommands.front(); |
||||||
|
|
||||||
|
for(shared_ptr<PendingCommandPacket> &pPacket: pPendingCommand->m_Packets) |
||||||
|
{ |
||||||
|
// In theory the API allows this to return success if the write completed successfully without needing to
|
||||||
|
// be async, like reads can. However, this can't really happen (the write always needs to go to the device
|
||||||
|
// first, unlike reads which might already be buffered), and there's no way to test it if we implement that,
|
||||||
|
// so this assumes all writes are async.
|
||||||
|
DWORD unused; |
||||||
|
if(!WriteFile(m_hDevice->value(), pPacket->sData.data(), pPacket->sData.size(), &unused, &pPacket->m_OverlappedWrite)) |
||||||
|
{ |
||||||
|
int windows_error = GetLastError(); |
||||||
|
if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE) |
||||||
|
{ |
||||||
|
error = wstring(L"Error writing to device: ") + GetErrorString(windows_error).c_str(); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Remove this command and store it in m_pCurrentCommand, and we'll stop sending data until the command finishes.
|
||||||
|
m_pCurrentCommand = pPendingCommand; |
||||||
|
m_aPendingCommands.pop_front(); |
||||||
|
} |
||||||
|
|
||||||
|
// Request device info. This is the same as sending an 'i' command, but we can send it safely
|
||||||
|
// at any time, even if another application is talking to the device, so we can do this during
|
||||||
|
// enumeration.
|
||||||
|
void SMX::SMXDeviceConnection::RequestDeviceInfo(function<void()> pComplete) |
||||||
|
{ |
||||||
|
shared_ptr<PendingCommand> pPendingCommand = make_shared<PendingCommand>(); |
||||||
|
pPendingCommand->m_pComplete = pComplete; |
||||||
|
pPendingCommand->m_bIsDeviceInfoCommand = true; |
||||||
|
|
||||||
|
shared_ptr<PendingCommandPacket> pCommandPacket = make_shared<PendingCommandPacket>(); |
||||||
|
|
||||||
|
string sPacket({ |
||||||
|
5, // report ID
|
||||||
|
(char) (uint8_t) PACKET_FLAG_DEVICE_INFO, // flags
|
||||||
|
(char) 0, // bytes in packet
|
||||||
|
}); |
||||||
|
sPacket.resize(64, 0); |
||||||
|
pCommandPacket->sData = sPacket; |
||||||
|
|
||||||
|
pPendingCommand->m_Packets.push_back(pCommandPacket); |
||||||
|
|
||||||
|
m_aPendingCommands.push_back(pPendingCommand); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceConnection::SendCommand(const string &cmd, function<void()> pComplete) |
||||||
|
{ |
||||||
|
shared_ptr<PendingCommand> pPendingCommand = make_shared<PendingCommand>(); |
||||||
|
pPendingCommand->m_pComplete = pComplete; |
||||||
|
|
||||||
|
// Send the command in packets. We allow sending zero-length packets here
|
||||||
|
// for testing purposes.
|
||||||
|
int i = 0; |
||||||
|
do { |
||||||
|
shared_ptr<PendingCommandPacket> pCommandPacket = make_shared<PendingCommandPacket>(); |
||||||
|
|
||||||
|
int iFlags = 0; |
||||||
|
int iPacketSize = min(cmd.size() - i, 61); |
||||||
|
|
||||||
|
bool bFirstPacket = (i == 0); |
||||||
|
if(bFirstPacket) |
||||||
|
iFlags |= PACKET_FLAG_START_OF_COMMAND; |
||||||
|
|
||||||
|
bool bLastPacket = (i + iPacketSize == cmd.size()); |
||||||
|
if(bLastPacket) |
||||||
|
iFlags |= PACKET_FLAG_END_OF_COMMAND; |
||||||
|
|
||||||
|
string sPacket({ |
||||||
|
5, // report ID
|
||||||
|
(char) iFlags, |
||||||
|
(char) iPacketSize, // bytes in packet
|
||||||
|
}); |
||||||
|
|
||||||
|
sPacket.append(cmd.begin() + i, cmd.begin() + i + iPacketSize); |
||||||
|
sPacket.resize(64, 0); |
||||||
|
pCommandPacket->sData = sPacket; |
||||||
|
|
||||||
|
pPendingCommand->m_Packets.push_back(pCommandPacket); |
||||||
|
|
||||||
|
i += iPacketSize; |
||||||
|
} |
||||||
|
while(i < cmd.size()); |
||||||
|
|
||||||
|
m_aPendingCommands.push_back(pPendingCommand); |
||||||
|
} |
@ -0,0 +1,122 @@ |
|||||||
|
#ifndef SMXDevice_H |
||||||
|
#define SMXDevice_H |
||||||
|
|
||||||
|
#include <windows.h> |
||||||
|
#include <vector> |
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
#include <list> |
||||||
|
#include <functional> |
||||||
|
using namespace std; |
||||||
|
|
||||||
|
#include "Helpers.h" |
||||||
|
|
||||||
|
namespace SMX |
||||||
|
{ |
||||||
|
|
||||||
|
struct SMXDeviceInfo |
||||||
|
{ |
||||||
|
// If true, this controller is set to player 2.
|
||||||
|
bool m_bP2 = false; |
||||||
|
|
||||||
|
// This device's serial number.
|
||||||
|
char m_Serial[33]; |
||||||
|
|
||||||
|
// This device's firmware version (normally 1).
|
||||||
|
uint16_t m_iFirmwareVersion; |
||||||
|
}; |
||||||
|
|
||||||
|
// Low-level SMX device handling.
|
||||||
|
class SMXDeviceConnection |
||||||
|
{ |
||||||
|
public: |
||||||
|
static shared_ptr<SMXDeviceConnection> Create(); |
||||||
|
SMXDeviceConnection(shared_ptr<SMXDeviceConnection> &pSelf); |
||||||
|
~SMXDeviceConnection(); |
||||||
|
|
||||||
|
bool Open(shared_ptr<AutoCloseHandle> DeviceHandle, wstring &error); |
||||||
|
|
||||||
|
void Close(); |
||||||
|
|
||||||
|
// Get the device handle opened by Open(), or NULL if we're not open.
|
||||||
|
shared_ptr<AutoCloseHandle> GetDeviceHandle() const { return m_hDevice; } |
||||||
|
|
||||||
|
void Update(wstring &sError); |
||||||
|
|
||||||
|
// Devices are inactive by default, and will just read device info and then idle. We'll
|
||||||
|
// process input state packets, but we won't send any commands to the device or process
|
||||||
|
// any commands from it. It's safe to have a device open but inactive if it's being used
|
||||||
|
// by another application.
|
||||||
|
void SetActive(bool bActive); |
||||||
|
bool GetActive() const { return m_bActive; } |
||||||
|
|
||||||
|
bool IsConnected() const { return m_hDevice != nullptr; } |
||||||
|
bool IsConnectedWithDeviceInfo() const { return m_hDevice != nullptr && m_bGotInfo; } |
||||||
|
SMXDeviceInfo GetDeviceInfo() const { return m_DeviceInfo; } |
||||||
|
|
||||||
|
// Read from the read buffer. This only returns data that we've already read, so there aren't
|
||||||
|
// any errors to report here.
|
||||||
|
bool ReadPacket(string &out); |
||||||
|
|
||||||
|
// Send a command. This must be a single complete command: partial writes and multiple
|
||||||
|
// commands in a call aren't allowed.
|
||||||
|
void SendCommand(const string &cmd, function<void()> pComplete=nullptr); |
||||||
|
|
||||||
|
uint16_t GetInputState() const { return m_iInputState; } |
||||||
|
|
||||||
|
private: |
||||||
|
void RequestDeviceInfo(function<void()> pComplete = nullptr); |
||||||
|
|
||||||
|
void CheckReads(wstring &error); |
||||||
|
void BeginAsyncRead(wstring &error); |
||||||
|
void CheckWrites(wstring &error); |
||||||
|
void HandleUsbPacket(const string &buf); |
||||||
|
|
||||||
|
weak_ptr<SMXDeviceConnection> m_pSelf; |
||||||
|
shared_ptr<AutoCloseHandle> m_hDevice; |
||||||
|
|
||||||
|
bool m_bActive = false; |
||||||
|
|
||||||
|
// After we open a device, we request basic info. Once we get it, this is set to true.
|
||||||
|
bool m_bGotInfo = false; |
||||||
|
|
||||||
|
list<string> m_sReadBuffers; |
||||||
|
string m_sCurrentReadBuffer; |
||||||
|
|
||||||
|
struct PendingCommandPacket { |
||||||
|
PendingCommandPacket(); |
||||||
|
|
||||||
|
string sData; |
||||||
|
OVERLAPPED m_OverlappedWrite; |
||||||
|
}; |
||||||
|
|
||||||
|
// Commands that are waiting to be sent:
|
||||||
|
struct PendingCommand { |
||||||
|
list<shared_ptr<PendingCommandPacket>> m_Packets; |
||||||
|
|
||||||
|
// This is only called if m_bWaitForResponse if true. Otherwise, we send the command
|
||||||
|
// and forget about it.
|
||||||
|
function<void()> m_pComplete; |
||||||
|
|
||||||
|
// If true, once we send this command we won't send any other commands until we get
|
||||||
|
// a response.
|
||||||
|
bool m_bIsDeviceInfoCommand = false; |
||||||
|
}; |
||||||
|
list<shared_ptr<PendingCommand>> m_aPendingCommands; |
||||||
|
|
||||||
|
// If set, we've sent a command out of m_aPendingCommands and we're waiting for a response. We
|
||||||
|
// can't send another command until the previous one has completed.
|
||||||
|
shared_ptr<PendingCommand> m_pCurrentCommand = nullptr; |
||||||
|
|
||||||
|
// We always have a read in progress.
|
||||||
|
OVERLAPPED overlapped_read; |
||||||
|
char overlapped_read_buffer[64]; |
||||||
|
|
||||||
|
uint16_t m_iInputState = 0; |
||||||
|
|
||||||
|
// The current device info. We retrieve this when we connect.
|
||||||
|
SMXDeviceInfo m_DeviceInfo; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,155 @@ |
|||||||
|
#include "SMXDeviceSearch.h" |
||||||
|
|
||||||
|
#include "SMXDeviceConnection.h" |
||||||
|
#include "Helpers.h" |
||||||
|
|
||||||
|
#include <string> |
||||||
|
#include <memory> |
||||||
|
#include <set> |
||||||
|
using namespace std; |
||||||
|
using namespace SMX; |
||||||
|
|
||||||
|
#include <hidsdi.h> |
||||||
|
#include <SetupAPI.h> |
||||||
|
|
||||||
|
// Return all USB HID device paths. This doesn't open the device to filter just our devices.
|
||||||
|
static set<wstring> GetAllHIDDevicePaths(wstring &error) |
||||||
|
{ |
||||||
|
HDEVINFO DeviceInfoSet = NULL; |
||||||
|
|
||||||
|
GUID HidGuid; |
||||||
|
HidD_GetHidGuid(&HidGuid); |
||||||
|
DeviceInfoSet = SetupDiGetClassDevs(&HidGuid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT); |
||||||
|
if(DeviceInfoSet == NULL) |
||||||
|
return {}; |
||||||
|
|
||||||
|
set<wstring> paths; |
||||||
|
SP_DEVICE_INTERFACE_DATA DeviceInterfaceData; |
||||||
|
DeviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); |
||||||
|
for(DWORD iIndex = 0; |
||||||
|
SetupDiEnumDeviceInterfaces(DeviceInfoSet, NULL, &HidGuid, iIndex, &DeviceInterfaceData); |
||||||
|
iIndex++) |
||||||
|
{ |
||||||
|
DWORD iSize; |
||||||
|
if(!SetupDiGetDeviceInterfaceDetail(DeviceInfoSet, &DeviceInterfaceData, NULL, 0, &iSize, NULL)) |
||||||
|
{ |
||||||
|
// This call normally fails with ERROR_INSUFFICIENT_BUFFER.
|
||||||
|
int iError = GetLastError(); |
||||||
|
if(iError != ERROR_INSUFFICIENT_BUFFER) |
||||||
|
{ |
||||||
|
// Helpers::FormatWindowsError(error);
|
||||||
|
continue; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
PSP_DEVICE_INTERFACE_DETAIL_DATA DeviceInterfaceDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA) alloca(iSize); |
||||||
|
DeviceInterfaceDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); |
||||||
|
|
||||||
|
SP_DEVINFO_DATA DeviceInfoData; |
||||||
|
ZeroMemory(&DeviceInfoData, sizeof(SP_DEVINFO_DATA)); |
||||||
|
DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); |
||||||
|
if(!SetupDiGetDeviceInterfaceDetail(DeviceInfoSet, &DeviceInterfaceData, DeviceInterfaceDetailData, iSize, NULL, &DeviceInfoData)) |
||||||
|
continue; |
||||||
|
|
||||||
|
paths.insert(DeviceInterfaceDetailData->DevicePath); |
||||||
|
} |
||||||
|
|
||||||
|
SetupDiDestroyDeviceInfoList(DeviceInfoSet); |
||||||
|
|
||||||
|
return paths; |
||||||
|
} |
||||||
|
|
||||||
|
static shared_ptr<AutoCloseHandle> OpenUSBDevice(LPCTSTR DevicePath, wstring &error) |
||||||
|
{ |
||||||
|
HANDLE OpenDevice = CreateFile( |
||||||
|
DevicePath, |
||||||
|
GENERIC_READ | GENERIC_WRITE, |
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE, |
||||||
|
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL |
||||||
|
); |
||||||
|
if(OpenDevice == INVALID_HANDLE_VALUE) |
||||||
|
return nullptr; |
||||||
|
|
||||||
|
auto result = make_shared<AutoCloseHandle>(OpenDevice); |
||||||
|
|
||||||
|
// Get the HID attributes to check the IDs.
|
||||||
|
HIDD_ATTRIBUTES HidAttributes; |
||||||
|
HidAttributes.Size = sizeof(HidAttributes); |
||||||
|
if(!HidD_GetAttributes(result->value(), &HidAttributes)) |
||||||
|
{ |
||||||
|
error = L"HidD_GetAttributes failed"; |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
|
||||||
|
if(HidAttributes.VendorID != 0x2341 || HidAttributes.ProductID != 0x8037) |
||||||
|
return nullptr; |
||||||
|
|
||||||
|
// Since we're using the default Arduino IDs, check the product name to make sure
|
||||||
|
// this isn't some other Arduino device.
|
||||||
|
WCHAR ProductName[255]; |
||||||
|
ZeroMemory(ProductName, sizeof(ProductName)); |
||||||
|
if(!HidD_GetProductString(result->value(), ProductName, 255)) |
||||||
|
return nullptr; |
||||||
|
|
||||||
|
if(wstring(ProductName) != L"StepManiaX") |
||||||
|
return nullptr; |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
vector<shared_ptr<AutoCloseHandle>> SMX::SMXDeviceSearch::GetDevices(wstring &error) |
||||||
|
{ |
||||||
|
set<wstring> aDevicePaths = GetAllHIDDevicePaths(error); |
||||||
|
|
||||||
|
// Remove any entries in m_Devices that are no longer in the list.
|
||||||
|
for(wstring sPath: m_setLastDevicePaths) |
||||||
|
{ |
||||||
|
if(aDevicePaths.find(sPath) != aDevicePaths.end()) |
||||||
|
continue; |
||||||
|
|
||||||
|
Log(ssprintf("Device removed: %ls", sPath.c_str())); |
||||||
|
m_Devices.erase(sPath); |
||||||
|
} |
||||||
|
|
||||||
|
// Check for new entries.
|
||||||
|
for(wstring sPath: aDevicePaths) |
||||||
|
{ |
||||||
|
// Only look at devices that weren't in the list last time. OpenUSBDevice has
|
||||||
|
// to open the device and causes requests to be sent to it.
|
||||||
|
if(m_setLastDevicePaths.find(sPath) != m_setLastDevicePaths.end()) |
||||||
|
continue; |
||||||
|
|
||||||
|
// This will return NULL if this isn't our device.
|
||||||
|
shared_ptr<AutoCloseHandle> hDevice = OpenUSBDevice(sPath.c_str(), error); |
||||||
|
if(hDevice == nullptr) |
||||||
|
continue; |
||||||
|
|
||||||
|
Log(ssprintf("Device added: %ls", sPath.c_str())); |
||||||
|
m_Devices[sPath] = hDevice; |
||||||
|
} |
||||||
|
|
||||||
|
m_setLastDevicePaths = aDevicePaths; |
||||||
|
|
||||||
|
vector<shared_ptr<AutoCloseHandle>> aDevices; |
||||||
|
for(auto it: m_Devices) |
||||||
|
aDevices.push_back(it.second); |
||||||
|
|
||||||
|
return aDevices; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceSearch::DeviceWasClosed(shared_ptr<AutoCloseHandle> pDevice) |
||||||
|
{ |
||||||
|
map<wstring, shared_ptr<AutoCloseHandle>> aDevices; |
||||||
|
for(auto it: m_Devices) |
||||||
|
{ |
||||||
|
if(it.second == pDevice) |
||||||
|
{ |
||||||
|
m_setLastDevicePaths.erase(it.first); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
aDevices[it.first] = it.second; |
||||||
|
} |
||||||
|
} |
||||||
|
m_Devices = aDevices; |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
#ifndef SMXDeviceSearch_h |
||||||
|
#define SMXDeviceSearch_h |
||||||
|
|
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
#include <vector> |
||||||
|
#include <set> |
||||||
|
#include <map> |
||||||
|
using namespace std; |
||||||
|
|
||||||
|
#include "Helpers.h" |
||||||
|
|
||||||
|
namespace SMX { |
||||||
|
|
||||||
|
class SMXDeviceSearch |
||||||
|
{ |
||||||
|
public: |
||||||
|
// Return a list of connected devices. If the same device stays connected and this
|
||||||
|
// is called multiple times, the same handle will be returned.
|
||||||
|
vector<shared_ptr<AutoCloseHandle>> GetDevices(wstring &error); |
||||||
|
|
||||||
|
// After a device is opened and then closed, tell this class that the device was closed.
|
||||||
|
// We'll discard our record of it, so we'll notice a new device plugged in on the same
|
||||||
|
// path.
|
||||||
|
void DeviceWasClosed(shared_ptr<AutoCloseHandle> pDevice); |
||||||
|
|
||||||
|
private: |
||||||
|
set<wstring> m_setLastDevicePaths; |
||||||
|
map<wstring, shared_ptr<AutoCloseHandle>> m_Devices; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,97 @@ |
|||||||
|
#include "SMXDeviceSearchThreaded.h" |
||||||
|
#include "SMXDeviceSearch.h" |
||||||
|
#include "SMXDeviceConnection.h" |
||||||
|
|
||||||
|
#include <windows.h> |
||||||
|
#include <memory> |
||||||
|
using namespace std; |
||||||
|
using namespace SMX; |
||||||
|
|
||||||
|
SMX::SMXDeviceSearchThreaded::SMXDeviceSearchThreaded() |
||||||
|
{ |
||||||
|
m_hEvent = make_shared<AutoCloseHandle>(CreateEvent(NULL, false, false, NULL)); |
||||||
|
m_pDeviceList = make_shared<SMXDeviceSearch>(); |
||||||
|
|
||||||
|
// Start the thread.
|
||||||
|
DWORD id; |
||||||
|
m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &id); |
||||||
|
SMX::SetThreadName(id, "SMXDeviceSearch"); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::SMXDeviceSearchThreaded::~SMXDeviceSearchThreaded() |
||||||
|
{ |
||||||
|
// Shut down the thread, if it's still running.
|
||||||
|
Shutdown(); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceSearchThreaded::Shutdown() |
||||||
|
{ |
||||||
|
if(m_hThread == INVALID_HANDLE_VALUE) |
||||||
|
return; |
||||||
|
|
||||||
|
// Tell the thread to shut down, and wait for it before returning.
|
||||||
|
m_bShutdown = true; |
||||||
|
SetEvent(m_hEvent->value()); |
||||||
|
|
||||||
|
WaitForSingleObject(m_hThread, INFINITE); |
||||||
|
m_hThread = INVALID_HANDLE_VALUE; |
||||||
|
} |
||||||
|
|
||||||
|
DWORD WINAPI SMX::SMXDeviceSearchThreaded::ThreadMainStart(void *self_) |
||||||
|
{ |
||||||
|
SMXDeviceSearchThreaded *self = (SMXDeviceSearchThreaded *) self_; |
||||||
|
self->ThreadMain(); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceSearchThreaded::UpdateDeviceList() |
||||||
|
{ |
||||||
|
m_Lock.AssertNotLockedByCurrentThread(); |
||||||
|
|
||||||
|
// Tell m_pDeviceList about closed devices, so it knows that any device on the
|
||||||
|
// same path is new.
|
||||||
|
m_Lock.Lock(); |
||||||
|
for(auto pDevice: m_apClosedDevices) |
||||||
|
m_pDeviceList->DeviceWasClosed(pDevice); |
||||||
|
m_apClosedDevices.clear(); |
||||||
|
m_Lock.Unlock(); |
||||||
|
|
||||||
|
// Get the current device list.
|
||||||
|
wstring sError; |
||||||
|
vector<shared_ptr<AutoCloseHandle>> apDevices = m_pDeviceList->GetDevices(sError); |
||||||
|
if(!sError.empty()) |
||||||
|
{ |
||||||
|
Log(ssprintf("Error listing USB devices: %ls", sError.c_str())); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Update the device list returned by GetDevices.
|
||||||
|
m_Lock.Lock(); |
||||||
|
m_apDevices = apDevices; |
||||||
|
m_Lock.Unlock(); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceSearchThreaded::ThreadMain() |
||||||
|
{ |
||||||
|
while(!m_bShutdown) |
||||||
|
{ |
||||||
|
UpdateDeviceList(); |
||||||
|
WaitForSingleObjectEx(m_hEvent->value(), 250, true); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXDeviceSearchThreaded::DeviceWasClosed(shared_ptr<AutoCloseHandle> pDevice) |
||||||
|
{ |
||||||
|
// Add pDevice to the list of closed devices. We'll call m_pDeviceList->DeviceWasClosed
|
||||||
|
// on these from the scanning thread.
|
||||||
|
m_apClosedDevices.push_back(pDevice); |
||||||
|
} |
||||||
|
|
||||||
|
vector<shared_ptr<AutoCloseHandle>> SMX::SMXDeviceSearchThreaded::GetDevices() |
||||||
|
{ |
||||||
|
// Lock to make a copy of the device list.
|
||||||
|
m_Lock.Lock(); |
||||||
|
vector<shared_ptr<AutoCloseHandle>> apResult = m_apDevices; |
||||||
|
m_Lock.Unlock(); |
||||||
|
return apResult; |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
#ifndef SMXDeviceSearchThreaded_h |
||||||
|
#define SMXDeviceSearchThreaded_h |
||||||
|
|
||||||
|
#include "Helpers.h" |
||||||
|
#include <windows.h> |
||||||
|
#include <memory> |
||||||
|
#include <vector> |
||||||
|
using namespace std; |
||||||
|
|
||||||
|
namespace SMX { |
||||||
|
|
||||||
|
class SMXDeviceSearch; |
||||||
|
|
||||||
|
// This is a wrapper around SMXDeviceSearch which performs USB scanning in a thread.
|
||||||
|
// It's free on Win10, but takes a while on Windows 7 (about 8ms), so running it on
|
||||||
|
// a separate thread prevents random timing errors when reading HID updates.
|
||||||
|
class SMXDeviceSearchThreaded |
||||||
|
{ |
||||||
|
public: |
||||||
|
SMXDeviceSearchThreaded(); |
||||||
|
~SMXDeviceSearchThreaded(); |
||||||
|
|
||||||
|
// The same interface as SMXDeviceSearch:
|
||||||
|
vector<shared_ptr<SMX::AutoCloseHandle>> GetDevices(); |
||||||
|
void DeviceWasClosed(shared_ptr<SMX::AutoCloseHandle> pDevice); |
||||||
|
|
||||||
|
// Synchronously shut down the thread.
|
||||||
|
void Shutdown(); |
||||||
|
|
||||||
|
private: |
||||||
|
void UpdateDeviceList(); |
||||||
|
|
||||||
|
static DWORD WINAPI ThreadMainStart(void *self_); |
||||||
|
void ThreadMain(); |
||||||
|
|
||||||
|
SMX::Mutex m_Lock; |
||||||
|
shared_ptr<SMXDeviceSearch> m_pDeviceList; |
||||||
|
shared_ptr<SMX::AutoCloseHandle> m_hEvent; |
||||||
|
vector<shared_ptr<SMX::AutoCloseHandle>> m_apDevices; |
||||||
|
vector<shared_ptr<SMX::AutoCloseHandle>> m_apClosedDevices; |
||||||
|
bool m_bShutdown = false; |
||||||
|
HANDLE m_hThread = INVALID_HANDLE_VALUE; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,76 @@ |
|||||||
|
#include "SMXHelperThread.h" |
||||||
|
|
||||||
|
#include <windows.h> |
||||||
|
using namespace SMX; |
||||||
|
|
||||||
|
SMX::SMXHelperThread::SMXHelperThread(const string &sThreadName) |
||||||
|
{ |
||||||
|
m_hEvent = make_shared<AutoCloseHandle>(CreateEvent(NULL, false, false, NULL)); |
||||||
|
|
||||||
|
// Start the thread.
|
||||||
|
m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &m_iThreadId); |
||||||
|
SMX::SetThreadName(m_iThreadId, sThreadName); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::SMXHelperThread::~SMXHelperThread() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXHelperThread::SetHighPriority(bool bHighPriority) |
||||||
|
{ |
||||||
|
SetThreadPriority( m_hThread, THREAD_PRIORITY_HIGHEST ); |
||||||
|
} |
||||||
|
|
||||||
|
DWORD WINAPI SMX::SMXHelperThread::ThreadMainStart(void *self_) |
||||||
|
{ |
||||||
|
SMXHelperThread *self = (SMXHelperThread *) self_; |
||||||
|
self->ThreadMain(); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXHelperThread::ThreadMain() |
||||||
|
{ |
||||||
|
m_Lock.Lock(); |
||||||
|
while(true) |
||||||
|
{ |
||||||
|
vector<function<void()>> funcs; |
||||||
|
swap(m_FunctionsToCall, funcs); |
||||||
|
|
||||||
|
// If we're shutting down and have no more functions to call, stop.
|
||||||
|
if(funcs.empty() && m_bShutdown) |
||||||
|
break; |
||||||
|
|
||||||
|
// Unlock while we call the queued functions.
|
||||||
|
m_Lock.Unlock(); |
||||||
|
for(auto &func: funcs) |
||||||
|
func(); |
||||||
|
|
||||||
|
WaitForSingleObjectEx(m_hEvent->value(), 250, true); |
||||||
|
m_Lock.Lock(); |
||||||
|
} |
||||||
|
m_Lock.Unlock(); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXHelperThread::Shutdown() |
||||||
|
{ |
||||||
|
if(m_hThread == INVALID_HANDLE_VALUE) |
||||||
|
return; |
||||||
|
|
||||||
|
// Tell the thread to shut down, and wait for it before returning.
|
||||||
|
m_bShutdown = true; |
||||||
|
SetEvent(m_hEvent->value()); |
||||||
|
|
||||||
|
WaitForSingleObject(m_hThread, INFINITE); |
||||||
|
m_hThread = INVALID_HANDLE_VALUE; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXHelperThread::RunInThread(function<void()> func) |
||||||
|
{ |
||||||
|
m_Lock.AssertNotLockedByCurrentThread(); |
||||||
|
|
||||||
|
// Add func to the list, and poke the event to wake up the thread if needed.
|
||||||
|
m_Lock.Lock(); |
||||||
|
m_FunctionsToCall.push_back(func); |
||||||
|
SetEvent(m_hEvent->value()); |
||||||
|
m_Lock.Unlock(); |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
#ifndef SMXHelperThread_h |
||||||
|
#define SMXHelperThread_h |
||||||
|
|
||||||
|
#include "Helpers.h" |
||||||
|
|
||||||
|
#include <functional> |
||||||
|
#include <vector> |
||||||
|
#include <memory> |
||||||
|
using namespace std; |
||||||
|
|
||||||
|
namespace SMX |
||||||
|
{ |
||||||
|
class SMXHelperThread |
||||||
|
{ |
||||||
|
public: |
||||||
|
SMXHelperThread(const string &sThreadName); |
||||||
|
~SMXHelperThread(); |
||||||
|
|
||||||
|
// Raise the priority of the helper thread.
|
||||||
|
void SetHighPriority(bool bHighPriority); |
||||||
|
|
||||||
|
// Shut down the thread. Any calls queued by RunInThread will complete before
|
||||||
|
// this returns.
|
||||||
|
void Shutdown(); |
||||||
|
|
||||||
|
// Call func asynchronously from the helper thread.
|
||||||
|
void RunInThread(function<void()> func); |
||||||
|
|
||||||
|
// Return the Win32 thread ID, or INVALID_HANDLE_VALUE if the thread has been
|
||||||
|
// shut down.
|
||||||
|
DWORD GetThreadId() const { return m_iThreadId; } |
||||||
|
|
||||||
|
private: |
||||||
|
static DWORD WINAPI ThreadMainStart(void *self_); |
||||||
|
void ThreadMain(); |
||||||
|
|
||||||
|
DWORD m_iThreadId = 0; |
||||||
|
SMX::Mutex m_Lock; |
||||||
|
shared_ptr<SMX::AutoCloseHandle> m_hEvent; |
||||||
|
bool m_bShutdown = false; |
||||||
|
HANDLE m_hThread = INVALID_HANDLE_VALUE; |
||||||
|
vector<function<void()>> m_FunctionsToCall; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,452 @@ |
|||||||
|
#include "SMXManager.h" |
||||||
|
#include "SMXDevice.h" |
||||||
|
#include "SMXDeviceConnection.h" |
||||||
|
#include "SMXDeviceSearchThreaded.h" |
||||||
|
#include "Helpers.h" |
||||||
|
|
||||||
|
#include <windows.h> |
||||||
|
#include <memory> |
||||||
|
using namespace std; |
||||||
|
using namespace SMX; |
||||||
|
|
||||||
|
namespace { |
||||||
|
Mutex g_Lock; |
||||||
|
} |
||||||
|
|
||||||
|
SMX::SMXManager::SMXManager(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback): |
||||||
|
m_UserCallbackThread("SMXUserCallbackThread") |
||||||
|
{ |
||||||
|
// Raise the priority of the user callback thread, since we don't want input
|
||||||
|
// events to be preempted by other things and reduce timing accuracy.
|
||||||
|
m_UserCallbackThread.SetHighPriority(true); |
||||||
|
m_hEvent = make_shared<AutoCloseHandle>(CreateEvent(NULL, false, false, NULL)); |
||||||
|
m_pSMXDeviceSearchThreaded = make_shared<SMXDeviceSearchThreaded>(); |
||||||
|
|
||||||
|
// Create the SMXDevices. We don't create these as we connect, we just reuse the same
|
||||||
|
// ones.
|
||||||
|
for(int i = 0; i < 2; ++i) |
||||||
|
{ |
||||||
|
shared_ptr<SMXDevice> pDevice = SMXDevice::Create(m_hEvent, g_Lock); |
||||||
|
m_pDevices.push_back(pDevice); |
||||||
|
} |
||||||
|
|
||||||
|
// The callback we send to SMXDeviceConnection will be called from our thread. Wrap
|
||||||
|
// it so it's called from UserCallbackThread instead.
|
||||||
|
auto pCallbackInThread = [this, pCallback](int PadNumber, SMXUpdateCallbackReason reason) { |
||||||
|
m_UserCallbackThread.RunInThread([pCallback, PadNumber, reason]() { |
||||||
|
pCallback(PadNumber, reason); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
// Set the update callbacks. Do this before starting the thread, to avoid race conditions.
|
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
m_pDevices[pad]->SetUpdateCallback(pCallbackInThread); |
||||||
|
|
||||||
|
// Start the thread.
|
||||||
|
DWORD id; |
||||||
|
m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &id); |
||||||
|
SMX::SetThreadName(id, "SMXManager"); |
||||||
|
|
||||||
|
// Raise the priority of the I/O thread, since we don't want input
|
||||||
|
// events to be preempted by other things and reduce timing accuracy.
|
||||||
|
SetThreadPriority( m_hThread, THREAD_PRIORITY_HIGHEST ); |
||||||
|
} |
||||||
|
|
||||||
|
SMX::SMXManager::~SMXManager() |
||||||
|
{ |
||||||
|
// Shut down the thread, if it's still running.
|
||||||
|
Shutdown(); |
||||||
|
} |
||||||
|
|
||||||
|
shared_ptr<SMXDevice> SMX::SMXManager::GetDevice(int pad) |
||||||
|
{ |
||||||
|
return m_pDevices[pad]; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXManager::Shutdown() |
||||||
|
{ |
||||||
|
g_Lock.AssertNotLockedByCurrentThread(); |
||||||
|
|
||||||
|
// Make sure we're not being called from within m_UserCallbackThread, since that'll
|
||||||
|
// deadlock when we shut down m_UserCallbackThread.
|
||||||
|
if(m_UserCallbackThread.GetThreadId() == GetCurrentThreadId()) |
||||||
|
throw runtime_error("SMX::SMXManager::Shutdown must not be called from an SMX callback"); |
||||||
|
|
||||||
|
// Shut down the thread we make user callbacks from.
|
||||||
|
m_UserCallbackThread.Shutdown(); |
||||||
|
|
||||||
|
// Shut down the device search thread.
|
||||||
|
m_pSMXDeviceSearchThreaded->Shutdown(); |
||||||
|
|
||||||
|
if(m_hThread == INVALID_HANDLE_VALUE) |
||||||
|
return; |
||||||
|
|
||||||
|
// Tell the thread to shut down, and wait for it before returning.
|
||||||
|
m_bShutdown = true; |
||||||
|
SetEvent(m_hEvent->value()); |
||||||
|
|
||||||
|
WaitForSingleObject(m_hThread, INFINITE); |
||||||
|
m_hThread = INVALID_HANDLE_VALUE; |
||||||
|
} |
||||||
|
|
||||||
|
DWORD WINAPI SMX::SMXManager::ThreadMainStart(void *self_) |
||||||
|
{ |
||||||
|
SMXManager *self = (SMXManager *) self_; |
||||||
|
self->ThreadMain(); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
// When we connect to a device, we don't know whether it's P1 or P2, since we get that
|
||||||
|
// info from the device after we connect to it. If we have a P2 device in SMX_PadNumber_1
|
||||||
|
// or a P1 device in SMX_PadNumber_2, swap the two.
|
||||||
|
void SMX::SMXManager::CorrectDeviceOrder() |
||||||
|
{ |
||||||
|
// We're still holding the lock from when we updated the devices, so the application
|
||||||
|
// won't see the devices out of order before we do this.
|
||||||
|
g_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
SMXInfo info[2]; |
||||||
|
m_pDevices[0]->GetInfoLocked(info[0]); |
||||||
|
m_pDevices[1]->GetInfoLocked(info[1]); |
||||||
|
|
||||||
|
// If we have two P1s or two P2s, the pads are misconfigured and we'll just leave the order alone.
|
||||||
|
bool Player2[2] = { |
||||||
|
m_pDevices[0]->IsPlayer2Locked(), |
||||||
|
m_pDevices[1]->IsPlayer2Locked(), |
||||||
|
}; |
||||||
|
if(info[0].m_bConnected && info[1].m_bConnected && Player2[0] == Player2[1]) |
||||||
|
return; |
||||||
|
|
||||||
|
bool bP1NeedsSwap = info[0].m_bConnected && Player2[0]; |
||||||
|
bool bP2NeedsSwap = info[1].m_bConnected && !Player2[1]; |
||||||
|
if(bP1NeedsSwap || bP2NeedsSwap) |
||||||
|
swap(m_pDevices[0], m_pDevices[1]); |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXManager::ThreadMain() |
||||||
|
{ |
||||||
|
g_Lock.Lock(); |
||||||
|
|
||||||
|
while(!m_bShutdown) |
||||||
|
{ |
||||||
|
// If there are any lights commands to be sent, send them now. Do this before callig Update(),
|
||||||
|
// since this actually just queues commands, which are actually handled in Update.
|
||||||
|
SendLightUpdates(); |
||||||
|
|
||||||
|
// See if there are any new devices.
|
||||||
|
AttemptConnections(); |
||||||
|
|
||||||
|
// Update all connected devices.
|
||||||
|
for(shared_ptr<SMXDevice> pDevice: m_pDevices) |
||||||
|
{ |
||||||
|
wstring sError; |
||||||
|
pDevice->Update(sError); |
||||||
|
|
||||||
|
if(!sError.empty()) |
||||||
|
{ |
||||||
|
Log(ssprintf("Device error: %ls", sError.c_str())); |
||||||
|
|
||||||
|
// Tell m_pDeviceList that the device was closed, so it'll discard the device
|
||||||
|
// and notice if a new device shows up on the same path.
|
||||||
|
m_pSMXDeviceSearchThreaded->DeviceWasClosed(pDevice->GetDeviceHandle()); |
||||||
|
pDevice->CloseDevice(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Devices may have finished initializing, so see if we need to update the ordering.
|
||||||
|
CorrectDeviceOrder(); |
||||||
|
|
||||||
|
// Make a list of handles for WaitForMultipleObjectsEx.
|
||||||
|
vector<HANDLE> aHandles = { m_hEvent->value() }; |
||||||
|
for(shared_ptr<SMXDevice> pDevice: m_pDevices) |
||||||
|
{ |
||||||
|
shared_ptr<AutoCloseHandle> pHandle = pDevice->GetDeviceHandle(); |
||||||
|
if(pHandle) |
||||||
|
aHandles.push_back(pHandle->value()); |
||||||
|
} |
||||||
|
|
||||||
|
// See how long we should block waiting for I/O. If we have any scheduled lights commands,
|
||||||
|
// wait until the next command should be sent, otherwise wait for a second.
|
||||||
|
int iDelayMS = 1000; |
||||||
|
if(!m_aPendingCommands.empty()) |
||||||
|
{ |
||||||
|
double fSendIn = m_aPendingCommands[0].fTimeToSend - GetMonotonicTime(); |
||||||
|
|
||||||
|
// Add 1ms to the delay time. We're using a high resolution timer, but
|
||||||
|
// WaitForMultipleObjectsEx only has 1ms resolution, so this keeps us from
|
||||||
|
// repeatedly waking up slightly too early.
|
||||||
|
iDelayMS = int(fSendIn * 1000) + 1; |
||||||
|
iDelayMS = max(0, iDelayMS); |
||||||
|
} |
||||||
|
|
||||||
|
// Wait until there's something to do for a connected device, or delay briefly if we're
|
||||||
|
// not connected to anything. Unlock while we block. Devices are only ever opened or
|
||||||
|
// closed from within this thread, so the handles won't go away while we're waiting on
|
||||||
|
// them.
|
||||||
|
g_Lock.Unlock(); |
||||||
|
WaitForMultipleObjectsEx(aHandles.size(), aHandles.data(), false, iDelayMS, true); |
||||||
|
g_Lock.Lock(); |
||||||
|
} |
||||||
|
g_Lock.Unlock(); |
||||||
|
} |
||||||
|
|
||||||
|
// Lights are updated with two commands. The top two rows of LEDs in each panel are
|
||||||
|
// updated by the first command, and the bottom two rows are updated by the second
|
||||||
|
// command. We need to send the two commands in order. The panel won't update lights
|
||||||
|
// until both commands have been received, so we don't flicker the partial top update
|
||||||
|
// before the bottom update is received.
|
||||||
|
//
|
||||||
|
// A complete update can be performed at up to 30 FPS, but we actually update at 60
|
||||||
|
// FPS, alternating between updating the top and bottom half.
|
||||||
|
//
|
||||||
|
// This interlacing is performed to reduce the amount of work the panels and master
|
||||||
|
// controller need to do on each update. This improves timing accuracy, since less
|
||||||
|
// time is taken by each update.
|
||||||
|
//
|
||||||
|
// The order of lights is:
|
||||||
|
//
|
||||||
|
// 0123 0123 0123
|
||||||
|
// 4567 4567 4567
|
||||||
|
// 89AB 89AB 89AB
|
||||||
|
// CDEF CDEF CDEF
|
||||||
|
//
|
||||||
|
// 0123 0123 0123
|
||||||
|
// 4567 4567 4567
|
||||||
|
// 89AB 89AB 89AB
|
||||||
|
// CDEF CDEF CDEF
|
||||||
|
//
|
||||||
|
// 0123 0123 0123
|
||||||
|
// 4567 4567 4567
|
||||||
|
// 89AB 89AB 89AB
|
||||||
|
// CDEF CDEF CDEF
|
||||||
|
//
|
||||||
|
// with panels left-to-right, top-to-bottom. The first packet sends all 0123 and 4567
|
||||||
|
// lights, and the second packet sends 78AB and CDEF.
|
||||||
|
//
|
||||||
|
// We hide these details from the API to simplify things for the user:
|
||||||
|
//
|
||||||
|
// - The user sends us a complete lights set. This should be sent at (up to) 30Hz.
|
||||||
|
// If we get lights data too quickly, we'll always complete the one we started before
|
||||||
|
// sending the next.
|
||||||
|
// - We don't limit to exactly 30Hz to prevent phase issues where a 60 FPS game is
|
||||||
|
// coming in and out of phase with our timer. To avoid this, we limit to 40Hz.
|
||||||
|
// - When we have new lights data to send, we send the first half right away, wait
|
||||||
|
// 16ms (60Hz), then send the second half, which is the pacing the device expects.
|
||||||
|
// - If we get a new lights update in between the two lights commands, we won't split
|
||||||
|
// the lights. The two lights commands will always come from the same update, so
|
||||||
|
// we don't get weird interlacing effects.
|
||||||
|
// - If SMX_ReenableAutoLights is called between the two commands, we need to guarantee
|
||||||
|
// that we don't send the second lights commands, since that may re-disable auto lights.
|
||||||
|
// - If we have two pads, the lights update is for both pads and we'll send both commands
|
||||||
|
// for both pads at the same time, so both pads update lights simultaneously.
|
||||||
|
void SMX::SMXManager::SetLights(const string &sLightData) |
||||||
|
{ |
||||||
|
g_Lock.AssertNotLockedByCurrentThread(); |
||||||
|
LockMutex L(g_Lock); |
||||||
|
|
||||||
|
// Sanity check the lights data. It should have 18*16*3 bytes of data: RGB for each of 4x4
|
||||||
|
// LEDs on 18 panels.
|
||||||
|
if(sLightData.size() != 2*3*3*16*3) |
||||||
|
{ |
||||||
|
Log(ssprintf("SetLights: Lights data should be %i bytes, received %i", 2*3*3*16*3, sLightData.size())); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Split the lights data into P1 and P2.
|
||||||
|
string sPanelLights[2]; |
||||||
|
sPanelLights[0] = sLightData.substr(0, 9*16*3); |
||||||
|
sPanelLights[1] = sLightData.substr(9*16*3); |
||||||
|
|
||||||
|
// Separate top and bottom lights commands.
|
||||||
|
//
|
||||||
|
// sPanelLights[iPad] is
|
||||||
|
//
|
||||||
|
// 0123 0123 0123
|
||||||
|
// 4567 4567 4567
|
||||||
|
// 89AB 89AB 89AB
|
||||||
|
// CDEF CDEF CDEF
|
||||||
|
//
|
||||||
|
// 0123 0123 0123
|
||||||
|
// 4567 4567 4567
|
||||||
|
// 89AB 89AB 89AB
|
||||||
|
// CDEF CDEF CDEF
|
||||||
|
//
|
||||||
|
// 0123 0123 0123
|
||||||
|
// 4567 4567 4567
|
||||||
|
// 89AB 89AB 89AB
|
||||||
|
// CDEF CDEF CDEF
|
||||||
|
//
|
||||||
|
// Set sLightsCommand[iPad][0] to include 0123 4567, and [1] to 89AB CDEF.
|
||||||
|
string sLightCommands[2][2]; // sLightCommands[command][pad]
|
||||||
|
|
||||||
|
auto addByte = [&sLightCommands](int iPanel, int iByte, uint8_t iColor) { |
||||||
|
// If iPanel is 0-8, this is for pad 0. For 9-17, it's for pad 1.
|
||||||
|
// If the color byte within the panel is in the top half, it's the first
|
||||||
|
// command, otherwise it's the second command.
|
||||||
|
int iPad = iPanel < 9? 0:1; |
||||||
|
int iCommandIndex = iByte < 4*2*3? 0:1; |
||||||
|
sLightCommands[iCommandIndex][iPad].append(1, iColor); |
||||||
|
}; |
||||||
|
|
||||||
|
// Read the linearly arranged color data we've been given and split it into top and
|
||||||
|
// bottom commands for each pad.
|
||||||
|
int iNextInputByte = 0; |
||||||
|
for(int iPanel = 0; iPanel < 18; ++iPanel) |
||||||
|
{ |
||||||
|
for(int iByte = 0; iByte < 4*4*3; ++iByte) |
||||||
|
{ |
||||||
|
uint8_t iColor = sLightData[iNextInputByte++]; |
||||||
|
addByte(iPanel, iByte, iColor); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for(int iPad = 0; iPad < 2; ++iPad) |
||||||
|
{ |
||||||
|
for(int iCommand = 0; iCommand < 2; ++iCommand) |
||||||
|
{ |
||||||
|
string &sCommand = sLightCommands[iCommand][iPad]; |
||||||
|
|
||||||
|
// Apply color scaling. Values over about 170 don't make the LEDs any brighter, so this
|
||||||
|
// gives better contrast and draws less power.
|
||||||
|
for(char &c: sCommand) |
||||||
|
c = char(uint8_t(c) * 0.6666f); |
||||||
|
|
||||||
|
// Add the command byte.
|
||||||
|
sCommand.insert(sCommand.begin(), 1, iCommand == 0? '2':'3'); |
||||||
|
sCommand.push_back('\n'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Each update adds two entries to m_aPendingCommands, one for the top half and one
|
||||||
|
// for the lower half.
|
||||||
|
//
|
||||||
|
// If there's one entry in the list, we've already sent the first half of a previous update,
|
||||||
|
// and the remaining entry is the second half. We'll leave it in place so we always finish
|
||||||
|
// an update once we start it, and add this update after it.
|
||||||
|
//
|
||||||
|
// If there are two entries in the list, then it's an existing update that we haven't sent yet.
|
||||||
|
// If there are three entries, we added an update after a partial update. In either case, the
|
||||||
|
// last two commands in the list are a complete lights update, and we'll just update it in-place.
|
||||||
|
//
|
||||||
|
// This way, we'll always finish a lights update once we start it, so if we receive lights updates
|
||||||
|
// very quickly we won't just keep sending the first half and never finish one. Otherwise, we'll
|
||||||
|
// update with the newest data we have available.
|
||||||
|
if(m_aPendingCommands.size() <= 1) |
||||||
|
{ |
||||||
|
static const double fDelayBetweenLightsCommands = 1/60.0; |
||||||
|
|
||||||
|
double fNow = GetMonotonicTime(); |
||||||
|
double fSendCommandAt = max(fNow, m_fDelayLightCommandsUntil); |
||||||
|
float fFirstCommandTime = fSendCommandAt; |
||||||
|
float fSecondCommandTime = fFirstCommandTime + fDelayBetweenLightsCommands; |
||||||
|
|
||||||
|
// Update m_fDelayLightCommandsUntil, so we know when the next
|
||||||
|
m_fDelayLightCommandsUntil = fSecondCommandTime + fDelayBetweenLightsCommands; |
||||||
|
|
||||||
|
// Add two commands to the list, scheduled at fFirstCommandTime and fSecondCommandTime.
|
||||||
|
m_aPendingCommands.push_back(PendingCommand(fFirstCommandTime)); |
||||||
|
m_aPendingCommands.push_back(PendingCommand(fSecondCommandTime)); |
||||||
|
// Log(ssprintf("Scheduled commands at %f and %f", fFirstCommandTime, fSecondCommandTime));
|
||||||
|
|
||||||
|
// Wake up the I/O thread if it's blocking on WaitForMultipleObjectsEx.
|
||||||
|
SetEvent(m_hEvent->value()); |
||||||
|
} |
||||||
|
|
||||||
|
// Set the pad commands.
|
||||||
|
PendingCommand *pPendingCommands[2]; |
||||||
|
pPendingCommands[0] = &m_aPendingCommands[m_aPendingCommands.size()-2]; |
||||||
|
pPendingCommands[1] = &m_aPendingCommands[m_aPendingCommands.size()-1]; |
||||||
|
|
||||||
|
pPendingCommands[0]->sPadCommand[0] = sLightCommands[0][0]; |
||||||
|
pPendingCommands[0]->sPadCommand[1] = sLightCommands[0][1]; |
||||||
|
pPendingCommands[1]->sPadCommand[0] = sLightCommands[1][0]; |
||||||
|
pPendingCommands[1]->sPadCommand[1] = sLightCommands[1][1]; |
||||||
|
} |
||||||
|
|
||||||
|
void SMX::SMXManager::ReenableAutoLights() |
||||||
|
{ |
||||||
|
g_Lock.AssertNotLockedByCurrentThread(); |
||||||
|
LockMutex L(g_Lock); |
||||||
|
|
||||||
|
// Clear any pending lights commands, so we don't re-disable auto-lighting by sending a
|
||||||
|
// lights command after we enable it. If we've sent the first half of a lights update
|
||||||
|
// and this causes us to not send the second half, the controller will just discard it.
|
||||||
|
m_aPendingCommands.clear(); |
||||||
|
for(int iPad = 0; iPad < 2; ++iPad) |
||||||
|
m_pDevices[iPad]->SendCommandLocked(string("S 1\n", 4)); |
||||||
|
} |
||||||
|
|
||||||
|
// Check to see if we should send any commands in m_aPendingCommands.
|
||||||
|
void SMX::SMXManager::SendLightUpdates() |
||||||
|
{ |
||||||
|
g_Lock.AssertLockedByCurrentThread(); |
||||||
|
if(m_aPendingCommands.empty()) |
||||||
|
return; |
||||||
|
|
||||||
|
const PendingCommand &command = m_aPendingCommands[0]; |
||||||
|
|
||||||
|
// See if it's time to send the next command. We only need to look at the first
|
||||||
|
// command, since these are always sorted.
|
||||||
|
if(command.fTimeToSend > GetMonotonicTime()) |
||||||
|
return; |
||||||
|
|
||||||
|
// Send the lights command for each pad. If either pad isn't connected, this won't do
|
||||||
|
// anything.
|
||||||
|
for(int iPad = 0; iPad < 2; ++iPad) |
||||||
|
m_pDevices[iPad]->SendCommandLocked(command.sPadCommand[iPad]); |
||||||
|
|
||||||
|
// Remove the command we've sent.
|
||||||
|
m_aPendingCommands.erase(m_aPendingCommands.begin(), m_aPendingCommands.begin()+1); |
||||||
|
} |
||||||
|
|
||||||
|
// See if there are any new devices to connect to.
|
||||||
|
void SMX::SMXManager::AttemptConnections() |
||||||
|
{ |
||||||
|
g_Lock.AssertLockedByCurrentThread(); |
||||||
|
|
||||||
|
vector<shared_ptr<AutoCloseHandle>> apDevices = m_pSMXDeviceSearchThreaded->GetDevices(); |
||||||
|
|
||||||
|
// Check each device that we've found. This will include ones we already have open.
|
||||||
|
for(shared_ptr<AutoCloseHandle> pHandle: apDevices) |
||||||
|
{ |
||||||
|
// See if this device is already open. If it is, we don't need to do anything with it.
|
||||||
|
bool bAlreadyOpen = false; |
||||||
|
for(shared_ptr<SMXDevice> pDevice: m_pDevices) |
||||||
|
{ |
||||||
|
if(pDevice->GetDeviceHandle() == pHandle) |
||||||
|
bAlreadyOpen = true; |
||||||
|
} |
||||||
|
if(bAlreadyOpen) |
||||||
|
continue; |
||||||
|
|
||||||
|
// Find an open device slot.
|
||||||
|
shared_ptr<SMXDevice> pDeviceToOpen; |
||||||
|
for(shared_ptr<SMXDevice> pDevice: m_pDevices) |
||||||
|
{ |
||||||
|
// Note that we check whether the device has a handle rather than calling IsConnected, since
|
||||||
|
// devices aren't actually considered connected until they've read the configuration.
|
||||||
|
if(pDevice->GetDeviceHandle() == NULL) |
||||||
|
{ |
||||||
|
pDeviceToOpen = pDevice; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(pDeviceToOpen == nullptr) |
||||||
|
{ |
||||||
|
// All device slots are used. Are there more than two devices plugged in?
|
||||||
|
Log("Error: No available slots for device. Are more than two devices connected?"); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// Open the device in this slot.
|
||||||
|
Log("Opening SMX device"); |
||||||
|
wstring sError; |
||||||
|
pDeviceToOpen->OpenDeviceHandle(pHandle, sError); |
||||||
|
if(!sError.empty()) |
||||||
|
Log(ssprintf("Error opening device: %ls", sError.c_str())); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,76 @@ |
|||||||
|
#ifndef SMXManager_h |
||||||
|
#define SMXManager_h |
||||||
|
|
||||||
|
#include <windows.h> |
||||||
|
#include <memory> |
||||||
|
#include <vector> |
||||||
|
#include <functional> |
||||||
|
using namespace std; |
||||||
|
|
||||||
|
#include "Helpers.h" |
||||||
|
#include "../SMX.h" |
||||||
|
#include "SMXHelperThread.h" |
||||||
|
|
||||||
|
namespace SMX { |
||||||
|
class SMXDevice; |
||||||
|
class SMXDeviceSearchThreaded; |
||||||
|
|
||||||
|
struct SMXControllerState |
||||||
|
{ |
||||||
|
// True
|
||||||
|
bool m_bConnected[2]; |
||||||
|
|
||||||
|
// Pressed panels for player 1 and player 2:
|
||||||
|
uint16_t m_Inputs[2]; |
||||||
|
}; |
||||||
|
|
||||||
|
// This implements the main thread that controller communication and device searching
|
||||||
|
// happens in, finding and opening devices, and running device updates.
|
||||||
|
//
|
||||||
|
// Connected controllers can be accessed with GetDevice(),
|
||||||
|
// This also abstracts controller numbers. GetDevice(SMX_PadNumber_1) will return the
|
||||||
|
// first device that connected,
|
||||||
|
class SMXManager |
||||||
|
{ |
||||||
|
public: |
||||||
|
// pCallback is a function to be called when something changes on any device. This allows
|
||||||
|
// efficiently detecting when a panel is pressed or other changes happen.
|
||||||
|
SMXManager(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback); |
||||||
|
~SMXManager(); |
||||||
|
|
||||||
|
void Shutdown(); |
||||||
|
shared_ptr<SMXDevice> GetDevice(int pad); |
||||||
|
void SetLights(const string &sLightData); |
||||||
|
void ReenableAutoLights(); |
||||||
|
|
||||||
|
private: |
||||||
|
static DWORD WINAPI ThreadMainStart(void *self_); |
||||||
|
void ThreadMain(); |
||||||
|
void AttemptConnections(); |
||||||
|
void CorrectDeviceOrder(); |
||||||
|
void SendLightUpdates(); |
||||||
|
|
||||||
|
HANDLE m_hThread = INVALID_HANDLE_VALUE; |
||||||
|
shared_ptr<SMX::AutoCloseHandle> m_hEvent; |
||||||
|
shared_ptr<SMXDeviceSearchThreaded> m_pSMXDeviceSearchThreaded; |
||||||
|
bool m_bShutdown = false; |
||||||
|
vector<shared_ptr<SMXDevice>> m_pDevices; |
||||||
|
|
||||||
|
// We make user callbacks asynchronously in this thread, to avoid any locking or timing
|
||||||
|
// issues that could occur by calling them in our I/O thread.
|
||||||
|
SMXHelperThread m_UserCallbackThread; |
||||||
|
|
||||||
|
// A list of queued lights commands to send to the controllers. This is always sorted
|
||||||
|
// by iTimeToSend.
|
||||||
|
struct PendingCommand |
||||||
|
{ |
||||||
|
PendingCommand(float fTime): fTimeToSend(fTime) { } |
||||||
|
double fTimeToSend = 0; |
||||||
|
string sPadCommand[2]; |
||||||
|
}; |
||||||
|
vector<PendingCommand> m_aPendingCommands; |
||||||
|
double m_fDelayLightCommandsUntil = 0; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1 @@ |
|||||||
|
bin |
@ -0,0 +1,9 @@ |
|||||||
|
<Application x:Class="smx_config.App" |
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:local="clr-namespace:smx_config" |
||||||
|
StartupUri="MainWindow.xaml"> |
||||||
|
<Application.Resources> |
||||||
|
|
||||||
|
</Application.Resources> |
||||||
|
</Application> |
@ -0,0 +1,32 @@ |
|||||||
|
using System; |
||||||
|
using System.Windows; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
|
||||||
|
namespace smx_config |
||||||
|
{ |
||||||
|
public partial class App: Application |
||||||
|
{ |
||||||
|
[DllImport("Kernel32")] |
||||||
|
private static extern void AllocConsole(); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern void SMX_Internal_OpenConsole(); |
||||||
|
|
||||||
|
App() |
||||||
|
{ |
||||||
|
if(Helpers.GetDebug()) |
||||||
|
SMX_Internal_OpenConsole(); |
||||||
|
CurrentSMXDevice.singleton = new CurrentSMXDevice(); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e) |
||||||
|
{ |
||||||
|
base.OnExit(e); |
||||||
|
|
||||||
|
// Shut down cleanly, to make sure we don't run any threaded callbacks during shutdown. |
||||||
|
Console.WriteLine("Application exiting"); |
||||||
|
CurrentSMXDevice.singleton.Shutdown(); |
||||||
|
CurrentSMXDevice.singleton = null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,137 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Text; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace smx_config |
||||||
|
{ |
||||||
|
class ConfigPresets |
||||||
|
{ |
||||||
|
static private string[] Presets = { "low", "normal", "high" }; |
||||||
|
|
||||||
|
static public string GetPreset(SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
// If the config we're comparing against is a V1 config, that means the per-panel thresholds |
||||||
|
// weren't set and those fields are ignored (or not supported by) by the firmware. Sync those |
||||||
|
// thresholds to the unified thresholds before comparing. That way, we'll correctly match up |
||||||
|
// each preset regardless of what happens to be in the unused per-panel threshold fields. |
||||||
|
// If we don't do this, we won't recognize that the default preset is active because unused |
||||||
|
// fields won't match up. |
||||||
|
if(config.configVersion == 0xFF || config.configVersion < 2) |
||||||
|
SyncUnifiedThresholds(ref config); |
||||||
|
|
||||||
|
foreach(string Preset in Presets) |
||||||
|
{ |
||||||
|
SMX.SMXConfig PresetConfig = config; |
||||||
|
SetPreset(Preset, ref PresetConfig); |
||||||
|
if(SamePreset(config, PresetConfig)) |
||||||
|
return Preset; |
||||||
|
} |
||||||
|
return ""; |
||||||
|
} |
||||||
|
|
||||||
|
// Return true if the config matches, only comparing values that we set in presets. |
||||||
|
static private bool SamePreset(SMX.SMXConfig config1, SMX.SMXConfig config2) |
||||||
|
{ |
||||||
|
// These aren't arrays for compatibility reasons. |
||||||
|
if( config1.panelThreshold0High != config2.panelThreshold0High || |
||||||
|
config1.panelThreshold1High != config2.panelThreshold1High || |
||||||
|
config1.panelThreshold2High != config2.panelThreshold2High || |
||||||
|
config1.panelThreshold3High != config2.panelThreshold3High || |
||||||
|
config1.panelThreshold4High != config2.panelThreshold4High || |
||||||
|
config1.panelThreshold5High != config2.panelThreshold5High || |
||||||
|
config1.panelThreshold6High != config2.panelThreshold6High || |
||||||
|
config1.panelThreshold7High != config2.panelThreshold7High || |
||||||
|
config1.panelThreshold8High != config2.panelThreshold8High) |
||||||
|
return false; |
||||||
|
if( config1.panelThreshold0Low != config2.panelThreshold0Low || |
||||||
|
config1.panelThreshold1Low != config2.panelThreshold1Low || |
||||||
|
config1.panelThreshold2Low != config2.panelThreshold2Low || |
||||||
|
config1.panelThreshold3Low != config2.panelThreshold3Low || |
||||||
|
config1.panelThreshold4Low != config2.panelThreshold4Low || |
||||||
|
config1.panelThreshold5Low != config2.panelThreshold5Low || |
||||||
|
config1.panelThreshold6Low != config2.panelThreshold6Low || |
||||||
|
config1.panelThreshold7Low != config2.panelThreshold7Low || |
||||||
|
config1.panelThreshold8Low != config2.panelThreshold8Low) |
||||||
|
return false; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
static public void SetPreset(string name, ref SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
switch(name) |
||||||
|
{ |
||||||
|
case "low": SetLowPreset(ref config); return; |
||||||
|
case "normal": SetNormalPreset(ref config); return; |
||||||
|
case "high": SetHighPreset(ref config); return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static private void SetHighPreset(ref SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
config.panelThreshold7Low = // cardinal |
||||||
|
config.panelThreshold1Low = // up |
||||||
|
config.panelThreshold2Low = 20; // corner |
||||||
|
config.panelThreshold7High = // cardinal |
||||||
|
config.panelThreshold1High = // up |
||||||
|
config.panelThreshold2High = 25; // corner |
||||||
|
|
||||||
|
config.panelThreshold4Low = 20; // center |
||||||
|
config.panelThreshold4High = 30; |
||||||
|
SyncUnifiedThresholds(ref config); |
||||||
|
} |
||||||
|
|
||||||
|
static private void SetNormalPreset(ref SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
config.panelThreshold7Low = // cardinal |
||||||
|
config.panelThreshold1Low = // up |
||||||
|
config.panelThreshold2Low = 33; // corner |
||||||
|
config.panelThreshold7High = // cardinal |
||||||
|
config.panelThreshold1High = // up |
||||||
|
config.panelThreshold2High = 42; // corner |
||||||
|
|
||||||
|
config.panelThreshold4Low = 35; // center |
||||||
|
config.panelThreshold4High = 60; |
||||||
|
SyncUnifiedThresholds(ref config); |
||||||
|
} |
||||||
|
|
||||||
|
static private void SetLowPreset(ref SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
config.panelThreshold7Low = // cardinal |
||||||
|
config.panelThreshold1Low = // up |
||||||
|
config.panelThreshold2Low = 70; // corner |
||||||
|
config.panelThreshold7High = // cardinal |
||||||
|
config.panelThreshold1High = // up |
||||||
|
config.panelThreshold2High = 80; // corner |
||||||
|
|
||||||
|
config.panelThreshold4Low = 100; // center |
||||||
|
config.panelThreshold4High = 120; |
||||||
|
SyncUnifiedThresholds(ref config); |
||||||
|
} |
||||||
|
|
||||||
|
// The simplified configuration scheme sets thresholds for up, center, cardinal directions |
||||||
|
// and corners. Rev1 firmware uses those only. Copy cardinal directions (down) to the |
||||||
|
// other cardinal directions (except for up, which already had its own setting) and corners |
||||||
|
// to the other corners. |
||||||
|
static public void SyncUnifiedThresholds(ref SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
// left = right = down (cardinal) |
||||||
|
config.panelThreshold3Low = config.panelThreshold5Low = config.panelThreshold7Low; |
||||||
|
config.panelThreshold3High = config.panelThreshold5High = config.panelThreshold7High; |
||||||
|
|
||||||
|
// UL = DL = DR = UR (corners) |
||||||
|
config.panelThreshold0Low = config.panelThreshold6Low = config.panelThreshold8Low = config.panelThreshold2Low; |
||||||
|
config.panelThreshold0High = config.panelThreshold6High = config.panelThreshold8High = config.panelThreshold2High; |
||||||
|
} |
||||||
|
|
||||||
|
// Return true if the panel thresholds are already synced, so SyncUnifiedThresholds would |
||||||
|
// have no effect. |
||||||
|
static public bool AreUnifiedThresholdsSynced(SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
SMX.SMXConfig config2 = config; |
||||||
|
SyncUnifiedThresholds(ref config2); |
||||||
|
return SamePreset(config, config2); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,281 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Windows; |
||||||
|
using System.Linq; |
||||||
|
using System.Text; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using System.Windows.Threading; |
||||||
|
using System.Windows.Controls; |
||||||
|
using System.ComponentModel; |
||||||
|
|
||||||
|
namespace smx_config |
||||||
|
{ |
||||||
|
// The state and configuration of a pad. |
||||||
|
public struct LoadFromConfigDelegateArgsPerController |
||||||
|
{ |
||||||
|
public SMX.SMXInfo info; |
||||||
|
public SMX.SMXConfig config; |
||||||
|
public SMX.SMXSensorTestModeData test_data; |
||||||
|
|
||||||
|
// The panels that are activated. Note that to receive notifications from OnConfigChange |
||||||
|
// when inputs change state, set RefreshOnInputChange to true. Otherwise, this field will |
||||||
|
// be filled in but notifications won't be sent due to only inputs changing. |
||||||
|
public bool[] inputs; |
||||||
|
} |
||||||
|
|
||||||
|
public struct LoadFromConfigDelegateArgs |
||||||
|
{ |
||||||
|
// This indicates which fields changed since the last call. |
||||||
|
public bool ConfigurationChanged, InputChanged, TestDataChanged; |
||||||
|
|
||||||
|
// Data for each of two controllers: |
||||||
|
public LoadFromConfigDelegateArgsPerController[] controller; |
||||||
|
|
||||||
|
// For convenience, this is the index in controller of the first connected controller. |
||||||
|
// If no controllers are connected, this is 0. |
||||||
|
public int FirstController; |
||||||
|
|
||||||
|
// The control that changed the configuration (passed to FireConfigurationChanged). |
||||||
|
public object source; |
||||||
|
}; |
||||||
|
|
||||||
|
// This class tracks the device we're currently configuring, and runs a callback when |
||||||
|
// it changes. |
||||||
|
class CurrentSMXDevice |
||||||
|
{ |
||||||
|
public static CurrentSMXDevice singleton; |
||||||
|
|
||||||
|
// This is fired when FireConfigurationChanged is called, and when the current device |
||||||
|
// changes. |
||||||
|
public delegate void ConfigurationChangedDelegate(LoadFromConfigDelegateArgs args); |
||||||
|
public event ConfigurationChangedDelegate ConfigurationChanged; |
||||||
|
|
||||||
|
private bool[] WasConnected = new bool[2] { false, false }; |
||||||
|
private bool[][] LastInputs = new bool[2][]; |
||||||
|
private SMX.SMXSensorTestModeData[] LastTestData = new SMX.SMXSensorTestModeData[2]; |
||||||
|
private Dispatcher MainDispatcher; |
||||||
|
|
||||||
|
public CurrentSMXDevice() |
||||||
|
{ |
||||||
|
// Grab the main thread's dispatcher, so we can invoke into it. |
||||||
|
MainDispatcher = Dispatcher.CurrentDispatcher; |
||||||
|
|
||||||
|
// Set our update callback. This will be called when something happens: connection or disconnection, |
||||||
|
// inputs changed, configuration updated, test data updated, etc. It doesn't specify what's changed, |
||||||
|
// we simply check the whole state. |
||||||
|
SMX.SMX.Start(delegate(int PadNumber, SMX.SMX.SMXUpdateCallbackReason reason) { |
||||||
|
// Console.WriteLine("... " + reason); |
||||||
|
// This is called from a thread, with SMX's internal mutex locked. We must not call into SMX |
||||||
|
// or do anything with the UI from here. Just queue an update back into the UI thread. |
||||||
|
MainDispatcher.InvokeAsync(delegate() { |
||||||
|
switch(reason) |
||||||
|
{ |
||||||
|
case SMX.SMX.SMXUpdateCallbackReason.Updated: |
||||||
|
CheckForChanges(); |
||||||
|
break; |
||||||
|
case SMX.SMX.SMXUpdateCallbackReason.FactoryResetCommandComplete: |
||||||
|
Console.WriteLine("SMX_FactoryResetCommandComplete"); |
||||||
|
FireConfigurationChanged(null); |
||||||
|
break; |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public void Shutdown() |
||||||
|
{ |
||||||
|
SMX.SMX.SetUpdateCallback(null); |
||||||
|
SMX.SMX.Stop(); |
||||||
|
} |
||||||
|
|
||||||
|
private void CheckForChanges() |
||||||
|
{ |
||||||
|
LoadFromConfigDelegateArgs args = GetState(); |
||||||
|
|
||||||
|
// Mark which parts have changed. |
||||||
|
// |
||||||
|
// For configuration, we only check for connection state changes. Actual configuration |
||||||
|
// changes are fired by controls via FireConfigurationChanged. |
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
{ |
||||||
|
LoadFromConfigDelegateArgsPerController controller = args.controller[pad]; |
||||||
|
if(WasConnected[pad] != controller.info.connected) |
||||||
|
{ |
||||||
|
args.ConfigurationChanged = true; |
||||||
|
WasConnected[pad] = controller.info.connected; |
||||||
|
} |
||||||
|
|
||||||
|
if(LastInputs[pad] == null || !Enumerable.SequenceEqual(controller.inputs, LastInputs[pad])) |
||||||
|
{ |
||||||
|
args.InputChanged = true; |
||||||
|
LastInputs[pad] = controller.inputs; |
||||||
|
} |
||||||
|
|
||||||
|
if(!controller.test_data.Equals(LastTestData[pad])) |
||||||
|
{ |
||||||
|
args.TestDataChanged = true; |
||||||
|
LastTestData[pad] = controller.test_data; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Only fire the delegate if something has actually changed. |
||||||
|
if(args.ConfigurationChanged || args.InputChanged || args.TestDataChanged) |
||||||
|
ConfigurationChanged?.Invoke(args); |
||||||
|
} |
||||||
|
|
||||||
|
public void FireConfigurationChanged(object source) |
||||||
|
{ |
||||||
|
LoadFromConfigDelegateArgs args = GetState(); |
||||||
|
args.ConfigurationChanged = true; |
||||||
|
args.source = source; |
||||||
|
ConfigurationChanged?.Invoke(args); |
||||||
|
} |
||||||
|
|
||||||
|
public LoadFromConfigDelegateArgs GetState() |
||||||
|
{ |
||||||
|
LoadFromConfigDelegateArgs args = new LoadFromConfigDelegateArgs(); |
||||||
|
args.FirstController = -1; |
||||||
|
args.controller = new LoadFromConfigDelegateArgsPerController[2]; |
||||||
|
|
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
{ |
||||||
|
LoadFromConfigDelegateArgsPerController controller; |
||||||
|
controller.test_data = new SMX.SMXSensorTestModeData(); |
||||||
|
|
||||||
|
// Expand the inputs mask to an array. |
||||||
|
UInt16 Inputs = SMX.SMX.GetInputState(pad); |
||||||
|
controller.inputs = new bool[9]; |
||||||
|
for(int i = 0; i < 9; ++i) |
||||||
|
controller.inputs[i] = (Inputs & (1 << i)) != 0; |
||||||
|
SMX.SMX.GetInfo(pad, out controller.info); |
||||||
|
SMX.SMX.GetConfig(pad, out controller.config); |
||||||
|
SMX.SMX.GetTestData(pad, out controller.test_data); |
||||||
|
args.controller[pad] = controller; |
||||||
|
|
||||||
|
// If this is the first connected controller, set FirstController. |
||||||
|
if(controller.info.connected && args.FirstController == -1) |
||||||
|
args.FirstController = pad; |
||||||
|
} |
||||||
|
|
||||||
|
if(args.FirstController == -1) |
||||||
|
args.FirstController = 0; |
||||||
|
|
||||||
|
return args; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Call a delegate on configuration change. Configuration changes are notified by calling |
||||||
|
// FireConfigurationChanged. Listeners won't receive notifications for changes that they |
||||||
|
// fired themselves. |
||||||
|
public class OnConfigChange |
||||||
|
{ |
||||||
|
public delegate void LoadFromConfigDelegate(LoadFromConfigDelegateArgs args); |
||||||
|
private readonly Control Owner; |
||||||
|
private readonly LoadFromConfigDelegate Callback; |
||||||
|
private bool _RefreshOnInputChange = false; |
||||||
|
|
||||||
|
// If set to true, the callback will be invoked on input changes in addition to configuration |
||||||
|
// changes. This can cause the callback to be run at any time, such as while the user is |
||||||
|
// interacting with the control. |
||||||
|
public bool RefreshOnInputChange { |
||||||
|
get { return _RefreshOnInputChange; } |
||||||
|
set {_RefreshOnInputChange = value; } |
||||||
|
} |
||||||
|
|
||||||
|
private bool _RefreshOnTestDataChange = false; |
||||||
|
|
||||||
|
// Like RefreshOnInputChange, but enables callbacks when test data changes. |
||||||
|
public bool RefreshOnTestDataChange { |
||||||
|
get { return _RefreshOnTestDataChange; } |
||||||
|
set { _RefreshOnTestDataChange = value; } |
||||||
|
} |
||||||
|
|
||||||
|
// Owner is the Control that we're calling. This callback will be disable when the |
||||||
|
// control is unloaded, and we won't call it if it's the same control that fired |
||||||
|
// the change via FireConfigurationChanged. |
||||||
|
// |
||||||
|
// In addition, the callback is called when the control is Loaded, to load the initial |
||||||
|
// state. |
||||||
|
public OnConfigChange(Control owner, LoadFromConfigDelegate callback) |
||||||
|
{ |
||||||
|
Owner = owner; |
||||||
|
Callback = callback; |
||||||
|
|
||||||
|
Owner.Loaded += delegate(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
if(CurrentSMXDevice.singleton != null) |
||||||
|
CurrentSMXDevice.singleton.ConfigurationChanged += ConfigurationChanged; |
||||||
|
Refresh(); |
||||||
|
}; |
||||||
|
|
||||||
|
Owner.Unloaded += delegate(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
if(CurrentSMXDevice.singleton != null) |
||||||
|
CurrentSMXDevice.singleton.ConfigurationChanged -= ConfigurationChanged; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private void ConfigurationChanged(LoadFromConfigDelegateArgs args) |
||||||
|
{ |
||||||
|
if(args.ConfigurationChanged || |
||||||
|
(RefreshOnInputChange && args.InputChanged) || |
||||||
|
(RefreshOnTestDataChange && args.TestDataChanged)) |
||||||
|
{ |
||||||
|
Callback(args); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void Refresh() |
||||||
|
{ |
||||||
|
if(CurrentSMXDevice.singleton != null) |
||||||
|
Callback(CurrentSMXDevice.singleton.GetState()); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
public class OnInputChange |
||||||
|
{ |
||||||
|
public delegate void LoadFromConfigDelegate(LoadFromConfigDelegateArgs args); |
||||||
|
private readonly Control Owner; |
||||||
|
private readonly LoadFromConfigDelegate Callback; |
||||||
|
|
||||||
|
// Owner is the Control that we're calling. This callback will be disable when the |
||||||
|
// control is unloaded, and we won't call it if it's the same control that fired |
||||||
|
// the change via FireConfigurationChanged. |
||||||
|
// |
||||||
|
// In addition, the callback is called when the control is Loaded, to load the initial |
||||||
|
// state. |
||||||
|
public OnInputChange(Control owner, LoadFromConfigDelegate callback) |
||||||
|
{ |
||||||
|
Owner = owner; |
||||||
|
Callback = callback; |
||||||
|
|
||||||
|
// This is available when the application is running, but will be null in the XAML designer. |
||||||
|
if(CurrentSMXDevice.singleton == null) |
||||||
|
return; |
||||||
|
|
||||||
|
Owner.Loaded += delegate(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
CurrentSMXDevice.singleton.ConfigurationChanged += ConfigurationChanged; |
||||||
|
Refresh(); |
||||||
|
}; |
||||||
|
|
||||||
|
Owner.Unloaded += delegate(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
CurrentSMXDevice.singleton.ConfigurationChanged -= ConfigurationChanged; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private void ConfigurationChanged(LoadFromConfigDelegateArgs args) |
||||||
|
{ |
||||||
|
Callback(args); |
||||||
|
} |
||||||
|
|
||||||
|
private void Refresh() |
||||||
|
{ |
||||||
|
if(CurrentSMXDevice.singleton != null) |
||||||
|
Callback(CurrentSMXDevice.singleton.GetState()); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,289 @@ |
|||||||
|
using System; |
||||||
|
using System.Windows; |
||||||
|
using System.Windows.Controls; |
||||||
|
using System.Windows.Media; |
||||||
|
using System.Windows.Shapes; |
||||||
|
using System.Windows.Input; |
||||||
|
|
||||||
|
namespace smx_config |
||||||
|
{ |
||||||
|
public class DiagnosticsPanelButton: PanelSelectButton |
||||||
|
{ |
||||||
|
// True if this panel is being pressed. |
||||||
|
public static readonly DependencyProperty PressedProperty = DependencyProperty.Register("Pressed", |
||||||
|
typeof(bool), typeof(DiagnosticsPanelButton), new FrameworkPropertyMetadata(false)); |
||||||
|
|
||||||
|
public bool Pressed { |
||||||
|
get { return (bool) GetValue(PressedProperty); } |
||||||
|
set { SetValue(PressedProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
// True if a warning icon should be displayed for this panel. |
||||||
|
public static readonly DependencyProperty WarningProperty = DependencyProperty.Register("Warning", |
||||||
|
typeof(bool), typeof(DiagnosticsPanelButton), new FrameworkPropertyMetadata(false)); |
||||||
|
|
||||||
|
public bool Warning { |
||||||
|
get { return (bool) GetValue(WarningProperty); } |
||||||
|
set { SetValue(WarningProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
OnConfigChange onConfigChange; |
||||||
|
onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { |
||||||
|
int SelectedPad = Panel < 9? 0:1; |
||||||
|
int PanelIndex = Panel % 9; |
||||||
|
Pressed = args.controller[SelectedPad].inputs[PanelIndex]; |
||||||
|
|
||||||
|
Warning = !args.controller[SelectedPad].test_data.bHaveDataFromPanel[PanelIndex] || |
||||||
|
args.controller[SelectedPad].test_data.AnySensorsOnPanelNotResponding(PanelIndex); |
||||||
|
|
||||||
|
}); |
||||||
|
onConfigChange.RefreshOnInputChange = true; |
||||||
|
onConfigChange.RefreshOnTestDataChange = true; |
||||||
|
} |
||||||
|
|
||||||
|
protected override void OnClick() |
||||||
|
{ |
||||||
|
base.OnClick(); |
||||||
|
|
||||||
|
// Select this panel. |
||||||
|
Console.WriteLine(SelectedPanel + " -> " + Panel); |
||||||
|
SelectedPanel = Panel; |
||||||
|
|
||||||
|
CurrentSMXDevice.singleton.FireConfigurationChanged(this); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public class LevelBar: Control |
||||||
|
{ |
||||||
|
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", |
||||||
|
typeof(double), typeof(LevelBar), new FrameworkPropertyMetadata(0.5, ValueChangedCallback)); |
||||||
|
|
||||||
|
public double Value { |
||||||
|
get { return (double) GetValue(ValueProperty); } |
||||||
|
set { SetValue(ValueProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly DependencyProperty ErrorProperty = DependencyProperty.Register("Error", |
||||||
|
typeof(bool), typeof(LevelBar), new FrameworkPropertyMetadata(false, ValueChangedCallback)); |
||||||
|
|
||||||
|
public bool Error { |
||||||
|
get { return (bool) GetValue(ErrorProperty); } |
||||||
|
set { SetValue(ErrorProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
private Rectangle Fill, Back; |
||||||
|
|
||||||
|
private static void ValueChangedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args) |
||||||
|
{ |
||||||
|
LevelBar self = target as LevelBar; |
||||||
|
self.Refresh(); |
||||||
|
} |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
Fill = Template.FindName("Fill", this) as Rectangle; |
||||||
|
Back = Template.FindName("Back", this) as Rectangle; |
||||||
|
Refresh(); |
||||||
|
} |
||||||
|
|
||||||
|
private void Refresh() |
||||||
|
{ |
||||||
|
// If Error is true, fill the bar red. |
||||||
|
double FillHeight = Error? 1:Value; |
||||||
|
Fill.Height = Math.Round(Math.Max(FillHeight, 0) * (Back.Height - 2)); |
||||||
|
|
||||||
|
if(Error) |
||||||
|
{ |
||||||
|
Fill.Fill = new SolidColorBrush(Color.FromRgb(255,0,0)); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
// Scale from green (#FF0000) to yellow (#FFFF00) as we go from 0 to .4. |
||||||
|
double ColorValue = Value / 0.4; |
||||||
|
Byte Yellow = (Byte) (Math.Max(0, Math.Min(255, ColorValue * 255)) ); |
||||||
|
Fill.Fill = new SolidColorBrush(Color.FromRgb(255,Yellow,0)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public class DiagnosticsControl: Control |
||||||
|
{ |
||||||
|
// Which panel is currently selected: |
||||||
|
public static readonly DependencyProperty SelectedPanelProperty = DependencyProperty.Register("SelectedPanel", |
||||||
|
typeof(int), typeof(DiagnosticsControl), new FrameworkPropertyMetadata(0)); |
||||||
|
|
||||||
|
public int SelectedPanel { |
||||||
|
get { return (int) this.GetValue(SelectedPanelProperty); } |
||||||
|
set { this.SetValue(SelectedPanelProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
private LevelBar[] LevelBars; |
||||||
|
private Label[] LevelBarText; |
||||||
|
private ComboBox DiagnosticMode; |
||||||
|
private FrameImage CurrentDIP; |
||||||
|
private FrameImage ExpectedDIP; |
||||||
|
private FrameworkElement NoResponseFromPanel; |
||||||
|
private FrameworkElement NoResponseFromSensors; |
||||||
|
private FrameworkElement P1Diagnostics, P2Diagnostics; |
||||||
|
|
||||||
|
public delegate void ShowAllLightsEvent(bool on); |
||||||
|
public event ShowAllLightsEvent SetShowAllLights; |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
LevelBars = new LevelBar[4]; |
||||||
|
LevelBars[0] = Template.FindName("SensorBar1", this) as LevelBar; |
||||||
|
LevelBars[1] = Template.FindName("SensorBar2", this) as LevelBar; |
||||||
|
LevelBars[2] = Template.FindName("SensorBar3", this) as LevelBar; |
||||||
|
LevelBars[3] = Template.FindName("SensorBar4", this) as LevelBar; |
||||||
|
|
||||||
|
LevelBarText = new Label[4]; |
||||||
|
LevelBarText[0] = Template.FindName("SensorBarLevel1", this) as Label; |
||||||
|
LevelBarText[1] = Template.FindName("SensorBarLevel2", this) as Label; |
||||||
|
LevelBarText[2] = Template.FindName("SensorBarLevel3", this) as Label; |
||||||
|
LevelBarText[3] = Template.FindName("SensorBarLevel4", this) as Label; |
||||||
|
|
||||||
|
DiagnosticMode = Template.FindName("DiagnosticMode", this) as ComboBox; |
||||||
|
CurrentDIP = Template.FindName("CurrentDIP", this) as FrameImage; |
||||||
|
ExpectedDIP = Template.FindName("ExpectedDIP", this) as FrameImage; |
||||||
|
NoResponseFromPanel = Template.FindName("NoResponseFromPanel", this) as FrameworkElement; |
||||||
|
NoResponseFromSensors = Template.FindName("NoResponseFromSensors", this) as FrameworkElement; |
||||||
|
P1Diagnostics = Template.FindName("P1Diagnostics", this) as FrameworkElement; |
||||||
|
P2Diagnostics = Template.FindName("P2Diagnostics", this) as FrameworkElement; |
||||||
|
|
||||||
|
// Only show the mode dropdown in debug mode. In regular use, just show calibrated values. |
||||||
|
DiagnosticMode.Visibility = Helpers.GetDebug()? Visibility.Visible:Visibility.Collapsed; |
||||||
|
|
||||||
|
Button Recalibrate = Template.FindName("Recalibrate", this) as Button; |
||||||
|
Recalibrate.Click += delegate(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
SMX.SMX.ForceRecalibration(pad); |
||||||
|
}; |
||||||
|
|
||||||
|
Button LightAll = Template.FindName("LightAll", this) as Button; |
||||||
|
LightAll.PreviewMouseDown += delegate(object sender, MouseButtonEventArgs e) |
||||||
|
{ |
||||||
|
SetShowAllLights?.Invoke(true); |
||||||
|
}; |
||||||
|
LightAll.PreviewMouseUp += delegate(object sender, MouseButtonEventArgs e) |
||||||
|
{ |
||||||
|
SetShowAllLights?.Invoke(false); |
||||||
|
}; |
||||||
|
|
||||||
|
// Update the test mode when the dropdown is changed. |
||||||
|
DiagnosticMode.AddHandler(ComboBox.SelectionChangedEvent, new RoutedEventHandler(delegate(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
SMX.SMX.SetTestMode(pad, GetTestMode()); |
||||||
|
})); |
||||||
|
|
||||||
|
OnConfigChange onConfigChange; |
||||||
|
onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { |
||||||
|
Refresh(args); |
||||||
|
}); |
||||||
|
onConfigChange.RefreshOnTestDataChange = true; |
||||||
|
|
||||||
|
Loaded += delegate(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
SMX.SMX.SetTestMode(pad, GetTestMode()); |
||||||
|
}; |
||||||
|
|
||||||
|
Unloaded += delegate(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
SMX.SMX.SetTestMode(pad, SMX.SMX.SensorTestMode.Off); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private SMX.SMX.SensorTestMode GetTestMode() |
||||||
|
{ |
||||||
|
switch(DiagnosticMode.SelectedIndex) |
||||||
|
{ |
||||||
|
case 0: return SMX.SMX.SensorTestMode.CalibratedValues; |
||||||
|
case 1: return SMX.SMX.SensorTestMode.UncalibratedValues; |
||||||
|
case 2: return SMX.SMX.SensorTestMode.Noise; |
||||||
|
case 3: |
||||||
|
default: return SMX.SMX.SensorTestMode.Tare; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void Refresh(LoadFromConfigDelegateArgs args) |
||||||
|
{ |
||||||
|
P1Diagnostics.Visibility = args.controller[0].info.connected? Visibility.Visible:Visibility.Collapsed; |
||||||
|
P2Diagnostics.Visibility = args.controller[1].info.connected? Visibility.Visible:Visibility.Collapsed; |
||||||
|
|
||||||
|
// Update the displayed DIP switch icons. |
||||||
|
int SelectedPad = SelectedPanel < 9? 0:1; |
||||||
|
int PanelIndex = SelectedPanel % 9; |
||||||
|
int dip = args.controller[SelectedPad].test_data.iDIPSwitchPerPanel[PanelIndex]; |
||||||
|
CurrentDIP.Frame = dip; |
||||||
|
ExpectedDIP.Frame = PanelIndex; |
||||||
|
|
||||||
|
// Show or hide the sensor error text. |
||||||
|
bool AnySensorsNotResponding = false; |
||||||
|
if(args.controller[SelectedPad].test_data.bHaveDataFromPanel[PanelIndex]) |
||||||
|
AnySensorsNotResponding = args.controller[SelectedPad].test_data.AnySensorsOnPanelNotResponding(PanelIndex); |
||||||
|
NoResponseFromSensors.Visibility = AnySensorsNotResponding? Visibility.Visible:Visibility.Collapsed; |
||||||
|
|
||||||
|
// Update the level bar from the test mode data for the selected panel. |
||||||
|
for(int sensor = 0; sensor < 4; ++sensor) |
||||||
|
{ |
||||||
|
Int16 value = args.controller[SelectedPad].test_data.sensorLevel[PanelIndex*4+sensor]; |
||||||
|
|
||||||
|
if(GetTestMode() == SMX.SMX.SensorTestMode.Noise) |
||||||
|
{ |
||||||
|
// In noise mode, we receive standard deviation values squared. Display the square |
||||||
|
// root, since the panels don't do this for us. This makes the numbers different |
||||||
|
// than the configured value (square it to convert back), but without this we display |
||||||
|
// a bunch of 4 and 5-digit numbers that are too hard to read. |
||||||
|
value = (Int16) Math.Sqrt(value); |
||||||
|
} |
||||||
|
|
||||||
|
LevelBarText[sensor].Visibility = Visibility.Visible; |
||||||
|
if(!args.controller[SelectedPad].test_data.bHaveDataFromPanel[PanelIndex]) |
||||||
|
{ |
||||||
|
LevelBars[sensor].Value = 0; |
||||||
|
LevelBarText[sensor].Visibility = Visibility.Hidden; |
||||||
|
LevelBarText[sensor].Content = "-"; |
||||||
|
LevelBars[sensor].Error = false; |
||||||
|
} |
||||||
|
else if(args.controller[SelectedPad].test_data.bBadSensorInput[PanelIndex*4+sensor]) |
||||||
|
{ |
||||||
|
LevelBars[sensor].Value = 0; |
||||||
|
LevelBarText[sensor].Content = "!"; |
||||||
|
LevelBars[sensor].Error = true; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
// Very slightly negative values happen due to noise. They don't indicate a |
||||||
|
// problem, but they're confusing in the UI, so clamp them away. |
||||||
|
if(value < 0 && value >= -10) |
||||||
|
value = 0; |
||||||
|
|
||||||
|
LevelBars[sensor].Value = value / 500.0; |
||||||
|
LevelBarText[sensor].Content = value; |
||||||
|
LevelBars[sensor].Error = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(!args.controller[SelectedPad].test_data.bHaveDataFromPanel[PanelIndex]) |
||||||
|
{ |
||||||
|
NoResponseFromPanel.Visibility = Visibility.Visible; |
||||||
|
NoResponseFromSensors.Visibility = Visibility.Collapsed; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
NoResponseFromPanel.Visibility = Visibility.Collapsed; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,203 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.ComponentModel; |
||||||
|
using System.Linq; |
||||||
|
using System.Text; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using System.Windows; |
||||||
|
using System.Windows.Controls; |
||||||
|
using System.Windows.Controls.Primitives; |
||||||
|
using System.Windows.Input; |
||||||
|
|
||||||
|
namespace smx_config |
||||||
|
{ |
||||||
|
// A slider with two handles, and a handle connecting them. Dragging the handle drags both |
||||||
|
// of the sliders. |
||||||
|
class DoubleSlider: Control |
||||||
|
{ |
||||||
|
public delegate void ValueChangedDelegate(DoubleSlider slider); |
||||||
|
public event ValueChangedDelegate ValueChanged; |
||||||
|
|
||||||
|
// The minimum value for either knob. |
||||||
|
public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", |
||||||
|
typeof(double), typeof(DoubleSlider), new FrameworkPropertyMetadata(0.0)); |
||||||
|
|
||||||
|
public double Minimum { |
||||||
|
get { return (double) GetValue(MinimumProperty); } |
||||||
|
set { SetValue(MinimumProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
// The maximum value for either knob. |
||||||
|
public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", |
||||||
|
typeof(double), typeof(DoubleSlider), new FrameworkPropertyMetadata(20.0)); |
||||||
|
|
||||||
|
public double Maximum { |
||||||
|
get { return (double) GetValue(MaximumProperty); } |
||||||
|
set { SetValue(MaximumProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
// The minimum distance between the two values. |
||||||
|
public static readonly DependencyProperty MinimumDistanceProperty = DependencyProperty.Register("MinimumDistance", |
||||||
|
typeof(double), typeof(DoubleSlider), new FrameworkPropertyMetadata(0.0)); |
||||||
|
|
||||||
|
public double MinimumDistance { |
||||||
|
get { return (double) GetValue(MinimumDistanceProperty); } |
||||||
|
set { SetValue(MinimumDistanceProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
// Clamp value between minimum and maximum. |
||||||
|
private double CoerceValueToLimits(double value) |
||||||
|
{ |
||||||
|
return Math.Min(Math.Max(value, Minimum), Maximum); |
||||||
|
} |
||||||
|
|
||||||
|
// Note that we only clamp LowerValue and UpperValue to the min/max values. We don't |
||||||
|
// clamp them to each other or to MinimumDistance here, since that complicates setting |
||||||
|
// properties a lot. We only clamp to those when the user manipulates the control, not |
||||||
|
// when we set values directly. |
||||||
|
private static object LowerValueCoerceValueCallback(DependencyObject target, object valueObject) |
||||||
|
{ |
||||||
|
DoubleSlider slider = target as DoubleSlider; |
||||||
|
double value = (double)valueObject; |
||||||
|
value = slider.CoerceValueToLimits(value); |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
private static object UpperValueCoerceValueCallback(DependencyObject target, object valueObject) |
||||||
|
{ |
||||||
|
DoubleSlider slider = target as DoubleSlider; |
||||||
|
double value = (double)valueObject; |
||||||
|
value = slider.CoerceValueToLimits(value); |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
private static void SliderValueChangedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args) |
||||||
|
{ |
||||||
|
DoubleSlider slider = target as DoubleSlider; |
||||||
|
if(slider.ValueChanged != null) |
||||||
|
slider.ValueChanged.Invoke(slider); |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly DependencyProperty LowerValueProperty = DependencyProperty.Register("LowerValue", |
||||||
|
typeof(double), typeof(DoubleSlider), |
||||||
|
new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsArrange, SliderValueChangedCallback, LowerValueCoerceValueCallback)); |
||||||
|
|
||||||
|
public double LowerValue { |
||||||
|
get { return (double) GetValue(LowerValueProperty); } |
||||||
|
set { SetValue(LowerValueProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly DependencyProperty UpperValueProperty = DependencyProperty.Register("UpperValue", |
||||||
|
typeof(double), typeof(DoubleSlider), |
||||||
|
new FrameworkPropertyMetadata(15.0, FrameworkPropertyMetadataOptions.AffectsArrange, SliderValueChangedCallback, UpperValueCoerceValueCallback)); |
||||||
|
|
||||||
|
public double UpperValue { |
||||||
|
get { return (double) GetValue(UpperValueProperty); } |
||||||
|
set { SetValue(UpperValueProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
private Thumb Middle; |
||||||
|
|
||||||
|
Thumb UpperThumb; |
||||||
|
Thumb LowerThumb; |
||||||
|
|
||||||
|
private RepeatButton DecreaseButton; |
||||||
|
private RepeatButton IncreaseButton; |
||||||
|
|
||||||
|
protected override Size ArrangeOverride(Size arrangeSize) |
||||||
|
{ |
||||||
|
arrangeSize = base.ArrangeOverride(arrangeSize); |
||||||
|
|
||||||
|
// Figure out the X position of the upper and lower thumbs. Note that we have to provide |
||||||
|
// our width to GetValueToSize, since ActualWidth isn't available yet. |
||||||
|
double valueToSize = GetValueToSize(arrangeSize.Width); |
||||||
|
double UpperPointX = (UpperValue-Minimum) * valueToSize; |
||||||
|
double LowerPointX = (LowerValue-Minimum) * valueToSize; |
||||||
|
|
||||||
|
// Move the upper and lower handles out by this much, and extend this middle. This |
||||||
|
// makes the middle handle bigger. |
||||||
|
double OffsetOutwards = 5; |
||||||
|
Middle.Arrange(new Rect(LowerPointX-OffsetOutwards-1, 0, |
||||||
|
Math.Max(1, UpperPointX-LowerPointX+OffsetOutwards*2+2), arrangeSize.Height)); |
||||||
|
|
||||||
|
// Right-align the lower thumb and left-align the upper thumb. |
||||||
|
LowerThumb.Arrange(new Rect(LowerPointX-LowerThumb.Width-OffsetOutwards, 0, LowerThumb.Width, arrangeSize.Height)); |
||||||
|
UpperThumb.Arrange(new Rect(UpperPointX +OffsetOutwards, 0, UpperThumb.Width, arrangeSize.Height)); |
||||||
|
|
||||||
|
DecreaseButton.Arrange(new Rect(0, 0, Math.Max(1, LowerPointX), Math.Max(1, arrangeSize.Height))); |
||||||
|
IncreaseButton.Arrange(new Rect(UpperPointX, 0, Math.Max(1, arrangeSize.Width - UpperPointX), arrangeSize.Height)); |
||||||
|
return arrangeSize; |
||||||
|
} |
||||||
|
|
||||||
|
private void MoveValue(double delta) |
||||||
|
{ |
||||||
|
if(delta > 0) |
||||||
|
{ |
||||||
|
// If this increase will be clamped when changing the upper value, reduce it |
||||||
|
// so it clamps the lower value too. This way, the distance between the upper |
||||||
|
// and lower value stays the same. |
||||||
|
delta = Math.Min(delta, Maximum - UpperValue); |
||||||
|
UpperValue += delta; |
||||||
|
LowerValue += delta; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
delta *= -1; |
||||||
|
delta = Math.Min(delta, LowerValue - Minimum); |
||||||
|
LowerValue -= delta; |
||||||
|
UpperValue -= delta; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private double GetValueToSize() |
||||||
|
{ |
||||||
|
return GetValueToSize(this.ActualWidth); |
||||||
|
} |
||||||
|
|
||||||
|
private double GetValueToSize(double width) |
||||||
|
{ |
||||||
|
double Range = Maximum - Minimum; |
||||||
|
return Math.Max(0.0, (width - UpperThumb.RenderSize.Width) / Range); |
||||||
|
} |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
LowerThumb = GetTemplateChild("PART_LowerThumb") as Thumb; |
||||||
|
UpperThumb = GetTemplateChild("PART_UpperThumb") as Thumb; |
||||||
|
Middle = GetTemplateChild("PART_Middle") as Thumb; |
||||||
|
DecreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton; |
||||||
|
IncreaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton; |
||||||
|
DecreaseButton.Click += delegate(object sender, RoutedEventArgs e) { MoveValue(-1); }; |
||||||
|
IncreaseButton.Click += delegate(object sender, RoutedEventArgs e) { MoveValue(+1); }; |
||||||
|
|
||||||
|
LowerThumb.DragDelta += delegate(object sender, DragDeltaEventArgs e) |
||||||
|
{ |
||||||
|
double sizeToValue = 1 / GetValueToSize(); |
||||||
|
|
||||||
|
double NewValue = LowerValue + e.HorizontalChange * sizeToValue; |
||||||
|
NewValue = Math.Min(NewValue, UpperValue - MinimumDistance); |
||||||
|
LowerValue = NewValue; |
||||||
|
}; |
||||||
|
|
||||||
|
UpperThumb.DragDelta += delegate(object sender, DragDeltaEventArgs e) |
||||||
|
{ |
||||||
|
double sizeToValue = 1 / GetValueToSize(); |
||||||
|
double NewValue = UpperValue + e.HorizontalChange * sizeToValue; |
||||||
|
NewValue = Math.Max(NewValue, LowerValue + MinimumDistance); |
||||||
|
UpperValue = NewValue; |
||||||
|
}; |
||||||
|
|
||||||
|
Middle.DragDelta += delegate(object sender, DragDeltaEventArgs e) |
||||||
|
{ |
||||||
|
// Convert the pixel delta to a value change. |
||||||
|
double sizeToValue = 1 / GetValueToSize(); |
||||||
|
Console.WriteLine("drag: " + e.HorizontalChange + ", " + sizeToValue + ", " + e.HorizontalChange * sizeToValue); |
||||||
|
MoveValue(e.HorizontalChange * sizeToValue); |
||||||
|
}; |
||||||
|
|
||||||
|
InvalidateArrange(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,204 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Windows.Media; |
||||||
|
using System.Windows.Threading; |
||||||
|
|
||||||
|
namespace smx_config |
||||||
|
{ |
||||||
|
static class Helpers |
||||||
|
{ |
||||||
|
// Return true if we're in debug mode. |
||||||
|
public static bool GetDebug() |
||||||
|
{ |
||||||
|
foreach(string arg in Environment.GetCommandLineArgs()) |
||||||
|
{ |
||||||
|
if(arg == "-d") |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// Work around Enumerable.SequenceEqual not checking if the arrays are null. |
||||||
|
public static bool SequenceEqual<TSource>(this IEnumerable<TSource> first, IEnumerable<TSource> second) |
||||||
|
{ |
||||||
|
if(first == second) |
||||||
|
return true; |
||||||
|
if(first == null || second == null) |
||||||
|
return false; |
||||||
|
return Enumerable.SequenceEqual(first, second); |
||||||
|
} |
||||||
|
|
||||||
|
public static Color ColorFromFloatRGB(double r, double g, double b) |
||||||
|
{ |
||||||
|
byte R = (byte) Math.Max(0, Math.Min(255, r * 255)); |
||||||
|
byte G = (byte) Math.Max(0, Math.Min(255, g * 255)); |
||||||
|
byte B = (byte) Math.Max(0, Math.Min(255, b * 255)); |
||||||
|
return Color.FromRgb(R, G, B); |
||||||
|
} |
||||||
|
|
||||||
|
public static Color FromHSV(double H, double S, double V) |
||||||
|
{ |
||||||
|
H = H % 360; |
||||||
|
S = Math.Max(0, Math.Min(1, S)); |
||||||
|
V = Math.Max(0, Math.Min(1, V)); |
||||||
|
if(H < 0) |
||||||
|
H += 360; |
||||||
|
H /= 60; |
||||||
|
|
||||||
|
if( S < 0.0001f ) |
||||||
|
return ColorFromFloatRGB(V, V, V); |
||||||
|
|
||||||
|
double C = V * S; |
||||||
|
double X = C * (1 - Math.Abs((H % 2) - 1)); |
||||||
|
|
||||||
|
Color ret; |
||||||
|
switch( (int) Math.Round(Math.Floor(H)) ) |
||||||
|
{ |
||||||
|
case 0: ret = ColorFromFloatRGB(C, X, 0); break; |
||||||
|
case 1: ret = ColorFromFloatRGB(X, C, 0); break; |
||||||
|
case 2: ret = ColorFromFloatRGB(0, C, X); break; |
||||||
|
case 3: ret = ColorFromFloatRGB(0, X, C); break; |
||||||
|
case 4: ret = ColorFromFloatRGB(X, 0, C); break; |
||||||
|
default: ret = ColorFromFloatRGB(C, 0, X); break; |
||||||
|
} |
||||||
|
|
||||||
|
ret -= ColorFromFloatRGB(C-V, C-V, C-V); |
||||||
|
return ret; |
||||||
|
} |
||||||
|
|
||||||
|
public static void ToHSV(Color c, out double h, out double s, out double v) |
||||||
|
{ |
||||||
|
h = s = v = 0; |
||||||
|
if( c.R == 0 && c.G == 0 && c.B == 0 ) |
||||||
|
return; |
||||||
|
|
||||||
|
double r = c.R / 255.0; |
||||||
|
double g = c.G / 255.0; |
||||||
|
double b = c.B / 255.0; |
||||||
|
|
||||||
|
double m = Math.Min(Math.Min(r, g), b); |
||||||
|
double M = Math.Max(Math.Max(r, g), b); |
||||||
|
double C = M - m; |
||||||
|
if( Math.Abs(r-g) < 0.0001f && Math.Abs(g-b) < 0.0001f ) // grey |
||||||
|
h = 0; |
||||||
|
else if( Math.Abs(r-M) < 0.0001f ) // M == R |
||||||
|
h = ((g - b)/C) % 6; |
||||||
|
else if( Math.Abs(g-M) < 0.0001f ) // M == G |
||||||
|
h = (b - r)/C + 2; |
||||||
|
else // M == B |
||||||
|
h = (r - g)/C + 4; |
||||||
|
|
||||||
|
h *= 60; |
||||||
|
if( h < 0 ) |
||||||
|
h += 360; |
||||||
|
|
||||||
|
s = C / M; |
||||||
|
v = M; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This class just makes it easier to assemble binary command packets. |
||||||
|
public class CommandBuffer |
||||||
|
{ |
||||||
|
public void Write(string s) |
||||||
|
{ |
||||||
|
char[] buf = s.ToCharArray(); |
||||||
|
byte[] data = new byte[buf.Length]; |
||||||
|
for(int i = 0; i < buf.Length; ++i) |
||||||
|
data[i] = (byte) buf[i]; |
||||||
|
Write(data); |
||||||
|
} |
||||||
|
public void Write(byte[] s) { parts.AddLast(s); } |
||||||
|
public void Write(byte b) { Write(new byte[] { b }); } |
||||||
|
public void Write(char b) { Write((byte) b); } |
||||||
|
|
||||||
|
public byte[] Get() |
||||||
|
{ |
||||||
|
int length = 0; |
||||||
|
foreach(byte[] part in parts) |
||||||
|
length += part.Length; |
||||||
|
|
||||||
|
byte[] result = new byte[length]; |
||||||
|
int next = 0; |
||||||
|
foreach(byte[] part in parts) |
||||||
|
{ |
||||||
|
Buffer.BlockCopy(part, 0, result, next, part.Length); |
||||||
|
next += part.Length; |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private LinkedList<byte[]> parts = new LinkedList<byte[]>(); |
||||||
|
}; |
||||||
|
|
||||||
|
// When enabled, periodically set all lights to the current auto-lighting color. This |
||||||
|
// is enabled while manipulating the step color slider. |
||||||
|
class ShowAutoLightsColor |
||||||
|
{ |
||||||
|
private DispatcherTimer LightsTimer; |
||||||
|
|
||||||
|
public ShowAutoLightsColor() |
||||||
|
{ |
||||||
|
LightsTimer = new DispatcherTimer(); |
||||||
|
|
||||||
|
// Run at 30fps. |
||||||
|
LightsTimer.Interval = new TimeSpan(0,0,0,0, 1000 / 33); |
||||||
|
|
||||||
|
LightsTimer.Tick += delegate(object sender, EventArgs e) |
||||||
|
{ |
||||||
|
if(!LightsTimer.IsEnabled) |
||||||
|
return; |
||||||
|
|
||||||
|
AutoLightsColorRefreshColor(); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
public void Start() |
||||||
|
{ |
||||||
|
// To show the current color, send a lights command periodically. If we stop sending |
||||||
|
// this for a while the controller will return to auto-lights, which we won't want to |
||||||
|
// happen until AutoLightsColorEnd is called. |
||||||
|
if(LightsTimer.IsEnabled) |
||||||
|
return; |
||||||
|
|
||||||
|
// Don't wait for an interval to send the first update. |
||||||
|
//AutoLightsColorRefreshColor(); |
||||||
|
|
||||||
|
LightsTimer.Start(); |
||||||
|
} |
||||||
|
|
||||||
|
public void Stop() |
||||||
|
{ |
||||||
|
LightsTimer.Stop(); |
||||||
|
|
||||||
|
// Reenable auto-lights immediately, without waiting for lights to time out. |
||||||
|
SMX.SMX.ReenableAutoLights(); |
||||||
|
} |
||||||
|
|
||||||
|
private void AutoLightsColorRefreshColor() |
||||||
|
{ |
||||||
|
byte[] lights = new byte[864]; |
||||||
|
CommandBuffer cmd = new CommandBuffer(); |
||||||
|
|
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
{ |
||||||
|
SMX.SMXConfig config; |
||||||
|
if(!SMX.SMX.GetConfig(pad, out config)) |
||||||
|
continue; |
||||||
|
|
||||||
|
byte[] color = config.stepColor; |
||||||
|
for( int iPanel = 0; iPanel < 9; ++iPanel ) |
||||||
|
{ |
||||||
|
for( int i = 0; i < 16; ++i ) |
||||||
|
{ |
||||||
|
cmd.Write( color[iPanel*3+0] ); |
||||||
|
cmd.Write( color[iPanel*3+1] ); |
||||||
|
cmd.Write( color[iPanel*3+2] ); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
SMX.SMX.SetLights(cmd.Get()); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,798 @@ |
|||||||
|
<Window x:Class="smx_config.MainWindow" |
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||||
|
xmlns:clr="clr-namespace:System;assembly=mscorlib" |
||||||
|
xmlns:controls="clr-namespace:smx_config" |
||||||
|
mc:Ignorable="d" |
||||||
|
x:Name="root" |
||||||
|
Title="StepManiaX Platform Settings" |
||||||
|
Icon="Resources/window icon.png" |
||||||
|
Height="700" Width="525" ResizeMode="CanMinimize"> |
||||||
|
<Window.Resources> |
||||||
|
<clr:String x:Key="HighPresetDescription" xml:space="preserve">Lighter steps will activate the arrows. |
||||||
|
Use if small children are having difficulty pressing the arrows.</clr:String> |
||||||
|
<clr:String x:Key="NormalPresetDescription" xml:space="preserve">This is the recommended setting.</clr:String> |
||||||
|
<clr:String x:Key="LowPresetDescription" xml:space="preserve">More force is required to activate the arrows. |
||||||
|
Use if the platform is too sensitive.</clr:String> |
||||||
|
|
||||||
|
<FontFamily x:Key="WarningMarkFont">Segoe UI Black</FontFamily> |
||||||
|
|
||||||
|
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" /> |
||||||
|
|
||||||
|
<SolidColorBrush x:Key="DoubleSlider.Static.Foreground" Color="#FFE5E5E5"/> |
||||||
|
<SolidColorBrush x:Key="DoubleSlider.Static.Background" Color="#FFF0F0F0"/> |
||||||
|
<SolidColorBrush x:Key="DoubleSlider.Static.Border" Color="#FFACACAC"/> |
||||||
|
<SolidColorBrush x:Key="DoubleSlider.MouseOver.Background" Color="#FFDCECFC"/> |
||||||
|
<SolidColorBrush x:Key="DoubleSlider.MouseOver.Border" Color="#FF7Eb4EA"/> |
||||||
|
<SolidColorBrush x:Key="DoubleSlider.Pressed.Background" Color="#FFDAECFC"/> |
||||||
|
<SolidColorBrush x:Key="DoubleSlider.Pressed.Border" Color="#FF569DE5"/> |
||||||
|
|
||||||
|
<Style x:Key="DoubleSliderThumb" TargetType="{x:Type Thumb}"> |
||||||
|
<Setter Property="Height" Value="18"/> |
||||||
|
<Setter Property="Width" Value="11"/> |
||||||
|
<Setter Property="Focusable" Value="False"/> |
||||||
|
<Setter Property="OverridesDefaultStyle" Value="True"/> |
||||||
|
|
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type Thumb}"> |
||||||
|
<Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center" |
||||||
|
Height="18" Width="11"> |
||||||
|
<Path x:Name="grip" Data="M 0,0 C0,0 11,0 11,0 11,0 11,18 11,18 11,18 0,18 0,18 0,18 0,0 0,0 z" Fill="{StaticResource DoubleSlider.Static.Background}" Stretch="Fill" SnapsToDevicePixels="True" Stroke="{StaticResource DoubleSlider.Static.Border}" StrokeThickness="1" UseLayoutRounding="True" VerticalAlignment="Center"/> |
||||||
|
</Grid> |
||||||
|
<ControlTemplate.Triggers> |
||||||
|
<Trigger Property="IsMouseOver" Value="true"> |
||||||
|
<Setter Property="Fill" TargetName="grip" Value="{StaticResource DoubleSlider.MouseOver.Background}"/> |
||||||
|
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource DoubleSlider.MouseOver.Border}"/> |
||||||
|
</Trigger> |
||||||
|
<Trigger Property="IsDragging" Value="true"> |
||||||
|
<Setter Property="Fill" TargetName="grip" Value="{StaticResource DoubleSlider.Pressed.Background}"/> |
||||||
|
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource DoubleSlider.Pressed.Border}"/> |
||||||
|
</Trigger> |
||||||
|
</ControlTemplate.Triggers> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<ControlTemplate x:Key="DoubleSliderMiddleThumb" TargetType="{x:Type Thumb}"> |
||||||
|
<Grid x:Name="grip" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"> |
||||||
|
<Border x:Name="Border" Background="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/> |
||||||
|
<Border x:Name="Fill" Background="{TemplateBinding Background}" Margin="1"/> |
||||||
|
</Grid> |
||||||
|
<ControlTemplate.Triggers> |
||||||
|
<Trigger Property="IsMouseOver" Value="true"> |
||||||
|
<Setter Property="Background" TargetName="Fill" Value="{StaticResource DoubleSlider.MouseOver.Background}"/> |
||||||
|
<Setter Property="Background" TargetName="Border" Value="{StaticResource DoubleSlider.MouseOver.Border}"/> |
||||||
|
</Trigger> |
||||||
|
</ControlTemplate.Triggers> |
||||||
|
</ControlTemplate> |
||||||
|
|
||||||
|
<Style x:Key="DoubleSliderRepeatButton" TargetType="{x:Type RepeatButton}"> |
||||||
|
<Setter Property="OverridesDefaultStyle" Value="true"/> |
||||||
|
<Setter Property="Background" Value="Transparent"/> |
||||||
|
<Setter Property="Focusable" Value="false"/> |
||||||
|
<Setter Property="IsTabStop" Value="false"/> |
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type RepeatButton}"> |
||||||
|
<Rectangle Fill="#00FFFFFF" Height="{TemplateBinding Height}" Width="{TemplateBinding Width}"/> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style x:Key="DoubleSlider" TargetType="{x:Type controls:DoubleSlider}"> |
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:DoubleSlider}"> |
||||||
|
<Grid> |
||||||
|
<!-- The underlying bar: --> |
||||||
|
<Border BorderBrush="#FFD6D6D6" |
||||||
|
BorderThickness="1" |
||||||
|
Background="#FFE7EAEA" |
||||||
|
Height="4.0" Margin="5,0" VerticalAlignment="center" |
||||||
|
/> |
||||||
|
|
||||||
|
<!-- These hidden buttons handle moving the slider when clicking outside of a handle. --> |
||||||
|
<RepeatButton x:Name="PART_DecreaseButton" Style="{StaticResource DoubleSliderRepeatButton}"/> |
||||||
|
<RepeatButton x:Name="PART_IncreaseButton" Style="{StaticResource DoubleSliderRepeatButton}"/> |
||||||
|
|
||||||
|
<Thumb x:Name="PART_UpperThumb" Style="{StaticResource DoubleSliderThumb}" /> |
||||||
|
<Thumb x:Name="PART_LowerThumb" Style="{StaticResource DoubleSliderThumb}" /> |
||||||
|
|
||||||
|
<!-- The connecting bar: --> |
||||||
|
<Thumb x:Name="PART_Middle" |
||||||
|
Template="{StaticResource DoubleSliderMiddleThumb}" |
||||||
|
Width="Auto" |
||||||
|
Height="10" |
||||||
|
Foreground="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" |
||||||
|
Stylus.IsPressAndHoldEnabled="false" |
||||||
|
|
||||||
|
HorizontalAlignment="Stretch" |
||||||
|
VerticalAlignment="Center" |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<ControlTemplate.Triggers> |
||||||
|
<Trigger Property="IsEnabled" Value="false"> |
||||||
|
<Setter Property="Visibility" TargetName="PART_UpperThumb" Value="Hidden"/> |
||||||
|
<Setter Property="Visibility" TargetName="PART_LowerThumb" Value="Hidden"/> |
||||||
|
<Setter Property="Visibility" TargetName="PART_Middle" Value="Hidden"/> |
||||||
|
</Trigger> |
||||||
|
</ControlTemplate.Triggers> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style TargetType="{x:Type controls:ThresholdSlider}"> |
||||||
|
<Setter Property="Focusable" Value="false"/> |
||||||
|
<Setter Property="AdvancedModeEnabled" Value="{Binding ElementName=AdvancedModeEnabledCheckbox, Path=AdvancedModeEnabled, Mode=OneWay}"/> |
||||||
|
|
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:ThresholdSlider}"> |
||||||
|
<StackPanel Margin="0,0,0,0" Orientation="Horizontal" HorizontalAlignment="Center"> |
||||||
|
<Image |
||||||
|
Margin="0,0,0,0" |
||||||
|
Height="28" Width="28" |
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Top" |
||||||
|
Source="{TemplateBinding Icon}" |
||||||
|
/> |
||||||
|
<Label Margin="5,0,0,0" x:Name="LowerValue" Content="0" Width="30" HorizontalContentAlignment="Right" |
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Top" |
||||||
|
/> |
||||||
|
<controls:DoubleSlider x:Name="Slider" |
||||||
|
Margin="32,5,0,0" |
||||||
|
Minimum="20" Maximum="200" MinimumDistance="5" |
||||||
|
LowerValue="20" UpperValue="35" |
||||||
|
VerticalAlignment="Top" |
||||||
|
Width="240" |
||||||
|
Focusable="False" |
||||||
|
Style="{DynamicResource DoubleSlider}" |
||||||
|
/> |
||||||
|
<Label Margin="11,0,0,0" x:Name="UpperValue" Content="0" Width="30" HorizontalContentAlignment="Center" |
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Top" |
||||||
|
/> |
||||||
|
</StackPanel> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style TargetType="{x:Type controls:PresetButton}"> |
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:PresetButton}"> |
||||||
|
<Button x:Name="PART_Button" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"> |
||||||
|
<StackPanel Orientation="Horizontal"> |
||||||
|
<Label x:Name="Checkbox" Padding="0" VerticalAlignment="Center">✔</Label> |
||||||
|
<Label Content="{TemplateBinding Label}"></Label> |
||||||
|
</StackPanel> |
||||||
|
</Button> |
||||||
|
|
||||||
|
<ControlTemplate.Triggers> |
||||||
|
<!-- Show the checkmark if the configuration matches this preset. --> |
||||||
|
<Trigger Property="Selected" Value="False"> |
||||||
|
<Setter Property="Visibility" TargetName="Checkbox" Value="Collapsed" /> |
||||||
|
</Trigger> |
||||||
|
</ControlTemplate.Triggers> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style TargetType="{x:Type controls:PresetWidget}"> |
||||||
|
<Setter Property="Focusable" Value="false" /> |
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:PresetWidget}"> |
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> |
||||||
|
<controls:PresetButton |
||||||
|
Width="70" |
||||||
|
Padding="5,2" Margin="5" |
||||||
|
Label="{TemplateBinding Label}" |
||||||
|
Type="{TemplateBinding Type}" |
||||||
|
/> |
||||||
|
|
||||||
|
<TextBlock Width="350" VerticalAlignment="Center" |
||||||
|
TextAlignment="Center" xml:space="preserve" Text="{TemplateBinding Description}" /> |
||||||
|
</StackPanel> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<!-- Change this to any pure hue i.e. no more than 2 rgb components set and at least 1 set to FF --> |
||||||
|
<Color x:Key="CurrentColor">#00FF00</Color> |
||||||
|
|
||||||
|
<!-- http://stackoverflow.com/a/32514853/136829 --> |
||||||
|
<LinearGradientBrush x:Key="HueBrush" StartPoint="0,0" EndPoint="1,0"> |
||||||
|
<LinearGradientBrush.GradientStops> |
||||||
|
<GradientStop Color="#FF0000" Offset="0.000" /> |
||||||
|
<GradientStop Color="#FFFF00" Offset="0.167" /> |
||||||
|
<GradientStop Color="#00FF00" Offset="0.333" /> |
||||||
|
<GradientStop Color="#00FFFF" Offset="0.5" /> |
||||||
|
<GradientStop Color="#0000FF" Offset="0.667" /> |
||||||
|
<GradientStop Color="#FF00FF" Offset="0.833" /> |
||||||
|
<GradientStop Color="#FF0000" Offset="1.000" /> |
||||||
|
</LinearGradientBrush.GradientStops> |
||||||
|
</LinearGradientBrush> |
||||||
|
|
||||||
|
<Style x:Key="PanelButton" TargetType="{x:Type controls:PanelButton}"> |
||||||
|
<Setter Property="VerticalAlignment" Value="Center" /> |
||||||
|
<Setter Property="Width" Value="25" /> |
||||||
|
|
||||||
|
<Setter Property="FocusVisualStyle"> |
||||||
|
<Setter.Value> |
||||||
|
<Style> |
||||||
|
<Setter Property="Control.Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate> |
||||||
|
<Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
<Setter Property="Background" Value="#FFDDDDDD"/> |
||||||
|
<Setter Property="BorderBrush" Value="#FF707070"/> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> |
||||||
|
<Setter Property="BorderThickness" Value="1"/> |
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center"/> |
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/> |
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:PanelButton}"> |
||||||
|
<Border x:Name="border" BorderBrush="#000000" BorderThickness="1" SnapsToDevicePixels="true"> |
||||||
|
<ContentPresenter |
||||||
|
SnapsToDevicePixels="true" |
||||||
|
Focusable="False" |
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" |
||||||
|
/> |
||||||
|
</Border> |
||||||
|
|
||||||
|
<ControlTemplate.Triggers> |
||||||
|
<Trigger Property="IsChecked" Value="False"> |
||||||
|
<Setter Property="Background" TargetName="border" Value="#FFFF0000"/> |
||||||
|
</Trigger> |
||||||
|
<Trigger Property="IsChecked" Value="True"> |
||||||
|
<Setter Property="Background" TargetName="border" Value="#FF00FF00"/> |
||||||
|
</Trigger> |
||||||
|
<Trigger Property="IsPressed" Value="true"> |
||||||
|
<Setter Property="Background" TargetName="border" Value="#FFC4E5F6"/> |
||||||
|
<Setter Property="BorderBrush" TargetName="border" Value="#FF2C628B"/> |
||||||
|
</Trigger> |
||||||
|
</ControlTemplate.Triggers> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style TargetType="{x:Type controls:PanelColorButton}"> |
||||||
|
<Setter Property="VerticalAlignment" Value="Center" /> |
||||||
|
<Setter Property="Width" Value="25" /> |
||||||
|
<Setter Property="AllowDrop" Value="True" /> |
||||||
|
|
||||||
|
<Setter Property="Focusable" Value="false"/> |
||||||
|
<Setter Property="Background" Value="#FFDDDDDD"/> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> |
||||||
|
<Setter Property="BorderBrush" Value="#FF707070"/> |
||||||
|
<Setter Property="BorderThickness" Value="1"/> |
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center"/> |
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/> |
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:PanelColorButton}"> |
||||||
|
<Border x:Name="border" |
||||||
|
Background="{TemplateBinding PanelColor}" |
||||||
|
BorderBrush="#000000" BorderThickness="1" SnapsToDevicePixels="true"> |
||||||
|
<ContentPresenter |
||||||
|
SnapsToDevicePixels="true" |
||||||
|
Focusable="False" |
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" |
||||||
|
/> |
||||||
|
</Border> |
||||||
|
|
||||||
|
<ControlTemplate.Triggers> |
||||||
|
<Trigger Property="IsSelected" Value="true"> |
||||||
|
<Setter Property="BorderBrush" TargetName="border" Value="#FF000000"/> |
||||||
|
<Setter Property="BorderThickness" TargetName="border" Value="2"/> |
||||||
|
</Trigger> |
||||||
|
<Trigger Property="IsSelected" Value="false"> |
||||||
|
<Setter Property="BorderBrush" TargetName="border" Value="#FF808080"/> |
||||||
|
</Trigger> |
||||||
|
</ControlTemplate.Triggers> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style TargetType="{x:Type controls:ColorPicker}"> |
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:ColorPicker}"> |
||||||
|
<StackPanel> |
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> |
||||||
|
<Label Content="Step color" Width="80" HorizontalContentAlignment="Center" /> |
||||||
|
<Grid Width="400" Height="20"> |
||||||
|
<DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> |
||||||
|
<Rectangle DockPanel.Dock="Left" Fill="#FFFFFFFF" Width="25" |
||||||
|
Stroke="Black" StrokeThickness="1" SnapsToDevicePixels="True" /> |
||||||
|
<Rectangle DockPanel.Dock="Left" Fill="{DynamicResource HueBrush}" |
||||||
|
Stroke="Black" StrokeThickness="1" SnapsToDevicePixels="True" /> |
||||||
|
</DockPanel> |
||||||
|
<controls:ColorPickerSlider x:Name="HueSlider" Minimum="-14" Maximum="359" |
||||||
|
Focusable="false" |
||||||
|
VerticalAlignment="Center" SmallChange="1" LargeChange="10" |
||||||
|
Margin="7 0 0 0" |
||||||
|
IsMoveToPointEnabled="True" IsSnapToTickEnabled="True"/> |
||||||
|
</Grid> |
||||||
|
</StackPanel> |
||||||
|
</StackPanel> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style TargetType="{x:Type controls:PanelSelector}"> |
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:PanelSelector}"> |
||||||
|
|
||||||
|
<Grid Background="#FFE5E5E5" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,10,0,-1"> |
||||||
|
<controls:PanelButton controls:PanelButton.Button="7" x:Name="EnablePanel7" Content="↖" Style="{StaticResource PanelButton}" Margin="-60,-50,0,0" /> |
||||||
|
<controls:PanelButton controls:PanelButton.Button="8" x:Name="EnablePanel8" Content="↑" Style="{StaticResource PanelButton}" Margin="0,-50,0,0" /> |
||||||
|
<controls:PanelButton controls:PanelButton.Button="9" x:Name="EnablePanel9" Content="↗" Style="{StaticResource PanelButton}" Margin="60,-50,0,0" /> |
||||||
|
|
||||||
|
<controls:PanelButton controls:PanelButton.Button="4" x:Name="EnablePanel4" Content="←" Style="{StaticResource PanelButton}" Margin="-60,0,0,0" /> |
||||||
|
<controls:PanelButton controls:PanelButton.Button="5" x:Name="EnablePanel5" Content="☐" Style="{StaticResource PanelButton}" Margin="0,0,0,0" /> |
||||||
|
<controls:PanelButton controls:PanelButton.Button="6" x:Name="EnablePanel6" Content="→" Style="{StaticResource PanelButton}" Margin="60,0,0,0" /> |
||||||
|
|
||||||
|
<controls:PanelButton controls:PanelButton.Button="1" x:Name="EnablePanel1" Content="↙" Style="{StaticResource PanelButton}" Margin="-60,50,0,0" /> |
||||||
|
<controls:PanelButton controls:PanelButton.Button="2" x:Name="EnablePanel2" Content="↓" Style="{StaticResource PanelButton}" Margin="0,50,0,0" /> |
||||||
|
<controls:PanelButton controls:PanelButton.Button="3" x:Name="EnablePanel3" Content="↘" Style="{StaticResource PanelButton}" Margin="60,50,0,0" /> |
||||||
|
</Grid> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style TargetType="{x:Type controls:DiagnosticsPanelButton}"> |
||||||
|
<Setter Property="VerticalAlignment" Value="Center" /> |
||||||
|
<Setter Property="Width" Value="50" /> |
||||||
|
<Setter Property="Height" Value="50" /> |
||||||
|
|
||||||
|
<Setter Property="Focusable" Value="false"/> |
||||||
|
<Setter Property="Background" Value="#FFDDDDDD"/> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> |
||||||
|
<Setter Property="BorderBrush" Value="#FF707070"/> |
||||||
|
<Setter Property="BorderThickness" Value="1"/> |
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center"/> |
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/> |
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:DiagnosticsPanelButton}"> |
||||||
|
<Border x:Name="border" |
||||||
|
Background="#FFA0A0A0" |
||||||
|
BorderBrush="#000000" BorderThickness="1" SnapsToDevicePixels="true"> |
||||||
|
<Grid |
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" |
||||||
|
> |
||||||
|
<TextBlock |
||||||
|
x:Name="WarningIcon" |
||||||
|
Text="!" |
||||||
|
Visibility="{TemplateBinding Warning, Converter={StaticResource BooleanToVisibilityConverter}}" |
||||||
|
Foreground="#FFFF4040" |
||||||
|
Margin="0,-5,0,0" |
||||||
|
FontSize="40" |
||||||
|
FontFamily="{StaticResource WarningMarkFont}" |
||||||
|
/> |
||||||
|
<ContentPresenter |
||||||
|
SnapsToDevicePixels="true" |
||||||
|
Focusable="False" |
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
</Border> |
||||||
|
|
||||||
|
<ControlTemplate.Triggers> |
||||||
|
<!-- Highlight on press: --> |
||||||
|
<Trigger Property="Pressed" Value="true"> |
||||||
|
<Setter Property="Background" TargetName="border" Value="#FF20FF20"/> |
||||||
|
</Trigger> |
||||||
|
|
||||||
|
<!-- Show which panel is selected for displaying diagnostics: --> |
||||||
|
<Trigger Property="IsSelected" Value="true"> |
||||||
|
<Setter Property="BorderBrush" TargetName="border" Value="#FF000000"/> |
||||||
|
<Setter Property="BorderThickness" TargetName="border" Value="2"/> |
||||||
|
</Trigger> |
||||||
|
<Trigger Property="IsSelected" Value="false"> |
||||||
|
<Setter Property="BorderBrush" TargetName="border" Value="#FF808080"/> |
||||||
|
</Trigger> |
||||||
|
</ControlTemplate.Triggers> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style TargetType="{x:Type controls:LevelBar}"> |
||||||
|
<Setter Property="Width" Value="35" /> |
||||||
|
<Setter Property="Height" Value="200" /> |
||||||
|
<Setter Property="Focusable" Value="false"/> |
||||||
|
|
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:LevelBar}"> |
||||||
|
<Grid> |
||||||
|
<!-- Black backing: --> |
||||||
|
<Rectangle |
||||||
|
x:Name="Back" |
||||||
|
Width="35" Height="200" Fill="#FF000000" |
||||||
|
HorizontalAlignment="Center" |
||||||
|
Stroke="Black" StrokeThickness="1" |
||||||
|
/> |
||||||
|
|
||||||
|
<!-- Filled portion: --> |
||||||
|
<Rectangle |
||||||
|
x:Name="Fill" |
||||||
|
Margin="0,0,0,1" |
||||||
|
Width="33" Height="100" Fill="#FF00FF00" |
||||||
|
HorizontalAlignment="Center" |
||||||
|
VerticalAlignment="Bottom" |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
|
||||||
|
<Style TargetType="{x:Type controls:DiagnosticsControl}"> |
||||||
|
<Setter Property="Focusable" Value="false"/> |
||||||
|
|
||||||
|
<Setter Property="Template"> |
||||||
|
<Setter.Value> |
||||||
|
<ControlTemplate TargetType="{x:Type controls:DiagnosticsControl}"> |
||||||
|
<Grid Background="#FFE5E5E5"> |
||||||
|
<TextBlock HorizontalAlignment="Center" |
||||||
|
xml:space="preserve" FontSize="16" Margin="0,15,0,0">Platform diagnostics</TextBlock> |
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top"> |
||||||
|
<Grid x:Name="P1Diagnostics" |
||||||
|
Margin="0,50,0,0" |
||||||
|
Background="#FFE5E5E5" Width="200" Height="200"> |
||||||
|
<controls:DiagnosticsPanelButton Panel="0" Margin="-120,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="1" Margin="0,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="2" Margin="120,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
|
||||||
|
<controls:DiagnosticsPanelButton Panel="3" Margin="-120,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="4" Margin="0,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="5" Margin="120,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
|
||||||
|
<controls:DiagnosticsPanelButton Panel="6" Margin="-120,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="7" Margin="0,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="8" Margin="120,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
</Grid> |
||||||
|
|
||||||
|
<Grid x:Name="P2Diagnostics" Margin="0,50,0,0" |
||||||
|
Background="#FFE5E5E5" Width="200" Height="200" |
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Top"> |
||||||
|
<controls:DiagnosticsPanelButton Panel="9" Margin="-120,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="10" Margin="0,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="11" Margin="120,-120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
|
||||||
|
<controls:DiagnosticsPanelButton Panel="12" Margin="-120,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="13" Margin="0,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="14" Margin="120,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
|
||||||
|
<controls:DiagnosticsPanelButton Panel="15" Margin="-120,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="16" Margin="0,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
<controls:DiagnosticsPanelButton Panel="17" Margin="120,120,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> |
||||||
|
</Grid> |
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" |
||||||
|
Margin="50,300,0,0" |
||||||
|
Width="200" |
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Top"> |
||||||
|
<DockPanel x:Name="SensorBarPanel" HorizontalAlignment="Center"> |
||||||
|
<ComboBox |
||||||
|
x:Name="DiagnosticMode" DockPanel.Dock="Top" |
||||||
|
Margin="0,0,0,5" |
||||||
|
HorizontalAlignment="Left" SelectedIndex="0" Width="120"> |
||||||
|
<ListBoxItem Content="Calibrated values"/> |
||||||
|
<ListBoxItem Content="Raw values"/> |
||||||
|
<ListBoxItem Content="Noise"/> |
||||||
|
<ListBoxItem Content="Tare"/> |
||||||
|
</ComboBox> |
||||||
|
|
||||||
|
<StackPanel DockPanel.Dock="Left" Orientation="Vertical"> |
||||||
|
<controls:LevelBar x:Name="SensorBar1" Margin="0,0,5,0"/> |
||||||
|
<Label x:Name="SensorBarLevel1" HorizontalAlignment="Center" Content="xxx"/> |
||||||
|
</StackPanel> |
||||||
|
<StackPanel DockPanel.Dock="Left" Orientation="Vertical"> |
||||||
|
<controls:LevelBar x:Name="SensorBar2" Margin="0,0,5,0"/> |
||||||
|
<Label x:Name="SensorBarLevel2" HorizontalAlignment="Center" Content="xxx"/> |
||||||
|
</StackPanel> |
||||||
|
<StackPanel DockPanel.Dock="Left" Orientation="Vertical"> |
||||||
|
<controls:LevelBar x:Name="SensorBar3" Margin="0,0,5,0"/> |
||||||
|
<Label x:Name="SensorBarLevel3" HorizontalAlignment="Center" Content="xxx"/> |
||||||
|
</StackPanel> |
||||||
|
<StackPanel DockPanel.Dock="Left" Orientation="Vertical"> |
||||||
|
<controls:LevelBar x:Name="SensorBar4" Margin="0,0,0,0"/> |
||||||
|
<Label x:Name="SensorBarLevel4" HorizontalAlignment="Center" Content="xxx"/> |
||||||
|
</StackPanel> |
||||||
|
</DockPanel> |
||||||
|
|
||||||
|
<DockPanel |
||||||
|
x:Name="NoResponseFromPanel" |
||||||
|
Width="200" |
||||||
|
HorizontalAlignment="Center" |
||||||
|
Margin="0,-20,0,0" |
||||||
|
> |
||||||
|
<TextBlock |
||||||
|
HorizontalAlignment="Stretch" VerticalAlignment="Center" |
||||||
|
Foreground="#FFFF0000" FontSize="20" |
||||||
|
FontFamily="{StaticResource WarningMarkFont}" |
||||||
|
Text="!" |
||||||
|
/> |
||||||
|
|
||||||
|
<TextBlock |
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" |
||||||
|
TextWrapping="Wrap" TextAlignment="Center" |
||||||
|
Text="No data received from this panel." |
||||||
|
/> |
||||||
|
</DockPanel> |
||||||
|
|
||||||
|
<DockPanel |
||||||
|
x:Name="NoResponseFromSensors" |
||||||
|
Width="200" |
||||||
|
> |
||||||
|
<TextBlock |
||||||
|
HorizontalAlignment="Stretch" VerticalAlignment="Center" |
||||||
|
Foreground="#FFFF0000" FontSize="20" |
||||||
|
FontFamily="{StaticResource WarningMarkFont}" |
||||||
|
Text="!" |
||||||
|
/> |
||||||
|
|
||||||
|
<TextBlock |
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" |
||||||
|
TextWrapping="Wrap" TextAlignment="Center" |
||||||
|
Text="Some sensors on this panel aren't responding correctly." |
||||||
|
/> |
||||||
|
</DockPanel> |
||||||
|
<StackPanel Orientation="Horizontal" |
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Top" |
||||||
|
Margin="10,10,0,0" |
||||||
|
> |
||||||
|
<Button |
||||||
|
x:Name="Recalibrate" Content="Recalibrate" |
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Top" |
||||||
|
Padding="8,4" |
||||||
|
/> |
||||||
|
<Button |
||||||
|
x:Name="LightAll" Content="Light panels" |
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Top" |
||||||
|
Padding="8,4" |
||||||
|
Margin="10,0,0,0" |
||||||
|
/> |
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" |
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Top" |
||||||
|
Margin="300,350,0,0" |
||||||
|
> |
||||||
|
<TextBlock Text="Current DIP"/> |
||||||
|
<controls:FrameImage |
||||||
|
Image="Resources/DIP.png" |
||||||
|
x:Name="CurrentDIP" |
||||||
|
FramesX="16" |
||||||
|
Frame="1" |
||||||
|
Width="60" |
||||||
|
Height="100" |
||||||
|
/> |
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" |
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Top" |
||||||
|
Margin="400,350,0,0" |
||||||
|
> |
||||||
|
<TextBlock Text="Expected DIP"/> |
||||||
|
<controls:FrameImage |
||||||
|
Image="Resources/DIP.png" |
||||||
|
x:Name="ExpectedDIP" |
||||||
|
FramesX="16" |
||||||
|
Frame="1" |
||||||
|
Width="60" |
||||||
|
Height="100" |
||||||
|
/> |
||||||
|
</StackPanel> |
||||||
|
</Grid> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter.Value> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<!-- P1, P2 labels in the top right: --> |
||||||
|
<Style x:Key="EnabledIcon" TargetType="Label"> |
||||||
|
<Style.Triggers> |
||||||
|
<Trigger Property="IsEnabled" Value="true"> |
||||||
|
<Setter Property="Foreground" Value="#000000"/> |
||||||
|
</Trigger> |
||||||
|
<Trigger Property="IsEnabled" Value="false"> |
||||||
|
<Setter Property="Foreground" Value="#A0A0A0"/> |
||||||
|
</Trigger> |
||||||
|
</Style.Triggers> |
||||||
|
</Style> |
||||||
|
|
||||||
|
</Window.Resources> |
||||||
|
|
||||||
|
<Grid> |
||||||
|
<!-- A list of which pads are connected, overlapping the tab bar in the top right. --> |
||||||
|
<Grid x:Name="ConnectedPads" Background="#DDD"> |
||||||
|
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Right" Orientation="Horizontal"> |
||||||
|
<Label Content="Connected:" FontSize="10"/> |
||||||
|
<Label x:Name="P1Connected" Style="{StaticResource EnabledIcon}" Content="P1" Margin="1,0,4,0" FontSize="10"/> |
||||||
|
<Label x:Name="P2Connected" Style="{StaticResource EnabledIcon}" Content="P2" Margin="0,0,4,0" FontSize="10"/> |
||||||
|
</StackPanel> |
||||||
|
</Grid> |
||||||
|
|
||||||
|
<Grid x:Name="Searching" Visibility="Hidden" Background="#DDD"> |
||||||
|
<Label Content="Searching for controller..." |
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="33.333"/> |
||||||
|
</Grid> |
||||||
|
<TabControl x:Name="Main" Margin="0,0,0,0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"> |
||||||
|
<TabItem Header="Settings"> |
||||||
|
<Grid Background="#FFE5E5E5" RenderTransformOrigin="0.5,0.5"> |
||||||
|
|
||||||
|
<StackPanel Margin="0,0,0,0" VerticalAlignment="Top"> |
||||||
|
<TextBlock HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top" |
||||||
|
TextAlignment="Center" |
||||||
|
xml:space="preserve" FontSize="16">Panel sensitivity</TextBlock> |
||||||
|
|
||||||
|
<controls:PresetWidget Type="high" Label="High" |
||||||
|
Description="{StaticResource HighPresetDescription}" |
||||||
|
Margin="0,5,0,10" |
||||||
|
/> |
||||||
|
<controls:PresetWidget controls:PresetWidget.Type="normal" Label="Medium" |
||||||
|
Description="{StaticResource NormalPresetDescription}" |
||||||
|
Margin="0,5,0,10" |
||||||
|
/> |
||||||
|
<controls:PresetWidget controls:PresetWidget.Type="low" Label="Low" |
||||||
|
Description="{StaticResource LowPresetDescription}" |
||||||
|
Margin="0,5,0,10" |
||||||
|
/> |
||||||
|
|
||||||
|
<Separator Margin="0,10,0,4" /> |
||||||
|
|
||||||
|
<TextBlock HorizontalAlignment="Center" Margin="0,15,0,0" VerticalAlignment="Top" |
||||||
|
TextAlignment="Center" |
||||||
|
xml:space="preserve" FontSize="16">Panel colors</TextBlock> |
||||||
|
|
||||||
|
<TextBlock DockPanel.Dock="Top" HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top" |
||||||
|
TextAlignment="Center" |
||||||
|
xml:space="preserve" |
||||||
|
>Set the color each arrow lights when pressed.</TextBlock> |
||||||
|
|
||||||
|
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> |
||||||
|
|
||||||
|
<!-- The duplication here could be factored out, but with WPF it's not really worth it: --> |
||||||
|
<Grid x:Name="PanelColorP1" Background="#FFE5E5E5" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Top"> |
||||||
|
<controls:PanelColorButton Panel="0" Content="↖" Margin="-60,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="1" Content="↑" Margin="0,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="2" Content="↗" Margin="60,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
|
||||||
|
<controls:PanelColorButton Panel="3" Content="←" Margin="-60,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="4" Content="☐" Margin="0,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="5" Content="→" Margin="60,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
|
||||||
|
<controls:PanelColorButton Panel="6" Content="↙" Margin="-60,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="7" Content="↓" Margin="0,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="8" Content="↘" Margin="60,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
</Grid> |
||||||
|
|
||||||
|
<Grid x:Name="PanelColorP2" Background="#FFE5E5E5" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Top"> |
||||||
|
<controls:PanelColorButton Panel="9" Content="↖" Margin="-60,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="10" Content="↑" Margin="0,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="11" Content="↗" Margin="60,-50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
|
||||||
|
<controls:PanelColorButton Panel="12" Content="←" Margin="-60,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="13" Content="☐" Margin="0,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="14" Content="→" Margin="60,0,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
|
||||||
|
<controls:PanelColorButton Panel="15" Content="↙" Margin="-60,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="16" Content="↓" Margin="0,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
<controls:PanelColorButton Panel="17" Content="↘" Margin="60,50,0,0" SelectedPanel="{Binding Path=SelectedPanel, Mode=TwoWay, ElementName=AutoLightsColor}" /> |
||||||
|
</Grid> |
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
<controls:ColorPicker |
||||||
|
x:Name="AutoLightsColor" Focusable="false" |
||||||
|
controls:ColorPicker.SelectedPanel="0" |
||||||
|
/> |
||||||
|
|
||||||
|
<Button x:Name="SetAllPanelsToCurrentColor" Content="Set all panels to this color" HorizontalAlignment="Center" Padding="6,2" Margin="0,6,0,0" /> |
||||||
|
</StackPanel> |
||||||
|
</Grid> |
||||||
|
</TabItem> |
||||||
|
|
||||||
|
<TabItem Header="Sensitivity"> |
||||||
|
<Grid Background="#FFE5E5E5" RenderTransformOrigin="0.5,0.5"> |
||||||
|
<DockPanel Margin="0,0,0,0" VerticalAlignment="Stretch"> |
||||||
|
<TextBlock DockPanel.Dock="Top" HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top" |
||||||
|
TextAlignment="Center" |
||||||
|
xml:space="preserve" FontSize="16">Custom sensitivity</TextBlock> |
||||||
|
<TextBlock DockPanel.Dock="Top" HorizontalAlignment="Center" Margin="0,0,0,10" VerticalAlignment="Top" |
||||||
|
xml:space="preserve" |
||||||
|
TextAlignment="Center" |
||||||
|
>Set the force required to trigger each arrow.</TextBlock> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="up-left" controls:ThresholdSlider.Icon="Resources/pad_up_left.png" Margin="0,8,0,0" /> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="up" controls:ThresholdSlider.Icon="Resources/pad_up.png" Margin="0,8,0,0" /> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="up-right" controls:ThresholdSlider.Icon="Resources/pad_up_right.png" Margin="0,8,0,0" /> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="left" controls:ThresholdSlider.Icon="Resources/pad_left.png" Margin="0,8,0,0" /> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="center" controls:ThresholdSlider.Icon="Resources/pad_center.png" Margin="0,8,0,0" /> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="right" controls:ThresholdSlider.Icon="Resources/pad_right.png" Margin="0,8,0,0" /> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="down-left" controls:ThresholdSlider.Icon="Resources/pad_down_left.png" Margin="0,8,0,0" /> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="down" controls:ThresholdSlider.Icon="Resources/pad_down.png" Margin="0,8,0,0" /> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="down-right" controls:ThresholdSlider.Icon="Resources/pad_down_right.png" Margin="0,8,0,0" /> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="cardinal" controls:ThresholdSlider.Icon="Resources/pad_cardinal.png" Margin="0,8,0,0" /> |
||||||
|
<controls:ThresholdSlider DockPanel.Dock="Top" controls:ThresholdSlider.Type="corner" controls:ThresholdSlider.Icon="Resources/pad_diagonal.png" Margin="0,8,0,0" /> |
||||||
|
<TextBlock DockPanel.Dock="Top" HorizontalAlignment="Center" Margin="0,25,0,0" VerticalAlignment="Top" |
||||||
|
xml:space="preserve" |
||||||
|
TextAlignment="Center" |
||||||
|
>Each slider sets the amount of weight necessary for |
||||||
|
a panel to activate and deactivate. |
||||||
|
|
||||||
|
A panel will activate when it reaches the right side of the |
||||||
|
slider, and deactivate when it reaches the left side of the slider.</TextBlock> |
||||||
|
|
||||||
|
<DockPanel DockPanel.Dock="Bottom" |
||||||
|
VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Margin="0,0,5,0"> |
||||||
|
<Label Content="Presets:" VerticalAlignment="Center" /> |
||||||
|
<controls:PresetButton Width="70" Padding="5,2" Margin="5" Label="Low" Type="low" /> |
||||||
|
<controls:PresetButton Width="70" Padding="5,2" Margin="5" Label="Normal" Type="normal" /> |
||||||
|
<controls:PresetButton Width="70" Padding="5,2" Margin="5" Label="High" Type="high" /> |
||||||
|
<controls:AdvancedThresholdViewCheckbox |
||||||
|
x:Name="AdvancedModeEnabledCheckbox" |
||||||
|
DockPanel.Dock="Right" Content="Advanced view" |
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Right" |
||||||
|
IsChecked="{Binding Path=AdvancedModeEnabled, Mode=TwoWay, RelativeSource={RelativeSource Self}}" |
||||||
|
/> |
||||||
|
</DockPanel> |
||||||
|
</DockPanel> |
||||||
|
</Grid> |
||||||
|
</TabItem> |
||||||
|
|
||||||
|
<TabItem Header="Advanced"> |
||||||
|
<StackPanel Background="#FFE5E5E5"> |
||||||
|
<TextBlock HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top" |
||||||
|
TextAlignment="Center" |
||||||
|
xml:space="preserve" FontSize="16">Active panels</TextBlock> |
||||||
|
<TextBlock xml:space="preserve" HorizontalAlignment="Center" Margin="0,0,0,0" TextAlignment="Center">Select which directions have sensors, and deselect panels that aren't in use. |
||||||
|
|
||||||
|
Input will be disabled from deselected panels.</TextBlock> |
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> |
||||||
|
<controls:PanelSelector Focusable="false" /> |
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
<Separator Margin="0,10,0,10" /> |
||||||
|
<TextBlock HorizontalAlignment="Center" |
||||||
|
xml:space="preserve" FontSize="16">Reset all settings</TextBlock> |
||||||
|
<Button Content="Factory reset" Width="140" Margin="0 10 0 0" Padding="0 4" Click="FactoryReset_Click"/> |
||||||
|
</StackPanel> |
||||||
|
</TabItem> |
||||||
|
<TabItem Header="Diagnostics"> |
||||||
|
<StackPanel Background="#FFE5E5E5"> |
||||||
|
<controls:DiagnosticsControl x:Name="Diagnostics" /> |
||||||
|
</StackPanel> |
||||||
|
</TabItem> |
||||||
|
</TabControl> |
||||||
|
|
||||||
|
</Grid> |
||||||
|
</Window> |
@ -0,0 +1,86 @@ |
|||||||
|
using System; |
||||||
|
using System.Windows; |
||||||
|
|
||||||
|
namespace smx_config |
||||||
|
{ |
||||||
|
public partial class MainWindow: Window |
||||||
|
{ |
||||||
|
OnConfigChange onConfigChange; |
||||||
|
ShowAutoLightsColor showAutoLightsColor = new ShowAutoLightsColor(); |
||||||
|
|
||||||
|
public MainWindow() |
||||||
|
{ |
||||||
|
InitializeComponent(); |
||||||
|
|
||||||
|
onConfigChange = new OnConfigChange(this, delegate(LoadFromConfigDelegateArgs args) { |
||||||
|
LoadUIFromConfig(args); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
AutoLightsColor.StartedDragging += delegate() { showAutoLightsColor.Start(); }; |
||||||
|
AutoLightsColor.StoppedDragging += delegate() { showAutoLightsColor.Stop(); }; |
||||||
|
AutoLightsColor.StoppedDragging += delegate() { showAutoLightsColor.Stop(); }; |
||||||
|
|
||||||
|
// This doesn't happen at the same time AutoLightsColor is used, since they're on different tabs. |
||||||
|
Diagnostics.SetShowAllLights += delegate(bool on) |
||||||
|
{ |
||||||
|
if(on) |
||||||
|
showAutoLightsColor.Start(); |
||||||
|
else |
||||||
|
showAutoLightsColor.Stop(); |
||||||
|
}; |
||||||
|
|
||||||
|
SetAllPanelsToCurrentColor.Click += delegate(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
int SelectedPanel = AutoLightsColor.SelectedPanel % 9; |
||||||
|
int SelectedPad = AutoLightsColor.SelectedPanel < 9? 0:1; |
||||||
|
|
||||||
|
// Get the color of the selected pad. |
||||||
|
SMX.SMXConfig copyFromConfig; |
||||||
|
if(!SMX.SMX.GetConfig(SelectedPad, out copyFromConfig)) |
||||||
|
return; |
||||||
|
|
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
{ |
||||||
|
SMX.SMXConfig config; |
||||||
|
if(!SMX.SMX.GetConfig(pad, out config)) |
||||||
|
continue; |
||||||
|
|
||||||
|
// Set all stepColors to the color of the selected panel. |
||||||
|
for(int i = 0; i < 9; ++i) |
||||||
|
{ |
||||||
|
config.stepColor[i*3+0] = copyFromConfig.stepColor[SelectedPanel*3+0]; |
||||||
|
config.stepColor[i*3+1] = copyFromConfig.stepColor[SelectedPanel*3+1]; |
||||||
|
config.stepColor[i*3+2] = copyFromConfig.stepColor[SelectedPanel*3+2]; |
||||||
|
} |
||||||
|
|
||||||
|
SMX.SMX.SetConfig(pad, config); |
||||||
|
} |
||||||
|
CurrentSMXDevice.singleton.FireConfigurationChanged(null); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private void LoadUIFromConfig(LoadFromConfigDelegateArgs args) |
||||||
|
{ |
||||||
|
bool EitherControllerConnected = args.controller[0].info.connected || args.controller[1].info.connected; |
||||||
|
Main.Visibility = EitherControllerConnected? Visibility.Visible:Visibility.Hidden; |
||||||
|
Searching.Visibility = EitherControllerConnected? Visibility.Hidden:Visibility.Visible; |
||||||
|
ConnectedPads.Visibility = EitherControllerConnected? Visibility.Visible:Visibility.Hidden; |
||||||
|
P1Connected.IsEnabled = args.controller[0].info.connected; |
||||||
|
P2Connected.IsEnabled = args.controller[1].info.connected; |
||||||
|
PanelColorP1.Visibility = args.controller[0].info.connected? Visibility.Visible:Visibility.Collapsed; |
||||||
|
PanelColorP2.Visibility = args.controller[1].info.connected? Visibility.Visible:Visibility.Collapsed; |
||||||
|
} |
||||||
|
|
||||||
|
private void FactoryReset_Click(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
SMX.SMX.FactoryReset(pad); |
||||||
|
CurrentSMXDevice.singleton.FireConfigurationChanged(null); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,296 @@ |
|||||||
|
using System; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using smx_config; |
||||||
|
|
||||||
|
// This is a binding to the native SMX.dll. |
||||||
|
namespace SMX |
||||||
|
{ |
||||||
|
[StructLayout(LayoutKind.Sequential, Pack=1)] |
||||||
|
public struct SMXInfo { |
||||||
|
[MarshalAs(UnmanagedType.I1)] // work around C# bug: marshals bool as int |
||||||
|
public bool connected; |
||||||
|
|
||||||
|
// The 32-byte hex serial number of the device, followed by '\0'. |
||||||
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 33)] |
||||||
|
public byte[] m_Serial; |
||||||
|
|
||||||
|
public Int16 m_iFirmwareVersion; |
||||||
|
|
||||||
|
// Padding to make this the same size as native, where MSVC adds padding even though |
||||||
|
// we tell it not to: |
||||||
|
private Byte dummy; |
||||||
|
}; |
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack=1)] |
||||||
|
public struct SMXConfig { |
||||||
|
public Byte unused1, unused2; |
||||||
|
public Byte unused3, unused4; |
||||||
|
public Byte unused5, unused6; |
||||||
|
public UInt16 masterDebounceMilliseconds; |
||||||
|
public Byte panelThreshold7Low, panelThreshold7High; // was "cardinal" |
||||||
|
public Byte panelThreshold4Low, panelThreshold4High; // was "center" |
||||||
|
public Byte panelThreshold2Low, panelThreshold2High; // was "corner" |
||||||
|
public UInt16 panelDebounceMicroseconds; |
||||||
|
public UInt16 autoCalibrationPeriodMilliseconds; |
||||||
|
public Byte autoCalibrationMaxDeviation; |
||||||
|
public Byte badSensorMinimumDelaySeconds; |
||||||
|
public UInt16 autoCalibrationAveragesPerUpdate; |
||||||
|
public Byte unused7, unused8; |
||||||
|
public Byte panelThreshold1Low, panelThreshold1High; // was "up" |
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] |
||||||
|
public Byte[] enabledSensors; |
||||||
|
|
||||||
|
public Byte autoLightsTimeout; |
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3*9)] |
||||||
|
public Byte[] stepColor; |
||||||
|
|
||||||
|
public Byte panelRotation; |
||||||
|
public UInt16 autoCalibrationSamplesPerAverage; |
||||||
|
public Byte masterVersion; |
||||||
|
public Byte configVersion; |
||||||
|
|
||||||
|
// The remaining thresholds (configVersion >= 2). |
||||||
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] |
||||||
|
public Byte[] unused9; |
||||||
|
public Byte panelThreshold0Low, panelThreshold0High; |
||||||
|
public Byte panelThreshold3Low, panelThreshold3High; |
||||||
|
public Byte panelThreshold5Low, panelThreshold5High; |
||||||
|
public Byte panelThreshold6Low, panelThreshold6High; |
||||||
|
public Byte panelThreshold8Low, panelThreshold8High; |
||||||
|
|
||||||
|
// enabledSensors is a mask of which panels are enabled. Return this as an array |
||||||
|
// for convenience. |
||||||
|
public bool[] GetEnabledPanels() |
||||||
|
{ |
||||||
|
return new bool[] { |
||||||
|
(enabledSensors[0] & 0xF0) != 0, |
||||||
|
(enabledSensors[0] & 0x0F) != 0, |
||||||
|
(enabledSensors[1] & 0xF0) != 0, |
||||||
|
(enabledSensors[1] & 0x0F) != 0, |
||||||
|
(enabledSensors[2] & 0xF0) != 0, |
||||||
|
(enabledSensors[2] & 0x0F) != 0, |
||||||
|
(enabledSensors[3] & 0xF0) != 0, |
||||||
|
(enabledSensors[3] & 0x0F) != 0, |
||||||
|
(enabledSensors[4] & 0xF0) != 0, |
||||||
|
}; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
public struct SMXSensorTestModeData |
||||||
|
{ |
||||||
|
// If false, sensorLevel[n][*] is zero because we didn't receive a response from that panel. |
||||||
|
[MarshalAs(UnmanagedType.ByValArray, ArraySubType=UnmanagedType.I1, SizeConst = 9)] |
||||||
|
public bool[] bHaveDataFromPanel; |
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 9*4)] |
||||||
|
public Int16[] sensorLevel; |
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValArray, ArraySubType=UnmanagedType.I1, SizeConst = 9*4)] |
||||||
|
public bool[] bBadSensorInput; |
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)] |
||||||
|
public int[] iDIPSwitchPerPanel; |
||||||
|
|
||||||
|
public override bool Equals(object obj) |
||||||
|
{ |
||||||
|
SMXSensorTestModeData other = (SMXSensorTestModeData) obj; |
||||||
|
return |
||||||
|
Helpers.SequenceEqual(bHaveDataFromPanel, other.bHaveDataFromPanel) && |
||||||
|
Helpers.SequenceEqual(sensorLevel, other.sensorLevel) && |
||||||
|
Helpers.SequenceEqual(bBadSensorInput, other.bBadSensorInput) && |
||||||
|
Helpers.SequenceEqual(iDIPSwitchPerPanel, other.iDIPSwitchPerPanel); |
||||||
|
} |
||||||
|
|
||||||
|
// Dummy override to silence a bad warning. We don't use these in containers to need |
||||||
|
// a hash code implementation. |
||||||
|
public override int GetHashCode() { return base.GetHashCode(); } |
||||||
|
|
||||||
|
|
||||||
|
public bool AnySensorsOnPanelNotResponding(int panel) |
||||||
|
{ |
||||||
|
if(!bHaveDataFromPanel[panel]) |
||||||
|
return false; |
||||||
|
for(int sensor = 0; sensor < 4; ++sensor) |
||||||
|
if(bBadSensorInput[panel*4+sensor]) |
||||||
|
return true; |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
public static class SMX |
||||||
|
{ |
||||||
|
[DllImport("kernel32", SetLastError=true)] |
||||||
|
static extern IntPtr LoadLibrary(string lpFileName); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern void SMX_Start( |
||||||
|
[MarshalAs(UnmanagedType.FunctionPtr)] InternalUpdateCallback callback, |
||||||
|
IntPtr user); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern void SMX_Stop(); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern void SMX_GetInfo(int pad, out SMXInfo info); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern UInt16 SMX_GetInputState(int pad); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
[return:MarshalAs(UnmanagedType.I1)] |
||||||
|
private static extern bool SMX_GetConfig(int pad, out SMXConfig config); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern void SMX_SetConfig(int pad, ref SMXConfig config); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern void SMX_FactoryReset(int pad); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern void SMX_ForceRecalibration(int pad); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern void SMX_SetTestMode(int pad, int mode); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern bool SMX_GetTestData(int pad, out SMXSensorTestModeData data); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern bool SMX_SetLights(byte[] buf); |
||||||
|
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||||
|
private static extern bool SMX_ReenableAutoLights(); |
||||||
|
|
||||||
|
// Check if the native DLL is available. This is mostly to avoid exceptions in the designer. |
||||||
|
private static bool DLLAvailable() |
||||||
|
{ |
||||||
|
return LoadLibrary("SMX.dll") != IntPtr.Zero; |
||||||
|
} |
||||||
|
|
||||||
|
public delegate void UpdateCallback(int PadNumber, SMXUpdateCallbackReason reason); |
||||||
|
|
||||||
|
// The C API allows a user pointer, but we don't use that here. |
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] |
||||||
|
public delegate void InternalUpdateCallback(int PadNumber, int reason, IntPtr user); |
||||||
|
private static InternalUpdateCallback CurrentUpdateCallback; |
||||||
|
public static void SetUpdateCallback(UpdateCallback callback) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) return; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
public static void Start(UpdateCallback callback) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) return; |
||||||
|
|
||||||
|
// Make a wrapper to convert from the native enum to SMXUpdateCallbackReason. |
||||||
|
InternalUpdateCallback NewCallback = delegate(int PadNumber, int reason, IntPtr user) { |
||||||
|
SMXUpdateCallbackReason ReasonEnum = (SMXUpdateCallbackReason) Enum.ToObject(typeof(SMXUpdateCallbackReason), reason); |
||||||
|
callback(PadNumber, ReasonEnum); |
||||||
|
}; |
||||||
|
if(callback == null) |
||||||
|
NewCallback = null; |
||||||
|
|
||||||
|
SMX_Start(NewCallback, IntPtr.Zero); |
||||||
|
|
||||||
|
// Keep a reference to the delegate, so it isn't garbage collected. Do this last. Once |
||||||
|
// we do this the old callback may be collected, so we want to be sure that native code |
||||||
|
// has already updated the callback. |
||||||
|
CurrentUpdateCallback = NewCallback; |
||||||
|
|
||||||
|
Console.WriteLine("Struct sizes (C#): " + |
||||||
|
Marshal.SizeOf(typeof(SMXConfig)) + " " + |
||||||
|
Marshal.SizeOf(typeof(SMXInfo)) + " " + |
||||||
|
Marshal.SizeOf(typeof(SMXSensorTestModeData))); |
||||||
|
} |
||||||
|
|
||||||
|
public static void Stop() |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) return; |
||||||
|
SMX_Stop(); |
||||||
|
} |
||||||
|
|
||||||
|
public enum SMXUpdateCallbackReason { |
||||||
|
Updated, |
||||||
|
FactoryResetCommandComplete |
||||||
|
}; |
||||||
|
|
||||||
|
public static void GetInfo(int pad, out SMXInfo info) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) { |
||||||
|
info = new SMXInfo(); |
||||||
|
return; |
||||||
|
} |
||||||
|
SMX_GetInfo(pad, out info); |
||||||
|
} |
||||||
|
|
||||||
|
public static UInt16 GetInputState(int pad) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) |
||||||
|
return 0; |
||||||
|
|
||||||
|
return SMX_GetInputState(pad); |
||||||
|
} |
||||||
|
|
||||||
|
public static bool GetConfig(int pad, out SMXConfig config) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) { |
||||||
|
config = new SMXConfig(); |
||||||
|
config.enabledSensors = new Byte[5]; |
||||||
|
config.stepColor = new Byte[3*9]; |
||||||
|
return false; |
||||||
|
} |
||||||
|
return SMX_GetConfig(pad, out config); |
||||||
|
} |
||||||
|
|
||||||
|
public static void SetConfig(int pad, SMXConfig config) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) return; |
||||||
|
|
||||||
|
// Always bump the configVersion to the version we support on write. |
||||||
|
config.configVersion = 2; |
||||||
|
|
||||||
|
SMX_SetConfig(pad, ref config); |
||||||
|
} |
||||||
|
|
||||||
|
public enum SensorTestMode { |
||||||
|
Off = 0, |
||||||
|
UncalibratedValues = '0', |
||||||
|
CalibratedValues = '1', |
||||||
|
Noise = '2', |
||||||
|
Tare = '3', |
||||||
|
}; |
||||||
|
|
||||||
|
public static void SetTestMode(int pad, SensorTestMode mode) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) return; |
||||||
|
SMX_SetTestMode(pad, (int) mode); |
||||||
|
} |
||||||
|
|
||||||
|
public static bool GetTestData(int pad, out SMXSensorTestModeData data) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) { |
||||||
|
data = new SMXSensorTestModeData(); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return SMX_GetTestData(pad, out data); |
||||||
|
} |
||||||
|
|
||||||
|
public static void FactoryReset(int pad) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) return; |
||||||
|
SMX_FactoryReset(pad); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static void ForceRecalibration(int pad) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) return; |
||||||
|
SMX_ForceRecalibration(pad); |
||||||
|
} |
||||||
|
|
||||||
|
public static void SetLights(byte[] buf) |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) return; |
||||||
|
SMX_SetLights(buf); |
||||||
|
} |
||||||
|
|
||||||
|
public static void ReenableAutoLights() |
||||||
|
{ |
||||||
|
if(!DLLAvailable()) return; |
||||||
|
SMX_ReenableAutoLights(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,921 @@ |
|||||||
|
using System; |
||||||
|
using System.Windows; |
||||||
|
using System.Windows.Controls; |
||||||
|
using System.Windows.Controls.Primitives; |
||||||
|
using System.Windows.Media; |
||||||
|
using System.Windows.Input; |
||||||
|
using System.Windows.Data; |
||||||
|
using System.Windows.Shapes; |
||||||
|
using System.Windows.Media.Imaging; |
||||||
|
|
||||||
|
namespace smx_config |
||||||
|
{ |
||||||
|
// The checkbox to enable and disable the advanced per-panel sliders. |
||||||
|
// |
||||||
|
// This is always enabled if the thresholds in the configuration are set to different |
||||||
|
// values. If the user enables us, we'll remember that we were forced on. If the user |
||||||
|
// disables us, we'll sync the thresholds back up and turn the ForcedOn flag off. |
||||||
|
public class AdvancedThresholdViewCheckbox: CheckBox |
||||||
|
{ |
||||||
|
public static readonly DependencyProperty AdvancedModeEnabledProperty = DependencyProperty.Register("AdvancedModeEnabled", |
||||||
|
typeof(bool), typeof(AdvancedThresholdViewCheckbox), new FrameworkPropertyMetadata(false)); |
||||||
|
public bool AdvancedModeEnabled { |
||||||
|
get { return (bool) GetValue(AdvancedModeEnabledProperty); } |
||||||
|
set { SetValue(AdvancedModeEnabledProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
OnConfigChange onConfigChange; |
||||||
|
|
||||||
|
// If true, the user enabled advanced view and we should display it even if |
||||||
|
// the thresholds happen to be synced. If false, we'll only show the advanced |
||||||
|
// view if we need to because the thresholds aren't synced. |
||||||
|
bool ForcedOn; |
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { |
||||||
|
LoadUIFromConfig(args.controller[args.FirstController].config); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private void LoadUIFromConfig(SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
// The master version doesn't actually matter, but we use this as a signal that the panels |
||||||
|
// have a new enough firmware to support this. |
||||||
|
bool SupportsAdvancedMode = config.masterVersion != 0xFF && config.masterVersion >= 2; |
||||||
|
Visibility = SupportsAdvancedMode? Visibility.Visible:Visibility.Collapsed; |
||||||
|
|
||||||
|
// If the thresholds are different, force the checkbox on. This way, if you load the application |
||||||
|
// with a platform with per-panel thresholds, and change the thresholds to no longer be different, |
||||||
|
// advanced mode stays forced on. It'll only turn off if you uncheck the box, or if you exit |
||||||
|
// the application with synced thresholds and then restart it. |
||||||
|
if(SupportsAdvancedMode && !ConfigPresets.AreUnifiedThresholdsSynced(config)) |
||||||
|
ForcedOn = true; |
||||||
|
|
||||||
|
// Enable advanced mode if the master says it's supported, and either the user has checked the |
||||||
|
// box to turn it on or the thresholds are different in the current configuration. |
||||||
|
AdvancedModeEnabled = SupportsAdvancedMode && ForcedOn; |
||||||
|
} |
||||||
|
|
||||||
|
protected override void OnClick() |
||||||
|
{ |
||||||
|
if(AdvancedModeEnabled) |
||||||
|
{ |
||||||
|
// Stop forcing advanced mode on, and sync the thresholds so we exit advanced mode. |
||||||
|
ForcedOn = false; |
||||||
|
|
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
{ |
||||||
|
SMX.SMXConfig config; |
||||||
|
if(!SMX.SMX.GetConfig(pad, out config)) |
||||||
|
continue; |
||||||
|
|
||||||
|
ConfigPresets.SyncUnifiedThresholds(ref config); |
||||||
|
SMX.SMX.SetConfig(pad, config); |
||||||
|
} |
||||||
|
CurrentSMXDevice.singleton.FireConfigurationChanged(this); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
// Enable advanced mode. |
||||||
|
ForcedOn = true; |
||||||
|
} |
||||||
|
|
||||||
|
// Refresh the UI. |
||||||
|
LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState(); |
||||||
|
LoadUIFromConfig(args.controller[args.FirstController].config); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This implements the threshold slider widget for changing an upper/lower threshold pair. |
||||||
|
public class ThresholdSlider: Control |
||||||
|
{ |
||||||
|
public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", |
||||||
|
typeof(ImageSource), typeof(ThresholdSlider), new FrameworkPropertyMetadata(null)); |
||||||
|
|
||||||
|
public ImageSource Icon { |
||||||
|
get { return (ImageSource) GetValue(IconProperty); } |
||||||
|
set { SetValue(IconProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type", |
||||||
|
typeof(string), typeof(ThresholdSlider), new FrameworkPropertyMetadata("")); |
||||||
|
|
||||||
|
public string Type { |
||||||
|
get { return (string) GetValue(TypeProperty); } |
||||||
|
set { SetValue(TypeProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly DependencyProperty AdvancedModeEnabledProperty = DependencyProperty.Register("AdvancedModeEnabled", |
||||||
|
typeof(bool), typeof(ThresholdSlider), new FrameworkPropertyMetadata(false, RefreshAdvancedModeEnabledCallback)); |
||||||
|
|
||||||
|
public bool AdvancedModeEnabled { |
||||||
|
get { return (bool) GetValue(AdvancedModeEnabledProperty); } |
||||||
|
set { SetValue(AdvancedModeEnabledProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
private static void RefreshAdvancedModeEnabledCallback(DependencyObject target, DependencyPropertyChangedEventArgs args) |
||||||
|
{ |
||||||
|
ThresholdSlider self = target as ThresholdSlider; |
||||||
|
self.RefreshVisibility(); |
||||||
|
} |
||||||
|
|
||||||
|
DoubleSlider slider; |
||||||
|
Label LowerLabel, UpperLabel; |
||||||
|
|
||||||
|
OnConfigChange onConfigChange; |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
slider = GetTemplateChild("Slider") as DoubleSlider; |
||||||
|
LowerLabel = GetTemplateChild("LowerValue") as Label; |
||||||
|
UpperLabel = GetTemplateChild("UpperValue") as Label; |
||||||
|
|
||||||
|
slider.ValueChanged += delegate(DoubleSlider slider) { SaveToConfig(); }; |
||||||
|
|
||||||
|
onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { |
||||||
|
LoadUIFromConfig(args.controller[args.FirstController].config); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private void SetValueToConfig(ref SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
byte lower = (byte) slider.LowerValue; |
||||||
|
byte upper = (byte) slider.UpperValue; |
||||||
|
|
||||||
|
switch(Type) |
||||||
|
{ |
||||||
|
case "up-left": config.panelThreshold0Low = lower; config.panelThreshold0High = upper; break; |
||||||
|
case "up": config.panelThreshold1Low = lower; config.panelThreshold1High = upper; break; |
||||||
|
case "up-right": config.panelThreshold2Low = lower; config.panelThreshold2High = upper; break; |
||||||
|
case "left": config.panelThreshold3Low = lower; config.panelThreshold3High = upper; break; |
||||||
|
case "center": config.panelThreshold4Low = lower; config.panelThreshold4High = upper; break; |
||||||
|
case "right": config.panelThreshold5Low = lower; config.panelThreshold5High = upper; break; |
||||||
|
case "down-left": config.panelThreshold6Low = lower; config.panelThreshold6High = upper; break; |
||||||
|
case "down": config.panelThreshold7Low = lower; config.panelThreshold7High = upper; break; |
||||||
|
case "down-right": config.panelThreshold8Low = lower; config.panelThreshold8High = upper; break; |
||||||
|
case "cardinal": config.panelThreshold7Low = lower; config.panelThreshold7High = upper; break; |
||||||
|
case "corner": config.panelThreshold2Low = lower; config.panelThreshold2High = upper; break; |
||||||
|
} |
||||||
|
|
||||||
|
// If we're not in advanced mode, sync the cardinal value to each of the panel values. |
||||||
|
if(!AdvancedModeEnabled) |
||||||
|
ConfigPresets.SyncUnifiedThresholds(ref config); |
||||||
|
} |
||||||
|
|
||||||
|
private void GetValueFromConfig(SMX.SMXConfig config, out byte lower, out byte upper) |
||||||
|
{ |
||||||
|
switch(Type) |
||||||
|
{ |
||||||
|
case "up-left": lower = config.panelThreshold0Low; upper = config.panelThreshold0High; return; |
||||||
|
case "up": lower = config.panelThreshold1Low; upper = config.panelThreshold1High; return; |
||||||
|
case "up-right": lower = config.panelThreshold2Low; upper = config.panelThreshold2High; return; |
||||||
|
case "left": lower = config.panelThreshold3Low; upper = config.panelThreshold3High; return; |
||||||
|
case "center": lower = config.panelThreshold4Low; upper = config.panelThreshold4High; return; |
||||||
|
case "right": lower = config.panelThreshold5Low; upper = config.panelThreshold5High; return; |
||||||
|
case "down-left": lower = config.panelThreshold6Low; upper = config.panelThreshold6High; return; |
||||||
|
case "down": lower = config.panelThreshold7Low; upper = config.panelThreshold7High; return; |
||||||
|
case "down-right": lower = config.panelThreshold8Low; upper = config.panelThreshold8High; return; |
||||||
|
case "cardinal": lower = config.panelThreshold7Low; upper = config.panelThreshold7High; return; |
||||||
|
case "corner": lower = config.panelThreshold2Low; upper = config.panelThreshold2High; return; |
||||||
|
default: |
||||||
|
lower = upper = 0; |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void SaveToConfig() |
||||||
|
{ |
||||||
|
if(UpdatingUI) |
||||||
|
return; |
||||||
|
|
||||||
|
// Apply the change and save it to the devices. |
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
{ |
||||||
|
SMX.SMXConfig config; |
||||||
|
if(!SMX.SMX.GetConfig(pad, out config)) |
||||||
|
continue; |
||||||
|
|
||||||
|
SetValueToConfig(ref config); |
||||||
|
SMX.SMX.SetConfig(pad, config); |
||||||
|
CurrentSMXDevice.singleton.FireConfigurationChanged(this); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
bool UpdatingUI = false; |
||||||
|
private void LoadUIFromConfig(SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
// Make sure SaveToConfig doesn't treat these as the user changing values. |
||||||
|
UpdatingUI = true; |
||||||
|
|
||||||
|
byte lower, upper; |
||||||
|
GetValueFromConfig(config, out lower, out upper); |
||||||
|
|
||||||
|
if(lower == 0xFF) |
||||||
|
{ |
||||||
|
LowerLabel.Content = "Off"; |
||||||
|
UpperLabel.Content = ""; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
slider.LowerValue = lower; |
||||||
|
slider.UpperValue = upper; |
||||||
|
LowerLabel.Content = lower.ToString(); |
||||||
|
UpperLabel.Content = upper.ToString(); |
||||||
|
} |
||||||
|
|
||||||
|
RefreshVisibility(); |
||||||
|
UpdatingUI = false; |
||||||
|
} |
||||||
|
|
||||||
|
void RefreshVisibility() |
||||||
|
{ |
||||||
|
LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState(); |
||||||
|
SMX.SMXConfig config = args.controller[args.FirstController].config; |
||||||
|
this.Visibility = ShouldBeDisplayed(config)? Visibility.Visible:Visibility.Collapsed; |
||||||
|
} |
||||||
|
|
||||||
|
// Return true if this slider should be displayed. Only display a slider if it affects |
||||||
|
// at least one panel which is enabled. |
||||||
|
private bool ShouldBeDisplayed(SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
bool[] enabledPanels = config.GetEnabledPanels(); |
||||||
|
|
||||||
|
// Up and center are shown in both modes. |
||||||
|
switch(Type) |
||||||
|
{ |
||||||
|
case "up-left": return AdvancedModeEnabled && enabledPanels[0]; |
||||||
|
case "up": return enabledPanels[1]; |
||||||
|
case "up-right": return AdvancedModeEnabled && enabledPanels[2]; |
||||||
|
case "left": return AdvancedModeEnabled && enabledPanels[3]; |
||||||
|
case "center": return enabledPanels[4]; |
||||||
|
case "right": return AdvancedModeEnabled && enabledPanels[5]; |
||||||
|
case "down-left": return AdvancedModeEnabled && enabledPanels[6]; |
||||||
|
case "down": return AdvancedModeEnabled && enabledPanels[7]; |
||||||
|
case "down-right": return AdvancedModeEnabled && enabledPanels[8]; |
||||||
|
|
||||||
|
// Show cardinal and corner if at least one panel they affect is enabled. |
||||||
|
case "cardinal": return !AdvancedModeEnabled && (enabledPanels[3] || enabledPanels[5] || enabledPanels[8]); |
||||||
|
case "corner": return !AdvancedModeEnabled && (enabledPanels[0] || enabledPanels[2] || enabledPanels[6] || enabledPanels[8]); |
||||||
|
default: return true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// A button that selects a preset, and shows a checkmark if that preset is set. |
||||||
|
public class PresetButton: Control |
||||||
|
{ |
||||||
|
public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type", |
||||||
|
typeof(string), typeof(PresetButton), new FrameworkPropertyMetadata("")); |
||||||
|
public string Type { |
||||||
|
get { return (string) GetValue(TypeProperty); } |
||||||
|
set { SetValue(TypeProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly DependencyProperty SelectedProperty = DependencyProperty.Register("Selected", |
||||||
|
typeof(bool), typeof(PresetButton), new FrameworkPropertyMetadata(true)); |
||||||
|
public bool Selected { |
||||||
|
get { return (bool) GetValue(SelectedProperty); } |
||||||
|
set { SetValue(SelectedProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly DependencyProperty LabelProperty = DependencyProperty.Register("Label", |
||||||
|
typeof(string), typeof(PresetButton), new FrameworkPropertyMetadata("")); |
||||||
|
public string Label { |
||||||
|
get { return (string) GetValue(LabelProperty); } |
||||||
|
set { SetValue(LabelProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
private OnConfigChange onConfigChange; |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
Button button = GetTemplateChild("PART_Button") as Button; |
||||||
|
button.Click += delegate(object sender, RoutedEventArgs e) { Select(); }; |
||||||
|
|
||||||
|
onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { |
||||||
|
string CurrentPreset = ConfigPresets.GetPreset(args.controller[args.FirstController].config); |
||||||
|
Selected = CurrentPreset == Type; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private void Select() |
||||||
|
{ |
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
{ |
||||||
|
SMX.SMXConfig config; |
||||||
|
if(!SMX.SMX.GetConfig(pad, out config)) |
||||||
|
continue; |
||||||
|
|
||||||
|
ConfigPresets.SetPreset(Type, ref config); |
||||||
|
Console.WriteLine("PresetButton::Select (" + Type + "): " + |
||||||
|
config.panelThreshold1Low + ", " + config.panelThreshold4Low + ", " + config.panelThreshold7Low + ", " + config.panelThreshold2Low); |
||||||
|
SMX.SMX.SetConfig(pad, config); |
||||||
|
} |
||||||
|
CurrentSMXDevice.singleton.FireConfigurationChanged(this); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public class PresetWidget: Control |
||||||
|
{ |
||||||
|
public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type", |
||||||
|
typeof(string), typeof(PresetWidget), new FrameworkPropertyMetadata("")); |
||||||
|
public string Type { |
||||||
|
get { return (string) GetValue(TypeProperty); } |
||||||
|
set { SetValue(TypeProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register("Description", |
||||||
|
typeof(string), typeof(PresetWidget), new FrameworkPropertyMetadata("")); |
||||||
|
public string Description { |
||||||
|
get { return (string) GetValue(DescriptionProperty); } |
||||||
|
set { SetValue(DescriptionProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly DependencyProperty LabelProperty = DependencyProperty.Register("Label", |
||||||
|
typeof(string), typeof(PresetWidget), new FrameworkPropertyMetadata("")); |
||||||
|
public string Label { |
||||||
|
get { return (string) GetValue(LabelProperty); } |
||||||
|
set { SetValue(LabelProperty, value); } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public class PanelButton: ToggleButton |
||||||
|
{ |
||||||
|
public static readonly DependencyProperty ButtonProperty = DependencyProperty.RegisterAttached("Button", |
||||||
|
typeof(string), typeof(PanelButton), new FrameworkPropertyMetadata(null)); |
||||||
|
|
||||||
|
public string Button { |
||||||
|
get { return (string) this.GetValue(ButtonProperty); } |
||||||
|
set { this.SetValue(ButtonProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
protected override void OnIsPressedChanged(DependencyPropertyChangedEventArgs e) |
||||||
|
{ |
||||||
|
base.OnIsPressedChanged(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// A base class for buttons used to select a panel to work with. |
||||||
|
public class PanelSelectButton: Button |
||||||
|
{ |
||||||
|
// Which panel this is (P1 0-8, P2 9-17): |
||||||
|
public static readonly DependencyProperty PanelProperty = DependencyProperty.RegisterAttached("Panel", |
||||||
|
typeof(int), typeof(PanelSelectButton), new FrameworkPropertyMetadata(0, RefreshIsSelectedCallback)); |
||||||
|
|
||||||
|
public int Panel { |
||||||
|
get { return (int) this.GetValue(PanelProperty); } |
||||||
|
set { this.SetValue(PanelProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
// Which panel is currently selected. If this == Panel, this panel is selected. This is |
||||||
|
// bound to ColorPicker.SelectedPanel, so changing this changes which panel the picker edits. |
||||||
|
public static readonly DependencyProperty SelectedPanelProperty = DependencyProperty.RegisterAttached("SelectedPanel", |
||||||
|
typeof(int), typeof(PanelSelectButton), new FrameworkPropertyMetadata(0, RefreshIsSelectedCallback)); |
||||||
|
|
||||||
|
public int SelectedPanel { |
||||||
|
get { return (int) this.GetValue(SelectedPanelProperty); } |
||||||
|
set { this.SetValue(SelectedPanelProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
// Whether this panel is selected. This is true if Panel == SelectedPanel. |
||||||
|
public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.RegisterAttached("IsSelected", |
||||||
|
typeof(bool), typeof(PanelSelectButton), new FrameworkPropertyMetadata(false)); |
||||||
|
|
||||||
|
public bool IsSelected { |
||||||
|
get { return (bool) this.GetValue(IsSelectedProperty); } |
||||||
|
set { this.SetValue(IsSelectedProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
// When Panel or SelectedPanel change, update IsSelected. |
||||||
|
private static void RefreshIsSelectedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args) |
||||||
|
{ |
||||||
|
PanelSelectButton self = target as PanelSelectButton; |
||||||
|
self.RefreshIsSelected(); |
||||||
|
} |
||||||
|
|
||||||
|
private void RefreshIsSelected() |
||||||
|
{ |
||||||
|
IsSelected = Panel == SelectedPanel; |
||||||
|
} |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
RefreshIsSelected(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This button shows the color configured for that panel, and chooses which color is being |
||||||
|
// edited by the ColorPicker. |
||||||
|
public class PanelColorButton: PanelSelectButton |
||||||
|
{ |
||||||
|
// The color configured for this panel: |
||||||
|
public static readonly DependencyProperty PanelColorProperty = DependencyProperty.RegisterAttached("PanelColor", |
||||||
|
typeof(SolidColorBrush), typeof(PanelColorButton), new FrameworkPropertyMetadata(new SolidColorBrush())); |
||||||
|
|
||||||
|
public SolidColorBrush PanelColor { |
||||||
|
get { return (SolidColorBrush) this.GetValue(PanelColorProperty); } |
||||||
|
set { this.SetValue(PanelColorProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
OnConfigChange onConfigChange; |
||||||
|
onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { |
||||||
|
int pad = Panel < 9? 0:1; |
||||||
|
LoadUIFromConfig(args.controller[pad].config); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void OnClick() |
||||||
|
{ |
||||||
|
base.OnClick(); |
||||||
|
|
||||||
|
// Select this panel. |
||||||
|
SelectedPanel = Panel; |
||||||
|
|
||||||
|
// Fire configuration changed, so the color slider updates to show this panel. |
||||||
|
CurrentSMXDevice.singleton.FireConfigurationChanged(this); |
||||||
|
} |
||||||
|
|
||||||
|
// Set PanelColor. This widget doesn't change the color, it only reflects the current configuration. |
||||||
|
private void LoadUIFromConfig(SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
int PanelIndex = Panel % 9; |
||||||
|
|
||||||
|
// Hide color buttons for disabled panels. |
||||||
|
bool[] enabledPanels = config.GetEnabledPanels(); |
||||||
|
Visibility = enabledPanels[PanelIndex]? Visibility.Visible:Visibility.Hidden; |
||||||
|
|
||||||
|
Color rgb = ColorPicker.UnscaleColor(Color.FromRgb( |
||||||
|
config.stepColor[PanelIndex*3+0], |
||||||
|
config.stepColor[PanelIndex*3+1], |
||||||
|
config.stepColor[PanelIndex*3+2])); |
||||||
|
PanelColor = new SolidColorBrush(rgb); |
||||||
|
} |
||||||
|
|
||||||
|
// Return #RRGGBB for the color set on this panel. |
||||||
|
private string GetColorString() |
||||||
|
{ |
||||||
|
// WPF's Color.ToString() returns #AARRGGBB, which is just wrong. Alpha is always |
||||||
|
// last in HTML color codes. We don't need alpha, so just strip it off. |
||||||
|
return "#" + PanelColor.Color.ToString().Substring(3); |
||||||
|
} |
||||||
|
|
||||||
|
// Parse #RRGGBB and return a Color, or white if the string isn't in the correct format. |
||||||
|
private static Color ParseColorString(string s) |
||||||
|
{ |
||||||
|
// We only expect "#RRGGBB". |
||||||
|
if(s.Length != 7 || !s.StartsWith("#")) |
||||||
|
return Color.FromRgb(255,255,255); |
||||||
|
|
||||||
|
try { |
||||||
|
return (Color) ColorConverter.ConvertFromString(s); |
||||||
|
} |
||||||
|
catch(System.FormatException) |
||||||
|
{ |
||||||
|
return Color.FromRgb(255,255,255); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Point MouseDownPosition; |
||||||
|
|
||||||
|
protected override void OnMouseDown(MouseButtonEventArgs e) |
||||||
|
{ |
||||||
|
MouseDownPosition = e.GetPosition(null); |
||||||
|
base.OnMouseDown(e); |
||||||
|
} |
||||||
|
|
||||||
|
// Handle initiating drag. |
||||||
|
protected override void OnMouseMove(MouseEventArgs e) |
||||||
|
{ |
||||||
|
if(e.LeftButton == MouseButtonState.Pressed) |
||||||
|
{ |
||||||
|
Point position = e.GetPosition(null); |
||||||
|
|
||||||
|
// Why do we have to handle drag thresholding manually? This is the platform's job. |
||||||
|
// If we don't do this, clicks won't work at all. |
||||||
|
if (Math.Abs(position.X - MouseDownPosition.X) >= SystemParameters.MinimumHorizontalDragDistance || |
||||||
|
Math.Abs(position.Y - MouseDownPosition.Y) >= SystemParameters.MinimumVerticalDragDistance) |
||||||
|
{ |
||||||
|
DragDrop.DoDragDrop(this, GetColorString(), DragDropEffects.Copy); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
base.OnMouseMove(e); |
||||||
|
} |
||||||
|
|
||||||
|
private bool HandleDrop(DragEventArgs e) |
||||||
|
{ |
||||||
|
PanelColorButton Button = e.Source as PanelColorButton; |
||||||
|
if(Button == null) |
||||||
|
return false; |
||||||
|
|
||||||
|
// A color is being dropped from another button. Don't just update our color, since |
||||||
|
// that will just change the button color and not actually apply it. |
||||||
|
DataObject data = e.Data as DataObject; |
||||||
|
if(data == null) |
||||||
|
return false; |
||||||
|
|
||||||
|
// Parse the color being dragged onto us. |
||||||
|
Color color = ParseColorString(data.GetData(typeof(string)) as string); |
||||||
|
|
||||||
|
// Update the panel color. |
||||||
|
int PanelIndex = Panel % 9; |
||||||
|
int Pad = Panel < 9? 0:1; |
||||||
|
SMX.SMXConfig config; |
||||||
|
if(!SMX.SMX.GetConfig(Pad, out config)) |
||||||
|
return false; |
||||||
|
|
||||||
|
// Light colors are 8-bit values, but we only use values between 0-170. Higher values |
||||||
|
// don't make the panel noticeably brighter, and just draw more power. |
||||||
|
config.stepColor[PanelIndex*3+0] = ColorPicker.ScaleColor(color.R); |
||||||
|
config.stepColor[PanelIndex*3+1] = ColorPicker.ScaleColor(color.G); |
||||||
|
config.stepColor[PanelIndex*3+2] = ColorPicker.ScaleColor(color.B); |
||||||
|
|
||||||
|
SMX.SMX.SetConfig(Pad, config); |
||||||
|
CurrentSMXDevice.singleton.FireConfigurationChanged(this); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
protected override void OnDrop(DragEventArgs e) |
||||||
|
{ |
||||||
|
if(!HandleDrop(e)) |
||||||
|
base.OnDrop(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This is a Slider class with some added helpers. |
||||||
|
public class Slider2: Slider |
||||||
|
{ |
||||||
|
public delegate void DragEvent(); |
||||||
|
public event DragEvent StartedDragging, StoppedDragging; |
||||||
|
|
||||||
|
protected Thumb Thumb; |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
Track track = Template.FindName("PART_Track", this) as Track; |
||||||
|
Thumb = track.Thumb; |
||||||
|
} |
||||||
|
|
||||||
|
// How are there no events for this? |
||||||
|
protected override void OnThumbDragStarted(DragStartedEventArgs e) |
||||||
|
{ |
||||||
|
base.OnThumbDragStarted(e); |
||||||
|
StartedDragging?.Invoke(); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void OnThumbDragCompleted(DragCompletedEventArgs e) |
||||||
|
{ |
||||||
|
base.OnThumbDragCompleted(e); |
||||||
|
StoppedDragging?.Invoke(); |
||||||
|
} |
||||||
|
|
||||||
|
public Slider2() |
||||||
|
{ |
||||||
|
// Fix the slider not dragging after clicking outside the thumb. |
||||||
|
// http://stackoverflow.com/a/30575638/136829 |
||||||
|
bool clickedInSlider = false; |
||||||
|
MouseMove += delegate(object sender, MouseEventArgs args) |
||||||
|
{ |
||||||
|
if(args.LeftButton == MouseButtonState.Released || !clickedInSlider || Thumb.IsDragging) |
||||||
|
return; |
||||||
|
|
||||||
|
Thumb.RaiseEvent(new MouseButtonEventArgs(args.MouseDevice, args.Timestamp, MouseButton.Left) |
||||||
|
{ |
||||||
|
RoutedEvent = UIElement.MouseLeftButtonDownEvent, |
||||||
|
Source = args.Source, |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
AddHandler(UIElement.PreviewMouseLeftButtonDownEvent, new RoutedEventHandler((sender, args) => |
||||||
|
{ |
||||||
|
clickedInSlider = true; |
||||||
|
}), true); |
||||||
|
|
||||||
|
AddHandler(UIElement.PreviewMouseLeftButtonUpEvent, new RoutedEventHandler((sender, args) => |
||||||
|
{ |
||||||
|
clickedInSlider = false; |
||||||
|
}), true); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// This is the Slider inside a ColorPicker. |
||||||
|
public class ColorPickerSlider: Slider2 |
||||||
|
{ |
||||||
|
public ColorPickerSlider() |
||||||
|
{ |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
public class ColorPicker: Control |
||||||
|
{ |
||||||
|
// Which panel is currently selected: |
||||||
|
public static readonly DependencyProperty SelectedPanelProperty = DependencyProperty.Register("SelectedPanel", |
||||||
|
typeof(int), typeof(ColorPicker), new FrameworkPropertyMetadata(0)); |
||||||
|
|
||||||
|
public int SelectedPanel { |
||||||
|
get { return (int) this.GetValue(SelectedPanelProperty); } |
||||||
|
set { this.SetValue(SelectedPanelProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
ColorPickerSlider HueSlider; |
||||||
|
public delegate void Event(); |
||||||
|
|
||||||
|
public event Event StartedDragging, StoppedDragging; |
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
HueSlider = GetTemplateChild("HueSlider") as ColorPickerSlider; |
||||||
|
HueSlider.ValueChanged += delegate(object sender, RoutedPropertyChangedEventArgs<double> e) { |
||||||
|
SaveToConfig(); |
||||||
|
}; |
||||||
|
|
||||||
|
HueSlider.StartedDragging += delegate() { StartedDragging?.Invoke(); }; |
||||||
|
HueSlider.StoppedDragging += delegate() { StoppedDragging?.Invoke(); }; |
||||||
|
|
||||||
|
DoubleCollection ticks = new DoubleCollection(); |
||||||
|
// Add a tick at the minimum value, which is a negative value. This is the |
||||||
|
// tick for white. |
||||||
|
ticks.Add(HueSlider.Minimum); |
||||||
|
|
||||||
|
// Add a tick for 0-359. Don't add 360, since that's the same as 0. |
||||||
|
for(int i = 0; i < 360; ++i) |
||||||
|
ticks.Add(i); |
||||||
|
HueSlider.Ticks = ticks; |
||||||
|
|
||||||
|
OnConfigChange onConfigChange; |
||||||
|
onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { |
||||||
|
int pad = SelectedPanel < 9? 0:1; |
||||||
|
LoadUIFromConfig(args.controller[pad].config); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// Light values are actually in the range 0-170 and not 0-255, since higher values aren't |
||||||
|
// any brighter and just draw more power. The auto-lighting colors that we're configuring |
||||||
|
// need to be scaled to this range too, but show full range colors in the UI. |
||||||
|
readonly static double LightsScaleFactor = 0.666666f; |
||||||
|
static public Byte ScaleColor(Byte c) { return (Byte) (c * LightsScaleFactor); } |
||||||
|
static public Byte UnscaleColor(Byte c) { return (Byte) Math.Min(255, c / LightsScaleFactor); } |
||||||
|
|
||||||
|
static public Color ScaleColor(Color c) |
||||||
|
{ |
||||||
|
return Color.FromRgb( |
||||||
|
ColorPicker.ScaleColor(c.R), |
||||||
|
ColorPicker.ScaleColor(c.G), |
||||||
|
ColorPicker.ScaleColor(c.B)); |
||||||
|
} |
||||||
|
|
||||||
|
static public Color UnscaleColor(Color c) |
||||||
|
{ |
||||||
|
return Color.FromRgb( |
||||||
|
ColorPicker.UnscaleColor(c.R), |
||||||
|
ColorPicker.UnscaleColor(c.G), |
||||||
|
ColorPicker.UnscaleColor(c.B)); |
||||||
|
} |
||||||
|
|
||||||
|
private void SaveToConfig() |
||||||
|
{ |
||||||
|
if(UpdatingUI) |
||||||
|
return; |
||||||
|
|
||||||
|
// Apply the change and save it to the device. |
||||||
|
int pad = SelectedPanel < 9? 0:1; |
||||||
|
SMX.SMXConfig config; |
||||||
|
if(!SMX.SMX.GetConfig(pad, out config)) |
||||||
|
return; |
||||||
|
|
||||||
|
Color color = Helpers.FromHSV(HueSlider.Value, 1, 1); |
||||||
|
|
||||||
|
// If we're set to the minimum value, use white instead. |
||||||
|
if(HueSlider.Value == HueSlider.Minimum) |
||||||
|
color = Color.FromRgb(255,255,255); |
||||||
|
|
||||||
|
// Light colors are 8-bit values, but we only use values between 0-170. Higher values |
||||||
|
// don't make the panel noticeably brighter, and just draw more power. |
||||||
|
int PanelIndex = SelectedPanel % 9; |
||||||
|
config.stepColor[PanelIndex*3+0] = ScaleColor(color.R); |
||||||
|
config.stepColor[PanelIndex*3+1] = ScaleColor(color.G); |
||||||
|
config.stepColor[PanelIndex*3+2] = ScaleColor(color.B); |
||||||
|
|
||||||
|
SMX.SMX.SetConfig(pad, config); |
||||||
|
CurrentSMXDevice.singleton.FireConfigurationChanged(this); |
||||||
|
} |
||||||
|
|
||||||
|
bool UpdatingUI = false; |
||||||
|
private void LoadUIFromConfig(SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
// Make sure SaveToConfig doesn't treat these as the user changing values. |
||||||
|
UpdatingUI = true; |
||||||
|
|
||||||
|
// Reverse the scaling we applied in SaveToConfig. |
||||||
|
int PanelIndex = SelectedPanel % 9; |
||||||
|
Color rgb = Color.FromRgb( |
||||||
|
UnscaleColor(config.stepColor[PanelIndex*3+0]), |
||||||
|
UnscaleColor(config.stepColor[PanelIndex*3+1]), |
||||||
|
UnscaleColor(config.stepColor[PanelIndex*3+2])); |
||||||
|
double h, s, v; |
||||||
|
Helpers.ToHSV(rgb, out h, out s, out v); |
||||||
|
|
||||||
|
// Check for white. Since the conversion through LightsScaleFactor may not round trip |
||||||
|
// back to exactly #FFFFFF, give some room for error in the value (brightness). |
||||||
|
if(s <= 0.001 && v >= .90) |
||||||
|
{ |
||||||
|
// This is white, so set it to the white block at the left edge of the slider. |
||||||
|
HueSlider.Value = HueSlider.Minimum; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
HueSlider.Value = h; |
||||||
|
} |
||||||
|
|
||||||
|
UpdatingUI = false; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// This widget selects which panels are enabled. We only show one of these for both pads. |
||||||
|
class PanelSelector: Control |
||||||
|
{ |
||||||
|
PanelButton[] EnabledPanelButtons; |
||||||
|
OnConfigChange onConfigChange; |
||||||
|
|
||||||
|
public override void OnApplyTemplate() |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(); |
||||||
|
|
||||||
|
int[] PanelToIndex = new int[] { |
||||||
|
7, 8, 9, |
||||||
|
4, 5, 6, |
||||||
|
1, 2, 3, |
||||||
|
}; |
||||||
|
|
||||||
|
EnabledPanelButtons = new PanelButton[9]; |
||||||
|
for(int i = 0; i < 9; ++i) |
||||||
|
EnabledPanelButtons[i] = GetTemplateChild("EnablePanel" + PanelToIndex[i]) as PanelButton; |
||||||
|
|
||||||
|
foreach(PanelButton button in EnabledPanelButtons) |
||||||
|
button.Click += EnabledPanelButtonClicked; |
||||||
|
|
||||||
|
onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { |
||||||
|
LoadUIFromConfig(args.controller[args.FirstController].config); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private void LoadUIFromConfig(SMX.SMXConfig config) |
||||||
|
{ |
||||||
|
// The firmware configuration allows disabling each of the four sensors in a panel |
||||||
|
// individually, but currently we only have a UI for toggling the whole sensor. Taking |
||||||
|
// individual sensors out isn't recommended. |
||||||
|
bool[] enabledPanels = { |
||||||
|
(config.enabledSensors[0] & 0xF0) != 0, |
||||||
|
(config.enabledSensors[0] & 0x0F) != 0, |
||||||
|
(config.enabledSensors[1] & 0xF0) != 0, |
||||||
|
(config.enabledSensors[1] & 0x0F) != 0, |
||||||
|
(config.enabledSensors[2] & 0xF0) != 0, |
||||||
|
(config.enabledSensors[2] & 0x0F) != 0, |
||||||
|
(config.enabledSensors[3] & 0xF0) != 0, |
||||||
|
(config.enabledSensors[3] & 0x0F) != 0, |
||||||
|
(config.enabledSensors[4] & 0xF0) != 0, |
||||||
|
}; |
||||||
|
|
||||||
|
for(int i = 0; i < 9; ++i) |
||||||
|
EnabledPanelButtons[i].IsChecked = enabledPanels[i]; |
||||||
|
} |
||||||
|
|
||||||
|
private int GetIndexFromButton(object sender) |
||||||
|
{ |
||||||
|
for(int i = 0; i < 9; i++) |
||||||
|
{ |
||||||
|
if(sender == EnabledPanelButtons[i]) |
||||||
|
return i; |
||||||
|
} |
||||||
|
|
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
private void EnabledPanelButtonClicked(object sender, EventArgs e) |
||||||
|
{ |
||||||
|
// One of the panel buttons on the panel toggle UI was clicked. Toggle the |
||||||
|
// panel. |
||||||
|
int button = GetIndexFromButton(sender); |
||||||
|
Console.WriteLine("Clicked " + button); |
||||||
|
|
||||||
|
// Set the enabled sensor mask on both pads to the state of the UI. |
||||||
|
for(int pad = 0; pad < 2; ++pad) |
||||||
|
{ |
||||||
|
SMX.SMXConfig config; |
||||||
|
if(!SMX.SMX.GetConfig(pad, out config)) |
||||||
|
continue; |
||||||
|
|
||||||
|
// This could be done algorithmically, but this is clearer. |
||||||
|
int[] PanelButtonToSensorIndex = { |
||||||
|
0, 0, 1, 1, 2, 2, 3, 3, 4 |
||||||
|
}; |
||||||
|
byte[] PanelButtonToSensorMask = { |
||||||
|
0xF0, 0x0F, |
||||||
|
0xF0, 0x0F, |
||||||
|
0xF0, 0x0F, |
||||||
|
0xF0, 0x0F, |
||||||
|
0xF0, |
||||||
|
}; |
||||||
|
for(int i = 0; i < 5; ++i) |
||||||
|
config.enabledSensors[i] = 0; |
||||||
|
|
||||||
|
for(int Panel = 0; Panel < 9; ++Panel) |
||||||
|
{ |
||||||
|
int index = PanelButtonToSensorIndex[Panel]; |
||||||
|
byte mask = PanelButtonToSensorMask[Panel]; |
||||||
|
if(EnabledPanelButtons[Panel].IsChecked == true) |
||||||
|
config.enabledSensors[index] |= (byte) mask; |
||||||
|
} |
||||||
|
|
||||||
|
SMX.SMX.SetConfig(pad, config); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
public class FrameImage: Image |
||||||
|
{ |
||||||
|
// The source image. Changing this after load isn't supported. |
||||||
|
public static readonly DependencyProperty ImageProperty = DependencyProperty.Register("Image", |
||||||
|
typeof(BitmapSource), typeof(FrameImage), new FrameworkPropertyMetadata(null, ImageChangedCallback)); |
||||||
|
|
||||||
|
public BitmapSource Image { |
||||||
|
get { return (BitmapSource) this.GetValue(ImageProperty); } |
||||||
|
set { this.SetValue(ImageProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
// Which frame is currently displayed: |
||||||
|
public static readonly DependencyProperty FrameProperty = DependencyProperty.Register("Frame", |
||||||
|
typeof(int), typeof(FrameImage), new FrameworkPropertyMetadata(0, FrameChangedCallback)); |
||||||
|
|
||||||
|
public int Frame { |
||||||
|
get { return (int) this.GetValue(FrameProperty); } |
||||||
|
set { this.SetValue(FrameProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly DependencyProperty FramesXProperty = DependencyProperty.Register("FramesX", |
||||||
|
typeof(int), typeof(FrameImage), new FrameworkPropertyMetadata(0, ImageChangedCallback)); |
||||||
|
|
||||||
|
public int FramesX { |
||||||
|
get { return (int) this.GetValue(FramesXProperty); } |
||||||
|
set { this.SetValue(FramesXProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
private static void ImageChangedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args) |
||||||
|
{ |
||||||
|
FrameImage self = target as FrameImage; |
||||||
|
self.Load(); |
||||||
|
} |
||||||
|
|
||||||
|
private static void FrameChangedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args) |
||||||
|
{ |
||||||
|
FrameImage self = target as FrameImage; |
||||||
|
self.Refresh(); |
||||||
|
} |
||||||
|
|
||||||
|
private BitmapSource[] ImageFrames; |
||||||
|
|
||||||
|
private void Load() |
||||||
|
{ |
||||||
|
if(Image == null || FramesX == 0) |
||||||
|
{ |
||||||
|
ImageFrames = null; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Split the image into frames. |
||||||
|
int FrameWidth = Image.PixelWidth / FramesX; |
||||||
|
int FrameHeight = Image.PixelHeight; |
||||||
|
ImageFrames = new BitmapSource[FramesX]; |
||||||
|
for(int i = 0; i < FramesX; ++i) |
||||||
|
ImageFrames[i] = new CroppedBitmap(Image, new Int32Rect(FrameWidth*i, 0, FrameWidth, FrameHeight)); |
||||||
|
|
||||||
|
Refresh(); |
||||||
|
} |
||||||
|
|
||||||
|
private void Refresh() |
||||||
|
{ |
||||||
|
if(ImageFrames == null || Frame >= ImageFrames.Length) |
||||||
|
{ |
||||||
|
this.Source = null; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.Source = ImageFrames[Frame]; |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
InstallSMXConfig.exe |
@ -0,0 +1,106 @@ |
|||||||
|
!include "MUI2.nsh" |
||||||
|
|
||||||
|
Name "StepManiaX Platform" |
||||||
|
OutFile "SMXConfigInstaller.exe" |
||||||
|
|
||||||
|
!define MUI_ICON "..\window icon.ico" |
||||||
|
|
||||||
|
InstallDir "$PROGRAMFILES32\SMXConfig" |
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM |
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES |
||||||
|
;!insertmacro MUI_PAGE_DIRECTORY |
||||||
|
|
||||||
|
Function InstallNetRuntime |
||||||
|
# Check if .NET 4.5.2 is installed. https://msdn.microsoft.com/en-us/library/hh925568(v=vs.110).aspx |
||||||
|
Var /Global Net4Version |
||||||
|
ReadRegDWORD $Net4Version HKLM "SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" "Release" |
||||||
|
IntCmp $Net4Version 379893 already_installed do_install already_installed |
||||||
|
|
||||||
|
already_installed: |
||||||
|
DetailPrint ".NET runtime 4.5.2 already installed." |
||||||
|
return |
||||||
|
|
||||||
|
do_install: |
||||||
|
|
||||||
|
# Download the runtime. |
||||||
|
NSISdl::download "https://go.microsoft.com/fwlink/?LinkId=397708" "$TEMP\NET452Installer.exe" |
||||||
|
Var /GLOBAL download_result |
||||||
|
Pop $download_result |
||||||
|
DetailPrint "$download_result" |
||||||
|
StrCmp $download_result success download_successful |
||||||
|
|
||||||
|
MessageBox MB_OK|MB_ICONEXCLAMATION "The .NET 4.5.2 runtime couldn't be downloaded." |
||||||
|
return |
||||||
|
|
||||||
|
download_successful: |
||||||
|
|
||||||
|
# Run the installer. |
||||||
|
# We can run this without opening the install dialog like this, but this runtime can take a |
||||||
|
# while to install and it makes it look like the installation has stalled. |
||||||
|
# ExecWait '"$TEMP\NET452Installer.exe" /q /norestart /c:"install /q"' |
||||||
|
ExecWait '"$TEMP\NET452Installer.exe" /passive /norestart /c:"install"' |
||||||
|
FunctionEnd |
||||||
|
|
||||||
|
Function InstallMSVCRuntime |
||||||
|
# Check if the runtime is already installed. |
||||||
|
Var /Global MSVCVersion1 |
||||||
|
Var /Global MSVCVersion2 |
||||||
|
GetDLLVersion "$sysdir\MSVCP140.dll" $MSVCVersion1 $MSVCVersion2 |
||||||
|
StrCmp $MSVCVersion1 "" do_install |
||||||
|
|
||||||
|
DetailPrint "MSVC runtime already installed." |
||||||
|
return |
||||||
|
|
||||||
|
do_install: |
||||||
|
|
||||||
|
DetailPrint "Installing MSVC runtime" |
||||||
|
|
||||||
|
# Download the runtime. |
||||||
|
NSISdl::download "https://go.microsoft.com/fwlink/?LinkId=615459" "$TEMP\vcredist_x86.exe" |
||||||
|
Var /GLOBAL download_result_2 |
||||||
|
Pop $download_result_2 |
||||||
|
DetailPrint "$download_result_2" |
||||||
|
StrCmp $download_result_2 success download_successful |
||||||
|
|
||||||
|
MessageBox MB_OK|MB_ICONEXCLAMATION "The MSVC runtime couldn't be downloaded." |
||||||
|
return |
||||||
|
|
||||||
|
download_successful: |
||||||
|
|
||||||
|
# Run the installer. |
||||||
|
ExecWait '"$TEMP\vcredist_x86.exe" /passive /norestart' |
||||||
|
FunctionEnd |
||||||
|
|
||||||
|
Page directory |
||||||
|
Page instfiles |
||||||
|
|
||||||
|
Section |
||||||
|
Call InstallNetRuntime |
||||||
|
Call InstallMSVCRuntime |
||||||
|
|
||||||
|
SetOutPath $INSTDIR |
||||||
|
File "..\..\out\SMX.dll" |
||||||
|
File "..\..\out\SMXConfig.exe" |
||||||
|
|
||||||
|
CreateShortCut "$SMPROGRAMS\StepManiaX Platform.lnk" "$INSTDIR\SMXConfig.exe" |
||||||
|
WriteUninstaller $INSTDIR\uninstall.exe |
||||||
|
|
||||||
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\StepManiaX Platform" \ |
||||||
|
"DisplayName" "StepManiaX Platform" |
||||||
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\StepManiaX Platform" \ |
||||||
|
"Publisher" "Step Revolution" |
||||||
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\StepManiaX Platform" \ |
||||||
|
"DisplayIcon" "$INSTDIR\SMXConfig.exe" |
||||||
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\StepManiaX Platform" \ |
||||||
|
"UninstallString" "$\"$INSTDIR\uninstall.exe$\"" |
||||||
|
SectionEnd |
||||||
|
|
||||||
|
Section "Uninstall" |
||||||
|
Delete $INSTDIR\SMX.dll |
||||||
|
Delete $INSTDIR\SMXConfig.exe |
||||||
|
Delete $INSTDIR\uninstall.exe |
||||||
|
rmdir $INSTDIR |
||||||
|
Delete "$SMPROGRAMS\StepManiaX Platform.lnk" |
||||||
|
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\StepManiaX Platform" |
||||||
|
SectionEnd |
||||||
|
|
@ -0,0 +1,3 @@ |
|||||||
|
@echo off |
||||||
|
"C:\Program Files (x86)\NSIS\makensis.exe" SMX.nsi |
||||||
|
pause |
After Width: | Height: | Size: 37 KiB |