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