
Adjusts the great pinscape controller to work with a cheap linear potentiometer instead of the expensive CCD array
Fork of Pinscape_Controller by
Revision 9:fd65b0a94720, committed 2014-08-18
- Comitter:
- mjr
- Date:
- Mon Aug 18 21:46:10 2014 +0000
- Parent:
- 8:c732e279ee29
- Child:
- 10:976666ffa4ef
- Commit message:
- Tweaks to plunger firing detection
Changed in this revision
--- a/USBJoystick/USBJoystick.cpp Fri Aug 08 20:59:39 2014 +0000 +++ b/USBJoystick/USBJoystick.cpp Mon Aug 18 21:46:10 2014 +0000 @@ -20,14 +20,13 @@ #include "stdint.h" #include "USBJoystick.h" -bool USBJoystick::update(int16_t x, int16_t y, int16_t z, int16_t rx, int16_t ry, uint16_t buttons) +bool USBJoystick::update(int16_t x, int16_t y, int16_t z, uint16_t buttons, uint16_t status) { _x = x; _y = y; _z = z; - _rx = rx; - _ry = ry; _buttons = buttons; + _status = (uint8_t)status; // send the report return update(); @@ -37,29 +36,18 @@ HID_REPORT report; // Fill the report according to the Joystick Descriptor - report.data[0] = _buttons & 0xff; - report.data[1] = (_buttons >> 8) & 0xff; -#if 0 // 8-bit coordinate reports - report.data[2] = _x & 0xff; - report.data[3] = _y & 0xff; - report.data[4] = _z & 0xff; - report.data[5] = _rx & 0xff; - report.data[6] = _ry & 0xff; - report.length = 7; -#else // 16-bit reports #define put(idx, val) (report.data[idx] = (val) & 0xff, report.data[(idx)+1] = ((val) >> 8) & 0xff) + put(0, _buttons); put(2, _x); put(4, _y); put(6, _z); - put(8, _rx); - put(10, _ry); - report.length = 12; -#endif + report.data[8] = _status; + report.length = 9; // send the report return sendNB(&report); } - + bool USBJoystick::move(int16_t x, int16_t y) { _x = x; _y = y; @@ -83,6 +71,7 @@ _y = 0; _z = 0; _buttons = 0x0000; + _status = 0; } @@ -94,8 +83,29 @@ USAGE(1), 0x04, // Joystick COLLECTION(1), 0x01, // Application + + // NB - the canonical joystick has a nested collection at this + // point. We remove the inner collection to enable the LedWiz + // emulation. The LedWiz API implementation on the PC side + // appears to use the collection structure as part of the + // device signature, and the real LedWiz descriptor has just + // one top-level collection. The built-in Windows HID drivers + // don't appear to care whether this collection is present or + // not for the purposes of recognizing a joystick, so it seems + // to make everyone happy to leave it out. + // + // All of the reference material for USB joystick device builders + // does use the inner collection, so it's possible that omitting + // it will create an incompatibility with some non-Windows hosts. + // But that seems largely moot in that VP only runs on Windows. + // If you're you're trying to adapt this code for a different + // device and run into problems connecting to a non-Windows host, + // try restoring the inner collection. You probably won't + // care about LedWiz compatibility in such a situation so there + // should be no reason not to return to the standard structure. // COLLECTION(1), 0x00, // Physical + // input report (device to host) USAGE_PAGE(1), 0x09, // Buttons USAGE_MINIMUM(1), 0x01, // { buttons } USAGE_MAXIMUM(1), 0x10, // { 1-16 } @@ -108,18 +118,26 @@ INPUT(1), 0x02, // Data, Variable, Absolute USAGE_PAGE(1), 0x01, // Generic desktop - USAGE(1), 0x30, // X - USAGE(1), 0x31, // Y - USAGE(1), 0x32, // Z - USAGE(1), 0x33, // Rx - USAGE(1), 0x34, // Ry + USAGE(1), 0x30, // X axis + USAGE(1), 0x31, // Y axis + USAGE(1), 0x32, // Z axis LOGICAL_MINIMUM(2), 0x00,0xF0, // each value ranges -4096 LOGICAL_MAXIMUM(2), 0x00,0x10, // ...to +4096 REPORT_SIZE(1), 0x10, // 16 bits per report - REPORT_COUNT(1), 0x05, // 5 reports (X, Y, Z, Rx, Ry) + REPORT_COUNT(1), 0x03, // 3 reports (X, Y, Z) + INPUT(1), 0x02, // Data, Variable, Absolute + + USAGE_PAGE(1), 0x06, // generic device controls - for config status + USAGE(1), 0x00, // undefined device control + LOGICAL_MINIMUM(1), 0x00, // 1-bit flags + LOGICAL_MAXIMUM(1), 0x01, + REPORT_SIZE(1), 0x01, // 1 bit per report + REPORT_COUNT(1), 0x08, // 8 reports (8 bits) INPUT(1), 0x02, // Data, Variable, Absolute - REPORT_COUNT(1), 0x08, // input report count (LEDWiz messages) + // output report (host to device) + REPORT_SIZE(1), 0x08, // 8 bits per report + REPORT_COUNT(1), 0x08, // output report count (LEDWiz messages) 0x09, 0x01, // usage 0x91, 0x01, // Output (array) @@ -151,7 +169,7 @@ uint8_t * USBJoystick::stringIproductDesc() { static uint8_t stringIproductDescriptor[] = { - 0x2E, /*bLength*/ + 0x28, /*bLength*/ STRING_DESCRIPTOR, /*bDescriptorType 0x03*/ 'P',0,'i',0,'n',0,'s',0,'c',0,'a',0,'p',0,'e',0, ' ',0,'C',0,'o',0,'n',0,'t',0,'r',0,'o',0,'l',0,
--- a/USBJoystick/USBJoystick.h Fri Aug 08 20:59:39 2014 +0000 +++ b/USBJoystick/USBJoystick.h Mon Aug 18 21:46:10 2014 +0000 @@ -106,7 +106,7 @@ * @param buttons buttons state, as a bit mask (combination with '|' of JOY_Bn values) * @returns true if there is no error, false otherwise */ - bool update(int16_t x, int16_t y, int16_t z, int16_t rx, int16_t ry, uint16_t buttons); + bool update(int16_t x, int16_t y, int16_t z, uint16_t buttons, uint16_t status); /** * Write a state of the mouse @@ -114,7 +114,7 @@ * @returns true if there is no error, false otherwise */ bool update(); - + /** * Move the cursor to (x, y) * @@ -155,9 +155,8 @@ int16_t _x; int16_t _y; int16_t _z; - int16_t _rx; - int16_t _ry; uint16_t _buttons; + uint8_t _status; void _init(); };
--- a/main.cpp Fri Aug 08 20:59:39 2014 +0000 +++ b/main.cpp Mon Aug 18 21:46:10 2014 +0000 @@ -171,7 +171,16 @@ // byte 2 = new LedWiz unit number, 0x01 to 0x0f // byte 3 = feature enable bit mask: // 0x01 = enable CCD (default = on) - +// +// Plunger calibration mode: the host can activate plunger calibration mode +// by sending this packet. This has the same effect as pressing and holding +// the plunger calibration button for two seconds, to allow activating this +// mode without attaching a physical button. +// +// length = 8 bytes +// byte 0 = 65 (0x41) +// byte 1 = 2 (0x02) +// #include "mbed.h" #include "math.h" @@ -220,6 +229,22 @@ const uint16_t USB_VERSION_NO = 0x0006; const uint8_t DEFAULT_LEDWIZ_UNIT_NUMBER = 0x07; +// Number of pixels we read from the sensor on each frame. This can be +// less than the physical pixel count if desired; we'll read every nth +// piexl if so. E.g., with a 1280-pixel physical sensor, if npix is 320, +// we'll read every 4th pixel. It takes time to read each pixel, so the +// fewer pixels we read, the higher the refresh rate we can achieve. +// It's therefore better not to read more pixels than we have to. +// +// VP seems to have an internal resolution in the 8-bit range, so there's +// no apparent benefit to reading more than 128-256 pixels when using VP. +// Empirically, 160 pixels seems about right. The overall travel of a +// standard pinball plunger is about 3", so 160 pixels gives us resolution +// of about 1/50". This seems to take full advantage of VP's modeling +// ability, and is probably also more precise than a human player's +// perception of the plunger position. +const int npix = 160; + // On-board RGB LED elements - we use these for diagnostic displays. DigitalOut ledR(LED1), ledG(LED2), ledB(LED3); @@ -302,23 +327,24 @@ { PTD5, true }, // pin J2-4, LW port 8 (PWM capable - TPM 0.5 = channel 6) { PTD0, true }, // pin J2-6, LW port 9 (PWM capable - TPM 0.0 = channel 1) { PTD3, true }, // pin J2-10, LW port 10 (PWM capable - TPM 0.3 = channel 4) - { PTC8, false }, // pin J1-14, LW port 11 - { PTC9, false }, // pin J1-16, LW port 12 - { PTC7, false }, // pin J1-1, LW port 13 - { PTC0, false }, // pin J1-3, LW port 14 - { PTC3, false }, // pin J1-5, LW port 15 - { PTC4, false }, // pin J1-7, LW port 16 - { PTC5, false }, // pin J1-9, LW port 17 - { PTC6, false }, // pin J1-11, LW port 18 - { PTC10, false }, // pin J1-13, LW port 19 - { PTC11, false }, // pin J1-15, LW port 20 - { PTC12, false }, // pin J2-1, LW port 21 - { PTC13, false }, // pin J2-3, LW port 22 - { PTC16, false }, // pin J2-5, LW port 23 - { PTC17, false }, // pin J2-7, LW port 24 - { PTA16, false }, // pin J2-9, LW port 25 - { PTA17, false }, // pin J2-11, LW port 26 - { PTE31, false }, // pin J2-13, LW port 27 + { PTD2, false }, // pin J2-8, LW port 11 + { PTC8, false }, // pin J1-14, LW port 12 + { PTC9, false }, // pin J1-16, LW port 13 + { PTC7, false }, // pin J1-1, LW port 14 + { PTC0, false }, // pin J1-3, LW port 15 + { PTC3, false }, // pin J1-5, LW port 16 + { PTC4, false }, // pin J1-7, LW port 17 + { PTC5, false }, // pin J1-9, LW port 18 + { PTC6, false }, // pin J1-11, LW port 19 + { PTC10, false }, // pin J1-13, LW port 20 + { PTC11, false }, // pin J1-15, LW port 21 + { PTC12, false }, // pin J2-1, LW port 22 + { PTC13, false }, // pin J2-3, LW port 23 + { PTC16, false }, // pin J2-5, LW port 24 + { PTC17, false }, // pin J2-7, LW port 25 + { PTA16, false }, // pin J2-9, LW port 26 + { PTA17, false }, // pin J2-11, LW port 27 + { PTE31, false }, // pin J2-13, LW port 28 { PTD6, false }, // pin J2-17, LW port 29 { PTD7, false }, // pin J2-19, LW port 30 { PTE0, false }, // pin J2-18, LW port 31 @@ -343,6 +369,12 @@ // --------------------------------------------------------------------------- +// utilities + +// number of elements in an array +#define countof(x) (sizeof(x)/sizeof((x)[0])) + +// --------------------------------------------------------------------------- // // LedWiz emulation // @@ -381,7 +413,7 @@ // initialize the output pin array void initLwOut() { - for (int i = 0 ; i < sizeof(lwPin) / sizeof(lwPin[0]) ; ++i) + for (int i = 0 ; i < countof(lwPin) ; ++i) { PinName p = ledWizPortMap[i].pin; lwPin[i] = (ledWizPortMap[i].isPWM @@ -467,6 +499,15 @@ iap.program_flash(addr, this, sizeof(*this)); } + // reset calibration data for calibration mode + void resetPlunger() + { + // set extremes for the calibration data + d.plungerMax = 0; + d.plungerZero = npix; + d.plungerMin = npix; + } + // stored data (excluding the checksum) struct { @@ -640,7 +681,7 @@ tInt_.start(); } - void get(int &x, int &y, int &rx, int &ry) + void get(int &x, int &y) { // disable interrupts while manipulating the shared data __disable_irq(); @@ -711,14 +752,6 @@ x = rawToReport(vx); y = rawToReport(vy); - // apply a small dead zone near the center - // if (abs(x) < 6) x = 0; - // if (abs(y) < 6) y = 0; - - // report the calibrated instantaneous acceleration in rx,ry - rx = int(round((ax - cx_)*JOYMAX)); - ry = int(round((ay - cy_)*JOYMAX)); - #ifdef DEBUG_PRINTF if (x != 0 || y != 0) printf("%f %f %d %d %f\r\n", vx, vy, x, y, dt); @@ -874,22 +907,6 @@ // check for valid flash bool flash_valid = flash->valid(); - // Number of pixels we read from the sensor on each frame. This can be - // less than the physical pixel count if desired; we'll read every nth - // piexl if so. E.g., with a 1280-pixel physical sensor, if npix is 320, - // we'll read every 4th pixel. It takes time to read each pixel, so the - // fewer pixels we read, the higher the refresh rate we can achieve. - // It's therefore better not to read more pixels than we have to. - // - // VP seems to have an internal resolution in the 8-bit range, so there's - // no apparent benefit to reading more than 128-256 pixels when using VP. - // Empirically, 160 pixels seems about right. The overall travel of a - // standard pinball plunger is about 3", so 160 pixels gives us resolution - // of about 1/50". This seems to take full advantage of VP's modeling - // ability, and is probably also more precise than a human player's - // perception of the plunger position. - const int npix = 160; - // if the flash is valid, load it; otherwise initialize to defaults if (flash_valid) { memcpy(&cfg, flash, sizeof(cfg)); @@ -918,7 +935,6 @@ // plunger calibration button debounce timer Timer calBtnTimer; calBtnTimer.start(); - int calBtnDownTime = 0; int calBtnLit = false; // Calibration button state: @@ -957,10 +973,16 @@ // so when we detect the start of this motion, we immediately tell VP // to return the plunger to rest, then we monitor the real plunger // until it atcually stops. - bool firing = false; + int firing = 0; // start the first CCD integration cycle ccd.clear(); + + // Device status. We report this on each update so that the host config + // tool can detect our current settings. This is a bit mask consisting + // of these bits: + // 0x01 -> plunger sensor enabled + uint16_t statusFlags = (cfg.d.ccdEnabled ? 0x01 : 0x00); // we're all set up - now just loop, processing sensor reports and // host requests @@ -1009,7 +1031,7 @@ // message type. if (data[1] == 1) { - // Set Configuration: + // 1 = Set Configuration: // data[2] = LedWiz unit number (0x00 to 0x0f) // data[3] = feature enable bit mask: // 0x01 = enable CCD @@ -1022,9 +1044,26 @@ cfg.d.ledWizUnitNo = newUnitNo; cfg.d.ccdEnabled = data[3] & 0x01; + // update the status flags + statusFlags = (statusFlags & ~0x01) | (data[3] & 0x01); + + // if the ccd is no longer enabled, use 0 for z reports + if (!cfg.d.ccdEnabled) + z = 0; + // save the configuration cfg.save(iap, flash_addr); } + else if (data[1] == 2) + { + // 2 = Calibrate plunger + // (No parameters) + + // enter calibration mode + calBtnState = 3; + calBtnTimer.reset(); + cfg.resetPlunger(); + } } else { @@ -1057,38 +1096,32 @@ case 0: // button not yet pushed - start debouncing calBtnTimer.reset(); - calBtnDownTime = calBtnTimer.read_ms(); calBtnState = 1; break; case 1: // pushed, not yet debounced - if the debounce time has // passed, start the hold period - if (calBtnTimer.read_ms() - calBtnDownTime > 50) + if (calBtnTimer.read_ms() > 50) calBtnState = 2; break; case 2: // in the hold period - if the button has been held down // for the entire hold period, move to calibration mode - if (calBtnTimer.read_ms() - calBtnDownTime > 2050) + if (calBtnTimer.read_ms() > 2050) { // enter calibration mode calBtnState = 3; - - // set extremes for the calibration data, so that the actual - // values we read will set new high/low water marks on the fly - cfg.d.plungerMax = 0; - cfg.d.plungerZero = npix; - cfg.d.plungerMin = npix; + calBtnTimer.reset(); + cfg.resetPlunger(); } break; case 3: - // Already in calibration mode - pushing the button in this - // state doesn't change the current state, but we won't leave - // this state as long as it's held down. We can simply do - // nothing here. + // Already in calibration mode - pushing the button here + // doesn't change the current state, but we won't leave this + // state as long as it's held down. So nothing changes here. break; } } @@ -1101,8 +1134,7 @@ // Otherwise, return to the base state without saving anything. // If the button is released before we make it to calibration // mode, it simply cancels the attempt. - if (calBtnState == 3 - && calBtnTimer.read_ms() - calBtnDownTime > 17500) + if (calBtnState == 3 && calBtnTimer.read_ms() > 15000) { // exit calibration mode calBtnState = 0; @@ -1127,7 +1159,7 @@ { case 2: // in the hold period - flash the light - newCalBtnLit = (((calBtnTimer.read_ms() - calBtnDownTime)/250) & 1); + newCalBtnLit = ((calBtnTimer.read_ms()/250) & 1); break; case 3: @@ -1150,13 +1182,13 @@ calBtnLed = 1; ledR = 1; ledG = 1; - ledB = 1; + ledB = 0; } else { calBtnLed = 0; ledR = 1; ledG = 1; - ledB = 0; + ledB = 1; } } @@ -1246,14 +1278,34 @@ // is complete, allowing VP to carry out the firing motion using // its internal model plunger rather than trying to track the // intermediate positions of the mechanical plunger throughout - // the firing motion. This has several benefits. First is that - // our readings aren't very accurate during rapid movement, - // because we get too much motion blur. Second is that the - // event approach allows VP to simulate the plunger motion - // according to each table's particular plunger settings. - // Different tables have different plunger strengths and speeds, - // so we want to defer to the model for the physics of the firing - // motion within each simulation. + // the firing motion. This is essential because the firing + // motion is too fast for us to track - in the time it takes us + // to read one frame, the plunger can make it all the way to the + // zero position and bounce back halfway. Fortunately, the range + // of motions for the plunger is limited, so if we see any rapid + // change of position toward the rest position, it's reasonably + // safe to interpret it as a firing event. + // + // This isn't foolproof. The user can trick us by doing a + // controlled rapid forward push but stopping short of the rest + // position. We'll misinterpret that as a firing event. But + // that's not a natural motion that a user would make with a + // plunger, so it's probably an acceptable false positive. + // + // Possible future enhancement: we could add a second physical + // sensor that detects when the plunger reaches the zero position + // and asserts an interrupt. In the interrupt handler, set a + // flag indicating the zero position signal. On each scan of + // the CCD, also check that flag; if it's set, enter firing + // event mode just as we do now. The key here is that the + // secondary sensor would have to be something much faster + // than our CCD scan - it would have to react on, say, the + // millisecond time scale. A simple mechanical switch or a + // proximity sensor could work. This would let us detect + // with certainty when the plunger physically fires, eliminating + // the case where the use can fool us with motion that's fast + // enough to look like a release but doesn't actually reach the + // starting position. // // To detremine when a firing even occurs, we watch for rapid // motion from a retracted position towards the rest position - @@ -1264,12 +1316,38 @@ // position, and then suspend reports until the mechanical // readings indicate that the plunger has come to rest (indicated // by several readings in a row at roughly the same position). - - // Check to see if plunger firing is in progress. If not, check - // to see if it looks like we just started firing. - const int restTol = JOYMAX/npix * 4; - const int fireTol = JOYMAX/npix * 12; - if (firing) + // + // Tolerance for firing is 1/3 of the current pull distance, or + // about 1/2", whichever is greater. Making this value too small + // makes for too many false positives. Empirically, 1/4" is too + // twitchy, so set a floor at about 1/2". But we can be less + // sensitive the further back the plunger is pulled, since even + // a long pull will snap back quickly. Note that JOYMAX always + // corresponds to about 3", no matter how many pixels we're + // reading, since the physical sensor is about 3" long; so we + // factor out the pixel count calculate (approximate) physical + // distances based on the normalized axis range. + // + // Firing pattern: when firing, don't simply report a solid 0, + // but instead report a series of pseudo-bouces. This looks + // more realistic, beacause the real plunger is also bouncing + // around during this time. To get maximum firing power in + // the simulation, though, our pseudo-bounces are tiny cmopared + // to the real thing. + const int restTol = JOYMAX/24; + int fireTol = z/3 > JOYMAX/6 ? z/3 : JOYMAX/6; + static const int firePattern[] = { + -JOYMAX/12, -JOYMAX/12, -JOYMAX/12, + 0, 0, 0, + JOYMAX/16, JOYMAX/16, JOYMAX/16, + 0, 0, 0, + -JOYMAX/20, -JOYMAX/20, -JOYMAX/20, + 0, 0, 0, + JOYMAX/24, JOYMAX/24, JOYMAX/24, + 0, 0, 0, + -JOYMAX/30, -JOYMAX/30, -JOYMAX/30 + }; + if (firing != 0) { // Firing in progress - we've already told VP to send its // model plunger all the way back to the rest position, so @@ -1278,11 +1356,23 @@ if (abs(z0 - z2) < restTol && abs(znew - z2) < restTol) { // the plunger is back at rest - firing is done - firing = false; + firing = 0; // resume normal reporting z = z2; } + else if (firing < countof(firePattern)) + { + // firing - report the next position in the pseudo-bounce + // pattern + z = firePattern[firing++]; + } + else + { + // firing, out of pseudo-bounce items - just report the + // rest position + z = 0; + } } else if (z0 < z2 && z1 < z2 && znew < z2 && (z0 < z2 - fireTol @@ -1305,8 +1395,10 @@ // virtual plunger, rather than imposing the actual // mechanical charateristics of the physical plunger on // every table. - firing = true; - z = 0; + firing = 1; + + // report the first firing pattern position + z = firePattern[0]; } else { @@ -1320,10 +1412,16 @@ z1 = z0; z0 = znew; } + else + { + // plunger disabled - pause 10ms to throttle updates to a + // reasonable pace + wait_ms(10); + } // read the accelerometer - int xa, ya, rxa, rya; - accel.get(xa, ya, rxa, rya); + int xa, ya; + accel.get(xa, ya); // confine the results to our joystick axis range if (xa < -JOYMAX) xa = -JOYMAX; @@ -1342,7 +1440,7 @@ // arrangement of our nominal axes aligns with VP's standard // setting, so that we can configure VP with X Axis = X on the // joystick and Y Axis = Y on the joystick. - js.update(y, x, z, rxa, rya, 0); + js.update(y, x, z, 0, statusFlags); #ifdef DEBUG_PRINTF if (x != 0 || y != 0)