Compare commits
No commits in common. '7ea04b949990d516fe4afc8c7285329373821651' and '5979eddc21a09ae71b7b3ddb31fd14078f2a7c2c' have entirely different histories.
7ea04b9499
...
5979eddc21
@ -0,0 +1,8 @@ |
||||
root=true |
||||
|
||||
[*] |
||||
indent_style = space |
||||
|
||||
[*.{cpp,cs,h}] |
||||
indent_size = 4 |
||||
|
@ -1,63 +0,0 @@ |
||||
############################################################################### |
||||
# Set default behavior to automatically normalize line endings. |
||||
############################################################################### |
||||
* text=auto |
||||
|
||||
############################################################################### |
||||
# Set default behavior for command prompt diff. |
||||
# |
||||
# This is need for earlier builds of msysgit that does not have it on by |
||||
# default for csharp files. |
||||
# Note: This is only used by command line |
||||
############################################################################### |
||||
#*.cs diff=csharp |
||||
|
||||
############################################################################### |
||||
# Set the merge driver for project and solution files |
||||
# |
||||
# Merging from the command prompt will add diff markers to the files if there |
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS |
||||
# the diff markers are never inserted). Diff markers may cause the following |
||||
# file extensions to fail to load in VS. An alternative would be to treat |
||||
# these files as binary and thus will always conflict and require user |
||||
# intervention with every merge. To do so, just uncomment the entries below |
||||
############################################################################### |
||||
#*.sln merge=binary |
||||
#*.csproj merge=binary |
||||
#*.vbproj merge=binary |
||||
#*.vcxproj merge=binary |
||||
#*.vcproj merge=binary |
||||
#*.dbproj merge=binary |
||||
#*.fsproj merge=binary |
||||
#*.lsproj merge=binary |
||||
#*.wixproj merge=binary |
||||
#*.modelproj merge=binary |
||||
#*.sqlproj merge=binary |
||||
#*.wwaproj merge=binary |
||||
|
||||
############################################################################### |
||||
# behavior for image files |
||||
# |
||||
# image files are treated as binary by default. |
||||
############################################################################### |
||||
#*.jpg binary |
||||
#*.png binary |
||||
#*.gif binary |
||||
|
||||
############################################################################### |
||||
# diff behavior for common document formats |
||||
# |
||||
# Convert binary document formats to text before diffing them. This feature |
||||
# is only available from the command line. Turn it on by uncommenting the |
||||
# entries below. |
||||
############################################################################### |
||||
#*.doc diff=astextplain |
||||
#*.DOC diff=astextplain |
||||
#*.docx diff=astextplain |
||||
#*.DOCX diff=astextplain |
||||
#*.dot diff=astextplain |
||||
#*.DOT diff=astextplain |
||||
#*.pdf diff=astextplain |
||||
#*.PDF diff=astextplain |
||||
#*.rtf diff=astextplain |
||||
#*.RTF diff=astextplain |
@ -1,363 +1,5 @@ |
||||
## Ignore Visual Studio temporary files, build results, and |
||||
## files generated by popular Visual Studio add-ons. |
||||
## |
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore |
||||
|
||||
# User-specific files |
||||
*.rsuser |
||||
*.suo |
||||
.vs |
||||
*.user |
||||
*.userosscache |
||||
*.sln.docstates |
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio) |
||||
*.userprefs |
||||
|
||||
# Mono auto generated files |
||||
mono_crash.* |
||||
|
||||
# Build results |
||||
[Dd]ebug/ |
||||
[Dd]ebugPublic/ |
||||
[Rr]elease/ |
||||
[Rr]eleases/ |
||||
x64/ |
||||
x86/ |
||||
[Ww][Ii][Nn]32/ |
||||
[Aa][Rr][Mm]/ |
||||
[Aa][Rr][Mm]64/ |
||||
bld/ |
||||
[Bb]in/ |
||||
[Oo]bj/ |
||||
[Oo]ut/ |
||||
[Ll]og/ |
||||
[Ll]ogs/ |
||||
|
||||
# Visual Studio 2015/2017 cache/options directory |
||||
.vs/ |
||||
# Uncomment if you have tasks that create the project's static files in wwwroot |
||||
#wwwroot/ |
||||
|
||||
# Visual Studio 2017 auto generated files |
||||
Generated\ Files/ |
||||
|
||||
# MSTest test Results |
||||
[Tt]est[Rr]esult*/ |
||||
[Bb]uild[Ll]og.* |
||||
|
||||
# NUnit |
||||
*.VisualState.xml |
||||
TestResult.xml |
||||
nunit-*.xml |
||||
|
||||
# Build Results of an ATL Project |
||||
[Dd]ebugPS/ |
||||
[Rr]eleasePS/ |
||||
dlldata.c |
||||
|
||||
# Benchmark Results |
||||
BenchmarkDotNet.Artifacts/ |
||||
|
||||
# .NET Core |
||||
project.lock.json |
||||
project.fragment.lock.json |
||||
artifacts/ |
||||
|
||||
# ASP.NET Scaffolding |
||||
ScaffoldingReadMe.txt |
||||
|
||||
# StyleCop |
||||
StyleCopReport.xml |
||||
|
||||
# Files built by Visual Studio |
||||
*_i.c |
||||
*_p.c |
||||
*_h.h |
||||
*.ilk |
||||
*.meta |
||||
*.obj |
||||
*.iobj |
||||
*.pch |
||||
*.pdb |
||||
*.ipdb |
||||
*.pgc |
||||
*.pgd |
||||
*.rsp |
||||
*.sbr |
||||
*.tlb |
||||
*.tli |
||||
*.tlh |
||||
*.tmp |
||||
*.tmp_proj |
||||
*_wpftmp.csproj |
||||
*.log |
||||
*.vspscc |
||||
*.vssscc |
||||
.builds |
||||
*.pidb |
||||
*.svclog |
||||
*.scc |
||||
|
||||
# Chutzpah Test files |
||||
_Chutzpah* |
||||
|
||||
# Visual C++ cache files |
||||
ipch/ |
||||
*.aps |
||||
*.ncb |
||||
*.opendb |
||||
*.opensdf |
||||
*.sdf |
||||
*.cachefile |
||||
*.VC.db |
||||
*.VC.VC.opendb |
||||
|
||||
# Visual Studio profiler |
||||
*.psess |
||||
*.vsp |
||||
*.vspx |
||||
*.sap |
||||
|
||||
# Visual Studio Trace Files |
||||
*.e2e |
||||
|
||||
# TFS 2012 Local Workspace |
||||
$tf/ |
||||
|
||||
# Guidance Automation Toolkit |
||||
*.gpState |
||||
|
||||
# ReSharper is a .NET coding add-in |
||||
_ReSharper*/ |
||||
*.[Rr]e[Ss]harper |
||||
*.DotSettings.user |
||||
|
||||
# TeamCity is a build add-in |
||||
_TeamCity* |
||||
|
||||
# DotCover is a Code Coverage Tool |
||||
*.dotCover |
||||
|
||||
# AxoCover is a Code Coverage Tool |
||||
.axoCover/* |
||||
!.axoCover/settings.json |
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool |
||||
coverage*.json |
||||
coverage*.xml |
||||
coverage*.info |
||||
|
||||
# Visual Studio code coverage results |
||||
*.coverage |
||||
*.coveragexml |
||||
|
||||
# NCrunch |
||||
_NCrunch_* |
||||
.*crunch*.local.xml |
||||
nCrunchTemp_* |
||||
|
||||
# MightyMoose |
||||
*.mm.* |
||||
AutoTest.Net/ |
||||
|
||||
# Web workbench (sass) |
||||
.sass-cache/ |
||||
|
||||
# Installshield output folder |
||||
[Ee]xpress/ |
||||
|
||||
# DocProject is a documentation generator add-in |
||||
DocProject/buildhelp/ |
||||
DocProject/Help/*.HxT |
||||
DocProject/Help/*.HxC |
||||
DocProject/Help/*.hhc |
||||
DocProject/Help/*.hhk |
||||
DocProject/Help/*.hhp |
||||
DocProject/Help/Html2 |
||||
DocProject/Help/html |
||||
|
||||
# Click-Once directory |
||||
publish/ |
||||
|
||||
# Publish Web Output |
||||
*.[Pp]ublish.xml |
||||
*.azurePubxml |
||||
# Note: Comment the next line if you want to checkin your web deploy settings, |
||||
# but database connection strings (with potential passwords) will be unencrypted |
||||
*.pubxml |
||||
*.publishproj |
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to |
||||
# checkin your Azure Web App publish settings, but sensitive information contained |
||||
# in these scripts will be unencrypted |
||||
PublishScripts/ |
||||
|
||||
# NuGet Packages |
||||
*.nupkg |
||||
# NuGet Symbol Packages |
||||
*.snupkg |
||||
# The packages folder can be ignored because of Package Restore |
||||
**/[Pp]ackages/* |
||||
# except build/, which is used as an MSBuild target. |
||||
!**/[Pp]ackages/build/ |
||||
# Uncomment if necessary however generally it will be regenerated when needed |
||||
#!**/[Pp]ackages/repositories.config |
||||
# NuGet v3's project.json files produces more ignorable files |
||||
*.nuget.props |
||||
*.nuget.targets |
||||
|
||||
# Microsoft Azure Build Output |
||||
csx/ |
||||
*.build.csdef |
||||
|
||||
# Microsoft Azure Emulator |
||||
ecf/ |
||||
rcf/ |
||||
|
||||
# Windows Store app package directories and files |
||||
AppPackages/ |
||||
BundleArtifacts/ |
||||
Package.StoreAssociation.xml |
||||
_pkginfo.txt |
||||
*.appx |
||||
*.appxbundle |
||||
*.appxupload |
||||
|
||||
# Visual Studio cache files |
||||
# files ending in .cache can be ignored |
||||
*.[Cc]ache |
||||
# but keep track of directories ending in .cache |
||||
!?*.[Cc]ache/ |
||||
|
||||
# Others |
||||
ClientBin/ |
||||
~$* |
||||
*~ |
||||
*.dbmdl |
||||
*.dbproj.schemaview |
||||
*.jfm |
||||
*.pfx |
||||
*.publishsettings |
||||
orleans.codegen.cs |
||||
|
||||
# Including strong name files can present a security risk |
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424) |
||||
#*.snk |
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components |
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) |
||||
#bower_components/ |
||||
|
||||
# RIA/Silverlight projects |
||||
Generated_Code/ |
||||
|
||||
# Backup & report files from converting an old project file |
||||
# to a newer Visual Studio version. Backup files are not needed, |
||||
# because we have git ;-) |
||||
_UpgradeReport_Files/ |
||||
Backup*/ |
||||
UpgradeLog*.XML |
||||
UpgradeLog*.htm |
||||
ServiceFabricBackup/ |
||||
*.rptproj.bak |
||||
|
||||
# SQL Server files |
||||
*.mdf |
||||
*.ldf |
||||
*.ndf |
||||
|
||||
# Business Intelligence projects |
||||
*.rdl.data |
||||
*.bim.layout |
||||
*.bim_*.settings |
||||
*.rptproj.rsuser |
||||
*- [Bb]ackup.rdl |
||||
*- [Bb]ackup ([0-9]).rdl |
||||
*- [Bb]ackup ([0-9][0-9]).rdl |
||||
|
||||
# Microsoft Fakes |
||||
FakesAssemblies/ |
||||
|
||||
# GhostDoc plugin setting file |
||||
*.GhostDoc.xml |
||||
|
||||
# Node.js Tools for Visual Studio |
||||
.ntvs_analysis.dat |
||||
node_modules/ |
||||
|
||||
# Visual Studio 6 build log |
||||
*.plg |
||||
|
||||
# Visual Studio 6 workspace options file |
||||
*.opt |
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) |
||||
*.vbw |
||||
|
||||
# Visual Studio LightSwitch build output |
||||
**/*.HTMLClient/GeneratedArtifacts |
||||
**/*.DesktopClient/GeneratedArtifacts |
||||
**/*.DesktopClient/ModelManifest.xml |
||||
**/*.Server/GeneratedArtifacts |
||||
**/*.Server/ModelManifest.xml |
||||
_Pvt_Extensions |
||||
|
||||
# Paket dependency manager |
||||
.paket/paket.exe |
||||
paket-files/ |
||||
|
||||
# FAKE - F# Make |
||||
.fake/ |
||||
|
||||
# CodeRush personal settings |
||||
.cr/personal |
||||
|
||||
# Python Tools for Visual Studio (PTVS) |
||||
__pycache__/ |
||||
*.pyc |
||||
|
||||
# Cake - Uncomment if you are using it |
||||
# tools/** |
||||
# !tools/packages.config |
||||
|
||||
# Tabs Studio |
||||
*.tss |
||||
|
||||
# Telerik's JustMock configuration file |
||||
*.jmconfig |
||||
|
||||
# BizTalk build output |
||||
*.btp.cs |
||||
*.btm.cs |
||||
*.odx.cs |
||||
*.xsd.cs |
||||
|
||||
# OpenCover UI analysis results |
||||
OpenCover/ |
||||
|
||||
# Azure Stream Analytics local run output |
||||
ASALocalRun/ |
||||
|
||||
# MSBuild Binary and Structured Log |
||||
*.binlog |
||||
|
||||
# NVidia Nsight GPU debugger configuration file |
||||
*.nvuser |
||||
|
||||
# MFractors (Xamarin productivity tool) working folder |
||||
.mfractor/ |
||||
|
||||
# Local History for Visual Studio |
||||
.localhistory/ |
||||
|
||||
# BeatPulse healthcheck temp database |
||||
healthchecksdb |
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017 |
||||
MigrationBackup/ |
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder |
||||
.ionide/ |
||||
|
||||
# Fody - auto-generated XML schema |
||||
FodyWeavers.xsd |
||||
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,7 @@ |
||||
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. |
||||
|
||||
SDK support: [sdk@stepmaniax.com](mailto:sdk@stepmaniax.com) |
||||
|
@ -1,37 +0,0 @@ |
||||
#define OLC_PGE_APPLICATION |
||||
#include "olcPixelGameEngine.h" |
||||
|
||||
class Example : public olc::PixelGameEngine |
||||
{ |
||||
public: |
||||
Example() |
||||
{ |
||||
sAppName = "Example"; |
||||
} |
||||
|
||||
public: |
||||
bool OnUserCreate() override |
||||
{ |
||||
// Called once at the start, so create things here
|
||||
return true; |
||||
} |
||||
|
||||
bool OnUserUpdate(float fElapsedTime) override |
||||
{ |
||||
// called once per frame
|
||||
for (int x = 0; x < ScreenWidth(); x++) |
||||
for (int y = 0; y < ScreenHeight(); y++) |
||||
Draw(x, y, olc::Pixel(rand() % 255, rand() % 255, rand()% 255));
|
||||
return true; |
||||
} |
||||
}; |
||||
|
||||
|
||||
int main() |
||||
{ |
||||
Example demo; |
||||
if (demo.Construct(256, 240, 4, 4)) |
||||
demo.Start(); |
||||
|
||||
return 0; |
||||
} |
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,183 @@ |
||||
<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>. |
||||
<p> |
||||
SDK support: <a href=mailto:sdk@stepmaniax.com>sdk@stepmaniax.com</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>SMXSample</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_SetLights2</code>. |
||||
|
||||
<h2>Update notes</h2> |
||||
|
||||
2019-07-18-01: Added SMX_SetLights2. This is the same as SMX_SetLights, with an added |
||||
parameter to specify the size of the buffer. This must be used to control the Gen4 |
||||
pads which have additional LEDs. |
||||
|
||||
<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 sensors 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> |
||||
|
||||
(deprecated) |
||||
<p> |
||||
Equivalent to SMX_SetLights2(lightsData, 864). SMX_SetLights2 should be used instead. |
||||
|
||||
<h3 class=ref>void SMX_SetLights2(const char *lightsData, int lightDataSize);</h3> |
||||
Update the lights. Both pads are always updated together. lightsData is a list of 8-bit RGB |
||||
colors, one for each LED. |
||||
<p> |
||||
lightDataSize is the number of bytes in lightsData. This should be 1350 (2 pads * 9 panels * |
||||
25 lights * 3 RGB colors). For backwards-compatibility, this can also be 864. |
||||
<p> |
||||
25-LED panels have lights in the following order: |
||||
<p> |
||||
<pre> |
||||
00 01 02 03 |
||||
16 17 18 |
||||
04 05 06 07 |
||||
19 20 21 |
||||
08 09 10 11 |
||||
22 23 24 |
||||
12 13 14 15 |
||||
</pre> |
||||
<p> |
||||
|
||||
16-LED panels have the same layout, ignoring LEDs 16 and up. |
||||
<p> |
||||
Panels are in the following order: |
||||
<p> |
||||
<pre> |
||||
012 9AB |
||||
345 CDE |
||||
678 F01 |
||||
</pre> |
||||
|
||||
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. |
||||
<p> |
||||
For backwards compatibility, if lightDataSize is 864, the old 4x4-only order is used, |
||||
which simply omits lights 16-24. |
||||
|
||||
<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,57 @@ |
||||
#include "SMX.h" |
||||
#define OLC_PGE_APPLICATION |
||||
#include "olcPixelGameEngine.h" |
||||
using namespace olc; |
||||
|
||||
class SMX_PGE : public olc::PixelGameEngine |
||||
{ |
||||
public: |
||||
SMX_PGE() |
||||
{ |
||||
sAppName = "Example"; |
||||
} |
||||
|
||||
static void SMXStateChangedCallback(int pad, SMXUpdateCallbackReason reason, void *pUser) |
||||
{ |
||||
SMX_PGE *pSelf = (SMX_PGE*) 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)); |
||||
|
||||
} |
||||
|
||||
public: |
||||
bool OnUserCreate() override |
||||
{ |
||||
SMX_Start( SMXStateChangedCallback, this ); |
||||
// Called once at the start, so create things here
|
||||
return true; |
||||
} |
||||
|
||||
bool OnUserUpdate(float fElapsedTime) override |
||||
{ |
||||
// called once per frame
|
||||
for (int x = 0; x < ScreenWidth(); x++) |
||||
for (int y = 0; y < ScreenHeight(); y++) |
||||
Draw(x, y, olc::Pixel(rand() % 255, rand() % 255, rand()% 255));
|
||||
return true; |
||||
} |
||||
}; |
||||
|
||||
|
||||
int main() |
||||
{ |
||||
SMX_PGE demo; |
||||
if (demo.Construct(256, 240, 4, 4)) |
||||
demo.Start(); |
||||
|
||||
return 0; |
||||
} |
@ -0,0 +1,319 @@ |
||||
#ifndef SMX_H |
||||
#define SMX_H |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> // for offsetof |
||||
|
||||
#ifdef SMX_EXPORTS |
||||
#define SMX_API extern "C" __declspec(dllexport) |
||||
#else |
||||
#define SMX_API extern "C" __declspec(dllimport) |
||||
#endif |
||||
|
||||
struct SMXInfo; |
||||
struct SMXConfig; |
||||
enum SensorTestMode; |
||||
enum PanelTestMode; |
||||
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); |
||||
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.
|
||||
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); |
||||
SMX_API void SMX_SetLogCallback(SMXLogCallback callback); |
||||
|
||||
// Get info about a pad. Use this to detect which pads are currently connected.
|
||||
SMX_API void SMX_GetInfo(int pad, SMXInfo *info); |
||||
|
||||
// Get a mask of the currently pressed panels.
|
||||
SMX_API uint16_t SMX_GetInputState(int pad); |
||||
|
||||
// (deprecated) Equivalent to SMX_SetLights2(lightsData, 864).
|
||||
SMX_API void SMX_SetLights(const char lightData[864]); |
||||
|
||||
// Update the lights. Both pads are always updated together. lightData is a list of 8-bit RGB
|
||||
// colors, one for each LED.
|
||||
//
|
||||
// lightDataSize is the number of bytes in lightsData. This should be 1350 (2 pads * 9 panels *
|
||||
// 25 lights * 3 RGB colors). For backwards-compatibility, this can also be 864.
|
||||
//
|
||||
// Each panel has lights in the following order:
|
||||
//
|
||||
// 00 01 02 03
|
||||
// 16 17 18
|
||||
// 04 05 06 07
|
||||
// 19 20 21
|
||||
// 08 09 10 11
|
||||
// 22 23 24
|
||||
// 12 13 14 15
|
||||
//
|
||||
// Panels are in the following order:
|
||||
//
|
||||
// 012 9AB
|
||||
// 345 CDE
|
||||
// 678 F01
|
||||
//
|
||||
// With 18 panels, 25 LEDs per panel and 3 bytes per LED, each light update has 1350 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.
|
||||
//
|
||||
// For backwards compatibility, if lightDataSize is 864, the old 4x4-only order is used,
|
||||
// which simply omits lights 16-24.
|
||||
SMX_API void SMX_SetLights2(const char *lightData, int lightDataSize); |
||||
|
||||
// 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.
|
||||
SMX_API void SMX_ReenableAutoLights(); |
||||
|
||||
// Get the current controller's configuration.
|
||||
//
|
||||
// Return true if a configuration is available. If false is returned, no panel is connected
|
||||
// and no data will be set.
|
||||
SMX_API bool 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.
|
||||
SMX_API void SMX_SetConfig(int pad, const SMXConfig *config); |
||||
|
||||
// Reset a pad to its original configuration.
|
||||
SMX_API void SMX_FactoryReset(int pad); |
||||
|
||||
// Request an immediate panel recalibration. This is normally not necessary, but can be helpful
|
||||
// for diagnostics.
|
||||
SMX_API void SMX_ForceRecalibration(int pad); |
||||
|
||||
// Set a sensor test mode and request test data. This is used by the configuration tool.
|
||||
SMX_API void SMX_SetTestMode(int pad, SensorTestMode mode); |
||||
SMX_API bool SMX_GetTestData(int pad, SMXSensorTestModeData *data); |
||||
|
||||
// Set a panel test mode. These only appear as debug lighting on the panel and don't
|
||||
// return data to us. Lights can't be updated while a panel test mode is active.
|
||||
// This applies to all connected pads.
|
||||
SMX_API void SMX_SetPanelTestMode(PanelTestMode mode); |
||||
|
||||
// Return the build version of the DLL, which is based on the git tag at build time. This
|
||||
// is only intended for diagnostic logging, and it's also the version we show in SMXConfig.
|
||||
SMX_API const char *SMX_Version(); |
||||
|
||||
// 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 |
||||
}; |
||||
|
||||
// Bits for SMXConfig::flags.
|
||||
enum SMXConfigFlags { |
||||
// If set, panels will use the pressed animation when pressed, and stepColor
|
||||
// is ignored. If unset, panels will be lit solid using stepColor.
|
||||
// masterVersion >= 4. Previous versions always use stepColor.
|
||||
PlatformFlags_AutoLightingUsePressedAnimations = 1 << 0, |
||||
|
||||
// If set, panels are using FSRs, otherwise load cells.
|
||||
PlatformFlags_FSR = 1 << 1, |
||||
}; |
||||
#pragma pack(push, 1) |
||||
|
||||
struct packed_sensor_settings_t { |
||||
// Load cell thresholds:
|
||||
uint8_t loadCellLowThreshold; |
||||
uint8_t loadCellHighThreshold; |
||||
|
||||
// FSR thresholds:
|
||||
uint8_t fsrLowThreshold[4]; |
||||
uint8_t fsrHighThreshold[4]; |
||||
|
||||
uint16_t combinedLowThreshold; |
||||
uint16_t combinedHighThreshold; |
||||
|
||||
// This must be left unchanged.
|
||||
uint16_t reserved; |
||||
}; |
||||
|
||||
static_assert(sizeof(packed_sensor_settings_t) == 16, "Incorrect packed_sensor_settings_t size"); |
||||
|
||||
// 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 |
||||
{ |
||||
// 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
|
||||
// - 0x03: debounceDelayMs added
|
||||
uint8_t configVersion = 0x05; |
||||
|
||||
// Packed flags (masterVersion >= 4).
|
||||
uint8_t flags = 0; |
||||
|
||||
// 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.
|
||||
|
||||
// These are internal tunables and should be left unchanged.
|
||||
uint16_t debounceNodelayMilliseconds = 0; |
||||
uint16_t debounceDelayMilliseconds = 0; |
||||
uint16_t panelDebounceMicroseconds = 4000; |
||||
uint8_t autoCalibrationMaxDeviation = 100; |
||||
uint8_t badSensorMinimumDelaySeconds = 15; |
||||
uint16_t autoCalibrationAveragesPerUpdate = 60; |
||||
uint16_t autoCalibrationSamplesPerAverage = 500; |
||||
|
||||
// The maximum tare value to calibrate to (except on startup).
|
||||
uint16_t autoCalibrationMaxTare = 0xFFFF; |
||||
|
||||
// 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 default color to set the platform LED strip to.
|
||||
uint8_t platformStripColor[3]; |
||||
|
||||
// Which panels to enable auto-lighting for. Disabled panels will be unlit.
|
||||
// 0x01 = panel 0, 0x02 = panel 1, 0x04 = panel 2, etc. This only affects
|
||||
// the master controller's built-in auto lighting and not lights data send
|
||||
// from the SDK.
|
||||
uint16_t autoLightPanelMask = 0xFFFF; |
||||
|
||||
// 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; |
||||
|
||||
// Per-panel sensor settings:
|
||||
packed_sensor_settings_t panelSettings[9]; |
||||
|
||||
// These are internal tunables and should be left unchanged.
|
||||
uint8_t preDetailsDelayMilliseconds = 5; |
||||
|
||||
// Pad the struct to 250 bytes. This keeps this struct size from changing
|
||||
// as we add fields, so the ABI doesn't change. Applications should leave
|
||||
// any data in here unchanged when calling SMX_SetConfig.
|
||||
uint8_t padding[49]; |
||||
}; |
||||
#pragma pack(pop) |
||||
|
||||
static_assert(offsetof(SMXConfig, padding) == 201, "Incorrect padding alignment"); |
||||
static_assert(sizeof(SMXConfig) == 250, "Expected 250 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]; |
||||
|
||||
// Bad sensor selection jumper indication for each panel.
|
||||
bool iBadJumper[9][4]; |
||||
}; |
||||
|
||||
// The values also correspond with the protocol and must not be changed.
|
||||
// These are panel-side diagnostics modes.
|
||||
enum PanelTestMode { |
||||
PanelTestMode_Off = '0', |
||||
PanelTestMode_PressureTest = '1', |
||||
}; |
||||
|
||||
#endif |
@ -0,0 +1,3 @@ |
||||
# Ignore updates to the auto-generated build version. |
||||
SMXBuildVersion.h |
||||
|
@ -0,0 +1,336 @@ |
||||
#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::Log(wstring s) |
||||
{ |
||||
Log(WideStringToUTF8(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) |
||||
{ |
||||
int iChars = vsnprintf(NULL, 0, szFormat, argList); |
||||
if(iChars == -1) |
||||
return string("Error formatting string: ") + szFormat; |
||||
|
||||
string sStr; |
||||
sStr.resize(iChars+1); |
||||
vsnprintf((char *) sStr.data(), iChars+1, szFormat, argList); |
||||
sStr.resize(iChars); |
||||
|
||||
return sStr; |
||||
} |
||||
|
||||
string SMX::ssprintf(const char *fmt, ...) |
||||
{ |
||||
va_list va; |
||||
va_start(va, fmt); |
||||
return vssprintf(fmt, va); |
||||
} |
||||
|
||||
wstring SMX::wvssprintf(const wchar_t *szFormat, va_list argList) |
||||
{ |
||||
int iChars = _vsnwprintf(NULL, 0, szFormat, argList); |
||||
if(iChars == -1) |
||||
return wstring(L"Error formatting string: ") + szFormat; |
||||
|
||||
wstring sStr; |
||||
sStr.resize(iChars+1); |
||||
_vsnwprintf((wchar_t *) sStr.data(), iChars+1, szFormat, argList); |
||||
sStr.resize(iChars); |
||||
|
||||
return sStr; |
||||
} |
||||
|
||||
wstring SMX::wssprintf(const wchar_t *fmt, ...) |
||||
{ |
||||
va_list va; |
||||
va_start(va, fmt); |
||||
return wvssprintf(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; |
||||
} |
||||
|
||||
void SMX::GenerateRandom(void *pOut, int iSize) |
||||
{ |
||||
// These calls shouldn't fail.
|
||||
HCRYPTPROV cryptProv; |
||||
if(!CryptAcquireContext(&cryptProv, nullptr, |
||||
L"Microsoft Base Cryptographic Provider v1.0", |
||||
PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) |
||||
throw exception("CryptAcquireContext error"); |
||||
|
||||
if(!CryptGenRandom(cryptProv, iSize, (BYTE *) pOut))
|
||||
throw exception("CryptGenRandom error"); |
||||
|
||||
if(!CryptReleaseContext(cryptProv, 0)) |
||||
throw exception("CryptReleaseContext error"); |
||||
} |
||||
|
||||
string SMX::WideStringToUTF8(wstring s) |
||||
{ |
||||
if(s.empty()) |
||||
return ""; |
||||
|
||||
int iBytes = WideCharToMultiByte( CP_ACP, 0, s.data(), s.size(), NULL, 0, NULL, FALSE ); |
||||
|
||||
string ret; |
||||
ret.resize(iBytes); |
||||
WideCharToMultiByte( CP_ACP, 0, s.data(), s.size(), (char *) ret.data(), iBytes, NULL, FALSE ); |
||||
|
||||
return ret; |
||||
} |
||||
|
||||
const char *SMX::CreateError(string error) |
||||
{ |
||||
// Store the string in a static so it doesn't get deallocated.
|
||||
static string buf; |
||||
buf = error; |
||||
return buf.c_str(); |
||||
} |
||||
|
||||
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,152 @@ |
||||
#ifndef HELPERS_H |
||||
#define HELPERS_H |
||||
|
||||
#include <string> |
||||
#include <stdarg.h> |
||||
#include <windows.h> |
||||
#include <functional> |
||||
#include <memory> |
||||
#include <vector> |
||||
using namespace std; |
||||
|
||||
namespace SMX |
||||
{ |
||||
void Log(string s); |
||||
void Log(wstring 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, ...); |
||||
wstring wvssprintf(const wchar_t *szFormat, va_list argList); |
||||
wstring wssprintf(const wchar_t *fmt, ...); |
||||
string BinaryToHex(const void *pData_, int iNumBytes); |
||||
string BinaryToHex(const string &sString); |
||||
bool GetRandomBytes(void *pData, int iBytes); |
||||
double GetMonotonicTime(); |
||||
void GenerateRandom(void *pOut, int iSize); |
||||
string WideStringToUTF8(wstring s); |
||||
|
||||
// Create a char* string that will be valid until the next call to CreateError.
|
||||
// This is used to return error messages to the caller.
|
||||
const char *CreateError(string error); |
||||
|
||||
#define arraylen(a) (sizeof(a) / sizeof((a)[0])) |
||||
|
||||
// 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; |
||||
}; |
||||
|
||||
|
||||
class Event |
||||
{ |
||||
public: |
||||
Event(Mutex &lock): |
||||
m_Lock(lock) |
||||
{ |
||||
m_hEvent = make_shared<AutoCloseHandle>(CreateEvent(NULL, false, false, NULL)); |
||||
} |
||||
|
||||
void Set() |
||||
{ |
||||
SetEvent(m_hEvent->value()); |
||||
} |
||||
|
||||
// Unlock m_Lock, wait up to iDelayMilliseconds for the event to be set,
|
||||
// then lock m_Lock. If iDelayMilliseconds is -1, wait forever.
|
||||
void Wait(int iDelayMilliseconds) |
||||
{ |
||||
if(iDelayMilliseconds == -1) |
||||
iDelayMilliseconds = INFINITE; |
||||
|
||||
m_Lock.AssertLockedByCurrentThread(); |
||||
|
||||
m_Lock.Unlock(); |
||||
vector<HANDLE> aHandles = { m_hEvent->value() }; |
||||
WaitForSingleObjectEx(m_hEvent->value(), iDelayMilliseconds, true); |
||||
m_Lock.Lock(); |
||||
} |
||||
|
||||
private: |
||||
shared_ptr<SMX::AutoCloseHandle> m_hEvent; |
||||
Mutex &m_Lock; |
||||
}; |
||||
|
||||
} |
||||
|
||||
#endif |
@ -0,0 +1,124 @@ |
||||
// This implements the public API.
|
||||
|
||||
#include <windows.h> |
||||
#include <memory> |
||||
|
||||
#include "../SMX.h" |
||||
#include "SMXManager.h" |
||||
#include "SMXDevice.h" |
||||
#include "SMXBuildVersion.h" |
||||
#include "SMXPanelAnimation.h" // for SMX_LightsAnimation_SetAuto |
||||
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; |
||||
} |
||||
|
||||
// DLL interface:
|
||||
SMX_API void SMX_Start(SMXUpdateCallback callback, void *pUser) |
||||
{ |
||||
if(SMXManager::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)));
|
||||
SMXManager::g_pSMX = make_shared<SMXManager>(UpdateCallback); |
||||
} |
||||
|
||||
SMX_API void SMX_Stop() |
||||
{ |
||||
// If lights animation is running, shut it down first.
|
||||
SMX_LightsAnimation_SetAuto(false); |
||||
|
||||
SMXManager::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 bool SMX_GetConfig(int pad, SMXConfig *config) { return SMXManager::g_pSMX->GetDevice(pad)->GetConfig(*config); } |
||||
SMX_API void SMX_SetConfig(int pad, const SMXConfig *config) { SMXManager::g_pSMX->GetDevice(pad)->SetConfig(*config); } |
||||
SMX_API void SMX_GetInfo(int pad, SMXInfo *info) { SMXManager::g_pSMX->GetDevice(pad)->GetInfo(*info); } |
||||
SMX_API uint16_t SMX_GetInputState(int pad) { return SMXManager::g_pSMX->GetDevice(pad)->GetInputState(); } |
||||
SMX_API void SMX_FactoryReset(int pad) { SMXManager::g_pSMX->GetDevice(pad)->FactoryReset(); } |
||||
SMX_API void SMX_ForceRecalibration(int pad) { SMXManager::g_pSMX->GetDevice(pad)->ForceRecalibration(); } |
||||
SMX_API void SMX_SetTestMode(int pad, SensorTestMode mode) { SMXManager::g_pSMX->GetDevice(pad)->SetSensorTestMode(mode); } |
||||
SMX_API bool SMX_GetTestData(int pad, SMXSensorTestModeData *data) { return SMXManager::g_pSMX->GetDevice(pad)->GetTestData(*data); } |
||||
SMX_API void SMX_SetPanelTestMode(PanelTestMode mode) { SMXManager::g_pSMX->SetPanelTestMode(mode); } |
||||
|
||||
SMX_API void SMX_SetLights(const char lightData[864]) |
||||
{ |
||||
SMX_SetLights2(lightData, 864); |
||||
} |
||||
SMX_API void SMX_SetLights2(const char *lightData, int lightDataSize) |
||||
{ |
||||
// The lightData into data per pad depending on whether we've been
|
||||
// given 16 or 25 lights of data.
|
||||
string lights[2]; |
||||
const int BytesPerPad16 = 9*16*3; |
||||
const int BytesPerPad25 = 9*25*3; |
||||
if(lightDataSize == 2*BytesPerPad16) |
||||
{ |
||||
lights[0] = string(lightData, BytesPerPad16); |
||||
lights[1] = string(lightData + BytesPerPad16, BytesPerPad16); |
||||
} |
||||
else if(lightDataSize == 2*BytesPerPad25) |
||||
{ |
||||
lights[0] = string(lightData, BytesPerPad25); |
||||
lights[1] = string(lightData + BytesPerPad25, BytesPerPad25); |
||||
} |
||||
else |
||||
{ |
||||
Log(ssprintf("SMX_SetLights2: lightDataSize is invalid (must be %i or %i)\n", |
||||
2*BytesPerPad16, 2*BytesPerPad25)); |
||||
return; |
||||
} |
||||
|
||||
SMXManager::g_pSMX->SetLights(lights); |
||||
|
||||
// If we're running auto animations, stop them when we get an API call to set lights.
|
||||
SMXAutoPanelAnimations::TemporaryStopAnimating(); |
||||
} |
||||
|
||||
// This is internal for SMXConfig. These lights aren't meant to be animated.
|
||||
SMX_API void SMX_SetPlatformLights(const char lightData[88*3], int lightDataSize) |
||||
{ |
||||
if(lightDataSize != 88*3) |
||||
{ |
||||
Log(ssprintf("SMX_SetPlatformLights: lightDataSize is invalid (must be %i)\n", |
||||
88*3)); |
||||
return; |
||||
} |
||||
|
||||
string lights[2]; |
||||
lights[0] = string(lightData, 44*3); |
||||
lights[1] = string(lightData + 44*3, 44*3); |
||||
SMXManager::g_pSMX->SetPlatformLights(lights); |
||||
} |
||||
|
||||
SMX_API void SMX_ReenableAutoLights() { SMXManager::g_pSMX->ReenableAutoLights(); } |
||||
SMX_API const char *SMX_Version() { return SMX_BUILD_VERSION; } |
||||
|
||||
// These aren't exposed in the public API, since they're only used internally.
|
||||
SMX_API void SMX_SetOnlySendLightsOnChange(bool value) { SMXManager::g_pSMX->SetOnlySendLightsOnChange(value); } |
||||
SMX_API void SMX_SetSerialNumbers() { SMXManager::g_pSMX->SetSerialNumbers(); } |
@ -0,0 +1,169 @@ |
||||
#include "SMXConfigPacket.h" |
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
// The config packet format changed in version 5. This handles compatibility with
|
||||
// the old configuration packet. The config packet in SMX.h matches the new format.
|
||||
//
|
||||
|
||||
|
||||
#pragma pack(push, 1) |
||||
struct OldSMXConfig |
||||
{ |
||||
uint8_t unused1 = 0xFF, unused2 = 0xFF; |
||||
uint8_t unused3 = 0xFF, unused4 = 0xFF; |
||||
uint8_t unused5 = 0xFF, unused6 = 0xFF; |
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
uint8_t enabledSensors[5]; |
||||
|
||||
uint8_t autoLightsTimeout = 1000/128; // 1 second
|
||||
|
||||
uint8_t stepColor[3*9]; |
||||
|
||||
uint8_t panelRotation; |
||||
|
||||
uint16_t autoCalibrationSamplesPerAverage = 500; |
||||
|
||||
uint8_t masterVersion = 0xFF; |
||||
uint8_t configVersion = 0x03; |
||||
|
||||
uint8_t unused9[10]; |
||||
uint8_t panelThreshold0Low, panelThreshold0High; |
||||
uint8_t panelThreshold3Low, panelThreshold3High; |
||||
uint8_t panelThreshold5Low, panelThreshold5High; |
||||
uint8_t panelThreshold6Low, panelThreshold6High; |
||||
uint8_t panelThreshold8Low, panelThreshold8High; |
||||
|
||||
uint16_t debounceDelayMilliseconds = 0; |
||||
|
||||
uint8_t padding[164]; |
||||
}; |
||||
#pragma pack(pop) |
||||
static_assert(offsetof(OldSMXConfig, padding) == 86, "Incorrect padding alignment"); |
||||
static_assert(sizeof(OldSMXConfig) == 250, "Expected 250 bytes"); |
||||
|
||||
void ConvertToNewConfig(const vector<uint8_t> &oldConfigData, SMXConfig &newConfig) |
||||
{ |
||||
// Copy data in its order within OldSMXConfig. This lets us easily stop at each
|
||||
// known packet version. Any fields that aren't present in oldConfigData will be
|
||||
// left at their default values in SMXConfig.
|
||||
const OldSMXConfig &oldConfig = (OldSMXConfig &) *oldConfigData.data(); |
||||
|
||||
newConfig.debounceNodelayMilliseconds = oldConfig.masterDebounceMilliseconds; |
||||
|
||||
newConfig.panelSettings[7].loadCellLowThreshold = oldConfig.panelThreshold7Low; |
||||
newConfig.panelSettings[4].loadCellLowThreshold = oldConfig.panelThreshold4Low; |
||||
newConfig.panelSettings[2].loadCellLowThreshold = oldConfig.panelThreshold2Low; |
||||
|
||||
newConfig.panelSettings[7].loadCellHighThreshold = oldConfig.panelThreshold7High; |
||||
newConfig.panelSettings[4].loadCellHighThreshold = oldConfig.panelThreshold4High; |
||||
newConfig.panelSettings[2].loadCellHighThreshold = oldConfig.panelThreshold2High; |
||||
|
||||
newConfig.panelDebounceMicroseconds = oldConfig.panelDebounceMicroseconds; |
||||
newConfig.autoCalibrationMaxDeviation = oldConfig.autoCalibrationMaxDeviation; |
||||
newConfig.badSensorMinimumDelaySeconds = oldConfig.badSensorMinimumDelaySeconds; |
||||
newConfig.autoCalibrationAveragesPerUpdate = oldConfig.autoCalibrationAveragesPerUpdate; |
||||
|
||||
newConfig.panelSettings[1].loadCellLowThreshold = oldConfig.panelThreshold1Low; |
||||
newConfig.panelSettings[1].loadCellHighThreshold = oldConfig.panelThreshold1High; |
||||
|
||||
memcpy(newConfig.enabledSensors, oldConfig.enabledSensors, sizeof(newConfig.enabledSensors)); |
||||
newConfig.autoLightsTimeout = oldConfig.autoLightsTimeout; |
||||
memcpy(newConfig.stepColor, oldConfig.stepColor, sizeof(newConfig.stepColor)); |
||||
newConfig.panelRotation = oldConfig.panelRotation; |
||||
newConfig.autoCalibrationSamplesPerAverage = oldConfig.autoCalibrationSamplesPerAverage; |
||||
|
||||
if(oldConfig.configVersion == 0xFF) |
||||
return; |
||||
|
||||
newConfig.masterVersion = oldConfig.masterVersion; |
||||
newConfig.configVersion = oldConfig.configVersion; |
||||
|
||||
if(oldConfig.configVersion < 2) |
||||
return; |
||||
|
||||
newConfig.panelSettings[0].loadCellLowThreshold = oldConfig.panelThreshold0Low; |
||||
newConfig.panelSettings[3].loadCellLowThreshold = oldConfig.panelThreshold3Low; |
||||
newConfig.panelSettings[5].loadCellLowThreshold = oldConfig.panelThreshold5Low; |
||||
newConfig.panelSettings[6].loadCellLowThreshold = oldConfig.panelThreshold6Low; |
||||
newConfig.panelSettings[8].loadCellLowThreshold = oldConfig.panelThreshold8Low; |
||||
|
||||
newConfig.panelSettings[0].loadCellHighThreshold = oldConfig.panelThreshold0High; |
||||
newConfig.panelSettings[3].loadCellHighThreshold = oldConfig.panelThreshold3High; |
||||
newConfig.panelSettings[5].loadCellHighThreshold = oldConfig.panelThreshold5High; |
||||
newConfig.panelSettings[6].loadCellHighThreshold = oldConfig.panelThreshold6High; |
||||
newConfig.panelSettings[8].loadCellHighThreshold = oldConfig.panelThreshold8High; |
||||
|
||||
if(oldConfig.configVersion < 3) |
||||
return; |
||||
|
||||
newConfig.debounceDelayMilliseconds = oldConfig.debounceDelayMilliseconds; |
||||
} |
||||
|
||||
// oldConfigData contains the data we're replacing. Any fields that exist in the old
|
||||
// config format and not the new one will be left unchanged.
|
||||
void ConvertToOldConfig(const SMXConfig &newConfig, vector<uint8_t> &oldConfigData) |
||||
{ |
||||
OldSMXConfig &oldConfig = (OldSMXConfig &) *oldConfigData.data(); |
||||
|
||||
// We don't need to check configVersion here. It's safe to set all fields in
|
||||
// the output config packet. If oldConfigData isn't 128 bytes, extend it.
|
||||
if(oldConfigData.size() < 128) |
||||
oldConfigData.resize(128, 0xFF); |
||||
|
||||
oldConfig.masterDebounceMilliseconds = newConfig.debounceNodelayMilliseconds; |
||||
|
||||
oldConfig.panelThreshold7Low = newConfig.panelSettings[7].loadCellLowThreshold; |
||||
oldConfig.panelThreshold4Low = newConfig.panelSettings[4].loadCellLowThreshold; |
||||
oldConfig.panelThreshold2Low = newConfig.panelSettings[2].loadCellLowThreshold; |
||||
|
||||
oldConfig.panelThreshold7High = newConfig.panelSettings[7].loadCellHighThreshold; |
||||
oldConfig.panelThreshold4High = newConfig.panelSettings[4].loadCellHighThreshold; |
||||
oldConfig.panelThreshold2High = newConfig.panelSettings[2].loadCellHighThreshold; |
||||
|
||||
oldConfig.panelDebounceMicroseconds = newConfig.panelDebounceMicroseconds; |
||||
oldConfig.autoCalibrationMaxDeviation = newConfig.autoCalibrationMaxDeviation; |
||||
oldConfig.badSensorMinimumDelaySeconds = newConfig.badSensorMinimumDelaySeconds; |
||||
oldConfig.autoCalibrationAveragesPerUpdate = newConfig.autoCalibrationAveragesPerUpdate; |
||||
|
||||
oldConfig.panelThreshold1Low = newConfig.panelSettings[1].loadCellLowThreshold; |
||||
oldConfig.panelThreshold1High = newConfig.panelSettings[1].loadCellHighThreshold; |
||||
|
||||
memcpy(oldConfig.enabledSensors, newConfig.enabledSensors, sizeof(newConfig.enabledSensors)); |
||||
oldConfig.autoLightsTimeout = newConfig.autoLightsTimeout; |
||||
memcpy(oldConfig.stepColor, newConfig.stepColor, sizeof(newConfig.stepColor)); |
||||
oldConfig.panelRotation = newConfig.panelRotation; |
||||
oldConfig.autoCalibrationSamplesPerAverage = newConfig.autoCalibrationSamplesPerAverage; |
||||
|
||||
oldConfig.masterVersion = newConfig.masterVersion; |
||||
oldConfig.configVersion= newConfig.configVersion; |
||||
|
||||
oldConfig.panelThreshold0Low = newConfig.panelSettings[0].loadCellLowThreshold; |
||||
oldConfig.panelThreshold3Low = newConfig.panelSettings[3].loadCellLowThreshold; |
||||
oldConfig.panelThreshold5Low = newConfig.panelSettings[5].loadCellLowThreshold; |
||||
oldConfig.panelThreshold6Low = newConfig.panelSettings[6].loadCellLowThreshold; |
||||
oldConfig.panelThreshold8Low = newConfig.panelSettings[8].loadCellLowThreshold; |
||||
|
||||
oldConfig.panelThreshold0High = newConfig.panelSettings[0].loadCellHighThreshold; |
||||
oldConfig.panelThreshold3High = newConfig.panelSettings[3].loadCellHighThreshold; |
||||
oldConfig.panelThreshold5High = newConfig.panelSettings[5].loadCellHighThreshold; |
||||
oldConfig.panelThreshold6High = newConfig.panelSettings[6].loadCellHighThreshold; |
||||
oldConfig.panelThreshold8High = newConfig.panelSettings[8].loadCellHighThreshold; |
||||
|
||||
oldConfig.debounceDelayMilliseconds = newConfig.debounceDelayMilliseconds; |
||||
} |
@ -0,0 +1,12 @@ |
||||
#ifndef SMXConfigPacket_h |
||||
#define SMXConfigPacket_h |
||||
|
||||
#include <vector> |
||||
using namespace std; |
||||
|
||||
#include "../SMX.h" |
||||
|
||||
void ConvertToNewConfig(const vector<uint8_t> &oldConfig, SMXConfig &newConfig); |
||||
void ConvertToOldConfig(const SMXConfig &newConfig, vector<uint8_t> &oldConfigData); |
||||
|
||||
#endif |
@ -0,0 +1,607 @@ |
||||
#include "SMXDevice.h" |
||||
|
||||
#include "../SMX.h" |
||||
#include "Helpers.h" |
||||
#include "SMXDeviceConnection.h" |
||||
#include "SMXDeviceSearch.h" |
||||
#include "SMXConfigPacket.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; |
||||
m_bSendingConfig = false; |
||||
m_bWaitingForConfigResponse = 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(string response)> pComplete) |
||||
{ |
||||
LockMutex Lock(m_Lock); |
||||
SendCommandLocked(cmd, pComplete); |
||||
} |
||||
|
||||
void SMX::SMXDevice::SendCommandLocked(string cmd, function<void(string response)> pComplete) |
||||
{ |
||||
m_Lock.AssertLockedByCurrentThread(); |
||||
|
||||
if(!m_pConnection->IsConnected()) |
||||
{ |
||||
// If we're not connected, just call pComplete.
|
||||
if(pComplete) |
||||
pComplete(""); |
||||
return; |
||||
} |
||||
|
||||
// This call is nonblocking, so it's safe to do this in the UI thread.
|
||||
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); |
||||
return GetConfigLocked(configOut); |
||||
} |
||||
|
||||
bool SMX::SMXDevice::GetConfigLocked(SMXConfig &configOut) |
||||
{ |
||||
m_Lock.AssertLockedByCurrentThread(); |
||||
|
||||
// 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"); |
||||
|
||||
SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo(); |
||||
|
||||
SendCommandLocked(deviceInfo.m_iFirmwareVersion >= 5? "G":"g\n", |
||||
[&](string response) { |
||||
// We now have the new configuration.
|
||||
m_Lock.AssertLockedByCurrentThread(); |
||||
|
||||
if(deviceInfo.m_iFirmwareVersion >= 5) |
||||
{ |
||||
// Factory reset resets the platform strip color saved to the configuration, but doesn't
|
||||
// apply it to the lights. Do that now.
|
||||
string sLightCommand; |
||||
sLightCommand.push_back('L'); |
||||
sLightCommand.push_back(0); // LED strip index (always 0)
|
||||
sLightCommand.push_back(44); // number of LEDs to set
|
||||
|
||||
config.platformStripColor[0]; |
||||
for(int i = 0; i < 44; ++i) |
||||
{ |
||||
sLightCommand += config.platformStripColor[0]; |
||||
sLightCommand += config.platformStripColor[1]; |
||||
sLightCommand += config.platformStripColor[2]; |
||||
} |
||||
|
||||
SendCommandLocked(sLightCommand); |
||||
} |
||||
|
||||
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; |
||||
|
||||
// 'g' is sent by firmware versions 1-4. Version 5 and newer send 'G', to ensure
|
||||
// older code doesn't misinterpret the modified config packet format.
|
||||
case 'g': |
||||
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; |
||||
} |
||||
|
||||
// Store the raw config data in rawConfig. For V1-4 firmwares, this is the
|
||||
// old config format.
|
||||
rawConfig.resize(iSize); |
||||
memcpy(rawConfig.data(), buf.data()+2, min(iSize, sizeof(config))); |
||||
|
||||
if(buf[0] == 'g') |
||||
{ |
||||
// Convert the old config format to the new one, so the rest of the SDK and
|
||||
// user code doesn't need to deal with multiple formats.
|
||||
ConvertToNewConfig(rawConfig, config); |
||||
} |
||||
else |
||||
{ |
||||
// This is the new config format. Copy it directly into config.
|
||||
memcpy(&config, buf.data()+2, min(iSize, sizeof(config))); |
||||
} |
||||
|
||||
m_bHaveConfig = true; |
||||
buf.erase(buf.begin(), buf.begin()+iSize+2); |
||||
|
||||
// Log(ssprintf("Read back configuration: %i bytes, first byte %i", iSize, buf[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; |
||||
|
||||
// If we're still waiting for a previous configuration to read back, don't send
|
||||
// another yet.
|
||||
if(m_bWaitingForConfigResponse) |
||||
return; |
||||
|
||||
// Rate limit updating the configuration, to prevent excess EEPROM wear. This is just
|
||||
// a safeguard in case applications try to change the configuration in realtime. If we've
|
||||
// written the configuration recently, stop. We'll write the most recent configuration
|
||||
// once enough time has passed. This is hidden to the application, since GetConfig returns
|
||||
// wanted_config if it's set.
|
||||
const float fTimeBetweenConfigUpdates = 1.0f; |
||||
double fNow = SMX::GetMonotonicTime(); |
||||
if(m_fDelayConfigUpdatesUntil > fNow) |
||||
return; |
||||
m_fDelayConfigUpdatesUntil = fNow + 1.0f; |
||||
|
||||
SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo(); |
||||
|
||||
// Write configuration command. This is "w" in versions 1-4, and "W" in versions 5
|
||||
// and newer.
|
||||
string sData = ssprintf(deviceInfo.m_iFirmwareVersion >= 5? "W":"w"); |
||||
|
||||
// Append the config packet.
|
||||
if(deviceInfo.m_iFirmwareVersion < 5) |
||||
{ |
||||
// Convert wanted_config to the old configuration format.
|
||||
vector<uint8_t> outputConfig = rawConfig; |
||||
ConvertToOldConfig(wanted_config, outputConfig); |
||||
|
||||
uint8_t iSize = (uint8_t) outputConfig.size(); |
||||
sData.append((char *) &iSize, sizeof(iSize)); |
||||
sData.append((char *) outputConfig.data(), outputConfig.size()); |
||||
} |
||||
else |
||||
{ |
||||
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, [&](string response) { |
||||
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; |
||||
|
||||
// Don't send another configuration packet until we receive the response to the above
|
||||
// command. If we're sending updates quickly (eg. dragging the color slider), we can
|
||||
// send multiple updates before we get a response.
|
||||
m_bWaitingForConfigResponse = true; |
||||
|
||||
// After we write the configuration, read back the updated configuration to
|
||||
// verify it. This command is "g" in versions 1-4, and "G" in versions 5 and
|
||||
// newer.
|
||||
SendCommandLocked( |
||||
deviceInfo.m_iFirmwareVersion >= 5? "G":"g\n", [this](string response) { |
||||
m_bWaitingForConfigResponse = false; |
||||
}); |
||||
} |
||||
|
||||
void SMX::SMXDevice::Update(wstring &sError) |
||||
{ |
||||
m_Lock.AssertLockedByCurrentThread(); |
||||
|
||||
if(!m_pConnection->IsConnected()) |
||||
return; |
||||
|
||||
CheckActive(); |
||||
SendConfig(); |
||||
UpdateSensorTestMode(); |
||||
|
||||
{ |
||||
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); |
||||
|
||||
SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo(); |
||||
|
||||
// Read the current configuration. The device will return a "g" or "G" response
|
||||
// containing its current SMXConfig.
|
||||
SendCommandLocked(deviceInfo.m_iFirmwareVersion >= 5? "G":"g\n"); |
||||
} |
||||
|
||||
// Check if we need to request test mode data.
|
||||
void SMX::SMXDevice::UpdateSensorTestMode() |
||||
{ |
||||
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 bad_sensor_dip_0:1; |
||||
uint8_t bad_sensor_dip_1:1; |
||||
uint8_t bad_sensor_dip_2:1; |
||||
uint8_t bad_sensor_dip_3:1; |
||||
}; |
||||
#pragma pack(pop) |
||||
|
||||
m_HaveSensorTestModeData = true; |
||||
SMXSensorTestModeData &output = m_SensorTestData; |
||||
|
||||
bool bLastHaveDataFromPanel[9]; |
||||
memcpy(bLastHaveDataFromPanel, output.bHaveDataFromPanel, sizeof(output.bHaveDataFromPanel)); |
||||
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)); |
||||
memset(output.iBadJumper, 0, sizeof(output.iBadJumper)); |
||||
|
||||
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) |
||||
{ |
||||
if(bLastHaveDataFromPanel[iPanel]) |
||||
Log(ssprintf("No data from panel %i (%02x %02x %02x)", iPanel, pad_data.sig1, pad_data.sig2, pad_data.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; |
||||
output.iBadJumper[iPanel][0] = pad_data.bad_sensor_dip_0; |
||||
output.iBadJumper[iPanel][1] = pad_data.bad_sensor_dip_1; |
||||
output.iBadJumper[iPanel][2] = pad_data.bad_sensor_dip_2; |
||||
output.iBadJumper[iPanel][3] = pad_data.bad_sensor_dip_3; |
||||
|
||||
for(int iSensor = 0; iSensor < 4; ++iSensor) |
||||
output.sensorLevel[iPanel][iSensor] = pad_data.sensors[iSensor]; |
||||
} |
||||
|
||||
CallUpdateCallback(SMXUpdateCallback_Updated); |
||||
} |
@ -0,0 +1,136 @@ |
||||
#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(string response)> pComplete=nullptr); |
||||
void SendCommandLocked(string sCmd, function<void(string response)> 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); |
||||
bool GetConfigLocked(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; |
||||
vector<uint8_t> rawConfig; |
||||
bool m_bHaveConfig = false; |
||||
double m_fDelayConfigUpdatesUntil = 0; |
||||
|
||||
// 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; |
||||
bool m_bWaitingForConfigResponse = false; |
||||
|
||||
void CallUpdateCallback(SMXUpdateCallbackReason reason); |
||||
void HandlePackets(); |
||||
|
||||
void SendConfig(); |
||||
void CheckActive(); |
||||
bool IsConnectedLocked() const; |
||||
|
||||
// Test/diagnostics mode handling.
|
||||
void UpdateSensorTestMode(); |
||||
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,437 @@ |
||||
#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() |
||||
{ |
||||
} |
||||
|
||||
SMXDeviceConnection::PendingCommand::PendingCommand() |
||||
{ |
||||
memset(&m_Overlapped, 0, sizeof(m_Overlapped)); |
||||
} |
||||
|
||||
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([&](string response) { |
||||
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()); |
||||
|
||||
// If we're being closed while a command was in progress, call its completion
|
||||
// callback, so it's guaranteed to always be called.
|
||||
if(m_pCurrentCommand && m_pCurrentCommand->m_pComplete) |
||||
m_pCurrentCommand->m_pComplete(""); |
||||
|
||||
// If any commands were queued with completion callbacks, call their completion
|
||||
// callbacks.
|
||||
for(auto &pendingCommand: m_aPendingCommands) |
||||
{ |
||||
if(pendingCommand->m_pComplete) |
||||
pendingCommand->m_pComplete(""); |
||||
} |
||||
|
||||
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) |
||||
{ |
||||
if(m_pCurrentCommand) |
||||
{ |
||||
// See if this command timed out. This doesn't happen often, so this is
|
||||
// mostly just a failsafe. The controller takes a moment to initialize on
|
||||
// startup, so we use a large enough timeout that this doesn't trigger on
|
||||
// every connection.
|
||||
double fSecondsAgo = SMX::GetMonotonicTime() - m_pCurrentCommand->m_fSentAt; |
||||
if(fSecondsAgo > 2.0f) |
||||
{ |
||||
// If we didn't get a response in this long, we're not going to. Retry the
|
||||
// command by cancelling its I/O and moving it back to the command queue.
|
||||
//
|
||||
// if we were delayed and the response is in the queue, we'll get out of sync
|
||||
Log("Command timed out. Retrying..."); |
||||
CancelIoEx(m_hDevice->value(), &m_pCurrentCommand->m_Overlapped); |
||||
|
||||
// Block until the cancellation completes. This should happen quickly.
|
||||
DWORD unused; |
||||
GetOverlappedResult(m_hDevice->value(), &m_pCurrentCommand->m_Overlapped, &unused, true); |
||||
|
||||
m_aPendingCommands.push_front(m_pCurrentCommand); |
||||
m_pCurrentCommand = nullptr; |
||||
Log("Command requeued"); |
||||
} |
||||
} |
||||
|
||||
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", 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(sPacket); |
||||
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; |
||||
|
||||
if(cmd & PACKET_FLAG_START_OF_COMMAND && !m_sCurrentReadBuffer.empty()) |
||||
{ |
||||
// When we get a start packet, the read buffer should already be empty. If
|
||||
// it isn't, we got a command that didn't end with an END_OF_COMMAND packet,
|
||||
// and something is wrong. This shouldn't happen, so warn about it and recover
|
||||
// by clearing the junk in the buffer.
|
||||
Log(ssprintf("Got PACKET_FLAG_START_OF_COMMAND, but we had %i bytes in the read buffer", |
||||
m_sCurrentReadBuffer.size())); |
||||
|
||||
m_sCurrentReadBuffer.clear(); |
||||
} |
||||
|
||||
m_sCurrentReadBuffer.append(sPacket); |
||||
|
||||
// Note that if PACKET_FLAG_HOST_CMD_FINISHED is set, PACKET_FLAG_END_OF_COMMAND
|
||||
// will always also be set.
|
||||
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_sCurrentReadBuffer); |
||||
m_pCurrentCommand = nullptr; |
||||
} |
||||
|
||||
if(cmd & PACKET_FLAG_END_OF_COMMAND) |
||||
{ |
||||
if(!m_sCurrentReadBuffer.empty()) |
||||
m_sReadBuffers.push_back(m_sCurrentReadBuffer); |
||||
m_sCurrentReadBuffer.clear(); |
||||
} |
||||
|
||||
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) |
||||
{ |
||||
// A command is in progress. See if its writes have completed.
|
||||
if(m_pCurrentCommand->m_bWriting) |
||||
{ |
||||
DWORD bytes; |
||||
int iResult = GetOverlappedResult(m_hDevice->value(), &m_pCurrentCommand->m_Overlapped, &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_bWriting = false; |
||||
} |
||||
|
||||
// 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.
|
||||
return; |
||||
} |
||||
|
||||
// Stop if we have nothing to do.
|
||||
if(m_aPendingCommands.empty()) |
||||
return; |
||||
|
||||
// Send the next command.
|
||||
shared_ptr<PendingCommand> pPendingCommand = m_aPendingCommands.front(); |
||||
|
||||
// Record the time. We can use this for timeouts.
|
||||
pPendingCommand->m_fSentAt = SMX::GetMonotonicTime(); |
||||
|
||||
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; |
||||
// Log(ssprintf("Write: %s", BinaryToHex(pPacket->sData).c_str()));
|
||||
if(!WriteFile(m_hDevice->value(), pPacket->sData.data(), pPacket->sData.size(), &unused, &pPendingCommand->m_Overlapped)) |
||||
{ |
||||
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; |
||||
} |
||||
} |
||||
} |
||||
|
||||
pPendingCommand->m_bWriting = true; |
||||
|
||||
// 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(string response)> 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(string response)> 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,131 @@ |
||||
#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(string response)> pComplete=nullptr); |
||||
|
||||
uint16_t GetInputState() const { return m_iInputState; } |
||||
|
||||
private: |
||||
void RequestDeviceInfo(function<void(string response)> 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; |
||||
}; |
||||
|
||||
// Commands that are waiting to be sent:
|
||||
struct PendingCommand { |
||||
PendingCommand(); |
||||
|
||||
list<shared_ptr<PendingCommandPacket>> m_Packets; |
||||
|
||||
// The overlapped struct for writing this command's packets. m_bWriting is true
|
||||
// if we're waiting for the write to complete.
|
||||
OVERLAPPED m_Overlapped; |
||||
bool m_bWriting = false; |
||||
|
||||
// This is only called if m_bWaitForResponse if true. Otherwise, we send the command
|
||||
// and forget about it. If the command has a response, it'll be in buf.
|
||||
function<void(string response)> 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; |
||||
|
||||
// The SMX::GetMonotonicTime when we started sending this command.
|
||||
double m_fSentAt = 0; |
||||
}; |
||||
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,174 @@ |
||||
#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) |
||||
{ |
||||
Log(wssprintf(L"SetupDiGetDeviceInterfaceDetail failed: %ls", GetErrorString(iError).c_str())); |
||||
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)) |
||||
{ |
||||
Log(wssprintf(L"SetupDiGetDeviceInterfaceDetail failed: %ls", GetErrorString(GetLastError()).c_str())); |
||||
continue; |
||||
} |
||||
|
||||
paths.insert(DeviceInterfaceDetailData->DevicePath); |
||||
} |
||||
|
||||
SetupDiDestroyDeviceInfoList(DeviceInfoSet); |
||||
|
||||
return paths; |
||||
} |
||||
|
||||
static shared_ptr<AutoCloseHandle> OpenUSBDevice(LPCTSTR DevicePath, wstring &error) |
||||
{ |
||||
// Log(ssprintf("Opening device: %ls", DevicePath));
|
||||
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) |
||||
{ |
||||
// Many unrelated devices will fail to open, so don't return this as an error.
|
||||
Log(wssprintf(L"Error opening device %ls: %ls", DevicePath, GetErrorString(GetLastError()).c_str())); |
||||
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)) |
||||
{ |
||||
Log(ssprintf("Error opening device %ls: HidD_GetAttributes failed", DevicePath)); |
||||
error = L"HidD_GetAttributes failed"; |
||||
return nullptr; |
||||
} |
||||
|
||||
if(HidAttributes.VendorID != 0x2341 || HidAttributes.ProductID != 0x8037) |
||||
{ |
||||
Log(ssprintf("Device %ls: not our device (ID %04x:%04x)", DevicePath, HidAttributes.VendorID, HidAttributes.ProductID)); |
||||
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)) |
||||
{ |
||||
Log(ssprintf("Error opening device %ls: HidD_GetProductString failed", DevicePath)); |
||||
return nullptr; |
||||
} |
||||
|
||||
if(wstring(ProductName) != L"StepManiaX") |
||||
{ |
||||
Log(ssprintf("Device %ls: not our device (%ls)", DevicePath, ProductName)); |
||||
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,545 @@ |
||||
#include "SMXGif.h" |
||||
#include <stdint.h> |
||||
#include <string> |
||||
#include <vector> |
||||
using namespace std; |
||||
|
||||
// This is a simple animated GIF decoder. It always decodes to RGBA color,
|
||||
// discarding palettes, and decodes the whole file at once.
|
||||
|
||||
class GIFError: public exception { }; |
||||
|
||||
struct Palette |
||||
{ |
||||
SMXGif::Color color[256]; |
||||
}; |
||||
|
||||
void SMXGif::GIFImage::Init(int width_, int height_) |
||||
{ |
||||
width = width_; |
||||
height = height_; |
||||
image.resize(width * height); |
||||
} |
||||
|
||||
void SMXGif::GIFImage::Clear(const Color &color) |
||||
{ |
||||
for(int y = 0; y < height; ++y) |
||||
for(int x = 0; x < width; ++x) |
||||
get(x,y) = color; |
||||
} |
||||
|
||||
void SMXGif::GIFImage::CropImage(SMXGif::GIFImage &dst, int crop_left, int crop_top, int crop_width, int crop_height) const |
||||
{ |
||||
dst.Init(crop_width, crop_height); |
||||
|
||||
for(int y = 0; y < crop_height; ++y) |
||||
{ |
||||
for(int x = 0; x < crop_width; ++x) |
||||
dst.get(x,y) = get(x + crop_left, y + crop_top); |
||||
} |
||||
} |
||||
|
||||
void SMXGif::GIFImage::Blit(SMXGif::GIFImage &src, int dst_left, int dst_top, int dst_width, int dst_height) |
||||
{ |
||||
for(int y = 0; y < dst_height; ++y) |
||||
{ |
||||
for(int x = 0; x < dst_width; ++x) |
||||
get(x + dst_left, y + dst_top) = src.get(x, y); |
||||
} |
||||
} |
||||
bool SMXGif::GIFImage::operator==(const GIFImage &rhs) const |
||||
{ |
||||
return |
||||
width == rhs.width && |
||||
height == rhs.height && |
||||
image == rhs.image; |
||||
} |
||||
|
||||
class DataStream |
||||
{ |
||||
public: |
||||
DataStream(const string &data_): |
||||
data(data_) |
||||
{ |
||||
} |
||||
|
||||
uint8_t ReadByte() |
||||
{ |
||||
if(pos >= data.size()) |
||||
throw GIFError(); |
||||
|
||||
uint8_t result = data[pos]; |
||||
pos++; |
||||
return result; |
||||
} |
||||
|
||||
uint16_t ReadLE16() |
||||
{ |
||||
uint8_t byte1 = ReadByte(); |
||||
uint8_t byte2 = ReadByte(); |
||||
return byte1 | (byte2 << 8); |
||||
} |
||||
|
||||
void ReadBytes(string &s, int count) |
||||
{ |
||||
s.clear(); |
||||
while(count--) |
||||
s.push_back(ReadByte()); |
||||
} |
||||
|
||||
void skip(int bytes) |
||||
{ |
||||
pos += bytes; |
||||
} |
||||
|
||||
private: |
||||
const string &data; |
||||
uint32_t pos = 0; |
||||
}; |
||||
|
||||
class LWZStream |
||||
{ |
||||
public: |
||||
LWZStream(DataStream &stream_): |
||||
stream(stream_) |
||||
{ |
||||
} |
||||
|
||||
// Read one LZW code from the input data.
|
||||
uint32_t ReadLZWCode(uint32_t bit_count) |
||||
{ |
||||
while(bits_in_buffer < bit_count) |
||||
{ |
||||
if(bytes_remaining == 0) |
||||
{ |
||||
// Read the next block's byte count.
|
||||
bytes_remaining = stream.ReadByte(); |
||||
if(bytes_remaining == 0) |
||||
throw GIFError(); |
||||
} |
||||
|
||||
// Shift in another 8 bits into the end of self.bits.
|
||||
bits |= stream.ReadByte() << bits_in_buffer; |
||||
bits_in_buffer += 8; |
||||
bytes_remaining -= 1; |
||||
} |
||||
|
||||
// Shift out bit_count worth of data from the end.
|
||||
uint32_t result = bits & ((1 << bit_count) - 1); |
||||
bits >>= bit_count; |
||||
bits_in_buffer -= bit_count; |
||||
|
||||
return result; |
||||
} |
||||
|
||||
// Skip the rest of the LZW data.
|
||||
void Flush() |
||||
{ |
||||
stream.skip(bytes_remaining); |
||||
bytes_remaining = 0; |
||||
|
||||
// If there are any blocks past the end of data, skip them.
|
||||
while(1) |
||||
{ |
||||
uint8_t blocksize = stream.ReadByte(); |
||||
stream.skip(blocksize); |
||||
if(bytes_remaining == 0) |
||||
break; |
||||
} |
||||
} |
||||
|
||||
private: |
||||
DataStream &stream; |
||||
uint32_t bits = 0; |
||||
int bytes_remaining = 0; |
||||
int bits_in_buffer = 0; |
||||
}; |
||||
|
||||
struct LWZDecoder |
||||
{ |
||||
LWZDecoder(DataStream &stream): |
||||
lzw_stream(LWZStream(stream)) |
||||
{ |
||||
// Each frame has a single bits field.
|
||||
code_bits = stream.ReadByte(); |
||||
} |
||||
|
||||
string DecodeImage(); |
||||
|
||||
private: |
||||
uint16_t code_bits; |
||||
LWZStream lzw_stream; |
||||
}; |
||||
|
||||
|
||||
static const int GIFBITS = 12; |
||||
|
||||
string LWZDecoder::DecodeImage() |
||||
{ |
||||
uint32_t dictionary_bits = code_bits + 1; |
||||
int prev_code1 = -1; |
||||
int prev_code2 = -1; |
||||
|
||||
uint32_t clear = 1 << code_bits; |
||||
uint32_t end = clear + 1; |
||||
uint32_t next_free_slot = clear + 2; |
||||
|
||||
vector<pair<int,int>> dictionary; |
||||
dictionary.resize(1 << GIFBITS); |
||||
|
||||
// We append to this buffer as we decode data, then append the data in reverse
|
||||
// order.
|
||||
string append_buffer; |
||||
|
||||
string result; |
||||
while(1) |
||||
{ |
||||
// Flush append_buffer.
|
||||
for(int i = append_buffer.size() - 1; i >= 0; --i) |
||||
result.push_back(append_buffer[i]); |
||||
append_buffer.clear(); |
||||
|
||||
int code1 = lzw_stream.ReadLZWCode(dictionary_bits); |
||||
// printf("%02x");
|
||||
if(code1 == end) |
||||
break; |
||||
|
||||
if(code1 == clear) |
||||
{ |
||||
// Clear the dictionary and reset.
|
||||
dictionary_bits = code_bits + 1; |
||||
next_free_slot = clear + 2; |
||||
prev_code1 = -1; |
||||
prev_code2 = -1; |
||||
continue; |
||||
} |
||||
|
||||
int code2; |
||||
if(code1 < next_free_slot) |
||||
code2 = code1; |
||||
else if(code1 == next_free_slot && prev_code2 != -1) |
||||
{ |
||||
append_buffer.push_back(prev_code2); |
||||
code2 = prev_code1; |
||||
} |
||||
else |
||||
throw GIFError(); |
||||
|
||||
// Walk through the linked list of codes in the dictionary and append.
|
||||
while(code2 >= clear + 2) |
||||
{ |
||||
uint8_t append_char = dictionary[code2].first; |
||||
code2 = dictionary[code2].second; |
||||
append_buffer.push_back(append_char); |
||||
} |
||||
append_buffer.push_back(code2); |
||||
|
||||
// If we're already at the last free slot, the dictionary is full and can't be expanded.
|
||||
if(next_free_slot < (1 << dictionary_bits)) |
||||
{ |
||||
// If we have any free dictionary slots, save.
|
||||
if(prev_code1 != -1) |
||||
{ |
||||
dictionary[next_free_slot] = make_pair(code2, prev_code1); |
||||
next_free_slot += 1; |
||||
} |
||||
// If we've just filled the last dictionary slot, expand the dictionary size if possible.
|
||||
if(next_free_slot >= (1 << dictionary_bits) && dictionary_bits < GIFBITS) |
||||
dictionary_bits += 1; |
||||
} |
||||
|
||||
prev_code1 = code1; |
||||
prev_code2 = code2; |
||||
} |
||||
|
||||
// Skip any remaining data in this block.
|
||||
lzw_stream.Flush(); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
struct GlobalGIFData |
||||
{ |
||||
int width = 0, height = 0; |
||||
int background_index = -1; |
||||
bool use_transparency = false; |
||||
int transparency_index = -1; |
||||
int duration = 0; |
||||
int disposal_method = 0; |
||||
bool have_global_palette = false; |
||||
Palette palette; |
||||
}; |
||||
|
||||
class GIFDecoder |
||||
{ |
||||
public: |
||||
GIFDecoder(DataStream &stream_): |
||||
stream(stream_) |
||||
{ |
||||
} |
||||
|
||||
void ReadAllFrames(vector<SMXGif::SMXGifFrame> &frames); |
||||
|
||||
private: |
||||
bool ReadPacket(string &packet); |
||||
Palette ReadPalette(int palette_size); |
||||
void DecodeImage(GlobalGIFData global_data, SMXGif::GIFImage &out); |
||||
|
||||
DataStream &stream; |
||||
SMXGif::GIFImage image; |
||||
int frame; |
||||
}; |
||||
|
||||
// Read a palette with size colors.
|
||||
//
|
||||
// This is a simple string, with 4 RGBA bytes per color.
|
||||
Palette GIFDecoder::ReadPalette(int palette_size) |
||||
{ |
||||
Palette result; |
||||
for(int i = 0; i < palette_size; ++i) |
||||
{ |
||||
result.color[i].color[0] = stream.ReadByte(); // R
|
||||
result.color[i].color[1] = stream.ReadByte(); // G
|
||||
result.color[i].color[2] = stream.ReadByte(); // B
|
||||
result.color[i].color[3] = 0xFF; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
bool GIFDecoder::ReadPacket(string &packet) |
||||
{ |
||||
uint8_t packet_size = stream.ReadByte(); |
||||
if(packet_size == 0) |
||||
return false; |
||||
|
||||
stream.ReadBytes(packet, packet_size); |
||||
return true; |
||||
} |
||||
|
||||
void GIFDecoder::ReadAllFrames(vector<SMXGif::SMXGifFrame> &frames) |
||||
{ |
||||
string header; |
||||
stream.ReadBytes(header, 6); |
||||
|
||||
if(header != "GIF87a" && header != "GIF89a") |
||||
throw GIFError(); |
||||
|
||||
GlobalGIFData global_data; |
||||
|
||||
global_data.width = stream.ReadLE16(); |
||||
global_data.height = stream.ReadLE16(); |
||||
image.Init(global_data.width, global_data.height); |
||||
|
||||
// Ignore the aspect ratio field. (Supporting pixel aspect ratios in a format
|
||||
// this rudimentary was almost ambitious of them...)
|
||||
uint8_t global_flags = stream.ReadByte(); |
||||
global_data.background_index = stream.ReadByte(); |
||||
|
||||
// Ignore the aspect ratio field. (Supporting pixel aspect ratios in a format
|
||||
// this rudimentary was almost ambitious of them...)
|
||||
stream.ReadByte(); |
||||
|
||||
// Decode global_flags.
|
||||
uint8_t global_palette_size = global_flags & 0x7; |
||||
|
||||
global_data.have_global_palette = (global_flags >> 7) & 0x1; |
||||
|
||||
|
||||
// If there's no global palette, leave it empty.
|
||||
if(global_data.have_global_palette) |
||||
global_data.palette = ReadPalette(1 << (global_palette_size + 1)); |
||||
|
||||
frame = 0; |
||||
|
||||
// Save a copy of global data, so we can restore it after each frame.
|
||||
GlobalGIFData saved_global_data = global_data; |
||||
|
||||
// Decode all packets.
|
||||
while(1) |
||||
{ |
||||
uint8_t packet_type = stream.ReadByte(); |
||||
if(packet_type == 0x21) |
||||
{ |
||||
// Extension packet
|
||||
uint8_t extension_type = stream.ReadByte(); |
||||
|
||||
if(extension_type == 0xF9) |
||||
{ |
||||
string packet; |
||||
if(!ReadPacket(packet)) |
||||
throw GIFError(); |
||||
|
||||
DataStream packet_buf(packet); |
||||
|
||||
// Graphics control extension
|
||||
uint8_t gce_flags = packet_buf.ReadByte(); |
||||
global_data.duration = packet_buf.ReadLE16(); |
||||
global_data.transparency_index = packet_buf.ReadByte(); |
||||
|
||||
global_data.use_transparency = bool(gce_flags & 1); |
||||
global_data.disposal_method = (gce_flags >> 2) & 0xF; |
||||
if(!global_data.use_transparency) |
||||
global_data.transparency_index = -1; |
||||
} |
||||
|
||||
// Read any remaining packets in this extension packet.
|
||||
while(1) |
||||
{ |
||||
string packet; |
||||
if(!ReadPacket(packet)) |
||||
break; |
||||
} |
||||
} |
||||
else if(packet_type == 0x2C) |
||||
{ |
||||
// Image data
|
||||
SMXGif::GIFImage frame_image; |
||||
DecodeImage(global_data, frame_image); |
||||
|
||||
SMXGif::SMXGifFrame gif_frame; |
||||
gif_frame.width = global_data.width; |
||||
gif_frame.height = global_data.height; |
||||
gif_frame.milliseconds = global_data.duration * 10; |
||||
gif_frame.frame = frame_image; |
||||
|
||||
// If this frame is identical to the previous one, just extend the previous frame.
|
||||
if(!frames.empty() && gif_frame.frame == frames.back().frame) |
||||
{ |
||||
frames.back().milliseconds += gif_frame.milliseconds; |
||||
continue; |
||||
} |
||||
|
||||
frames.push_back(gif_frame); |
||||
|
||||
frame++; |
||||
|
||||
// Reset GCE (frame-specific) data.
|
||||
global_data = saved_global_data; |
||||
} |
||||
else if(packet_type == 0x3B) |
||||
{ |
||||
// EOF
|
||||
return; |
||||
} |
||||
else |
||||
throw GIFError(); |
||||
} |
||||
} |
||||
|
||||
// Decode a single GIF image into out, leaving this->image ready for
|
||||
// the next frame (with this frame's dispose applied).
|
||||
void GIFDecoder::DecodeImage(GlobalGIFData global_data, SMXGif::GIFImage &out) |
||||
{ |
||||
uint16_t block_left = stream.ReadLE16(); |
||||
uint16_t block_top = stream.ReadLE16(); |
||||
uint16_t block_width = stream.ReadLE16(); |
||||
uint16_t block_height = stream.ReadLE16(); |
||||
uint8_t local_flags = stream.ReadByte(); |
||||
|
||||
// area = (block_left, block_top, block_left + block_width, block_top + block_height)
|
||||
// Extract flags:
|
||||
uint8_t have_local_palette = (local_flags >> 7) & 1; |
||||
// bool interlaced = (local_flags >> 6) & 1;
|
||||
uint8_t local_palette_size = (local_flags >> 0) & 0x7; |
||||
// print 'Interlaced:', interlaced
|
||||
|
||||
// We don't support interlaced GIFs right now.
|
||||
// assert interlaced == 0
|
||||
|
||||
// If this frame has a local palette, use it. Otherwise, use the global palette.
|
||||
Palette active_palette = global_data.palette; |
||||
if(have_local_palette) |
||||
active_palette = ReadPalette(1 << (local_palette_size + 1)); |
||||
|
||||
if(!global_data.have_global_palette && !have_local_palette) |
||||
{ |
||||
// We have no palette. This is an invalid file.
|
||||
throw GIFError(); |
||||
} |
||||
|
||||
if(frame == 0) |
||||
{ |
||||
// On the first frame, clear the buffer. If we have a transparency index,
|
||||
// clear to transparent. Otherwise, clear to the background color.
|
||||
if(global_data.transparency_index != -1) |
||||
image.Clear(SMXGif::Color(0,0,0,0)); |
||||
else |
||||
image.Clear(active_palette.color[global_data.background_index]); |
||||
} |
||||
|
||||
// Decode the compressed image data.
|
||||
LWZDecoder decoder(stream); |
||||
string decompressed_data = decoder.DecodeImage(); |
||||
|
||||
if(decompressed_data.size() < block_width*block_height) |
||||
throw GIFError(); |
||||
|
||||
// Save the region to restore after decoding.
|
||||
SMXGif::GIFImage dispose; |
||||
if(global_data.disposal_method <= 1) |
||||
{ |
||||
// No disposal.
|
||||
} |
||||
else if(global_data.disposal_method == 2) |
||||
{ |
||||
// Clear the region to a background color afterwards.
|
||||
dispose.Init(block_width, block_height); |
||||
|
||||
if(global_data.transparency_index != -1) |
||||
dispose.Clear(SMXGif::Color(0,0,0,0)); |
||||
else |
||||
{ |
||||
uint8_t palette_idx = global_data.background_index; |
||||
dispose.Clear(active_palette.color[palette_idx]); |
||||
} |
||||
|
||||
} |
||||
else if(global_data.disposal_method == 3) |
||||
{ |
||||
// Restore the previous frame afterwards.
|
||||
image.CropImage(dispose, block_left, block_top, block_width, block_height); |
||||
} |
||||
else |
||||
{ |
||||
// Unknown disposal method
|
||||
} |
||||
|
||||
int pos = 0; |
||||
for(int y = block_top; y < block_top + block_height; ++y) |
||||
{ |
||||
for(int x = block_left; x < block_left + block_width; ++x) |
||||
{ |
||||
uint8_t palette_idx = decompressed_data[pos]; |
||||
pos++; |
||||
|
||||
if(palette_idx == global_data.transparency_index) |
||||
{ |
||||
// If this pixel is transparent, leave the existing color in place.
|
||||
} |
||||
else |
||||
{ |
||||
image.get(x,y) = active_palette.color[palette_idx]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Copy the image before we run dispose.
|
||||
out = image; |
||||
|
||||
// Restore the dispose area.
|
||||
if(dispose.width != 0) |
||||
image.Blit(dispose, block_left, block_top, block_width, block_height); |
||||
} |
||||
|
||||
bool SMXGif::DecodeGIF(string buf, vector<SMXGif::SMXGifFrame> &frames) |
||||
{ |
||||
DataStream stream(buf); |
||||
GIFDecoder gif(stream); |
||||
try { |
||||
gif.ReadAllFrames(frames); |
||||
} catch(GIFError &) { |
||||
// We don't return error strings for this, just success or failure.
|
||||
return false; |
||||
} |
||||
return true; |
||||
} |
@ -0,0 +1,72 @@ |
||||
#ifndef SMXGif_h |
||||
#define SMXGif_h |
||||
|
||||
#include <stdint.h> |
||||
#include <string> |
||||
#include <vector> |
||||
|
||||
// This is a simple internal GIF decoder. It's only meant to be used by
|
||||
// SMXConfig.
|
||||
namespace SMXGif |
||||
{ |
||||
struct Color |
||||
{ |
||||
uint8_t color[4]; |
||||
Color() |
||||
{ |
||||
memset(color, 0, sizeof(color)); |
||||
} |
||||
|
||||
Color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) |
||||
{ |
||||
color[0] = r; |
||||
color[1] = g; |
||||
color[2] = b; |
||||
color[3] = a; |
||||
} |
||||
bool operator==(const Color &rhs) const |
||||
{ |
||||
return !memcmp(color, rhs.color, sizeof(color)); |
||||
} |
||||
}; |
||||
|
||||
struct GIFImage |
||||
{ |
||||
int width = 0, height = 0; |
||||
void Init(int width, int height); |
||||
|
||||
Color get(int x, int y) const { return image[y*width+x]; } |
||||
Color &get(int x, int y) { return image[y*width+x]; } |
||||
|
||||
// Clear to a solid color.
|
||||
void Clear(const Color &color); |
||||
|
||||
// Copy a rectangle from this image into dst.
|
||||
void CropImage(GIFImage &dst, int crop_left, int crop_top, int crop_width, int crop_height) const; |
||||
|
||||
// Copy src into a rectangle in this image.
|
||||
void Blit(GIFImage &src, int dst_left, int dst_top, int dst_width, int dst_height); |
||||
|
||||
bool operator==(const GIFImage &rhs) const; |
||||
|
||||
private: |
||||
std::vector<Color> image; |
||||
}; |
||||
|
||||
struct SMXGifFrame |
||||
{ |
||||
int width = 0, height = 0; |
||||
|
||||
// GIF images have a delay in 10ms units. We use 1ms for clarity.
|
||||
int milliseconds = 0; |
||||
|
||||
GIFImage frame; |
||||
}; |
||||
|
||||
// Decode a GIF into a list of frames.
|
||||
bool DecodeGIF(std::string buf, std::vector<SMXGifFrame> &frames); |
||||
} |
||||
|
||||
void gif_test(); |
||||
|
||||
#endif |
@ -0,0 +1,42 @@ |
||||
#include "SMXHelperThread.h" |
||||
using namespace SMX; |
||||
|
||||
SMX::SMXHelperThread::SMXHelperThread(const string &sThreadName): |
||||
SMXThread(m_Lock) |
||||
{ |
||||
Start(sThreadName); |
||||
} |
||||
|
||||
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(); |
||||
m_Lock.Lock(); |
||||
|
||||
m_Event.Wait(250); |
||||
} |
||||
m_Lock.Unlock(); |
||||
} |
||||
|
||||
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); |
||||
m_Event.Set(); |
||||
m_Lock.Unlock(); |
||||
} |
@ -0,0 +1,31 @@ |
||||
#ifndef SMXHelperThread_h |
||||
#define SMXHelperThread_h |
||||
|
||||
#include "Helpers.h" |
||||
#include "SMXThread.h" |
||||
|
||||
#include <functional> |
||||
#include <vector> |
||||
#include <memory> |
||||
using namespace std; |
||||
|
||||
namespace SMX |
||||
{ |
||||
class SMXHelperThread: public SMXThread |
||||
{ |
||||
public: |
||||
SMXHelperThread(const string &sThreadName); |
||||
|
||||
// Call func asynchronously from the helper thread.
|
||||
void RunInThread(function<void()> func); |
||||
|
||||
private: |
||||
void ThreadMain(); |
||||
|
||||
// Helper threads use their independent lock.
|
||||
SMX::Mutex m_Lock; |
||||
vector<function<void()>> m_FunctionsToCall; |
||||
}; |
||||
} |
||||
|
||||
#endif |
@ -0,0 +1,704 @@ |
||||
#include "SMXManager.h" |
||||
#include "SMXDevice.h" |
||||
#include "SMXDeviceConnection.h" |
||||
#include "SMXDeviceSearchThreaded.h" |
||||
#include "Helpers.h" |
||||
|
||||
#include <windows.h> |
||||
#include <memory> |
||||
#include <stdexcept> |
||||
using namespace std; |
||||
using namespace SMX; |
||||
|
||||
namespace { |
||||
Mutex g_Lock; |
||||
} |
||||
|
||||
shared_ptr<SMXManager> SMXManager::g_pSMX; |
||||
|
||||
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.IsCurrentThread()) |
||||
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(); |
||||
|
||||
// Send panel test mode commands if needed.
|
||||
UpdatePanelTestMode(); |
||||
|
||||
// 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_aPendingLightsCommands.empty()) |
||||
{ |
||||
double fSendIn = m_aPendingLightsCommands[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 sPanelLights[2]) |
||||
{ |
||||
g_Lock.AssertNotLockedByCurrentThread(); |
||||
LockMutex L(g_Lock); |
||||
|
||||
// Don't send lights when a panel test mode is active.
|
||||
if(m_PanelTestMode != PanelTestMode_Off) |
||||
return; |
||||
|
||||
// If m_bOnlySendLightsOnChange is true, only send lights commands if the lights have
|
||||
// actually changed. This is only used for internal testing, and the controllers normally
|
||||
// expect to receive regular lights updates, even if the lights aren't actually changing.
|
||||
if(m_bOnlySendLightsOnChange) |
||||
{ |
||||
static string sLastPanelLights[2]; |
||||
if(sPanelLights[0] == sLastPanelLights[0] && sPanelLights[1] == sLastPanelLights[1]) |
||||
{ |
||||
Log("no change"); |
||||
return; |
||||
} |
||||
|
||||
sLastPanelLights[0] = sPanelLights[0]; |
||||
sLastPanelLights[1] = sPanelLights[1]; |
||||
} |
||||
|
||||
// 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
|
||||
//
|
||||
// If we're on a 25-light device, we have an additional grid of 3x3 LEDs:
|
||||
//
|
||||
//
|
||||
// x x x x
|
||||
// 0 1 2
|
||||
// x x x x
|
||||
// 3 4 5
|
||||
// x x x x
|
||||
// 6 7 8
|
||||
// x x x x
|
||||
//
|
||||
// Set sLightsCommand[iPad][0] to include 0123 4567, [1] to 89AB CDEF,
|
||||
// and [2] to the 3x3 grid.
|
||||
string sLightCommands[3][2]; // sLightCommands[command][pad]
|
||||
|
||||
// Read the linearly arranged color data we've been given and split it into top and
|
||||
// bottom commands for each pad.
|
||||
for(int iPad = 0; iPad < 2; ++iPad) |
||||
{ |
||||
// If there's no data for this pad, leave the command empty.
|
||||
string sLightsDataForPad = sPanelLights[iPad]; |
||||
if(sLightsDataForPad.empty()) |
||||
continue; |
||||
|
||||
// Sanity check the lights data. For 4x4 lights, it should have 9*4*4*3 bytes of
|
||||
// data: RGB for each of 4x4 LEDs on 9 panels. For 25-light panels there should
|
||||
// be 4x4+3x3 (25) lights of data.
|
||||
int LightSize4x4 = 9*4*4*3; |
||||
int LightSize25 = 9*5*5*3; |
||||
if(sLightsDataForPad.size() != LightSize4x4 && sLightsDataForPad.size() != LightSize25) |
||||
{ |
||||
Log(ssprintf("SetLights: Lights data should be %i or %i bytes, received %i", |
||||
LightSize4x4, LightSize25, sLightsDataForPad.size())); |
||||
continue; |
||||
} |
||||
|
||||
// If we've been given 16 lights, pad to 25.
|
||||
if(sLightsDataForPad.size() == LightSize4x4) |
||||
sLightsDataForPad.append(LightSize25 - LightSize4x4, '\0'); |
||||
|
||||
// Lights are sent in three commands:
|
||||
//
|
||||
// 4: the 3x3 inner grid
|
||||
// 2: the top 4x2 lights
|
||||
// 3: the bottom 4x2 lights
|
||||
//
|
||||
// Command 4 is only used by firmware version 4+.
|
||||
//
|
||||
// Always send all three commands if the firmware expects it, even if we've
|
||||
// been given 4x4 data.
|
||||
sLightCommands[0][iPad] = "4"; |
||||
sLightCommands[1][iPad] = "2"; |
||||
sLightCommands[2][iPad] = "3"; |
||||
int iNextInputByte = 0; |
||||
auto scaleLight = [](uint8_t iColor) { |
||||
// Apply color scaling. Values over about 170 don't make the LEDs any brighter, so this
|
||||
// gives better contrast and draws less power.
|
||||
return uint8_t(iColor * 0.6666f); |
||||
}; |
||||
for(int iPanel = 0; iPanel < 9; ++iPanel) |
||||
{ |
||||
// Create the 2 and 3 commands.
|
||||
for(int iByte = 0; iByte < 4*4*3; ++iByte) |
||||
{ |
||||
uint8_t iColor = sLightsDataForPad[iNextInputByte++]; |
||||
iColor = scaleLight(iColor); |
||||
|
||||
int iCommandIndex = iByte < 4*2*3? 1:2; |
||||
sLightCommands[iCommandIndex][iPad].append(1, iColor); |
||||
} |
||||
|
||||
// Create the 4 command.
|
||||
for(int iByte = 0; iByte < 3*3*3; ++iByte) |
||||
{ |
||||
uint8_t iColor = sLightsDataForPad[iNextInputByte++]; |
||||
iColor = scaleLight(iColor); |
||||
sLightCommands[0][iPad].append(1, iColor); |
||||
} |
||||
} |
||||
|
||||
sLightCommands[0][iPad].push_back('\n'); |
||||
sLightCommands[1][iPad].push_back('\n'); |
||||
sLightCommands[2][iPad].push_back('\n'); |
||||
} |
||||
|
||||
// Each update adds one entry to m_aPendingLightsCommands for each lights command.
|
||||
//
|
||||
// If there are at least as many entries in m_aPendingLightsCommands as there are
|
||||
// commands to send, then lights updates are happening faster than they can be sent
|
||||
// to the pad. If that happens, replace the existing commands rather than adding
|
||||
// new ones.
|
||||
//
|
||||
// Make sure we 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.
|
||||
//
|
||||
// Note that m_aPendingLightsCommands contains the update for both pads, to guarantee
|
||||
// we always send light updates for both pads together and they never end up out of
|
||||
// phase.
|
||||
if(m_aPendingLightsCommands.size() < 3) |
||||
{ |
||||
// There's a subtle but important difference between command timing in
|
||||
// firmware version 4 compared to earlier versions:
|
||||
//
|
||||
// Earlier firmwares would process host commands as soon as they're received.
|
||||
// Because of this, we have to wait before sending the '3' command to give
|
||||
// the master controller time to finish sending the '2' command to panels.
|
||||
// If we don't do this everything will still work, but the master will block
|
||||
// while processing the second command waiting for panel data to finish sending
|
||||
// since the TX queue will be full. If this happens it isn't processing HID
|
||||
// data, which reduces input timing accuracy.
|
||||
//
|
||||
// Firmware version 4 won't process a host command if there's data still being
|
||||
// sent to the panels. It'll wait until the data is flushed. This means that
|
||||
// we can queue all three lights commands at once, and just send them as fast
|
||||
// as the host acknowledges them. The second command will sit around on the
|
||||
// master controller's buffer until it finishes sending the first command to
|
||||
// the panels, then the third command will do the same.
|
||||
//
|
||||
// This change is only needed due to the larger amount of data sent in 25-light
|
||||
// mode. Since we're spending more time sending data from the master to the
|
||||
// panels, the timing requirements are tighter. Doing it in the same manual-delay
|
||||
// fashion causes too much latency and makes it harder to maintain 30 FPS.
|
||||
//
|
||||
// If two controllers are connected, they should either both be 4+ or not. We
|
||||
// don't handle the case where they're different and both timings are needed.
|
||||
double fNow = GetMonotonicTime(); |
||||
double fSendCommandAt = max(fNow, m_fDelayLightCommandsUntil); |
||||
double fCommandTimes[3] = { fNow, fNow, fNow }; |
||||
|
||||
bool masterIsV4 = false; |
||||
bool anyMasterConnected = false; |
||||
for(int iPad = 0; iPad < 2; ++iPad) |
||||
{ |
||||
SMXConfig config; |
||||
if(!m_pDevices[iPad]->GetConfigLocked(config)) |
||||
continue; |
||||
|
||||
anyMasterConnected = true; |
||||
if(config.masterVersion >= 4) |
||||
masterIsV4 = true; |
||||
} |
||||
|
||||
// If we don't have the config yet, the master is in the process of connecting, so don't
|
||||
// queue lights.
|
||||
if(!anyMasterConnected) |
||||
return; |
||||
|
||||
// If we're on master firmware < 4, set delay times. For 4+, just queue commands.
|
||||
// We don't need to set fCommandTimes[0] since the '4' packet won't be sent.
|
||||
if(!masterIsV4) |
||||
{ |
||||
const double fDelayBetweenLightsCommands = 1/60.0; |
||||
fCommandTimes[1] = fSendCommandAt; |
||||
fCommandTimes[2] = fCommandTimes[1] + fDelayBetweenLightsCommands; |
||||
} |
||||
|
||||
// Update m_fDelayLightCommandsUntil, so we know when the next
|
||||
// lights command can be sent.
|
||||
m_fDelayLightCommandsUntil = fSendCommandAt + 1/30.0f; |
||||
|
||||
// Add three commands to the list, scheduled at fFirstCommandTime and fSecondCommandTime.
|
||||
m_aPendingLightsCommands.push_back(PendingCommand(fCommandTimes[0])); |
||||
m_aPendingLightsCommands.push_back(PendingCommand(fCommandTimes[1])); |
||||
m_aPendingLightsCommands.push_back(PendingCommand(fCommandTimes[2])); |
||||
} |
||||
|
||||
// Set the pad commands.
|
||||
for(int iPad = 0; iPad < 2; ++iPad) |
||||
{ |
||||
// If the command for this pad is empty, leave any existing pad command alone.
|
||||
if(sLightCommands[0][iPad].empty()) |
||||
continue; |
||||
|
||||
SMXConfig config; |
||||
if(!m_pDevices[iPad]->GetConfigLocked(config)) |
||||
continue; |
||||
|
||||
// If this pad is firmware version 4, send the 4 command. Otherwise, leave the 4 command
|
||||
// empty and no command will be sent.
|
||||
PendingCommand *pPending4Commands = &m_aPendingLightsCommands[m_aPendingLightsCommands.size()-3]; // 3
|
||||
if(config.masterVersion >= 4) |
||||
pPending4Commands->sPadCommand[iPad] = sLightCommands[0][iPad]; |
||||
else |
||||
pPending4Commands->sPadCommand[iPad] = ""; |
||||
|
||||
PendingCommand *pPending2Commands = &m_aPendingLightsCommands[m_aPendingLightsCommands.size()-2]; // 2
|
||||
pPending2Commands->sPadCommand[iPad] = sLightCommands[1][iPad]; |
||||
|
||||
PendingCommand *pPending3Commands = &m_aPendingLightsCommands[m_aPendingLightsCommands.size()-1]; // 3
|
||||
pPending3Commands->sPadCommand[iPad] = sLightCommands[2][iPad]; |
||||
} |
||||
|
||||
// Wake up the I/O thread if it's blocking on WaitForMultipleObjectsEx.
|
||||
SetEvent(m_hEvent->value()); |
||||
} |
||||
|
||||
void SMX::SMXManager::SetPlatformLights(const string sPanelLights[2]) |
||||
{ |
||||
g_Lock.AssertNotLockedByCurrentThread(); |
||||
LockMutex L(g_Lock); |
||||
|
||||
// Read the linearly arranged color data we've been given and split it into top and
|
||||
// bottom commands for each pad.
|
||||
for(int iPad = 0; iPad < 2; ++iPad) |
||||
{ |
||||
// If there's no data for this pad, skip it.
|
||||
string sLightsDataForPad = sPanelLights[iPad]; |
||||
if(sLightsDataForPad.empty()) |
||||
continue; |
||||
|
||||
if(sLightsDataForPad.size() != 44*3) |
||||
{ |
||||
Log(ssprintf("SetPlatformLights: Platform lights data should be %i bytes, received %i", |
||||
44*3, sLightsDataForPad.size())); |
||||
continue; |
||||
} |
||||
|
||||
// If this master doesn't support this, skip it.
|
||||
SMXConfig config; |
||||
if(!m_pDevices[iPad]->GetConfigLocked(config)) |
||||
continue; |
||||
if(config.masterVersion < 4) |
||||
continue; |
||||
|
||||
string sLightCommand; |
||||
sLightCommand.push_back('L'); |
||||
sLightCommand.push_back(0); // LED strip index (always 0)
|
||||
sLightCommand.push_back(44); // number of LEDs to set
|
||||
sLightCommand += sLightsDataForPad; |
||||
|
||||
m_pDevices[iPad]->SendCommandLocked(sLightCommand); |
||||
} |
||||
|
||||
// Wake up the I/O thread if it's blocking on WaitForMultipleObjectsEx.
|
||||
SetEvent(m_hEvent->value()); |
||||
} |
||||
|
||||
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_aPendingLightsCommands.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_aPendingLightsCommands.
|
||||
void SMX::SMXManager::SendLightUpdates() |
||||
{ |
||||
g_Lock.AssertLockedByCurrentThread(); |
||||
|
||||
// If previous lights commands are being sent, wait for them to complete before
|
||||
// queueing more.
|
||||
if(m_iLightsCommandsInProgress > 0) |
||||
return; |
||||
|
||||
// If we have more than one command queued, we can queue several of them if we're
|
||||
// before fTimeToSend. For the V4 pads that require more commands, this lets us queue
|
||||
// the whole lights update at once. V3 pads require us to time commands, so we can't
|
||||
// spam both lights commands at once, which is handled by fTimeToSend.
|
||||
while( !m_aPendingLightsCommands.empty() ) |
||||
{ |
||||
// Send the lights command for each pad. If either pad isn't connected, this won't do
|
||||
// anything.
|
||||
const PendingCommand &command = m_aPendingLightsCommands[0]; |
||||
|
||||
// See if it's time to send this command.
|
||||
if(command.fTimeToSend > GetMonotonicTime()) |
||||
break; |
||||
|
||||
for(int iPad = 0; iPad < 2; ++iPad) |
||||
{ |
||||
if(!command.sPadCommand[iPad].empty()) |
||||
{ |
||||
// Count the number of commands we've queued. We won't send any more until
|
||||
// this reaches 0 and all queued commands were sent.
|
||||
m_iLightsCommandsInProgress++; |
||||
|
||||
// The completion callback is guaranteed to always be called, even if the controller
|
||||
// disconnects and the command wasn't sent.
|
||||
m_pDevices[iPad]->SendCommandLocked(command.sPadCommand[iPad], [this, iPad](string response) { |
||||
g_Lock.AssertLockedByCurrentThread(); |
||||
m_iLightsCommandsInProgress--; |
||||
}); |
||||
} |
||||
} |
||||
|
||||
// Remove the command we've sent.
|
||||
m_aPendingLightsCommands.erase(m_aPendingLightsCommands.begin(), m_aPendingLightsCommands.begin()+1); |
||||
} |
||||
} |
||||
|
||||
void SMX::SMXManager::SetPanelTestMode(PanelTestMode mode) |
||||
{ |
||||
g_Lock.AssertNotLockedByCurrentThread(); |
||||
LockMutex Lock(g_Lock); |
||||
m_PanelTestMode = mode; |
||||
} |
||||
|
||||
void SMX::SMXManager::UpdatePanelTestMode() |
||||
{ |
||||
// If the test mode has changed, send the new test mode.
|
||||
//
|
||||
// When the test mode is enabled, send the test mode again periodically, or it'll time
|
||||
// out on the master and be turned off. Don't repeat the PanelTestMode_Off command.
|
||||
g_Lock.AssertLockedByCurrentThread(); |
||||
uint32_t now = GetTickCount(); |
||||
if(m_PanelTestMode == m_LastSentPanelTestMode &&
|
||||
(m_PanelTestMode == PanelTestMode_Off || now - m_SentPanelTestModeAtTicks < 1000)) |
||||
return; |
||||
|
||||
// When we first send the test mode command (not for repeats), turn off lights.
|
||||
if(m_LastSentPanelTestMode == PanelTestMode_Off) |
||||
{ |
||||
// The 'l' command used to set lights, but it's now only used to turn lights off
|
||||
// for cases like this.
|
||||
string sData = "l"; |
||||
sData.append(108, 0); |
||||
sData += "\n"; |
||||
for(int iPad = 0; iPad < 2; ++iPad) |
||||
m_pDevices[iPad]->SendCommandLocked(sData); |
||||
} |
||||
|
||||
m_SentPanelTestModeAtTicks = now; |
||||
m_LastSentPanelTestMode = m_PanelTestMode; |
||||
for(int iPad = 0; iPad < 2; ++iPad) |
||||
m_pDevices[iPad]->SendCommandLocked(ssprintf("t %c\n", m_PanelTestMode)); |
||||
} |
||||
|
||||
// Assign a serial number to master controllers if one isn't already assigned. This
|
||||
// will have no effect if a serial is already set.
|
||||
//
|
||||
// We just assign a random number. The serial number will be used as the USB serial
|
||||
// number, and can be queried in SMXInfo.
|
||||
void SMX::SMXManager::SetSerialNumbers() |
||||
{ |
||||
g_Lock.AssertNotLockedByCurrentThread(); |
||||
LockMutex L(g_Lock); |
||||
|
||||
m_aPendingLightsCommands.clear(); |
||||
for(int iPad = 0; iPad < 2; ++iPad) |
||||
{ |
||||
string sData = "s"; |
||||
uint8_t serial[16]; |
||||
SMX::GenerateRandom(serial, sizeof(serial)); |
||||
sData.append((char *) serial, sizeof(serial)); |
||||
sData.append(1, '\n'); |
||||
|
||||
m_pDevices[iPad]->SendCommandLocked(sData); |
||||
} |
||||
} |
||||
|
||||
void SMX::SMXManager::RunInHelperThread(function<void()> func) |
||||
{ |
||||
m_UserCallbackThread.RunInThread(func); |
||||
} |
||||
|
||||
// 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,96 @@ |
||||
#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: |
||||
// Our singleton:
|
||||
static shared_ptr<SMXManager> g_pSMX; |
||||
|
||||
// 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 sLights[2]); |
||||
void SetPlatformLights(const string sLights[2]); |
||||
void ReenableAutoLights(); |
||||
void SetPanelTestMode(PanelTestMode mode); |
||||
void SetSerialNumbers(); |
||||
void SetOnlySendLightsOnChange(bool value) { m_bOnlySendLightsOnChange = value; } |
||||
|
||||
// Run a function in the user callback thread.
|
||||
void RunInHelperThread(function<void()> func); |
||||
|
||||
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_aPendingLightsCommands; |
||||
int m_iLightsCommandsInProgress = 0; |
||||
double m_fDelayLightCommandsUntil = 0; |
||||
|
||||
// Panel test mode. This is separate from the sensor test mode (pressure display),
|
||||
// which is handled in SMXDevice.
|
||||
void UpdatePanelTestMode(); |
||||
uint32_t m_SentPanelTestModeAtTicks = 0; |
||||
PanelTestMode m_PanelTestMode = PanelTestMode_Off; |
||||
PanelTestMode m_LastSentPanelTestMode = PanelTestMode_Off; |
||||
|
||||
bool m_bOnlySendLightsOnChange = false; |
||||
}; |
||||
} |
||||
|
||||
#endif |
@ -0,0 +1,519 @@ |
||||
// Handle playing GIF animations from inside SMXConfig.
|
||||
//
|
||||
// This can load two GIF animations, one for when panels are released
|
||||
// and one for when they're pressed, and play them automatically on the
|
||||
// pad in the background. Applications that control lights can do more
|
||||
// sophisticated things with the lights, but this gives an easy way for
|
||||
// people to create simple animations.
|
||||
//
|
||||
// If you're implementing the SDK in a game, you don't need this and should
|
||||
// use SMX.h instead.
|
||||
//
|
||||
// An animation is a single GIF with animations for all panels, in the
|
||||
// following layout:
|
||||
//
|
||||
// 0000|1111|2222
|
||||
// 0000|1111|2222
|
||||
// 0000|1111|2222
|
||||
// 0000|1111|2222
|
||||
// --------------
|
||||
// 3333|4444|5555
|
||||
// 3333|4444|5555
|
||||
// 3333|4444|5555
|
||||
// 3333|4444|5555
|
||||
// --------------
|
||||
// 6666|7777|8888
|
||||
// 6666|7777|8888
|
||||
// 6666|7777|8888
|
||||
// 6666|7777|8888
|
||||
// x-------------
|
||||
//
|
||||
// The - | regions are ignored and are only there to space out the animation
|
||||
// to make it easier to view.
|
||||
//
|
||||
// The extra bottom row is a flag row and should normally be black. The first
|
||||
// pixel (bottom-left) optionally marks a loop frame. By default, the animation
|
||||
// plays all the way through and then loops back to the beginning. If the loop
|
||||
// frame pixel is white, it marks a frame to loop to instead of the beginning.
|
||||
// This allows pressed animations to have a separate lead-in and loop.
|
||||
//
|
||||
// Each animation is for a single pad. You can load the same animation for both
|
||||
// pads or use different ones.
|
||||
|
||||
#include "SMXPanelAnimation.h" |
||||
#include "SMXManager.h" |
||||
#include "SMXDevice.h" |
||||
#include "SMXThread.h" |
||||
using namespace std; |
||||
using namespace SMX; |
||||
|
||||
namespace { |
||||
Mutex g_Lock; |
||||
} |
||||
|
||||
#define LIGHTS_PER_PANEL 25 |
||||
|
||||
// XXX: go to sleep if there are no pads connected
|
||||
|
||||
struct AnimationState |
||||
{ |
||||
SMXPanelAnimation animation; |
||||
|
||||
// Seconds into the animation:
|
||||
float fTime = 0; |
||||
|
||||
// The currently displayed frame:
|
||||
int iCurrentFrame = 0; |
||||
|
||||
bool bPlaying = false; |
||||
|
||||
double m_fLastUpdateTime = -1; |
||||
|
||||
// Return the current animation frame.
|
||||
const vector<SMXGif::Color> &GetAnimationFrame() const |
||||
{ |
||||
// If we're not playing, return an empty array. As a sanity check, do this
|
||||
// if the frame is out of bounds too.
|
||||
if(!bPlaying || iCurrentFrame >= animation.m_aPanelGraphics.size()) |
||||
{ |
||||
static vector<SMXGif::Color> dummy; |
||||
return dummy; |
||||
} |
||||
|
||||
return animation.m_aPanelGraphics[iCurrentFrame]; |
||||
} |
||||
|
||||
// Start the animation if it's not playing.
|
||||
void Play() |
||||
{ |
||||
bPlaying = true; |
||||
} |
||||
|
||||
// Stop and disable the animation.
|
||||
void Stop() |
||||
{ |
||||
bPlaying = false; |
||||
Rewind(); |
||||
} |
||||
|
||||
// Reset to the first frame.
|
||||
void Rewind() |
||||
{ |
||||
fTime = 0; |
||||
iCurrentFrame = 0; |
||||
} |
||||
|
||||
// Advance the animation by fSeconds.
|
||||
void Update() |
||||
{ |
||||
// fSeconds is the time since the last update:
|
||||
double fNow = SMX::GetMonotonicTime(); |
||||
double fSeconds = m_fLastUpdateTime == -1? 0: (fNow - m_fLastUpdateTime); |
||||
m_fLastUpdateTime = fNow; |
||||
|
||||
if(!bPlaying || animation.m_aPanelGraphics.empty()) |
||||
return; |
||||
|
||||
// If the current frame is past the end, a new animation was probably
|
||||
// loaded.
|
||||
if(iCurrentFrame >= animation.m_aPanelGraphics.size()) |
||||
Rewind(); |
||||
|
||||
// Advance time.
|
||||
fTime += fSeconds; |
||||
|
||||
// If we're still on this frame, we're done.
|
||||
float fFrameDuration = animation.m_iFrameDurations[iCurrentFrame]; |
||||
if(fTime - 0.00001f < fFrameDuration) |
||||
return; |
||||
|
||||
// If we've passed the end of the frame, move to the next frame. Don't
|
||||
// skip frames if we're updating too quickly.
|
||||
fTime -= fFrameDuration; |
||||
if(fTime > 0) |
||||
fTime = 0; |
||||
|
||||
// Advance the frame.
|
||||
iCurrentFrame++; |
||||
|
||||
// If we're at the end of the frame, rewind to the loop frame.
|
||||
if(iCurrentFrame == animation.m_aPanelGraphics.size()) |
||||
iCurrentFrame = animation.m_iLoopFrame; |
||||
} |
||||
}; |
||||
|
||||
struct AnimationStateForPad |
||||
{ |
||||
// asLightsData is an array of lights data to send to the pad and graphic
|
||||
// is an animation graphic. Overlay graphic on top of the lights.
|
||||
void OverlayLights(char *asLightsData, const vector<SMXGif::Color> &graphic) const |
||||
{ |
||||
// Stop if this graphic isn't loaded or is paused.
|
||||
if(graphic.empty()) |
||||
return; |
||||
|
||||
for(int i = 0; i < graphic.size(); ++i) |
||||
{ |
||||
if(i >= LIGHTS_PER_PANEL) |
||||
return; |
||||
|
||||
// If this color is transparent, leave the released animation alone.
|
||||
if(graphic[i].color[3] == 0) |
||||
continue; |
||||
|
||||
asLightsData[i*3+0] = graphic[i].color[0]; |
||||
asLightsData[i*3+1] = graphic[i].color[1]; |
||||
asLightsData[i*3+2] = graphic[i].color[2]; |
||||
} |
||||
} |
||||
|
||||
// Return the command to set the current animation state as pad lights.
|
||||
string GetLightsCommand(int iPadState, const SMXConfig &config) const |
||||
{ |
||||
g_Lock.AssertLockedByCurrentThread(); |
||||
|
||||
// If AutoLightingUsePressedAnimations is set, use lights animations.
|
||||
// If it's not (the config tool is set to step color), mimic the built-in
|
||||
// step color behavior instead of using pressed animations. Any released
|
||||
// animation will always be used.
|
||||
bool bUsePressedAnimations = config.flags & PlatformFlags_AutoLightingUsePressedAnimations; |
||||
|
||||
const int iBytesPerPanel = LIGHTS_PER_PANEL*3; |
||||
const int iTotalLights = 9*iBytesPerPanel; |
||||
string result(iTotalLights, 0); |
||||
|
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
// The portion of lights data for this panel:
|
||||
char *out = &result[panel*iBytesPerPanel]; |
||||
|
||||
// Skip this panel if it's not in autoLightPanelMask.
|
||||
if(!(config.autoLightPanelMask & (1 << panel))) |
||||
continue; |
||||
|
||||
// Add the released animation, then overlay the pressed animation if we're pressed.
|
||||
OverlayLights(out, animations[SMX_LightsType_Released][panel].GetAnimationFrame()); |
||||
bool bPressed = bool(iPadState & (1 << panel)); |
||||
if(bPressed && bUsePressedAnimations) |
||||
OverlayLights(out, animations[SMX_LightsType_Pressed][panel].GetAnimationFrame()); |
||||
else if(bPressed && !bUsePressedAnimations) |
||||
{ |
||||
// Light all LEDs on this panel using stepColor.
|
||||
double LightsScaleFactor = 0.666666f; |
||||
const uint8_t *color = &config.stepColor[panel*3]; |
||||
|
||||
for(int light = 0; light < LIGHTS_PER_PANEL; ++light) |
||||
{ |
||||
for(int i = 0; i < 3; ++i) |
||||
{ |
||||
// stepColor is scaled to the 0-170 range. Scale it back to the 0-255 range.
|
||||
// User applications don't need to worry about this since they normally don't
|
||||
// need to care about stepColor.
|
||||
uint8_t c = color[i]; |
||||
c = (uint8_t) lrintf(min(255.0f, c / LightsScaleFactor)); |
||||
out[light*3+i] = c; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
// State for both animations on each panel:
|
||||
AnimationState animations[NUM_SMX_LightsType][9]; |
||||
}; |
||||
|
||||
namespace |
||||
{ |
||||
// Animations and animation states for both pads.
|
||||
AnimationStateForPad pad_states[2]; |
||||
} |
||||
|
||||
namespace { |
||||
// The X,Y positions of each possible panel.
|
||||
vector<pair<int,int>> graphic_positions = { |
||||
{ 0,0 }, |
||||
{ 1,0 }, |
||||
{ 2,0 }, |
||||
{ 0,1 }, |
||||
{ 1,1 }, |
||||
{ 2,1 }, |
||||
{ 0,2 }, |
||||
{ 1,2 }, |
||||
{ 2,2 }, |
||||
}; |
||||
|
||||
// Given a 14x15 graphic frame and a panel number, return an array of 16 colors, containing
|
||||
// each light in the order it's sent to the master controller.
|
||||
void ConvertToPanelGraphic16(const SMXGif::GIFImage &src, vector<SMXGif::Color> &dst, int panel) |
||||
{ |
||||
dst.clear(); |
||||
|
||||
// The top-left corner for this panel:
|
||||
int x = graphic_positions[panel].first * 5; |
||||
int y = graphic_positions[panel].second * 5; |
||||
|
||||
// Add the 4x4 grid.
|
||||
for(int dy = 0; dy < 4; ++dy) |
||||
for(int dx = 0; dx < 4; ++dx) |
||||
dst.push_back(src.get(x+dx, y+dy)); |
||||
|
||||
// These animations have no data for the 3x3 grid, so just set them to transparent.
|
||||
for(int dy = 0; dy < 3; ++dy) |
||||
for(int dx = 0; dx < 3; ++dx) |
||||
dst.push_back(SMXGif::Color(0,0,0,0)); |
||||
} |
||||
|
||||
// Given a 23x24 graphic frame and a panel number, return an array of 25 colors, containing
|
||||
// each light in the order it's sent to the master controller.
|
||||
void ConvertToPanelGraphic25(const SMXGif::GIFImage &src, vector<SMXGif::Color> &dst, int panel) |
||||
{ |
||||
dst.clear(); |
||||
|
||||
// The top-left corner for this panel:
|
||||
int x = graphic_positions[panel].first * 8; |
||||
int y = graphic_positions[panel].second * 8; |
||||
|
||||
// Add the 4x4 grid first.
|
||||
for(int dy = 0; dy < 4; ++dy) |
||||
for(int dx = 0; dx < 4; ++dx) |
||||
dst.push_back(src.get(x+dx*2, y+dy*2)); |
||||
|
||||
// Add the 3x3 grid.
|
||||
for(int dy = 0; dy < 3; ++dy) |
||||
for(int dx = 0; dx < 3; ++dx) |
||||
dst.push_back(src.get(x+dx*2+1, y+dy*2+1)); |
||||
} |
||||
} |
||||
|
||||
// Load an array of animation frames as a panel animation. Each frame must
|
||||
// be 14x15 or 23x24.
|
||||
void SMXPanelAnimation::Load(const vector<SMXGif::SMXGifFrame> &frames, int panel) |
||||
{ |
||||
m_aPanelGraphics.clear(); |
||||
m_iFrameDurations.clear(); |
||||
m_iLoopFrame = -1; |
||||
|
||||
for(int frame_no = 0; frame_no < frames.size(); ++frame_no) |
||||
{ |
||||
const SMXGif::SMXGifFrame &gif_frame = frames[frame_no]; |
||||
|
||||
// If the bottom-left pixel is white, this is the loop frame, which marks the
|
||||
// frame the animation should start at after a loop. This is global to the
|
||||
// animation, not specific to each panel.
|
||||
SMXGif::Color marker = gif_frame.frame.get(0, gif_frame.frame.height-1); |
||||
if(marker.color[3] == 0xFF && marker.color[0] >= 0x80) |
||||
{ |
||||
// We shouldn't see more than one of these. If we do, use the first.
|
||||
if(m_iLoopFrame == -1) |
||||
m_iLoopFrame = frame_no; |
||||
} |
||||
|
||||
// Extract this frame. If the graphic is 14x15 it's a 4x4 animation,
|
||||
// and if it's 23x24 it's 25-light.
|
||||
vector<SMXGif::Color> panel_graphic; |
||||
if(frames[0].width == 14) |
||||
ConvertToPanelGraphic16(gif_frame.frame, panel_graphic, panel); |
||||
else |
||||
ConvertToPanelGraphic25(gif_frame.frame, panel_graphic, panel); |
||||
|
||||
// GIFs have a very low-resolution duration field, with 10ms units.
|
||||
// The panels run at 30 FPS internally, or 33 1/3 ms, but GIF can only
|
||||
// represent 30ms or 40ms. Most applications will probably output 30,
|
||||
// but snap both 30ms and 40ms to exactly 30 FPS to make sure animations
|
||||
// that are meant to run at native framerate do.
|
||||
float seconds; |
||||
if(gif_frame.milliseconds == 30 || gif_frame.milliseconds == 40) |
||||
seconds = 1 / 30.0f; |
||||
else |
||||
seconds = gif_frame.milliseconds / 1000.0; |
||||
|
||||
m_aPanelGraphics.push_back(panel_graphic); |
||||
m_iFrameDurations.push_back(seconds); |
||||
} |
||||
|
||||
// By default, loop back to the first frame.
|
||||
if(m_iLoopFrame == -1) |
||||
m_iLoopFrame = 0; |
||||
} |
||||
|
||||
#include "SMXPanelAnimationUpload.h" |
||||
|
||||
// Load a GIF into SMXLoadedPanelAnimations::animations.
|
||||
bool SMX_LightsAnimation_Load(const char *gif, int size, int pad, SMX_LightsType type, const char **error) |
||||
{ |
||||
// Parse the GIF.
|
||||
string buf(gif, size); |
||||
vector<SMXGif::SMXGifFrame> frames; |
||||
if(!SMXGif::DecodeGIF(buf, frames) || frames.empty()) |
||||
{ |
||||
*error = "The GIF couldn't be read."; |
||||
return false; |
||||
} |
||||
|
||||
// Check the dimensions of the image. We only need to check the first, the
|
||||
// others will always have the same size.
|
||||
if((frames[0].width != 14 || frames[0].height != 15) && (frames[0].width != 23 || frames[0].height != 24)) |
||||
{ |
||||
*error = "The GIF must be 14x15 or 23x24."; |
||||
return false; |
||||
} |
||||
|
||||
// Load the graphics into SMXPanelAnimations.
|
||||
SMXPanelAnimation animations[9]; |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
animations[panel].Load(frames, panel); |
||||
|
||||
// Set up the upload for this graphic.
|
||||
if(!SMX_LightsUpload_PrepareUpload(pad, type, animations, error)) |
||||
return false; |
||||
|
||||
// Lock while we access pad_states.
|
||||
g_Lock.AssertNotLockedByCurrentThread(); |
||||
LockMutex L(g_Lock); |
||||
|
||||
// Commit the animation to pad_states now that we know there are no errors.
|
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
SMXPanelAnimation &animation = pad_states[pad].animations[type][panel].animation; |
||||
animation = animations[panel]; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
namespace |
||||
{ |
||||
double g_fStopAnimatingUntil = -1; |
||||
} |
||||
|
||||
void SMXAutoPanelAnimations::TemporaryStopAnimating() |
||||
{ |
||||
// Stop animating for 100ms.
|
||||
double fStopForSeconds = 1/10.0f; |
||||
g_fStopAnimatingUntil = SMX::GetMonotonicTime() + fStopForSeconds; |
||||
} |
||||
|
||||
// A thread to handle setting light animations. We do this in a separate
|
||||
// thread rather than in the SMXManager thread so this can be treated as
|
||||
// if it's external application thread, and it's making normal threaded
|
||||
// calls to SetLights.
|
||||
class PanelAnimationThread: public SMXThread |
||||
{ |
||||
public: |
||||
static shared_ptr<PanelAnimationThread> g_pSingleton; |
||||
PanelAnimationThread(): |
||||
SMXThread(g_Lock) |
||||
{ |
||||
Start("SMX light animations"); |
||||
} |
||||
|
||||
private: |
||||
void ThreadMain() |
||||
{ |
||||
m_Lock.Lock(); |
||||
|
||||
// Update lights at 30 FPS.
|
||||
const int iDelayMS = 33; |
||||
|
||||
while(!m_bShutdown) |
||||
{ |
||||
// Check if we've temporarily stopped updating lights.
|
||||
bool bSkipUpdate = g_fStopAnimatingUntil > SMX::GetMonotonicTime(); |
||||
|
||||
// Run a single panel lights update.
|
||||
if(!bSkipUpdate) |
||||
UpdateLights(); |
||||
|
||||
// Wait up to 30 FPS, or until we're signalled. We can only be signalled
|
||||
// if we're shutting down, so we don't need to worry about partial frame
|
||||
// delays.
|
||||
m_Event.Wait(iDelayMS); |
||||
} |
||||
|
||||
m_Lock.Unlock(); |
||||
} |
||||
|
||||
// Return lights for the given pad and pad state, using the loaded panel animations.
|
||||
bool GetCurrentLights(string &asLightsDataOut, int pad, int iPadState) |
||||
{ |
||||
m_Lock.AssertLockedByCurrentThread(); |
||||
|
||||
// Get this pad's configuration.
|
||||
SMXConfig config; |
||||
if(!SMXManager::g_pSMX->GetDevice(pad)->GetConfig(config)) |
||||
return false; |
||||
|
||||
// If this controller handles animation itself, don't handle it here too. It can
|
||||
// lead to confusing situations if SMXConfig's animations don't match the ones stored
|
||||
// on the pad.
|
||||
if(config.masterVersion >= 4) |
||||
return false; |
||||
|
||||
AnimationStateForPad &pad_state = pad_states[pad]; |
||||
|
||||
// Make sure the correct animations are playing.
|
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
// The released animation is always playing.
|
||||
pad_state.animations[SMX_LightsType_Released][panel].Play(); |
||||
|
||||
// The pressed animation only plays while the button is pressed,
|
||||
// and rewind when it's released.
|
||||
bool bPressed = iPadState & (1 << panel); |
||||
if(bPressed) |
||||
pad_state.animations[SMX_LightsType_Pressed][panel].Play(); |
||||
else |
||||
pad_state.animations[SMX_LightsType_Pressed][panel].Stop(); |
||||
} |
||||
|
||||
// Set the current state.
|
||||
asLightsDataOut = pad_state.GetLightsCommand(iPadState, config); |
||||
|
||||
// Advance animations.
|
||||
for(int type = 0; type < NUM_SMX_LightsType; ++type) |
||||
{ |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
pad_state.animations[type][panel].Update(); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
// Run a single light animation update.
|
||||
void UpdateLights() |
||||
{ |
||||
string asLightsData[2]; |
||||
bool bHaveLights = false; |
||||
for(int pad = 0; pad < 2; pad++) |
||||
{ |
||||
int iPadState = SMXManager::g_pSMX->GetDevice(pad)->GetInputState(); |
||||
if(GetCurrentLights(asLightsData[pad], pad, iPadState)) |
||||
bHaveLights = true; |
||||
} |
||||
|
||||
// Update lights.
|
||||
if(bHaveLights) |
||||
SMXManager::g_pSMX->SetLights(asLightsData); |
||||
} |
||||
}; |
||||
|
||||
void SMX_LightsAnimation_SetAuto(bool enable) |
||||
{ |
||||
if(!enable) |
||||
{ |
||||
// If we're turning off, shut down the thread if it's running.
|
||||
if(PanelAnimationThread::g_pSingleton) |
||||
PanelAnimationThread::g_pSingleton->Shutdown(); |
||||
PanelAnimationThread::g_pSingleton.reset(); |
||||
return; |
||||
} |
||||
|
||||
// Create the animation thread if it's not already running.
|
||||
if(PanelAnimationThread::g_pSingleton) |
||||
return; |
||||
PanelAnimationThread::g_pSingleton.reset(new PanelAnimationThread()); |
||||
} |
||||
|
||||
shared_ptr<PanelAnimationThread> PanelAnimationThread::g_pSingleton; |
@ -0,0 +1,57 @@ |
||||
#ifndef SMXPanelAnimation_h |
||||
#define SMXPanelAnimation_h |
||||
|
||||
#include <vector> |
||||
#include "SMXGif.h" |
||||
|
||||
enum SMX_LightsType |
||||
{ |
||||
SMX_LightsType_Released, // animation while panels are released
|
||||
SMX_LightsType_Pressed, // animation while panel is pressed
|
||||
NUM_SMX_LightsType, |
||||
}; |
||||
|
||||
// SMXPanelAnimation holds an animation, with graphics for a single panel.
|
||||
class SMXPanelAnimation |
||||
{ |
||||
public: |
||||
void Load(const std::vector<SMXGif::SMXGifFrame> &frames, int panel); |
||||
|
||||
// The high-level animated GIF frames:
|
||||
std::vector<std::vector<SMXGif::Color>> m_aPanelGraphics; |
||||
|
||||
// The animation starts on frame 0. When it reaches the end, it loops
|
||||
// back to this frame.
|
||||
int m_iLoopFrame = 0; |
||||
|
||||
// The duration of each frame in seconds.
|
||||
std::vector<float> m_iFrameDurations; |
||||
}; |
||||
|
||||
namespace SMXAutoPanelAnimations |
||||
{ |
||||
// If SMX_LightsAnimation_SetAuto is active, stop sending animations briefly. This is
|
||||
// called when lights are set directly, so they don't compete with the animation.
|
||||
void TemporaryStopAnimating(); |
||||
} |
||||
|
||||
// For SMX_API:
|
||||
#include "../SMX.h" |
||||
|
||||
// High-level interface for C# bindings:
|
||||
//
|
||||
// Load an animated GIF as a panel animation. pad is the pad this animation is for (0 or 1),
|
||||
// and type is which animation this is for. Any previously loaded animation will be replaced.
|
||||
// On error, false is returned and error is set to a plain-text error message which is valid
|
||||
// until the next call. On success, the animation can be uploaded to the pad if supported using
|
||||
// SMX_LightsUpload_BeginUpload, or used directly with SMX_LightsAnimation_SetAuto.
|
||||
SMX_API bool SMX_LightsAnimation_Load(const char *gif, int size, int pad, SMX_LightsType type, const char **error); |
||||
|
||||
// Enable or disable automatically handling lights animations. If enabled, any animations
|
||||
// loaded with SMX_LightsAnimation_Load will run automatically as long as the SDK is loaded.
|
||||
// This only has an effect if the platform doesn't handle animations directly. On newer firmware,
|
||||
// this has no effect (upload the animation to the panel instead).
|
||||
// XXX: should we automatically disable SMX_SetLights when this is enabled?
|
||||
SMX_API void SMX_LightsAnimation_SetAuto(bool enable); |
||||
|
||||
#endif |
@ -0,0 +1,484 @@ |
||||
#include "SMXPanelAnimationUpload.h" |
||||
#include "SMXPanelAnimation.h" |
||||
#include "SMXGif.h" |
||||
#include "SMXManager.h" |
||||
#include "SMXDevice.h" |
||||
#include "Helpers.h" |
||||
#include <string> |
||||
#include <vector> |
||||
using namespace std; |
||||
using namespace SMX; |
||||
|
||||
// This handles setting up commands to upload panel animations to the
|
||||
// controller.
|
||||
//
|
||||
// This is only meant to be used by configuration tools to allow setting
|
||||
// up animations that work while the pad isn't being controlled by the
|
||||
// SDK. If you want to control lights for your game, this isn't what
|
||||
// you want. Use SMX_SetLights instead.
|
||||
//
|
||||
// Panel animations are sent to the master controller one panel at a time, and
|
||||
// each animation can take several commands to upload to fit in the protocol packet
|
||||
// size. These commands are stateful.
|
||||
namespace |
||||
{ |
||||
// Panel names for error messages.
|
||||
static const char *panel_names[] = { |
||||
"up-left", "up", "up-right", |
||||
"left", "center", "right", |
||||
"down-left", "down", "down-right", |
||||
}; |
||||
} |
||||
|
||||
// These structs are the protocol we use to send offline graphics to the pad.
|
||||
// This isn't related to realtime lighting.
|
||||
namespace PanelLightGraphic |
||||
{ |
||||
// One 24-bit RGB color:
|
||||
struct color_t { |
||||
uint8_t rgb[3]; |
||||
}; |
||||
|
||||
// 4-bit palette, 15 colors. Our graphics are 4-bit. Color 0xF is transparent,
|
||||
// so we don't have a palette entry for it.
|
||||
struct palette_t { |
||||
color_t colors[15]; |
||||
}; |
||||
|
||||
// A single 4-bit paletted graphic.
|
||||
struct graphic_t { |
||||
uint8_t data[13]; |
||||
}; |
||||
|
||||
struct panel_animation_data_t |
||||
{ |
||||
// Our graphics and palettes. We can apply either palette to any graphic. Note that
|
||||
// each graphic is 13 bytes and each palette is 45 bytes.
|
||||
graphic_t graphics[64]; |
||||
palette_t palettes[2]; |
||||
}; |
||||
|
||||
struct animation_timing_t |
||||
{ |
||||
// An index into frames[]:
|
||||
uint8_t loop_animation_frame; |
||||
|
||||
// A list of graphic frames to display, and how long to display them in
|
||||
// 30 FPS frames. A frame index of 0xFF (or reaching the end) loops.
|
||||
uint8_t frames[64]; |
||||
uint8_t delay[64]; |
||||
}; |
||||
|
||||
// Commands to upload data:
|
||||
#pragma pack(push, 1) |
||||
struct upload_packet |
||||
{ |
||||
// 'm' to upload master animation data.
|
||||
uint8_t cmd = 'm'; |
||||
|
||||
// The panel this data is for. If this is 0xFF, it's for the master.
|
||||
uint8_t panel = 0; |
||||
|
||||
// For master uploads, the animation number to modify. Panels ignore this field.
|
||||
uint8_t animation_idx = 0; |
||||
|
||||
// True if this is the last upload packet. This lets the firmware know that
|
||||
// this part of the upload is finished and it can update anything that might
|
||||
// be affected by it, like resetting lights animations.
|
||||
bool final_packet = false; |
||||
|
||||
uint16_t offset = 0; |
||||
uint8_t size = 0; |
||||
uint8_t data[240]; |
||||
}; |
||||
#pragma pack(pop) |
||||
|
||||
#pragma pack(push, 1) |
||||
struct delay_packet |
||||
{ |
||||
// 'd' to ask the master to delay.
|
||||
uint8_t cmd = 'd'; |
||||
|
||||
// How long to delay:
|
||||
uint16_t milliseconds = 0; |
||||
}; |
||||
#pragma pack(pop) |
||||
|
||||
// Make sure the packet fits in a command packet.
|
||||
static_assert(sizeof(upload_packet) <= 0xFF, ""); |
||||
} |
||||
|
||||
// The GIFs can use variable framerates. The panels update at 30 FPS.
|
||||
#define FPS 30 |
||||
|
||||
// Helpers for converting PanelGraphics to the packed sprite representation
|
||||
// we give to the pad.
|
||||
namespace ProtocolHelpers |
||||
{ |
||||
// Return a color's index in palette. If the color isn't found, return 0xFF.
|
||||
// We can use a dumb linear search here since the graphics are so small.
|
||||
uint8_t GetColorIndex(const PanelLightGraphic::palette_t &palette, const SMXGif::Color &color) |
||||
{ |
||||
// Transparency is always palette index 15.
|
||||
if(color.color[3] == 0) |
||||
return 15; |
||||
|
||||
for(int idx = 0; idx < 15; ++idx) |
||||
{ |
||||
PanelLightGraphic::color_t pad_color = palette.colors[idx]; |
||||
if(pad_color.rgb[0] == color.color[0] && |
||||
pad_color.rgb[1] == color.color[1] && |
||||
pad_color.rgb[2] == color.color[2]) |
||||
return idx; |
||||
} |
||||
return 0xFF; |
||||
} |
||||
|
||||
// Create a palette for an animation.
|
||||
//
|
||||
// We're loading from paletted GIFs, but we create a separate small palette
|
||||
// for each panel's animation, so we don't use the GIF's palette.
|
||||
bool CreatePalette(const SMXPanelAnimation &animation, PanelLightGraphic::palette_t &palette) |
||||
{ |
||||
int next_color = 0; |
||||
for(const auto &panel_graphic: animation.m_aPanelGraphics) |
||||
{ |
||||
for(const SMXGif::Color &color: panel_graphic) |
||||
{ |
||||
// If this color is transparent, leave it out of the palette.
|
||||
if(color.color[3] == 0) |
||||
continue; |
||||
|
||||
// Check if this color is already in the palette.
|
||||
uint8_t existing_idx = GetColorIndex(palette, color); |
||||
if(existing_idx != 0xFF) |
||||
continue; |
||||
|
||||
// Return false if we're using too many colors.
|
||||
if(next_color == 15) |
||||
return false; |
||||
|
||||
// Add this color.
|
||||
PanelLightGraphic::color_t pad_color; |
||||
pad_color.rgb[0] = color.color[0]; |
||||
pad_color.rgb[1] = color.color[1]; |
||||
pad_color.rgb[2] = color.color[2]; |
||||
palette.colors[next_color] = pad_color; |
||||
next_color++; |
||||
} |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
// Return packed paletted graphics for each frame, using a palette created
|
||||
// with CreatePalette. The palette must have fewer than 16 colors.
|
||||
void CreatePackedGraphic(const vector<SMXGif::Color> &image, const PanelLightGraphic::palette_t &palette, |
||||
PanelLightGraphic::graphic_t &out) |
||||
{ |
||||
int position = 0; |
||||
memset(out.data, 0, sizeof(out.data)); |
||||
for(auto color: image) |
||||
{ |
||||
// Transparency is always palette index 15.
|
||||
uint8_t palette_idx = GetColorIndex(palette, color); |
||||
if(palette_idx == 0xFF) |
||||
palette_idx = 0; |
||||
|
||||
// If this is an odd index, put the palette index in the low 4
|
||||
// bits. Otherwise, put it in the high 4 bits.
|
||||
if(position & 1) |
||||
out.data[position/2] |= (palette_idx & 0x0F) << 0; |
||||
else |
||||
out.data[position/2] |= (palette_idx & 0x0F) << 4; |
||||
position++; |
||||
} |
||||
} |
||||
|
||||
vector<uint8_t> get_frame_delays(const SMXPanelAnimation &animation) |
||||
{ |
||||
vector<uint8_t> result; |
||||
int current_frame = 0; |
||||
|
||||
float time_left_in_frame = animation.m_iFrameDurations[0]; |
||||
result.push_back(0); |
||||
while(1) |
||||
{ |
||||
// Advance time by 1/FPS seconds.
|
||||
time_left_in_frame -= 1.0f / FPS; |
||||
result.back()++; |
||||
|
||||
if(time_left_in_frame <= 0.00001f) |
||||
{ |
||||
// We've displayed this frame long enough, so advance to the next frame.
|
||||
if(current_frame + 1 == animation.m_iFrameDurations.size()) |
||||
break; |
||||
|
||||
current_frame += 1; |
||||
result.push_back(0); |
||||
time_left_in_frame += animation.m_iFrameDurations[current_frame]; |
||||
|
||||
// If time_left_in_frame is still negative, the animation is too fast.
|
||||
if(time_left_in_frame < 0.00001) |
||||
time_left_in_frame = 0; |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
// Create the master data. This just has timing information.
|
||||
bool CreateMasterAnimationData(SMX_LightsType type, |
||||
const SMXPanelAnimation &animation, |
||||
PanelLightGraphic::animation_timing_t &animation_timing, const char **error) |
||||
{ |
||||
// Released (idle) animations use frames 0-31, and pressed animations use 32-63.
|
||||
int first_graphic = type == SMX_LightsType_Released? 0:32; |
||||
|
||||
// Check that we don't have more frames than we can fit in animation_timing.
|
||||
// This is currently the same as the "too many frames" error below, but if
|
||||
// we support longer delays (staying on the same graphic for multiple animation_timings)
|
||||
// or deduping they'd be different.
|
||||
if(animation.m_aPanelGraphics.size() > arraylen(animation_timing.frames)) |
||||
{ |
||||
*error = "The animation is too long."; |
||||
return false; |
||||
} |
||||
|
||||
memset(&animation_timing.frames[0], 0xFF, sizeof(animation_timing.frames)); |
||||
for(int i = 0; i < animation.m_aPanelGraphics.size(); ++i) |
||||
animation_timing.frames[i] = i + first_graphic; |
||||
|
||||
// Set frame delays.
|
||||
memset(&animation_timing.delay[0], 0, sizeof(animation_timing.delay)); |
||||
vector<uint8_t> delays = get_frame_delays(animation); |
||||
for(int i = 0; i < delays.size() && i < 64; ++i) |
||||
animation_timing.delay[i] = delays[i]; |
||||
|
||||
// These frame numbers are relative to the animation, so don't add first_graphic.
|
||||
animation_timing.loop_animation_frame = animation.m_iLoopFrame; |
||||
|
||||
return true; |
||||
} |
||||
|
||||
// Pack panel graphics.
|
||||
bool CreatePanelAnimationData(PanelLightGraphic::panel_animation_data_t &panel_data, |
||||
int pad, SMX_LightsType type, int panel, const SMXPanelAnimation &animation, const char **error) |
||||
{ |
||||
// We have a single buffer of animation frames for each panel, which we pack
|
||||
// both the pressed and released frames into. This is the index of the next
|
||||
// frame.
|
||||
int next_graphic_idx = type == SMX_LightsType_Released? 0:32; |
||||
|
||||
// Create this animation's 4-bit palette.
|
||||
if(!ProtocolHelpers::CreatePalette(animation, panel_data.palettes[type])) |
||||
{ |
||||
*error = SMX::CreateError(SMX::ssprintf("The %s panel uses too many colors.", panel_names[panel])); |
||||
return false; |
||||
} |
||||
|
||||
// Create a small 4-bit paletted graphic with the 4-bit palette we created.
|
||||
// These are the graphics we'll send to the controller.
|
||||
for(const auto &panel_graphic: animation.m_aPanelGraphics) |
||||
{ |
||||
if(next_graphic_idx > arraylen(panel_data.graphics)) |
||||
{ |
||||
*error = "The animation has too many frames."; |
||||
return false; |
||||
} |
||||
|
||||
ProtocolHelpers::CreatePackedGraphic(panel_graphic, panel_data.palettes[type], panel_data.graphics[next_graphic_idx]); |
||||
next_graphic_idx++; |
||||
} |
||||
|
||||
// Apply color scaling to the palette, in the same way SMXManager::SetLights does.
|
||||
// Do this after we've finished creating the graphic, so this is only applied to
|
||||
// the final result and doesn't affect palettization.
|
||||
for(PanelLightGraphic::color_t &color: panel_data.palettes[type].colors) |
||||
{ |
||||
for(int i = 0; i < 3; ++i) |
||||
color.rgb[i] = uint8_t(color.rgb[i] * 0.6666f); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
// Create upload packets to upload a block of data.
|
||||
void CreateUploadPackets(vector<PanelLightGraphic::upload_packet> &packets, |
||||
const void *data_block, int start, int size, |
||||
uint8_t panel, int animation_idx) |
||||
{ |
||||
const uint8_t *buf = (const uint8_t *) data_block; |
||||
for(int offset = 0; offset < size; ) |
||||
{ |
||||
PanelLightGraphic::upload_packet packet; |
||||
packet.panel = panel; |
||||
packet.animation_idx = animation_idx; |
||||
packet.offset = start + offset; |
||||
|
||||
int bytes_left = size - offset; |
||||
packet.size = min(sizeof(PanelLightGraphic::upload_packet::data), bytes_left); |
||||
memcpy(packet.data, buf, packet.size); |
||||
packets.push_back(packet); |
||||
|
||||
offset += packet.size; |
||||
buf += packet.size; |
||||
} |
||||
} |
||||
} |
||||
|
||||
namespace LightsUploadData |
||||
{ |
||||
vector<string> commands[2]; |
||||
} |
||||
|
||||
// Prepare the loaded graphics for upload.
|
||||
bool SMX_LightsUpload_PrepareUpload(int pad, SMX_LightsType type, const SMXPanelAnimation animations[9], const char **error) |
||||
{ |
||||
// Create master animation data.
|
||||
PanelLightGraphic::animation_timing_t master_animation_data; |
||||
memset(&master_animation_data, 0xFF, sizeof(master_animation_data)); |
||||
|
||||
// All animations of each type have the same timing for all panels, since
|
||||
// they come from the same GIF, so just use the first frame to generate the
|
||||
// master data.
|
||||
if(!ProtocolHelpers::CreateMasterAnimationData(type, animations[0], master_animation_data, error)) |
||||
return false; |
||||
|
||||
// Create panel animation data.
|
||||
PanelLightGraphic::panel_animation_data_t all_panel_data[9]; |
||||
memset(&all_panel_data, 0xFF, sizeof(all_panel_data)); |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
if(!ProtocolHelpers::CreatePanelAnimationData(all_panel_data[panel], pad, type, panel, animations[panel], error)) |
||||
return false; |
||||
} |
||||
|
||||
// We successfully created the data, so there's nothing else that can fail from
|
||||
// here on.
|
||||
//
|
||||
// A list of the final commands we'll send:
|
||||
vector<string> &pad_commands = LightsUploadData::commands[pad]; |
||||
pad_commands.clear(); |
||||
|
||||
// Add an upload packet to pad_commands:
|
||||
auto add_packet_command = [&pad_commands](const PanelLightGraphic::upload_packet &packet) { |
||||
string command((char *) &packet, sizeof(packet)); |
||||
pad_commands.push_back(command); |
||||
}; |
||||
|
||||
// Add a command to briefly delay the master, to give panels a chance to finish writing to EEPROM.
|
||||
auto add_delay = [&pad_commands](int milliseconds) { |
||||
PanelLightGraphic::delay_packet packet; |
||||
packet.milliseconds = milliseconds; |
||||
|
||||
string command((char *) &packet, sizeof(packet)); |
||||
pad_commands.push_back(command); |
||||
}; |
||||
|
||||
// Create the packets we'll send, grouped by panel.
|
||||
vector<PanelLightGraphic::upload_packet> packetsPerPanel[9]; |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
// Only upload the panel graphic data and the palette we're changing. If type
|
||||
// is 0 (SMX_LightsType_Released), we're uploading the first 32 graphics and palette
|
||||
// 0. If it's 1 (SMX_LightsType_Pressed), we're uploading the second 32 graphics
|
||||
// and palette 1.
|
||||
const auto &panel_data_block = all_panel_data[panel]; |
||||
{ |
||||
int first_graphic = type == SMX_LightsType_Released? 0:32; |
||||
const PanelLightGraphic::graphic_t *graphics = &panel_data_block.graphics[first_graphic]; |
||||
int offset = offsetof(PanelLightGraphic::panel_animation_data_t, graphics[first_graphic]); |
||||
ProtocolHelpers::CreateUploadPackets(packetsPerPanel[panel], graphics, offset, sizeof(PanelLightGraphic::graphic_t) * 32, panel, type); |
||||
} |
||||
|
||||
{ |
||||
const PanelLightGraphic::palette_t *palette = &panel_data_block.palettes[type]; |
||||
int offset = offsetof(PanelLightGraphic::panel_animation_data_t, palettes[type]); |
||||
ProtocolHelpers::CreateUploadPackets(packetsPerPanel[panel], palette, offset, sizeof(PanelLightGraphic::palette_t), panel, type); |
||||
} |
||||
} |
||||
|
||||
// It takes 3.4ms per byte to write to EEPROM, and we need to avoid writing data
|
||||
// to any single panel faster than that or data won't be written. However, we're
|
||||
// writing each data separately to each panel, so we can write data to panel 1, then
|
||||
// immediately write to panel 2 while panel 1 is busy doing the write. Taking advantage
|
||||
// of this makes the upload go much faster. Panels will miss commands while they're
|
||||
// writing data, but we don't care if panel 1 misses a command that's writing to panel
|
||||
// 2 that it would ignore anyway.
|
||||
//
|
||||
// We write the first set of packets for each panel, then explicitly delay long enough
|
||||
// for them to finish before writing the next set of packets.
|
||||
|
||||
while(1) |
||||
{ |
||||
bool added_any_packets = false; |
||||
int max_size = 0; |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
// Pull this panel's next packet. It doesn't actually matter what order we
|
||||
// send the packets in.
|
||||
// Add the next packet for each panel.
|
||||
vector<PanelLightGraphic::upload_packet> &packets = packetsPerPanel[panel]; |
||||
if(packets.empty()) |
||||
continue; |
||||
|
||||
PanelLightGraphic::upload_packet packet = packets.back(); |
||||
packets.pop_back(); |
||||
add_packet_command(packet); |
||||
max_size = max(max_size, packet.size); |
||||
added_any_packets = true; |
||||
} |
||||
|
||||
// Delay long enough for the biggest write in this burst to finish. We do this
|
||||
// by sending a command to the master to tell it to delay synchronously by the
|
||||
// right amount.
|
||||
int millisecondsToDelay = lrintf(max_size * 3.4); |
||||
add_delay(millisecondsToDelay); |
||||
|
||||
// Stop if there were no more packets to add.
|
||||
if(!added_any_packets) |
||||
break; |
||||
} |
||||
|
||||
// Add the master data.
|
||||
vector<PanelLightGraphic::upload_packet> masterPackets; |
||||
ProtocolHelpers::CreateUploadPackets(masterPackets, &master_animation_data, 0, sizeof(master_animation_data), 0xFF, type); |
||||
masterPackets.back().final_packet = true; |
||||
for(const auto &packet: masterPackets) |
||||
add_packet_command(packet); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
// Start sending a prepared upload.
|
||||
//
|
||||
// The commands to send to upload the data are in LightsUploadData::commands[pad].
|
||||
void SMX_LightsUpload_BeginUpload(int pad, SMX_LightsUploadCallback pCallback, void *pUser) |
||||
{ |
||||
shared_ptr<SMXDevice> pDevice = SMXManager::g_pSMX->GetDevice(pad); |
||||
vector<string> asCommands = LightsUploadData::commands[pad]; |
||||
int iTotalCommands = asCommands.size(); |
||||
|
||||
// Queue all commands at once. As each command finishes, our callback
|
||||
// will be called.
|
||||
for(int i = 0; i < asCommands.size(); ++i) |
||||
{ |
||||
const string &sCommand = asCommands[i]; |
||||
pDevice->SendCommand(sCommand, [i, iTotalCommands, pCallback, pUser](string response) { |
||||
// Command #i has finished being sent.
|
||||
//
|
||||
// If this isn't the last command, make sure progress isn't 100.
|
||||
// Once we send 100%, the callback is no longer valid.
|
||||
int progress; |
||||
if(i != iTotalCommands-1) |
||||
progress = min((i*100) / (iTotalCommands - 1), 99); |
||||
else |
||||
progress = 100; |
||||
|
||||
// We're currently in the SMXManager thread. Call the user thread from
|
||||
// the user callback thread.
|
||||
SMXManager::g_pSMX->RunInHelperThread([pCallback, pUser, progress]() { |
||||
pCallback(progress, pUser); |
||||
}); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,39 @@ |
||||
#ifndef SMXPanelAnimationUpload_h |
||||
#define SMXPanelAnimationUpload_h |
||||
|
||||
#include "SMXPanelAnimation.h" |
||||
|
||||
// For SMX_API:
|
||||
#include "../SMX.h" |
||||
|
||||
// This is used to upload panel animations to the firmware. This is
|
||||
// only needed for offline animations. For live animations, either
|
||||
// use SMX_LightsAnimation_SetAuto, or to control lights directly
|
||||
// (recommended), use SMX_SetLights. animations[] contains the animations
|
||||
// to load.
|
||||
//
|
||||
// Prepare the currently loaded animations to be stored on the pad.
|
||||
// Return false with an error message on error.
|
||||
//
|
||||
// All LightTypes must be loaded before beginning the upload.
|
||||
//
|
||||
// If a lights upload is already in progress, returns an error.
|
||||
SMX_API bool SMX_LightsUpload_PrepareUpload(int pad, SMX_LightsType type, const SMXPanelAnimation animations[9], const char **error); |
||||
|
||||
typedef void SMX_LightsUploadCallback(int progress, void *pUser); |
||||
|
||||
// After a successful call to SMX_LightsUpload_PrepareUpload, begin uploading data
|
||||
// to the master controller for the given pad and animation type.
|
||||
//
|
||||
// The callback will be called as the upload progresses, with progress values
|
||||
// from 0-100.
|
||||
//
|
||||
// callback will always be called exactly once with a progress value of 100.
|
||||
// Once the 100% progress is called, the callback won't be accessed, so the
|
||||
// caller can safely clean up. This will happen even if the pad disconnects
|
||||
// partway through the upload.
|
||||
//
|
||||
// The callback will be called from the user callback helper thread.
|
||||
SMX_API void SMX_LightsUpload_BeginUpload(int pad, SMX_LightsUploadCallback callback, void *pUser); |
||||
|
||||
#endif |
@ -0,0 +1,49 @@ |
||||
#include "SMXThread.h" |
||||
|
||||
using namespace std; |
||||
using namespace SMX; |
||||
|
||||
SMXThread::SMXThread(Mutex &lock): |
||||
m_Lock(lock), |
||||
m_Event(lock) |
||||
{ |
||||
} |
||||
|
||||
void SMX::SMXThread::SetHighPriority(bool bHighPriority) |
||||
{ |
||||
if(m_hThread == INVALID_HANDLE_VALUE) |
||||
throw exception("SetHighPriority called while the thread isn't running"); |
||||
|
||||
SetThreadPriority(m_hThread, THREAD_PRIORITY_HIGHEST); |
||||
} |
||||
|
||||
bool SMX::SMXThread::IsCurrentThread() const |
||||
{ |
||||
return GetCurrentThreadId() == m_iThreadId; |
||||
} |
||||
|
||||
void SMXThread::Start(string name) |
||||
{ |
||||
// Start the thread.
|
||||
m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &m_iThreadId); |
||||
SMX::SetThreadName(m_iThreadId, name); |
||||
} |
||||
|
||||
void SMXThread::Shutdown() |
||||
{ |
||||
m_Lock.AssertNotLockedByCurrentThread(); |
||||
|
||||
// Shut down the thread and wait for it to exit.
|
||||
m_bShutdown = true; |
||||
m_Event.Set(); |
||||
|
||||
WaitForSingleObject(m_hThread, INFINITE); |
||||
m_hThread = INVALID_HANDLE_VALUE; |
||||
} |
||||
|
||||
DWORD WINAPI SMXThread::ThreadMainStart(void *self_) |
||||
{ |
||||
SMXThread *self = (SMXThread *) self_; |
||||
self->ThreadMain(); |
||||
return 0; |
||||
} |
@ -0,0 +1,45 @@ |
||||
#ifndef SMXThread_h |
||||
#define SMXThread_h |
||||
|
||||
// A base class for a thread.
|
||||
#include "Helpers.h" |
||||
#include <string> |
||||
|
||||
namespace SMX |
||||
{ |
||||
|
||||
class SMXThread |
||||
{ |
||||
public: |
||||
SMXThread(SMX::Mutex &lock); |
||||
|
||||
// Raise the priority of the thread.
|
||||
void SetHighPriority(bool bHighPriority); |
||||
|
||||
// Start the thread, giving it a name for debugging.
|
||||
void Start(std::string name); |
||||
|
||||
// Shut down the thread. This function won't return until the thread
|
||||
// has been stopped.
|
||||
void Shutdown(); |
||||
|
||||
// Return true if this is the calling thread.
|
||||
bool IsCurrentThread() const; |
||||
|
||||
// The derived class implements this.
|
||||
virtual void ThreadMain() = 0; |
||||
|
||||
protected: |
||||
static DWORD WINAPI ThreadMainStart(void *self); |
||||
|
||||
SMX::Mutex &m_Lock; |
||||
SMX::Event m_Event; |
||||
bool m_bShutdown = false; |
||||
|
||||
private: |
||||
HANDLE m_hThread = INVALID_HANDLE_VALUE; |
||||
DWORD m_iThreadId = 0; |
||||
}; |
||||
} |
||||
|
||||
#endif |
@ -0,0 +1,43 @@ |
||||
@echo off |
||||
setlocal ENABLEDELAYEDEXPANSION |
||||
|
||||
rem A good old 80s batch file, because it's guaranteed to always be available. |
||||
rem This assumes git is in the path. |
||||
|
||||
for /F %%I in ('git describe --always --dirty') do set GITVER=%%I |
||||
if "%GITVER%" == "" goto git_error |
||||
|
||||
rem Replace -dirty with -devel, to indicate builds with uncommitted changes. |
||||
set GITVER=%GITVER:-dirty=-devel% |
||||
|
||||
goto continue |
||||
|
||||
:git_error |
||||
rem If calling git fails for some reason, put a message in the version instead of |
||||
rem letting it be blank. |
||||
set GITVER=git failed |
||||
|
||||
:continue |
||||
|
||||
rem Output the current version to a temp file. |
||||
set TEMP_FILE=%TEMP%\temp-SMXBuildVersion.h |
||||
set OUTPUT_FILE=SMXBuildVersion.h |
||||
|
||||
echo // This file is auto-generated by update-build-version.bat. > %TEMP_FILE% |
||||
echo. >> %TEMP_FILE% |
||||
echo #ifndef SMXBuildVersion_h >> %TEMP_FILE% |
||||
echo #define SMXBuildVersion_h >> %TEMP_FILE% |
||||
echo. >> %TEMP_FILE% |
||||
echo #define SMX_BUILD_VERSION "%GITVER%" >> %TEMP_FILE% |
||||
echo. >> %TEMP_FILE% |
||||
echo #endif >> %TEMP_FILE% |
||||
|
||||
rem Compare the temp file to any existing file. Only copy the new file over the old |
||||
rem one if it's different, so we don't trigger dependency rebuilds every time. |
||||
fc %TEMP_FILE% %OUTPUT_FILE% > nul |
||||
if %errorlevel% == 0 goto end |
||||
|
||||
echo Updated to version %GITVER% |
||||
copy %TEMP_FILE% %OUTPUT_FILE% > nul |
||||
|
||||
:end |
@ -0,0 +1 @@ |
||||
bin |
@ -0,0 +1,8 @@ |
||||
<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"> |
||||
<Application.Resources> |
||||
|
||||
</Application.Resources> |
||||
</Application> |
@ -0,0 +1,250 @@ |
||||
using System; |
||||
using System.Windows; |
||||
using System.Runtime.InteropServices; |
||||
using System.IO; |
||||
using System.Threading; |
||||
|
||||
namespace smx_config |
||||
{ |
||||
public partial class App: Application |
||||
{ |
||||
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||
private static extern void SMX_Internal_OpenConsole(); |
||||
|
||||
private System.Windows.Forms.NotifyIcon trayIcon; |
||||
private MainWindow window; |
||||
|
||||
App() |
||||
{ |
||||
AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionEventHandler; |
||||
} |
||||
|
||||
protected override void OnStartup(StartupEventArgs e) |
||||
{ |
||||
base.OnStartup(e); |
||||
|
||||
// If an instance is already running, foreground it and exit. |
||||
if(ForegroundExistingInstance()) |
||||
{ |
||||
Shutdown(); |
||||
return; |
||||
} |
||||
|
||||
// This is used by the installer to close a running instance automatically when updating. |
||||
ListenForShutdownRequest(); |
||||
|
||||
// If we're being launched on startup, but the LaunchOnStartup setting is false, |
||||
// then the user turned off auto-launching but we're still being launched for some |
||||
// reason (eg. a renamed launch shortcut that we couldn't find to remove). As |
||||
// a safety so we don't launch when the user doesn't want us to, just exit in this |
||||
// case. |
||||
if(Helpers.LaunchedOnStartup() && !LaunchOnStartup.Enable) |
||||
{ |
||||
Shutdown(); |
||||
return; |
||||
} |
||||
|
||||
LaunchOnStartup.Enable = true; |
||||
if(!SMX.SMX.DLLExists()) |
||||
{ |
||||
MessageBox.Show("SMXConfig encountered an unexpected error.\n\nSMX.dll couldn't be found:\n\n" + Helpers.GetLastWin32ErrorString(), "SMXConfig"); |
||||
Current.Shutdown(); |
||||
return; |
||||
} |
||||
|
||||
if(!SMX.SMX.DLLAvailable()) |
||||
{ |
||||
MessageBox.Show("SMXConfig encountered an unexpected error.\n\nSMX.dll failed to load:\n\n" + Helpers.GetLastWin32ErrorString(), "SMXConfig"); |
||||
Current.Shutdown(); |
||||
return; |
||||
} |
||||
|
||||
if(Helpers.GetDebug()) |
||||
SMX_Internal_OpenConsole(); |
||||
|
||||
CurrentSMXDevice.singleton = new CurrentSMXDevice(); |
||||
|
||||
// Load animations. |
||||
Helpers.LoadSavedPanelAnimations(); |
||||
|
||||
CreateTrayIcon(); |
||||
|
||||
// Create the main window. |
||||
if(!Helpers.LaunchedOnStartup()) |
||||
ToggleMainWindow(); |
||||
} |
||||
|
||||
// Open or close the main window. |
||||
// |
||||
// We don't create our UI until the first time it's opened, so we use |
||||
// less memory when we're launched on startup. However, when we're minimized |
||||
// back to the tray, we don't destroy the main window. WPF is just too |
||||
// leaky to recreate the main window each time it's called due to internal |
||||
// circular references. Instead, we just focus on minimizing CPU overhead. |
||||
void ToggleMainWindow() |
||||
{ |
||||
if(window == null) |
||||
{ |
||||
window = new MainWindow(); |
||||
window.Closed += MainWindowClosed; |
||||
window.Show(); |
||||
} |
||||
else if(IsMinimizedToTray()) |
||||
{ |
||||
window.Visibility = Visibility.Visible; |
||||
window.Activate(); |
||||
} |
||||
else |
||||
{ |
||||
MinimizeToTray(); |
||||
} |
||||
} |
||||
|
||||
public bool IsMinimizedToTray() |
||||
{ |
||||
return window.Visibility == Visibility.Collapsed; |
||||
} |
||||
|
||||
public void MinimizeToTray() |
||||
{ |
||||
// Just hide the window. Don't actually set the window to minimized, since it |
||||
// won't do anything and it causes problems when restoring the window. |
||||
window.Visibility = Visibility.Collapsed; |
||||
} |
||||
|
||||
public void BringToForeground() |
||||
{ |
||||
// Restore or create the window. Don't minimize if we're already restored. |
||||
if(window == null || IsMinimizedToTray()) |
||||
ToggleMainWindow(); |
||||
|
||||
// Focus the window. |
||||
window.WindowState = WindowState.Normal; |
||||
window.Activate(); |
||||
} |
||||
|
||||
private void MainWindowClosed(object sender, EventArgs e) |
||||
{ |
||||
window = null; |
||||
} |
||||
|
||||
private void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e) |
||||
{ |
||||
string message = e.ExceptionObject.ToString(); |
||||
MessageBox.Show("SMXConfig encountered an unexpected error:\n\n" + message, "SMXConfig"); |
||||
} |
||||
|
||||
protected override void OnExit(ExitEventArgs e) |
||||
{ |
||||
base.OnExit(e); |
||||
|
||||
Console.WriteLine("Application exiting"); |
||||
|
||||
// Remove the tray icon. |
||||
if(trayIcon != null) |
||||
{ |
||||
trayIcon.Visible = false; |
||||
trayIcon = null; |
||||
} |
||||
|
||||
// Shut down cleanly, to make sure we don't run any threaded callbacks during shutdown. |
||||
if(CurrentSMXDevice.singleton != null) |
||||
{ |
||||
CurrentSMXDevice.singleton.Shutdown(); |
||||
CurrentSMXDevice.singleton = null; |
||||
} |
||||
} |
||||
|
||||
// If another instance other than this one is running, send it WM_USER to tell it to |
||||
// foreground itself. Return true if another instance was found. |
||||
private bool ForegroundExistingInstance() |
||||
{ |
||||
bool createdNew = false; |
||||
EventWaitHandle SMXConfigEvent = new EventWaitHandle(false, EventResetMode.AutoReset, "SMXConfigEvent", out createdNew); |
||||
if(!createdNew) |
||||
{ |
||||
// Signal the event to foreground the existing instance. |
||||
SMXConfigEvent.Set(); |
||||
return true; |
||||
} |
||||
|
||||
ThreadPool.RegisterWaitForSingleObject(SMXConfigEvent, ForegroundApplicationCallback, this, Timeout.Infinite, false); |
||||
|
||||
return false; |
||||
} |
||||
|
||||
private static void ForegroundApplicationCallback(Object self, Boolean timedOut) |
||||
{ |
||||
// This is called when another instance sends us a message over SMXConfigEvent. |
||||
Application.Current.Dispatcher.Invoke(new Action(() => { |
||||
App application = (App) Application.Current; |
||||
application.BringToForeground(); |
||||
})); |
||||
} |
||||
|
||||
private void ListenForShutdownRequest() |
||||
{ |
||||
// We've already checked that we're the only instance when we get here, so this event shouldn't |
||||
// exist. If it already exists for some reason, we'll listen to it anyway. |
||||
EventWaitHandle SMXConfigShutdown = new EventWaitHandle(false, EventResetMode.AutoReset, "SMXConfigShutdown"); |
||||
ThreadPool.RegisterWaitForSingleObject(SMXConfigShutdown, ShutdownApplicationCallback, this, Timeout.Infinite, false); |
||||
} |
||||
|
||||
private static void ShutdownApplicationCallback(Object self, Boolean timedOut) |
||||
{ |
||||
// This is called when another instance sends us a message over SMXConfigShutdown. |
||||
Application.Current.Dispatcher.Invoke(new Action(() => { |
||||
App application = (App) Application.Current; |
||||
application.Shutdown(); |
||||
})); |
||||
} |
||||
|
||||
// Create a tray icon. For some reason there's no WPF interface for this, |
||||
// so we have to use Forms. |
||||
void CreateTrayIcon() |
||||
{ |
||||
Stream iconStream = GetResourceStream(new Uri( "pack://application:,,,/Resources/window%20icon%20grey.ico")).Stream; |
||||
System.Drawing.Icon icon = new System.Drawing.Icon(iconStream); |
||||
|
||||
trayIcon = new System.Windows.Forms.NotifyIcon(); |
||||
trayIcon.Text = "StepManiaX"; |
||||
trayIcon.Visible = true; |
||||
|
||||
// Show or hide the application window on click. |
||||
trayIcon.Click += delegate (object sender, EventArgs e) { ToggleMainWindow(); }; |
||||
trayIcon.DoubleClick += delegate (object sender, EventArgs e) { ToggleMainWindow(); }; |
||||
|
||||
CurrentSMXDevice.singleton.ConfigurationChanged += delegate(LoadFromConfigDelegateArgs args) { |
||||
RefreshTrayIcon(args); |
||||
}; |
||||
|
||||
// Do the initial refresh. |
||||
RefreshTrayIcon(CurrentSMXDevice.singleton.GetState(), true); |
||||
} |
||||
|
||||
// Refresh the tray icon when we're connected or disconnected. |
||||
bool wasConnected; |
||||
void RefreshTrayIcon(LoadFromConfigDelegateArgs args, bool force=false) |
||||
{ |
||||
if(trayIcon == null) |
||||
return; |
||||
|
||||
bool EitherControllerConnected = false; |
||||
for(int pad = 0; pad < 2; ++pad) |
||||
if(args.controller[pad].info.connected) |
||||
EitherControllerConnected = true; |
||||
|
||||
// Skip the refresh if the connected state didn't change. |
||||
if(wasConnected == EitherControllerConnected && !force) |
||||
return; |
||||
wasConnected = EitherControllerConnected; |
||||
|
||||
trayIcon.Text = EitherControllerConnected? "StepManiaX (connected)":"StepManiaX (disconnected)"; |
||||
|
||||
// Set the tray icon. |
||||
string filename = EitherControllerConnected? "window%20icon.ico":"window%20icon%20grey.ico"; |
||||
Stream iconStream = GetResourceStream(new Uri( "pack://application:,,,/Resources/" + filename)).Stream; |
||||
trayIcon.Icon = new System.Drawing.Icon(iconStream); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,159 @@ |
||||
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 = SMX.SMXConfig.Create(); |
||||
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) |
||||
{ |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
if(config1.panelSettings[panel].loadCellLowThreshold != config2.panelSettings[panel].loadCellLowThreshold || |
||||
config1.panelSettings[panel].loadCellHighThreshold != config2.panelSettings[panel].loadCellHighThreshold) |
||||
return false; |
||||
|
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
{ |
||||
if(config1.panelSettings[panel].fsrLowThreshold[sensor] != config2.panelSettings[panel].fsrLowThreshold[sensor] || |
||||
config1.panelSettings[panel].fsrHighThreshold[sensor] != config2.panelSettings[panel].fsrHighThreshold[sensor]) |
||||
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 SetPreset(ref SMX.SMXConfig config, |
||||
byte loadCellLow, byte loadCellHigh, byte loadCellLowCenter, byte loadCellHighCenter, |
||||
byte fsrLow, byte fsrHigh, byte fsrLowCenter, byte fsrHighCenter) |
||||
{ |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
config.panelSettings[panel].loadCellLowThreshold = loadCellLow; |
||||
config.panelSettings[panel].loadCellHighThreshold = loadCellHigh; |
||||
} |
||||
|
||||
// Center: |
||||
config.panelSettings[4].loadCellLowThreshold = loadCellLowCenter; |
||||
config.panelSettings[4].loadCellHighThreshold = loadCellHighCenter; |
||||
|
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
{ |
||||
config.panelSettings[panel].fsrLowThreshold[sensor] = fsrLow; |
||||
config.panelSettings[panel].fsrHighThreshold[sensor] = fsrHigh; |
||||
} |
||||
} |
||||
|
||||
// Center: |
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
{ |
||||
config.panelSettings[4].fsrLowThreshold[sensor] = fsrLowCenter; |
||||
config.panelSettings[4].fsrHighThreshold[sensor] = fsrHighCenter; |
||||
} |
||||
} |
||||
|
||||
static private void SetHighPreset(ref SMX.SMXConfig config) |
||||
{ |
||||
SetPreset(ref config, |
||||
20, 25, 20, 30, |
||||
148, 150, 145, 160); |
||||
} |
||||
|
||||
static private void SetNormalPreset(ref SMX.SMXConfig config) |
||||
{ |
||||
SetPreset(ref config, |
||||
33, 42, 35, 60, |
||||
162, 175, 202, 225); |
||||
} |
||||
|
||||
static private void SetLowPreset(ref SMX.SMXConfig config) |
||||
{ |
||||
SetPreset(ref config, |
||||
70, 80, 100, 120, |
||||
200, 219, 212, 225); |
||||
} |
||||
|
||||
// Return the extra panels that the given panel's sensitivities control when |
||||
// advanced threshold mode is off. |
||||
static public List<int> GetPanelsToSyncUnifiedThresholds(int fromPanel) |
||||
{ |
||||
List<int> result = new List<int>(); |
||||
switch(fromPanel) |
||||
{ |
||||
case 7: // down (cardinal) |
||||
result.Add(3); // left |
||||
result.Add(5); // right |
||||
break; |
||||
case 2: // up-right (corners) |
||||
result.Add(0); // up-left |
||||
result.Add(6); // down-left |
||||
result.Add(8); // down-right |
||||
break; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
// 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 private void SyncUnifiedThresholds(ref SMX.SMXConfig config) |
||||
{ |
||||
for(int fromPanel = 0; fromPanel < 9; ++fromPanel) |
||||
{ |
||||
foreach(int toPanel in GetPanelsToSyncUnifiedThresholds(fromPanel)) |
||||
{ |
||||
config.panelSettings[toPanel].loadCellLowThreshold = config.panelSettings[fromPanel].loadCellLowThreshold; |
||||
config.panelSettings[toPanel].loadCellHighThreshold = config.panelSettings[fromPanel].loadCellHighThreshold; |
||||
|
||||
// Do the same for FSR thresholds. |
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
{ |
||||
config.panelSettings[toPanel].fsrLowThreshold[sensor] = config.panelSettings[fromPanel].fsrLowThreshold[sensor]; |
||||
config.panelSettings[toPanel].fsrHighThreshold[sensor] = config.panelSettings[fromPanel].fsrHighThreshold[sensor]; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,283 @@ |
||||
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 ConnectionsChanged, ConfigurationChanged, InputChanged, TestDataChanged; |
||||
|
||||
// Data for each of two controllers: |
||||
public LoadFromConfigDelegateArgsPerController[] controller; |
||||
|
||||
// If we have more than one connected controller, we expect them to be the same version. |
||||
// Return the newest firmware version that's connected. |
||||
public int firmwareVersion() |
||||
{ |
||||
int result = 1; |
||||
foreach(var data in controller) |
||||
{ |
||||
if(data.info.connected && data.info.m_iFirmwareVersion > result) |
||||
result = data.info.m_iFirmwareVersion; |
||||
|
||||
} |
||||
return result; |
||||
} |
||||
|
||||
// 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.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; |
||||
args.ConnectionsChanged = 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.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; |
||||
} |
||||
|
||||
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,403 @@ |
||||
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 |
||||
{ |
||||
public static readonly DependencyProperty PanelProperty = DependencyProperty.RegisterAttached("Panel", |
||||
typeof(int), typeof(PanelSelectButton), new FrameworkPropertyMetadata(0)); |
||||
|
||||
public int Panel { |
||||
get { return (int) this.GetValue(PanelProperty); } |
||||
set { this.SetValue(PanelProperty, value); } |
||||
} |
||||
// Which panel is currently selected. |
||||
public static readonly DependencyProperty SelectedPanelProperty = DependencyProperty.RegisterAttached("SelectedPanel", |
||||
typeof(int), typeof(PanelSelectButton), new FrameworkPropertyMetadata(0)); |
||||
|
||||
public int SelectedPanel { |
||||
get { return (int) this.GetValue(SelectedPanelProperty); } |
||||
set { this.SetValue(SelectedPanelProperty, value); } |
||||
} |
||||
|
||||
// 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) || |
||||
args.controller[SelectedPad].test_data.AnyBadJumpersOnPanel(PanelIndex); |
||||
|
||||
// Only show this panel button if the panel's input is enabled. |
||||
SMX.SMXConfig config = ActivePad.GetFirstActivePadConfig(args); |
||||
Visibility = ShouldBeDisplayed(config)? Visibility.Visible:Visibility.Collapsed; |
||||
}); |
||||
onConfigChange.RefreshOnInputChange = true; |
||||
onConfigChange.RefreshOnTestDataChange = true; |
||||
} |
||||
|
||||
protected override void OnClick() |
||||
{ |
||||
base.OnClick(); |
||||
|
||||
// Select this panel. |
||||
SelectedPanel = Panel; |
||||
|
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(this); |
||||
} |
||||
|
||||
// Return true if this button should be displayed. |
||||
private bool ShouldBeDisplayed(SMX.SMXConfig config) |
||||
{ |
||||
bool[] enabledPanels = config.GetEnabledPanels(); |
||||
int PanelIndex = Panel % 9; |
||||
return enabledPanels[PanelIndex]; |
||||
} |
||||
} |
||||
|
||||
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 Panel CurrentDIPGroup; |
||||
private FrameImage CurrentDIP; |
||||
private FrameImage ExpectedDIP; |
||||
private FrameworkElement NoResponseFromPanel; |
||||
private FrameworkElement NoResponseFromSensors; |
||||
private FrameworkElement BadSensorDIPSwitches; |
||||
private FrameworkElement P1Diagnostics, P2Diagnostics; |
||||
private FrameworkElement DIPLabelLeft, DIPLabelRight; |
||||
|
||||
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; |
||||
CurrentDIPGroup = Template.FindName("CurrentDIPGroup", this) as Panel; |
||||
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; |
||||
BadSensorDIPSwitches = Template.FindName("BadSensorDIPSwitches", this) as FrameworkElement; |
||||
P1Diagnostics = Template.FindName("P1Diagnostics", this) as FrameworkElement; |
||||
P2Diagnostics = Template.FindName("P2Diagnostics", this) as FrameworkElement; |
||||
|
||||
DIPLabelRight = Template.FindName("DIPLabelRight", this) as FrameworkElement; |
||||
DIPLabelLeft = Template.FindName("DIPLabelLeft", this) as FrameworkElement; |
||||
|
||||
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); |
||||
}; |
||||
|
||||
// Note that we won't get a MouseUp if the display is hidden due to a controller |
||||
// disconnection while the mouse is held. We handle this in Refresh(). |
||||
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.SetSensorTestMode(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.SetSensorTestMode(pad, GetTestMode()); |
||||
}; |
||||
|
||||
Unloaded += delegate(object sender, RoutedEventArgs e) |
||||
{ |
||||
for(int pad = 0; pad < 2; ++pad) |
||||
SMX.SMX.SetSensorTestMode(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) |
||||
{ |
||||
// First, make sure a valid panel is selected. |
||||
SMX.SMXConfig config = ActivePad.GetFirstActivePadConfig(args); |
||||
SelectValidPanel(config); |
||||
|
||||
RefreshSelectedPanel(); |
||||
|
||||
// Make sure SetShowAllLights is disabled if the controller is disconnected, since |
||||
// we can miss mouse up events. |
||||
bool EitherControllerConnected = args.controller[0].info.connected || args.controller[1].info.connected; |
||||
if(!EitherControllerConnected) |
||||
SetShowAllLights?.Invoke(false); |
||||
|
||||
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, HaveIncorrectSensorDIP = false; |
||||
if(args.controller[SelectedPad].test_data.bHaveDataFromPanel[PanelIndex]) |
||||
{ |
||||
AnySensorsNotResponding = args.controller[SelectedPad].test_data.AnySensorsOnPanelNotResponding(PanelIndex); |
||||
|
||||
// Don't show both warnings. |
||||
HaveIncorrectSensorDIP = !AnySensorsNotResponding && args.controller[SelectedPad].test_data.AnyBadJumpersOnPanel(PanelIndex); |
||||
} |
||||
NoResponseFromSensors.Visibility = AnySensorsNotResponding? Visibility.Visible:Visibility.Collapsed; |
||||
BadSensorDIPSwitches.Visibility = HaveIncorrectSensorDIP? Visibility.Visible:Visibility.Collapsed; |
||||
|
||||
// Adjust the DIP labels to match the PCB. |
||||
bool DIPLabelsOnLeft = config.masterVersion < 4; |
||||
DIPLabelRight.Visibility = DIPLabelsOnLeft? Visibility.Collapsed:Visibility.Visible; |
||||
DIPLabelLeft.Visibility = DIPLabelsOnLeft? Visibility.Visible:Visibility.Collapsed; |
||||
|
||||
// Update the level bar from the test mode data for the selected panel. |
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
{ |
||||
var controllerData = args.controller[SelectedPad]; |
||||
Int16 value = controllerData.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; |
||||
|
||||
// Scale differently depending on if this is an FSR panel or a load cell panel. |
||||
bool isFSR = controllerData.config.masterVersion >= 4 && (controllerData.config.configFlags & SMX.SMXConfigFlags.PlatformFlags_FSR) != 0; |
||||
if(isFSR) |
||||
value >>= 2; |
||||
float maxValue = isFSR? 250:500; |
||||
LevelBars[sensor].Value = value / maxValue; |
||||
LevelBarText[sensor].Content = value; |
||||
LevelBars[sensor].Error = false; |
||||
} |
||||
} |
||||
|
||||
NoResponseFromPanel.Visibility = Visibility.Collapsed; |
||||
CurrentDIPGroup.Visibility = Visibility.Visible; |
||||
if(!args.controller[SelectedPad].test_data.bHaveDataFromPanel[PanelIndex]) |
||||
{ |
||||
NoResponseFromPanel.Visibility = Visibility.Visible; |
||||
NoResponseFromSensors.Visibility = Visibility.Collapsed; |
||||
CurrentDIPGroup.Visibility = Visibility.Hidden; |
||||
return; |
||||
} |
||||
|
||||
} |
||||
|
||||
// If the selected panel isn't enabled for input, select another one. |
||||
private void SelectValidPanel(SMX.SMXConfig config) |
||||
{ |
||||
bool[] enabledPanels = config.GetEnabledPanels(); |
||||
int SelectedPanelIndex = SelectedPanel % 9; |
||||
|
||||
// If we're not selected, or this button is visible, we don't need to do anything. |
||||
if(!enabledPanels[SelectedPanelIndex]) |
||||
SelectedPanel = config.GetFirstEnabledPanel(); |
||||
} |
||||
|
||||
// Update the selected diagnostics button based on the value of selectedButton. |
||||
private void RefreshSelectedPanel() |
||||
{ |
||||
LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState(); |
||||
|
||||
DiagnosticsPanelButton[] buttons = getPanelSelectionButtons(); |
||||
|
||||
// Tell the buttons which one is selected. |
||||
foreach(DiagnosticsPanelButton button in buttons) |
||||
button.IsSelected = button.Panel == SelectedPanel; |
||||
} |
||||
|
||||
// Return all panel selection buttons. |
||||
DiagnosticsPanelButton[] getPanelSelectionButtons() |
||||
{ |
||||
DiagnosticsPanelButton[] result = new DiagnosticsPanelButton[18]; |
||||
for(int i = 0; i < 18; ++i) |
||||
{ |
||||
result[i] = Template.FindName("Panel" + i, this) as DiagnosticsPanelButton; |
||||
} |
||||
return result; |
||||
} |
||||
} |
||||
|
||||
public class PanelTestModeCheckbox: CheckBox |
||||
{ |
||||
public override void OnApplyTemplate() |
||||
{ |
||||
base.OnApplyTemplate(); |
||||
} |
||||
|
||||
protected override void OnClick() |
||||
{ |
||||
base.OnClick(); |
||||
|
||||
SMX.SMX.SetPanelTestMode((bool) IsChecked? SMX.SMX.PanelTestMode.PressureTest:SMX.SMX.PanelTestMode.Off); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,222 @@ |
||||
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(); |
||||
} |
||||
} |
||||
|
||||
class DoubleSliderThumb: Thumb |
||||
{ |
||||
public static readonly DependencyProperty ShowUpArrowProperty = DependencyProperty.Register("ShowUpArrow", |
||||
typeof(bool), typeof(DoubleSliderThumb)); |
||||
|
||||
public bool ShowUpArrow { |
||||
get { return (bool) this.GetValue(ShowUpArrowProperty); } |
||||
set { this.SetValue(ShowUpArrowProperty, value); } |
||||
} |
||||
|
||||
public static readonly DependencyProperty ShowDownArrowProperty = DependencyProperty.Register("ShowDownArrow", |
||||
typeof(bool), typeof(DoubleSliderThumb)); |
||||
|
||||
public bool ShowDownArrow { |
||||
get { return (bool) this.GetValue(ShowDownArrowProperty); } |
||||
set { this.SetValue(ShowDownArrowProperty, value); } |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,984 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Runtime.InteropServices; |
||||
using System.Runtime.Serialization.Formatters.Binary; |
||||
using System.Windows; |
||||
using System.Windows.Media; |
||||
using System.Windows.Resources; |
||||
using System.Windows.Threading; |
||||
using SMXJSON; |
||||
|
||||
namespace smx_config |
||||
{ |
||||
// Track whether we're configuring one pad or both at once. |
||||
static class ActivePad |
||||
{ |
||||
public enum SelectedPad { |
||||
P1, |
||||
P2, |
||||
Both, |
||||
}; |
||||
|
||||
// The actual pad selection. This defaults to both, and doesn't change if |
||||
// only one pad is selected. We don't actually show "both" in the dropdown |
||||
// unless two pads are connected, but the underlying setting remains. |
||||
public static SelectedPad selectedPad = SelectedPad.Both; |
||||
|
||||
// A shortcut for when a LoadFromConfigDelegateArgs isn't available: |
||||
public static IEnumerable<Tuple<int, SMX.SMXConfig>> ActivePads() |
||||
{ |
||||
// In case we're called in design mode, just return an empty list. |
||||
if(CurrentSMXDevice.singleton == null) |
||||
return new List<Tuple<int, SMX.SMXConfig>>(); |
||||
|
||||
return ActivePads(CurrentSMXDevice.singleton.GetState()); |
||||
} |
||||
|
||||
// Yield each connected pad which is currently active for configuration. |
||||
public static IEnumerable<Tuple<int, SMX.SMXConfig>> ActivePads(LoadFromConfigDelegateArgs args) |
||||
{ |
||||
bool Pad1Connected = args.controller[0].info.connected; |
||||
bool Pad2Connected = args.controller[1].info.connected; |
||||
|
||||
// If both pads are connected and a single pad is selected, ignore the deselected pad. |
||||
if(Pad1Connected && Pad2Connected) |
||||
{ |
||||
if(selectedPad == SelectedPad.P1) |
||||
Pad2Connected = false; |
||||
if(selectedPad == SelectedPad.P2) |
||||
Pad1Connected = false; |
||||
} |
||||
|
||||
if(Pad1Connected) |
||||
yield return Tuple.Create(0, args.controller[0].config); |
||||
if(Pad2Connected) |
||||
yield return Tuple.Create(1, args.controller[1].config); |
||||
} |
||||
|
||||
// We know the selected pads are synced if there are two active, and when refreshing a |
||||
// UI we just want one of them to set the UI to. For convenience, return the first one. |
||||
public static SMX.SMXConfig GetFirstActivePadConfig(LoadFromConfigDelegateArgs args) |
||||
{ |
||||
foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePads(args)) |
||||
return activePad.Item2; |
||||
|
||||
// There aren't any pads connected. Just return a dummy config, since the UI |
||||
// isn't visible. |
||||
return SMX.SMXConfig.Create(); |
||||
} |
||||
|
||||
public static SMX.SMXConfig GetFirstActivePadConfig() |
||||
{ |
||||
return GetFirstActivePadConfig(CurrentSMXDevice.singleton.GetState()); |
||||
} |
||||
} |
||||
|
||||
static class Helpers |
||||
{ |
||||
// Return true if arg is in the commandline. |
||||
public static bool HasCommandlineArgument(string arg) |
||||
{ |
||||
foreach(string s in Environment.GetCommandLineArgs()) |
||||
{ |
||||
if(s == arg) |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
// Return true if we're in debug mode. |
||||
public static bool GetDebug() |
||||
{ |
||||
return HasCommandlineArgument("-d"); |
||||
} |
||||
|
||||
// Return true if we were launched on startup. |
||||
public static bool LaunchedOnStartup() |
||||
{ |
||||
return HasCommandlineArgument("-s"); |
||||
} |
||||
|
||||
// Return the last Win32 error as a string. |
||||
public static string GetLastWin32ErrorString() |
||||
{ |
||||
int error = Marshal.GetLastWin32Error(); |
||||
if(error == 0) |
||||
return ""; |
||||
return new System.ComponentModel.Win32Exception(error).Message; |
||||
} |
||||
|
||||
// https://stackoverflow.com/a/129395/136829 |
||||
public static T DeepClone<T>(T obj) |
||||
{ |
||||
using (var ms = new MemoryStream()) |
||||
{ |
||||
var formatter = new BinaryFormatter(); |
||||
formatter.Serialize(ms, obj); |
||||
ms.Position = 0; |
||||
|
||||
return (T) formatter.Deserialize(ms); |
||||
} |
||||
} |
||||
|
||||
// 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); |
||||
} |
||||
|
||||
// Return a Color as an HTML color code. |
||||
public static string ColorToString(Color color) |
||||
{ |
||||
// 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 "#" + color.ToString().Substring(3); |
||||
} |
||||
|
||||
// Parse #RRGGBB and return a Color, or white if the string isn't in the correct format. |
||||
public 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); |
||||
} |
||||
} |
||||
|
||||
// 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) Math.Round(c * LightsScaleFactor); |
||||
} |
||||
static public Byte UnscaleColor(Byte c) |
||||
{ |
||||
Byte result = (Byte) Math.Round(Math.Min(255, c / LightsScaleFactor)); |
||||
|
||||
// The color values we output are quantized, since we're scaling an 8-bit value. |
||||
// This doesn't have any real effect, but it causes #FFFFFF in the settings export |
||||
// file to be written out as #FDFDFD (which has the same value in hardware). Just |
||||
// so the common value of white is clean, snap these values to 0xFF. The end result |
||||
// will be the same. |
||||
if(result >= 0xFD) |
||||
return 0xFF; |
||||
return result; |
||||
} |
||||
|
||||
static public Color ScaleColor(Color c) |
||||
{ |
||||
return Color.FromRgb(ScaleColor(c.R), ScaleColor(c.G), ScaleColor(c.B)); |
||||
} |
||||
|
||||
static public Color UnscaleColor(Color c) |
||||
{ |
||||
return Color.FromRgb(UnscaleColor(c.R), UnscaleColor(c.G), UnscaleColor(c.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; |
||||
} |
||||
|
||||
// Return our settings directory, creating it if it doesn't exist. |
||||
public static string GetSettingsDirectory() |
||||
{ |
||||
string result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/StepManiaX/"; |
||||
System.IO.Directory.CreateDirectory(result); |
||||
return result; |
||||
} |
||||
|
||||
public static byte[] ReadFileFromSettings(string filename) |
||||
{ |
||||
string outputFilename = GetSettingsDirectory() + filename; |
||||
try { |
||||
return System.IO.File.ReadAllBytes(outputFilename); |
||||
} catch { |
||||
// If the file doesn't exist or can't be read for some other reason, just |
||||
// return null. |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
public static void SaveFileToSettings(string filename, byte[] data) |
||||
{ |
||||
string outputFilename = GetSettingsDirectory() + filename; |
||||
string directory = System.IO.Path.GetDirectoryName(outputFilename); |
||||
System.IO.Directory.CreateDirectory(directory); |
||||
System.IO.File.WriteAllBytes(outputFilename, data); |
||||
} |
||||
|
||||
// Read path. If an error is encountered, return "". |
||||
public static string ReadFile(string path) |
||||
{ |
||||
try { |
||||
return System.IO.File.ReadAllText(path); |
||||
} |
||||
catch(System.IO.IOException) |
||||
{ |
||||
return ""; |
||||
} |
||||
} |
||||
|
||||
// Read path. If an error is encountered, return null. |
||||
public static byte[] ReadBinaryFile(string path) |
||||
{ |
||||
try { |
||||
return System.IO.File.ReadAllBytes(path); |
||||
} |
||||
catch(System.IO.IOException) |
||||
{ |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
public static Dictionary<SMX.SMX.LightsType, string> LightsTypeNames = new Dictionary<SMX.SMX.LightsType, string>() |
||||
{ |
||||
{ SMX.SMX.LightsType.LightsType_Pressed, "pressed" }, |
||||
{ SMX.SMX.LightsType.LightsType_Released, "released" }, |
||||
}; |
||||
|
||||
// Load any saved animations from disk. |
||||
public static void LoadSavedPanelAnimations() |
||||
{ |
||||
for(int pad = 0; pad < 2; ++pad) |
||||
{ |
||||
foreach(var it in LightsTypeNames) |
||||
LoadSavedAnimationType(pad, it.Key); |
||||
} |
||||
} |
||||
|
||||
public static void SaveAnimationToDisk(int pad, SMX.SMX.LightsType type, byte[] data) |
||||
{ |
||||
string filename = LightsTypeNames[type] + ".gif"; |
||||
string path = "Animations/Pad" + (pad+1) + "/" + filename; |
||||
Helpers.SaveFileToSettings(path, data); |
||||
} |
||||
|
||||
// Read a saved PanelAnimation. |
||||
// |
||||
// Data will always be returned. If the user hasn't saved anything, we'll return |
||||
// our default animation. |
||||
private static byte[] ReadSavedAnimationType(int pad, SMX.SMX.LightsType type) |
||||
{ |
||||
string filename = LightsTypeNames[type] + ".gif"; |
||||
string path = "Animations/Pad" + (pad+1) + "/" + filename; |
||||
byte[] gif = Helpers.ReadFileFromSettings(path); |
||||
if(gif == null) |
||||
{ |
||||
// If the user has never loaded a file, load our default. |
||||
Uri url = new Uri("pack://application:,,,/Resources/" + filename); |
||||
StreamResourceInfo info = Application.GetResourceStream(url); |
||||
gif = new byte[info.Stream.Length]; |
||||
info.Stream.Read(gif, 0, gif.Length); |
||||
} |
||||
return gif; |
||||
} |
||||
|
||||
// Load a PanelAnimation from disk. |
||||
private static void LoadSavedAnimationType(int pad, SMX.SMX.LightsType type) |
||||
{ |
||||
byte[] gif = ReadSavedAnimationType(pad, type); |
||||
string error; |
||||
SMX.SMX.LightsAnimation_Load(gif, pad, type, out error); |
||||
} |
||||
|
||||
// Some broken antivirus software locks files when they're read. This is horrifying and |
||||
// breaks lots of software, including WPF's settings class. This is a race condition, |
||||
// so try to work around this by trying repeatedly. There's not much else we can do about |
||||
// it other than asking users to use a better antivirus. |
||||
public static void SaveApplicationSettings() |
||||
{ |
||||
for(int i = 0; i < 10; ++i) |
||||
{ |
||||
try { |
||||
Properties.Settings.Default.Save(); |
||||
return; |
||||
} catch(IOException e) |
||||
{ |
||||
Console.WriteLine("Error writing settings. Trying again: " + e); |
||||
} |
||||
} |
||||
|
||||
MessageBox.Show("Settings couldn't be saved.\n\nThis is usually caused by faulty antivirus software.", |
||||
"Error", MessageBoxButton.OK, MessageBoxImage.Warning); |
||||
} |
||||
|
||||
// Create a .lnk. |
||||
public static void CreateShortcut(string outputFile, string targetPath, string arguments) |
||||
{ |
||||
Type shellType = Type.GetTypeFromProgID("WScript.Shell"); |
||||
dynamic shell = Activator.CreateInstance(shellType); |
||||
dynamic shortcut = shell.CreateShortcut(outputFile); |
||||
|
||||
shortcut.TargetPath = targetPath; |
||||
shortcut.Arguments = arguments; |
||||
shortcut.WindowStyle = 0; |
||||
shortcut.Save(); |
||||
} |
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Auto)] |
||||
public static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam); |
||||
} |
||||
|
||||
// The threshold sliders in the advanced tab affect different panels and sensors depending |
||||
// on the user's settings. This handles managing which sensors each slider controls. |
||||
static public class ThresholdSettings |
||||
{ |
||||
[Serializable] |
||||
public struct PanelAndSensor |
||||
{ |
||||
public PanelAndSensor(int panel, int sensor) |
||||
{ |
||||
this.panel = panel; |
||||
this.sensor = sensor; |
||||
} |
||||
public int panel; |
||||
public int sensor; |
||||
}; |
||||
|
||||
public static List<string> thresholdSliderNames = new List<string>() |
||||
{ |
||||
"up-left", "up", "up-right", |
||||
"left", "center", "right", |
||||
"down-left", "down", "down-right", |
||||
"cardinal", "corner", |
||||
"inner-sensors", |
||||
"outer-sensors", |
||||
"custom-sensors", |
||||
}; |
||||
|
||||
// These correspond with ThresholdSlider.Type. |
||||
static Dictionary<string, int> panelNameToIndex = new Dictionary<string, int>() { |
||||
{ "up-left", 0 }, |
||||
{ "up", 1 }, |
||||
{ "up-right", 2 }, |
||||
{ "left", 3 }, |
||||
{ "center", 4 }, |
||||
{ "right", 5 }, |
||||
{ "down-left", 6 }, |
||||
{ "down", 7 }, |
||||
{ "down-right", 8 }, |
||||
|
||||
// The cardinal and corner sliders write to the down and up-right panels, and |
||||
// are then synced to the other panels. |
||||
{ "cardinal", 7 }, |
||||
{ "corner", 2 }, |
||||
}; |
||||
|
||||
// Save and load the list of custom threshold sensors to settings. These aren't saved to the pad, we |
||||
// just keep them in application settings. |
||||
static List<PanelAndSensor> cachedCustomSensors; |
||||
static public void SetCustomSensors(List<PanelAndSensor> panelAndSensors) |
||||
{ |
||||
List<object> result = new List<object>(); |
||||
foreach(PanelAndSensor panelAndSensor in panelAndSensors) |
||||
{ |
||||
List<int> panelAndSensorArray = new List<int>() { panelAndSensor.panel, panelAndSensor.sensor }; |
||||
result.Add(panelAndSensorArray); |
||||
} |
||||
|
||||
SetCustomSensorsJSON(result); |
||||
} |
||||
|
||||
// Set CustomSensors from a [[1,1],[2,2]] array. This is what we save to settings and |
||||
// export to JSON. |
||||
static public void SetCustomSensorsJSON(List<object> panelAndSensors) |
||||
{ |
||||
Properties.Settings.Default.CustomSensors = SerializeJSON.Serialize(panelAndSensors); |
||||
Helpers.SaveApplicationSettings(); |
||||
|
||||
// Clear the cache. Set it to null instead of assigning panelAndSensors to it to force |
||||
// it to re-parse at least once, to catch problems early. |
||||
cachedCustomSensors = null; |
||||
} |
||||
|
||||
// Return the sensors that are controlled by the custom-sensors slider. The other |
||||
// threshold sliders will leave these alone. |
||||
static public List<PanelAndSensor> GetCustomSensors() |
||||
{ |
||||
// Properties.Settings.Default.CustomSensors = "[[0,0], [1,0]]"; |
||||
// This is only ever changed with calls to SetCustomSensors. |
||||
if(cachedCustomSensors != null) |
||||
return Helpers.DeepClone(cachedCustomSensors); |
||||
|
||||
List<PanelAndSensor> result = new List<PanelAndSensor>(); |
||||
if(Properties.Settings.Default.CustomSensors == "") |
||||
return result; |
||||
|
||||
try { |
||||
// This is a list of [panel,sensor] arrays: |
||||
// [[0,0], [0,1], [1,0]] |
||||
List<object> sensors = GetCustomSensorsJSON(); |
||||
foreach(object panelAndSensorObj in sensors) |
||||
{ |
||||
List<object> panelAndSensor = (List<object>) panelAndSensorObj; |
||||
int panel = panelAndSensor.Get(0, -1); |
||||
int sensor = panelAndSensor.Get(1, -1); |
||||
if(panel == -1 || sensor == -1) |
||||
continue; |
||||
|
||||
result.Add(new PanelAndSensor(panel, sensor)); |
||||
} |
||||
} catch(ParseError) { |
||||
return result; |
||||
} |
||||
|
||||
cachedCustomSensors = result; |
||||
|
||||
return Helpers.DeepClone(cachedCustomSensors); |
||||
} |
||||
|
||||
static public List<object> GetCustomSensorsJSON() |
||||
{ |
||||
try { |
||||
return SMXJSON.ParseJSON.Parse<List<object>>(Properties.Settings.Default.CustomSensors); |
||||
} catch(ParseError) { |
||||
// CustomSensors is empty by default. We could test if it's empty, but as a more general |
||||
// safety, just catch any JSON errors in case something invalid is saved to it. |
||||
return new List<object>(); |
||||
} |
||||
} |
||||
|
||||
const int SensorLeft = 0; |
||||
const int SensorRight = 1; |
||||
const int SensorUp = 2; |
||||
const int SensorDown = 3; |
||||
static public List<PanelAndSensor> GetInnerSensors() |
||||
{ |
||||
return new List<PanelAndSensor>() |
||||
{ |
||||
new PanelAndSensor(1,SensorDown), // up panel, bottom sensor |
||||
new PanelAndSensor(3,SensorRight), // left panel, right sensor |
||||
new PanelAndSensor(5,SensorLeft), // right panel, left sensor |
||||
new PanelAndSensor(7,SensorUp), // down panel, top sensor |
||||
}; |
||||
} |
||||
|
||||
static public List<PanelAndSensor> GetOuterSensors() |
||||
{ |
||||
return new List<PanelAndSensor>() |
||||
{ |
||||
new PanelAndSensor(1,SensorUp), // up panel, top sensor |
||||
new PanelAndSensor(3,SensorLeft), // left panel, left sensor |
||||
new PanelAndSensor(5,SensorRight), // right panel, right sensor |
||||
new PanelAndSensor(7,SensorDown), // down panel, bottom sensor |
||||
}; |
||||
} |
||||
// Return the sensors controlled by the given slider. Most of the work is done |
||||
// in GetControlledSensorsForSliderTypeInternal. This just handles removing overlapping |
||||
// sensors. If inner-sensors is enabled, the inner sensors are removed from the normal |
||||
// thresholds. |
||||
// |
||||
// This is really inefficient: it calls GetControlledSensorsForSliderTypeInternal a lot, |
||||
// and the filtering is a linear search, but it doesn't matter. |
||||
// |
||||
// If includeOverridden is true, include sensors that would be controlled by this slider |
||||
// by default, but which have been overridden by a higher priority slider, or which are |
||||
// disabled by checkboxes. This is used for the UI. |
||||
static public List<PanelAndSensor> GetControlledSensorsForSliderType(string Type, bool advancedMode, bool includeOverridden) |
||||
{ |
||||
List<PanelAndSensor> result = GetControlledSensorsForSliderTypeInternal(Type, advancedMode, includeOverridden); |
||||
|
||||
if(!includeOverridden) |
||||
{ |
||||
// inner-sensors, outer-sensors and custom thresholds overlap each other and the standard |
||||
// sliders. inner-sensors and outer-sensors take over the equivalent sensors in the standard |
||||
// sliders, and custom thresholds take priority over everything else. |
||||
// |
||||
// We always pass false to includeOverridden here, since we need to know the real state of the |
||||
// sliders we're removing. |
||||
if(Type == "inner-sensors" || Type == "outer-sensors") |
||||
{ |
||||
// Remove any sensors controlled by the custom threshold. |
||||
RemoveFromSensorList(result, GetControlledSensorsForSliderTypeInternal("custom-sensors", advancedMode, false)); |
||||
} |
||||
else if(Type != "custom-sensors") |
||||
{ |
||||
// This is a regular slider. Remove any sensors controlled by inner-sensors, outer-sensors |
||||
// or custom-sensors. |
||||
RemoveFromSensorList(result, GetControlledSensorsForSliderTypeInternal("inner-sensors", advancedMode, false)); |
||||
RemoveFromSensorList(result, GetControlledSensorsForSliderTypeInternal("outer-sensors", advancedMode, false)); |
||||
RemoveFromSensorList(result, GetControlledSensorsForSliderTypeInternal("custom-sensors", advancedMode, false)); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
static private void RemoveFromSensorList(List<PanelAndSensor> target, List<PanelAndSensor> sensorsToRemove) |
||||
{ |
||||
foreach(PanelAndSensor panelAndSensor in sensorsToRemove) |
||||
target.Remove(panelAndSensor); |
||||
} |
||||
|
||||
static private List<PanelAndSensor> GetControlledSensorsForSliderTypeInternal(string Type, bool advancedMode, bool includeOverridden) |
||||
{ |
||||
// inner-sensors and outer-sensors do nothing if their checkbox is disabled. We do this here because we |
||||
// need to skip this for the RemoveFromSensorList logic above. |
||||
if(!includeOverridden) |
||||
{ |
||||
if(Type == "inner-sensors" && !Properties.Settings.Default.UseInnerSensorThresholds) |
||||
return new List<PanelAndSensor>(); |
||||
if(Type == "outer-sensors" && !Properties.Settings.Default.UseOuterSensorThresholds) |
||||
return new List<PanelAndSensor>(); |
||||
} |
||||
|
||||
// Special sliders: |
||||
if(Type == "custom-sensors") return GetCustomSensors(); |
||||
if(Type == "inner-sensors") return GetInnerSensors(); |
||||
if(Type == "outer-sensors") return GetOuterSensors(); |
||||
|
||||
List<PanelAndSensor> result = new List<PanelAndSensor>(); |
||||
|
||||
// Check if this slider is shown in this mode. |
||||
if(advancedMode) |
||||
{ |
||||
// Hide the combo sliders in advanced mode. |
||||
if(Type == "cardinal" || Type == "corner") |
||||
return result; |
||||
} |
||||
|
||||
if(!advancedMode) |
||||
{ |
||||
// Only these sliders are shown in normal mode. |
||||
if(Type != "up" && Type != "center" && Type != "cardinal" && Type != "corner") |
||||
return result; |
||||
} |
||||
|
||||
// If advanced mode is disabled, save to all panels this slider affects. The down arrow controls |
||||
// all four cardinal panels. (If advanced mode is enabled we'll never be a different cardinal |
||||
// direction, since those widgets won't exist.) If it's disabled, just write to our own panel. |
||||
List<int> saveToPanels = new List<int>(); |
||||
int ourPanelIdx = panelNameToIndex[Type]; |
||||
saveToPanels.Add(ourPanelIdx); |
||||
if(!advancedMode) |
||||
saveToPanels.AddRange(ConfigPresets.GetPanelsToSyncUnifiedThresholds(ourPanelIdx)); |
||||
|
||||
foreach(int panelIdx in saveToPanels) |
||||
{ |
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
result.Add(new PanelAndSensor(panelIdx, sensor)); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
// If the user disables inner-sensors after setting a value and control of those thresholds |
||||
// goes back to other sliders, the old inner-sensors thresholds will still be set in config |
||||
// until the user changes them, which is confusing. Make sure the value of each slider is |
||||
// actually set to config, even if the user doesn't change them. |
||||
// |
||||
// This isn't perfect. If the user assigns the first up sensor to custom and then removes it, |
||||
// so that sensor goes back to the normal up slider, this will sync the custom value to up. |
||||
// That's because we don't know which thresholds were actually being controlled by the up slider |
||||
// before it was changed. This is tricky to fix and not a big problem. |
||||
private static void SyncSliderThresholdsForConfig(ref SMX.SMXConfig config) |
||||
{ |
||||
if(!config.fsr()) |
||||
return; |
||||
|
||||
bool AdvancedModeEnabled = Properties.Settings.Default.AdvancedMode; |
||||
foreach(string sliderName in thresholdSliderNames) |
||||
{ |
||||
List<PanelAndSensor> controlledSensors = GetControlledSensorsForSliderType(sliderName, AdvancedModeEnabled, false); |
||||
if(controlledSensors.Count == 0) |
||||
continue; |
||||
PanelAndSensor firstSensor = controlledSensors[0]; |
||||
|
||||
foreach(PanelAndSensor panelAndSensor in controlledSensors) |
||||
{ |
||||
config.panelSettings[panelAndSensor.panel].fsrLowThreshold[panelAndSensor.sensor] = |
||||
config.panelSettings[firstSensor.panel].fsrLowThreshold[firstSensor.sensor]; |
||||
config.panelSettings[panelAndSensor.panel].fsrHighThreshold[panelAndSensor.sensor] = |
||||
config.panelSettings[firstSensor.panel].fsrHighThreshold[firstSensor.sensor]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public static void SyncSliderThresholds() |
||||
{ |
||||
foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
SMX.SMXConfig config = activePad.Item2; |
||||
SyncSliderThresholdsForConfig(ref config); |
||||
SMX.SMX.SetConfig(activePad.Item1, config); |
||||
} |
||||
|
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(null); |
||||
} |
||||
|
||||
public static bool IsAdvancedModeRequired() |
||||
{ |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
// 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[]>(); |
||||
}; |
||||
|
||||
// Manage launching on startup. |
||||
static class LaunchOnStartup |
||||
{ |
||||
public static string GetLaunchShortcutFilename() |
||||
{ |
||||
string startupFolder = Environment.GetFolderPath(Environment.SpecialFolder.Startup); |
||||
return startupFolder + "/StepManiaX.lnk"; |
||||
} |
||||
|
||||
// Enable or disable launching on startup. |
||||
public static bool Enable |
||||
{ |
||||
get { |
||||
return Properties.Settings.Default.LaunchOnStartup; |
||||
} |
||||
|
||||
set { |
||||
if(Properties.Settings.Default.LaunchOnStartup == value) |
||||
return; |
||||
|
||||
// Remember whether we want to be launched on startup. This is used as a sanity |
||||
// check in case we're not able to remove our launch shortcut. |
||||
Properties.Settings.Default.LaunchOnStartup = value; |
||||
Helpers.SaveApplicationSettings(); |
||||
|
||||
string shortcutFilename = GetLaunchShortcutFilename(); |
||||
if(value) |
||||
{ |
||||
string filename = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; |
||||
Helpers.CreateShortcut(shortcutFilename, filename, "-s"); |
||||
} else { |
||||
|
||||
try { |
||||
System.IO.File.Delete(shortcutFilename); |
||||
} catch { |
||||
// If there's an error deleting the shortcut (most likely it doesn't exist), |
||||
// don't do anything. |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
|
||||
// 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 pad auto-lighting. If we're running animations in SMXPanelAnimation, |
||||
// this will be overridden by it once it sends lights. |
||||
SMX.SMX.ReenableAutoLights(); |
||||
} |
||||
|
||||
private void AutoLightsColorRefreshColor() |
||||
{ |
||||
CommandBuffer cmd = new CommandBuffer(); |
||||
|
||||
for(int pad = 0; pad < 2; ++pad) |
||||
{ |
||||
// Use this panel's color. If a panel isn't connected, we still need to run the |
||||
// loop below to insert data for the panel. |
||||
byte[] color = new byte[9*3]; |
||||
SMX.SMXConfig config; |
||||
if(SMX.SMX.GetConfig(pad, out config)) |
||||
color = config.stepColor; |
||||
for( int iPanel = 0; iPanel < 9; ++iPanel ) |
||||
{ |
||||
for( int i = 0; i < 25; ++i ) |
||||
{ |
||||
// Auto-lights colors in the config packet are scaled so the firmware |
||||
// doesn't have to do it, but here we're setting the panel color to |
||||
// the auto-light color directly to preview the color. SetLights |
||||
// will apply the scaling, so we need to remove it. |
||||
cmd.Write( Helpers.UnscaleColor(color[iPanel*3+0]) ); |
||||
cmd.Write( Helpers.UnscaleColor(color[iPanel*3+1]) ); |
||||
cmd.Write( Helpers.UnscaleColor(color[iPanel*3+2]) ); |
||||
} |
||||
} |
||||
} |
||||
|
||||
SMX.SMX.SetLights2(cmd.Get()); |
||||
} |
||||
}; |
||||
|
||||
static class SMXHelpers |
||||
{ |
||||
// Export configurable values in SMXConfig to a JSON string. |
||||
public static string ExportSettingsToJSON(SMX.SMXConfig config) |
||||
{ |
||||
// The user only uses one of low or high thresholds. Only export the |
||||
// settings the user is actually using. |
||||
Dictionary<string, Object> dict = new Dictionary<string, Object>(); |
||||
if(config.fsr()) |
||||
{ |
||||
List<int> fsrLowThresholds = new List<int>(); |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
fsrLowThresholds.Add(config.panelSettings[panel].fsrLowThreshold[0]); |
||||
dict.Add("fsrLowThresholds", fsrLowThresholds); |
||||
|
||||
List<int> fsrHighThresholds = new List<int>(); |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
fsrHighThresholds.Add(config.panelSettings[panel].fsrHighThreshold[0]); |
||||
dict.Add("fsrHighThresholds", fsrHighThresholds); |
||||
} |
||||
else |
||||
{ |
||||
List<int> panelLowThresholds = new List<int>(); |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
panelLowThresholds.Add(config.panelSettings[panel].loadCellLowThreshold); |
||||
dict.Add("panelLowThresholds", panelLowThresholds); |
||||
|
||||
List<int> panelHighThresholds = new List<int>(); |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
panelHighThresholds.Add(config.panelSettings[panel].loadCellHighThreshold); |
||||
dict.Add("panelHighThresholds", panelHighThresholds); |
||||
} |
||||
|
||||
// Store the enabled panel mask as a simple list of which panels are selected. |
||||
bool[] enabledPanels = config.GetEnabledPanels(); |
||||
List<int> enabledPanelList = new List<int>(); |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
if(enabledPanels[panel]) |
||||
enabledPanelList.Add(panel); |
||||
} |
||||
dict.Add("enabledPanels", enabledPanelList); |
||||
|
||||
// Store panel colors. |
||||
List<string> panelColors = new List<string>(); |
||||
for(int PanelIndex = 0; PanelIndex < 9; ++PanelIndex) |
||||
{ |
||||
// Scale colors from the hardware value back to the 0-255 value we use in the UI. |
||||
Color color = Color.FromRgb(config.stepColor[PanelIndex*3+0], config.stepColor[PanelIndex*3+1], config.stepColor[PanelIndex*3+2]); |
||||
color = Helpers.UnscaleColor(color); |
||||
panelColors.Add(Helpers.ColorToString(color)); |
||||
} |
||||
dict.Add("panelColors", panelColors); |
||||
|
||||
dict.Add("advancedMode", Properties.Settings.Default.AdvancedMode); |
||||
dict.Add("useOuterSensorThresholds", Properties.Settings.Default.UseOuterSensorThresholds); |
||||
dict.Add("useInnerSensorThresholds", Properties.Settings.Default.UseInnerSensorThresholds); |
||||
dict.Add("customSensors", ThresholdSettings.GetCustomSensorsJSON()); |
||||
|
||||
return SMXJSON.SerializeJSON.Serialize(dict); |
||||
} |
||||
|
||||
// Import a saved JSON configuration to an SMXConfig. |
||||
public static void ImportSettingsFromJSON(string json, ref SMX.SMXConfig config) |
||||
{ |
||||
Dictionary<string, Object> dict; |
||||
try { |
||||
dict = SMXJSON.ParseJSON.Parse<Dictionary<string, Object>>(json); |
||||
} catch(ParseError e) { |
||||
MessageBox.Show(e.Message, "Error importing configuration", MessageBoxButton.OK, MessageBoxImage.Warning); |
||||
return; |
||||
} |
||||
|
||||
// Read the thresholds. If any values are missing, we'll leave the value in config alone. |
||||
if(config.fsr()) |
||||
{ |
||||
List<Object> newPanelLowThresholds = dict.Get("fsrLowThresholds", new List<Object>()); |
||||
List<Object> newPanelHighThresholds = dict.Get("fsrHighThresholds", new List<Object>()); |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
{ |
||||
config.panelSettings[panel].fsrLowThreshold[sensor] = (byte) newPanelLowThresholds.Get(panel, (int) config.panelSettings[panel].fsrLowThreshold[sensor]); |
||||
config.panelSettings[panel].fsrHighThreshold[sensor] = (byte) newPanelHighThresholds.Get(panel, (int) config.panelSettings[panel].fsrHighThreshold[sensor]); |
||||
} |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
List<Object> newPanelLowThresholds = dict.Get("panelLowThresholds", new List<Object>()); |
||||
List<Object> newPanelHighThresholds = dict.Get("panelHighThresholds", new List<Object>()); |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
config.panelSettings[panel].loadCellLowThreshold = newPanelLowThresholds.Get(panel, config.panelSettings[panel].loadCellLowThreshold); |
||||
config.panelSettings[panel].loadCellHighThreshold = newPanelHighThresholds.Get(panel, config.panelSettings[panel].loadCellHighThreshold); |
||||
} |
||||
} |
||||
|
||||
List<Object> enabledPanelList = dict.Get<List<Object>>("enabledPanels", null); |
||||
if(enabledPanelList != null) |
||||
{ |
||||
bool[] enabledPanels = new bool[9]; |
||||
for(int i = 0; i < enabledPanelList.Count; ++i) |
||||
{ |
||||
int panel = enabledPanelList.Get(i, 0); |
||||
|
||||
// Sanity check: |
||||
if(panel < 0 || panel >= 9) |
||||
continue; |
||||
enabledPanels[panel] = true; |
||||
} |
||||
config.SetEnabledPanels(enabledPanels); |
||||
} |
||||
|
||||
List<Object> panelColors = dict.Get<List<Object>>("panelColors", null); |
||||
if(panelColors != null) |
||||
{ |
||||
for(int PanelIndex = 0; PanelIndex < 9 && PanelIndex < panelColors.Count; ++PanelIndex) |
||||
{ |
||||
string colorString = panelColors.Get(PanelIndex, "#FFFFFF"); |
||||
Color color = Helpers.ParseColorString(colorString); |
||||
color = Helpers.ScaleColor(color); |
||||
|
||||
config.stepColor[PanelIndex*3+0] = color.R; |
||||
config.stepColor[PanelIndex*3+1] = color.G; |
||||
config.stepColor[PanelIndex*3+2] = color.B; |
||||
} |
||||
} |
||||
|
||||
// Older exported settings don't have advancedMode. Set it to true if it's missing. |
||||
Properties.Settings.Default.AdvancedMode = dict.Get<bool>("advancedMode", true); |
||||
Properties.Settings.Default.UseOuterSensorThresholds = dict.Get<bool>("useOuterSensorThresholds", false); |
||||
Properties.Settings.Default.UseInnerSensorThresholds = dict.Get<bool>("useInnerSensorThresholds", false); |
||||
List<object> customSensors = dict.Get<List<object>>("customSensors", null); |
||||
if(customSensors != null) |
||||
ThresholdSettings.SetCustomSensorsJSON(customSensors); |
||||
} |
||||
}; |
||||
} |
@ -0,0 +1,668 @@ |
||||
using System; |
||||
using System.Linq; |
||||
using System.Collections.Generic; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using System.Windows.Media; |
||||
using System.Windows.Interop; |
||||
|
||||
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); |
||||
}); |
||||
|
||||
// If we're controlling GIF animations and the firmware doesn't support |
||||
// doing animations internally, confirm exiting, since you can minimize |
||||
// to tray to keep playing animations. If we're not controlling animations, |
||||
// or the firmware supports doing them automatically, don't bug the user |
||||
// with a prompt. |
||||
Closing += delegate (object sender, System.ComponentModel.CancelEventArgs e) |
||||
{ |
||||
LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState(); |
||||
|
||||
// Don't use ActivePads here. Even if P1 is selected for configuration, |
||||
// we can still be controlling animations for P2, so check both connected |
||||
// pads. |
||||
bool shouldConfirmExit = false; |
||||
for(int pad = 0; pad < 2; ++pad) |
||||
{ |
||||
SMX.SMXConfig config; |
||||
if(!SMX.SMX.GetConfig(pad, out config)) |
||||
continue; |
||||
|
||||
// If the firmware is version 4 or higher, it supports animations directly. |
||||
// The user can upload GIF animations and doesn't need to leave us running |
||||
// for them to work. You can still use this tool to drive animations, but |
||||
// don't confirm exiting. |
||||
if(config.masterVersion >= 4) |
||||
continue; |
||||
|
||||
// If AutoLightingUsePressedAnimations isn't set, the panel is using step |
||||
// coloring instead of pressed animations. All firmwares support this. |
||||
// Don't confirm exiting for this mode. |
||||
if((config.configFlags & SMX.SMXConfigFlags.AutoLightingUsePressedAnimations) == 0) |
||||
continue; |
||||
|
||||
shouldConfirmExit = true; |
||||
} |
||||
|
||||
if(!shouldConfirmExit) |
||||
return; |
||||
|
||||
MessageBoxResult result = MessageBox.Show( |
||||
"Close StepManiaX configuration?\n\n" + |
||||
"GIF animations will keep playing if the application is minimized.", |
||||
"StepManiaX", System.Windows.MessageBoxButton.YesNo); |
||||
if(result == MessageBoxResult.No) |
||||
e.Cancel = true; |
||||
}; |
||||
} |
||||
|
||||
bool IsThresholdSliderShown(string type) |
||||
{ |
||||
bool AdvancedModeEnabled = Properties.Settings.Default.AdvancedMode; |
||||
SMX.SMXConfig config = ActivePad.GetFirstActivePadConfig(); |
||||
bool[] enabledPanels = config.GetEnabledPanels(); |
||||
|
||||
// Check the list of sensors this slider controls. If the list is empty, don't show it. |
||||
// For example, if the user adds all four sensors on the up panel to custom-sensors, the |
||||
// up button has nothing left to control, so we'll hide it. |
||||
// |
||||
// Don't do this for custom, inner-sensors or outer-sensors. Those are always shown in |
||||
// advanced mode. |
||||
List<ThresholdSettings.PanelAndSensor> panelAndSensors = ThresholdSettings.GetControlledSensorsForSliderType(type, AdvancedModeEnabled, false); |
||||
if(type == "custom-sensors" || type == "inner-sensors" || type == "outer-sensors") |
||||
{ |
||||
if(!AdvancedModeEnabled || !config.fsr()) |
||||
return false; |
||||
} |
||||
else |
||||
{ |
||||
if(panelAndSensors.Count == 0) |
||||
return false; |
||||
} |
||||
|
||||
// Hide thresholds that only affect panels that are disabled, so we don't show |
||||
// corner panel sliders in advanced mode if the corner panels are disabled. We |
||||
// don't handle this in GetControlledSensorsForSliderType, since we do want cardinal |
||||
// and corner to write thresholds to disabled panels, so they're in sync if they're |
||||
// turned back on. |
||||
switch(type) |
||||
{ |
||||
case "up-left": return enabledPanels[0]; |
||||
case "up": return enabledPanels[1]; |
||||
case "up-right": return enabledPanels[2]; |
||||
case "left": return enabledPanels[3]; |
||||
case "center": return enabledPanels[4]; |
||||
case "right": return enabledPanels[5]; |
||||
case "down-left": return enabledPanels[6]; |
||||
case "down": return enabledPanels[7]; |
||||
case "down-right": return enabledPanels[8]; |
||||
|
||||
// Show cardinal and corner if at least one panel they affect is enabled. |
||||
case "cardinal": return enabledPanels[3] || enabledPanels[5] || enabledPanels[8]; |
||||
case "corner": return enabledPanels[0] || enabledPanels[2] || enabledPanels[6] || enabledPanels[8]; |
||||
default: return true; |
||||
} |
||||
} |
||||
|
||||
ThresholdSlider CreateThresholdSlider(string type) |
||||
{ |
||||
ThresholdSlider slider = new ThresholdSlider(); |
||||
slider.Type = type; |
||||
return slider; |
||||
} |
||||
|
||||
void CreateThresholdSliders() |
||||
{ |
||||
// Remove and recreate threshold sliders. |
||||
ThresholdSliderContainer.Children.Clear(); |
||||
foreach(string sliderName in ThresholdSettings.thresholdSliderNames) |
||||
{ |
||||
if(!IsThresholdSliderShown(sliderName)) |
||||
continue; |
||||
|
||||
ThresholdSlider slider = CreateThresholdSlider(sliderName); |
||||
DockPanel.SetDock(slider, Dock.Top); |
||||
slider.Margin = new Thickness(0, 8, 0, 0); |
||||
ThresholdSliderContainer.Children.Add(slider); |
||||
} |
||||
|
||||
ThresholdSettings.SyncSliderThresholds(); |
||||
} |
||||
|
||||
public override void OnApplyTemplate() |
||||
{ |
||||
base.OnApplyTemplate(); |
||||
|
||||
// Add our WndProc hook. |
||||
HwndSource source = (HwndSource)PresentationSource.FromVisual(this); |
||||
source.AddHook(new HwndSourceHook(WndProcHook)); |
||||
|
||||
Version1.Content = "SMXConfig version " + SMX.SMX.Version(); |
||||
Version2.Content = "SMXConfig version " + SMX.SMX.Version(); |
||||
|
||||
AutoLightsColor.StartedDragging += delegate () { showAutoLightsColor.Start(); }; |
||||
AutoLightsColor.StoppedDragging += delegate () { showAutoLightsColor.Stop(); }; |
||||
AutoLightsColor.StoppedDragging += delegate () { showAutoLightsColor.Stop(); }; |
||||
|
||||
CreateThresholdSliders(); |
||||
|
||||
// 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) |
||||
{ |
||||
// Get the color of the selected color button, and apply it to all other buttons. |
||||
Color color = selectedButton.getColor(); |
||||
|
||||
ColorButton[] colorButtons = getColorPickerButtons(); |
||||
foreach(ColorButton button in colorButtons) |
||||
{ |
||||
// Only apply this to panel colors, not the floor color. |
||||
if((button as PanelColorButton) == null) |
||||
continue; |
||||
|
||||
button.setColor(color); |
||||
} |
||||
|
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(null); |
||||
}; |
||||
|
||||
// Listen to clicks on the panel color buttons. |
||||
ColorButton[] buttons = getColorPickerButtons(); |
||||
foreach(ColorButton button in buttons) |
||||
{ |
||||
button.Click += delegate (object sender, RoutedEventArgs e) |
||||
{ |
||||
ColorButton clickedButton = sender as ColorButton; |
||||
selectedButton = clickedButton; |
||||
|
||||
RefreshSelectedColorPicker(); |
||||
}; |
||||
} |
||||
} |
||||
|
||||
// We have two main modes: color mode and GIF animation mode. These behave differently |
||||
// on gen1-3 pads and gen4 pads. |
||||
// |
||||
// On gen4 pads, this determines whether we show animations on press, or show a solid color. |
||||
// This makes it easy to pick a solid color if that's all you want to change, instead of having |
||||
// to make a GIF with the color. We always show animations on release in both modes. |
||||
// |
||||
// On gen1-3 pads, panels are black in color mode, so they behave the same as they did originally. |
||||
// Animations are only shown in GIF animation mode. These animations are enabled with |
||||
// LightsAnimation_SetAuto. |
||||
// |
||||
// The gen1-3 firmware ignores the AutoLightingUsePressedAnimations flag, but we still use it to |
||||
// store which mode the user has selected. |
||||
private void PressedColorModeButton(object sender, RoutedEventArgs e) |
||||
{ |
||||
// The user pressed either the "panel colors" or "GIF animations" button. |
||||
bool pressedPanelColors = sender == PanelColorsButton; |
||||
|
||||
foreach(Tuple<int, SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
SMX.SMXConfig config = activePad.Item2; |
||||
|
||||
// If we're in panel colors mode, clear the AutoLightingUsePressedAnimations flag. |
||||
// Otherwise, set it. |
||||
if(pressedPanelColors) |
||||
config.configFlags &= ~SMX.SMXConfigFlags.AutoLightingUsePressedAnimations; |
||||
else |
||||
config.configFlags |= SMX.SMXConfigFlags.AutoLightingUsePressedAnimations; |
||||
SMX.SMX.SetConfig(activePad.Item1, config); |
||||
} |
||||
|
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(null); |
||||
} |
||||
|
||||
private void LoadUIFromConfig(LoadFromConfigDelegateArgs args) |
||||
{ |
||||
// Refresh whether LightsAnimation_SetAuto should be enabled. |
||||
SMX.SMXConfig firstConfig = ActivePad.GetFirstActivePadConfig(); |
||||
bool usePressedAnimationsEnabled = (firstConfig.configFlags & SMX.SMXConfigFlags.AutoLightingUsePressedAnimations) != 0; |
||||
SMX.SMX.LightsAnimation_SetAuto(usePressedAnimationsEnabled); |
||||
|
||||
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; |
||||
PanelColorP1.Visibility = args.controller[0].info.connected ? Visibility.Visible : Visibility.Collapsed; |
||||
PanelColorP2.Visibility = args.controller[1].info.connected ? Visibility.Visible : Visibility.Collapsed; |
||||
EnableCenterTopSensorCheckbox.Visibility = |
||||
P1_Floor.Visibility = |
||||
P2_Floor.Visibility = |
||||
args.firmwareVersion() >= 5 ? Visibility.Visible : Visibility.Collapsed; |
||||
|
||||
// Show the color slider or GIF UI depending on which one is set in flags. |
||||
// If both pads are turned on, just use the first one. |
||||
foreach(Tuple<int, SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
SMX.SMXConfig config = activePad.Item2; |
||||
|
||||
// If SMXConfigFlags_AutoLightingUsePressedAnimations is set, show the GIF UI. |
||||
// If it's not set, show the color slider UI. |
||||
SMX.SMXConfigFlags flags = config.configFlags; |
||||
bool usePressedAnimations = (flags & SMX.SMXConfigFlags.AutoLightingUsePressedAnimations) != 0; |
||||
ColorPickerGroup.Visibility = usePressedAnimations ? Visibility.Collapsed : Visibility.Visible; |
||||
GIFGroup.Visibility = usePressedAnimations ? Visibility.Visible : Visibility.Collapsed; |
||||
|
||||
// Tell the color mode buttons which one is selected, to set the button highlight. |
||||
PanelColorsButton.Selected = !usePressedAnimations; |
||||
GIFAnimationsButton.Selected = usePressedAnimations; |
||||
|
||||
break; |
||||
} |
||||
|
||||
RefreshConnectedPadList(args); |
||||
RefreshUploadPadText(args); |
||||
RefreshSelectedColorPicker(); |
||||
|
||||
// If a device has connected or disconnected, refresh the displayed threshold |
||||
// sliders. Don't do this otherwise, or we'll do this when the sliders are |
||||
// dragged. |
||||
if(args.ConnectionsChanged) |
||||
CreateThresholdSliders(); |
||||
|
||||
// Show the threshold warning explanation if any panels are showing the threshold warning icon. |
||||
bool ShowThresholdWarningText = false; |
||||
foreach(Tuple<int, SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
SMX.SMXConfig config = activePad.Item2; |
||||
for(int panelIdx = 0; panelIdx < 9; ++panelIdx) |
||||
{ |
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
{ |
||||
if(config.ShowThresholdWarning(panelIdx, sensor)) |
||||
ShowThresholdWarningText = true; |
||||
} |
||||
} |
||||
} |
||||
ThresholdWarningText.Visibility = ShowThresholdWarningText ? Visibility.Visible : Visibility.Hidden; |
||||
|
||||
// If a second controller has connected and we're on Both, see if we need to prompt |
||||
// to sync configs. We only actually need to do this if a controller just connected. |
||||
if(args.ConfigurationChanged) |
||||
CheckConfiguringBothPads(args); |
||||
} |
||||
|
||||
ColorButton selectedButton; |
||||
|
||||
// Return all color picker buttons. |
||||
ColorButton[] getColorPickerButtons() |
||||
{ |
||||
return new ColorButton[] { |
||||
P1_0, P1_1, P1_2, |
||||
P1_3, P1_4, P1_5, |
||||
P1_6, P1_7, P1_8, |
||||
P1_Floor, |
||||
|
||||
P2_0, P2_1, P2_2, |
||||
P2_3, P2_4, P2_5, |
||||
P2_6, P2_7, P2_8, |
||||
P2_Floor, |
||||
}; |
||||
} |
||||
|
||||
// Update the selected color picker based on the value of selectedButton. |
||||
private void RefreshSelectedColorPicker() |
||||
{ |
||||
LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState(); |
||||
|
||||
ColorButton[] buttons = getColorPickerButtons(); |
||||
|
||||
// If our selected button isn't enabled (or no button is selected), try to select a |
||||
// different one. |
||||
if(selectedButton == null || !selectedButton.isEnabled(args)) |
||||
{ |
||||
foreach(ColorButton button in buttons) |
||||
{ |
||||
if(button.isEnabled(args)) |
||||
{ |
||||
selectedButton = button; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Tell the buttons which one is selected. |
||||
foreach(ColorButton button in buttons) |
||||
button.IsSelected = button == selectedButton; |
||||
|
||||
// Tell the color slider which button is selected. |
||||
AutoLightsColor.colorButton = selectedButton; |
||||
} |
||||
|
||||
// Update which of the "Leave this application running", etc. blocks to display. |
||||
private void RefreshUploadPadText(LoadFromConfigDelegateArgs args) |
||||
{ |
||||
foreach(Tuple<int, SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
SMX.SMXConfig config = activePad.Item2; |
||||
|
||||
bool uploadsSupported = config.masterVersion >= 4; |
||||
LeaveRunning.Visibility = uploadsSupported ? Visibility.Collapsed : Visibility.Visible; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
private void ConnectedPadList_SelectionChanged(object sender, SelectionChangedEventArgs e) |
||||
{ |
||||
ComboBoxItem selection = ConnectedPadList.SelectedItem as ComboBoxItem; |
||||
ActivePad.SelectedPad newSelection; |
||||
if(selection == ConnectedPadList_P1) |
||||
newSelection = ActivePad.SelectedPad.P1; |
||||
else if(selection == ConnectedPadList_P2) |
||||
newSelection = ActivePad.SelectedPad.P2; |
||||
else |
||||
newSelection = ActivePad.SelectedPad.Both; |
||||
if(ActivePad.selectedPad == newSelection) |
||||
return; |
||||
|
||||
ActivePad.selectedPad = newSelection; |
||||
|
||||
// Before firing and updating UI, run CheckConfiguringBothPads to see if we should |
||||
// sync the config and/or change the selection again. |
||||
CheckConfiguringBothPads(CurrentSMXDevice.singleton.GetState()); |
||||
|
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(null); |
||||
} |
||||
|
||||
// If the user selects "Both", or connects a second pad while on "Both", both pads need |
||||
// to have the same configuration, since we're configuring them together. Check if the |
||||
// configuration is out of sync, and ask the user before syncing them up so we don't |
||||
// clobber P2's configuration if this wasn't intended. |
||||
// |
||||
// If the user cancels, change the pad selection to P1 so we don't clobber P2. |
||||
private void CheckConfiguringBothPads(LoadFromConfigDelegateArgs args) |
||||
{ |
||||
// Check if we're actually in "Both" mode with two controllers connected. If not, |
||||
// we don't have to do anything. |
||||
bool Pad1Connected = args.controller[0].info.connected; |
||||
bool Pad2Connected = args.controller[1].info.connected; |
||||
if(ActivePad.selectedPad != ActivePad.SelectedPad.Both || !Pad1Connected || !Pad2Connected) |
||||
return; |
||||
|
||||
// If the two pads have the same configuration, there's nothing to do. |
||||
SMX.SMXConfig config1 = args.controller[0].config; |
||||
SMX.SMXConfig config2 = args.controller[1].config; |
||||
if(ConfigurationsSynced(config1, config2)) |
||||
return; |
||||
|
||||
string messageBoxText = "The two pads have different settings. Do you want to " + |
||||
"match P2 settings to P1 and configure both pads together? (This won't affect panel colors.)"; |
||||
MessageBoxResult result = MessageBox.Show(messageBoxText, "StepManiaX", MessageBoxButton.YesNo, MessageBoxImage.None); |
||||
if(result == MessageBoxResult.Yes) |
||||
{ |
||||
SyncP2FromP1(config1, config2); |
||||
return; |
||||
} |
||||
else |
||||
{ |
||||
// Switch to P1. |
||||
ActivePad.selectedPad = ActivePad.SelectedPad.P1; |
||||
RefreshConnectedPadList(CurrentSMXDevice.singleton.GetState()); |
||||
} |
||||
|
||||
} |
||||
|
||||
// Return true if the two pads have the same configuration, so we can configure them together |
||||
// without clobbering separate configurations. |
||||
bool ConfigurationsSynced(SMX.SMXConfig config1, SMX.SMXConfig config2) |
||||
{ |
||||
if(!Enumerable.SequenceEqual(config1.GetLowThresholds(), config2.GetLowThresholds())) |
||||
return false; |
||||
if(!Enumerable.SequenceEqual(config1.GetHighThresholds(), config2.GetHighThresholds())) |
||||
return false; |
||||
if(!Enumerable.SequenceEqual(config1.enabledSensors, config2.enabledSensors)) |
||||
return false; |
||||
return true; |
||||
} |
||||
|
||||
// Copy the P2 pad configuration to P1. |
||||
// |
||||
// This only copies settings that we actually configure, and it doesn't copy pad |
||||
// colors, which is separate from pad selection. |
||||
void SyncP2FromP1(SMX.SMXConfig config1, SMX.SMXConfig config2) |
||||
{ |
||||
// Copy P1's configuration to P2. |
||||
Array.Copy(config1.enabledSensors, config2.enabledSensors, config1.enabledSensors.Count()); |
||||
config2.SetLowThresholds(config1.GetLowThresholds()); |
||||
config2.SetHighThresholds(config1.GetHighThresholds()); |
||||
SMX.SMX.SetConfig(1, config2); |
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(null); |
||||
} |
||||
|
||||
// Refresh which items are visible in the connected pads list, and which item is displayed as selected. |
||||
private void RefreshConnectedPadList(LoadFromConfigDelegateArgs args) |
||||
{ |
||||
bool TwoControllersConnected = args.controller[0].info.connected && args.controller[1].info.connected; |
||||
|
||||
// Only show the dropdown if two controllers are connected. |
||||
ConnectedPadList.Visibility = TwoControllersConnected ? Visibility.Visible : Visibility.Collapsed; |
||||
|
||||
// Only show the P1/P2 text if only one controller is connected, since it takes the place |
||||
// of the dropdown. |
||||
P1Connected.Visibility = (!TwoControllersConnected && args.controller[0].info.connected) ? Visibility.Visible : Visibility.Collapsed; |
||||
P2Connected.Visibility = (!TwoControllersConnected && args.controller[1].info.connected) ? Visibility.Visible : Visibility.Collapsed; |
||||
|
||||
if(!TwoControllersConnected) |
||||
return; |
||||
|
||||
// Set the current selection. |
||||
ActivePad.SelectedPad selectedPad = ActivePad.selectedPad; |
||||
switch(ActivePad.selectedPad) |
||||
{ |
||||
case ActivePad.SelectedPad.P1: ConnectedPadList.SelectedItem = ConnectedPadList_P1; break; |
||||
case ActivePad.SelectedPad.P2: ConnectedPadList.SelectedItem = ConnectedPadList_P2; break; |
||||
case ActivePad.SelectedPad.Both: ConnectedPadList.SelectedItem = ConnectedPadList_Both; break; |
||||
} |
||||
} |
||||
|
||||
|
||||
private void FactoryReset_Click(object sender, RoutedEventArgs e) |
||||
{ |
||||
foreach(Tuple<int, SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
int pad = activePad.Item1; |
||||
SMX.SMX.FactoryReset(pad); |
||||
} |
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(null); |
||||
} |
||||
|
||||
private void AdvancedModeEnabledCheckbox_Changed(object sender, RoutedEventArgs e) |
||||
{ |
||||
CreateThresholdSliders(); |
||||
} |
||||
|
||||
private void ExportSettings(object sender, RoutedEventArgs e) |
||||
{ |
||||
// Save the current thresholds on the first available pad as a preset. |
||||
foreach(Tuple<int, SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
int pad = activePad.Item1; |
||||
SMX.SMXConfig config = activePad.Item2; |
||||
string json = SMXHelpers.ExportSettingsToJSON(config); |
||||
|
||||
Microsoft.Win32.SaveFileDialog dialog = new Microsoft.Win32.SaveFileDialog(); |
||||
dialog.FileName = "StepManiaX settings"; |
||||
dialog.DefaultExt = ".smxcfg"; |
||||
dialog.Filter = "StepManiaX settings (.smxcfg)|*.smxcfg"; |
||||
bool? result = dialog.ShowDialog(); |
||||
if(result == null || !(bool)result) |
||||
return; |
||||
|
||||
System.IO.File.WriteAllText(dialog.FileName, json); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
private void ImportSettings(object sender, RoutedEventArgs e) |
||||
{ |
||||
// Prompt for a file to read. |
||||
Microsoft.Win32.OpenFileDialog dialog = new Microsoft.Win32.OpenFileDialog(); |
||||
dialog.FileName = "StepManiaX settings"; |
||||
dialog.DefaultExt = ".smxcfg"; |
||||
dialog.Filter = "StepManiaX settings (.smxcfg)|*.smxcfg"; |
||||
bool? result = dialog.ShowDialog(); |
||||
if(result == null || !(bool)result) |
||||
return; |
||||
|
||||
string json = Helpers.ReadFile(dialog.FileName); |
||||
|
||||
// Apply settings from the file to all active pads. |
||||
foreach(Tuple<int, SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
int pad = activePad.Item1; |
||||
SMX.SMXConfig config = activePad.Item2; |
||||
|
||||
SMXHelpers.ImportSettingsFromJSON(json, ref config); |
||||
SMX.SMX.SetConfig(pad, config); |
||||
} |
||||
|
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(null); |
||||
} |
||||
|
||||
private void LoadGIF(object sender, RoutedEventArgs e) |
||||
{ |
||||
// If the "load idle GIF" button was pressed, load the released animation. |
||||
// Otherwise, load the pressed animation. |
||||
bool pressed = sender == this.LoadPressed; |
||||
|
||||
// Prompt for a file to read. |
||||
Microsoft.Win32.OpenFileDialog dialog = new Microsoft.Win32.OpenFileDialog(); |
||||
dialog.FileName = "Select an animated GIF"; |
||||
dialog.DefaultExt = ".gif"; |
||||
dialog.Filter = "Animated GIF (.gif)|*.gif"; |
||||
bool? result = dialog.ShowDialog(); |
||||
if(result == null || !(bool)result) |
||||
return; |
||||
|
||||
byte[] buf = Helpers.ReadBinaryFile(dialog.FileName); |
||||
SMX.SMX.LightsType type = pressed ? SMX.SMX.LightsType.LightsType_Pressed : SMX.SMX.LightsType.LightsType_Released; |
||||
|
||||
foreach(Tuple<int, SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
int pad = activePad.Item1; |
||||
|
||||
// Load the animation. |
||||
string error; |
||||
if(!SMX.SMX.LightsAnimation_Load(buf, pad, type, out error)) |
||||
{ |
||||
// Any errors here are problems with the GIF, so there's no point trying |
||||
// to load it for the second pad if the first returns an error. Just show the |
||||
// error and stop. |
||||
MessageBox.Show(error, "Error", MessageBoxButton.OK, MessageBoxImage.Warning); |
||||
|
||||
// Return without saving to settings on error. |
||||
return; |
||||
} |
||||
|
||||
// Save the GIF to disk so we can load it quickly later. |
||||
Helpers.SaveAnimationToDisk(pad, type, buf); |
||||
|
||||
// Refresh after loading a GIF to update the "Leave this application running" text. |
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(null); |
||||
} |
||||
|
||||
// For firmwares that support it, upload the animation to the pad now. Otherwise, |
||||
// we'll run the animation directly. |
||||
foreach(Tuple<int, SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
int pad = activePad.Item1; |
||||
|
||||
SMX.SMXConfig config; |
||||
if(!SMX.SMX.GetConfig(pad, out config)) |
||||
continue; |
||||
|
||||
if(config.masterVersion >= 4) |
||||
UploadLatestGIF(); |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
private void UploadLatestGIF() |
||||
{ |
||||
// Create a progress window. Center it on top of the main window. |
||||
ProgressWindow dialog = new ProgressWindow(); |
||||
dialog.Left = (Left + Width/2) - (dialog.Width/2); |
||||
dialog.Top = (Top + Height/2) - (dialog.Height/2); |
||||
dialog.Title = "Storing animations on pad..."; |
||||
|
||||
int[] CurrentProgress = new int[] { 0, 0 }; |
||||
|
||||
// Upload graphics for all connected pads. If two pads are connected |
||||
// we can start both of these simultaneously, and they'll be sent in |
||||
// parallel. |
||||
int total = 0; |
||||
foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads()) |
||||
{ |
||||
int pad = activePad.Item1; |
||||
SMX.SMX.LightsUpload_BeginUpload(pad, delegate(int progress) { |
||||
// This is called from a thread, so dispatch back to the main thread. |
||||
Dispatcher.Invoke(delegate() { |
||||
// Store progress, so we can sum both pads. |
||||
CurrentProgress[pad] = progress; |
||||
|
||||
dialog.SetProgress(CurrentProgress[0] + CurrentProgress[1]); |
||||
if(progress == 100) |
||||
dialog.Close(); |
||||
}); |
||||
}); |
||||
|
||||
// Each pad that we start uploading to is 100 units of progress. |
||||
total += 100; |
||||
dialog.SetTotal(total); |
||||
} |
||||
|
||||
// Show the progress window as a modal dialog. This function won't return |
||||
// until we call dialog.Close above. |
||||
dialog.ShowDialog(); |
||||
} |
||||
|
||||
const int WM_SYSCOMMAND = 0x0112; |
||||
const int SC_MINIMIZE = 0xF020; |
||||
private IntPtr WndProcHook(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) |
||||
{ |
||||
App application = (App) Application.Current; |
||||
|
||||
if(msg == WM_SYSCOMMAND && ((int)wparam & 0xFFF0) == SC_MINIMIZE) |
||||
{ |
||||
// Cancel minimize, and call MinimizeToTray instead. |
||||
handled = true; |
||||
application.MinimizeToTray(); |
||||
} |
||||
|
||||
return IntPtr.Zero; |
||||
} |
||||
|
||||
private void MainTab_Selected(object sender, RoutedEventArgs e) |
||||
{ |
||||
if(Main.SelectedItem == SensitivityTab) |
||||
{ |
||||
// Refresh the threshold sliders, in case the enabled panels were changed |
||||
// on the advanced tab. |
||||
CreateThresholdSliders(); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
<Window x:Class="smx_config.ProgressWindow" |
||||
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" |
||||
mc:Ignorable="d" |
||||
Icon="Resources/window icon.png" |
||||
Background="#DDD" |
||||
Height="100" Width="325" ResizeMode="NoResize"> |
||||
<ProgressBar Name="ProgressBar" |
||||
Width="300" Height="50" |
||||
Value="0" |
||||
Maximum="100" |
||||
/> |
||||
</Window> |
@ -0,0 +1,45 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Runtime.InteropServices; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using System.Windows.Interop; |
||||
|
||||
namespace smx_config |
||||
{ |
||||
public partial class ProgressWindow: Window |
||||
{ |
||||
private const int GWL_STYLE = -16; |
||||
private const int WS_SYSMENU = 0x80000; |
||||
[DllImport("user32.dll", SetLastError = true)] |
||||
private static extern int GetWindowLong(IntPtr hWnd, int nIndex); |
||||
[DllImport("user32.dll")] |
||||
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); |
||||
|
||||
public ProgressWindow() |
||||
{ |
||||
InitializeComponent(); |
||||
|
||||
// Hide the window close button, since we can't easily cancel. |
||||
Loaded += delegate(object sender, RoutedEventArgs e) { |
||||
var hwnd = new WindowInteropHelper(this).Handle; |
||||
SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU); |
||||
}; |
||||
} |
||||
|
||||
public void SetTotal(int total) |
||||
{ |
||||
ProgressBar.Maximum = total; |
||||
} |
||||
|
||||
public void SetProgress(int progress) |
||||
{ |
||||
ProgressBar.Value = progress; |
||||
} |
||||
|
||||
public override void OnApplyTemplate() |
||||
{ |
||||
base.OnApplyTemplate(); |
||||
} |
||||
} |
||||
} |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 108 B |
After Width: | Height: | Size: 141 B |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,652 @@ |
||||
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; |
||||
}; |
||||
|
||||
// Bits for SMXConfig::flags. |
||||
public enum SMXConfigFlags { |
||||
AutoLightingUsePressedAnimations = 1 << 0, |
||||
PlatformFlags_FSR = 1 << 1, |
||||
}; |
||||
|
||||
[Serializable] |
||||
public struct PackedSensorSettings { |
||||
// Load cell thresholds: |
||||
public Byte loadCellLowThreshold; |
||||
public Byte loadCellHighThreshold; |
||||
|
||||
// FSR thresholds (16-bit): |
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] |
||||
public Byte[] fsrLowThreshold; |
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] |
||||
public Byte[] fsrHighThreshold; |
||||
|
||||
public UInt16 combinedLowThreshold; |
||||
public UInt16 combinedHighThreshold; |
||||
|
||||
// This must be left unchanged. |
||||
public UInt16 reserved; |
||||
}; |
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack=1)] |
||||
[Serializable] |
||||
public struct SMXConfig { |
||||
public Byte masterVersion; |
||||
public Byte configVersion; |
||||
|
||||
// Packed flags (SMXConfigFlags). |
||||
public Byte flags; |
||||
|
||||
public UInt16 debounceNodelayMilliseconds; |
||||
public UInt16 debounceDelayMs; |
||||
public UInt16 panelDebounceMicroseconds; |
||||
public Byte autoCalibrationMaxDeviation; |
||||
public Byte badSensorMinimumDelaySeconds; |
||||
public UInt16 autoCalibrationAveragesPerUpdate; |
||||
public UInt16 autoCalibrationSamplesPerAverage; |
||||
public UInt16 autoCalibrationMaxTare; |
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] |
||||
public Byte[] enabledSensors; |
||||
|
||||
public Byte autoLightsTimeout; |
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3*9)] |
||||
public Byte[] stepColor; |
||||
|
||||
// The default color to set the platform LED strip to. |
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] |
||||
public Byte[] platformStripColor; |
||||
|
||||
// Which panels to enable auto-lighting for. Disabled panels will be unlit. |
||||
// 0x01 = panel 0, 0x02 = panel 1, 0x04 = panel 2, etc. This only affects |
||||
// the master controller's built-in auto lighting and not lights data send |
||||
// from the SDK. |
||||
public UInt16 autoLightPanelMask; |
||||
|
||||
public Byte panelRotation; |
||||
|
||||
// Per-panel sensor settings: |
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)] |
||||
public PackedSensorSettings[] panelSettings; |
||||
|
||||
// These are internal tunables and should be left unchanged. |
||||
public Byte preDetailsDelayMilliseconds; |
||||
|
||||
// Pad this struct to exactly 250 bytes. |
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 49)] |
||||
public Byte[] padding; |
||||
|
||||
// It would be simpler to set flags to [MarshalAs(UnmanagedType.U8)], but |
||||
// that doesn't work. |
||||
public SMXConfigFlags configFlags { |
||||
get { |
||||
return (SMXConfigFlags) flags; |
||||
} |
||||
|
||||
set { |
||||
flags = (Byte) value; |
||||
} |
||||
} |
||||
|
||||
// Return true if the platform is using FSRs, or false for load cells. |
||||
public bool fsr() |
||||
{ |
||||
return masterVersion >= 4 && (configFlags & SMXConfigFlags.PlatformFlags_FSR) != 0; |
||||
} |
||||
|
||||
// Return true if the low threshold is set too low. |
||||
// |
||||
// Higher low threshold values make the panel respond to the panel being released more |
||||
// quickly. It shouldn't be set too low. |
||||
public bool ShowThresholdWarning(int panel, int sensor) |
||||
{ |
||||
if(!fsr()) |
||||
return false; |
||||
|
||||
// Don't show warnings for disabled panels. |
||||
if(!GetEnabledPanels()[panel]) |
||||
return false; |
||||
|
||||
int lower = panelSettings[panel].fsrLowThreshold[sensor]; |
||||
int MinimumRecommendedLowThreshold = 140; |
||||
return lower < MinimumRecommendedLowThreshold; |
||||
} |
||||
|
||||
// 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, |
||||
}; |
||||
} |
||||
|
||||
// Set enabledSensors from an array returned from GetEnabledPanels. |
||||
public void SetEnabledPanels(bool[] panels) |
||||
{ |
||||
for(int i = 0; i < 5; ++i) |
||||
enabledSensors[i] = 0; |
||||
|
||||
if(panels[0]) enabledSensors[0] |= 0xF0; |
||||
if(panels[1]) enabledSensors[0] |= 0x0F; |
||||
if(panels[2]) enabledSensors[1] |= 0xF0; |
||||
if(panels[3]) enabledSensors[1] |= 0x0F; |
||||
if(panels[4]) enabledSensors[2] |= 0xF0; |
||||
if(panels[5]) enabledSensors[2] |= 0x0F; |
||||
if(panels[6]) enabledSensors[3] |= 0xF0; |
||||
if(panels[7]) enabledSensors[3] |= 0x0F; |
||||
if(panels[8]) enabledSensors[4] |= 0xF0; |
||||
} |
||||
|
||||
// Return the index of the first enabled panel, or 1 (up) if no panels |
||||
// are enabled. |
||||
public int GetFirstEnabledPanel() |
||||
{ |
||||
bool[] enabledPanels = GetEnabledPanels(); |
||||
for(int i = 0; i < 9; ++i) |
||||
{ |
||||
if(enabledPanels[i]) |
||||
return i; |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
// The layout of this structure (and the underlying C struct) matches the firmware configuration |
||||
// data. This is a bit inconvenient for the panel thresholds which aren't contiguous, so these |
||||
// helpers just convert them to and from arrays. |
||||
// XXX: used? |
||||
public Byte[] GetLowThresholds() |
||||
{ |
||||
return new Byte[] { |
||||
panelSettings[0].loadCellLowThreshold, |
||||
panelSettings[1].loadCellLowThreshold, |
||||
panelSettings[2].loadCellLowThreshold, |
||||
panelSettings[3].loadCellLowThreshold, |
||||
panelSettings[4].loadCellLowThreshold, |
||||
panelSettings[5].loadCellLowThreshold, |
||||
panelSettings[6].loadCellLowThreshold, |
||||
panelSettings[7].loadCellLowThreshold, |
||||
panelSettings[8].loadCellLowThreshold, |
||||
}; |
||||
} |
||||
|
||||
public Byte[] GetHighThresholds() |
||||
{ |
||||
return new Byte[] { |
||||
panelSettings[0].loadCellHighThreshold, |
||||
panelSettings[1].loadCellHighThreshold, |
||||
panelSettings[2].loadCellHighThreshold, |
||||
panelSettings[3].loadCellHighThreshold, |
||||
panelSettings[4].loadCellHighThreshold, |
||||
panelSettings[5].loadCellHighThreshold, |
||||
panelSettings[6].loadCellHighThreshold, |
||||
panelSettings[7].loadCellHighThreshold, |
||||
panelSettings[8].loadCellHighThreshold, |
||||
}; |
||||
} |
||||
|
||||
public void SetLowThresholds(Byte[] values) |
||||
{ |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
panelSettings[panel].loadCellLowThreshold = values[panel]; |
||||
} |
||||
|
||||
public void SetHighThresholds(Byte[] values) |
||||
{ |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
panelSettings[panel].loadCellHighThreshold = values[panel]; |
||||
} |
||||
|
||||
// Create an empty SMXConfig. |
||||
static public SMXConfig Create() |
||||
{ |
||||
SMXConfig result = new SMXConfig(); |
||||
result.enabledSensors = new Byte[5]; |
||||
result.stepColor = new Byte[3*9]; |
||||
result.panelSettings = new PackedSensorSettings[9]; |
||||
result.platformStripColor = new Byte[3]; |
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
result.panelSettings[panel].fsrLowThreshold = new Byte[4]; |
||||
result.panelSettings[panel].fsrHighThreshold = new Byte[4]; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
// autoLightPanelMask controls which lights the master controller will light. Only the |
||||
// first 9 bits (0x1ff) are meaningful for our 9 panels. As a special case, we use 0xFFFF |
||||
// to indicate that "light all panels" was checked. The controller doesn't care about this |
||||
// since it only looks at the first 9 bits. |
||||
public bool getLightAllPanelsMode() { return autoLightPanelMask == 0xFFFF; } |
||||
|
||||
public void setLightAllPanelsMode(bool enable) |
||||
{ |
||||
if(enable) |
||||
autoLightPanelMask = 0xFFFF; |
||||
else |
||||
refreshAutoLightPanelMask(false); |
||||
} |
||||
|
||||
// If we're not in light all panels mode, set autoLightPanelMask to the currently |
||||
// enabled panels. This should be called if enabledSensors is changed. |
||||
public void refreshAutoLightPanelMask(bool onlyIfEnabled=true) |
||||
{ |
||||
if(onlyIfEnabled && getLightAllPanelsMode()) |
||||
return; |
||||
|
||||
// Set autoLightPanelMask to just the enabled panels. |
||||
autoLightPanelMask = 0; |
||||
bool[] enabledPanels = GetEnabledPanels(); |
||||
for(int i = 0; i < 9; ++i) |
||||
if(enabledPanels[i]) |
||||
autoLightPanelMask |= (UInt16) (1 << i); |
||||
} |
||||
}; |
||||
|
||||
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; |
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, ArraySubType=UnmanagedType.I1, SizeConst = 9*4)] |
||||
public bool[] iWrongSensorJumper; |
||||
|
||||
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) && |
||||
Helpers.SequenceEqual(iWrongSensorJumper, other.iWrongSensorJumper); |
||||
} |
||||
|
||||
// Dummy override to silence a bad warning. We don't use these in containers that 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 bool AnyBadJumpersOnPanel(int panel) |
||||
{ |
||||
if(!bHaveDataFromPanel[panel]) |
||||
return false; |
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
if(iWrongSensorJumper[panel*4+sensor]) |
||||
return true; |
||||
|
||||
return false; |
||||
} |
||||
}; |
||||
|
||||
public static class SMX |
||||
{ |
||||
[System.Flags] |
||||
enum LoadLibraryFlags : uint |
||||
{ |
||||
None = 0, |
||||
DONT_RESOLVE_DLL_REFERENCES = 0x00000001, |
||||
LOAD_IGNORE_CODE_AUTHZ_LEVEL = 0x00000010, |
||||
LOAD_LIBRARY_AS_DATAFILE = 0x00000002, |
||||
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040, |
||||
LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020, |
||||
LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 0x00000200, |
||||
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000, |
||||
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100, |
||||
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800, |
||||
LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400, |
||||
LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008 |
||||
} |
||||
|
||||
[DllImport("kernel32", SetLastError=true)] |
||||
static extern IntPtr LoadLibrary(string lpFileName); |
||||
[DllImport("kernel32.dll", SetLastError = true)] |
||||
static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hReservedNull, LoadLibraryFlags dwFlags); |
||||
[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 void SMX_SetPanelTestMode(int mode); |
||||
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||
private static extern bool SMX_SetLights2(byte[] buf, int lightDataSize); |
||||
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||
private static extern bool SMX_SetPlatformLights(byte[] buf, int lightDataSize); |
||||
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] |
||||
private static extern bool SMX_ReenableAutoLights(); |
||||
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] |
||||
private static extern IntPtr SMX_Version(); |
||||
|
||||
public enum LightsType |
||||
{ |
||||
LightsType_Released, // animation while panels are released |
||||
LightsType_Pressed, // animation while panel is pressed |
||||
}; |
||||
|
||||
public static string Version() |
||||
{ |
||||
if(!DLLAvailable()) return ""; |
||||
|
||||
// I can't find any way to marshal a simple null-terminated string. Marshalling |
||||
// UnmanagedType.LPStr tries to deallocate the string, which crashes since it's |
||||
// a static string. |
||||
unsafe { |
||||
sbyte *p = (sbyte *) SMX_Version(); |
||||
int length = 0; |
||||
while(p[length] != 0) |
||||
++length; |
||||
return new string(p, 0, length); |
||||
} |
||||
} |
||||
|
||||
// Check if the native DLL is available. This is mostly to avoid exceptions in the designer. |
||||
// This returns false if the DLL doesn't load. |
||||
public static bool DLLAvailable() |
||||
{ |
||||
return LoadLibrary("SMX.dll") != IntPtr.Zero; |
||||
} |
||||
|
||||
// Check if the native DLL exists. This will return false if SMX.dll is missing entirely, |
||||
// but not if it fails to load for another reason like runtime dependencies. This just lets |
||||
// us print a more specific error message. |
||||
public static bool DLLExists() |
||||
{ |
||||
return LoadLibraryEx("SMX.dll", (IntPtr)0, LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE) != 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 Start(UpdateCallback callback) |
||||
{ |
||||
if(!DLLAvailable()) return; |
||||
|
||||
// Sanity check SMXConfig, which should be 250 bytes. If this is incorrect, |
||||
// check the padding array. |
||||
{ |
||||
SMXConfig config = new SMXConfig(); |
||||
int bytes = Marshal.SizeOf(config); |
||||
if(bytes != 250) |
||||
throw new Exception("SMXConfig is " + bytes + " bytes, but should be 250 bytes"); |
||||
} |
||||
|
||||
// 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 = SMXConfig.Create(); |
||||
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 SetSensorTestMode(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 enum PanelTestMode { |
||||
Off = '0', |
||||
PressureTest = '1', |
||||
}; |
||||
|
||||
public static void SetPanelTestMode(PanelTestMode mode) |
||||
{ |
||||
if(!DLLAvailable()) return; |
||||
SMX_SetPanelTestMode((int) mode); |
||||
} |
||||
|
||||
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 SetLights2(byte[] buf) |
||||
{ |
||||
if(!DLLAvailable()) return; |
||||
|
||||
SMX_SetLights2(buf, buf.Length); |
||||
} |
||||
|
||||
public static void SMX_SetPlatformLights(byte[] buf) |
||||
{ |
||||
if(!DLLAvailable()) return; |
||||
|
||||
SMX_SetPlatformLights(buf, buf.Length); |
||||
} |
||||
|
||||
public static void ReenableAutoLights() |
||||
{ |
||||
if(!DLLAvailable()) return; |
||||
|
||||
SMX_ReenableAutoLights(); |
||||
} |
||||
|
||||
// SMXPanelAnimation |
||||
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] |
||||
[return:MarshalAs(UnmanagedType.I1)] |
||||
private static extern bool SMX_LightsAnimation_Load(byte[] buf, int size, int pad, int type, out IntPtr error); |
||||
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] |
||||
private static extern void SMX_LightsAnimation_SetAuto(bool enable); |
||||
|
||||
public static bool LightsAnimation_Load(byte[] buf, int pad, LightsType type, out string error) |
||||
{ |
||||
if(!DLLAvailable()) |
||||
{ |
||||
error = "SMX.DLL not available"; |
||||
return false; |
||||
} |
||||
|
||||
error = ""; |
||||
IntPtr error_pointer; |
||||
bool result = SMX_LightsAnimation_Load(buf, buf.Length, pad, (int) type, out error_pointer); |
||||
if(!result) |
||||
{ |
||||
// SMX_LightsAnimation_Load takes a char **error, which is set to the error |
||||
// string. |
||||
error = Marshal.PtrToStringAnsi(error_pointer); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
public static void LightsAnimation_SetAuto(bool enable) |
||||
{ |
||||
if(!DLLAvailable()) return; |
||||
SMX_LightsAnimation_SetAuto(enable); |
||||
} |
||||
|
||||
// SMXPanelAnimationUpload |
||||
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] |
||||
private static extern void SMX_LightsUpload_BeginUpload(int pad, |
||||
[MarshalAs(UnmanagedType.FunctionPtr)] InternalLightsUploadCallback callback, |
||||
IntPtr user); |
||||
|
||||
public delegate void LightsUploadCallback(int progress); |
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] |
||||
private delegate void InternalLightsUploadCallback(int reason, IntPtr user); |
||||
public static void LightsUpload_BeginUpload(int pad, LightsUploadCallback callback) |
||||
{ |
||||
if(!DLLAvailable()) |
||||
return; |
||||
|
||||
GCHandle handle = new GCHandle(); |
||||
InternalLightsUploadCallback wrapper = delegate(int progress, IntPtr user) |
||||
{ |
||||
try { |
||||
callback(progress); |
||||
} finally { |
||||
// When progress = 100, this is the final call and we can release this |
||||
// object to GC. |
||||
if(progress == 100) |
||||
handle.Free(); |
||||
} |
||||
}; |
||||
|
||||
// Pin the callback until we get the last call. |
||||
handle = GCHandle.Alloc(wrapper); |
||||
|
||||
SMX_LightsUpload_BeginUpload(pad, wrapper, IntPtr.Zero); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,102 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using System.Windows.Controls.Primitives; |
||||
|
||||
namespace smx_config |
||||
{ |
||||
public class SensorSelectionButton: ToggleButton |
||||
{ |
||||
} |
||||
|
||||
// A control with one button for each of four sensors: |
||||
class SensorSelector: Control |
||||
{ |
||||
// The panel we're editing (0-8). |
||||
public static readonly DependencyProperty PanelProperty = DependencyProperty.RegisterAttached("Panel", |
||||
typeof(int), typeof(SensorSelector), new FrameworkPropertyMetadata(0)); |
||||
|
||||
public int Panel { |
||||
get { return (int) this.GetValue(PanelProperty); } |
||||
set { this.SetValue(PanelProperty, value); } |
||||
} |
||||
|
||||
ToggleButton[] SensorSelectionButtons = new ToggleButton[4]; |
||||
public override void OnApplyTemplate() |
||||
{ |
||||
base.OnApplyTemplate(); |
||||
|
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
{ |
||||
int ThisSensor = sensor; // bind |
||||
SensorSelectionButtons[sensor] = GetTemplateChild("Sensor" + sensor) as ToggleButton; |
||||
SensorSelectionButtons[sensor].Click += delegate(object sender, RoutedEventArgs e) |
||||
{ |
||||
ClickedSensorButton(ThisSensor); |
||||
}; |
||||
} |
||||
|
||||
// These settings are stored in the application settings, not on the pad. However, |
||||
// we treat changes to this as config changes, so we can use the same OnConfigChange |
||||
// method for updating. |
||||
OnConfigChange onConfigChange; |
||||
onConfigChange = new OnConfigChange(this, delegate(LoadFromConfigDelegateArgs args) { |
||||
LoadUIFromConfig(args); |
||||
}); |
||||
} |
||||
|
||||
private void ClickedSensorButton(int sensor) |
||||
{ |
||||
// Toggle the clicked sensor. |
||||
Console.WriteLine("Clicked sensor " + sensor); |
||||
List<ThresholdSettings.PanelAndSensor> customSensors = ThresholdSettings.GetCustomSensors(); |
||||
bool enabled = !IsSensorEnabled(customSensors, sensor); |
||||
|
||||
if(enabled) |
||||
customSensors.Add(new ThresholdSettings.PanelAndSensor(Panel, sensor)); |
||||
else |
||||
customSensors.Remove(new ThresholdSettings.PanelAndSensor(Panel, sensor)); |
||||
ThresholdSettings.SetCustomSensors(customSensors); |
||||
|
||||
// Sync thresholds after changing custom sensors. |
||||
ThresholdSettings.SyncSliderThresholds(); |
||||
|
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(this); |
||||
} |
||||
|
||||
// Return true if the given sensor is included in custom-sensors. |
||||
bool IsSensorEnabled(List<ThresholdSettings.PanelAndSensor> customSensors, int sensor) |
||||
{ |
||||
foreach(ThresholdSettings.PanelAndSensor panelAndSensor in customSensors) |
||||
{ |
||||
if(panelAndSensor.panel == Panel && panelAndSensor.sensor == sensor) |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private void LoadUIFromConfig(LoadFromConfigDelegateArgs args) |
||||
{ |
||||
// Check the selected custom-sensors. |
||||
List<ThresholdSettings.PanelAndSensor> customSensors = ThresholdSettings.GetCustomSensors(); |
||||
for(int sensor = 0; sensor < 4; ++sensor) |
||||
SensorSelectionButtons[sensor].IsChecked = IsSensorEnabled(customSensors, sensor); |
||||
} |
||||
} |
||||
|
||||
// This dialog sets which sensors are controlled by custom-sensors. The actual work is done |
||||
// by SensorSelector above. |
||||
public partial class SetCustomSensors: Window |
||||
{ |
||||
public SetCustomSensors() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
|
||||
private void OK_Click(object sender, RoutedEventArgs e) |
||||
{ |
||||
Close(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
InstallSMXConfig.exe |
@ -0,0 +1,168 @@ |
||||
!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 |
||||
|
||||
# Global variables and lots of gotos. That's all NSIS can do. I feel like I'm 8 again, writing in BASIC. |
||||
# Also, we need to use some macro nastiness to convince NSIS to include this in both the installer and uninstaller. |
||||
Var /Global "ShutdownRetries" |
||||
!macro myfunc un |
||||
Function ${un}CheckRunning |
||||
# Reset ShutdownRetries. |
||||
StrCpy $ShutdownRetries "0" |
||||
|
||||
retry: |
||||
# Check if SMXConfig is running. For now we use SMXConfigEvent for this, which is used to foreground |
||||
# the application, since it's been around for a while. SMXConfigShutdown is only available in newer |
||||
# versions. |
||||
System::Call 'kernel32::OpenEventW(i 0x00100000, b 0, w "SMXConfigEvent") i .R0' |
||||
IntCmp $R0 0 done |
||||
System::Call 'kernel32::CloseHandle(i $R0)' |
||||
|
||||
try_to_shut_down: |
||||
IntOp $ShutdownRetries $ShutdownRetries + 1 |
||||
IntCmp $ShutdownRetries 10 failed_to_shut_down |
||||
|
||||
# SMXConfig is running. See if SMXConfigShutdown is available, to let us exit it automatically. |
||||
System::Call 'kernel32::OpenEventW(i 0x00100000|0x0002, b 0, w "SMXConfigShutdown") i .R0' |
||||
IntCmp $R0 0 CantShutdownAutomatically |
||||
|
||||
# We have the shutdown handle. Signal it to tell SMXConfig to exit. |
||||
System::Call 'kernel32::SetEvent(i $R0) i .R0' |
||||
System::Call 'kernel32::CloseHandle(i $R0)' |
||||
|
||||
# Wait briefly to give it a chance to shut down, then loop and check that it's shut down. If it isn't, |
||||
# we'll retry a few times. |
||||
Sleep 100 |
||||
goto retry |
||||
# goto done |
||||
|
||||
# System::Call "Kernel32::GetLastError() i() .r1" |
||||
|
||||
failed_to_shut_down: |
||||
StrCpy $ShutdownRetries "0" |
||||
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "Please close SMXConfig before updating." /SD IDCANCEL IDRETRY retry IDCANCEL cancel |
||||
Quit |
||||
|
||||
CantShutdownAutomatically: |
||||
# SMXConfig is running, but it's an older version that doesn't have a shutdown signal. Ask the |
||||
# user to shut it down. Retry will restart and check if it's not running. |
||||
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "Please close SMXConfig." /SD IDCANCEL IDRETRY retry IDCANCEL cancel |
||||
Quit |
||||
|
||||
cancel: |
||||
Quit |
||||
done: |
||||
FunctionEnd |
||||
!macroend |
||||
|
||||
!insertmacro myfunc "" |
||||
!insertmacro myfunc "un." |
||||
|
||||
Page custom CheckRunning |
||||
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 "un.CheckRunning" |
||||
#SectionEnd |
||||
|
||||
Section "Uninstall" |
||||
# Make sure SMXConfig isn't running. |
||||
Call un.CheckRunning |
||||
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 |