Compare commits

..

No commits in common. '7ea04b949990d516fe4afc8c7285329373821651' and '5979eddc21a09ae71b7b3ddb31fd14078f2a7c2c' have entirely different histories.

  1. 8
      .editorconfig
  2. 63
      .gitattributes
  3. 366
      .gitignore
  4. 22
      LICENSE.txt
  5. 7
      README.md
  6. 37
      SMX.sln
  7. 31
      SMX_PGE.sln
  8. 37
      SMX_PGE/SMX_PGE.cpp
  9. 6695
      SMX_PGE/olcPixelGameEngine.h
  10. BIN
      docs/icon.png
  11. 183
      docs/index.html
  12. BIN
      docs/logo.png
  13. 26
      docs/style.css
  14. 57
      sample/SMXSample.cpp
  15. 106
      sample/SMXSample.vcxproj
  16. 19
      sample/SMXSample.vcxproj.filters
  17. 6695
      sample/olcPixelGameEngine.h
  18. 319
      sdk/SMX.h
  19. 3
      sdk/Windows/.gitignore
  20. 336
      sdk/Windows/Helpers.cpp
  21. 152
      sdk/Windows/Helpers.h
  22. 124
      sdk/Windows/SMX.cpp
  23. 151
      sdk/Windows/SMX.vcxproj
  24. 94
      sdk/Windows/SMX.vcxproj.filters
  25. 169
      sdk/Windows/SMXConfigPacket.cpp
  26. 12
      sdk/Windows/SMXConfigPacket.h
  27. 607
      sdk/Windows/SMXDevice.cpp
  28. 136
      sdk/Windows/SMXDevice.h
  29. 437
      sdk/Windows/SMXDeviceConnection.cpp
  30. 131
      sdk/Windows/SMXDeviceConnection.h
  31. 174
      sdk/Windows/SMXDeviceSearch.cpp
  32. 33
      sdk/Windows/SMXDeviceSearch.h
  33. 97
      sdk/Windows/SMXDeviceSearchThreaded.cpp
  34. 46
      sdk/Windows/SMXDeviceSearchThreaded.h
  35. 545
      sdk/Windows/SMXGif.cpp
  36. 72
      sdk/Windows/SMXGif.h
  37. 42
      sdk/Windows/SMXHelperThread.cpp
  38. 31
      sdk/Windows/SMXHelperThread.h
  39. 704
      sdk/Windows/SMXManager.cpp
  40. 96
      sdk/Windows/SMXManager.h
  41. 519
      sdk/Windows/SMXPanelAnimation.cpp
  42. 57
      sdk/Windows/SMXPanelAnimation.h
  43. 484
      sdk/Windows/SMXPanelAnimationUpload.cpp
  44. 39
      sdk/Windows/SMXPanelAnimationUpload.h
  45. 49
      sdk/Windows/SMXThread.cpp
  46. 45
      sdk/Windows/SMXThread.h
  47. 43
      sdk/Windows/update-build-version.bat
  48. 1
      smx-config/.gitignore
  49. 30
      smx-config/App.config
  50. 8
      smx-config/App.xaml
  51. 250
      smx-config/App.xaml.cs
  52. 159
      smx-config/ConfigPresets.cs
  53. 283
      smx-config/CurrentSMXDevice.cs
  54. 403
      smx-config/DiagnosticsWidgets.cs
  55. 222
      smx-config/DoubleSlider.cs
  56. 984
      smx-config/Helpers.cs
  57. 1064
      smx-config/MainWindow.xaml
  58. 668
      smx-config/MainWindow.xaml.cs
  59. 15
      smx-config/ProgressWindow.xaml
  60. 45
      smx-config/ProgressWindow.xaml.cs
  61. 55
      smx-config/Properties/AssemblyInfo.cs
  62. 63
      smx-config/Properties/Resources.Designer.cs
  63. 120
      smx-config/Properties/Resources.resx
  64. 86
      smx-config/Properties/Settings.Designer.cs
  65. 21
      smx-config/Properties/Settings.settings
  66. BIN
      smx-config/Resources/DIP labels.png
  67. BIN
      smx-config/Resources/DIP.png
  68. BIN
      smx-config/Resources/pressed.gif
  69. BIN
      smx-config/Resources/released.gif
  70. BIN
      smx-config/Resources/sensor_down.png
  71. BIN
      smx-config/Resources/sensor_left.png
  72. BIN
      smx-config/Resources/sensor_right.png
  73. BIN
      smx-config/Resources/sensor_up.png
  74. BIN
      smx-config/Resources/threshold_warning.png
  75. BIN
      smx-config/Resources/window icon grey.ico
  76. BIN
      smx-config/Resources/window icon grey.png
  77. BIN
      smx-config/Resources/window icon.ico
  78. BIN
      smx-config/Resources/window icon.png
  79. 652
      smx-config/SMX.cs
  80. 222
      smx-config/SMXConfig.csproj
  81. 535
      smx-config/SMXJSON.cs
  82. 101
      smx-config/SetCustomSensors.xaml
  83. 102
      smx-config/SetCustomSensors.xaml.cs
  84. 1210
      smx-config/Widgets.cs
  85. 1
      smx-config/installer/.gitignore
  86. 168
      smx-config/installer/SMX.nsi
  87. 3
      smx-config/installer/build.bat
  88. BIN
      smx-config/window icon.ico

