-
Notifications
You must be signed in to change notification settings - Fork 2
/
SM5-Arduino-Lighting.ino
323 lines (234 loc) · 12.4 KB
/
SM5-Arduino-Lighting.ino
1
2
3
4
5
6
7
8
9
10
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
/*************************************
***** SM5 ARDUINO LIGHTS DRIVER *****
************************************/
// Get game-controlled cabinet and controller/pad lights in StepMania 5!
// All you need is any kind of Arduino that can connect to a PC via serial and you're good to go!
// (a.k.a. use pretty much any Arduino - Uno, mega, micro, etc)
// Written to be easy to understand/modify by 48productions
/*********************
*** WIRING CONFIG ***
********************/
// There's two routes for wiring, with and without shift registers
// - Hooking up individual lights to individual arduino pins (the default)? Comment out the below #define line.
// - Using shift registers (use only 3 pins for all the lights)? Uncomment the below line
//#define USE_SHIFT_REGISTERS
//Don't touch this next line please kthx
#ifndef USE_SHIFT_REGISTERS
/************************
* WIRING PINS (direct) *
***********************/
// PINS - What pins should the Arduino use?
// IF USING DIRECT LIGHTING MODE (#define USE_SHIFT_REGISTERS is COMMENTED OUT)
// Set which lights are output to what pins, here!
//P1 pad light pins
#define PIN_P1_LEFT 2 //P1 Left
#define PIN_P1_RIGHT 3 //P1 Right
#define PIN_P1_UP 4 //P1 Up
#define PIN_P1_DOWN 5 //P1 Down
// P2 pad light pins
#define PIN_P2_LEFT 6 //P2 Left
#define PIN_P2_RIGHT 7 //P2 Right
#define PIN_P2_UP 8 //P2 Up
#define PIN_P2_DOWN 9 //P2 Down
// Cab light pins
#define PIN_MARQUEE_1 A0 //Marquee up-left
#define PIN_MARQUEE_2 A1 //Marquee up-right
#define PIN_MARQUEE_3 A2 //Marquee down-left
#define PIN_MARQUEE_4 A3 //Marquee down-right
#define PIN_BASS A4 //Bass left/right
// Menu light pins
#define PIN_P1_START 10 //P1 start button
#define PIN_P1_MENU 11 //P1 left/right menu buttons
#define PIN_P2_START 12 //P2 start button
#define PIN_P2_MENU 13 //P2 left/right menu buttons
/********************************
* WIRING PINS (shift register) *
*******************************/
// IF USING SHIFT REGISTER LIGHTING (#define USE_SHIFT_REGISTERS is NOT commented)
// Set which pins you'll hook up your shift registers to here.
// That's right, only 3 pins for EVERY light!
#else
#define PIN_SHIFT_CLOCK 2
#define PIN_SHIFT_DATA 3
#define PIN_SHIFT_LATCH 4
#endif
/*****************
*** MAIN CODE ***
****************/
// First, define variables:
// (Each byte contains 8 bits, individual lights are tracked with individual bits)
byte p1LEDs = 0; //Tracks P1's lights (SM gameplay buttons 0-7)
byte p2LEDs = 0; //Tracks P2's lights (SM gameplay buttons 0-7)
byte cabLEDs = 0; //Tracks cabinet lighting. 0-3: Marquee lights, 4: P1 Start, 5: P1 Menu, 6: P2 Start, 7: P2 Menu
byte etcLEDs = 0; //Tracks other lights. 0: Bass neon. All other slots unused - if you want to output more lights from SM, here's a good spot.
byte receivedData = 0; //The last byte of data we received from Stepmania
short lightBytePos = 0; //Tracks how many bytes of lighting data we've received this update
// setup() - Run once on startup
void setup() {
Serial.begin(115200);
//Pinmode ALL the pins!
// ... except the ones we don't need
#ifdef USE_SHIFT_REGISTERS // If using shift registers, only pinmode the pins needed for shift registers
pinMode(PIN_SHIFT_CLOCK, OUTPUT);
pinMode(PIN_SHIFT_DATA, OUTPUT);
pinMode(PIN_SHIFT_LATCH, OUTPUT);
#else // In direct lighting mode? Only pinmode what's needed here
//P1 pad lights
pinMode(PIN_P1_LEFT, OUTPUT);
pinMode(PIN_P1_RIGHT, OUTPUT);
pinMode(PIN_P1_UP, OUTPUT);
pinMode(PIN_P1_DOWN, OUTPUT);
//P2 pad lights
pinMode(PIN_P2_LEFT, OUTPUT);
pinMode(PIN_P2_RIGHT, OUTPUT);
pinMode(PIN_P2_UP, OUTPUT);
pinMode(PIN_P2_DOWN, OUTPUT);
//Cabinet lights
pinMode(PIN_MARQUEE_1, OUTPUT);
pinMode(PIN_MARQUEE_2, OUTPUT);
pinMode(PIN_MARQUEE_3, OUTPUT);
pinMode(PIN_MARQUEE_4, OUTPUT);
pinMode(PIN_P1_START, OUTPUT);
pinMode(PIN_P1_MENU, OUTPUT);
pinMode(PIN_P2_START, OUTPUT);
pinMode(PIN_P2_MENU, OUTPUT);
pinMode(PIN_BASS, OUTPUT);
#endif
}
// loop() - runs over and over again
void loop() {
if (Serial.available() > 0) { //Do we have lights data to receive?
readSerialLightingData();
}
//Done receiving serial data this loop
}
//Writes SM lighting directly to arduino pins
// Called whenever new lighting data is received, if not using shift registers
#ifndef USE_SHIFT_REGISTERS //This function is only compiled if we're not in shift register mode
void writeDirectLighting() {
// SIDENOTE: Want to make mods to change which specific lights from SM are output to the Arduino pins?
// Or: Have an Arduino Mega and want to add more lights?
// This is the place for that!
//Data is placed into p1LEDs/p2LEDs/cabLEDs/etcLEDs when we read lighting data from Stepmania in "void loop()"
//We'll read specific lights from that data here, and turn on/off arduino pins based on them
digitalWrite(PIN_P1_LEFT, bitRead(p1LEDs, 0)); //P1 Pad
digitalWrite(PIN_P1_RIGHT, bitRead(p1LEDs, 1));
digitalWrite(PIN_P1_UP, bitRead(p1LEDs, 2));
digitalWrite(PIN_P1_DOWN, bitRead(p1LEDs, 3));
digitalWrite(PIN_P2_LEFT, bitRead(p2LEDs, 0)); //P2 pad
digitalWrite(PIN_P2_RIGHT, bitRead(p2LEDs, 1));
digitalWrite(PIN_P2_UP, bitRead(p2LEDs, 2));
digitalWrite(PIN_P2_DOWN, bitRead(p2LEDs, 3));
digitalWrite(PIN_MARQUEE_1, bitRead(cabLEDs, 0)); //Cabinet lights
digitalWrite(PIN_MARQUEE_2, bitRead(cabLEDs, 1));
digitalWrite(PIN_MARQUEE_3, bitRead(cabLEDs, 2));
digitalWrite(PIN_MARQUEE_4, bitRead(cabLEDs, 3));
digitalWrite(PIN_P1_START, bitRead(cabLEDs, 4));
digitalWrite(PIN_P1_MENU, bitRead(cabLEDs, 5));
digitalWrite(PIN_P2_START, bitRead(cabLEDs, 6));
digitalWrite(PIN_P2_MENU, bitRead(cabLEDs, 7));
digitalWrite(PIN_BASS, bitRead(etcLEDs, 0));
}
#endif
//Writes SM lighting data to a set of shift registers
// Called whenever new lighting data is received, when using shift registers
#ifdef USE_SHIFT_REGISTERS //This function is only compiled if we're in shift register mode
void writeShiftRegisterLighting() {
digitalWrite(PIN_SHIFT_LATCH, LOW); //Pull latch pin low, shift out data, and throw latch pin high again
// Add more shiftOut lines here if you want to add more shift registers
// (the first shiftOut line goes to the last shift register in the chain)
shiftOut(PIN_SHIFT_DATA, PIN_SHIFT_CLOCK, MSBFIRST, etcLEDs);
shiftOut(PIN_SHIFT_DATA, PIN_SHIFT_CLOCK, MSBFIRST, cabLEDs);
shiftOut(PIN_SHIFT_DATA, PIN_SHIFT_CLOCK, MSBFIRST, p2LEDs);
shiftOut(PIN_SHIFT_DATA, PIN_SHIFT_CLOCK, MSBFIRST, p1LEDs);
digitalWrite(PIN_SHIFT_LATCH, HIGH);
}
#endif
//Reads sextetstream-formatted lighting data from serial when called
// Updates p1LEDs, p2LEDs, cabLEDs, and etcLEDs
void readSerialLightingData() {
while (Serial.available() > 0) { //While we have lights data to receive, receive and process it!
receivedData = Serial.read(); //Read the next byte of serial data
//Serial.println(receivedData);
// > Q&A: "What format does Stepmania send lighting updates in?"
// Each lighting update sent by Stepmania contains 13 bytes of data, and a newline (\n) character.
// We track how many of these bytes we've received with lightBytePos - every time we get a byte, we increment this number.
// If we receive a newline character, we know the update is done and can reset this counter to 0
// Knowing how much data we've received so far tells us what lights the next byte of data controls!
// (More technical info at https://github.com/stepmania/stepmania/blob/master/src/arch/Lights/LightsDriver_SextetStream.md )
if (receivedData == '\n') { //If we got a newline (\n), we're done receiving new light states for this update
lightBytePos = 0; //The next byte of lighting data will be the first byte
//When we're done processing the serial data we have right now, write it out!
#ifdef USE_SHIFT_REGISTERS
writeShiftRegisterLighting();
#else
writeDirectLighting();
#endif
} else {
/* *******************************************
* ** SHIFT REGISTER MAPPING CUSTOMIZATION: **
* *******************************************
*
* Each bitWrite() line sets an LED on a shift register.
*
* The case the line is placed in decides what byte of SextetStream data from StepMania we'll be reading (case 0 = Byte 0, case 1 = Byte 1, etc)
* (SextetStream data -> Light mappings are at https://github.com/stepmania/stepmania/blob/master/src/arch/Lights/LightsDriver_SextetStream.md#bit-meanings)
* The first bitWrite() argument is the shift register to write to (each shift register gets it's own byte of data)
* The second is the LED number on that register to set, 0-7
* The third is the value to set it to, HIGH or LOW (true or false). In this case, we bitRead() an individual bit, 0-6, of the current byte of SextetStream data.
*
* Example:
* bitWrite(cabLEDs, 5, bitRead(receivedData, 0));
* This writes to LED position 5 on the cabLEDs shift register.
* It's value is set to the 0th bit in the current receivedData byte (place this in case 0, and it'll use Byte 0 of SextetStream data).
*/
switch (lightBytePos) { //Which byte of lighting data are we now receiving?
case 0: //Lighting data byte 0: Cabinet lights
bitWrite(cabLEDs, 0, bitRead(receivedData, 0)); //Marquee up left
bitWrite(cabLEDs, 1, bitRead(receivedData, 1)); //Marquee up right
bitWrite(cabLEDs, 2, bitRead(receivedData, 2)); //Marquee down left
bitWrite(cabLEDs, 3, bitRead(receivedData, 3)); //Marquee down right
bitWrite(etcLEDs, 0, bitRead(receivedData, 4)); //Bass L
//bitWrite(etcLEDs, 1, bitRead(receivedData, 5)); //Bass R (unless a song's lighting chart says otherwise, this is always the same state as Bass L)
break;
case 1: //Byte 1: P1 menu button lights)
bitWrite(cabLEDs, 5, bitRead(receivedData, 0)); //P1 menu left (menu right almost always the same state, from what I can tell)
bitWrite(cabLEDs, 4, bitRead(receivedData, 4)); //P1 start
//bitWrite(etcLEDs, 2, bitRead(receivedData, 5)); //P1 select
break;
//Byte 2 is for more P1 cabinet button lights that we currently don't read
case 3: //Byte 3: P1 gameplay button lights 1-6
//The first 6 gameplay button lights are now in receivedData.
//For this and P2's gameplay lights, we'll copy receivedData directly to p1LEDs.
//BUT: We'll omit the highest 2 bytes of receivedData with "& 0x3F" (bitwise AND) since they don't contain lighting data.
//(To customize what lights are mapped on the p1LEDs/p2LEDs shift registers, remove the below line and replace it with bitWrite() lines, described above)
p1LEDs = receivedData & 0x3F;
break;
case 4: //Byte 4: P1 gameplay button lights 7-12
//I can almost certainly bitwise this to be more compact, like with case 3 above. Buuut, nah.
bitWrite(p1LEDs, 6, bitRead(receivedData, 0));
bitWrite(p1LEDs, 7, bitRead(receivedData, 1));
break;
//Bytes 5-6 are for more P1 gameplay buttons that we don't read
case 7: //Byte 7: P2 menu
bitWrite(cabLEDs, 7, bitRead(receivedData, 0)); //P2 menu left (menu right almost always the same state, from what I can tell)
bitWrite(cabLEDs, 6, bitRead(receivedData, 4)); //P2 start
//bitWrite(etcLEDs, 3, bitRead(receivedData, 5)); //P2 select
break;
//Byte 8 is for more P2 menu button lights that we don't read
case 9: //Byte 9: P2 gameplay button lights 1-6
//Same as P1's gameplay lights, copy the lower 6 bis of receivedData to p2LEDs (high 2 bits omitted with & 0x3F)
p2LEDs = receivedData & 0x3F;
break;
case 10: //Byte 10: P2 gameplay button lights 7-12
bitWrite(p2LEDs, 6, bitRead(receivedData, 0));
bitWrite(p2LEDs, 7, bitRead(receivedData, 1));
break;
//Bytes 11-12 are for more P2 gameplay buttons we don't read
}
lightBytePos++; //Finally, update how many bytes of lighting data we've received.
}
//Done receiving this byte of data!
}
//We've read all the serial data we can for now
}