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 |
.vs |
||||||
## 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 |
|
||||||
*.user |
*.user |
||||||
*.userosscache |
build |
||||||
*.sln.docstates |
obj |
||||||
|
out |
||||||
# 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 |
|
||||||
|
@ -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 |