@ -0,0 +1,8 @@
root=true
[*]
indent_style = space
[*.{cpp,cs,h}]
indent_size = 4

63
.gitattributes vendored

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

366
.gitignore vendored

@ -1,363 +1,5 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
.vs
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
build
obj
out

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2017 Step Revolution LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,7 @@
This is the SDK for StepManiaX platform development.
See [the StepManiaX website](https://stepmaniax.com) and the [documentation](https://steprevolution.github.io/stepmaniax-sdk/)
for info.
SDK support: [sdk@stepmaniax.com](mailto:sdk@stepmaniax.com)

@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2009
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SMX", "sdk\Windows\SMX.vcxproj", "{C5FC0823-9896-4B7C-BFE1-B60DB671A462}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SMXSample", "sample\SMXSample.vcxproj", "{8861D665-FD49-4EFD-92C3-F4B8548AFD23}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMXConfig", "smx-config\SMXConfig.csproj", "{B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C5FC0823-9896-4B7C-BFE1-B60DB671A462}.Debug|x86.ActiveCfg = Debug|Win32
{C5FC0823-9896-4B7C-BFE1-B60DB671A462}.Debug|x86.Build.0 = Debug|Win32
{C5FC0823-9896-4B7C-BFE1-B60DB671A462}.Release|x86.ActiveCfg = Release|Win32
{C5FC0823-9896-4B7C-BFE1-B60DB671A462}.Release|x86.Build.0 = Release|Win32
{8861D665-FD49-4EFD-92C3-F4B8548AFD23}.Debug|x86.ActiveCfg = Debug|Win32
{8861D665-FD49-4EFD-92C3-F4B8548AFD23}.Debug|x86.Build.0 = Debug|Win32
{8861D665-FD49-4EFD-92C3-F4B8548AFD23}.Release|x86.ActiveCfg = Release|Win32
{8861D665-FD49-4EFD-92C3-F4B8548AFD23}.Release|x86.Build.0 = Release|Win32
{B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}.Debug|x86.ActiveCfg = Debug|x86
{B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}.Debug|x86.Build.0 = Debug|x86
{B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}.Release|x86.ActiveCfg = Release|x86
{B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8DAB67F2-430F-43CC-853E-03B5A24F9806}
EndGlobalSection
EndGlobal

@ -1,31 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33516.290
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SMX_PGE", "SMX_PGE\SMX_PGE.vcxproj", "{BDA52482-FD7A-4738-BD12-6D9431BD690C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BDA52482-FD7A-4738-BD12-6D9431BD690C}.Debug|x64.ActiveCfg = Debug|x64
{BDA52482-FD7A-4738-BD12-6D9431BD690C}.Debug|x64.Build.0 = Debug|x64
{BDA52482-FD7A-4738-BD12-6D9431BD690C}.Debug|x86.ActiveCfg = Debug|Win32
{BDA52482-FD7A-4738-BD12-6D9431BD690C}.Debug|x86.Build.0 = Debug|Win32
{BDA52482-FD7A-4738-BD12-6D9431BD690C}.Release|x64.ActiveCfg = Release|x64
{BDA52482-FD7A-4738-BD12-6D9431BD690C}.Release|x64.Build.0 = Release|x64
{BDA52482-FD7A-4738-BD12-6D9431BD690C}.Release|x86.ActiveCfg = Release|Win32
{BDA52482-FD7A-4738-BD12-6D9431BD690C}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5C403CF6-E9B4-4AA3-831E-272D8BB8C33B}
EndGlobalSection
EndGlobal

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

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.

