SextetStream input and lights drivers.

This commit is contained in:
Peter S. May
2014-10-19 16:51:54 -04:00
parent 1f81c64c00
commit a991ae18a0
7 changed files with 1676 additions and 2 deletions
+4 -2
View File
@@ -237,10 +237,12 @@ Threads = arch/Threads/Threads.h
# ===================================
# (Potentially) multiplatform drivers
# ===================================
InputHandler += arch/InputHandler/InputHandler_MonkeyKeyboard.cpp arch/InputHandler/InputHandler_MonkeyKeyboard.h
InputHandler += arch/InputHandler/InputHandler_MonkeyKeyboard.cpp arch/InputHandler/InputHandler_MonkeyKeyboard.h \
arch/InputHandler/InputHandler_SextetStream.cpp arch/InputHandler/InputHandler_SextetStream.h
Lights += arch/Lights/LightsDriver_Export.cpp arch/Lights/LightsDriver_Export.h \
arch/Lights/LightsDriver_SystemMessage.cpp arch/Lights/LightsDriver_SystemMessage.h
arch/Lights/LightsDriver_SystemMessage.cpp arch/Lights/LightsDriver_SystemMessage.h \
arch/Lights/LightsDriver_SextetStream.cpp arch/Lights/LightsDriver_SextetStream.h
LoadingWindow += arch/LoadingWindow/LoadingWindow_Null.h
@@ -0,0 +1,446 @@
#include "global.h"
#include "InputHandler_SextetStream.h"
#include "PrefsManager.h"
#include "RageLog.h"
#include "RageThreads.h"
#include "RageUtil.h"
#include <cerrno>
#include <cstdio>
#include <cstring>
using namespace std;
// In so many words, ceil(n/6).
#define NUMBER_OF_SEXTETS_FOR_BIT_COUNT(n) (((n) + 5) / 6)
#define FIRST_DEVICE DEVICE_JOY1
#define FIRST_JOY_BUTTON JOY_BUTTON_1
#define LAST_JOY_BUTTON JOY_BUTTON_32
#define COUNT_JOY_BUTTON ((LAST_JOY_BUTTON) - (FIRST_JOY_BUTTON) + 1)
#define FIRST_KEY KEY_OTHER_0
#define LAST_KEY KEY_LAST_OTHER
#define COUNT_KEY ((LAST_KEY) - (FIRST_KEY) + 1)
#define BUTTON_COUNT (COUNT_JOY_BUTTON + COUNT_KEY)
#define DEFAULT_TIMEOUT_MS 1000
#define STATE_BUFFER_SIZE NUMBER_OF_SEXTETS_FOR_BIT_COUNT(BUTTON_COUNT)
namespace
{
class LineReader
{
protected:
int timeout_ms;
public:
LineReader()
{
timeout_ms = DEFAULT_TIMEOUT_MS;
}
virtual ~LineReader()
{
}
virtual bool IsValid()
{
return false;
}
// Ideally, this method should return if timeout_ms passes
// before a line becomes available. Actually doing this may
// require some platform-specific non-blocking read capability.
// This sort of thing could be implemented using e.g. POSIX
// select() and Windows GetOverlappedResultEx(), both of which
// have timeout parameters. (I get the sense that the
// RageFileDriverTimeout class could be convinced to work, but
// there isn't a lot of code using it, so I'm lacking the proper
// examples.)
//
// If this method does block, almost everything will still work,
// but the blocking may prevent the loop from checking
// continueInputThread in a timely fashion. If the stream ceases
// to produce new data before this object is destroyed, the
// current thread will hang until the other side of the
// connection closes the stream (or produces a line of data). A
// workaround for that would be to have the far side of the
// connection repeat its last line every second or so as a
// keepalive.
//
// false (line undefined) if there is an error or EOF condition,
// true (line = next line from stream) if a whole line is available,
// true (line = "") if no error but still waiting for next line.
virtual bool ReadLine(RString& line) = 0;
};
}
class InputHandler_SextetStream::Impl
{
private:
InputHandler_SextetStream * handler;
protected:
void ButtonPressed(const DeviceInput& di)
{
handler->ButtonPressed(di);
}
uint8_t stateBuffer[STATE_BUFFER_SIZE];
size_t timeout_ms;
RageThread inputThread;
bool continueInputThread;
// Construct and return the LineReader that makes sense for this
// object. getLineReader() calls this; if the returned object claims
// it is valid, it is returned. Otherwise, it is destroyed and NULL
// is returned.
virtual LineReader * getUnvalidatedLineReader() = 0;
inline void clearStateBuffer()
{
memset(stateBuffer, 0, STATE_BUFFER_SIZE);
}
inline void createThread()
{
continueInputThread = true;
inputThread.SetName("SextetStream input thread");
inputThread.Create(StartInputThread, this);
}
LineReader * getLineReader()
{
LineReader * linereader = getUnvalidatedLineReader();
if(linereader != NULL) {
if(!linereader->IsValid()) {
delete linereader;
linereader = NULL;
}
}
return linereader;
}
public:
Impl(InputHandler_SextetStream * _this)
{
LOG->Info("Number of button states supported by current InputHandler_SextetStream: %u",
(unsigned)BUTTON_COUNT);
continueInputThread = false;
timeout_ms = DEFAULT_TIMEOUT_MS;
handler = _this;
clearStateBuffer();
createThread();
}
virtual ~Impl()
{
if(inputThread.IsCreated()) {
continueInputThread = false;
inputThread.Wait();
}
}
virtual void GetDevicesAndDescriptions(vector<InputDeviceInfo>& vDevicesOut)
{
vDevicesOut.push_back(InputDeviceInfo(FIRST_DEVICE, "SextetStream"));
}
static int StartInputThread(void * p)
{
((Impl*) p)->RunInputThread();
return 0;
}
inline void GetNewState(uint8_t * buffer, RString& line)
{
size_t lineLen = line.length();
size_t i, cursor;
cursor = 0;
memset(buffer, 0, STATE_BUFFER_SIZE);
// Copy from line to buffer until either it is full or we've run out
// of characters. Characters outside the sextet code range
// (0x30..0x6F) are skipped; the remaining characters have their two
// high bits cleared.
for(i = 0; i < lineLen; ++i) {
char b = line[i];
if((b >= 0x30) && (b <= 0x6F)) {
buffer[cursor] = b & 0x3F;
++cursor;
if(cursor >= STATE_BUFFER_SIZE) {
break;
}
}
}
}
inline DeviceButton ButtonAtIndex(size_t index)
{
if(index < COUNT_JOY_BUTTON) {
return enum_add2(FIRST_JOY_BUTTON, index);
}
else if(index < COUNT_JOY_BUTTON + COUNT_KEY) {
return enum_add2(FIRST_KEY, index - COUNT_JOY_BUTTON);
}
else {
return DeviceButton_Invalid;
}
}
inline void ReactToChanges(const uint8_t * newStateBuffer)
{
InputDevice id = InputDevice(FIRST_DEVICE);
uint8_t changes[STATE_BUFFER_SIZE];
RageTimer now;
// XOR to find differences
for(size_t i = 0; i < STATE_BUFFER_SIZE; ++i) {
changes[i] = stateBuffer[i] ^ newStateBuffer[i];
}
// Report on changes
for(size_t m = 0; m < STATE_BUFFER_SIZE; ++m) {
for(size_t n = 0; n < 6; ++n) {
size_t bi = (m * 6) + n;
if(bi < BUTTON_COUNT) {
if(changes[m] & (1 << n)) {
bool value = newStateBuffer[m] & (1 << n);
LOG->Trace("SS button index %zu %s", bi, value ? "pressed" : "released");
DeviceInput di = DeviceInput(id, ButtonAtIndex(bi), value, now);
ButtonPressed(di);
}
}
}
}
// Update current state
memcpy(stateBuffer, newStateBuffer, STATE_BUFFER_SIZE);
}
void RunInputThread()
{
RString line;
LineReader * linereader;
LOG->Trace("Input thread started; getting line reader");
linereader = getLineReader();
if(linereader == NULL) {
LOG->Warn("Could not open line reader for SextetStream input");
}
else {
LOG->Trace("Got line reader");
while(continueInputThread) {
LOG->Trace("Reading line");
if(linereader->ReadLine(line)) {
LOG->Trace("Got line: '%s'", line.c_str());
if(line.length() > 0) {
uint8_t newStateBuffer[STATE_BUFFER_SIZE];
GetNewState(newStateBuffer, line);
ReactToChanges(newStateBuffer);
}
}
else {
// Error or EOF condition.
LOG->Trace("Reached end of SextetStream input");
continueInputThread = false;
}
}
LOG->Info("SextetStream input stopped");
delete linereader;
}
}
};
void InputHandler_SextetStream::GetDevicesAndDescriptions(vector<InputDeviceInfo>& vDevicesOut)
{
if(_impl != NULL) {
_impl->GetDevicesAndDescriptions(vDevicesOut);
}
}
InputHandler_SextetStream::InputHandler_SextetStream()
{
_impl = NULL;
}
InputHandler_SextetStream::~InputHandler_SextetStream()
{
if(_impl != NULL) {
delete _impl;
}
}
// SextetStreamFromFile
REGISTER_INPUT_HANDLER_CLASS (SextetStreamFromFile);
#if defined(_WINDOWS)
#define DEFAULT_INPUT_FILENAME "\\\\.\\pipe\\StepMania-Input-SextetStream"
#else
#define DEFAULT_INPUT_FILENAME "Data/StepMania-Input-SextetStream.in"
#endif
static Preference<RString> g_sSextetStreamInputFilename("SextetStreamInputFilename", DEFAULT_INPUT_FILENAME);
namespace
{
class StdCFileLineReader: public LineReader
{
private:
// The buffer size isn't critical; the RString will simply be
// extended until the line is done.
static const size_t BUFFER_SIZE = 64;
char buffer[BUFFER_SIZE];
protected:
std::FILE * file;
public:
StdCFileLineReader(std::FILE * file)
{
LOG->Info("Starting InputHandler_SextetStreamFromFile from open std::FILE");
this->file = file;
}
StdCFileLineReader(const RString& filename)
{
LOG->Info("Starting InputHandler_SextetStreamFromFile from std::FILE with filename '%s'",
filename.c_str());
file = std::fopen(filename.c_str(), "rb");
if(file == NULL) {
LOG->Warn("Error opening file '%s' for input (cstdio): %s", filename.c_str(),
std::strerror(errno));
}
else {
LOG->Info("File opened");
// Disable buffering on the file
std::setbuf(file, NULL);
}
}
~StdCFileLineReader()
{
if(file != NULL) {
std::fclose(file);
}
}
virtual bool IsValid()
{
return file != NULL;
}
virtual bool ReadLine(RString& line)
{
bool afterFirst = false;
size_t len;
line = "";
if(file != NULL) {
while(fgets(buffer, BUFFER_SIZE, file) != NULL) {
afterFirst = true;
line += buffer;
len = line.length();
if(len > 0 && line[len - 1] == 0xA) {
break;
}
}
}
return afterFirst;
}
};
class StdCFileHandleImpl: public InputHandler_SextetStream::Impl
{
protected:
std::FILE * file;
public:
StdCFileHandleImpl(InputHandler_SextetStreamFromFile * handler, std::FILE * file) :
InputHandler_SextetStream::Impl(handler)
{
this->file = file;
}
virtual LineReader * getUnvalidatedLineReader()
{
return new StdCFileLineReader(this->file);
}
virtual ~StdCFileHandleImpl()
{
// line reader dtor will close file for us
}
};
class StdCFileNameImpl: public InputHandler_SextetStream::Impl
{
protected:
RString filename;
public:
StdCFileNameImpl(InputHandler_SextetStreamFromFile * handler, const RString& filename) :
InputHandler_SextetStream::Impl(handler)
{
this->filename = filename;
}
virtual LineReader * getUnvalidatedLineReader()
{
return new StdCFileLineReader(filename);
}
virtual ~StdCFileNameImpl()
{
// Nothing to destroy
}
};
}
InputHandler_SextetStreamFromFile::InputHandler_SextetStreamFromFile(FILE * file)
{
_impl = new StdCFileHandleImpl(this, file);
}
InputHandler_SextetStreamFromFile::InputHandler_SextetStreamFromFile(const RString& filename)
{
_impl = new StdCFileNameImpl(this, filename);
}
InputHandler_SextetStreamFromFile::InputHandler_SextetStreamFromFile()
{
_impl = new StdCFileNameImpl(this, g_sSextetStreamInputFilename);
}
/*
* Copyright © 2014 Peter S. May
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit
* persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
@@ -0,0 +1,70 @@
#ifndef INPUT_HANDLER_SEXTETSTREAM
#define INPUT_HANDLER_SEXTETSTREAM
#include "InputHandler.h"
#include <cstdio>
class InputHandler_SextetStream: public InputHandler
{
public:
InputHandler_SextetStream();
virtual ~InputHandler_SextetStream();
//virtual void Update();
virtual void GetDevicesAndDescriptions(vector<InputDeviceInfo>& vDevicesOut);
public:
class Impl;
protected:
Impl * _impl;
};
// Note: InputHandler_SextetStreamFromFile uses blocking I/O. For the
// handler thread to close in a timely fashion, the producer of data for the
// file (e.g. the program at the other end of the pipe) must either close
// the file or output and flush a line of data no less often than about once
// per second, even if there has been no change. (Repeating the most recent
// state accomplishes this without triggering any new events.) Either of
// these interrupts the blocking read so that the loop can check its
// continue flag.
class InputHandler_SextetStreamFromFile: public InputHandler_SextetStream
{
public:
// Note: In the current implementation, the filename (either the
// `filename` parameter or the `SextetStreamInputFilename` setting) is
// passed to fopen(), not a RageFile ctor, so specify the file to be
// opened on the actual filesystem instead of the mapped filesystem. (I
// couldn't get RageFile to work here, possibly because I haven't
// determined how to disable buffering on an input file.)
InputHandler_SextetStreamFromFile();
InputHandler_SextetStreamFromFile(const RString& filename);
// The file object passed here must already be open and buffering should
// be disabled. The file object will be closed in the destructor.
InputHandler_SextetStreamFromFile(std::FILE * file);
};
#endif
/*
* Copyright © 2014 Peter S. May
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit
* persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
@@ -0,0 +1,383 @@
`InputHandler_SextetStream_*`
=============================
Explanation
-----------
This is a set of drivers (currently, just one driver) that accept button
inputs encoded in a stream of character. By accepting this data from a
stream, input can be produced by a separate program. Such a program can
be implemented using any language/platform that supports reading from
the desired input device (and writing to an output stream). If C++ isn't
your thing, or if learning the guts of StepMania seems a little much
just to implement an input driver, you're in the right place.
Quick start
-----------
You'll need a working StepMania build with the driver
`InputHandler_SextetStreamFromFile` enabled.
For this test, you'll run a test input program to verify that the driver
is set up properly. This is a simple example of an *input program*, a
program that will receive input from some arbitrary input source, encode
the button states, and produce output to be read by StepMania. Because
this script is for testing and diagnostics, input is accepted as
keypresses on a GUI window. Actual input programs produce output in the
same way, but will read input from different sources, such as data from
a hardware interface (serial/parallel/USB).
### Set up `SextetStreamStdoutTest.jar`
You'll need the test input program,
[`SextetStreamStdoutTest.jar`](https://github.com/psmay/SextetStreamStdoutTest/releases),
as well as a working Java VM to run it. To ensure that the current
environment is correct, simply run the program:
java -jar SextetStreamStdoutTest.jar
A GUI window should open. Click on this window and then try pressing
some keys. When one of these keys is pressed, its code (number) appears;
when released, the code disappears.
The output of this program on your console is the encoded sextets
representing the state of the pressed keys. A new output line is
produced immediately when a button is either pressed or released. An
output line is also produced periodically (about once per second) if
there have been no recent changes so that the driver is never left
blocking for too long an interval.
Each output line is only as long as necessary to express the current
state. While nothing is pressed, the output should be this line, about
once per second:
@
Press and hold the up arrow. The output should be this line, again about
once per second:
@@@@@@D
If instead you get a rapidly scrolling alternation like
@@@@@@D
@
@@@@@@D
@
@@@@@@D
@
@@@@@@D
@
you should use your OS or window system settings to disable key repeat,
at least for the duration of the test, to avoid an undesired rapid-fire
effect.
### Linux
We assume that `SextetStreamStdoutTest.jar` is working (as outlined
above) and that `$SM` is the root StepMania directory.
In `Preferences.ini`, set:
InputDrivers=X11,SextetStreamFromFile
SextetStreamInputFilename=Data/StepMania-Input-SextetStream.in
(This also keeps X11-based keyboard input enabled. The X11 part can be
removed later after setting up input mappings, if desired.)
Create the FIFO:
mkfifo "$SM/Data/StepMania-Input-SextetStream.in"
Run the test input program using the FIFO as output:
java -jar SextetStreamStdoutTest.jar > "$SM/Data/StepMania-Input-SextetStream.in"
While the input program is running, start StepMania. (While using the
test program, use windowed mode to keep both StepMania and the input
program visible.) Using the keyboard directly on the StepMania window,
go to Options, Test Input. Switch to the test input program and try
pressing some keys. If StepMania displays corresponding messages, the
driver is working properly.
#### Serial port under Linux
FIXME: *This has not been tested at all.* The following is a guess at a
synopsis of how it should work if you're lucky enough for everything to
have fallen into place just so. (Please amend this message if you manage
to get this running consistently.)
If you have a microcontroller running firmware that produces output
compatible with the SextetStream protocol, you can use `socat` as the
input program to pipe the data directly from the device via a serial
port:
socat /dev/ttyUSB0,raw,echo=0,b115200 "$SM/Data/StepMania-Input-SextetStream.in"
Substitute your actual serial port device for `/dev/ttyUSB0` and the
actual port speed for `115200`. (A low speed will not freeze StepMania,
but may introduce an unacceptable input latency.)
TODO: Supply example Arduino/PIC/... firmware.
TODO: Supply example of full-duplex in cooperation with the SextetStream
lights driver.
### Mac OS X
FIXME: *This has not been tested at all.* The following is a guess at a
synopsis of how it should work if you're lucky enough for everything to
have fallen into place just so. (Please amend this message if you manage
to get this running consistently on Mac OS X.)
Follow the instructions for Linux, but change the `InputDrivers` setting
to:
InputDrivers=HID,SextetStreamFromFile
(This also keeps the default HID-based input enabled. The HID part can
be removed later after setting up input mappings, if desired.)
TODO: Serial examples.
### Windows
FIXME: *This has not been tested at all.* The following is a guess at a
synopsis of how it should work if you're lucky enough for everything to
have fallen into place just so. (Please amend this message if you manage
to get this running consistently on Windows.)
We assume that `SextetStreamStdoutTest.jar` is working (as outlined
above). You will also need
[`createAndWritePipe`](https://github.com/psmay/windows-named-pipe-utils/releases)
to be able to work with a named pipe from the console.
In `Preferences.ini`, set:
InputDrivers=DirectInput,SextetStreamFromFile
SextetStreamInputFilename=\\.\pipe\StepMania-Input-SextetStream
(This also keeps the default DirectInput-based input enabled. The
DirectInput part can be removed later after setting up input mappings,
if desired.)
Run the input program and use `createAndWritePipe` to redirect the
output into the named pipe:
java -jar SextetStreamStdoutTest.jar | createAndWritePipe StepMania-Input-SextetStream
While the input program is running, start StepMania. (While using the
test program, use windowed mode to keep both StepMania and the input
program visible.) Using the keyboard directly on the StepMania window,
go to Options, Test Input. Switch to the test input program and try
pressing some keys. If StepMania displays corresponding messages, the
driver is working properly.
TODO: Serial examples.
Keyboard-based demo limitations
-------------------------------
Due to the design of some keyboards and/or their drivers, the number of
keys that can be held at one time, which keys can be pressed
simultaneously, and which keys have priority over others may vary. The
SextetStream driver itself has no such limitation and can process all
buttons independently of each other if the input program is capable of
providing such information.
Encoding
--------
### Packing sextets
Values are encoded six bits at a time (hence "sextet"). The characters
are made printable, non-whitespace ASCII so that attempting to read the
data or pass it through a text-oriented channel will not cause problems.
The encoding is similar in concept to base64 or uuencode, but in this
scheme the low 6 bits remain unchanged.
Data is packed into the low 6 bits of a byte, then the two high bits are
set in such a way that the result is printable, non-whitespace ASCII:
0x00-0x0F -> 0x40-0x4F
0x10-0x1F -> 0x50-0x5F
0x20-0x2F -> 0x60-0x6F
0x30-0x3F -> 0x30-0x3F
(0x20-0x2F and 0x70-0x7F are avoided since they contain control or
whitespace characters.)
To encode a 6-bit value `s` in this way, this transform may be used:
((s + 0x10) & 0x3F) + 0x30
### Bit meanings
This driver produces events for a virtual game controller with no axes
and an arbitrary number of buttons. As with an actual gamepad, the
in-game meaning of each button can be configured in StepMania.
Note that the maximum number of gamepad buttons supported by StepMania
is currently 32. Higher codes are coded as keypresses for unknown keys
(which is unusual for a game controller, but works here).
* Byte 0
* 0x01 button B1
* 0x02 button B2
* 0x04 button B3
* 0x08 button B4
* 0x10 button B5
* 0x20 button B6
*
* Byte `n` for `n < 5`
* 0x01 button B`6n+1`
* 0x02 button B`6n+2`
* 0x04 button B`6n+3`
* 0x08 button B`6n+4`
* 0x10 button B`6n+5`
* 0x20 button B`6n+6`
*
* Byte 5
* 0x01 button B31
* 0x02 button B32
* 0x04 key unk 0
* 0x08 key unk 1
* 0x10 key unk 2
* 0x20 key unk 3
*
* Byte `n` for `n > 5`
* 0x01 key unk `6(n-6)+4`
* 0x02 key unk `6(n-6)+5`
* 0x04 key unk `6(n-6)+6`
* 0x08 key unk `6(n-6)+7`
* 0x10 key unk `6(n-6)+8`
* 0x20 key unk `6(n-6)+9`
A message is ended by terminating it with LF (0x0A) or CR LF (0x0D
0x0A). Data bytes outside 0x30-0x6F are discarded. The message can be as
large or as short as necessary, to encode all buttons. Any buttons not
encoded in a message are understood to have the value 0.
These messages both indicate that buttons 4 and 6 are pressed, and all
others are not:
# Note: 0x68 & 0x3F == 0x28; 0x40 & 0x3F == 0x00
0x68 0x40 0x40 0x0A
0x68 0x0A
The number of buttons supported by the implementation is currently the
number of supported joy buttons plus the number of supported unknown
keys (as of this writing: 32 + 321 = 353; you certainly shouldn't need
this many). Overlong input lines are allowed and truncated. Because the
number of bytes needed in an input line is proportional to the index of
the highest-indexed bit currently in an on state, it's probably a good
idea to keep that number low whenever possible, especially when using a
low-speed input (such as a serial or network connection).
Filesystem
----------
The current implementation of this driver uses C standard I/O (i.e.,
`fopen()`, `fread()`, etc.) instead of the `RageFile` abstraction
Therefore, any `RageFile`-based filesystem abstractions *are not*
applied. Please keep this in mind when specifying the input path.
FIXME: This behavior is definitely subject to change should the
following problem ever get worked out: I was unable to get `RageFile` to
work in this context; I believe the cause is that I couldn't get a
`RageFile` object to do unbuffered input. It may have been something
else.
Pipes
-----
The input of this driver is streamed from a file. At face value, this is
not too useful. The actual intent is for the system operator to create a
*named fifo* or *named pipe* that is being written to by some already
running program, such as a program that reads button state data in over
a serial connection. It just happens that opening a named fifo for
reading can be accomplished in the same fashion as opening a file for
reading, as long as buffering can be disabled. Since there's an easy,
platform-ignorant way to do that, that's what we've done.
### Blocking behavior
This driver uses blocking I/O, but does so in a separate thread so that
StepMania will read lines about as fast as they are written and there is
no technical minimum data rate. However, the blocking read temporarily
particular, as StepMania exits, it may hang waiting for the input thread
to finish until one of the following happens:
* The input program closes the stream
* The input program outputs a line, allowing StepMania to end the
input thread
So, for the purposes of this driver, it is good manners for an input
program to periodically (e.g. once per second) repeat its current state
so that the input thread is never left blocking for too long at a time.
(Alternatively, if the input program has some way to determine that
StepMania is exiting, it may just close the stream instead.)
### Linux (and some other unixish systems)
`mkfifo` is used to create a named FIFO. After this is done, two
programs (one reading and one writing) open the FIFO as if it were some
ordinary file and use ordinary file I/O to read or write.
For this driver, the input program and StepMania open the file for write
and read, respectively.
Input programs producing output on stdout work trivially; the program's
output is simply redirected to the FIFO.
### Windows
The client side of a Windows named pipe is fairly ordinary; the path to
an already-open pipe can be opened and used as if it were an ordinary
file. For this driver, StepMania is the client.
The server side of the pipe is a little trickier. Special Windows API
calls are needed to create the pipe and then wait for the client to
connect. Unlike in Linux, the FIFO goes away when it is closed. For this
driver, the input program is the server.
Input programs producing output on stdout do not work trivially because
named pipes are not eligible to be created or redirected to on the
command line. Such a program can be redirected to a bridging program
such as `createAndWritePipe`, which creates a pipe and then relays
whatever is received on stdin to the pipe.
A Windows-specific input program might also just create and write a
named pipe by itself.
License
=======
Copyright © 2014 Peter S. May
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,289 @@
#include "global.h"
#include "LightsDriver_SextetStream.h"
#include "PrefsManager.h"
#include "RageLog.h"
#include "RageUtil.h"
#include <cstring>
using namespace std;
// Number of printable characters used to encode lights
static const size_t CABINET_SEXTET_COUNT = 1;
static const size_t CONTROLLER_SEXTET_COUNT = 6;
// Number of bytes to contain the full pack and a trailing LF
static const size_t FULL_SEXTET_COUNT = CABINET_SEXTET_COUNT + (NUM_GameController * CONTROLLER_SEXTET_COUNT) + 1;
// Serialization routines
// Encodes the low 6 bits of a byte as a printable, non-space ASCII
// character (i.e., within the range 0x21-0x7E) such that the low 6 bits of
// the character are the same as the input.
inline uint8_t printableSextet(uint8_t data)
{
// Maps the 6-bit value into the range 0x30-0x6F, wrapped in such a way
// that the low 6 bits of the result are the same as the data (so
// decoding is trivial).
//
// 00nnnn -> 0100nnnn (0x4n)
// 01nnnn -> 0101nnnn (0x5n)
// 10nnnn -> 0110nnnn (0x6n)
// 11nnnn -> 0011nnnn (0x3n)
// Put another way, the top 4 bits H of the output are determined from
// the top two bits T of the input like so:
// H = ((T + 1) mod 4) + 3
return ((data + (uint8_t)0x10) & (uint8_t)0x3F) + (uint8_t)0x30;
}
// Packs 6 booleans into a 6-bit value
inline uint8_t packPlainSextet(bool b0, bool b1, bool b2, bool b3, bool b4, bool b5)
{
return (uint8_t)(
(b0 ? 0x01 : 0) |
(b1 ? 0x02 : 0) |
(b2 ? 0x04 : 0) |
(b3 ? 0x08 : 0) |
(b4 ? 0x10 : 0) |
(b5 ? 0x20 : 0));
}
// Packs 6 booleans into a printable sextet
inline uint8_t packPrintableSextet(bool b0, bool b1, bool b2, bool b3, bool b4, bool b5)
{
return printableSextet(packPlainSextet(b0, b1, b2, b3, b4, b5));
}
// Packs the cabinet lights into a printable sextet and adds it to a buffer
inline size_t packCabinetLights(const LightsState *ls, uint8_t* buffer)
{
buffer[0] = packPrintableSextet(
ls->m_bCabinetLights[LIGHT_MARQUEE_UP_LEFT],
ls->m_bCabinetLights[LIGHT_MARQUEE_UP_RIGHT],
ls->m_bCabinetLights[LIGHT_MARQUEE_LR_LEFT],
ls->m_bCabinetLights[LIGHT_MARQUEE_LR_RIGHT],
ls->m_bCabinetLights[LIGHT_BASS_LEFT],
ls->m_bCabinetLights[LIGHT_BASS_RIGHT]);
return CABINET_SEXTET_COUNT;
}
// Packs the button lights for a controller into 6 printable sextets and
// adds them to a buffer
inline size_t packControllerLights(const LightsState *ls, GameController gc, uint8_t* buffer)
{
// Menu buttons
buffer[0] = packPrintableSextet(
ls->m_bGameButtonLights[gc][GAME_BUTTON_MENULEFT],
ls->m_bGameButtonLights[gc][GAME_BUTTON_MENURIGHT],
ls->m_bGameButtonLights[gc][GAME_BUTTON_MENUUP],
ls->m_bGameButtonLights[gc][GAME_BUTTON_MENUDOWN],
ls->m_bGameButtonLights[gc][GAME_BUTTON_START],
ls->m_bGameButtonLights[gc][GAME_BUTTON_SELECT]);
// Other non-sensors
buffer[1] = packPrintableSextet(
ls->m_bGameButtonLights[gc][GAME_BUTTON_BACK],
ls->m_bGameButtonLights[gc][GAME_BUTTON_COIN],
ls->m_bGameButtonLights[gc][GAME_BUTTON_OPERATOR],
ls->m_bGameButtonLights[gc][GAME_BUTTON_EFFECT_UP],
ls->m_bGameButtonLights[gc][GAME_BUTTON_EFFECT_DOWN],
false);
// Sensors
buffer[2] = packPrintableSextet(
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_01],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_02],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_03],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_04],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_05],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_06]);
buffer[3] = packPrintableSextet(
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_07],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_08],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_09],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_10],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_11],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_12]);
buffer[4] = packPrintableSextet(
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_13],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_14],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_15],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_16],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_17],
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_18]);
buffer[5] = packPrintableSextet(
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_19],
false,
false,
false,
false,
false);
return CONTROLLER_SEXTET_COUNT;
}
inline size_t packLine(uint8_t * buffer, const LightsState* ls)
{
size_t index = 0;
index += packCabinetLights(ls, &(buffer[index]));
FOREACH_ENUM(GameController, gc)
{
index += packControllerLights(ls, gc, &(buffer[index]));
}
// Terminate with LF
buffer[index++] = 0xA;
return index;
}
// Private members/methods are kept out of the header using an opaque pointer `_impl`.
// Google "pimpl idiom" for an explanation of what's going on and why it is (or might be) useful.
// Implementation class
namespace
{
class Impl
{
protected:
uint8_t lastOutput[FULL_SEXTET_COUNT];
RageFile * out;
public:
Impl(RageFile * file) {
out = file;
// Ensure a non-match the first time
lastOutput[0] = 0;
}
virtual ~Impl() {
if(out != NULL)
{
out->Flush();
out->Close();
SAFE_DELETE(out);
}
}
void Set(const LightsState * ls)
{
uint8_t buffer[FULL_SEXTET_COUNT];
packLine(buffer, ls);
// Only write if the message has changed since the last write.
if(memcmp(buffer, lastOutput, FULL_SEXTET_COUNT) != 0)
{
if(out != NULL)
{
out->Write(buffer, FULL_SEXTET_COUNT);
out->Flush();
}
// Remember last message
memcpy(lastOutput, buffer, FULL_SEXTET_COUNT);
}
}
};
}
// LightsDriver_SextetStream interface
// (Wrapper for Impl)
#define IMPL ((Impl*)_impl)
LightsDriver_SextetStream::LightsDriver_SextetStream()
{
_impl = NULL;
}
LightsDriver_SextetStream::~LightsDriver_SextetStream()
{
if(IMPL != NULL)
{
delete IMPL;
}
}
void LightsDriver_SextetStream::Set(const LightsState *ls)
{
if(IMPL != NULL)
{
IMPL->Set(ls);
}
}
// LightsDriver_SextetStreamToFile implementation
REGISTER_SOUND_DRIVER_CLASS(SextetStreamToFile);
#if defined(_WINDOWS)
#define DEFAULT_OUTPUT_FILENAME "\\\\.\\pipe\\StepMania-Lights-SextetStream"
#else
#define DEFAULT_OUTPUT_FILENAME "Data/StepMania-Lights-SextetStream.out"
#endif
static Preference<RString> g_sSextetStreamOutputFilename("SextetStreamOutputFilename", DEFAULT_OUTPUT_FILENAME);
inline RageFile * openOutputStream(const RString& filename)
{
RageFile * file = new RageFile;
if(!file->Open(filename, RageFile::WRITE|RageFile::STREAMED))
{
LOG->Warn("Error opening file '%s' for output: %s", filename.c_str(), file->GetError().c_str());
SAFE_DELETE(file);
file = NULL;
}
return file;
}
LightsDriver_SextetStreamToFile::LightsDriver_SextetStreamToFile(RageFile * file)
{
_impl = new Impl(file);
}
LightsDriver_SextetStreamToFile::LightsDriver_SextetStreamToFile(const RString& filename)
{
_impl = new Impl(openOutputStream(filename));
}
LightsDriver_SextetStreamToFile::LightsDriver_SextetStreamToFile()
{
_impl = new Impl(openOutputStream(g_sSextetStreamOutputFilename));
}
/*
* Copyright © 2014 Peter S. May
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit
* persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
@@ -0,0 +1,64 @@
#ifndef LightsDriver_SextetStream_H
#define LightsDriver_SextetStream_H
/*
* `LightsDriver_SextetStream` (abstract): Streams the light data (in
* ASCII-safe sextets) to some output stream.
*
* * `LightsDriver_SextetStreamToFile`: Streams the light data to an
* output file.
* * The specified file may be a named pipe (Windows)/named fifo
* (Linux, others). This makes it possible to implement an
* out-of-process light controller without touching the StepMania
* source and without using C++. See the included notes for
* details.
*/
#include "LightsDriver.h"
#include "RageFile.h"
class LightsDriver_SextetStream : public LightsDriver
{
public:
LightsDriver_SextetStream();
virtual ~LightsDriver_SextetStream();
virtual void Set(const LightsState *ls);
protected:
void * _impl;
};
class LightsDriver_SextetStreamToFile : public LightsDriver_SextetStream
{
public:
LightsDriver_SextetStreamToFile();
LightsDriver_SextetStreamToFile(const RString& filename);
// The file object passed here should already be open, and will be
// flushed, closed, and deleted in the destructor.
LightsDriver_SextetStreamToFile(RageFile * file);
};
#endif
/*
* Copyright © 2014 Peter S. May
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit
* persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
@@ -0,0 +1,420 @@
`LightsDriver_SextetStream_*`
=============================
Explanation
-----------
This is a set of drivers (currently, just one driver) that encode light
states as text and stream the result to the desired output. By making
the light data available as a readable stream, light control can be
performed by another program. Such a program can be implemented using
any language/platform that supports driving the desired output device
(and reading from an input stream). If C++ isn't your thing, or if
learning the guts of StepMania seems a little much just to implement a
lights driver, you're in the right place.
Quick start
-----------
You'll need a working StepMania build with the driver
`LightsDriver_SextetStreamToFile` enabled.
For this test, you'll run a script, `SextetStreamStdinTest`, to verify
that the driver is set up properly. The script is a simple example of a
*lighting program*, a program that will receive input from StepMania and
produce output using the data. Because the script is for testing and
diagnostics, the is text in a console window. Actual lighting programs
receive input in the same way, but will do different things with the
output, such as sending commands out over a hardware interface
(serial/parallel/USB) to control physical lights.
### Linux
You'll need the perl script
[`SextetStreamStdinTest.pl`](https://gist.github.com/psmay/7f45a867c1ae8f88ec36#file-sextetstreamstdintest-pl).
Set the executable bit (`chmod ug+x SextetStreamStdinTest.pl`).
We assume that `$SM` is the root StepMania directory.
In `Preferences.ini`, set:
LightsDriver=SextetStreamToFile
SextetStreamOutputFilename=Data/StepMania-Lights-SextetStream.out
Create the FIFO:
mkfifo "$SM/Data/StepMania-Lights-SextetStream.out"
Run the test stdin-based lighting program, using the FIFO as input:
./SextetStreamStdinTest.pl < "$SM/Data/StepMania-Lights-SextetStream.out"
While the lighting program is running, start StepMania. (When using the
test lighting program, windowed mode may be needed to keep the program
visible.) The lighting program should display the light information.
When StepMania closes, the FIFO connection ends, causing the lighting
program to exit as well.
#### Serial port under Linux
If you have a microcontroller running firmware that understands the
SextetStream protocol, you can use `socat` as the lighting program to
pipe the data directly to the device via a serial port:
socat "$SM/Data/StepMania-Lights-SextetStream.out" /dev/ttyUSB0,raw,echo=0,b115200
Substitute your actual serial port device for `/dev/ttyUSB0` and the
actual port speed for `115200`. (But note that running at too low a
speed may result in the output blocking, causing StepMania to freeze or
stutter).
TODO: Supply example Arduino/PIC/... firmware.
TODO: Supply example of full-duplex in cooperation with the SextetStream
input driver.
### Mac OS X
FIXME: *This has not been tested at all.* The following is a guess at a
synopsis of how it should work if you're lucky enough for everything to
have fallen into place just so. (Please amend this message if you manage
to get this running consistently on Mac OS X.)
The instructions for Linux should work equally well for Mac OS X.
TODO: Serial examples.
### Windows
FIXME: *This has not been tested at all.* The following is a guess at a
synopsis of how it should work if you're lucky enough for everything to
have fallen into place just so. (Please amend this message if you manage
to get this running consistently on Windows.)
You'll need:
* The Windows Script Host script
[`SextetStreamStdinTest.wsf`](https://gist.github.com/psmay/7f45a867c1ae8f88ec36#file-sextetstreamstdintest-wsf),
the lighting program
* [`createAndReadPipe`](https://github.com/psmay/windows-named-pipe-utils/releases),
to work with a named pipe from the console.
In `Preferences.ini`, set:
LightsDriver=SextetStreamToFile
SextetStreamOutputFilename=\\.\pipe\StepMania-Lights-SextetStream
Use `createAndReadPipe` to create the named pipe, feeding the output
into the test stdin-based lighting program:
createAndReadPipe StepMania-Lights-SextetStream | cscript //nologo SextetStreamStdinTest.wsf
While the lighting program is running, start StepMania. (When using the
test lighting program, windowed mode may be needed to keep the program
visible.) The lighting program should display the light information.
When StepMania closes, the pipe connection ends, causing the lighting
program to exit as well.
TODO: Serial examples.
Encoding
--------
### Packing sextets
Values are encoded six bits at a time (hence "sextet").
Data is packed into the low 6 bits of a byte, then the two high bits are
set in such a way that the result is printable, non-whitespace ASCII:
0x00-0x0F -> 0x40-0x4F
0x10-0x1F -> 0x50-0x5F
0x20-0x2F -> 0x60-0x6F
0x30-0x3F -> 0x30-0x3F
(0x20-0x2F and 0x70-0x7F are avoided since they contain control or
whitespace characters.)
The characters are made printable so that attempting to read the output
or pass it through a text-oriented channel will not cause problems.
In any case, decoding these values is trivial and involves no
lookups—just do `& 0x3F`. (And even that may be optional, depending on
the application.)
### Bit meanings
The driver repeatedly outputs a serialization of the current light state
followed by an LF (0xA, `\n`), at least once each time any light should
change (and currently no more often than that).
Currently, this message is 13 data bytes followed by LF. The following
information is contained:
* Byte 0
* 0x01 Marquee upper-left
* 0x02 Marquee upper-right
* 0x04 Marquee lower-left
* 0x08 Marquee lower-right
* 0x10 Bass left
* 0x20 Bass right
* Byte 1
* 0x01 Player 1 menu left
* 0x02 Player 1 menu right
* 0x04 Player 1 menu up
* 0x08 Player 1 menu down
* 0x10 Player 1 start
* 0x20 Player 1 select
* Byte 2
* 0x01 Player 1 back
* 0x02 Player 1 coin
* 0x04 Player 1 operator
* 0x08 Player 1 effect up
* 0x10 Player 1 effect down
* 0x20 (reserved)
* Byte 3
* 0x01 Player 1 #1
* 0x02 Player 1 #2
* 0x04 Player 1 #3
* 0x08 Player 1 #4
* 0x10 Player 1 #5
* 0x20 Player 1 #6
* Byte 4
* 0x01 Player 1 #7
* 0x02 Player 1 #8
* 0x04 Player 1 #9
* 0x08 Player 1 #10
* 0x10 Player 1 #11
* 0x20 Player 1 #12
* Byte 5
* 0x01 Player 1 #13
* 0x02 Player 1 #14
* 0x04 Player 1 #15
* 0x08 Player 1 #16
* 0x10 Player 1 #17
* 0x20 Player 1 #18
* Byte 6
* 0x01 Player 1 #19
* 0x02 (reserved)
* 0x04 (reserved)
* 0x08 (reserved)
* 0x10 (reserved)
* 0x20 (reserved)
* Byte 7
* 0x01 Player 2 menu left
* 0x02 Player 2 menu right
* 0x04 Player 2 menu up
* 0x08 Player 2 menu down
* 0x10 Player 2 start
* 0x20 Player 2 select
* Byte 8
* 0x01 Player 2 back
* 0x02 Player 2 coin
* 0x04 Player 2 operator
* 0x08 Player 2 effect up
* 0x10 Player 2 effect down
* 0x20 (reserved)
* Byte 9
* 0x01 Player 2 #1
* 0x02 Player 2 #2
* 0x04 Player 2 #3
* 0x08 Player 2 #4
* 0x10 Player 2 #5
* 0x20 Player 2 #6
* Byte 10
* 0x01 Player 2 #7
* 0x02 Player 2 #8
* 0x04 Player 2 #9
* 0x08 Player 2 #10
* 0x10 Player 2 #11
* 0x20 Player 2 #12
* Byte 11
* 0x01 Player 2 #13
* 0x02 Player 2 #14
* 0x04 Player 2 #15
* 0x08 Player 2 #16
* 0x10 Player 2 #17
* 0x20 Player 2 #18
* Byte 12
* 0x01 Player 2 #19
* 0x02 (reserved)
* 0x04 (reserved)
* 0x08 (reserved)
* 0x10 (reserved)
* 0x20 (reserved)
Above, `Player 1 #n` and `Player 2 #n` refer to StepMania's internal
`GAME_BUTTON_CUSTOM_nn` values, whose meaning depends on the game.
Currently, these mappings are:
* dance
* 1 Pad left
* 2 Pad right
* 3 Pad up
* 4 Pad down
* 5 Pad up-left
* 6 Pad up-right
* techno: Same as dance, plus
* 7 Pad center
* 8 Pad down-left
* 9 Pad down-right
* pump
* 1 Pad up-left
* 2 Pad up-right
* 3 Pad center
* 4 Pad down-left
* 5 Pad down-right
* kb7
* 1-7 Keys 1-7
* ez2
* 1 Foot upper-left
* 2 Foot upper-right
* 3 Foot down
* 4 Hand upper-left
* 5 Hand upper-right
* 6 Hand lower-left
* 7 Hand lower-right
* para
* 1 Button left
* 2 Button up-left
* 3 Button up
* 4 Button up-right
* 5 Button right
* ds3ddx
* 1 Hand left
* 2 Foot down-left
* 3 Foot up-left
* 4 Hand up
* 5 Hand down
* 6 Foot up-right
* 7 Foot down-right
* 8 Hand right
* beat
* 1-7 Keys 1-7
* 8 Scratch up
* 9 Scratch down
* maniax
* 1 Sensor over-left
* 2 Sensor over-right
* 3 Sensor under-left
* 4 Sensor under-right
* popn
* 1 Button left white
* 2 Button left yellow
* 3 Button left green
* 4 Button left blue
* 5 Button center
* 6 Button right blue
* 7 Button right green
* 8 Button right yellow
* 9 Button right white
Filesystem
----------
The current implementation of this driver uses a `RageFile` for output.
Therefore, any `RageFile`-based filesystem abstractions *are* applied.
Please keep this in mind when specifying the input path.
FIXME: This behavior is probably permanent, but if it is not possible to
open a Windows pipe using `RageFile`, `RageFile` should probably change
so that it is.
Pipes
-----
The output of this driver streams to a file. At face value, this is not
too useful. The actual intent is for the system operator to create a
*named fifo* or *named pipe* that is being listened to by some already
running program, such as a program that sends light data out over a
serial connection. It just happens that opening a named fifo for writing
can be accomplished in the same fashion as opening a file for writing.
Since there's an easy, platform-ignorant way to do that, that's what
we've done.
Note that this uses blocking I/O, so your light processing program must
make sure to empty the fifo as quickly as it gets filled. If this
driver's write blocks, parts of the application stall. If the program is
non-trivial, using a separate thread dedicated to reading may be useful.
How you actually start up the other end of the connection depends on
your platform.
### Linux (and some other unixish systems)
`mkfifo` is used to create a named FIFO. After this is done, two
programs (one reading and one writing) open the FIFO as if it were some
ordinary file and use ordinary file I/O to read or write.
For this driver, the lighting program and StepMania open the file for
read and write, respectively.
Lighting programs accepting input on stdin work trivially; the FIFO is
simply redirected into the program's stdin.
### Windows
The client side of a Windows named pipe is fairly ordinary; the path to
an already-open pipe can be opened and used as if it were an ordinary
file. For this driver, StepMania is the client.
The server side of the pipe is a little trickier. Special Windows API
calls are needed to create the pipe and then wait for the client to
connect. Unlike in Linux, the FIFO goes away when it is closed. For this
driver, the lighting program is the server.
Lighting programs accepting input on stdin do not work trivially because
named pipes are not eligible to be created or redirected on the command
line. A bridging program such as `createAndReadPipe` can be used to
create a pipe and relay all of its input to stdout, which can then be
redirected.
A Windows-specific lighting program might also just create and read a
named pipe by itself.
License
=======
Copyright © 2014 Peter S. May
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.