diff --git a/src/Makefile.am b/src/Makefile.am index e74dc76ce1..19ac2d4d8a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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 diff --git a/src/arch/InputHandler/InputHandler_SextetStream.cpp b/src/arch/InputHandler/InputHandler_SextetStream.cpp new file mode 100644 index 0000000000..0516bfefd9 --- /dev/null +++ b/src/arch/InputHandler/InputHandler_SextetStream.cpp @@ -0,0 +1,446 @@ +#include "global.h" +#include "InputHandler_SextetStream.h" +#include "PrefsManager.h" +#include "RageLog.h" +#include "RageThreads.h" +#include "RageUtil.h" + +#include +#include +#include + +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& 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& 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 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. + */ + + diff --git a/src/arch/InputHandler/InputHandler_SextetStream.h b/src/arch/InputHandler/InputHandler_SextetStream.h new file mode 100644 index 0000000000..ee019cd1ea --- /dev/null +++ b/src/arch/InputHandler/InputHandler_SextetStream.h @@ -0,0 +1,70 @@ +#ifndef INPUT_HANDLER_SEXTETSTREAM +#define INPUT_HANDLER_SEXTETSTREAM + +#include "InputHandler.h" +#include + +class InputHandler_SextetStream: public InputHandler +{ +public: + InputHandler_SextetStream(); + virtual ~InputHandler_SextetStream(); + //virtual void Update(); + virtual void GetDevicesAndDescriptions(vector& 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. + */ diff --git a/src/arch/InputHandler/InputHandler_SextetStream.md b/src/arch/InputHandler/InputHandler_SextetStream.md new file mode 100644 index 0000000000..17f57b9d4f --- /dev/null +++ b/src/arch/InputHandler/InputHandler_SextetStream.md @@ -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. diff --git a/src/arch/Lights/LightsDriver_SextetStream.cpp b/src/arch/Lights/LightsDriver_SextetStream.cpp new file mode 100644 index 0000000000..88510e351a --- /dev/null +++ b/src/arch/Lights/LightsDriver_SextetStream.cpp @@ -0,0 +1,289 @@ +#include "global.h" +#include "LightsDriver_SextetStream.h" +#include "PrefsManager.h" +#include "RageLog.h" +#include "RageUtil.h" + +#include + +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 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. + */ diff --git a/src/arch/Lights/LightsDriver_SextetStream.h b/src/arch/Lights/LightsDriver_SextetStream.h new file mode 100644 index 0000000000..df96269f7c --- /dev/null +++ b/src/arch/Lights/LightsDriver_SextetStream.h @@ -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. + */ diff --git a/src/arch/Lights/LightsDriver_SextetStream.md b/src/arch/Lights/LightsDriver_SextetStream.md new file mode 100644 index 0000000000..040934eb68 --- /dev/null +++ b/src/arch/Lights/LightsDriver_SextetStream.md @@ -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.