Binary file not shown.

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;
}

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
@ -9,21 +9,13 @@
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{8861D665-FD49-4EFD-92C3-F4B8548AFD23}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{bda52482-fd7a-4738-bd12-6d9431bd690c}</ProjectGuid>
<RootNamespace>SMXPGE</RootNamespace>
<RootNamespace>SMXSample</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>SMXSample</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
@ -36,20 +28,7 @@
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<WholeProgramOptimization>false</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
@ -63,74 +42,65 @@
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
<OutDir>$(TargetDir)../out/</OutDir>
<IntDir>$(SolutionDir)/build/$(ProjectName)/$(Configuration)/</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(TargetDir)../out/</OutDir>
<IntDir>$(SolutionDir)/build/$(ProjectName)/$(Configuration)/</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level4</WarningLevel>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<DisableSpecificWarnings>4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<AdditionalIncludeDirectories>..\sdk</AdditionalIncludeDirectories>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(SolutionDir)/out/$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<PrecompiledHeader>
</PrecompiledHeader>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<DisableSpecificWarnings>4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<AdditionalIncludeDirectories>..\sdk</AdditionalIncludeDirectories>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(SolutionDir)/out/$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="olcPixelGameEngine.h" />
<ClCompile Include="SMXSample.cpp" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="SMX_PGE.cpp" />
<ProjectReference Include="..\sdk\Windows\SMX.vcxproj">
<Project>{c5fc0823-9896-4b7c-bfe1-b60db671a462}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ClInclude Include="olcPixelGameEngine.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">

@ -3,25 +3,20 @@
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
<UniqueIdentifier>{4453342b-4238-49ad-8412-9602572b054a}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="SMXSample.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="olcPixelGameEngine.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="SMX_PGE.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

@ -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,151 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\SMX.h" />
<ClInclude Include="Helpers.h" />
<ClInclude Include="SMXBuildVersion.h" />
<ClInclude Include="SMXConfigPacket.h" />
<ClInclude Include="SMXDevice.h" />
<ClInclude Include="SMXDeviceConnection.h" />
<ClInclude Include="SMXDeviceSearch.h" />
<ClInclude Include="SMXDeviceSearchThreaded.h" />
<ClInclude Include="SMXGif.h" />
<ClInclude Include="SMXHelperThread.h" />
<ClInclude Include="SMXManager.h" />
<ClInclude Include="SMXThread.h" />
<ClInclude Include="SMXPanelAnimation.h" />
<ClInclude Include="SMXPanelAnimationUpload.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Helpers.cpp" />
<ClCompile Include="SMX.cpp" />
<ClCompile Include="SMXConfigPacket.cpp" />
<ClCompile Include="SMXDevice.cpp" />
<ClCompile Include="SMXDeviceConnection.cpp" />
<ClCompile Include="SMXDeviceSearch.cpp" />
<ClCompile Include="SMXDeviceSearchThreaded.cpp" />
<ClCompile Include="SMXGif.cpp" />
<ClCompile Include="SMXHelperThread.cpp" />
<ClCompile Include="SMXManager.cpp" />
<ClCompile Include="SMXThread.cpp" />
<ClCompile Include="SMXPanelAnimation.cpp" />
<ClCompile Include="SMXPanelAnimationUpload.cpp" />
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{C5FC0823-9896-4B7C-BFE1-B60DB671A462}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>SMX</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>false</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(SolutionDir)out\</OutDir>
<IntDir>$(SolutionDir)/build/$(ProjectName)/$(Configuration)/</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(SolutionDir)out\</OutDir>
<IntDir>$(SolutionDir)/build/$(ProjectName)/$(Configuration)/</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level4</WarningLevel>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;SMX_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MinimalRebuild>false</MinimalRebuild>
<DisableSpecificWarnings>4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<RandomizedBaseAddress>false</RandomizedBaseAddress>
<AdditionalDependencies>hid.lib;setupapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<OutputFile>$(SolutionDir)/out/$(TargetName)$(TargetExt)</OutputFile>
</Link>
<PreBuildEvent>
<Command>$(SolutionDir)sdk\Windows\update-build-version.bat
</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<PrecompiledHeader>
</PrecompiledHeader>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;SMX_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<WholeProgramOptimization>false</WholeProgramOptimization>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<DisableSpecificWarnings>4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings)</DisableSpecificWarnings>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<RandomizedBaseAddress>false</RandomizedBaseAddress>
<AdditionalDependencies>hid.lib;setupapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<OutputFile>$(SolutionDir)/out/$(TargetName)$(TargetExt)</OutputFile>
</Link>
<CustomBuildStep>
<Command>
</Command>
</CustomBuildStep>
<CustomBuildStep>
<Message>
</Message>
</CustomBuildStep>
<CustomBuildStep>
<Inputs>
</Inputs>
</CustomBuildStep>
<PreBuildEvent>
<Command>$(SolutionDir)sdk\Windows\update-build-version.bat
</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\SMX.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXDevice.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXDeviceConnection.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXDeviceSearch.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXDeviceSearchThreaded.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXHelperThread.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXManager.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="Helpers.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXBuildVersion.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXThread.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXGif.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXPanelAnimation.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXPanelAnimationUpload.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="SMXConfigPacket.h">
<Filter>Source Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="SMX.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXDevice.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXDeviceConnection.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXDeviceSearch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXDeviceSearchThreaded.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXHelperThread.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Helpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXThread.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXGif.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXPanelAnimation.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXPanelAnimationUpload.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SMXConfigPacket.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

