Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
Diff: main.cpp
- Revision:
- 48:058ace2aed1d
- Parent:
- 47:df7a88cd249c
- Child:
- 49:37bd97eb7688
--- a/main.cpp Thu Feb 18 07:32:20 2016 +0000 +++ b/main.cpp Fri Feb 26 18:42:03 2016 +0000 @@ -10,7 +10,7 @@ * 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 +* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILIT Y, 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. @@ -20,12 +20,12 @@ // The Pinscape Controller // A comprehensive input/output controller for virtual pinball machines // -// This project implements an I/O controller for virtual pinball cabinets. Its -// function is to connect Windows pinball software, such as Visual Pinball, with -// physical devices in the cabinet: buttons, sensors, and feedback devices that -// create visual or mechanical effects during play. +// This project implements an I/O controller for virtual pinball cabinets. The +// controller's function is to connect Visual Pinball (and other Windows pinball +// emulators) with physical devices in the cabinet: buttons, sensors, and +// feedback devices that create visual or mechanical effects during play. // -// The software can perform several different functions, which can be used +// The controller can perform several different functions, which can be used // individually or in any combination: // // - Nudge sensing. This uses the KL25Z's on-board accelerometer to sense the @@ -153,19 +153,19 @@ // STATUS LIGHTS: The on-board LED on the KL25Z flashes to indicate the current // device status. The flash patterns are: // -// two short red flashes = the device is powered but hasn't successfully -// connected to the host via USB (either it's not physically connected -// to the USB port, or there was a problem with the software handshake -// with the USB device driver on the computer) +// short yellow flash = waiting to connect // -// short red flash = the host computer is in sleep/suspend mode +// short red flash = the connection is suspended (the host is in sleep +// or suspend mode, the USB cable is unplugged after a connection +// has been established) +// +// two short red flashes = connection lost (the device should immediately +// go back to short-yellow "waiting to reconnect" mode when a connection +// is lost, so this display shouldn't normally appear) // // long red/yellow = USB connection problem. The device still has a USB -// connection to the host, but data transmissions are failing. This -// condition shouldn't ever occur; if it does, it probably indicates -// a bug in the device's USB software. This display is provided to -// flag any occurrences for investigation. You'll probably need to -// manually reset the device if this occurs. +// connection to the host (or so it appears to the device), but data +// transmissions are failing. // // long yellow/green = everything's working, but the plunger hasn't // been calibrated. Follow the calibration procedure described in @@ -175,13 +175,27 @@ // alternating blue/green = everything's working normally, and plunger // calibration has been completed (or there's no plunger attached) // +// fast red/purple = out of memory. The controller halts and displays +// this diagnostic code until you manually reset it. If this happens, +// it's probably because the configuration is too complex, in which +// case the same error will occur after the reset. If it's stuck +// in this cycle, you'll have to restore the default configuration +// by re-installing the controller software (the Pinscape .bin file). // -// USB PROTOCOL: please refer to USBProtocol.h for details on the USB -// message protocol. +// +// USB PROTOCOL: Most of our USB messaging is through standard USB HID +// classes (joystick, keyboard). We also accept control messages on our +// primary HID interface "OUT endpoint" using a custom protocol that's +// not defined in any USB standards (we do have to provide a USB HID +// Report Descriptor for it, but this just describes the protocol as +// opaque vendor-defined bytes). The control protocol incorporates the +// LedWiz protocol as a subset, and adds our own private extensions. +// For full details, see USBProtocol.h. #include "mbed.h" #include "math.h" +#include "pinscape.h" #include "USBJoystick.h" #include "MMA8451Q.h" #include "tsl1410r.h" @@ -194,10 +208,37 @@ #include "ccdSensor.h" #include "potSensor.h" #include "nullSensor.h" +#include "TinyDigitalIn.h" #define DECL_EXTERNS #include "config.h" +// -------------------------------------------------------------------------- +// +// Custom memory allocator. We use our own version of malloc() to provide +// diagnostics if we run out of heap. +// +void *xmalloc(size_t siz) +{ + // allocate through the normal library malloc; if that succeeds, + // simply return the pointer we got from malloc + void *ptr = malloc(siz); + if (ptr != 0) + return ptr; + + // failed - display diagnostics + for (;;) + { + diagLED(1, 0, 0); + wait(.2); + diagLED(1, 0, 1); + wait(.2); + } +} + +// overload operator new to call our custom malloc +void *operator new(size_t siz) { return xmalloc(siz); } +void *operator new[](size_t siz) { return xmalloc(siz); } // --------------------------------------------------------------------------- // @@ -209,9 +250,6 @@ // --------------------------------------------------------------------------- // utilities -// number of elements in an array -#define countof(x) (sizeof(x)/sizeof((x)[0])) - // floating point square of a number inline float square(float x) { return x*x; } @@ -762,13 +800,19 @@ switch (typ) { case PortTypeGPIOPWM: - // PWM GPIO port - lwp = new LwPwmOut(wirePinName(pin), activeLow ? 255 : 0); + // PWM GPIO port - assign if we have a valid pin + if (pin != 0) + lwp = new LwPwmOut(wirePinName(pin), activeLow ? 255 : 0); + else + lwp = new LwVirtualOut(); break; case PortTypeGPIODig: // Digital GPIO port - lwp = new LwDigOut(wirePinName(pin), activeLow ? 255 : 0); + if (pin != 0) + lwp = new LwDigOut(wirePinName(pin), activeLow ? 255 : 0); + else + lwp = new LwVirtualOut(); break; case PortTypeTLC5940: @@ -1125,17 +1169,17 @@ } // DigitalIn for the button - DigitalIn *di; + TinyDigitalIn *di; // current PHYSICAL on/off state, after debouncing - uint8_t on; + uint8_t on : 1; // current LOGICAL on/off state as reported to the host. - uint8_t pressed; + uint8_t pressed : 1; // previous logical on/off state, when keys were last processed for USB // reports and local effects - uint8_t prev; + uint8_t prev : 1; // Debounce history. On each scan, we shift in a 1 bit to the lsb if // the physical key is reporting ON, and shift in a 0 bit if the physical @@ -1188,7 +1232,7 @@ uint8_t pulseState; float pulseTime; -} buttonState[MAX_BUTTONS]; +} __attribute__((packed)) buttonState[MAX_BUTTONS]; // Button data @@ -1200,7 +1244,7 @@ struct { bool changed; // flag: changed since last report sent - int nkeys; // number of active keys in the list + uint8_t nkeys; // number of active keys in the list uint8_t data[8]; // key state, in USB report format: byte 0 is the modifier key mask, // byte 1 is reserved, and bytes 2-7 are the currently pressed key codes } kbState = { false, 0, { 0, 0, 0, 0, 0, 0, 0, 0 } }; @@ -1266,7 +1310,7 @@ if (pin != NC) { // set up the GPIO input pin for this button - bs->di = new DigitalIn(pin); + bs->di = new TinyDigitalIn(pin); // if it's a pulse mode button, set the initial pulse state to Off if (cfg.button[i].flags & BtnFlagPulse) @@ -1641,7 +1685,7 @@ p->addAvg(ax, ay); // check for auto-centering every so often - if (tCenter_.read_ms() > 1000) + if (tCenter_.read_us() > 1000000) { // add the latest raw sample to the history list AccHist *prv = p; @@ -1694,7 +1738,6 @@ // of making the system more fault-tolerant. if (tInt_.read() > 1.0f) { - printf("unwedging the accelerometer\r\n"); float x, y, z; mma_.getAccXYZ(x, y, z); } @@ -2265,6 +2308,786 @@ } } +// Plunger reader +class PlungerReader +{ +public: + PlungerReader() + { + // not in a firing event yet + firing = 0; + + // no history yet + histIdx = 0; + + // not in calibration mode + cal = false; + } + + // Collect a reading from the plunger sensor. The main loop calls + // this frequently to read the current raw position data from the + // sensor. We analyze the raw data to produce the calibrated + // position that we report to the PC via the joystick interface. + void read() + { + // Read a sample from the sensor + PlungerReading r; + if (plungerSensor->read(r)) + { + // if in calibration mode, apply it to the calibration + if (cal) + { + // if it's outside of the current calibration bounds, + // expand the bounds + if (r.pos < cfg.plunger.cal.min) + cfg.plunger.cal.min = r.pos; + if (r.pos < cfg.plunger.cal.zero) + cfg.plunger.cal.zero = r.pos; + if (r.pos > cfg.plunger.cal.max) + cfg.plunger.cal.max = r.pos; + + // As long as we're in calibration mode, return the raw + // sensor position as the joystick value, adjusted to the + // JOYMAX scale. + z = int16_t((long(r.pos) * JOYMAX)/65535); + return; + } + + // If the new reading is within 2ms of the previous reading, + // ignore it. We require a minimum time between samples to + // ensure that we have a usable amount of precision in the + // denominator (the time interval) for calculating the plunger + // velocity. (The CCD sensor can't take readings faster than + // this anyway, but other sensor types, such as potentiometers, + // can, so we have to throttle the rate artifically in case + // we're using a fast sensor like that.) + if (uint32_t(r.t - prv.t) < 2000UL) + return; + + // bounds-check the calibration data + checkCalBounds(r.pos); + + // calibrate and rescale the value + int pos = int( + (long(r.pos - cfg.plunger.cal.zero) * JOYMAX) + / (cfg.plunger.cal.max - cfg.plunger.cal.zero)); + + // Calculate the velocity from the previous reading to here, + // in joystick distance units per microsecond. + // + // For reference, the physical plunger velocity ranges up + // to about 100,000 joystick distance units/sec. This is + // based on empirical measurements. The typical time for + // a real plunger to travel the full distance when released + // from full retraction is about 85ms, so the average velocity + // covering this distance is about 56,000 units/sec. The + // peak is probably about twice that. In real-world units, + // this translates to an average speed of about .75 m/s and + // a peak of about 1.5 m/s. + // + // Note that we actually calculate the value here in units + // per *microsecond* - the discussion above is in terms of + // units/sec because that's more on a human scale. Our + // choice of internal units here really isn't important, + // since we only use the velocity for comparison purposes, + // to detect acceleration trends. We therefore save ourselves + // a little CPU time by using the natural units of our inputs. + float v = float(pos - prv.pos)/float(r.t - prv.t); + + // presume we'll just report the latest reading + z = pos; + vz = v; + + // Check firing events + switch (firing) + { + case 0: + // Default state - not in a firing event. + + // Check for a recent high water mark. Keep the high point + // within a small window + + // If we have forward motion from a position that's retracted + // beyond a threshold, enter phase 1. + if (v < 0 && pos > JOYMAX/6) + { + // enter phase 1 + firingMode(1); + + // we don't have a freeze position yet, but note the start time + f1.pos = 0; + f1.t = r.t; + + // Figure the fake "bounce" position in case we complete the + // firing event. This is the barrel spring compression proportional + // to the starting position. The barrel spring is about 1/6 the + // length of the main spring, so figure it compresses by 1/6 the + // distance. + f2.pos = -pos/6; + } + break; + + case 1: + // Phase 1 - acceleration. If we cross the zero point, trigger + // the firing event. Otherwise, continue monitoring as long as we + // see acceleration in the forward direction. + if (pos <= 0) + { + // switch to the synthetic firing mode + firingMode(2); + + // note the start time for the firing phase + f2.t = r.t; + } + else if (v < vprv) + { + // We're still accelerating, and we haven't crossed the zero + // point yet - stay in phase 1. (Note that forward motion is + // negative velocity, so accelerating means that the new + // velocity is more negative than the previous one, which + // is to say numerically less than - that's why the test + // for acceleration is the seemingly backwards 'v < vprv'.) + } + else if (uint32_t(r.t - prv.t) < 5000UL) + { + // We're not accelerating relative to the previous reading, + // but we're within 5ms of it. Throw out this reading to + // collect more data. + pos = prv.pos; + r.t = prv.t; + v = vprv; + } + else + { + // We're not accelerating. Cancel the firing event. + firingMode(0); + } + + // If we've been in phase 1 for at least 25ms, we're probably + // really doing a release. Jump back to the recent local + // maximum where the release *really* started. This is always + // a bit before we started seeing sustained accleration, because + // the plunger motion for the first few milliseconds is too slow + // for our sensor precision to reliably detect acceleration. + if (firing == 1) + { + if (f1.pos != 0) + { + // we have a reset point - freeze there + z = f1.pos; + } + else if (uint32_t(r.t - f1.t) >= 25000UL) + { + // it's been long enough - set a reset point. + f1.pos = z = histLocalMax(r.t, 50000UL); + } + } + break; + + case 2: + // Phase 2 - start of synthetic firing event. Report the fake + // bounce for 25ms. VP polls the joystick about every 10ms, so + // this should be enough time to guarantee that VP sees this + // report at least once. + if (uint32_t(r.t - f2.t) < 25000UL) + { + // report the bounce position + z = f2.pos; + } + else + { + // it's been long enough - switch to phase 3, where we + // report the park position until the real plunger comes + // to rest + firingMode(3); + z = 0; + + // set the start of the "stability window" to the rest position + f3s.t = r.t; + f3s.pos = 0; + + // set the start of the "retraction window" to the actual position + f3r = r; + } + break; + + case 3: + // Phase 3 - in synthetic firing event. Report the park position + // until the plunger position stabilizes. Left to its own devices, + // the plunger will usualy bounce off the barrel spring several + // times before coming to rest, so we'll see oscillating motion + // for a second or two. In the simplest case, we can aimply wait + // for the plunger to stop moving for a short time. However, the + // player might intervene by pulling the plunger back again, so + // watch for that motion as well. If we're just bouncing freely, + // we'll see the direction change frequently. If the player is + // moving the plunger manually, the direction will be constant + // for longer. + if (v >= 0) + { + // We're moving back (or standing still). If this has been + // going on for a while, the user must have taken control. + if (uint32_t(r.t - f3r.t) > 65000UL) + { + // user has taken control - cancel firing mode + firingMode(0); + break; + } + } + else + { + // forward motion - reset retraction window + f3r.t = r.t; + } + + // check if we've come to rest, or close enough + if (abs(r.pos - f3s.pos) < 200) + { + // It's within an eighth inch of the last starting point. + // If it's been here for 30ms, consider it stable. + if (uint32_t(r.t - f3s.t) > 30000UL) + { + // we're done with the firing event + firingMode(0); + } + else + { + // it's close to the last position but hasn't been + // here long enough; stay in firing mode and continue + // to report the park position + z = 0; + } + } + else + { + // It's not close enough to the last starting point, so use + // this as a new starting point, and stay in firing mode. + f3s = r; + z = 0; + } + break; + } + + // save the new reading for next time + prv.pos = pos; + prv.t = r.t; + vprv = v; + + // add it to the circular history buffer as well + hist[histIdx++] = prv; + histIdx %= countof(hist); + } + } + + // Get the current value to report through the joystick interface + int16_t getPosition() const { return z; } + + // Get the current velocity (joystick distance units per microsecond) + float getVelocity() const { return vz; } + + // get the timestamp of the current joystick report (microseconds) + uint32_t getTimestamp() const { return prv.t; } + + // Set calibration mode on or off + void calMode(bool f) + { + // if entering calibration mode, reset the saved calibration data + if (f && !cal) + cfg.plunger.cal.begin(); + + // remember the new mode + cal = f; + } + + // is a firing event in progress? + bool isFiring() { return firing > 3; } + +private: + // set a firing mode + inline void firingMode(int m) + { + firing = m; + + // $$$ + lwPin[3]->set(0); + lwPin[4]->set(0); + lwPin[5]->set(0); + switch (m) + { + case 1: lwPin[3]->set(255); break; // red + case 2: lwPin[4]->set(255); break; // green + case 3: lwPin[5]->set(255); break; // blue + case 4: lwPin[3]->set(255); lwPin[5]->set(255); break; // purple + } + //$$$ + } + + // Find the most recent local maximum in the history data, up to + // the given time limit. + int histLocalMax(uint32_t tcur, uint32_t dt) + { + // start with the prior entry + int idx = (histIdx == 0 ? countof(hist) : histIdx) - 1; + int hi = hist[idx].pos; + + // scan backwards for a local maximum + for (int n = countof(hist) - 1 ; n > 0 ; idx = (idx == 0 ? countof(hist) : idx) - 1) + { + // if this isn't within the time window, stop + if (uint32_t(tcur - hist[idx].t) > dt) + break; + + // if this isn't above the current hith, stop + if (hist[idx].pos < hi) + break; + + // this is the new high + hi = hist[idx].pos; + } + + // return the local maximum + return hi; + } + + // Adjust the calibration bounds for a new reading. This is used + // while NOT in calibration mode to ensure that a reading doesn't + // violate the calibration limits. If it does, we'll readjust the + // limits to incorporate the new value. + void checkCalBounds(int pos) + { + // If the value is beyond the calibration maximum, increase the + // calibration point. This ensures that our joystick reading + // is always within the valid joystick field range. + if (pos > cfg.plunger.cal.max) + cfg.plunger.cal.max = pos; + + // make sure we don't overflow in the opposite direction + if (pos < cfg.plunger.cal.zero + && cfg.plunger.cal.zero - pos > cfg.plunger.cal.max) + { + // we need to raise 'max' by this much to keep things in range + int adj = cfg.plunger.cal.zero - pos - cfg.plunger.cal.max; + + // we can raise 'max' at most this much before overflowing + int lim = 0xffff - cfg.plunger.cal.max; + + // if we have headroom to raise 'max' by 'adj', do so, otherwise + // raise it as much as we can and apply the excess to lowering the + // zero point + if (adj > lim) + { + cfg.plunger.cal.zero -= adj - lim; + adj = lim; + } + cfg.plunger.cal.max += adj; + } + + // If the calibration max isn't higher than the calibration + // zero, we have a negative or zero scale range, which isn't + // physically meaningful. Fix it by forcing the max above + // the zero point (or the zero point below the max, if they're + // both pegged at the datatype maximum). + if (cfg.plunger.cal.max <= cfg.plunger.cal.zero) + { + if (cfg.plunger.cal.zero != 0xFFFF) + cfg.plunger.cal.max = cfg.plunger.cal.zero + 1; + else + cfg.plunger.cal.zero -= 1; + } + } + + // Previous reading + PlungerReading prv; + + // velocity at previous reading + float vprv; + + // Circular buffer of recent readings. We keep a short history + // of readings to analyze during firing events. We can only identify + // a firing event once it's somewhat under way, so we need a little + // retrospective information to accurately determine after the fact + // exactly when it started. We throttle our readings to no more + // than one every 2ms, so we have at least N*2ms of history in this + // array. + PlungerReading hist[25]; + int histIdx; + + // Firing event state. + // + // A "firing event" happens when we detect that the physical plunger + // is moving forward fast enough that it was probably released. When + // we detect a firing event, we momentarily disconnect the joystick + // readings from the physical sensor, and instead feed in a series of + // synthesized readings that simulate an idealized release motion. + // + // The reason we create these synthetic readings is that they give us + // better results in VP and other PC pinball players. The joystick + // interface only lets us report the instantaneous plunger position. + // VP only reads the position at certain intervals, so it picks up + // a series of snapshots of the position, which it uses to infer the + // plunger velocity. But the plunger release motion is so fast that + // VP's sampling rate creates a classic digital "aliasing" problem. + // + // Our synthesized report structure is designed to overcome the + // aliasing problem by removing the intermediate position reports + // and only reporting the starting and ending positions. This + // allows the PC side to reliably read the extremes of the travel + // and work entirely in the simulation domain to simulate a plunger + // release of the detected distance. This produces more realistic + // results than feeding VP the real data, ironically. + // + // DETECTING A RELEASE MOTION + // + // How do we tell when the plunger is being released? The basic + // idea is to monitor the sensor data and look for a series of + // readings that match the profile of a release motion. For an + // idealized, mathematical model of a plunger, a release causes + // the plunger to start accelerating under the spring force. + // + // The real system has a couple of complications. First, there + // are some mechanical effects that make the motion less than + // ideal (in the sense of matching the mathematical model), + // like friction and wobble. This seems to be especially + // significant for the first 10-20ms of the release, probably + // because friction is a bigger factor at slow speeds, and + // also because of the uneven forces as the user lets go. + // Second, our sensor doesn't have infinite precision, and + // our clock doesn't either, and these error bars compound + // when we combine position and time to compute velocity. + // + // To deal with these real-world complications, we have a couple + // of strategies. First, we tolerate a little bit of non-uniformity + // in the acceleration, by waiting a little longer if we get a + // reading that doesn't appear to be accelerating. We still + // insist on continuous acceleration, but we basically double-check + // a reading by extending the time window when necessary. Second, + // when we detect a series of accelerating readings, we go back + // to prior readings from before the sustained acceleration + // began to find out when the motion really began. + // + // PROCESSING A RELEASE MOTION + // + // We continuously monitor the sensor data. When we see the position + // moving forward, toward the zero point, we start watching for + // sustained acceleration . If we see acceleration for more than a + // minimum threshold time (about 20ms), we freeze the reported + // position at the recent local maximum (from the recent history of + // readings) and wait for the acceleration to stop or for the plunger + // to cross the zero position. If it crosses the zero position + // while still accelerating, we initiate a firing event. Otherwise + // we return to instantaneous reporting of the actual position. + // + // HOW THIS LOOKS TO THE USER + // + // The typical timing to reach the zero point during a release + // is about 60-80ms. This is essentially the longest that we can + // stay in phase 1, so it's the longest that the readings will be + // frozen while we try to decide about a firing event. This is + // fast enough that it should be barely perceptible to the user. + // The synthetic firing event should trigger almost immediately + // upon releasing the plunger, from the user's perspective. + // + // The big danger with this approach is "false positives": + // mistaking manual motion under the user's control for a possible + // firing event. A false positive would produce a highly visible + // artifact, namely the on-screen plunger freezing in place while + // the player moves the real plunger. The strategy we use makes it + // almost impossible for this to happen long enough to be + // perceptible. To fool the system, you have to accelerate the + // plunger very steadily - with about 5ms granularity. It's + // really hard to do this, and especially unlikely that a user + // would do so accidentally. + // + // FIRING STATE VARIABLE + // + // The firing states are: + // + // 0 - Default state. We report the real instantaneous plunger + // position to the joystick interface. + // + // 1 - Phase 1 - acceleration + // + // 2 - Firing event started. We report the "bounce" position for + // a minimum time. + // + // 3 - Firing event hold. We report the rest position for a + // minimum interval, or until the real plunger comes to rest + // somewhere, whichever comes first. + // + int firing; + + // Position/timestamp at start of firing phase 1. We freeze the + // joystick reports at this position until we decide whether or not + // we're actually in a firing event. This isn't set until we're + // confident that we've been in the accleration phase for long + // enough; pos is non-zero when this is valid. + PlungerReading f1; + + // Position/timestamp at start of firing phase 2. The position is + // the fake "bounce" position we report during this phase, and the + // timestamp tells us when the phase began so that we can end it + // after enough time elapses. + PlungerReading f2; + + // Position/timestamp of start of stability window during phase 3. + // We use this to determine when the plunger comes to rest. We set + // this at the beginning of phase 4, and then reset it when the + // plunger moves too far from the last position. + PlungerReading f3s; + + // Position/timestamp of start of retraction window during phase 3. + // We use this to determine if the user is drawing the plunger back. + // If we see retraction motion for more than about 65ms, we assume + // that the user has taken over, because we should see forward + // motion within this timeframe if the plunger is just bouncing + // freely. + PlungerReading f3r; + + // flag: we're in calibration mode + bool cal; + + // next Z value to report to the joystick interface (in joystick + // distance units) + int z; + + // velocity of this reading (joystick distance units per microsecond) + float vz; +}; + +// plunger reader singleton +PlungerReader plungerReader; + +// --------------------------------------------------------------------------- +// +// Handle the ZB Launch Ball feature. +// +// The ZB Launch Ball feature, if enabled, lets the mechanical plunger +// serve as a substitute for a physical Launch Ball button. When a table +// is loaded in VP, and the table has the ZB Launch Ball LedWiz port +// turned on, we'll disable mechanical plunger reports through the +// joystick interface and instead use the plunger only to simulate the +// Launch Ball button. When the mode is active, pulling back and +// releasing the plunger causes a brief simulated press of the Launch +// button, and pushing the plunger forward of the rest position presses +// the Launch button as long as the plunger is pressed forward. +// +// This feature has two configuration components: +// +// - An LedWiz port number. This port is a "virtual" port that doesn't +// have to be attached to any actual output. DOF uses it to signal +// that the current table uses a Launch button instead of a plunger. +// DOF simply turns the port on when such a table is loaded and turns +// it off at all other times. We use it to enable and disable the +// plunger/launch button connection. +// +// - A joystick button ID. We simulate pressing this button when the +// launch feature is activated via the LedWiz port and the plunger is +// either pulled back and releasd, or pushed forward past the rest +// position. +// +class ZBLaunchBall +{ +public: + ZBLaunchBall() + { + // start in the default state + lbState = 0; + + // get the button bit for the ZB Launch Ball button + lbButtonBit = (1 << (cfg.plunger.zbLaunchBall.btn - 1)); + + // start the state transition timer + lbTimer.start(); + } + + // Update state. This checks the current plunger position and + // the timers to see if the plunger is in a position that simulates + // a Launch Ball button press via the ZB Launch Ball feature. + // Updates the simulated button vector according to the current + // launch ball state. The main loop calls this before each + // joystick update to figure the new simulated button state. + void update(uint32_t &simButtons) + { + // Check for a simulated Launch Ball button press, if enabled + if (cfg.plunger.zbLaunchBall.port != 0) + { + int znew = plungerReader.getPosition(); + const int cockThreshold = JOYMAX/3; + const uint16_t pushThreshold = uint16_t(-JOYMAX/3.0 * cfg.plunger.zbLaunchBall.pushDistance/1000.0 * 65535.0); + int newState = lbState; + switch (lbState) + { + case 0: + // Base state. If the plunger is pulled back by an inch + // or more, go to "cocked" state. If the plunger is pushed + // forward by 1/4" or more, go to "pressed" state. + if (znew >= cockThreshold) + newState = 1; + else if (znew <= pushThreshold) + newState = 5; + break; + + case 1: + // Cocked state. If a firing event is now in progress, + // go to "launch" state. Otherwise, if the plunger is less + // than 1" retracted, go to "uncocked" state - the player + // might be slowly returning the plunger to rest so as not + // to trigger a launch. + if (plungerReader.isFiring() || znew <= 0) + newState = 3; + else if (znew < cockThreshold) + newState = 2; + break; + + case 2: + // Uncocked state. If the plunger is more than an inch + // retracted, return to cocked state. If we've been in + // the uncocked state for more than half a second, return + // to the base state. This allows the user to return the + // plunger to rest without triggering a launch, by moving + // it at manual speed to the rest position rather than + // releasing it. + if (znew >= cockThreshold) + newState = 1; + else if (lbTimer.read_us() > 500000) + newState = 0; + break; + + case 3: + // Launch state. If the plunger is no longer pushed + // forward, switch to launch rest state. + if (znew >= 0) + newState = 4; + break; + + case 4: + // Launch rest state. If the plunger is pushed forward + // again, switch back to launch state. If not, and we've + // been in this state for at least 200ms, return to the + // default state. + if (znew <= pushThreshold) + newState = 3; + else if (lbTimer.read_us() > 200000) + newState = 0; + break; + + case 5: + // Press-and-Hold state. If the plunger is no longer pushed + // forward, AND it's been at least 50ms since we generated + // the simulated Launch Ball button press, return to the base + // state. The minimum time is to ensure that VP has a chance + // to see the button press and to avoid transient key bounce + // effects when the plunger position is right on the threshold. + if (znew > pushThreshold && lbTimer.read_us() > 50000) + newState = 0; + break; + } + + // change states if desired + if (newState != lbState) + { + // If we're entering Launch state OR we're entering the + // Press-and-Hold state, AND the ZB Launch Ball LedWiz signal + // is turned on, simulate a Launch Ball button press. + if (((newState == 3 && lbState != 4) || newState == 5) + && wizOn[cfg.plunger.zbLaunchBall.port-1]) + { + lbBtnTimer.reset(); + lbBtnTimer.start(); + simButtons |= lbButtonBit; + } + + // if we're switching to state 0, release the button + if (newState == 0) + simButtons &= ~(1 << (cfg.plunger.zbLaunchBall.btn - 1)); + + // switch to the new state + lbState = newState; + + // start timing in the new state + lbTimer.reset(); + } + + // If the Launch Ball button press is in effect, but the + // ZB Launch Ball LedWiz signal is no longer turned on, turn + // off the button. + // + // If we're in one of the Launch states (state #3 or #4), + // and the button has been on for long enough, turn it off. + // The Launch mode is triggered by a pull-and-release gesture. + // From the user's perspective, this is just a single gesture + // that should trigger just one momentary press on the Launch + // Ball button. Physically, though, the plunger usually + // bounces back and forth for 500ms or so before coming to + // rest after this gesture. That's what the whole state + // #3-#4 business is all about - we stay in this pair of + // states until the plunger comes to rest. As long as we're + // in these states, we won't send duplicate button presses. + // But we also don't want the one button press to continue + // the whole time, so we'll time it out now. + // + // (This could be written as one big 'if' condition, but + // I'm breaking it out verbosely like this to make it easier + // for human readers such as myself to comprehend the logic.) + if ((simButtons & lbButtonBit) != 0) + { + int turnOff = false; + + // turn it off if the ZB Launch Ball signal is off + if (!wizOn[cfg.plunger.zbLaunchBall.port-1]) + turnOff = true; + + // also turn it off if we're in state 3 or 4 ("Launch"), + // and the button has been on long enough + if ((lbState == 3 || lbState == 4) && lbBtnTimer.read_us() > 250000) + turnOff = true; + + // if we decided to turn off the button, do so + if (turnOff) + { + lbBtnTimer.stop(); + simButtons &= ~lbButtonBit; + } + } + } + } + +private: + // Simulated Launch Ball button state. If a "ZB Launch Ball" port is + // defined for our LedWiz port mapping, any time that port is turned ON, + // we'll simulate pushing the Launch Ball button if the player pulls + // back and releases the plunger, or simply pushes on the plunger from + // the rest position. This allows the plunger to be used in lieu of a + // physical Launch Ball button for tables that don't have plungers. + // + // States: + // 0 = default + // 1 = cocked (plunger has been pulled back about 1" from state 0) + // 2 = uncocked (plunger is pulled back less than 1" from state 1) + // 3 = launching, plunger is forward beyond park position + // 4 = launching, plunger is behind park position + // 5 = pressed and holding (plunger has been pressed forward beyond + // the park position from state 0) + int lbState; + + // button bit for ZB launch ball button + uint32_t lbButtonBit; + + // Time since last lbState transition. Some of the states are time- + // sensitive. In the "uncocked" state, we'll return to state 0 if + // we remain in this state for more than a few milliseconds, since + // it indicates that the plunger is being slowly returned to rest + // rather than released. In the "launching" state, we need to release + // the Launch Ball button after a moment, and we need to wait for + // the plunger to come to rest before returning to state 0. + Timer lbTimer; + + // Launch Ball simulated push timer. We start this when we simulate + // the button push, and turn off the simulated button when enough time + // has elapsed. + Timer lbBtnTimer; +}; + // --------------------------------------------------------------------------- // // Reboot - resets the microcontroller @@ -2329,9 +3152,8 @@ // Pixel dump mode - the host requested a dump of image sensor pixels // (helpful for installing and setting up the sensor and light source) bool reportPix = false; -uint8_t reportPixMode; // pixel report mode bits: - // 0x01 -> raw pixels (0) / processed (1) - // 0x02 -> high res scan (0) / low res (1) +uint8_t reportPixFlags; // pixel report flag bits (see ccdSensor.h) +uint8_t reportPixVisMode; // pixel report visualization mode (see ccdSensor.h) // --------------------------------------------------------------------------- @@ -2384,7 +3206,7 @@ // Handle an input report from the USB host. Input reports use our extended // LedWiz protocol. // -void handleInputMsg(LedWizMsg &lwm, USBJoystick &js, int &z) +void handleInputMsg(LedWizMsg &lwm, USBJoystick &js) { // LedWiz commands come in two varieties: SBA and PBA. An // SBA is marked by the first byte having value 64 (0x40). In @@ -2487,10 +3309,6 @@ // update the status flags statusFlags = (statusFlags & ~0x01) | (data[3] & 0x01); - // if the plunger is no longer enabled, use 0 for z reports - if (!cfg.plunger.enabled) - z = 0; - // save the configuration saveConfigToFlash(); @@ -2506,17 +3324,17 @@ // enter calibration mode calBtnState = 3; + plungerReader.calMode(true); calBtnTimer.reset(); - cfg.plunger.cal.begin(); break; case 3: // 3 = pixel dump - // data[2] = mode bits: - // 0x01 -> return processed pixels (default is raw pixels) - // 0x02 -> low res scan (default is high res scan) + // data[2] = flag bits + // data[3] = visualization mode reportPix = true; - reportPixMode = data[2]; + reportPixFlags = data[2]; + reportPixVisMode = data[3]; // show purple until we finish sending the report diagLED(1, 0, 1); @@ -2660,7 +3478,7 @@ // void preConnectFlasher() { - diagLED(1, 0, 0); + diagLED(1, 1, 0); wait(0.05); diagLED(0, 0, 0); } @@ -2712,14 +3530,14 @@ // controllers will need to address their respective controller objects, // which don't exit until we initialize those subsystems. initLwOut(cfg); - + // start the TLC5940 clock if (tlc5940 != 0) tlc5940->start(); // start the TV timer, if applicable startTVTimer(cfg); - + // initialize the button input ports bool kbKeys = false; initButtons(cfg, kbKeys); @@ -2737,6 +3555,8 @@ Timer jsReportTimer; jsReportTimer.start(); + Timer plungerIntervalTimer; plungerIntervalTimer.start(); // $$$ + // Time since we successfully sent a USB report. This is a hacky workaround // for sporadic problems in the USB stack that I haven't been able to figure // out. If we go too long without successfully sending a USB report, we'll @@ -2767,63 +3587,11 @@ // create the accelerometer object Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, MMA8451_INT_PIN); - + // last accelerometer report, in joystick units (we report the nudge // acceleration via the joystick x & y axes, per the VP convention) int x = 0, y = 0; - // last plunger report position, on the 0.0..1.0 normalized scale - float pos = 0; - - // last plunger report, in joystick units (we report the plunger as the - // "z" axis of the joystick, per the VP convention) - int z = 0; - - // most recent prior plunger readings, for tracking release events(z0 is - // reading just before the last one we reported, z1 is the one before that, - // z2 the next before that) - int z0 = 0, z1 = 0, z2 = 0; - - // Simulated "bounce" position when firing. We model the bounce off of - // the barrel spring when the plunger is released as proportional to the - // distance it was retracted just before being released. - int zBounce = 0; - - // Simulated Launch Ball button state. If a "ZB Launch Ball" port is - // defined for our LedWiz port mapping, any time that port is turned ON, - // we'll simulate pushing the Launch Ball button if the player pulls - // back and releases the plunger, or simply pushes on the plunger from - // the rest position. This allows the plunger to be used in lieu of a - // physical Launch Ball button for tables that don't have plungers. - // - // States: - // 0 = default - // 1 = cocked (plunger has been pulled back about 1" from state 0) - // 2 = uncocked (plunger is pulled back less than 1" from state 1) - // 3 = launching, plunger is forward beyond park position - // 4 = launching, plunger is behind park position - // 5 = pressed and holding (plunger has been pressed forward beyond - // the park position from state 0) - int lbState = 0; - - // button bit for ZB launch ball button - const uint32_t lbButtonBit = (1 << (cfg.plunger.zbLaunchBall.btn - 1)); - - // Time since last lbState transition. Some of the states are time- - // sensitive. In the "uncocked" state, we'll return to state 0 if - // we remain in this state for more than a few milliseconds, since - // it indicates that the plunger is being slowly returned to rest - // rather than released. In the "launching" state, we need to release - // the Launch Ball button after a moment, and we need to wait for - // the plunger to come to rest before returning to state 0. - Timer lbTimer; - lbTimer.start(); - - // Launch Ball simulated push timer. We start this when we simulate - // the button push, and turn off the simulated button when enough time - // has elapsed. - Timer lbBtnTimer; - // Simulated button states. This is a vector of button states // for the simulated buttons. We combine this with the physical // button states on each USB joystick report, so we will report @@ -2832,56 +3600,27 @@ // simulated Launch Ball button. uint32_t simButtons = 0; - // Firing in progress: we set this when we detect the start of rapid - // plunger movement from a retracted position towards the rest position. - // - // When we detect a firing event, we send VP a series of synthetic - // reports simulating the idealized plunger motion. The actual physical - // motion is much too fast to report to VP; in the time between two USB - // reports, the plunger can shoot all the way forward, rebound off of - // the barrel spring, bounce back part way, and bounce forward again, - // or even do all of this more than once. This means that sampling the - // physical motion at the USB report rate would create a misleading - // picture of the plunger motion, since our samples would catch the - // plunger at random points in this oscillating motion. From the - // user's perspective, the physical action that occurred is simply that - // the plunger was released from a particular distance, so it's this - // high-level event that we want to convey to VP. To do this, we - // synthesize a series of reports to convey an idealized version of - // the release motion that's perfectly synchronized to the VP reports. - // Essentially we pretend that our USB position samples are exactly - // aligned in time with (1) the point of retraction just before the - // user released the plunger, (2) the point of maximum forward motion - // just after the user released the plunger (the point of maximum - // compression as the plunger bounces off of the barrel spring), and - // (3) the plunger coming to rest at the park position. This series - // of reports is synthetic in the sense that it's not what we actually - // see on the CCD at the times of these reports - the true plunger - // position is oscillating at high speed during this period. But at - // the same time it conveys a more faithful picture of the true physical - // motion to VP, and allows VP to reproduce the true physical motion - // more faithfully in its simulation model, by correcting for the - // relatively low sampling rate in the communication path between the - // real plunger and VP's model plunger. - // - // If 'firing' is non-zero, it's the index of our current report in - // the synthetic firing report series. - int firing = 0; - - // start the first CCD integration cycle + // initialize the plunger sensor plungerSensor->init(); + // set up the ZB Launch Ball monitor + ZBLaunchBall zbLaunchBall; + Timer dbgTimer; dbgTimer.start(); // $$$ plunger debug report timer // we're all set up - now just loop, processing sensor reports and // host requests for (;;) { - // Process incoming reports on the joystick interface. This channel - // is used for LedWiz commands are our extended protocol commands. + // Process incoming reports on the joystick interface. The joystick + // "out" (receive) endpoint is used for LedWiz commands and our + // extended protocol commands. Limit processing time to 5ms to + // ensure we don't starve the input side. LedWizMsg lwm; - while (js.readLedWizMsg(lwm)) - handleInputMsg(lwm, js, z); + Timer lwt; + lwt.start(); + while (js.readLedWizMsg(lwm) && lwt.read_us() < 5000) + handleInputMsg(lwm, js); // check for plunger calibration if (calBtn != 0 && !calBtn->read()) @@ -2898,21 +3637,21 @@ case 1: // pushed, not yet debounced - if the debounce time has // passed, start the hold period - if (calBtnTimer.read_ms() > 50) + if (calBtnTimer.read_us() > 50000) 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() > 2050) + if (calBtnTimer.read_us() > 2050000) { // enter calibration mode calBtnState = 3; calBtnTimer.reset(); // begin the plunger calibration limits - cfg.plunger.cal.begin(); + plungerReader.calMode(true); } break; @@ -2932,10 +3671,11 @@ // 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() > 15000) + if (calBtnState == 3 && calBtnTimer.read_us() > 15000000) { // exit calibration mode calBtnState = 0; + plungerReader.calMode(false); // save the updated configuration cfg.plunger.cal.calibrated = 1; @@ -2954,7 +3694,7 @@ { case 2: // in the hold period - flash the light - newCalBtnLit = ((calBtnTimer.read_ms()/250) & 1); + newCalBtnLit = ((calBtnTimer.read_us()/250000) & 1); break; case 3: @@ -2984,357 +3724,16 @@ diagLED(0, 0, 0); // off } } - - // If the plunger is enabled, and we're not in calibration mode, and - // we're not already in a firing event, and the last plunger reading had - // the plunger pulled back at least a bit, watch for plunger release - // events until it's time for our next USB report. - if (!firing && calBtnState != 3 && cfg.plunger.enabled && z >= JOYMAX/6) - { - // monitor the plunger until it's time for our next report - for (int i = 0 ; i < 20 && jsReportTimer.read_ms() < 12 ; ++i) - { - // do a fast low-res scan; if it's at or past the zero point, - // start a firing event - float pos0; - if (plungerSensor->lowResScan(pos0) && pos0 <= cfg.plunger.cal.zero) - { - firing = 1; - break; - } - } - } - - // read the plunger sensor, if it's enabled and we're not in firing mode - if (cfg.plunger.enabled && !firing) - { - // start with the previous reading, in case we don't have a - // clear result on this frame - int znew = z; - if (plungerSensor->highResScan(pos)) - { - // We have a new reading. If we're in calibration mode, use it - // to figure the new calibration, otherwise adjust the new reading - // for the established calibration. - if (calBtnState == 3) - { - // Calibration mode. If this reading is outside of the current - // calibration bounds, expand the bounds. - if (pos < cfg.plunger.cal.min) - cfg.plunger.cal.min = pos; - if (pos < cfg.plunger.cal.zero) - cfg.plunger.cal.zero = pos; - if (pos > cfg.plunger.cal.max) - cfg.plunger.cal.max = pos; - - // normalize to the full physical range while calibrating - znew = int(round(pos * JOYMAX)); - } - else - { - // Not in calibration mode, so normalize the new reading to the - // established calibration range. - // - // Note that negative values are allowed. Zero represents the - // "park" position, where the plunger sits when at rest. A mechanical - // plunger has a small amount of travel in the "push" direction, - // since the barrel spring can be compressed slightly. Negative - // values represent travel in the push direction. - if (pos > cfg.plunger.cal.max) - pos = cfg.plunger.cal.max; - znew = int(round( - (pos - cfg.plunger.cal.zero) - / (cfg.plunger.cal.max - cfg.plunger.cal.zero) - * JOYMAX)); - } - } - - // If we're not already in a firing event, check to see if the - // new position is forward of the last report. If it is, a firing - // event might have started during the high-res scan. This might - // seem unlikely given that the scan only takes about 5ms, but that - // 5ms represents about 25-30% of our total time between reports, - // there's about a 1 in 4 chance that a release starts during a - // scan. - if (!firing && z0 > 0 && znew < z0) - { - // The plunger has moved forward since the previous report. - // Watch it for a few more ms to see if we can get a stable - // new position. - float pos0; - if (plungerSensor->lowResScan(pos0)) - { - int pos1 = pos0; - Timer tw; - tw.start(); - while (tw.read_ms() < 6) - { - // read the new position - float pos2; - if (plungerSensor->lowResScan(pos2)) - { - // If it's stable over consecutive readings, stop looping. - // Count it as stable if the position is within about 1/8". - // The overall travel of a standard plunger is about 3.2", - // so on our normalized 0.0..1.0 scale, 1.0 equals 3.2", - // thus 1" = .3125 and 1/8" = .0391. - if (fabs(pos2 - pos1) < .0391f) - break; - // If we've crossed the rest position, and we've moved by - // a minimum distance from where we starting this loop, begin - // a firing event. (We require a minimum distance to prevent - // spurious firing from random analog noise in the readings - // when the plunger is actually just sitting still at the - // rest position. If it's at rest, it's normal to see small - // random fluctuations in the analog reading +/- 1% or so - // from the 0 point, especially with a sensor like a - // potentionemeter that reports the position as a single - // analog voltage.) Note that we compare the latest reading - // to the first reading of the loop - we don't require the - // threshold motion over consecutive readings, but any time - // over the stability wait loop. - if (pos1 < cfg.plunger.cal.zero && fabs(pos2 - pos0) > .0391f) - { - firing = 1; - break; - } - - // the new reading is now the prior reading - pos1 = pos2; - } - } - } - } - - // Check for a simulated Launch Ball button press, if enabled - if (cfg.plunger.zbLaunchBall.port != 0) - { - const int cockThreshold = JOYMAX/3; - const int pushThreshold = int(-JOYMAX/3.0 * cfg.plunger.zbLaunchBall.pushDistance/1000.0); - int newState = lbState; - switch (lbState) - { - case 0: - // Base state. If the plunger is pulled back by an inch - // or more, go to "cocked" state. If the plunger is pushed - // forward by 1/4" or more, go to "pressed" state. - if (znew >= cockThreshold) - newState = 1; - else if (znew <= pushThreshold) - newState = 5; - break; - - case 1: - // Cocked state. If a firing event is now in progress, - // go to "launch" state. Otherwise, if the plunger is less - // than 1" retracted, go to "uncocked" state - the player - // might be slowly returning the plunger to rest so as not - // to trigger a launch. - if (firing || znew <= 0) - newState = 3; - else if (znew < cockThreshold) - newState = 2; - break; - - case 2: - // Uncocked state. If the plunger is more than an inch - // retracted, return to cocked state. If we've been in - // the uncocked state for more than half a second, return - // to the base state. This allows the user to return the - // plunger to rest without triggering a launch, by moving - // it at manual speed to the rest position rather than - // releasing it. - if (znew >= cockThreshold) - newState = 1; - else if (lbTimer.read_ms() > 500) - newState = 0; - break; - - case 3: - // Launch state. If the plunger is no longer pushed - // forward, switch to launch rest state. - if (znew >= 0) - newState = 4; - break; - - case 4: - // Launch rest state. If the plunger is pushed forward - // again, switch back to launch state. If not, and we've - // been in this state for at least 200ms, return to the - // default state. - if (znew <= pushThreshold) - newState = 3; - else if (lbTimer.read_ms() > 200) - newState = 0; - break; - - case 5: - // Press-and-Hold state. If the plunger is no longer pushed - // forward, AND it's been at least 50ms since we generated - // the simulated Launch Ball button press, return to the base - // state. The minimum time is to ensure that VP has a chance - // to see the button press and to avoid transient key bounce - // effects when the plunger position is right on the threshold. - if (znew > pushThreshold && lbTimer.read_ms() > 50) - newState = 0; - break; - } - - // change states if desired - if (newState != lbState) - { - // If we're entering Launch state OR we're entering the - // Press-and-Hold state, AND the ZB Launch Ball LedWiz signal - // is turned on, simulate a Launch Ball button press. - if (((newState == 3 && lbState != 4) || newState == 5) - && wizOn[cfg.plunger.zbLaunchBall.port-1]) - { - lbBtnTimer.reset(); - lbBtnTimer.start(); - simButtons |= lbButtonBit; - } - - // if we're switching to state 0, release the button - if (newState == 0) - simButtons &= ~(1 << (cfg.plunger.zbLaunchBall.btn - 1)); - - // switch to the new state - lbState = newState; - - // start timing in the new state - lbTimer.reset(); - } - - // If the Launch Ball button press is in effect, but the - // ZB Launch Ball LedWiz signal is no longer turned on, turn - // off the button. - // - // If we're in one of the Launch states (state #3 or #4), - // and the button has been on for long enough, turn it off. - // The Launch mode is triggered by a pull-and-release gesture. - // From the user's perspective, this is just a single gesture - // that should trigger just one momentary press on the Launch - // Ball button. Physically, though, the plunger usually - // bounces back and forth for 500ms or so before coming to - // rest after this gesture. That's what the whole state - // #3-#4 business is all about - we stay in this pair of - // states until the plunger comes to rest. As long as we're - // in these states, we won't send duplicate button presses. - // But we also don't want the one button press to continue - // the whole time, so we'll time it out now. - // - // (This could be written as one big 'if' condition, but - // I'm breaking it out verbosely like this to make it easier - // for human readers such as myself to comprehend the logic.) - if ((simButtons & lbButtonBit) != 0) - { - int turnOff = false; - - // turn it off if the ZB Launch Ball signal is off - if (!wizOn[cfg.plunger.zbLaunchBall.port-1]) - turnOff = true; - - // also turn it off if we're in state 3 or 4 ("Launch"), - // and the button has been on long enough - if ((lbState == 3 || lbState == 4) && lbBtnTimer.read_ms() > 250) - turnOff = true; - - // if we decided to turn off the button, do so - if (turnOff) - { - lbBtnTimer.stop(); - simButtons &= ~lbButtonBit; - } - } - } - - // If a firing event is in progress, generate synthetic reports to - // describe an idealized version of the plunger motion to VP rather - // than reporting the actual physical plunger position. - // - // We use the synthetic reports during a release event because the - // physical plunger motion when released is too fast for VP to track. - // VP only syncs its internal physics model with the outside world - // about every 10ms. In that amount of time, the plunger moves - // fast enough when released that it can shoot all the way forward, - // bounce off of the barrel spring, and rebound part of the way - // back. The result is the classic analog-to-digital problem of - // sample aliasing. If we happen to time our sample during the - // release motion so that we catch the plunger at the peak of a - // bounce, the digital signal incorrectly looks like the plunger - // is moving slowly forward - VP thinks we went from fully - // retracted to half retracted in the sample interval, whereas - // we actually traveled all the way forward and half way back, - // so the speed VP infers is about 1/3 of the actual speed. - // - // To correct this, we take advantage of our ability to sample - // the CCD image several times in the course of a VP report. If - // we catch the plunger near the origin after we've seen it - // retracted, we go into Release Event mode. During this mode, - // we stop reporting the true physical plunger position, and - // instead report an idealized pattern: we report the plunger - // immediately shooting forward to a position in front of the - // park position that's in proportion to how far back the plunger - // was just before the release, and we then report it stationary - // at the park position. We continue to report the stationary - // park position until the actual physical plunger motion has - // stabilized on a new position. We then exit Release Event - // mode and return to reporting the true physical position. - if (firing) - { - // Firing in progress. Keep reporting the park position - // until the physical plunger position comes to rest. - const int restTol = JOYMAX/24; - if (firing == 1) - { - // For the first couple of frames, show the plunger shooting - // forward past the zero point, to simulate the momentum carrying - // it forward to bounce off of the barrel spring. Show the - // bounce as proportional to the distance it was retracted - // in the prior report. - z = zBounce = -z0/6; - ++firing; - } - else if (firing == 2) - { - // second frame - keep the bounce a little longer - z = zBounce; - ++firing; - } - else if (firing > 4 - && abs(znew - z0) < restTol - && abs(znew - z1) < restTol - && abs(znew - z2) < restTol) - { - // The physical plunger has come to rest. Exit firing - // mode and resume reporting the actual position. - firing = false; - z = znew; - } - else - { - // until the physical plunger comes to rest, simply - // report the park position - z = 0; - ++firing; - } - } - else - { - // not in firing mode - report the true physical position - z = znew; - } - - // shift the new reading into the recent history buffer - z2 = z1; - z1 = z0; - z0 = znew; - } - + // read the plunger sensor + plungerReader.read(); + // process button updates processButtons(); + // handle the ZB Launch Ball feature + zbLaunchBall.update(simButtons); + // send a keyboard report if we have new data if (kbState.changed) { @@ -3360,7 +3759,7 @@ // VP only wants to sync with the real world in 10ms intervals, // so reporting more frequently creates I/O overhead without // doing anything to improve the simulation. - if (cfg.joystickEnabled && jsReportTimer.read_ms() > 10) + if (cfg.joystickEnabled /* $$$ && jsReportTimer.read_us() > 10000 */) { // read the accelerometer int xa, ya; @@ -3376,16 +3775,28 @@ x = xa; y = ya; - // Report the current plunger position UNLESS the ZB Launch Ball - // signal is on, in which case just report a constant 0 value. - // ZB Launch Ball turns off the plunger position because it + // Report the current plunger position unless the plunger is + // disabled, or the ZB Launch Ball signal is on. In either of + // those cases, just report a constant 0 value. ZB Launch Ball + // temporarily disables mechanical plunger reporting because it // tells us that the table has a Launch Ball button instead of - // a traditional plunger. - int zrep = (cfg.plunger.zbLaunchBall.port != 0 && wizOn[cfg.plunger.zbLaunchBall.port-1] ? 0 : z); + // a traditional plunger, so we don't want to confuse VP with + // regular plunger inputs. + int z = plungerReader.getPosition(); + int zrep = (!cfg.plunger.enabled ? 0 : + cfg.plunger.zbLaunchBall.port != 0 + && wizOn[cfg.plunger.zbLaunchBall.port-1] ? 0 : + z); // rotate X and Y according to the device orientation in the cabinet accelRotate(x, y); +#if 0 + // $$$ report velocity in x axis and timestamp in y axis + x = int(plungerReader.getVelocity() * 1.0 * JOYMAX); + y = (plungerReader.getTimestamp() / 1000) % JOYMAX; +#endif + // send the joystick report jsOK = js.update(x, y, zrep, jsButtons | simButtons, statusFlags); @@ -3397,7 +3808,7 @@ if (reportPix) { // send the report - plungerSensor->sendExposureReport(js, reportPixMode); + plungerSensor->sendExposureReport(js, reportPixFlags, reportPixVisMode); // we have satisfied this request reportPix = false; @@ -3405,7 +3816,7 @@ // If joystick reports are turned off, send a generic status report // periodically for the sake of the Windows config tool. - if (!cfg.joystickEnabled && jsReportTimer.read_ms() > 200) + if (!cfg.joystickEnabled && jsReportTimer.read_us() > 200000) { jsOK = js.updateStatus(0); jsReportTimer.reset(); @@ -3490,23 +3901,39 @@ } } } + + // if we're disconnected, initiate a new connection + if (!connected && !js.isConnected()) + { + // show connect-wait diagnostics + diagLED(0, 0, 0); + preConnectTicker.attach(preConnectFlasher, 3); + + // wait for the connection + js.connect(true); + + // remove the connection diagnostic ticker + preConnectTicker.detach(); + } // $$$ +#if 0 if (dbgTimer.read() > 10) { dbgTimer.reset(); if (plungerSensor != 0 && (cfg.plunger.sensorType == PlungerType_TSL1410RS || cfg.plunger.sensorType == PlungerType_TSL1410RP)) { PlungerSensorTSL1410R *ps = (PlungerSensorTSL1410R *)plungerSensor; uint32_t nRuns; - float totalTime; + uint64_t totalTime; ps->ccd.getTimingStats(totalTime, nRuns); - printf("average plunger read time: %f ms (total=%f, n=%d)\r\n", totalTime*1000.0/nRuns, totalTime, nRuns); + printf("average plunger read time: %f ms (total=%f, n=%d)\r\n", totalTime / 1000.0f / nRuns, totalTime, nRuns); } } +#endif // end $$$ // provide a visual status indication on the on-board LED - if (calBtnState < 2 && hbTimer.read_ms() > 1000) + if (calBtnState < 2 && hbTimer.read_us() > 1000000) { if (!newConnected) {