();
121 | switch(fromPanel)
122 | {
123 | case 7: // down (cardinal)
124 | result.Add(3); // left
125 | result.Add(5); // right
126 | break;
127 | case 2: // up-right (corners)
128 | result.Add(0); // up-left
129 | result.Add(6); // down-left
130 | result.Add(8); // down-right
131 | break;
132 | }
133 | return result;
134 | }
135 |
136 | // The simplified configuration scheme sets thresholds for up, center, cardinal directions
137 | // and corners. Rev1 firmware uses those only. Copy cardinal directions (down) to the
138 | // other cardinal directions (except for up, which already had its own setting) and corners
139 | // to the other corners.
140 | static private void SyncUnifiedThresholds(ref SMX.SMXConfig config)
141 | {
142 | for(int fromPanel = 0; fromPanel < 9; ++fromPanel)
143 | {
144 | foreach(int toPanel in GetPanelsToSyncUnifiedThresholds(fromPanel))
145 | {
146 | config.panelSettings[toPanel].loadCellLowThreshold = config.panelSettings[fromPanel].loadCellLowThreshold;
147 | config.panelSettings[toPanel].loadCellHighThreshold = config.panelSettings[fromPanel].loadCellHighThreshold;
148 |
149 | // Do the same for FSR thresholds.
150 | for(int sensor = 0; sensor < 4; ++sensor)
151 | {
152 | config.panelSettings[toPanel].fsrLowThreshold[sensor] = config.panelSettings[fromPanel].fsrLowThreshold[sensor];
153 | config.panelSettings[toPanel].fsrHighThreshold[sensor] = config.panelSettings[fromPanel].fsrHighThreshold[sensor];
154 | }
155 | }
156 | }
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Introduction to the StepManiaX SDK
8 |
9 | The StepManiaX SDK supports C++ development for the StepManiaX dance platform.
10 |
11 | SDK support: sdk@stepmaniax.com
12 |
13 |
Usage
14 |
15 | You can either build the solution and link the resulting SMX.dll to your application,
16 | or import the source project and add it to your Visual Studio solution. The SDK
17 | interface is SMX.h.
18 |
19 | See SMXSample for a sample application.
20 |
21 | Up to two controllers are supported. SMX_GetInfo can be used to check which
22 | controllers are connected. Each pad argument to API calls can be 0 for the
23 | player 1 pad, or 1 for the player 2 pad.
24 |
25 |
HID support
26 |
27 | The platform can be used as a regular USB HID input device, which works in any game
28 | that supports input remapping.
29 |
30 | However, applications using this SDK to control the panels directly should ignore the
31 | HID interface, and instead use SMX_GetInputState to retrieve the input state.
32 |
33 |
Platform lights
34 |
35 | The platform can have up to nine panels. Each panel has a grid of 4x4 RGB LEDs, which can
36 | be individually controlled at up to 30 FPS.
37 |
38 | See SMX_SetLights2.
39 |
40 |
Update notes
41 |
42 | 2019-07-18-01: Added SMX_SetLights2. This is the same as SMX_SetLights, with an added
43 | parameter to specify the size of the buffer. This must be used to control the Gen4
44 | pads which have additional LEDs.
45 |
46 | Platform configuration
47 |
48 | The platform configuration can be read and modified using SMX_GetConfig and SMX_SetConfig.
49 | Most of the platform configuration doesn't need to be accessed by applications, since
50 | users can use the SMXConfig application to manage their platform.
51 |
52 |
53 | -
54 | enabledSensors
55 |
56 | Each platform can have up to nine panels in any configuration, but most devices have
57 | a smaller number of panels installed. If an application wants to adapt its UI to the
58 | user's panel configuration, see enabledSensors to detect which sensors are enabled.
59 |
60 | Each panel has four sensors, and if a panel is disabled, all four of its sensors will be
61 | disabled. Disabling individual sensors is possible, but removing individual sensors
62 | reduces the performance of the pad and isn't recommended.
63 |
64 | Note that this indicates which panels the player is using for input. Other panels may
65 | still have lights support, and the application should always send lights data for all
66 | possible panels even if it's not being used for input.
67 |
68 |
69 |
70 | Reference
71 |
72 | void SMX_Start(SMXUpdateCallback UpdateCallback, void *pUser);
73 |
74 | Initialize, and start searching for devices.
75 |
76 | UpdateCallback will be called when something happens: connection or disconnection, inputs
77 | changed, configuration updated, test data updated, etc. It doesn't specify what's changed,
78 | and the user should check all state that it's interested in.
79 |
80 | This is called asynchronously from a helper thread, so the receiver must be thread-safe.
81 |
82 |
void SMX_Stop();
83 |
84 | Shut down and disconnect from all devices. This will wait for any user callbacks to complete,
85 | and no user callbacks will be called after this returns.
86 |
87 | void SMX_SetLogCallback(SMXLogCallback callback);
88 |
89 | Set a function to receive diagnostic logs. By default, logs are written to stdout.
90 | This can be called before SMX_Start, so it affects any logs sent during initialization.
91 |
92 | void SMX_GetInfo(int pad, SMXInfo *info);
93 |
94 | Get info about a pad. Use this to detect which pads are currently connected.
95 |
96 | uint16_t SMX_GetInputState(int pad);
97 |
98 | Get a mask of the currently pressed panels.
99 |
100 | void SMX_SetLights(const char lightsData[864]);
101 |
102 | (deprecated)
103 |
104 | Equivalent to SMX_SetLights2(lightsData, 864). SMX_SetLights2 should be used instead.
105 |
106 |
void SMX_SetLights2(const char *lightsData, int lightDataSize);
107 | Update the lights. Both pads are always updated together. lightsData is a list of 8-bit RGB
108 | colors, one for each LED.
109 |
110 | lightDataSize is the number of bytes in lightsData. This should be 1350 (2 pads * 9 panels *
111 | 25 lights * 3 RGB colors). For backwards-compatibility, this can also be 864.
112 |
113 | 25-LED panels have lights in the following order:
114 |
115 |
116 | 00 01 02 03
117 | 16 17 18
118 | 04 05 06 07
119 | 19 20 21
120 | 08 09 10 11
121 | 22 23 24
122 | 12 13 14 15
123 |
124 |
125 |
126 | 16-LED panels have the same layout, ignoring LEDs 16 and up.
127 |
128 | Panels are in the following order:
129 |
130 |
131 | 012 9AB
132 | 345 CDE
133 | 678 F01
134 |
135 |
136 | Lights will update at up to 30 FPS. If lights data is sent more quickly, a best effort will be
137 | made to send the most recent lights data available, but the panels won't update more quickly.
138 |
139 | The panels will return to automatic lighting if no lights are received for a while, so applications
140 | controlling lights should send light updates continually, even if the lights aren't changing.
141 |
142 | For backwards compatibility, if lightDataSize is 864, the old 4x4-only order is used,
143 | which simply omits lights 16-24.
144 |
145 |
void SMX_ReenableAutoLights();
146 |
147 | By default, the panels light automatically when stepped on. If a lights command is sent by
148 | the application, this stops happening to allow the application to fully control lighting.
149 | If no lights update is received for a few seconds, automatic lighting is reenabled by the
150 | panels.
151 |
152 | SMX_ReenableAutoLights can be called to immediately reenable auto-lighting, without waiting
153 | for the timeout period to elapse. Games don't need to call this, since the panels will return
154 | to auto-lighting mode automatically after a brief period of no updates.
155 |
156 |
void SMX_GetConfig(int pad, SMXConfig *config);
157 |
158 | Get the current controller's configuration.
159 |
160 | void SMX_SetConfig(int pad, const SMXConfig *config);
161 |
162 | Update the current controller's configuration. This doesn't block, and the new configuration will
163 | be sent in the background. SMX_GetConfig will return the new configuration as soon as this call
164 | returns, without waiting for it to actually be sent to the controller.
165 |
166 | void SMX_FactoryReset(int pad);
167 |
168 | Reset a pad to its original configuration.
169 |
170 | void SMX_ForceRecalibration(int pad);
171 |
172 | Request an immediate panel recalibration. This is normally not necessary, but can be helpful
173 | for diagnostics.
174 |
175 |
176 | void SMX_SetTestMode(int pad, SensorTestMode mode);
177 |
178 | bool SMX_GetTestData(int pad, SMXSensorTestModeData *data);
179 |
180 |
181 | Set a panel test mode and request test data. This is used by the configuration tool.
182 |
183 |
184 |
--------------------------------------------------------------------------------
/sdk/Windows/SMX.vcxproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | Win32
7 |
8 |
9 | Release
10 | Win32
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {C5FC0823-9896-4B7C-BFE1-B60DB671A462}
46 | Win32Proj
47 | SMX
48 | 10.0
49 |
50 |
51 |
52 | DynamicLibrary
53 | true
54 | v142
55 | Unicode
56 |
57 |
58 | DynamicLibrary
59 | false
60 | v142
61 | false
62 | Unicode
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | false
78 | $(SolutionDir)out\
79 | $(SolutionDir)/build/$(ProjectName)/$(Configuration)/
80 |
81 |
82 | false
83 | $(SolutionDir)out\
84 | $(SolutionDir)/build/$(ProjectName)/$(Configuration)/
85 |
86 |
87 |
88 |
89 |
90 | Level4
91 | Disabled
92 | _DEBUG;_WINDOWS;_USRDLL;SMX_EXPORTS;%(PreprocessorDefinitions)
93 | false
94 | 4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings)
95 | ProgramDatabase
96 |
97 |
98 | Windows
99 | true
100 | false
101 | 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)
102 | $(SolutionDir)/out/$(TargetName)$(TargetExt)
103 |
104 |
105 | $(SolutionDir)sdk\Windows\update-build-version.bat
106 |
107 |
108 |
109 |
110 |
111 | Level4
112 |
113 |
114 | MaxSpeed
115 | true
116 | true
117 | NDEBUG;_WINDOWS;_USRDLL;SMX_EXPORTS;%(PreprocessorDefinitions)
118 | false
119 | true
120 | 4063;4100;4127;4201;4244;4275;4355;4505;4512;4702;4786;4996;4996;4005;4018;4389;4389;4800;4592;%(DisableSpecificWarnings)
121 |
122 |
123 | Windows
124 | true
125 | true
126 | true
127 | false
128 | 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)
129 | $(SolutionDir)/out/$(TargetName)$(TargetExt)
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | $(SolutionDir)sdk\Windows\update-build-version.bat
145 |
146 |
147 |
148 |
149 |
150 |
151 |
--------------------------------------------------------------------------------
/sdk/Windows/SMXConfigPacket.cpp:
--------------------------------------------------------------------------------
1 | #include "SMXConfigPacket.h"
2 | #include
3 | #include
4 |
5 | // The config packet format changed in version 5. This handles compatibility with
6 | // the old configuration packet. The config packet in SMX.h matches the new format.
7 | //
8 |
9 |
10 | #pragma pack(push, 1)
11 | struct OldSMXConfig
12 | {
13 | uint8_t unused1 = 0xFF, unused2 = 0xFF;
14 | uint8_t unused3 = 0xFF, unused4 = 0xFF;
15 | uint8_t unused5 = 0xFF, unused6 = 0xFF;
16 |
17 | uint16_t masterDebounceMilliseconds = 0;
18 | uint8_t panelThreshold7Low = 0xFF, panelThreshold7High = 0xFF; // was "cardinal"
19 | uint8_t panelThreshold4Low = 0xFF, panelThreshold4High = 0xFF; // was "center"
20 | uint8_t panelThreshold2Low = 0xFF, panelThreshold2High = 0xFF; // was "corner"
21 |
22 | uint16_t panelDebounceMicroseconds = 4000;
23 | uint16_t autoCalibrationPeriodMilliseconds = 1000;
24 | uint8_t autoCalibrationMaxDeviation = 100;
25 | uint8_t badSensorMinimumDelaySeconds = 15;
26 | uint16_t autoCalibrationAveragesPerUpdate = 60;
27 |
28 | uint8_t unused7 = 0xFF, unused8 = 0xFF;
29 |
30 | uint8_t panelThreshold1Low = 0xFF, panelThreshold1High = 0xFF; // was "up"
31 |
32 | uint8_t enabledSensors[5];
33 |
34 | uint8_t autoLightsTimeout = 1000/128; // 1 second
35 |
36 | uint8_t stepColor[3*9];
37 |
38 | uint8_t panelRotation;
39 |
40 | uint16_t autoCalibrationSamplesPerAverage = 500;
41 |
42 | uint8_t masterVersion = 0xFF;
43 | uint8_t configVersion = 0x03;
44 |
45 | uint8_t unused9[10];
46 | uint8_t panelThreshold0Low, panelThreshold0High;
47 | uint8_t panelThreshold3Low, panelThreshold3High;
48 | uint8_t panelThreshold5Low, panelThreshold5High;
49 | uint8_t panelThreshold6Low, panelThreshold6High;
50 | uint8_t panelThreshold8Low, panelThreshold8High;
51 |
52 | uint16_t debounceDelayMilliseconds = 0;
53 |
54 | uint8_t padding[164];
55 | };
56 | #pragma pack(pop)
57 | static_assert(offsetof(OldSMXConfig, padding) == 86, "Incorrect padding alignment");
58 | static_assert(sizeof(OldSMXConfig) == 250, "Expected 250 bytes");
59 |
60 | void ConvertToNewConfig(const vector &oldConfigData, SMXConfig &newConfig)
61 | {
62 | // Copy data in its order within OldSMXConfig. This lets us easily stop at each
63 | // known packet version. Any fields that aren't present in oldConfigData will be
64 | // left at their default values in SMXConfig.
65 | const OldSMXConfig &oldConfig = (OldSMXConfig &) *oldConfigData.data();
66 |
67 | newConfig.debounceNodelayMilliseconds = oldConfig.masterDebounceMilliseconds;
68 |
69 | newConfig.panelSettings[7].loadCellLowThreshold = oldConfig.panelThreshold7Low;
70 | newConfig.panelSettings[4].loadCellLowThreshold = oldConfig.panelThreshold4Low;
71 | newConfig.panelSettings[2].loadCellLowThreshold = oldConfig.panelThreshold2Low;
72 |
73 | newConfig.panelSettings[7].loadCellHighThreshold = oldConfig.panelThreshold7High;
74 | newConfig.panelSettings[4].loadCellHighThreshold = oldConfig.panelThreshold4High;
75 | newConfig.panelSettings[2].loadCellHighThreshold = oldConfig.panelThreshold2High;
76 |
77 | newConfig.panelDebounceMicroseconds = oldConfig.panelDebounceMicroseconds;
78 | newConfig.autoCalibrationMaxDeviation = oldConfig.autoCalibrationMaxDeviation;
79 | newConfig.badSensorMinimumDelaySeconds = oldConfig.badSensorMinimumDelaySeconds;
80 | newConfig.autoCalibrationAveragesPerUpdate = oldConfig.autoCalibrationAveragesPerUpdate;
81 |
82 | newConfig.panelSettings[1].loadCellLowThreshold = oldConfig.panelThreshold1Low;
83 | newConfig.panelSettings[1].loadCellHighThreshold = oldConfig.panelThreshold1High;
84 |
85 | memcpy(newConfig.enabledSensors, oldConfig.enabledSensors, sizeof(newConfig.enabledSensors));
86 | newConfig.autoLightsTimeout = oldConfig.autoLightsTimeout;
87 | memcpy(newConfig.stepColor, oldConfig.stepColor, sizeof(newConfig.stepColor));
88 | newConfig.panelRotation = oldConfig.panelRotation;
89 | newConfig.autoCalibrationSamplesPerAverage = oldConfig.autoCalibrationSamplesPerAverage;
90 |
91 | if(oldConfig.configVersion == 0xFF)
92 | return;
93 |
94 | newConfig.masterVersion = oldConfig.masterVersion;
95 | newConfig.configVersion = oldConfig.configVersion;
96 |
97 | if(oldConfig.configVersion < 2)
98 | return;
99 |
100 | newConfig.panelSettings[0].loadCellLowThreshold = oldConfig.panelThreshold0Low;
101 | newConfig.panelSettings[3].loadCellLowThreshold = oldConfig.panelThreshold3Low;
102 | newConfig.panelSettings[5].loadCellLowThreshold = oldConfig.panelThreshold5Low;
103 | newConfig.panelSettings[6].loadCellLowThreshold = oldConfig.panelThreshold6Low;
104 | newConfig.panelSettings[8].loadCellLowThreshold = oldConfig.panelThreshold8Low;
105 |
106 | newConfig.panelSettings[0].loadCellHighThreshold = oldConfig.panelThreshold0High;
107 | newConfig.panelSettings[3].loadCellHighThreshold = oldConfig.panelThreshold3High;
108 | newConfig.panelSettings[5].loadCellHighThreshold = oldConfig.panelThreshold5High;
109 | newConfig.panelSettings[6].loadCellHighThreshold = oldConfig.panelThreshold6High;
110 | newConfig.panelSettings[8].loadCellHighThreshold = oldConfig.panelThreshold8High;
111 |
112 | if(oldConfig.configVersion < 3)
113 | return;
114 |
115 | newConfig.debounceDelayMilliseconds = oldConfig.debounceDelayMilliseconds;
116 | }
117 |
118 | // oldConfigData contains the data we're replacing. Any fields that exist in the old
119 | // config format and not the new one will be left unchanged.
120 | void ConvertToOldConfig(const SMXConfig &newConfig, vector &oldConfigData)
121 | {
122 | OldSMXConfig &oldConfig = (OldSMXConfig &) *oldConfigData.data();
123 |
124 | // We don't need to check configVersion here. It's safe to set all fields in
125 | // the output config packet. If oldConfigData isn't 128 bytes, extend it.
126 | if(oldConfigData.size() < 128)
127 | oldConfigData.resize(128, 0xFF);
128 |
129 | oldConfig.masterDebounceMilliseconds = newConfig.debounceNodelayMilliseconds;
130 |
131 | oldConfig.panelThreshold7Low = newConfig.panelSettings[7].loadCellLowThreshold;
132 | oldConfig.panelThreshold4Low = newConfig.panelSettings[4].loadCellLowThreshold;
133 | oldConfig.panelThreshold2Low = newConfig.panelSettings[2].loadCellLowThreshold;
134 |
135 | oldConfig.panelThreshold7High = newConfig.panelSettings[7].loadCellHighThreshold;
136 | oldConfig.panelThreshold4High = newConfig.panelSettings[4].loadCellHighThreshold;
137 | oldConfig.panelThreshold2High = newConfig.panelSettings[2].loadCellHighThreshold;
138 |
139 | oldConfig.panelDebounceMicroseconds = newConfig.panelDebounceMicroseconds;
140 | oldConfig.autoCalibrationMaxDeviation = newConfig.autoCalibrationMaxDeviation;
141 | oldConfig.badSensorMinimumDelaySeconds = newConfig.badSensorMinimumDelaySeconds;
142 | oldConfig.autoCalibrationAveragesPerUpdate = newConfig.autoCalibrationAveragesPerUpdate;
143 |
144 | oldConfig.panelThreshold1Low = newConfig.panelSettings[1].loadCellLowThreshold;
145 | oldConfig.panelThreshold1High = newConfig.panelSettings[1].loadCellHighThreshold;
146 |
147 | memcpy(oldConfig.enabledSensors, newConfig.enabledSensors, sizeof(newConfig.enabledSensors));
148 | oldConfig.autoLightsTimeout = newConfig.autoLightsTimeout;
149 | memcpy(oldConfig.stepColor, newConfig.stepColor, sizeof(newConfig.stepColor));
150 | oldConfig.panelRotation = newConfig.panelRotation;
151 | oldConfig.autoCalibrationSamplesPerAverage = newConfig.autoCalibrationSamplesPerAverage;
152 |
153 | oldConfig.masterVersion = newConfig.masterVersion;
154 | oldConfig.configVersion= newConfig.configVersion;
155 |
156 | oldConfig.panelThreshold0Low = newConfig.panelSettings[0].loadCellLowThreshold;
157 | oldConfig.panelThreshold3Low = newConfig.panelSettings[3].loadCellLowThreshold;
158 | oldConfig.panelThreshold5Low = newConfig.panelSettings[5].loadCellLowThreshold;
159 | oldConfig.panelThreshold6Low = newConfig.panelSettings[6].loadCellLowThreshold;
160 | oldConfig.panelThreshold8Low = newConfig.panelSettings[8].loadCellLowThreshold;
161 |
162 | oldConfig.panelThreshold0High = newConfig.panelSettings[0].loadCellHighThreshold;
163 | oldConfig.panelThreshold3High = newConfig.panelSettings[3].loadCellHighThreshold;
164 | oldConfig.panelThreshold5High = newConfig.panelSettings[5].loadCellHighThreshold;
165 | oldConfig.panelThreshold6High = newConfig.panelSettings[6].loadCellHighThreshold;
166 | oldConfig.panelThreshold8High = newConfig.panelSettings[8].loadCellHighThreshold;
167 |
168 | oldConfig.debounceDelayMilliseconds = newConfig.debounceDelayMilliseconds;
169 | }
170 |
--------------------------------------------------------------------------------
/smx-config/SMXConfig.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {B9EFCD31-7ACB-4195-81A8-CEF4EFD16D6E}
8 | WinExe
9 | Properties
10 | smx_config
11 | SMXConfig
12 | v4.5.2
13 | 512
14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 4
16 | true
17 | publish\
18 | true
19 | Disk
20 | false
21 | Foreground
22 | 7
23 | Days
24 | false
25 | false
26 | true
27 | 0
28 | 1.0.0.%2a
29 | false
30 | false
31 | true
32 |
33 |
34 | AnyCPU
35 | true
36 | full
37 | false
38 | bin\Debug\
39 | DEBUG;TRACE
40 | prompt
41 | 4
42 |
43 |
44 | AnyCPU
45 | pdbonly
46 | true
47 | bin\Release\
48 | TRACE
49 | prompt
50 | 4
51 |
52 |
53 | true
54 | $(SolutionDir)\out\
55 | DEBUG;TRACE
56 | full
57 | x86
58 | prompt
59 | MinimumRecommendedRules.ruleset
60 | true
61 | false
62 | true
63 |
64 |
65 | ..\out\
66 | TRACE
67 | true
68 | pdbonly
69 | x86
70 | prompt
71 | MinimumRecommendedRules.ruleset
72 | true
73 | default
74 | true
75 | false
76 |
77 |
78 | window icon.ico
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | 4.0
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | MSBuild:Compile
101 | Designer
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | SetCustomSensors.xaml
110 |
111 |
112 | ProgressWindow.xaml
113 |
114 |
115 |
116 |
117 |
118 | MSBuild:Compile
119 | Designer
120 |
121 |
122 | App.xaml
123 | Code
124 |
125 |
126 | MainWindow.xaml
127 | Code
128 |
129 |
130 | MSBuild:Compile
131 | Designer
132 |
133 |
134 | MSBuild:Compile
135 | Designer
136 |
137 |
138 |
139 |
140 | Code
141 |
142 |
143 | True
144 | True
145 | Resources.resx
146 |
147 |
148 | True
149 | Settings.settings
150 | True
151 |
152 |
153 | ResXFileCodeGenerator
154 | Resources.Designer.cs
155 |
156 |
157 | SettingsSingleFileGenerator
158 | Settings.Designer.cs
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | False
168 | Microsoft .NET Framework 4.5.2 %28x86 and x64%29
169 | true
170 |
171 |
172 | False
173 | .NET Framework 3.5 SP1
174 | false
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | {c5fc0823-9896-4b7c-bfe1-b60db671a462}
189 | SMX
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
222 |
--------------------------------------------------------------------------------
/smx-config/DoubleSlider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using System.Windows;
8 | using System.Windows.Controls;
9 | using System.Windows.Controls.Primitives;
10 | using System.Windows.Input;
11 |
12 | namespace smx_config
13 | {
14 | // A slider with two handles, and a handle connecting them. Dragging the handle drags both
15 | // of the sliders.
16 | class DoubleSlider: Control
17 | {
18 | public delegate void ValueChangedDelegate(DoubleSlider slider);
19 | public event ValueChangedDelegate ValueChanged;
20 | private void FireValueChanged() { ValueChanged?.Invoke(this); }
21 |
22 | // The minimum value for either knob.
23 | public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum",
24 | typeof(double), typeof(DoubleSlider), new FrameworkPropertyMetadata(0.0));
25 |
26 | public double Minimum {
27 | get { return (double) GetValue(MinimumProperty); }
28 | set { SetValue(MinimumProperty, value); }
29 | }
30 |
31 | // The maximum value for either knob.
32 | public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum",
33 | typeof(double), typeof(DoubleSlider), new FrameworkPropertyMetadata(20.0));
34 |
35 | public double Maximum {
36 | get { return (double) GetValue(MaximumProperty); }
37 | set { SetValue(MaximumProperty, value); }
38 | }
39 |
40 | // The minimum distance between the two values.
41 | public static readonly DependencyProperty MinimumDistanceProperty = DependencyProperty.Register("MinimumDistance",
42 | typeof(double), typeof(DoubleSlider), new FrameworkPropertyMetadata(0.0));
43 |
44 | public double MinimumDistance {
45 | get { return (double) GetValue(MinimumDistanceProperty); }
46 | set { SetValue(MinimumDistanceProperty, value); }
47 | }
48 |
49 | // Clamp value between minimum and maximum.
50 | private double CoerceValueToLimits(double value)
51 | {
52 | return Math.Min(Math.Max(value, Minimum), Maximum);
53 | }
54 |
55 | // Note that we only clamp LowerValue and UpperValue to the min/max values. We don't
56 | // clamp them to each other or to MinimumDistance here, since that complicates setting
57 | // properties a lot. We only clamp to those when the user manipulates the control, not
58 | // when we set values directly.
59 | private static object LowerValueCoerceValueCallback(DependencyObject target, object valueObject)
60 | {
61 | DoubleSlider slider = target as DoubleSlider;
62 | double value = (double)valueObject;
63 | value = slider.CoerceValueToLimits(value);
64 | return value;
65 | }
66 |
67 | private static object UpperValueCoerceValueCallback(DependencyObject target, object valueObject)
68 | {
69 | DoubleSlider slider = target as DoubleSlider;
70 | double value = (double)valueObject;
71 | value = slider.CoerceValueToLimits(value);
72 | return value;
73 | }
74 |
75 | public static readonly DependencyProperty LowerValueProperty = DependencyProperty.Register("LowerValue",
76 | typeof(double), typeof(DoubleSlider),
77 | new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsArrange, null, LowerValueCoerceValueCallback));
78 |
79 | public double LowerValue {
80 | get { return (double) GetValue(LowerValueProperty); }
81 | set { SetValue(LowerValueProperty, value); }
82 | }
83 |
84 | public static readonly DependencyProperty UpperValueProperty = DependencyProperty.Register("UpperValue",
85 | typeof(double), typeof(DoubleSlider),
86 | new FrameworkPropertyMetadata(15.0, FrameworkPropertyMetadataOptions.AffectsArrange, null, UpperValueCoerceValueCallback));
87 |
88 | public double UpperValue {
89 | get { return (double) GetValue(UpperValueProperty); }
90 | set { SetValue(UpperValueProperty, value); }
91 | }
92 |
93 | private Thumb Middle;
94 |
95 | Thumb UpperThumb;
96 | Thumb LowerThumb;
97 |
98 | private RepeatButton DecreaseButton;
99 | private RepeatButton IncreaseButton;
100 |
101 | protected override Size ArrangeOverride(Size arrangeSize)
102 | {
103 | arrangeSize = base.ArrangeOverride(arrangeSize);
104 |
105 | // Figure out the X position of the upper and lower thumbs. Note that we have to provide
106 | // our width to GetValueToSize, since ActualWidth isn't available yet.
107 | double valueToSize = GetValueToSize(arrangeSize.Width);
108 | double UpperPointX = (UpperValue-Minimum) * valueToSize;
109 | double LowerPointX = (LowerValue-Minimum) * valueToSize;
110 |
111 | // Move the upper and lower handles out by this much, and extend this middle. This
112 | // makes the middle handle bigger.
113 | double OffsetOutwards = 5;
114 | Middle.Arrange(new Rect(LowerPointX-OffsetOutwards-1, 0,
115 | Math.Max(1, UpperPointX-LowerPointX+OffsetOutwards*2+2), arrangeSize.Height));
116 |
117 | // Right-align the lower thumb and left-align the upper thumb.
118 | LowerThumb.Arrange(new Rect(LowerPointX-LowerThumb.Width-OffsetOutwards, 0, LowerThumb.Width, arrangeSize.Height));
119 | UpperThumb.Arrange(new Rect(UpperPointX +OffsetOutwards, 0, UpperThumb.Width, arrangeSize.Height));
120 |
121 | DecreaseButton.Arrange(new Rect(0, 0, Math.Max(1, LowerPointX), Math.Max(1, arrangeSize.Height)));
122 | IncreaseButton.Arrange(new Rect(UpperPointX, 0, Math.Max(1, arrangeSize.Width - UpperPointX), arrangeSize.Height));
123 | return arrangeSize;
124 | }
125 |
126 | private void MoveValue(double delta)
127 | {
128 | if(delta > 0)
129 | {
130 | // If this increase will be clamped when changing the upper value, reduce it
131 | // so it clamps the lower value too. This way, the distance between the upper
132 | // and lower value stays the same.
133 | delta = Math.Min(delta, Maximum - UpperValue);
134 | UpperValue += delta;
135 | LowerValue += delta;
136 | }
137 | else
138 | {
139 | delta *= -1;
140 | delta = Math.Min(delta, LowerValue - Minimum);
141 | LowerValue -= delta;
142 | UpperValue -= delta;
143 | }
144 |
145 | FireValueChanged();
146 | }
147 |
148 | private double GetValueToSize()
149 | {
150 | return GetValueToSize(this.ActualWidth);
151 | }
152 |
153 | private double GetValueToSize(double width)
154 | {
155 | double Range = Maximum - Minimum;
156 | return Math.Max(0.0, (width - UpperThumb.RenderSize.Width) / Range);
157 | }
158 |
159 | public override void OnApplyTemplate()
160 | {
161 | base.OnApplyTemplate();
162 |
163 | LowerThumb = GetTemplateChild("PART_LowerThumb") as Thumb;
164 | UpperThumb = GetTemplateChild("PART_UpperThumb") as Thumb;
165 | Middle = GetTemplateChild("PART_Middle") as Thumb;
166 | DecreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton;
167 | IncreaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton;
168 | DecreaseButton.Click += delegate(object sender, RoutedEventArgs e) { MoveValue(-1); };
169 | IncreaseButton.Click += delegate(object sender, RoutedEventArgs e) { MoveValue(+1); };
170 |
171 | LowerThumb.DragDelta += delegate(object sender, DragDeltaEventArgs e)
172 | {
173 | double sizeToValue = 1 / GetValueToSize();
174 |
175 | double NewValue = LowerValue + e.HorizontalChange * sizeToValue;
176 | NewValue = Math.Min(NewValue, UpperValue - MinimumDistance);
177 | LowerValue = NewValue;
178 | FireValueChanged();
179 | };
180 |
181 | UpperThumb.DragDelta += delegate(object sender, DragDeltaEventArgs e)
182 | {
183 | double sizeToValue = 1 / GetValueToSize();
184 | double NewValue = UpperValue + e.HorizontalChange * sizeToValue;
185 | NewValue = Math.Max(NewValue, LowerValue + MinimumDistance);
186 | UpperValue = NewValue;
187 | FireValueChanged();
188 | };
189 |
190 | Middle.DragDelta += delegate(object sender, DragDeltaEventArgs e)
191 | {
192 | // Convert the pixel delta to a value change.
193 | double sizeToValue = 1 / GetValueToSize();
194 | Console.WriteLine("drag: " + e.HorizontalChange + ", " + sizeToValue + ", " + e.HorizontalChange * sizeToValue);
195 | MoveValue(e.HorizontalChange * sizeToValue);
196 | };
197 |
198 | InvalidateArrange();
199 | }
200 | }
201 |
202 | class DoubleSliderThumb: Thumb
203 | {
204 | public static readonly DependencyProperty ShowUpArrowProperty = DependencyProperty.Register("ShowUpArrow",
205 | typeof(bool), typeof(DoubleSliderThumb));
206 |
207 | public bool ShowUpArrow {
208 | get { return (bool) this.GetValue(ShowUpArrowProperty); }
209 | set { this.SetValue(ShowUpArrowProperty, value); }
210 | }
211 |
212 | public static readonly DependencyProperty ShowDownArrowProperty = DependencyProperty.Register("ShowDownArrow",
213 | typeof(bool), typeof(DoubleSliderThumb));
214 |
215 | public bool ShowDownArrow {
216 | get { return (bool) this.GetValue(ShowDownArrowProperty); }
217 | set { this.SetValue(ShowDownArrowProperty, value); }
218 | }
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/smx-config/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using System.Runtime.InteropServices;
4 | using System.IO;
5 | using System.Threading;
6 |
7 | namespace smx_config
8 | {
9 | public partial class App: Application
10 | {
11 | [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)]
12 | private static extern void SMX_Internal_OpenConsole();
13 |
14 | private System.Windows.Forms.NotifyIcon trayIcon;
15 | private MainWindow window;
16 |
17 | App()
18 | {
19 | AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionEventHandler;
20 | }
21 |
22 | protected override void OnStartup(StartupEventArgs e)
23 | {
24 | base.OnStartup(e);
25 |
26 | // If an instance is already running, foreground it and exit.
27 | if(ForegroundExistingInstance())
28 | {
29 | Shutdown();
30 | return;
31 | }
32 |
33 | // This is used by the installer to close a running instance automatically when updating.
34 | ListenForShutdownRequest();
35 |
36 | // If we're being launched on startup, but the LaunchOnStartup setting is false,
37 | // then the user turned off auto-launching but we're still being launched for some
38 | // reason (eg. a renamed launch shortcut that we couldn't find to remove). As
39 | // a safety so we don't launch when the user doesn't want us to, just exit in this
40 | // case.
41 | if(Helpers.LaunchedOnStartup() && !LaunchOnStartup.Enable)
42 | {
43 | Shutdown();
44 | return;
45 | }
46 |
47 | LaunchOnStartup.Enable = true;
48 | if(!SMX.SMX.DLLExists())
49 | {
50 | MessageBox.Show("SMXConfig encountered an unexpected error.\n\nSMX.dll couldn't be found:\n\n" + Helpers.GetLastWin32ErrorString(), "SMXConfig");
51 | Current.Shutdown();
52 | return;
53 | }
54 |
55 | if(!SMX.SMX.DLLAvailable())
56 | {
57 | MessageBox.Show("SMXConfig encountered an unexpected error.\n\nSMX.dll failed to load:\n\n" + Helpers.GetLastWin32ErrorString(), "SMXConfig");
58 | Current.Shutdown();
59 | return;
60 | }
61 |
62 | if(Helpers.GetDebug())
63 | SMX_Internal_OpenConsole();
64 |
65 | CurrentSMXDevice.singleton = new CurrentSMXDevice();
66 |
67 | // Load animations.
68 | Helpers.LoadSavedPanelAnimations();
69 |
70 | CreateTrayIcon();
71 |
72 | // Create the main window.
73 | if(!Helpers.LaunchedOnStartup())
74 | ToggleMainWindow();
75 | }
76 |
77 | // Open or close the main window.
78 | //
79 | // We don't create our UI until the first time it's opened, so we use
80 | // less memory when we're launched on startup. However, when we're minimized
81 | // back to the tray, we don't destroy the main window. WPF is just too
82 | // leaky to recreate the main window each time it's called due to internal
83 | // circular references. Instead, we just focus on minimizing CPU overhead.
84 | void ToggleMainWindow()
85 | {
86 | if(window == null)
87 | {
88 | window = new MainWindow();
89 | window.Closed += MainWindowClosed;
90 | window.Show();
91 | }
92 | else if(IsMinimizedToTray())
93 | {
94 | window.Visibility = Visibility.Visible;
95 | window.Activate();
96 | }
97 | else
98 | {
99 | MinimizeToTray();
100 | }
101 | }
102 |
103 | public bool IsMinimizedToTray()
104 | {
105 | return window.Visibility == Visibility.Collapsed;
106 | }
107 |
108 | public void MinimizeToTray()
109 | {
110 | // Just hide the window. Don't actually set the window to minimized, since it
111 | // won't do anything and it causes problems when restoring the window.
112 | window.Visibility = Visibility.Collapsed;
113 | }
114 |
115 | public void BringToForeground()
116 | {
117 | // Restore or create the window. Don't minimize if we're already restored.
118 | if(window == null || IsMinimizedToTray())
119 | ToggleMainWindow();
120 |
121 | // Focus the window.
122 | window.WindowState = WindowState.Normal;
123 | window.Activate();
124 | }
125 |
126 | private void MainWindowClosed(object sender, EventArgs e)
127 | {
128 | window = null;
129 | }
130 |
131 | private void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e)
132 | {
133 | string message = e.ExceptionObject.ToString();
134 | MessageBox.Show("SMXConfig encountered an unexpected error:\n\n" + message, "SMXConfig");
135 | }
136 |
137 | protected override void OnExit(ExitEventArgs e)
138 | {
139 | base.OnExit(e);
140 |
141 | Console.WriteLine("Application exiting");
142 |
143 | // Remove the tray icon.
144 | if(trayIcon != null)
145 | {
146 | trayIcon.Visible = false;
147 | trayIcon = null;
148 | }
149 |
150 | // Shut down cleanly, to make sure we don't run any threaded callbacks during shutdown.
151 | if(CurrentSMXDevice.singleton != null)
152 | {
153 | CurrentSMXDevice.singleton.Shutdown();
154 | CurrentSMXDevice.singleton = null;
155 | }
156 | }
157 |
158 | // If another instance other than this one is running, send it WM_USER to tell it to
159 | // foreground itself. Return true if another instance was found.
160 | private bool ForegroundExistingInstance()
161 | {
162 | bool createdNew = false;
163 | EventWaitHandle SMXConfigEvent = new EventWaitHandle(false, EventResetMode.AutoReset, "SMXConfigEvent", out createdNew);
164 | if(!createdNew)
165 | {
166 | // Signal the event to foreground the existing instance.
167 | SMXConfigEvent.Set();
168 | return true;
169 | }
170 |
171 | ThreadPool.RegisterWaitForSingleObject(SMXConfigEvent, ForegroundApplicationCallback, this, Timeout.Infinite, false);
172 |
173 | return false;
174 | }
175 |
176 | private static void ForegroundApplicationCallback(Object self, Boolean timedOut)
177 | {
178 | // This is called when another instance sends us a message over SMXConfigEvent.
179 | Application.Current.Dispatcher.Invoke(new Action(() => {
180 | App application = (App) Application.Current;
181 | application.BringToForeground();
182 | }));
183 | }
184 |
185 | private void ListenForShutdownRequest()
186 | {
187 | // We've already checked that we're the only instance when we get here, so this event shouldn't
188 | // exist. If it already exists for some reason, we'll listen to it anyway.
189 | EventWaitHandle SMXConfigShutdown = new EventWaitHandle(false, EventResetMode.AutoReset, "SMXConfigShutdown");
190 | ThreadPool.RegisterWaitForSingleObject(SMXConfigShutdown, ShutdownApplicationCallback, this, Timeout.Infinite, false);
191 | }
192 |
193 | private static void ShutdownApplicationCallback(Object self, Boolean timedOut)
194 | {
195 | // This is called when another instance sends us a message over SMXConfigShutdown.
196 | Application.Current.Dispatcher.Invoke(new Action(() => {
197 | App application = (App) Application.Current;
198 | application.Shutdown();
199 | }));
200 | }
201 |
202 | // Create a tray icon. For some reason there's no WPF interface for this,
203 | // so we have to use Forms.
204 | void CreateTrayIcon()
205 | {
206 | Stream iconStream = GetResourceStream(new Uri( "pack://application:,,,/Resources/window%20icon%20grey.ico")).Stream;
207 | System.Drawing.Icon icon = new System.Drawing.Icon(iconStream);
208 |
209 | trayIcon = new System.Windows.Forms.NotifyIcon();
210 | trayIcon.Text = "StepManiaX";
211 | trayIcon.Visible = true;
212 |
213 | // Show or hide the application window on click.
214 | trayIcon.Click += delegate (object sender, EventArgs e) { ToggleMainWindow(); };
215 | trayIcon.DoubleClick += delegate (object sender, EventArgs e) { ToggleMainWindow(); };
216 |
217 | CurrentSMXDevice.singleton.ConfigurationChanged += delegate(LoadFromConfigDelegateArgs args) {
218 | RefreshTrayIcon(args);
219 | };
220 |
221 | // Do the initial refresh.
222 | RefreshTrayIcon(CurrentSMXDevice.singleton.GetState(), true);
223 | }
224 |
225 | // Refresh the tray icon when we're connected or disconnected.
226 | bool wasConnected;
227 | void RefreshTrayIcon(LoadFromConfigDelegateArgs args, bool force=false)
228 | {
229 | if(trayIcon == null)
230 | return;
231 |
232 | bool EitherControllerConnected = false;
233 | for(int pad = 0; pad < 2; ++pad)
234 | if(args.controller[pad].info.connected)
235 | EitherControllerConnected = true;
236 |
237 | // Skip the refresh if the connected state didn't change.
238 | if(wasConnected == EitherControllerConnected && !force)
239 | return;
240 | wasConnected = EitherControllerConnected;
241 |
242 | trayIcon.Text = EitherControllerConnected? "StepManiaX (connected)":"StepManiaX (disconnected)";
243 |
244 | // Set the tray icon.
245 | string filename = EitherControllerConnected? "window%20icon.ico":"window%20icon%20grey.ico";
246 | Stream iconStream = GetResourceStream(new Uri( "pack://application:,,,/Resources/" + filename)).Stream;
247 | trayIcon.Icon = new System.Drawing.Icon(iconStream);
248 | }
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/sdk/Windows/Helpers.cpp:
--------------------------------------------------------------------------------
1 | #include "Helpers.h"
2 | #include
3 | #include
4 | using namespace std;
5 | using namespace SMX;
6 |
7 | namespace {
8 | function g_LogCallback = [](const string &log) {
9 | printf("%6.3f: %s\n", GetMonotonicTime(), log.c_str());
10 | };
11 | };
12 |
13 | void SMX::Log(string s)
14 | {
15 | g_LogCallback(s);
16 | }
17 |
18 | void SMX::Log(wstring s)
19 | {
20 | Log(WideStringToUTF8(s));
21 | }
22 |
23 | void SMX::SetLogCallback(function callback)
24 | {
25 | g_LogCallback = callback;
26 | }
27 |
28 | const DWORD MS_VC_EXCEPTION = 0x406D1388;
29 | #pragma pack(push,8)
30 | typedef struct tagTHREADNAME_INFO
31 | {
32 | DWORD dwType; // Must be 0x1000.
33 | LPCSTR szName; // Pointer to name (in user addr space).
34 | DWORD dwThreadID; // Thread ID (-1=caller thread).
35 | DWORD dwFlags; // Reserved for future use, must be zero.
36 | } THREADNAME_INFO;
37 |
38 | #pragma pack(pop)
39 | void SMX::SetThreadName(DWORD iThreadId, const string &name)
40 | {
41 |
42 | THREADNAME_INFO info;
43 | info.dwType = 0x1000;
44 | info.szName = name.c_str();
45 | info.dwThreadID = iThreadId;
46 | info.dwFlags = 0;
47 | #pragma warning(push)
48 | #pragma warning(disable: 6320 6322)
49 | __try{
50 | RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);
51 | }
52 | __except (EXCEPTION_EXECUTE_HANDLER) {
53 | }
54 | #pragma warning(pop)
55 | }
56 |
57 | void SMX::StripCrnl(wstring &s)
58 | {
59 | while(s.size() && (s[s.size()-1] == '\r' || s[s.size()-1] == '\n'))
60 | s.erase(s.size()-1);
61 | }
62 |
63 | wstring SMX::GetErrorString(int err)
64 | {
65 | wchar_t buf[1024] = L"";
66 | FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, err, 0, buf, sizeof(buf), NULL);
67 |
68 | // Fix badly formatted strings returned by FORMAT_MESSAGE_FROM_SYSTEM.
69 | wstring sResult = buf;
70 | StripCrnl(sResult);
71 | return sResult;
72 | }
73 |
74 | string SMX::vssprintf(const char *szFormat, va_list argList)
75 | {
76 | int iChars = vsnprintf(NULL, 0, szFormat, argList);
77 | if(iChars == -1)
78 | return string("Error formatting string: ") + szFormat;
79 |
80 | string sStr;
81 | sStr.resize(iChars+1);
82 | vsnprintf((char *) sStr.data(), iChars+1, szFormat, argList);
83 | sStr.resize(iChars);
84 |
85 | return sStr;
86 | }
87 |
88 | string SMX::ssprintf(const char *fmt, ...)
89 | {
90 | va_list va;
91 | va_start(va, fmt);
92 | return vssprintf(fmt, va);
93 | }
94 |
95 | wstring SMX::wvssprintf(const wchar_t *szFormat, va_list argList)
96 | {
97 | int iChars = _vsnwprintf(NULL, 0, szFormat, argList);
98 | if(iChars == -1)
99 | return wstring(L"Error formatting string: ") + szFormat;
100 |
101 | wstring sStr;
102 | sStr.resize(iChars+1);
103 | _vsnwprintf((wchar_t *) sStr.data(), iChars+1, szFormat, argList);
104 | sStr.resize(iChars);
105 |
106 | return sStr;
107 | }
108 |
109 | wstring SMX::wssprintf(const wchar_t *fmt, ...)
110 | {
111 | va_list va;
112 | va_start(va, fmt);
113 | return wvssprintf(fmt, va);
114 | }
115 |
116 | string SMX::BinaryToHex(const void *pData_, int iNumBytes)
117 | {
118 | const unsigned char *pData = (const unsigned char *) pData_;
119 | string s;
120 | for(int i=0; iHigh1Time;
169 | timeLow = InterruptTime->LowPart;
170 | } while (timeHigh != InterruptTime->High2Time);
171 | LONGLONG now = ((LONGLONG)timeHigh << 32) + timeLow;
172 | static LONGLONG d = now;
173 | return now - d;
174 | }
175 |
176 | LONGLONG ScaleQpc(LONGLONG qpc)
177 | {
178 | // We do the actual scaling in fixed-point rather than floating, to make sure
179 | // that we don't violate monotonicity due to rounding errors. There's no
180 | // need to cache QueryPerformanceFrequency().
181 | LARGE_INTEGER frequency;
182 | QueryPerformanceFrequency(&frequency);
183 | double fraction = 10000000/double(frequency.QuadPart);
184 | LONGLONG denom = 1024;
185 | LONGLONG numer = max(1LL, (LONGLONG)(fraction*denom + 0.5));
186 | return qpc * numer / denom;
187 | }
188 |
189 | ULONGLONG ReadUnbiasedQpc()
190 | {
191 | // We remove the suspend bias added to QueryPerformanceCounter results by
192 | // subtracting the interrupt time bias, which is not strictly speaking legal,
193 | // but the units are correct and I think it's impossible for the resulting
194 | // "unbiased QPC" value to go backwards.
195 | LONGLONG interruptTimeBias, qpc;
196 | do {
197 | interruptTimeBias = *InterruptTimeBias;
198 | LARGE_INTEGER counter;
199 | QueryPerformanceCounter(&counter);
200 | qpc = counter.QuadPart;
201 | } while (interruptTimeBias != *InterruptTimeBias);
202 | static std::pair d(qpc, interruptTimeBias);
203 | return ScaleQpc(qpc - d.first) - (interruptTimeBias - d.second);
204 | }
205 |
206 | bool Win7OrLater()
207 | {
208 | static int iWin7OrLater = -1;
209 | if(iWin7OrLater != -1)
210 | return bool(iWin7OrLater);
211 |
212 | OSVERSIONINFOW ver = { sizeof(OSVERSIONINFOW), };
213 | GetVersionEx(&ver);
214 | iWin7OrLater = (ver.dwMajorVersion > 6 || (ver.dwMajorVersion == 6 && ver.dwMinorVersion >= 1));
215 | return bool(iWin7OrLater);
216 | }
217 | }
218 |
219 | /// getMonotonicTime() returns the time elapsed since the application's first
220 | /// call to getMonotonicTime(), in 100ns units. The values returned are
221 | /// guaranteed to be monotonic. The time ticks in 15ms resolution and advances
222 | /// during suspend on XP and Vista, but we manage to avoid this on Windows 7
223 | /// and 8, which also use a high-precision timer. The time does not wrap after
224 | /// 49 days.
225 | double SMX::GetMonotonicTime()
226 | {
227 | // On Windows XP and earlier, QueryPerformanceCounter is not monotonic so we
228 | // steer well clear of it; on Vista, it's just a bit slow.
229 | uint64_t iTime = Win7OrLater()? ReadUnbiasedQpc() : ReadInterruptTime();
230 | return iTime / 10000000.0;
231 | }
232 |
233 | void SMX::GenerateRandom(void *pOut, int iSize)
234 | {
235 | // These calls shouldn't fail.
236 | HCRYPTPROV cryptProv;
237 | if(!CryptAcquireContext(&cryptProv, nullptr,
238 | L"Microsoft Base Cryptographic Provider v1.0",
239 | PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
240 | throw exception("CryptAcquireContext error");
241 |
242 | if(!CryptGenRandom(cryptProv, iSize, (BYTE *) pOut))
243 | throw exception("CryptGenRandom error");
244 |
245 | if(!CryptReleaseContext(cryptProv, 0))
246 | throw exception("CryptReleaseContext error");
247 | }
248 |
249 | string SMX::WideStringToUTF8(wstring s)
250 | {
251 | if(s.empty())
252 | return "";
253 |
254 | int iBytes = WideCharToMultiByte( CP_ACP, 0, s.data(), s.size(), NULL, 0, NULL, FALSE );
255 |
256 | string ret;
257 | ret.resize(iBytes);
258 | WideCharToMultiByte( CP_ACP, 0, s.data(), s.size(), (char *) ret.data(), iBytes, NULL, FALSE );
259 |
260 | return ret;
261 | }
262 |
263 | const char *SMX::CreateError(string error)
264 | {
265 | // Store the string in a static so it doesn't get deallocated.
266 | static string buf;
267 | buf = error;
268 | return buf.c_str();
269 | }
270 |
271 | SMX::AutoCloseHandle::AutoCloseHandle(HANDLE h)
272 | {
273 | handle = h;
274 | }
275 |
276 | SMX::AutoCloseHandle::~AutoCloseHandle()
277 | {
278 | if(handle != INVALID_HANDLE_VALUE)
279 | CloseHandle(handle);
280 | }
281 |
282 | SMX::Mutex::Mutex()
283 | {
284 | m_hLock = CreateMutex(NULL, false, NULL);
285 | }
286 |
287 | SMX::Mutex::~Mutex()
288 | {
289 | CloseHandle(m_hLock);
290 | }
291 |
292 | void SMX::Mutex::Lock()
293 | {
294 | WaitForSingleObject(m_hLock, INFINITE);
295 | m_iLockedByThread = GetCurrentThreadId();
296 | }
297 |
298 | void SMX::Mutex::Unlock()
299 | {
300 | m_iLockedByThread = 0;
301 | ReleaseMutex(m_hLock);
302 | }
303 |
304 | void SMX::Mutex::AssertNotLockedByCurrentThread()
305 | {
306 | if(m_iLockedByThread == GetCurrentThreadId())
307 | throw exception("Expected to not be locked");
308 | }
309 |
310 | void SMX::Mutex::AssertLockedByCurrentThread()
311 | {
312 | if(m_iLockedByThread != GetCurrentThreadId())
313 | throw exception("Expected to be locked");
314 | }
315 |
316 | SMX::LockMutex::LockMutex(SMX::Mutex &mutex):
317 | m_Mutex(mutex)
318 | {
319 | m_Mutex.AssertNotLockedByCurrentThread();
320 | m_Mutex.Lock();
321 | }
322 |
323 | SMX::LockMutex::~LockMutex()
324 | {
325 | m_Mutex.AssertLockedByCurrentThread();
326 | m_Mutex.Unlock();
327 | }
328 |
329 | // This is a helper to let the config tool open a window, which has no freopen.
330 | // This isn't exposed in SMX.h.
331 | extern "C" __declspec(dllexport) void SMX_Internal_OpenConsole()
332 | {
333 | AllocConsole();
334 | freopen("CONOUT$","wb", stdout);
335 | freopen("CONOUT$","wb", stderr);
336 | AttachConsole(ATTACH_PARENT_PROCESS);
337 | }
338 |
--------------------------------------------------------------------------------
/smx-config/CurrentSMXDevice.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Windows;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using System.Windows.Threading;
8 | using System.Windows.Controls;
9 | using System.ComponentModel;
10 |
11 | namespace smx_config
12 | {
13 | // The state and configuration of a pad.
14 | public struct LoadFromConfigDelegateArgsPerController
15 | {
16 | public SMX.SMXInfo info;
17 | public SMX.SMXConfig config;
18 | public SMX.SMXSensorTestModeData test_data;
19 |
20 | // The panels that are activated. Note that to receive notifications from OnConfigChange
21 | // when inputs change state, set RefreshOnInputChange to true. Otherwise, this field will
22 | // be filled in but notifications won't be sent due to only inputs changing.
23 | public bool[] inputs;
24 | }
25 |
26 | public struct LoadFromConfigDelegateArgs
27 | {
28 | // This indicates which fields changed since the last call.
29 | public bool ConnectionsChanged, ConfigurationChanged, InputChanged, TestDataChanged;
30 |
31 | // Data for each of two controllers:
32 | public LoadFromConfigDelegateArgsPerController[] controller;
33 |
34 | // If we have more than one connected controller, we expect them to be the same version.
35 | // Return the newest firmware version that's connected.
36 | public int firmwareVersion()
37 | {
38 | int result = 1;
39 | foreach(var data in controller)
40 | {
41 | if(data.info.connected && data.info.m_iFirmwareVersion > result)
42 | result = data.info.m_iFirmwareVersion;
43 |
44 | }
45 | return result;
46 | }
47 |
48 | // The control that changed the configuration (passed to FireConfigurationChanged).
49 | public object source;
50 | };
51 |
52 | // This class tracks the device we're currently configuring, and runs a callback when
53 | // it changes.
54 | class CurrentSMXDevice
55 | {
56 | public static CurrentSMXDevice singleton;
57 |
58 | // This is fired when FireConfigurationChanged is called, and when the current device
59 | // changes.
60 | public delegate void ConfigurationChangedDelegate(LoadFromConfigDelegateArgs args);
61 | public event ConfigurationChangedDelegate ConfigurationChanged;
62 |
63 | private bool[] WasConnected = new bool[2] { false, false };
64 | private bool[][] LastInputs = new bool[2][];
65 | private SMX.SMXSensorTestModeData[] LastTestData = new SMX.SMXSensorTestModeData[2];
66 | private Dispatcher MainDispatcher;
67 |
68 | public CurrentSMXDevice()
69 | {
70 | // Grab the main thread's dispatcher, so we can invoke into it.
71 | MainDispatcher = Dispatcher.CurrentDispatcher;
72 |
73 | // Set our update callback. This will be called when something happens: connection or disconnection,
74 | // inputs changed, configuration updated, test data updated, etc. It doesn't specify what's changed,
75 | // we simply check the whole state.
76 | SMX.SMX.Start(delegate(int PadNumber, SMX.SMX.SMXUpdateCallbackReason reason) {
77 | // Console.WriteLine("... " + reason);
78 | // This is called from a thread, with SMX's internal mutex locked. We must not call into SMX
79 | // or do anything with the UI from here. Just queue an update back into the UI thread.
80 | MainDispatcher.InvokeAsync(delegate() {
81 | switch(reason)
82 | {
83 | case SMX.SMX.SMXUpdateCallbackReason.Updated:
84 | CheckForChanges();
85 | break;
86 | case SMX.SMX.SMXUpdateCallbackReason.FactoryResetCommandComplete:
87 | Console.WriteLine("SMX_FactoryResetCommandComplete");
88 | FireConfigurationChanged(null);
89 | break;
90 | }
91 | });
92 | });
93 | }
94 |
95 | public void Shutdown()
96 | {
97 | SMX.SMX.Stop();
98 | }
99 |
100 | private void CheckForChanges()
101 | {
102 | LoadFromConfigDelegateArgs args = GetState();
103 |
104 | // Mark which parts have changed.
105 | //
106 | // For configuration, we only check for connection state changes. Actual configuration
107 | // changes are fired by controls via FireConfigurationChanged.
108 | for(int pad = 0; pad < 2; ++pad)
109 | {
110 | LoadFromConfigDelegateArgsPerController controller = args.controller[pad];
111 | if(WasConnected[pad] != controller.info.connected)
112 | {
113 | args.ConfigurationChanged = true;
114 | args.ConnectionsChanged = true;
115 | WasConnected[pad] = controller.info.connected;
116 | }
117 |
118 | if(LastInputs[pad] == null || !Enumerable.SequenceEqual(controller.inputs, LastInputs[pad]))
119 | {
120 | args.InputChanged = true;
121 | LastInputs[pad] = controller.inputs;
122 | }
123 |
124 | if(!controller.test_data.Equals(LastTestData[pad]))
125 | {
126 | args.TestDataChanged = true;
127 | LastTestData[pad] = controller.test_data;
128 | }
129 | }
130 |
131 | // Only fire the delegate if something has actually changed.
132 | if(args.ConfigurationChanged || args.InputChanged || args.TestDataChanged)
133 | ConfigurationChanged?.Invoke(args);
134 | }
135 |
136 | public void FireConfigurationChanged(object source)
137 | {
138 | LoadFromConfigDelegateArgs args = GetState();
139 | args.ConfigurationChanged = true;
140 | args.source = source;
141 | ConfigurationChanged?.Invoke(args);
142 | }
143 |
144 | public LoadFromConfigDelegateArgs GetState()
145 | {
146 | LoadFromConfigDelegateArgs args = new LoadFromConfigDelegateArgs();
147 | args.controller = new LoadFromConfigDelegateArgsPerController[2];
148 |
149 | for(int pad = 0; pad < 2; ++pad)
150 | {
151 | LoadFromConfigDelegateArgsPerController controller;
152 | controller.test_data = new SMX.SMXSensorTestModeData();
153 |
154 | // Expand the inputs mask to an array.
155 | UInt16 Inputs = SMX.SMX.GetInputState(pad);
156 | controller.inputs = new bool[9];
157 | for(int i = 0; i < 9; ++i)
158 | controller.inputs[i] = (Inputs & (1 << i)) != 0;
159 | SMX.SMX.GetInfo(pad, out controller.info);
160 | SMX.SMX.GetConfig(pad, out controller.config);
161 | SMX.SMX.GetTestData(pad, out controller.test_data);
162 | args.controller[pad] = controller;
163 | }
164 |
165 | return args;
166 | }
167 |
168 | }
169 |
170 | // Call a delegate on configuration change. Configuration changes are notified by calling
171 | // FireConfigurationChanged.
172 | public class OnConfigChange
173 | {
174 | public delegate void LoadFromConfigDelegate(LoadFromConfigDelegateArgs args);
175 | private readonly Control Owner;
176 | private readonly LoadFromConfigDelegate Callback;
177 | private bool _RefreshOnInputChange = false;
178 |
179 | // If set to true, the callback will be invoked on input changes in addition to configuration
180 | // changes. This can cause the callback to be run at any time, such as while the user is
181 | // interacting with the control.
182 | public bool RefreshOnInputChange {
183 | get { return _RefreshOnInputChange; }
184 | set {_RefreshOnInputChange = value; }
185 | }
186 |
187 | private bool _RefreshOnTestDataChange = false;
188 |
189 | // Like RefreshOnInputChange, but enables callbacks when test data changes.
190 | public bool RefreshOnTestDataChange {
191 | get { return _RefreshOnTestDataChange; }
192 | set { _RefreshOnTestDataChange = value; }
193 | }
194 |
195 | // Owner is the Control that we're calling. This callback will be disabled when the
196 | // control is unloaded, and we won't call it if it's the same control that fired
197 | // the change via FireConfigurationChanged.
198 | //
199 | // In addition, the callback is called when the control is Loaded, to load the initial
200 | // state.
201 | public OnConfigChange(Control owner, LoadFromConfigDelegate callback)
202 | {
203 | Owner = owner;
204 | Callback = callback;
205 |
206 | Owner.Loaded += delegate(object sender, RoutedEventArgs e)
207 | {
208 | if(CurrentSMXDevice.singleton == null)
209 | return;
210 | CurrentSMXDevice.singleton.ConfigurationChanged += ConfigurationChanged;
211 |
212 | // When a control is loaded, run the callback with the current state
213 | // as though a device was changed, so we'll refresh things like the
214 | // sensitivity sliders.
215 | LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState();
216 | args.ConnectionsChanged = true;
217 | Callback(args);
218 | };
219 |
220 | Owner.Unloaded += delegate(object sender, RoutedEventArgs e)
221 | {
222 | if(CurrentSMXDevice.singleton == null)
223 | return;
224 | CurrentSMXDevice.singleton.ConfigurationChanged -= ConfigurationChanged;
225 | };
226 | }
227 |
228 | private void ConfigurationChanged(LoadFromConfigDelegateArgs args)
229 | {
230 | if(args.ConfigurationChanged ||
231 | (RefreshOnInputChange && args.InputChanged) ||
232 | (RefreshOnTestDataChange && args.TestDataChanged))
233 | {
234 | Callback(args);
235 | }
236 | }
237 | };
238 |
239 |
240 | public class OnInputChange
241 | {
242 | public delegate void LoadFromConfigDelegate(LoadFromConfigDelegateArgs args);
243 | private readonly Control Owner;
244 | private readonly LoadFromConfigDelegate Callback;
245 |
246 | // Owner is the Control that we're calling. This callback will be disable when the
247 | // control is unloaded, and we won't call it if it's the same control that fired
248 | // the change via FireConfigurationChanged.
249 | //
250 | // In addition, the callback is called when the control is Loaded, to load the initial
251 | // state.
252 | public OnInputChange(Control owner, LoadFromConfigDelegate callback)
253 | {
254 | Owner = owner;
255 | Callback = callback;
256 |
257 | // This is available when the application is running, but will be null in the XAML designer.
258 | if(CurrentSMXDevice.singleton == null)
259 | return;
260 |
261 | Owner.Loaded += delegate(object sender, RoutedEventArgs e)
262 | {
263 | CurrentSMXDevice.singleton.ConfigurationChanged += ConfigurationChanged;
264 | Refresh();
265 | };
266 |
267 | Owner.Unloaded += delegate(object sender, RoutedEventArgs e)
268 | {
269 | CurrentSMXDevice.singleton.ConfigurationChanged -= ConfigurationChanged;
270 | };
271 | }
272 |
273 | private void ConfigurationChanged(LoadFromConfigDelegateArgs args)
274 | {
275 | Callback(args);
276 | }
277 |
278 | private void Refresh()
279 | {
280 | if(CurrentSMXDevice.singleton != null)
281 | Callback(CurrentSMXDevice.singleton.GetState());
282 | }
283 | };
284 | }
285 |
--------------------------------------------------------------------------------
/sdk/SMX.h:
--------------------------------------------------------------------------------
1 | #ifndef SMX_H
2 | #define SMX_H
3 |
4 | #include
5 | #include // for offsetof
6 |
7 | #ifdef SMX_EXPORTS
8 | #define SMX_API extern "C" __declspec(dllexport)
9 | #else
10 | #define SMX_API extern "C" __declspec(dllimport)
11 | #endif
12 |
13 | struct SMXInfo;
14 | struct SMXConfig;
15 | enum SensorTestMode;
16 | enum PanelTestMode;
17 | enum SMXUpdateCallbackReason;
18 | struct SMXSensorTestModeData;
19 |
20 | // All functions are nonblocking. Getters will return the most recent state. Setters will
21 | // return immediately and do their work in the background. No functions return errors, and
22 | // setting data on a pad which isn't connected will have no effect.
23 |
24 | // Initialize, and start searching for devices.
25 | //
26 | // UpdateCallback will be called when something happens: connection or disconnection, inputs
27 | // changed, configuration updated, test data updated, etc. It doesn't specify what's changed,
28 | // and the user should check all state that it's interested in.
29 | //
30 | // This is called asynchronously from a helper thread, so the receiver must be thread-safe.
31 | typedef void SMXUpdateCallback(int pad, SMXUpdateCallbackReason reason, void *pUser);
32 | SMX_API void SMX_Start(SMXUpdateCallback UpdateCallback, void *pUser);
33 |
34 | // Shut down and disconnect from all devices. This will wait for any user callbacks to complete,
35 | // and no user callbacks will be called after this returns. This must not be called from within
36 | // the update callback.
37 | SMX_API void SMX_Stop();
38 |
39 | // Set a function to receive diagnostic logs. By default, logs are written to stdout.
40 | // This can be called before SMX_Start, so it affects any logs sent during initialization.
41 | typedef void SMXLogCallback(const char *log);
42 | SMX_API void SMX_SetLogCallback(SMXLogCallback callback);
43 |
44 | // Get info about a pad. Use this to detect which pads are currently connected.
45 | SMX_API void SMX_GetInfo(int pad, SMXInfo *info);
46 |
47 | // Get a mask of the currently pressed panels.
48 | SMX_API uint16_t SMX_GetInputState(int pad);
49 |
50 | // (deprecated) Equivalent to SMX_SetLights2(lightsData, 864).
51 | SMX_API void SMX_SetLights(const char lightData[864]);
52 |
53 | // Update the lights. Both pads are always updated together. lightData is a list of 8-bit RGB
54 | // colors, one for each LED.
55 | //
56 | // lightDataSize is the number of bytes in lightsData. This should be 1350 (2 pads * 9 panels *
57 | // 25 lights * 3 RGB colors). For backwards-compatibility, this can also be 864.
58 | //
59 | // Each panel has lights in the following order:
60 | //
61 | // 00 01 02 03
62 | // 16 17 18
63 | // 04 05 06 07
64 | // 19 20 21
65 | // 08 09 10 11
66 | // 22 23 24
67 | // 12 13 14 15
68 | //
69 | // Panels are in the following order:
70 | //
71 | // 012 9AB
72 | // 345 CDE
73 | // 678 F01
74 | //
75 | // With 18 panels, 25 LEDs per panel and 3 bytes per LED, each light update has 1350 bytes of data.
76 | //
77 | // Lights will update at up to 30 FPS. If lights data is sent more quickly, a best effort will be
78 | // made to send the most recent lights data available, but the panels won't update more quickly.
79 | //
80 | // The panels will return to automatic lighting if no lights are received for a while, so applications
81 | // controlling lights should send light updates continually, even if the lights aren't changing.
82 | //
83 | // For backwards compatibility, if lightDataSize is 864, the old 4x4-only order is used,
84 | // which simply omits lights 16-24.
85 | SMX_API void SMX_SetLights2(const char *lightData, int lightDataSize);
86 |
87 | // By default, the panels light automatically when stepped on. If a lights command is sent by
88 | // the application, this stops happening to allow the application to fully control lighting.
89 | // If no lights update is received for a few seconds, automatic lighting is reenabled by the
90 | // panels.
91 | //
92 | // SMX_ReenableAutoLights can be called to immediately reenable auto-lighting, without waiting
93 | // for the timeout period to elapse. Games don't need to call this, since the panels will return
94 | // to auto-lighting mode automatically after a brief period of no updates.
95 | SMX_API void SMX_ReenableAutoLights();
96 |
97 | // Get the current controller's configuration.
98 | //
99 | // Return true if a configuration is available. If false is returned, no panel is connected
100 | // and no data will be set.
101 | SMX_API bool SMX_GetConfig(int pad, SMXConfig *config);
102 |
103 | // Update the current controller's configuration. This doesn't block, and the new configuration will
104 | // be sent in the background. SMX_GetConfig will return the new configuration as soon as this call
105 | // returns, without waiting for it to actually be sent to the controller.
106 | SMX_API void SMX_SetConfig(int pad, const SMXConfig *config);
107 |
108 | // Reset a pad to its original configuration.
109 | SMX_API void SMX_FactoryReset(int pad);
110 |
111 | // Request an immediate panel recalibration. This is normally not necessary, but can be helpful
112 | // for diagnostics.
113 | SMX_API void SMX_ForceRecalibration(int pad);
114 |
115 | // Set a sensor test mode and request test data. This is used by the configuration tool.
116 | SMX_API void SMX_SetTestMode(int pad, SensorTestMode mode);
117 | SMX_API bool SMX_GetTestData(int pad, SMXSensorTestModeData *data);
118 |
119 | // Set a panel test mode. These only appear as debug lighting on the panel and don't
120 | // return data to us. Lights can't be updated while a panel test mode is active.
121 | // This applies to all connected pads.
122 | SMX_API void SMX_SetPanelTestMode(PanelTestMode mode);
123 |
124 | // Return the build version of the DLL, which is based on the git tag at build time. This
125 | // is only intended for diagnostic logging, and it's also the version we show in SMXConfig.
126 | SMX_API const char *SMX_Version();
127 |
128 | // General info about a connected controller. This can be retrieved with SMX_GetInfo.
129 | struct SMXInfo
130 | {
131 | // True if we're fully connected to this controller. If this is false, the other
132 | // fields won't be set.
133 | bool m_bConnected = false;
134 |
135 | // This device's serial number. This can be used to distinguish devices from each
136 | // other if more than one is connected. This is a null-terminated string instead
137 | // of a C++ string for C# marshalling.
138 | char m_Serial[33];
139 |
140 | // This device's firmware version.
141 | uint16_t m_iFirmwareVersion;
142 | };
143 |
144 | enum SMXUpdateCallbackReason {
145 | // This is called when a generic state change happens: connection or disconnection, inputs changed,
146 | // test data updated, etc. It doesn't specify what's changed. We simply check the whole state.
147 | SMXUpdateCallback_Updated,
148 |
149 | // This is called when SMX_FactoryReset completes, indicating that SMX_GetConfig will now return
150 | // the reset configuration.
151 | SMXUpdateCallback_FactoryResetCommandComplete
152 | };
153 |
154 | // Bits for SMXConfig::flags.
155 | enum SMXConfigFlags {
156 | // If set, panels will use the pressed animation when pressed, and stepColor
157 | // is ignored. If unset, panels will be lit solid using stepColor.
158 | // masterVersion >= 4. Previous versions always use stepColor.
159 | PlatformFlags_AutoLightingUsePressedAnimations = 1 << 0,
160 |
161 | // If set, panels are using FSRs, otherwise load cells.
162 | PlatformFlags_FSR = 1 << 1,
163 | };
164 | #pragma pack(push, 1)
165 |
166 | struct packed_sensor_settings_t {
167 | // Load cell thresholds:
168 | uint8_t loadCellLowThreshold;
169 | uint8_t loadCellHighThreshold;
170 |
171 | // FSR thresholds:
172 | uint8_t fsrLowThreshold[4];
173 | uint8_t fsrHighThreshold[4];
174 |
175 | uint16_t combinedLowThreshold;
176 | uint16_t combinedHighThreshold;
177 |
178 | // This must be left unchanged.
179 | uint16_t reserved;
180 | };
181 |
182 | static_assert(sizeof(packed_sensor_settings_t) == 16, "Incorrect packed_sensor_settings_t size");
183 |
184 | // The configuration for a connected controller. This can be retrieved with SMX_GetConfig
185 | // and modified with SMX_SetConfig.
186 | //
187 | // The order and packing of this struct corresponds to the configuration packet sent to
188 | // the master controller, so it must not be changed.
189 | struct SMXConfig
190 | {
191 | // The firmware version of the master controller. Where supported (version 2 and up), this
192 | // will always read back the firmware version. This will default to 0xFF on version 1, and
193 | // we'll always write 0xFF here so it doesn't change on that firmware version.
194 | //
195 | // We don't need this since we can read the "I" command which also reports the version, but
196 | // this allows panels to also know the master version.
197 | uint8_t masterVersion = 0xFF;
198 |
199 | // The version of this config packet. This can be used by the firmware to know which values
200 | // have been filled in. Any values not filled in will always be 0xFF, which can be tested
201 | // for, but that doesn't work for values where 0xFF is a valid value. This value is unrelated
202 | // to the firmware version, and just indicates which fields in this packet have been set.
203 | // Note that we don't need to increase this any time we add a field, only when it's important
204 | // that we be able to tell if a field is set or not.
205 | //
206 | // Versions:
207 | // - 0xFF: This is a config packet from before configVersion was added.
208 | // - 0x00: configVersion added
209 | // - 0x02: panelThreshold0Low through panelThreshold8High added
210 | // - 0x03: debounceDelayMs added
211 | uint8_t configVersion = 0x05;
212 |
213 | // Packed flags (masterVersion >= 4).
214 | uint8_t flags = 0;
215 |
216 | // Panel thresholds are labelled by their numpad position, eg. Panel8 is up.
217 | // If m_iFirmwareVersion is 1, Panel7 corresponds to all of up, down, left and
218 | // right, and Panel2 corresponds to UpLeft, UpRight, DownLeft and DownRight. For
219 | // later firmware versions, each panel is configured independently.
220 | //
221 | // Setting a value to 0xFF disables that threshold.
222 |
223 | // These are internal tunables and should be left unchanged.
224 | uint16_t debounceNodelayMilliseconds = 0;
225 | uint16_t debounceDelayMilliseconds = 0;
226 | uint16_t panelDebounceMicroseconds = 4000;
227 | uint8_t autoCalibrationMaxDeviation = 100;
228 | uint8_t badSensorMinimumDelaySeconds = 15;
229 | uint16_t autoCalibrationAveragesPerUpdate = 60;
230 | uint16_t autoCalibrationSamplesPerAverage = 500;
231 |
232 | // The maximum tare value to calibrate to (except on startup).
233 | uint16_t autoCalibrationMaxTare = 0xFFFF;
234 |
235 | // Which sensors on each panel to enable. This can be used to disable sensors that
236 | // we know aren't populated. This is packed, with four sensors on two pads per byte:
237 | // enabledSensors[0] & 1 is the first sensor on the first pad, and so on.
238 | uint8_t enabledSensors[5];
239 |
240 | // How long the master controller will wait for a lights command before assuming the
241 | // game has gone away and resume auto-lights. This is in 128ms units.
242 | uint8_t autoLightsTimeout = 1000/128; // 1 second
243 |
244 | // The color to use for each panel when auto-lighting in master mode. This doesn't
245 | // apply when the pads are in autonomous lighting mode (no master), since they don't
246 | // store any configuration by themselves. These colors should be scaled to the 0-170
247 | // range.
248 | uint8_t stepColor[3*9];
249 |
250 | // The default color to set the platform LED strip to.
251 | uint8_t platformStripColor[3];
252 |
253 | // Which panels to enable auto-lighting for. Disabled panels will be unlit.
254 | // 0x01 = panel 0, 0x02 = panel 1, 0x04 = panel 2, etc. This only affects
255 | // the master controller's built-in auto lighting and not lights data send
256 | // from the SDK.
257 | uint16_t autoLightPanelMask = 0xFFFF;
258 |
259 | // The rotation of the panel, where 0 is the standard rotation, 1 means the panel is
260 | // rotated right 90 degrees, 2 is rotated 180 degrees, and 3 is rotated 270 degrees.
261 | // This value is unused.
262 | uint8_t panelRotation;
263 |
264 | // Per-panel sensor settings:
265 | packed_sensor_settings_t panelSettings[9];
266 |
267 | // These are internal tunables and should be left unchanged.
268 | uint8_t preDetailsDelayMilliseconds = 5;
269 |
270 | // Pad the struct to 250 bytes. This keeps this struct size from changing
271 | // as we add fields, so the ABI doesn't change. Applications should leave
272 | // any data in here unchanged when calling SMX_SetConfig.
273 | uint8_t padding[49];
274 | };
275 | #pragma pack(pop)
276 |
277 | static_assert(offsetof(SMXConfig, padding) == 201, "Incorrect padding alignment");
278 | static_assert(sizeof(SMXConfig) == 250, "Expected 250 bytes");
279 |
280 | // The values (except for Off) correspond with the protocol and must not be changed.
281 | enum SensorTestMode {
282 | SensorTestMode_Off = 0,
283 | // Return the raw, uncalibrated value of each sensor.
284 | SensorTestMode_UncalibratedValues = '0',
285 |
286 | // Return the calibrated value of each sensor.
287 | SensorTestMode_CalibratedValues = '1',
288 |
289 | // Return the sensor noise value.
290 | SensorTestMode_Noise = '2',
291 |
292 | // Return the sensor tare value.
293 | SensorTestMode_Tare = '3',
294 | };
295 |
296 | // Data for the current SensorTestMode. The interpretation of sensorLevel depends on the mode.
297 | struct SMXSensorTestModeData
298 | {
299 | // If false, sensorLevel[n][*] is zero because we didn't receive a response from that panel.
300 | bool bHaveDataFromPanel[9];
301 |
302 | int16_t sensorLevel[9][4];
303 | bool bBadSensorInput[9][4];
304 |
305 | // The DIP switch settings on each panel. This is used for diagnostics displays.
306 | int iDIPSwitchPerPanel[9];
307 |
308 | // Bad sensor selection jumper indication for each panel.
309 | bool iBadJumper[9][4];
310 | };
311 |
312 | // The values also correspond with the protocol and must not be changed.
313 | // These are panel-side diagnostics modes.
314 | enum PanelTestMode {
315 | PanelTestMode_Off = '0',
316 | PanelTestMode_PressureTest = '1',
317 | };
318 |
319 | #endif
320 |
--------------------------------------------------------------------------------
/sdk/Windows/SMXGif.cpp:
--------------------------------------------------------------------------------
1 | #include "SMXGif.h"
2 | #include
3 | #include
4 | #include
5 | using namespace std;
6 |
7 | // This is a simple animated GIF decoder. It always decodes to RGBA color,
8 | // discarding palettes, and decodes the whole file at once.
9 |
10 | class GIFError: public exception { };
11 |
12 | struct Palette
13 | {
14 | SMXGif::Color color[256];
15 | };
16 |
17 | void SMXGif::GIFImage::Init(int width_, int height_)
18 | {
19 | width = width_;
20 | height = height_;
21 | image.resize(width * height);
22 | }
23 |
24 | void SMXGif::GIFImage::Clear(const Color &color)
25 | {
26 | for(int y = 0; y < height; ++y)
27 | for(int x = 0; x < width; ++x)
28 | get(x,y) = color;
29 | }
30 |
31 | void SMXGif::GIFImage::CropImage(SMXGif::GIFImage &dst, int crop_left, int crop_top, int crop_width, int crop_height) const
32 | {
33 | dst.Init(crop_width, crop_height);
34 |
35 | for(int y = 0; y < crop_height; ++y)
36 | {
37 | for(int x = 0; x < crop_width; ++x)
38 | dst.get(x,y) = get(x + crop_left, y + crop_top);
39 | }
40 | }
41 |
42 | void SMXGif::GIFImage::Blit(SMXGif::GIFImage &src, int dst_left, int dst_top, int dst_width, int dst_height)
43 | {
44 | for(int y = 0; y < dst_height; ++y)
45 | {
46 | for(int x = 0; x < dst_width; ++x)
47 | get(x + dst_left, y + dst_top) = src.get(x, y);
48 | }
49 | }
50 | bool SMXGif::GIFImage::operator==(const GIFImage &rhs) const
51 | {
52 | return
53 | width == rhs.width &&
54 | height == rhs.height &&
55 | image == rhs.image;
56 | }
57 |
58 | class DataStream
59 | {
60 | public:
61 | DataStream(const string &data_):
62 | data(data_)
63 | {
64 | }
65 |
66 | uint8_t ReadByte()
67 | {
68 | if(pos >= data.size())
69 | throw GIFError();
70 |
71 | uint8_t result = data[pos];
72 | pos++;
73 | return result;
74 | }
75 |
76 | uint16_t ReadLE16()
77 | {
78 | uint8_t byte1 = ReadByte();
79 | uint8_t byte2 = ReadByte();
80 | return byte1 | (byte2 << 8);
81 | }
82 |
83 | void ReadBytes(string &s, int count)
84 | {
85 | s.clear();
86 | while(count--)
87 | s.push_back(ReadByte());
88 | }
89 |
90 | void skip(int bytes)
91 | {
92 | pos += bytes;
93 | }
94 |
95 | private:
96 | const string &data;
97 | uint32_t pos = 0;
98 | };
99 |
100 | class LWZStream
101 | {
102 | public:
103 | LWZStream(DataStream &stream_):
104 | stream(stream_)
105 | {
106 | }
107 |
108 | // Read one LZW code from the input data.
109 | uint32_t ReadLZWCode(uint32_t bit_count)
110 | {
111 | while(bits_in_buffer < bit_count)
112 | {
113 | if(bytes_remaining == 0)
114 | {
115 | // Read the next block's byte count.
116 | bytes_remaining = stream.ReadByte();
117 | if(bytes_remaining == 0)
118 | throw GIFError();
119 | }
120 |
121 | // Shift in another 8 bits into the end of self.bits.
122 | bits |= stream.ReadByte() << bits_in_buffer;
123 | bits_in_buffer += 8;
124 | bytes_remaining -= 1;
125 | }
126 |
127 | // Shift out bit_count worth of data from the end.
128 | uint32_t result = bits & ((1 << bit_count) - 1);
129 | bits >>= bit_count;
130 | bits_in_buffer -= bit_count;
131 |
132 | return result;
133 | }
134 |
135 | // Skip the rest of the LZW data.
136 | void Flush()
137 | {
138 | stream.skip(bytes_remaining);
139 | bytes_remaining = 0;
140 |
141 | // If there are any blocks past the end of data, skip them.
142 | while(1)
143 | {
144 | uint8_t blocksize = stream.ReadByte();
145 | if(blocksize == 0)
146 | break;
147 | stream.skip(blocksize);
148 | }
149 | }
150 |
151 | private:
152 | DataStream &stream;
153 | uint32_t bits = 0;
154 | int bytes_remaining = 0;
155 | int bits_in_buffer = 0;
156 | };
157 |
158 | struct LWZDecoder
159 | {
160 | LWZDecoder(DataStream &stream):
161 | lzw_stream(LWZStream(stream))
162 | {
163 | // Each frame has a single bits field.
164 | code_bits = stream.ReadByte();
165 | }
166 |
167 | string DecodeImage();
168 |
169 | private:
170 | uint16_t code_bits;
171 | LWZStream lzw_stream;
172 | };
173 |
174 |
175 | static const int GIFBITS = 12;
176 |
177 | string LWZDecoder::DecodeImage()
178 | {
179 | uint32_t dictionary_bits = code_bits + 1;
180 | int prev_code1 = -1;
181 | int prev_code2 = -1;
182 |
183 | uint32_t clear = 1 << code_bits;
184 | uint32_t end = clear + 1;
185 | uint32_t next_free_slot = clear + 2;
186 |
187 | vector> dictionary;
188 | dictionary.resize(1 << GIFBITS);
189 |
190 | // We append to this buffer as we decode data, then append the data in reverse
191 | // order.
192 | string append_buffer;
193 |
194 | string result;
195 | while(1)
196 | {
197 | // Flush append_buffer.
198 | for(int i = append_buffer.size() - 1; i >= 0; --i)
199 | result.push_back(append_buffer[i]);
200 | append_buffer.clear();
201 |
202 | int code1 = lzw_stream.ReadLZWCode(dictionary_bits);
203 | // printf("%02x");
204 | if(code1 == end)
205 | break;
206 |
207 | if(code1 == clear)
208 | {
209 | // Clear the dictionary and reset.
210 | dictionary_bits = code_bits + 1;
211 | next_free_slot = clear + 2;
212 | prev_code1 = -1;
213 | prev_code2 = -1;
214 | continue;
215 | }
216 |
217 | int code2;
218 | if(code1 < next_free_slot)
219 | code2 = code1;
220 | else if(code1 == next_free_slot && prev_code2 != -1)
221 | {
222 | append_buffer.push_back(prev_code2);
223 | code2 = prev_code1;
224 | }
225 | else
226 | throw GIFError();
227 |
228 | // Walk through the linked list of codes in the dictionary and append.
229 | while(code2 >= clear + 2)
230 | {
231 | uint8_t append_char = dictionary[code2].first;
232 | code2 = dictionary[code2].second;
233 | append_buffer.push_back(append_char);
234 | }
235 | append_buffer.push_back(code2);
236 |
237 | // If we're already at the last free slot, the dictionary is full and can't be expanded.
238 | if(next_free_slot < (1 << dictionary_bits))
239 | {
240 | // If we have any free dictionary slots, save.
241 | if(prev_code1 != -1)
242 | {
243 | dictionary[next_free_slot] = make_pair(code2, prev_code1);
244 | next_free_slot += 1;
245 | }
246 | // If we've just filled the last dictionary slot, expand the dictionary size if possible.
247 | if(next_free_slot >= (1 << dictionary_bits) && dictionary_bits < GIFBITS)
248 | dictionary_bits += 1;
249 | }
250 |
251 | prev_code1 = code1;
252 | prev_code2 = code2;
253 | }
254 |
255 | // Skip any remaining data in this block.
256 | lzw_stream.Flush();
257 |
258 | return result;
259 | }
260 |
261 | struct GlobalGIFData
262 | {
263 | int width = 0, height = 0;
264 | int background_index = -1;
265 | bool use_transparency = false;
266 | int transparency_index = -1;
267 | int duration = 0;
268 | int disposal_method = 0;
269 | bool have_global_palette = false;
270 | Palette palette;
271 | };
272 |
273 | class GIFDecoder
274 | {
275 | public:
276 | GIFDecoder(DataStream &stream_):
277 | stream(stream_)
278 | {
279 | }
280 |
281 | void ReadAllFrames(vector &frames);
282 |
283 | private:
284 | bool ReadPacket(string &packet);
285 | Palette ReadPalette(int palette_size);
286 | void DecodeImage(GlobalGIFData global_data, SMXGif::GIFImage &out);
287 |
288 | DataStream &stream;
289 | SMXGif::GIFImage image;
290 | int frame;
291 | };
292 |
293 | // Read a palette with size colors.
294 | //
295 | // This is a simple string, with 4 RGBA bytes per color.
296 | Palette GIFDecoder::ReadPalette(int palette_size)
297 | {
298 | Palette result;
299 | for(int i = 0; i < palette_size; ++i)
300 | {
301 | result.color[i].color[0] = stream.ReadByte(); // R
302 | result.color[i].color[1] = stream.ReadByte(); // G
303 | result.color[i].color[2] = stream.ReadByte(); // B
304 | result.color[i].color[3] = 0xFF;
305 | }
306 | return result;
307 | }
308 |
309 | bool GIFDecoder::ReadPacket(string &packet)
310 | {
311 | uint8_t packet_size = stream.ReadByte();
312 | if(packet_size == 0)
313 | return false;
314 |
315 | stream.ReadBytes(packet, packet_size);
316 | return true;
317 | }
318 |
319 | void GIFDecoder::ReadAllFrames(vector &frames)
320 | {
321 | string header;
322 | stream.ReadBytes(header, 6);
323 |
324 | if(header != "GIF87a" && header != "GIF89a")
325 | throw GIFError();
326 |
327 | GlobalGIFData global_data;
328 |
329 | global_data.width = stream.ReadLE16();
330 | global_data.height = stream.ReadLE16();
331 | image.Init(global_data.width, global_data.height);
332 |
333 | // Ignore the aspect ratio field. (Supporting pixel aspect ratios in a format
334 | // this rudimentary was almost ambitious of them...)
335 | uint8_t global_flags = stream.ReadByte();
336 | global_data.background_index = stream.ReadByte();
337 |
338 | // Ignore the aspect ratio field. (Supporting pixel aspect ratios in a format
339 | // this rudimentary was almost ambitious of them...)
340 | stream.ReadByte();
341 |
342 | // Decode global_flags.
343 | uint8_t global_palette_size = global_flags & 0x7;
344 |
345 | global_data.have_global_palette = (global_flags >> 7) & 0x1;
346 |
347 |
348 | // If there's no global palette, leave it empty.
349 | if(global_data.have_global_palette)
350 | global_data.palette = ReadPalette(1 << (global_palette_size + 1));
351 |
352 | frame = 0;
353 |
354 | // Save a copy of global data, so we can restore it after each frame.
355 | GlobalGIFData saved_global_data = global_data;
356 |
357 | // Decode all packets.
358 | while(1)
359 | {
360 | uint8_t packet_type = stream.ReadByte();
361 | if(packet_type == 0x21)
362 | {
363 | // Extension packet
364 | uint8_t extension_type = stream.ReadByte();
365 |
366 | if(extension_type == 0xF9)
367 | {
368 | string packet;
369 | if(!ReadPacket(packet))
370 | throw GIFError();
371 |
372 | DataStream packet_buf(packet);
373 |
374 | // Graphics control extension
375 | uint8_t gce_flags = packet_buf.ReadByte();
376 | global_data.duration = packet_buf.ReadLE16();
377 | global_data.transparency_index = packet_buf.ReadByte();
378 |
379 | global_data.use_transparency = bool(gce_flags & 1);
380 | global_data.disposal_method = (gce_flags >> 2) & 0xF;
381 | if(!global_data.use_transparency)
382 | global_data.transparency_index = -1;
383 | }
384 |
385 | // Read any remaining packets in this extension packet.
386 | while(1)
387 | {
388 | string packet;
389 | if(!ReadPacket(packet))
390 | break;
391 | }
392 | }
393 | else if(packet_type == 0x2C)
394 | {
395 | // Image data
396 | SMXGif::GIFImage frame_image;
397 | DecodeImage(global_data, frame_image);
398 |
399 | SMXGif::SMXGifFrame gif_frame;
400 | gif_frame.width = global_data.width;
401 | gif_frame.height = global_data.height;
402 | gif_frame.milliseconds = global_data.duration * 10;
403 | gif_frame.frame = frame_image;
404 |
405 | // If this frame is identical to the previous one, just extend the previous frame.
406 | if(!frames.empty() && gif_frame.frame == frames.back().frame)
407 | {
408 | frames.back().milliseconds += gif_frame.milliseconds;
409 | continue;
410 | }
411 |
412 | frames.push_back(gif_frame);
413 |
414 | frame++;
415 |
416 | // Reset GCE (frame-specific) data.
417 | global_data = saved_global_data;
418 | }
419 | else if(packet_type == 0x3B)
420 | {
421 | // EOF
422 | return;
423 | }
424 | else
425 | throw GIFError();
426 | }
427 | }
428 |
429 | // Decode a single GIF image into out, leaving this->image ready for
430 | // the next frame (with this frame's dispose applied).
431 | void GIFDecoder::DecodeImage(GlobalGIFData global_data, SMXGif::GIFImage &out)
432 | {
433 | uint16_t block_left = stream.ReadLE16();
434 | uint16_t block_top = stream.ReadLE16();
435 | uint16_t block_width = stream.ReadLE16();
436 | uint16_t block_height = stream.ReadLE16();
437 | uint8_t local_flags = stream.ReadByte();
438 |
439 | // area = (block_left, block_top, block_left + block_width, block_top + block_height)
440 | // Extract flags:
441 | uint8_t have_local_palette = (local_flags >> 7) & 1;
442 | // bool interlaced = (local_flags >> 6) & 1;
443 | uint8_t local_palette_size = (local_flags >> 0) & 0x7;
444 | // print 'Interlaced:', interlaced
445 |
446 | // We don't support interlaced GIFs right now.
447 | // assert interlaced == 0
448 |
449 | // If this frame has a local palette, use it. Otherwise, use the global palette.
450 | Palette active_palette = global_data.palette;
451 | if(have_local_palette)
452 | active_palette = ReadPalette(1 << (local_palette_size + 1));
453 |
454 | if(!global_data.have_global_palette && !have_local_palette)
455 | {
456 | // We have no palette. This is an invalid file.
457 | throw GIFError();
458 | }
459 |
460 | if(frame == 0)
461 | {
462 | // On the first frame, clear the buffer. If we have a transparency index,
463 | // clear to transparent. Otherwise, clear to the background color.
464 | if(global_data.transparency_index != -1)
465 | image.Clear(SMXGif::Color(0,0,0,0));
466 | else
467 | image.Clear(active_palette.color[global_data.background_index]);
468 | }
469 |
470 | // Decode the compressed image data.
471 | LWZDecoder decoder(stream);
472 | string decompressed_data = decoder.DecodeImage();
473 |
474 | if(decompressed_data.size() < block_width*block_height)
475 | throw GIFError();
476 |
477 | // Save the region to restore after decoding.
478 | SMXGif::GIFImage dispose;
479 | if(global_data.disposal_method <= 1)
480 | {
481 | // No disposal.
482 | }
483 | else if(global_data.disposal_method == 2)
484 | {
485 | // Clear the region to a background color afterwards.
486 | dispose.Init(block_width, block_height);
487 |
488 | if(global_data.transparency_index != -1)
489 | dispose.Clear(SMXGif::Color(0,0,0,0));
490 | else
491 | {
492 | uint8_t palette_idx = global_data.background_index;
493 | dispose.Clear(active_palette.color[palette_idx]);
494 | }
495 |
496 | }
497 | else if(global_data.disposal_method == 3)
498 | {
499 | // Restore the previous frame afterwards.
500 | image.CropImage(dispose, block_left, block_top, block_width, block_height);
501 | }
502 | else
503 | {
504 | // Unknown disposal method
505 | }
506 |
507 | int pos = 0;
508 | for(int y = block_top; y < block_top + block_height; ++y)
509 | {
510 | for(int x = block_left; x < block_left + block_width; ++x)
511 | {
512 | uint8_t palette_idx = decompressed_data[pos];
513 | pos++;
514 |
515 | if(palette_idx == global_data.transparency_index)
516 | {
517 | // If this pixel is transparent, leave the existing color in place.
518 | }
519 | else
520 | {
521 | image.get(x,y) = active_palette.color[palette_idx];
522 | }
523 | }
524 | }
525 |
526 | // Copy the image before we run dispose.
527 | out = image;
528 |
529 | // Restore the dispose area.
530 | if(dispose.width != 0)
531 | image.Blit(dispose, block_left, block_top, block_width, block_height);
532 | }
533 |
534 | bool SMXGif::DecodeGIF(string buf, vector &frames)
535 | {
536 | DataStream stream(buf);
537 | GIFDecoder gif(stream);
538 | try {
539 | gif.ReadAllFrames(frames);
540 | } catch(GIFError &) {
541 | // We don't return error strings for this, just success or failure.
542 | return false;
543 | }
544 | return true;
545 | }
546 |
--------------------------------------------------------------------------------