@ -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,30 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="smx_config.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
</sectionGroup>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
<userSettings>
<smx_config.Properties.Settings>
<setting name="LaunchOnStartup" serializeAs="String">
<value>False</value>
</setting>
<setting name="CustomSensors" serializeAs="String">
<value />
</setting>
<setting name="UseInnerSensorThresholds" serializeAs="String">
<value>False</value>
</setting>
<setting name="UseOuterSensorThresholds" serializeAs="String">
<value>False</value>
</setting>
<setting name="AdvancedMode" serializeAs="String">
<value>False</value>
</setting>
</smx_config.Properties.Settings>
</userSettings>
</configuration>

@ -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);
}
};
}

File diff suppressed because it is too large Load Diff

@ -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();
}
}
}

@ -0,0 +1,55 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("smx-config")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("smx-config")]
[assembly: AssemblyCopyright("© 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> to en-US. Then uncomment
//the NeutralResourceLanguage attribute below. Update the "en-US" in
//the line below to match the UICulture setting in the project file.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace smx_config.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("smx_config.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

@ -0,0 +1,86 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace smx_config.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool LaunchOnStartup {
get {
return ((bool)(this["LaunchOnStartup"]));
}
set {
this["LaunchOnStartup"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
public string CustomSensors {
get {
return ((string)(this["CustomSensors"]));
}
set {
this["CustomSensors"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool UseInnerSensorThresholds {
get {
return ((bool)(this["UseInnerSensorThresholds"]));
}
set {
this["UseInnerSensorThresholds"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool UseOuterSensorThresholds {
get {
return ((bool)(this["UseOuterSensorThresholds"]));
}
set {
this["UseOuterSensorThresholds"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool AdvancedMode {
get {
return ((bool)(this["AdvancedMode"]));
}
set {
this["AdvancedMode"] = value;
}
}
}
}

@ -0,0 +1,21 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="smx_config.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="LaunchOnStartup" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="CustomSensors" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="UseInnerSensorThresholds" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="UseOuterSensorThresholds" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="AdvancedMode" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
</Settings>
</SettingsFile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

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,222 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}</ProjectGuid>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>smx_config</RootNamespace>
<AssemblyName>SMXConfig</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>$(SolutionDir)\out\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>..\out\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<LangVersion>default</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>window icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="ConfigPresets.cs" />
<Compile Include="CurrentSMXDevice.cs" />
<Compile Include="DiagnosticsWidgets.cs" />
<Compile Include="DoubleSlider.cs" />
<Compile Include="Helpers.cs" />
<Compile Include="SetCustomSensors.xaml.cs">
<DependentUpon>SetCustomSensors.xaml</DependentUpon>
</Compile>
<Compile Include="ProgressWindow.xaml.cs">
<DependentUpon>ProgressWindow.xaml</DependentUpon>
</Compile>
<Compile Include="SMXJSON.cs" />
<Compile Include="SMX.cs" />
<Compile Include="Widgets.cs" />
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="SetCustomSensors.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="ProgressWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<AppDesigner Include="Properties\" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.5.2">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.5.2 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\window icon.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\DIP.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="window icon.ico" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\sdk\Windows\SMX.vcxproj">
<Project>{c5fc0823-9896-4b7c-bfe1-b60db671a462}</Project>
<Name>SMX</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\pressed.gif" />
<Resource Include="Resources\released.gif" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\window icon grey.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\window icon.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\DIP labels.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\sensor_down.png" />
<Resource Include="Resources\sensor_left.png" />
<Resource Include="Resources\sensor_right.png" />
<Resource Include="Resources\sensor_up.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\threshold_warning.png" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

@ -0,0 +1,535 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
// All C# JSON implementations are either pretty awful or incredibly bloated, so we
// just use our own.
namespace SMXJSON
{
public class JSONError: Exception
{
public JSONError(string error):
base(error)
{
}
};
public class ParseError: JSONError
{
public ParseError(StringReader reader, string error):
base(error)
{
}
};
public static class ObjectListExtensions
{
public static T Get<T>(this List<object> array, int idx, T defaultValue)
{
if(idx < 0 || idx >= array.Count)
return defaultValue;
object value = array[idx];
if(!typeof(T).IsAssignableFrom(value.GetType()))
return defaultValue;
return (T) value;
}
// Our numbers are always doubles. Add some other basic data types for convenience.
public static int Get(this List<object> array, int idx, int defaultValue)
{
return (int) array.Get(idx, (double) defaultValue);
}
public static Byte Get(this List<object> array, int idx, Byte defaultValue)
{
return (Byte) array.Get(idx, (double) defaultValue);
}
public static float Get(this List<object> array, int idx, float defaultValue)
{
return (float) array.Get(idx, (double) defaultValue);
}
// Return the value of key. If it doesn't exist, or doesn't have the expected
// type, return defaultValue.
public static T Get<T>(this Dictionary<string, Object> dict, string key, T defaultValue)
{
object value;
if(!dict.TryGetValue(key, out value))
return defaultValue;
if(!typeof(T).IsAssignableFrom(value.GetType()))
return defaultValue;
return (T) value;
}
// Set result to the value of key if it exists and has the correct type, and return
// true. Otherwise, leave result unchanged and return false.
public static bool GetValue<T>(this Dictionary<string, Object> dict, string key, ref T result)
{
object value;
if(!dict.TryGetValue(key, out value))
return false;
if(!typeof(T).IsAssignableFrom(result.GetType()))
return false;
result = (T) value;
return true;
}
// Our numbers are always doubles. Add some other basic data types for convenience.
public static int Get(this Dictionary<string, Object> dict, string key, int defaultValue)
{
return (int) dict.Get(key, (double) defaultValue);
}
public static Byte Get(this Dictionary<string, Object> dict, string key, Byte defaultValue)
{
return (Byte) dict.Get(key, (double) defaultValue);
}
public static float Get(this Dictionary<string, Object> dict, string key, float defaultValue)
{
return (float) dict.Get(key, (double) defaultValue);
}
}
class SerializeJSON
{
static public string Serialize(object obj)
{
StringBuilder output = new StringBuilder();
Serialize(obj, output, 0);
output.Append('\n');
return output.ToString();
}
// Add start-of-line indentation.
static private void AddIndent(StringBuilder output, int indent)
{
output.Append(' ', indent*4);
}
// Serialize a boolean.
static private void SerializeObject(bool value, StringBuilder output)
{
if(value)
output.Append("true");
else
output.Append("false");
}
// Serialize a string.
static private void SerializeObject(String str, StringBuilder output)
{
output.Append('"');
foreach(char c in str)
{
switch(c)
{
case '"': output.Append("\\\""); break;
case '\\': output.Append("\\\\"); break;
case '\b': output.Append("\\b"); break;
case '\n': output.Append("\\n"); break;
case '\r': output.Append("\\r"); break;
case '\t': output.Append("\\t"); break;
default:
// We don't escape Unicode. Every sane JSON parser accepts UTF-8.
output.Append(c);
break;
}
}
output.Append('"');
}
// Serialize an array.
static private void SerializeObject<T>(List<T> array, StringBuilder output, int indent)
{
output.Append("[\n");
bool first = true;
indent += 1;
foreach(T element in array)
{
if(first)
first = false;
else
output.Append(",\n");
AddIndent(output, indent);
Serialize(element, output, indent);
}
output.Append('\n');
indent -= 1;
AddIndent(output, indent);
output.Append(']');
}
// Serialize a dictionary.
static private void SerializeObject<T>(Dictionary<string, T> dict, StringBuilder output, int indent)
{
output.Append("{\n");
indent += 1;
bool first = true;
foreach(KeyValuePair<string,T> element in dict)
{
if(first)
first = false;
else
output.Append(",\n");
AddIndent(output, indent);
SerializeObject(element.Key, output);
output.Append(':');
output.Append(' ');
Serialize(element.Value, output, indent);
}
output.Append('\n');
indent -= 1;
AddIndent(output, indent);
output.Append('}');
}
// Serialize an object based on its type.
static public void Serialize(object obj, StringBuilder output, int indent)
{
if(obj == null) { output.Append("null"); return; }
if(typeof(Int32).IsInstanceOfType(obj)) { output.Append(obj.ToString()); return; }
if(typeof(float).IsInstanceOfType(obj)) { output.Append(obj.ToString()); return; }
if(typeof(Double).IsInstanceOfType(obj)) { output.Append(obj.ToString()); return; }
if(typeof(Boolean).IsInstanceOfType(obj)) { SerializeObject((Boolean) obj, output); return; }
if(typeof(string).IsInstanceOfType(obj)) { SerializeObject((string) obj, output); return; }
// C# generics aren't very well designed, so this is clunky. We should be able to cast
// a List<string> to List<object>, but some overzealous language designers thought that
// since that has some unsafe uses, we shouldn't be allowed to use it for perfectly safe
// uses either (eg. read-only access).
if(obj.GetType().GetGenericTypeDefinition().IsAssignableFrom(typeof(List<>)))
{
Type valueType = obj.GetType().GetGenericArguments()[0];
if(valueType == typeof(object)) { SerializeObject((List<object>)obj, output, indent); return; }
if(valueType == typeof(Int32)) { SerializeObject((List<Int32>)obj, output, indent); return; }
if(valueType == typeof(float)) { SerializeObject((List<float>)obj, output, indent); return; }
if(valueType == typeof(Double)) { SerializeObject((List<Double>)obj, output, indent); return; }
if(valueType == typeof(Boolean)) { SerializeObject((List<Boolean>)obj, output, indent); return; }
if(valueType == typeof(string)) { SerializeObject((List<string>)obj, output, indent); return; }
}
if(obj.GetType().GetGenericTypeDefinition().IsAssignableFrom(typeof(Dictionary<,>)))
{
Type keyType = obj.GetType().GetGenericArguments()[0];
if(typeof(string).IsAssignableFrom(keyType))
{
Type valueType = obj.GetType().GetGenericArguments()[1];
if(valueType == typeof(object)) { SerializeObject((Dictionary<string, object>)obj, output, indent); return; }
if(valueType == typeof(Int32)) { SerializeObject((Dictionary<string, Int32>)obj, output, indent); return; }
if(valueType == typeof(float)) { SerializeObject((Dictionary<string, float>)obj, output, indent); return; }
if(valueType == typeof(Double)) { SerializeObject((Dictionary<string, Double>)obj, output, indent); return; }
if(valueType == typeof(Boolean)) { SerializeObject((Dictionary<string, Boolean>)obj, output, indent); return; }
if(valueType == typeof(string)) { SerializeObject((Dictionary<string, string>)obj, output, indent); return; }
}
}
throw new JSONError("Unsupported type: " + obj.GetType());
}
}
class ParseJSON
{
static private void SkipWhitespace(StringReader reader)
{
while(true)
{
int c = reader.Peek();
switch(c)
{
case ' ':
case '\n':
case '\t':
reader.Read();
continue;
default:
return;
}
}
}
// Parse JSON. On error, return null.
public static Object Parse(string json)
{
return ParseWithExceptions(new StringReader(json));
}
// Parse JSON, expecting a specific outer type. On parse error, return a default value.
public static T Parse<T>(string json) where T: new()
{
Object result = Parse(json);
if(!typeof(T).IsAssignableFrom(result.GetType()))
return new T();
return (T) result;
}
// Parse JSON. On error, raise JSONError.
//
// Most of the time, errors aren't expected and putting exception handling around every
// place JSON is parsed can be brittle. Parse() can be used instead to just return
// a default value.
public static Object ParseWithExceptions(StringReader reader)
{
Object result = ParseJSONValue(reader);
SkipWhitespace(reader);
// Other than whitespace, we should be at the end of the file.
if(reader.Read() != -1)
throw new ParseError(reader, "Unexpected data at the end of the string");
return result;
}
private static Object ParseJSONValue(StringReader reader)
{
SkipWhitespace(reader);
int nextCharacter = reader.Peek();
switch(nextCharacter)
{
case '"':
return ReadJSONString(reader);
case '{':
return ReadJSONDictionary(reader);
case '[':
return ReadJSONArray(reader);
}
if(nextCharacter == '-' || (nextCharacter >= '0' && nextCharacter <= '9'))
return ReadJSONNumber(reader);
if(reader.Peek() == 'n')
{
// The only valid value this can be is "null".
Expect(reader, 'n');
Expect(reader, 'u');
Expect(reader, 'l');
Expect(reader, 'l');
return null;
}
if(reader.Peek() == 't')
{
Expect(reader, 't');
Expect(reader, 'r');
Expect(reader, 'u');
Expect(reader, 'e');
return true;
}
if(reader.Peek() == 'f')
{
Expect(reader, 'f');
Expect(reader, 'a');
Expect(reader, 'l');
Expect(reader, 's');
Expect(reader, 'e');
return true;
}
throw new ParseError(reader, "Unexpected token");
}
// Skip whitespace, then read one character, which we expect to have a specific value.
static private void Expect(StringReader reader, char character)
{
SkipWhitespace(reader);
if(reader.Read() != character)
throw new ParseError(reader, "Expected " + character);
}
static private List<Object> ReadJSONArray(StringReader reader)
{
Expect(reader, '[');
List<Object> result = new List<Object>();
while(true)
{
SkipWhitespace(reader);
if(reader.Peek() == ']')
{
reader.Read();
return result;
}
if(result.Count > 0)
{
int comma = reader.Read();
if(comma == -1)
throw new ParseError(reader, "Unexpected EOF reading array");
if(comma != ',')
throw new ParseError(reader, "Expected ',', got " + comma + " reading array");
SkipWhitespace(reader);
}
Object value = ParseJSONValue(reader);
result.Add(value);
}
}
static private Dictionary<string, Object> ReadJSONDictionary(StringReader reader)
{
Expect(reader, '{');
Dictionary<string, Object> result = new Dictionary<string, Object>();
while(true)
{
string key = ReadJSONString(reader);
Expect(reader, ':');
Object value = ParseJSONValue(reader);
result.Add(key, value);
SkipWhitespace(reader);
switch(reader.Read())
{
case '}':
return result;
case ',':
continue;
case -1:
throw new ParseError(reader, "Unexpected EOF reading dictionary");
default:
throw new ParseError(reader, "Unexpected token reading dictionary");
}
}
}
static private string ReadJSONString(StringReader reader)
{
Expect(reader, '"');
StringBuilder result = new StringBuilder();
while(true)
{
int c = reader.Read();
if(c == -1)
throw new ParseError(reader, "Unexpected EOF reading string");
if(c == '"')
break;
// XXX: untested
if(c == '\\')
{
c = reader.Read();
switch(c)
{
case '"':
case '\\':
case '/':
result.Append((char) c);
break;
case 'b': result.Append('\b'); break;
case 'n': result.Append('\n'); break;
case 'r': result.Append('\r'); break;
case 't': result.Append('\t'); break;
case 'u':
{
// Parse a \u1234 escape.
int codePoint = 0;
for(int i = 0; i < 4; ++i)
{
codePoint *= 10;
c = reader.Read();
if(c == -1)
throw new ParseError(reader, "Unexpected EOF reading string");
if(c < '0' || c > '9')
throw new ParseError(reader, "Unexpected token " + c + " reading Unicode escape");
codePoint += c - (int) '0';
}
result.Append((char) codePoint);
break;
}
default:
throw new ParseError(reader, "Unrecognized escape sequence in string: \\" + (char) c);
}
continue;
}
result.Append((char) c);
}
return result.ToString();
}
static private double ReadJSONNumber(StringReader reader)
{
StringBuilder number = new StringBuilder();
bool negative = false;
if(reader.Peek() == '-')
{
negative = true;
reader.Read();
}
int nextCharacter = reader.Read();
if(nextCharacter == '0')
number.Append((char) nextCharacter);
else
{
if(nextCharacter < '1' || nextCharacter > '9')
throw new ParseError(reader, "Unexpected token reading number");
number.Append((char) nextCharacter);
while(reader.Peek() >= '0' && reader.Peek() <= '9')
number.Append((char) reader.Read());
}
if(reader.Peek() == '.')
{
number.Append(reader.Read());
if(reader.Peek() < '0' || reader.Peek() > '9')
throw new ParseError(reader, "Unexpected token reading number");
while(reader.Peek() >= '0' && reader.Peek() <= '9')
number.Append((char) reader.Read());
}
if(reader.Peek() == 'e' || reader.Peek() == 'E')
{
number.Append((char) reader.Read());
if(reader.Peek() == '+' || reader.Peek() == '-')
number.Append((char) reader.Read());
if(reader.Peek() < '0' || reader.Peek() > '9')
throw new ParseError(reader, "Unexpected token reading number");
while(true)
{
nextCharacter = reader.Read();
if(nextCharacter < '0' || nextCharacter > '9')
break;
number.Append((char) nextCharacter);
}
}
double result;
if(!Double.TryParse(number.ToString(), out result))
throw new ParseError(reader, "Unexpected error parsing number \"" + number.ToString() + "\"");
if(negative)
result = -result;
return result;
}
}
}

@ -0,0 +1,101 @@
<Window x:Class="smx_config.SetCustomSensors"
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="422" Width="325" ResizeMode="NoResize"
xmlns:controls="clr-namespace:smx_config"
x:Name="root"
Title="Select Custom Sensors"
UseLayoutRounding="True">
<Window.Resources>
<Style x:Key="SensorSelectionButton" TargetType="{x:Type ToggleButton}">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Background" Value="#FFDDDDDD"/>
<Setter Property="BorderBrush" Value="#FF707070"/>
<Setter Property="Foreground" Value="#FFFFFF"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:SensorSelectionButton}">
<Border x:Name="border" BorderBrush="#000000" BorderThickness="1" SnapsToDevicePixels="true">
<ContentPresenter
SnapsToDevicePixels="true"
Focusable="False"
HorizontalAlignment="Center" VerticalAlignment="Center"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="False">
<Setter Property="Background" TargetName="border" Value="#FF2020A0"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" TargetName="border" Value="#FF00F000"/>
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter Property="Background" TargetName="border" Value="#FFC4E5F6"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF2C628B"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type controls:SensorSelector}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:SensorSelector}">
<Border Padding="1">
<Grid Width="70" Height="70">
<controls:SensorSelectionButton x:Name="Sensor0" Style="{StaticResource SensorSelectionButton}"
Width="13" Height="40" HorizontalAlignment="Left" />
<controls:SensorSelectionButton x:Name="Sensor1" Style="{StaticResource SensorSelectionButton}"
Width="13" Height="40" HorizontalAlignment="Right" />
<controls:SensorSelectionButton x:Name="Sensor2" Style="{StaticResource SensorSelectionButton}"
Width="40" Height="13" VerticalAlignment="Top" />
<controls:SensorSelectionButton x:Name="Sensor3" Style="{StaticResource SensorSelectionButton}"
Width="40" Height="13" VerticalAlignment="Bottom" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel Background="#FFE5E5E5">
<TextBlock xml:space="preserve" HorizontalAlignment="Center" Margin="0,15,0,10" TextAlignment="Center" FontSize="16">Select which sensors are controlled
by the custom threshold.</TextBlock>
<Grid Width="250" Height="250" Background="#FFAAAAAA" Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<controls:SensorSelector Grid.Row="0" Grid.Column="0" Panel="0" Focusable="false" />
<controls:SensorSelector Grid.Row="0" Grid.Column="1" Panel="1" Focusable="false" />
<controls:SensorSelector Grid.Row="0" Grid.Column="2" Panel="2" Focusable="false" />
<controls:SensorSelector Grid.Row="1" Grid.Column="0" Panel="3" Focusable="false" />
<controls:SensorSelector Grid.Row="1" Grid.Column="1" Panel="4" Focusable="false" />
<controls:SensorSelector Grid.Row="1" Grid.Column="2" Panel="5" Focusable="false" />
<controls:SensorSelector Grid.Row="2" Grid.Column="0" Panel="6" Focusable="false" />
<controls:SensorSelector Grid.Row="2" Grid.Column="1" Panel="7" Focusable="false" />
<controls:SensorSelector Grid.Row="2" Grid.Column="2" Panel="8" Focusable="false" />
</Grid>
<Button x:Name="OK" Content="OK" IsDefault="True" Click="OK_Click" HorizontalAlignment="Center" FontSize="20" Padding="10,4" Margin="0,10,0,0" />
</StackPanel>
</Window>

@ -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();
}
}
}

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Loading…
Cancel
Save