Pinscape Controller version 1 fork. This is a fork to allow for ongoing bug fixes to the original controller version, from before the major changes for the expansion board project.

Dependencies:   FastIO FastPWM SimpleDMA mbed

Fork of Pinscape_Controller by Mike R

main.cpp

Committer:
mjr
Date:
2015-02-28
Revision:
20:4c43877327ab
Parent:
19:054f8af32fce
Child:
21:5048e16cc9ef

File content as of revision 20:4c43877327ab:

/* Copyright 2014 M J Roberts, MIT License
*
* 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.
*/

//
// Pinscape Controller
//
// "Pinscape" is the name of my custom-built virtual pinball cabinet, so I call this
// software the Pinscape Controller.  I wrote it to handle several tasks that I needed
// for my cabinet.  It runs on a Freescale KL25Z microcontroller, which is a small and 
// inexpensive device that attaches to the cabinet PC via a USB cable, and can attach
// via custom wiring to sensors, buttons, and other devices in the cabinet.
//
// I designed the software and hardware in this project especially for my own
// cabinet, but it uses standard interfaces in Windows and Visual Pinball, so it should
// work in any VP-based cabinet, as long as you're using the usual VP software suite.  
// I've tried to document the hardware in enough detail for anyone else to duplicate 
// the entire project, and the full software is open source.
//
// The Freescale board appears to the host PC as a standard USB joystick.  This works 
// with the built-in Windows joystick device drivers, so there's no need to install any
// new drivers or other software on the PC.  Windows should recognize the Freescale
// as a joystick when you plug it into the USB port, and Windows shouldn't ask you to 
// install any drivers.  If you bring up the Windows control panel for USB Game 
// Controllers, this device will appear as "Pinscape Controller".  *Don't* do any 
// calibration with the Windows control panel or third-part calibration tools.  The 
// software calibrates the accelerometer portion automatically, and has its own special
// calibration procedure for the plunger sensor, if you're using that (see below).
//
// This software provides a whole bunch of separate features.  You can use any of these 
// features individually or all together.  If you're not using a particular feature, you
// can simply omit the extra wiring and/or hardware for that feature.  You can use
// the nudging feature by itself without any extra hardware attached, since the
// accelerometer is built in to the KL25Z board.
//
//  - Nudge sensing via the KL25Z's on-board accelerometer.  Nudging the cabinet
//    causes small accelerations that the accelerometer can detect; these are sent to
//    Visual Pinball via the joystick interface so that VP can simulate the effect
//    of the real physical nudges on its simulated ball.  VP has native handling for
//    this type of input, so all you have to do is set some preferences in VP to tell 
//    it that an accelerometer is attached.
//
//  - Plunger position sensing via an attached TAOS TSL 1410R CCD linear array sensor.  
//    To use this feature, you need to buy the TAOS device (it's not built in to the
//    KL25Z, obviously), wire it to the KL25Z (5 wire connections between the two
//    devices are required), and mount the TAOS sensor in your cabinet so that it's
//    positioned properly to capture images of the physical plunger shooter rod.
//
//    The physical mounting and wiring details are desribed in the project 
//    documentation.  
//
//    If the CCD is attached, the software constantly captures images from the CCD
//    and analyzes them to determine how far back the plunger is pulled.  It reports
//    this to Visual Pinball via the joystick interface.  This allows VP to make the
//    simulated on-screen plunger track the motion of the physical plunger in real
//    time.  As with the nudge data, VP has native handling for the plunger input, 
//    so you just need to set the VP preferences to tell it that an analog plunger 
//    device is attached.  One caveat, though: although VP itself has built-in 
//    support for an analog plunger, not all existing tables take advantage of it.  
//    Many existing tables have their own custom plunger scripting that doesn't
//    cooperate with the VP plunger input.  All tables *can* be made to work with
//    the plunger, and in most cases it only requires some simple script editing,
//    but in some cases it requires some more extensive surgery.
//
//    For best results, the plunger sensor should be calibrated.  The calibration
//    is stored in non-volatile memory on board the KL25Z, so it's only necessary
//    to do the calibration once, when you first install everything.  (You might
//    also want to re-calibrate if you physically remove and reinstall the CCD 
//    sensor or the mechanical plunger, since their alignment shift change slightly 
//    when you put everything back together.)  You can optionally install a
//    dedicated momentary switch or pushbutton to activate the calibration mode;
//    this is describe in the project documentation.  If you don't want to bother
//    with the extra button, you can also trigger calibration using the Windows 
//    setup software, which you can find on the Pinscape project page.
//
//    The calibration procedure is described in the project documentation.  Briefly,
//    when you trigger calibration mode, the software will scan the CCD for about
//    15 seconds, during which you should simply pull the physical plunger back
//    all the way, hold it for a moment, and then slowly return it to the rest
//    position.  (DON'T just release it from the retracted position, since that
//    let it shoot forward too far.  We want to measure the range from the park
//    position to the fully retracted position only.)
//
//  - Button input wiring.  24 of the KL25Z's GPIO ports are mapped as digital inputs
//    for buttons and switches.  The software reports these as joystick buttons when
//    it sends reports to the PC.  These can be used to wire physical pinball-style
//    buttons in the cabinet (e.g., flipper buttons, the Start button) and miscellaneous 
//    switches (such as a tilt bob) to the PC.  Visual Pinball can use joystick buttons
//    for input - you just have to assign a VP function to each button using VP's
//    keyboard options dialog.  To wire a button physically, connect one terminal of
//    the button switch to the KL25Z ground, and connect the other terminal to the
//    the GPIO port you wish to assign to the button.  See the buttonMap[] array
//    below for the available GPIO ports and their assigned joystick button numbers.
//    If you're not using a GPIO port, you can just leave it unconnected - the digital
//    inputs have built-in pull-up resistors, so an unconnected port is the same as
//    an open switch (an "off" state for the button).
//
//  - LedWiz emulation.  The KL25Z can appear to the PC as an LedWiz device, and will
//    accept and process LedWiz commands from the host.  The software can turn digital
//    output ports on and off, and can set varying PWM intensitiy levels on a subset
//    of ports.  (The KL25Z can only provide 6 PWM ports.  Intensity level settings on
//    other ports is ignored, so non-PWM ports can only be used for simple on/off
//    devices such as contactors and solenoids.)  The KL25Z can only supply 4mA on its
//    output ports, so external hardware is required to take advantage of the LedWiz
//    emulation.  Many different hardware designs are possible, but there's a simple
//    reference design in the documentation that uses a Darlington array IC to
//    increase the output from each port to 500mA (the same level as the LedWiz),
//    plus an extended design that adds an optocoupler and MOSFET to provide very
//    high power handling, up to about 45A or 150W, with voltages up to 100V.
//    That will handle just about any DC device directly (wtihout relays or other
//    amplifiers), and switches fast enough to support PWM devices.
//
//    The device can report any desired LedWiz unit number to the host, which makes
//    it possible to use the LedWiz emulation on a machine that also has one or more
//    actual LedWiz devices intalled.  The LedWiz design allows for up to 16 units
//    to be installed in one machine - each one is invidually addressable by its
//    distinct unit number.
//
//    The LedWiz emulation features are of course optional.  There's no need to 
//    build any of the external port hardware (or attach anything to the output 
//    ports at all) if the LedWiz features aren't needed.  Most people won't have
//    any use for the LedWiz features.  I built them mostly as a learning exercise,
//    but with a slight practical need for a handful of extra ports (I'm using the
//    cutting-edge 10-contactor setup, so my real LedWiz is full!).
//
// The on-board LED on the KL25Z flashes to indicate the current device status:
//
//    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 red flash = the host computer is in sleep/suspend mode
//
//    long red/green = the LedWiz unti number has been changed, so a reset
//        is needed.  You can simply unplug the device and plug it back in,
//        or presss and hold the reset button on the device for a few seconds.
//
//    long yellow/green = everything's working, but the plunger hasn't
//        been calibrated; follow the calibration procedure described above.
//        This flash mode won't appear if the CCD has been disabled.  Note
//        that the device can't tell whether a CCD is physically attached;
//        if you don't have a CCD attached, you can set the appropriate option 
//        in config.h or use the  Windows config tool to disable the CCD 
//        software features.
//
//    alternating blue/green = everything's working
//
// Software configuration: you can change option settings by sending special
// USB commands from the PC.  I've provided a Windows program for this purpose;
// refer to the documentation for details.  For reference, here's the format
// of the USB command for option changes:
//
//    length of report = 8 bytes
//    byte 0 = 65 (0x41)
//    byte 1 = 1 (0x01)
//    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)
//
// Exposure reports: the host can request a report of the full set of pixel
// values for the next frame by sending this special packet:
//
//    length = 8 bytes
//    byte 0 = 65 (0x41)
//    byte 1 = 3 (0x03)
//
// We'll respond with a series of special reports giving the exposure status.
// Each report has the following structure:
//
//    bytes 0:1 = 11-bit index, with high 5 bits set to 10000.  For 
//                example, 0x04 0x80 indicates index 4.  This is the 
//                starting pixel number in the report.  The first report 
//                will be 0x00 0x80 to indicate pixel #0.  
//    bytes 2:3 = 16-bit unsigned int brightness level of pixel at index
//    bytes 4:5 = brightness of pixel at index+1
//    etc for the rest of the packet
//
// This still has the form of a joystick packet at the USB level, but
// can be differentiated by the host via the status bits.  It would have
// been cleaner to use a different Report ID at the USB level, but this
// would have necessitated a different container structure in the report
// descriptor, which would have broken LedWiz compatibility.  Given that
// constraint, we have to re-use the joystick report type, making for
// this somewhat kludgey approach.
 
#include "mbed.h"
#include "math.h"
#include "USBJoystick.h"
#include "MMA8451Q.h"
#include "tsl1410r.h"
#include "FreescaleIAP.h"
#include "crc32.h"

// our local configuration file
#include "config.h"


// ---------------------------------------------------------------------------
// utilities

// number of elements in an array
#define countof(x) (sizeof(x)/sizeof((x)[0]))


// ---------------------------------------------------------------------------
// USB device vendor ID, product ID, and version.  
//
// We use the vendor ID for the LedWiz, so that the PC-side software can
// identify us as capable of performing LedWiz commands.  The LedWiz uses
// a product ID value from 0xF0 to 0xFF; the last four bits identify the
// unit number (e.g., product ID 0xF7 means unit #7).  This allows multiple
// LedWiz units to be installed in a single PC; the software on the PC side
// uses the unit number to route commands to the devices attached to each
// unit.  On the real LedWiz, the unit number must be set in the firmware
// at the factory; it's not configurable by the end user.  Most LedWiz's
// ship with the unit number set to 0, but the vendor will set different
// unit numbers if requested at the time of purchase.  So if you have a
// single LedWiz already installed in your cabinet, and you didn't ask for
// a non-default unit number, your existing LedWiz will be unit 0.
//
// Note that the USB_PRODUCT_ID value set here omits the unit number.  We
// take the unit number from the saved configuration.  We provide a
// configuration command that can be sent via the USB connection to change
// the unit number, so that users can select the unit number without having
// to install a different version of the software.  We'll combine the base
// product ID here with the unit number to get the actual product ID that
// we send to the USB controller.
const uint16_t USB_VENDOR_ID = 0xFAFA;
const uint16_t USB_PRODUCT_ID = 0x00F0;
const uint16_t USB_VERSION_NO = 0x0006;


// Joystick axis report range - we report from -JOYMAX to +JOYMAX
#define JOYMAX 4096


// --------------------------------------------------------------------------
//
// Potentiometer configuration
//
#ifdef POT_SENSOR_ENABLED
#define IF_POT(x) x
#else
#define IF_POT(x)
#endif


// ---------------------------------------------------------------------------
//
// On-board RGB LED elements - we use these for diagnostic displays.
//
DigitalOut ledR(LED1), ledG(LED2), ledB(LED3);


// ---------------------------------------------------------------------------
//
// LedWiz emulation
//

static int pbaIdx = 0;

// LedWiz output pin interface.  We create a cover class to virtualize
// digital vs PWM outputs and give them a common interface.  The KL25Z
// unfortunately doesn't have enough hardware PWM channels to support 
// PWM on all 32 LedWiz outputs, so we provide as many PWM channels as
// we can (10), and fill out the rest of the outputs with plain digital
// outs.
class LwOut
{
public:
    virtual void set(float val) = 0;
};
class LwPwmOut: public LwOut
{
public:
    LwPwmOut(PinName pin) : p(pin) { prv = -1; }
    virtual void set(float val) 
    { 
        if (val != prv)
            p.write(prv = val); 
    }
    PwmOut p;
    float prv;
};
class LwDigOut: public LwOut
{
public:
    LwDigOut(PinName pin) : p(pin) { prv = -1; }
    virtual void set(float val) 
    {
         if (val != prv)
            p.write((prv = val) == 0.0 ? 0 : 1); 
    }
    DigitalOut p;
    float prv;
};
class LwUnusedOut: public LwOut
{
public:
    LwUnusedOut() { }
    virtual void set(float val) { }
};

// output pin array
static LwOut *lwPin[32];

// initialize the output pin array
void initLwOut()
{
    for (int i = 0 ; i < countof(lwPin) ; ++i)
    {
        PinName p = (i < countof(ledWizPortMap) ? ledWizPortMap[i].pin : NC);
        if (p == NC)
            lwPin[i] = new LwUnusedOut();
        else if (ledWizPortMap[i].isPWM)
            lwPin[i] = new LwPwmOut(p);
        else
            lwPin[i] = new LwDigOut(p);
    }
}

// on/off state for each LedWiz output
static uint8_t wizOn[32];

// profile (brightness/blink) state for each LedWiz output
static uint8_t wizVal[32] = {
    48, 48, 48, 48, 48, 48, 48, 48,
    48, 48, 48, 48, 48, 48, 48, 48,
    48, 48, 48, 48, 48, 48, 48, 48,
    48, 48, 48, 48, 48, 48, 48, 48
};

static float wizState(int idx)
{
    if (wizOn[idx]) 
    {
        // on - map profile brightness state to PWM level
        uint8_t val = wizVal[idx];
        if (val <= 48)
        {
            // PWM brightness/intensity level.  Rescale from the LedWiz
            // 0..48 integer range to our internal PwmOut 0..1 float range.
            // Note that on the actual LedWiz, level 48 is actually about
            // 98% on - contrary to the LedWiz documentation, level 49 is 
            // the true 100% level.  (In the documentation, level 49 is
            // simply not a valid setting.)  Even so, we treat level 48 as
            // 100% on to match the documentation.  This won't be perfectly
            // ocmpatible with the actual LedWiz, but it makes for such a
            // small difference in brightness (if the output device is an
            // LED, say) that no one should notice.  It seems better to
            // err in this direction, because while the difference in
            // brightness when attached to an LED won't be noticeable, the
            // difference in duty cycle when attached to something like a
            // contactor *can* be noticeable - anything less than 100%
            // can cause a contactor or relay to chatter.  There's almost
            // never a situation where you'd want values other than 0% and
            // 100% for a contactor or relay, so treating level 48 as 100%
            // makes us work properly with software that's expecting the
            // documented LedWiz behavior and therefore uses level 48 to
            // turn a contactor or relay fully on.
            return val/48.0;
        }
        else if (val == 49)
        {
            // 49 is undefined in the LedWiz documentation, but actually
            // means 100% on.  The documentation says that levels 1-48 are
            // the full PWM range, but empirically it appears that the real
            // range implemented in the firmware is 1-49.  Some software on
            // the PC side (notably DOF) is aware of this and uses level 49
            // to mean "100% on".  To ensure compatibility with existing 
            // PC-side software, we need to recognize level 49.
            return 1.0;
        }
        else if (val >= 129 && val <= 132)
        {
            // Values of 129-132 select different flashing modes.  We don't
            // support any of these.  Instead, simply treat them as fully on.  
            // Note that DOF doesn't ever use modes 129-132, as it implements 
            // all flashing modes itself on the host side, so this limitation 
            // won't have any effect on DOF users.  You can observe it using 
            // LedBlinky, though.
            return 1.0;
        }
        else
        {
            // Other values are undefined in the LedWiz documentation.  Hosts
            // *should* never send undefined values, since whatever behavior an
            // LedWiz unit exhibits in response is accidental and could change
            // in a future version.  We'll treat all undefined values as equivalent 
            // to 48 (fully on).
            // 
            // NB: the 49 and 129-132 cases are broken out above for the sake
            // of documentation.  We end up using 1.0 as the return value for
            // everything outside of the defined 0-48 range, so we could collapse
            // this whole thing to a single 'else' branch, but I wanted to call 
            // out the specific reasons for handling the settings above as we do.
            return 1.0;
        }
    }
    else 
    {
        // off - show at 0 intensity
        return 0.0;
    }
}

static void updateWizOuts()
{
    for (int i = 0 ; i < 32 ; ++i)
        lwPin[i]->set(wizState(i));
}


// ---------------------------------------------------------------------------
//
// Button input
//

// button input map array
DigitalIn *buttonDigIn[32];

// button state
struct ButtonState
{
    // current on/off state
    int pressed;
    
    // Sticky time remaining for current state.  When a
    // state transition occurs, we set this to a debounce
    // period.  Future state transitions will be ignored
    // until the debounce time elapses.
    int t;
} buttonState[32];

// timer for button reports
static Timer buttonTimer;

// initialize the button inputs
void initButtons()
{
    // create the digital inputs
    for (int i = 0 ; i < countof(buttonDigIn) ; ++i)
    {
        if (i < countof(buttonMap) && buttonMap[i] != NC)
            buttonDigIn[i] = new DigitalIn(buttonMap[i]);
        else
            buttonDigIn[i] = 0;
    }
    
    // start the button timer
    buttonTimer.start();
}


// read the button input state
uint32_t readButtons()
{
    // start with all buttons off
    uint32_t buttons = 0;
    
    // figure the time elapsed since the last scan
    int dt = buttonTimer.read_ms();
    
    // reset the timef for the next scan
    buttonTimer.reset();
    
    // scan the button list
    uint32_t bit = 1;
    DigitalIn **di = buttonDigIn;
    ButtonState *bs = buttonState;
    for (int i = 0 ; i < countof(buttonDigIn) ; ++i, ++di, ++bs, bit <<= 1)
    {
        // read this button
        if (*di != 0)
        {
            // deduct the elapsed time since the last update
            // from the button's remaining sticky time
            bs->t -= dt;
            if (bs->t < 0)
                bs->t = 0;
            
            // If the sticky time has elapsed, note the new physical
            // state of the button.  If we still have sticky time
            // remaining, ignore the physical state; the last state
            // change persists until the sticky time elapses so that
            // we smooth out any "bounce" (electrical transients that
            // occur when the switch contact is opened or closed).
            if (bs->t == 0)
            {
                // get the new physical state
                int pressed = !(*di)->read();
                
                // update the button's logical state if this is a change
                if (pressed != bs->pressed)
                {
                    // store the new state
                    bs->pressed = pressed;
                    
                    // start a new sticky period for debouncing this
                    // state change
                    bs->t = 25;
                }
            }
            
            // if it's pressed, OR its bit into the state
            if (bs->pressed)
                buttons |= bit;
        }
    }
    
    // return the new button list
    return buttons;
}

// ---------------------------------------------------------------------------
//
// Customization joystick subbclass
//

class MyUSBJoystick: public USBJoystick
{
public:
    MyUSBJoystick(uint16_t vendor_id, uint16_t product_id, uint16_t product_release) 
        : USBJoystick(vendor_id, product_id, product_release, true)
    {
        suspended_ = false;
    }
    
    // are we connected?
    int isConnected()  { return configured(); }
    
    // Are we in suspend mode?
    int isSuspended() const { return suspended_; }
    
protected:
    virtual void suspendStateChanged(unsigned int suspended)
        { suspended_ = suspended; }

    // are we suspended?
    int suspended_; 
};

// ---------------------------------------------------------------------------
//
// Some simple math service routines
//

inline float square(float x) { return x*x; }
inline float round(float x) { return x > 0 ? floor(x + 0.5) : ceil(x - 0.5); }

// ---------------------------------------------------------------------------
// 
// Accelerometer (MMA8451Q)
//

// The MMA8451Q is the KL25Z's on-board 3-axis accelerometer.
//
// This is a custom wrapper for the library code to interface to the
// MMA8451Q.  This class encapsulates an interrupt handler and 
// automatic calibration.
//
// We install an interrupt handler on the accelerometer "data ready" 
// interrupt to ensure that we fetch each sample immediately when it
// becomes available.  The accelerometer data rate is fiarly high
// (800 Hz), so it's not practical to keep up with it by polling.
// Using an interrupt handler lets us respond quickly and read
// every sample.
//
// We automatically calibrate the accelerometer so that it's not
// necessary to get it exactly level when installing it, and so
// that it's also not necessary to calibrate it manually.  There's
// lots of experience that tells us that manual calibration is a
// terrible solution, mostly because cabinets tend to shift slightly
// during use, requiring frequent recalibration.  Instead, we
// calibrate automatically.  We continuously monitor the acceleration
// data, watching for periods of constant (or nearly constant) values.
// Any time it appears that the machine has been at rest for a while
// (about 5 seconds), we'll average the readings during that rest
// period and use the result as the level rest position.  This is
// is ongoing, so we'll quickly find the center point again if the 
// machine is moved during play (by an especially aggressive bout
// of nudging, say).
//

// I2C address of the accelerometer (this is a constant of the KL25Z)
const int MMA8451_I2C_ADDRESS = (0x1d<<1);

// SCL and SDA pins for the accelerometer (constant for the KL25Z)
#define MMA8451_SCL_PIN   PTE25
#define MMA8451_SDA_PIN   PTE24

// Digital in pin to use for the accelerometer interrupt.  For the KL25Z,
// this can be either PTA14 or PTA15, since those are the pins physically
// wired on this board to the MMA8451 interrupt controller.
#define MMA8451_INT_PIN   PTA15


// accelerometer input history item, for gathering calibration data
struct AccHist
{
    AccHist() { x = y = d = 0.0; xtot = ytot = 0.0; cnt = 0; }
    void set(float x, float y, AccHist *prv)
    {
        // save the raw position
        this->x = x;
        this->y = y;
        this->d = distance(prv);
    }
    
    // reading for this entry
    float x, y;
    
    // distance from previous entry
    float d;
    
    // total and count of samples averaged over this period
    float xtot, ytot;
    int cnt;

    void clearAvg() { xtot = ytot = 0.0; cnt = 0; }    
    void addAvg(float x, float y) { xtot += x; ytot += y; ++cnt; }
    float xAvg() const { return xtot/cnt; }
    float yAvg() const { return ytot/cnt; }
    
    float distance(AccHist *p)
        { return sqrt(square(p->x - x) + square(p->y - y)); }
};

// accelerometer wrapper class
class Accel
{
public:
    Accel(PinName sda, PinName scl, int i2cAddr, PinName irqPin)
        : mma_(sda, scl, i2cAddr), intIn_(irqPin)
    {
        // remember the interrupt pin assignment
        irqPin_ = irqPin;

        // reset and initialize
        reset();
    }
    
    void reset()
    {
        // clear the center point
        cx_ = cy_ = 0.0;
        
        // start the calibration timer
        tCenter_.start();
        iAccPrv_ = nAccPrv_ = 0;
        
        // reset and initialize the MMA8451Q
        mma_.init();
                
        // set the initial integrated velocity reading to zero
        vx_ = vy_ = 0;
        
        // set up our accelerometer interrupt handling
        intIn_.rise(this, &Accel::isr);
        mma_.setInterruptMode(irqPin_ == PTA14 ? 1 : 2);
        
        // read the current registers to clear the data ready flag
        mma_.getAccXYZ(ax_, ay_, az_);

        // start our timers
        tGet_.start();
        tInt_.start();
    }
    
    void get(int &x, int &y) 
    {
         // disable interrupts while manipulating the shared data
         __disable_irq();
         
         // read the shared data and store locally for calculations
         float ax = ax_, ay = ay_;
         float vx = vx_, vy = vy_;
         
         // reset the velocity sum for the next run
         vx_ = vy_ = 0;

         // get the time since the last get() sample
         float dt = tGet_.read_us()/1.0e6;
         tGet_.reset();
         
         // done manipulating the shared data
         __enable_irq();
         
         // adjust the readings for the integration time
         vx /= dt;
         vy /= dt;
         
         // add this sample to the current calibration interval's running total
         AccHist *p = accPrv_ + iAccPrv_;
         p->addAvg(ax, ay);

         // check for auto-centering every so often
         if (tCenter_.read_ms() > 1000)
         {
             // add the latest raw sample to the history list
             AccHist *prv = p;
             iAccPrv_ = (iAccPrv_ + 1) % maxAccPrv;
             p = accPrv_ + iAccPrv_;
             p->set(ax, ay, prv);

             // if we have a full complement, check for stability
             if (nAccPrv_ >= maxAccPrv)
             {
                 // check if we've been stable for all recent samples
                 static const float accTol = .01;
                 AccHist *p0 = accPrv_;
                 if (p0[0].d < accTol
                     && p0[1].d < accTol
                     && p0[2].d < accTol
                     && p0[3].d < accTol
                     && p0[4].d < accTol)
                 {
                     // Figure the new calibration point as the average of
                     // the samples over the rest period
                     cx_ = (p0[0].xAvg() + p0[1].xAvg() + p0[2].xAvg() + p0[3].xAvg() + p0[4].xAvg())/5.0;
                     cy_ = (p0[0].yAvg() + p0[1].yAvg() + p0[2].yAvg() + p0[3].yAvg() + p0[4].yAvg())/5.0;
                 }
             }
             else
             {
                // not enough samples yet; just up the count
                ++nAccPrv_;
             }
             
             // clear the new item's running totals
             p->clearAvg();
            
             // reset the timer
             tCenter_.reset();
         }
         
         // report our integrated velocity reading in x,y
         x = rawToReport(vx);
         y = rawToReport(vy);
         
#ifdef DEBUG_PRINTF
         if (x != 0 || y != 0)        
             printf("%f %f %d %d %f\r\n", vx, vy, x, y, dt);
#endif
     }    
    
private:
    // adjust a raw acceleration figure to a usb report value
    int rawToReport(float v)
    {
        // scale to the joystick report range and round to integer
        int i = int(round(v*JOYMAX));
        
        // if it's near the center, scale it roughly as 20*(i/20)^2,
        // to suppress noise near the rest position
        static const int filter[] = { 
            -18, -16, -14, -13, -11, -10, -8, -7, -6, -5, -4, -3, -2, -2, -1, -1, 0, 0, 0, 0,
            0,
            0, 0, 0, 0, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 10, 11, 13, 14, 16, 18
        };
        return (i > 20 || i < -20 ? i : filter[i+20]);
    }

    // interrupt handler
    void isr()
    {
        // Read the axes.  Note that we have to read all three axes
        // (even though we only really use x and y) in order to clear
        // the "data ready" status bit in the accelerometer.  The
        // interrupt only occurs when the "ready" bit transitions from
        // off to on, so we have to make sure it's off.
        float x, y, z;
        mma_.getAccXYZ(x, y, z);
        
        // calculate the time since the last interrupt
        float dt = tInt_.read_us()/1.0e6;
        tInt_.reset();

        // integrate the time slice from the previous reading to this reading
        vx_ += (x + ax_ - 2*cx_)*dt/2;
        vy_ += (y + ay_ - 2*cy_)*dt/2;
        
        // store the updates
        ax_ = x;
        ay_ = y;
        az_ = z;
    }
    
    // underlying accelerometer object
    MMA8451Q mma_;
    
    // last raw acceleration readings
    float ax_, ay_, az_;
    
    // integrated velocity reading since last get()
    float vx_, vy_;
        
    // timer for measuring time between get() samples
    Timer tGet_;
    
    // timer for measuring time between interrupts
    Timer tInt_;

    // Calibration reference point for accelerometer.  This is the
    // average reading on the accelerometer when in the neutral position
    // at rest.
    float cx_, cy_;

    // timer for atuo-centering
    Timer tCenter_;

    // Auto-centering history.  This is a separate history list that
    // records results spaced out sparesely over time, so that we can
    // watch for long-lasting periods of rest.  When we observe nearly
    // no motion for an extended period (on the order of 5 seconds), we
    // take this to mean that the cabinet is at rest in its neutral 
    // position, so we take this as the calibration zero point for the
    // accelerometer.  We update this history continuously, which allows
    // us to continuously re-calibrate the accelerometer.  This ensures
    // that we'll automatically adjust to any actual changes in the
    // cabinet's orientation (e.g., if it gets moved slightly by an
    // especially strong nudge) as well as any systematic drift in the
    // accelerometer measurement bias (e.g., from temperature changes).
    int iAccPrv_, nAccPrv_;
    static const int maxAccPrv = 5;
    AccHist accPrv_[maxAccPrv];
    
    // interurupt pin name
    PinName irqPin_;
    
    // interrupt router
    InterruptIn intIn_;
};


// ---------------------------------------------------------------------------
//
// Clear the I2C bus for the MMA8451Q.  This seems necessary some of the time
// for reasons that aren't clear to me.  Doing a hard power cycle has the same
// effect, but when we do a soft reset, the hardware sometimes seems to leave
// the MMA's SDA line stuck low.  Forcing a series of 9 clock pulses through
// the SCL line is supposed to clear this condition.  I'm not convinced this
// actually works with the way this component is wired on the KL25Z, but it
// seems harmless, so we'll do it on reset in case it does some good.  What
// we really seem to need is a way to power cycle the MMA8451Q if it ever 
// gets stuck, but this is simply not possible in software on the KL25Z. 
// 
// If the accelerometer does get stuck, and a software reboot doesn't reset
// it, the only workaround is to manually power cycle the whole KL25Z by 
// unplugging both of its USB connections.
//
void clear_i2c()
{
    // assume a general-purpose output pin to the I2C clock
    DigitalOut scl(MMA8451_SCL_PIN);
    DigitalIn sda(MMA8451_SDA_PIN);
    
    // clock the SCL 9 times
    for (int i = 0 ; i < 9 ; ++i)
    {
        scl = 1;
        wait_us(20);
        scl = 0;
        wait_us(20);
    }
}
 
// ---------------------------------------------------------------------------
//
// Include the appropriate plunger sensor definition.  This will define a
// class called PlungerSensor, with a standard interface that we use in
// the main loop below.  This is *kind of* like a virtual class interface,
// but it actually defines the methods statically, which is a little more
// efficient at run-time.  There's no need for a true virtual interface
// because we don't need to be able to change sensor types on the fly.
//

#ifdef ENABLE_CCD_SENSOR
#include "ccdSensor.h"
#elif ENABLE_POT_SENSOR
#include "potSensor.h"
#else
#include "nullSensor.h"
#endif


// ---------------------------------------------------------------------------
//
// Non-volatile memory (NVM)
//

// Structure defining our NVM storage layout.  We store a small
// amount of persistent data in flash memory to retain calibration
// data when powered off.
struct NVM
{
    // checksum - we use this to determine if the flash record
    // has been properly initialized
    uint32_t checksum;

    // signature value
    static const uint32_t SIGNATURE = 0x4D4A522A;
    static const uint16_t VERSION = 0x0003;
    
    // Is the data structure valid?  We test the signature and 
    // checksum to determine if we've been properly stored.
    int valid() const
    {
        return (d.sig == SIGNATURE 
                && d.vsn == VERSION
                && d.sz == sizeof(NVM)
                && checksum == CRC32(&d, sizeof(d)));
    }
    
    // save to non-volatile memory
    void save(FreescaleIAP &iap, int addr)
    {
        // update the checksum and structure size
        checksum = CRC32(&d, sizeof(d));
        d.sz = sizeof(NVM);
        
        // erase the sector
        iap.erase_sector(addr);

        // save the data
        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
    {
        // Signature, structure version, and structure size - further verification 
        // that we have valid initialized data.  The size is a simple proxy for a
        // structure version, as the most common type of change to the structure as
        // the software evolves will be the addition of new elements.  We also
        // provide an explicit version number that we can update manually if we
        // make any changes that don't affect the structure size but would affect
        // compatibility with a saved record (e.g., swapping two existing elements).
        uint32_t sig;
        uint16_t vsn;
        int sz;
        
        // has the plunger been manually calibrated?
        int plungerCal;
        
        // Plunger calibration min, zero, and max.  The zero point is the 
        // rest position (aka park position), where it's in equilibrium between 
        // the main spring and the barrel spring.  It can travel a small distance
        // forward of the rest position, because the barrel spring can be
        // compressed by the user pushing on the plunger or by the momentum
        // of a release motion.  The minimum is the maximum forward point where
        // the barrel spring can't be compressed any further.
        int plungerMin;
        int plungerZero;
        int plungerMax;
        
        // is the plunger sensor enabled?
        int plungerEnabled;
        
        // LedWiz unit number
        uint8_t ledWizUnitNo;
    } d;
};


// ---------------------------------------------------------------------------
//
// Main program loop.  This is invoked on startup and runs forever.  Our
// main work is to read our devices (the accelerometer and the CCD), process
// the readings into nudge and plunger position data, and send the results
// to the host computer via the USB joystick interface.  We also monitor
// the USB connection for incoming LedWiz commands and process those into
// port outputs.
//
int main(void)
{
    // turn off our on-board indicator LED
    ledR = 1;
    ledG = 1;
    ledB = 1;
    
    // initialize the LedWiz ports
    initLwOut();
    
    // initialize the button input ports
    initButtons();
    
    // we don't need a reset yet
    bool needReset = false;
    
    // clear the I2C bus for the accelerometer
    clear_i2c();
    
    // set up a flash memory controller
    FreescaleIAP iap;
    
    // use the last sector of flash for our non-volatile memory structure
    int flash_addr = (iap.flash_size() - SECTOR_SIZE);
    NVM *flash = (NVM *)flash_addr;
    NVM cfg;
    
    // check for valid flash
    bool flash_valid = flash->valid();
                      
    // if the flash is valid, load it; otherwise initialize to defaults
    if (flash_valid) {
        memcpy(&cfg, flash, sizeof(cfg));
        printf("Flash restored: plunger cal=%d, min=%d, zero=%d, max=%d\r\n", 
            cfg.d.plungerCal, cfg.d.plungerMin, cfg.d.plungerZero, cfg.d.plungerMax);
    }
    else {
        printf("Factory reset\r\n");
        cfg.d.sig = cfg.SIGNATURE;
        cfg.d.vsn = cfg.VERSION;
        cfg.d.plungerCal = 0;
        cfg.d.plungerMin = 0;        // assume we can go all the way forward...
        cfg.d.plungerMax = npix;     // ...and all the way back
        cfg.d.plungerZero = npix/6;  // the rest position is usually around 1/2" back
        cfg.d.ledWizUnitNo = DEFAULT_LEDWIZ_UNIT_NUMBER;
        cfg.d.plungerEnabled = true;
    }
    
    // Create the joystick USB client.  Note that we use the LedWiz unit
    // number from the saved configuration.
    MyUSBJoystick js(
        USB_VENDOR_ID, 
        USB_PRODUCT_ID | cfg.d.ledWizUnitNo,
        USB_VERSION_NO);
        
    // last report timer - we use this to throttle reports, since VP
    // doesn't want to hear from us more than about every 10ms
    Timer reportTimer;
    reportTimer.start();

    // initialize the calibration buttons, if present
    DigitalIn *calBtn = (CAL_BUTTON_PIN == NC ? 0 : new DigitalIn(CAL_BUTTON_PIN));
    DigitalOut *calBtnLed = (CAL_BUTTON_LED == NC ? 0 : new DigitalOut(CAL_BUTTON_LED));

    // plunger calibration button debounce timer
    Timer calBtnTimer;
    calBtnTimer.start();
    int calBtnLit = false;
    
    // Calibration button state:
    //  0 = not pushed
    //  1 = pushed, not yet debounced
    //  2 = pushed, debounced, waiting for hold time
    //  3 = pushed, hold time completed - in calibration mode
    int calBtnState = 0;
    
    // set up a timer for our heartbeat indicator
    Timer hbTimer;
    hbTimer.start();
    int hb = 0;
    uint16_t hbcnt = 0;
    
    // set a timer for accelerometer auto-centering
    Timer acTimer;
    acTimer.start();
    
    // 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;
    
    // create our plunger sensor object
    PlungerSensor plungerSensor;

    // last plunger report position, in 'npix' normalized pixel units
    int 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 has been released from state 1 or 2, or 
    //       pushed forward about 1/4" from state 0)
    //   4 = launching, plunger is no longer pushed forward
    int lbState = 0;
    
    // 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
    // a button as pressed if either the physical button is being pressed
    // or we're simulating a press on the button.  This is used for the
    // 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
    plungerSensor.init();
    
    // 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.plungerEnabled ? 0x01 : 0x00);
    
    // flag: send a pixel dump after the next read
    bool reportPix = false;

    // we're all set up - now just loop, processing sensor reports and 
    // host requests
    for (;;)
    {
        // Look for an incoming report.  Process a few input reports in
        // a row, but stop after a few so that a barrage of inputs won't
        // starve our output event processing.  Also, pause briefly between
        // reads; allowing reads to occur back-to-back seems to occasionally 
        // stall the USB pipeline (for reasons unknown; I'd fix the underlying 
        // problem if I knew what it was).
        HID_REPORT report;
        for (int rr = 0 ; rr < 4 && js.readNB(&report) ; ++rr, wait_ms(1))
        {
            // all Led-Wiz reports are 8 bytes exactly
            if (report.length == 8)
            {
                uint8_t *data = report.data;
                if (data[0] == 64) 
                {
                    // LWZ-SBA - first four bytes are bit-packed on/off flags
                    // for the outputs; 5th byte is the pulse speed (0-7)
                    //printf("LWZ-SBA %02x %02x %02x %02x ; %02x\r\n",
                    //       data[1], data[2], data[3], data[4], data[5]);
    
                    // update all on/off states
                    for (int i = 0, bit = 1, ri = 1 ; i < 32 ; ++i, bit <<= 1)
                    {
                        if (bit == 0x100) {
                            bit = 1;
                            ++ri;
                        }
                        wizOn[i] = ((data[ri] & bit) != 0);
                    }
        
                    // update the physical outputs
                    updateWizOuts();
                    
                    // reset the PBA counter
                    pbaIdx = 0;
                }
                else if (data[0] == 65)
                {
                    // Private control message.  This isn't an LedWiz message - it's
                    // an extension for this device.  65 is an invalid PBA setting,
                    // and isn't used for any other LedWiz message, so we appropriate
                    // it for our own private use.  The first byte specifies the 
                    // message type.
                    if (data[1] == 1)
                    {
                        // 1 = Set Configuration:
                        //     data[2] = LedWiz unit number (0x00 to 0x0f)
                        //     data[3] = feature enable bit mask:
                        //               0x01 = enable CCD
                        
                        // we'll need a reset if the LedWiz unit number is changing
                        uint8_t newUnitNo = data[2] & 0x0f;
                        needReset |= (newUnitNo != cfg.d.ledWizUnitNo);
                        
                        // set the configuration parameters from the message
                        cfg.d.ledWizUnitNo = newUnitNo;
                        cfg.d.plungerEnabled = 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.plungerEnabled)
                            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 if (data[1] == 3)
                    {
                        // 3 = pixel dump
                        // (No parameters)
                        reportPix = true;
                        
                        // show purple until we finish sending the report
                        ledR = 0;
                        ledB = 0;
                        ledG = 1;
                    }
                }
                else 
                {
                    // LWZ-PBA - full state dump; each byte is one output
                    // in the current bank.  pbaIdx keeps track of the bank;
                    // this is incremented implicitly by each PBA message.
                    //printf("LWZ-PBA[%d] %02x %02x %02x %02x %02x %02x %02x %02x\r\n",
                    //       pbaIdx, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
    
                    // update all output profile settings
                    for (int i = 0 ; i < 8 ; ++i)
                        wizVal[pbaIdx + i] = data[i];
    
                    // update the physical LED state if this is the last bank                    
                    if (pbaIdx == 24)
                    {
                        updateWizOuts();
                        pbaIdx = 0;
                    }
                    else
                        pbaIdx += 8;
                }
            }
        }
       
        // check for plunger calibration
        if (calBtn != 0 && !calBtn->read())
        {
            // check the state
            switch (calBtnState)
            {
            case 0: 
                // button not yet pushed - start debouncing
                calBtnTimer.reset();
                calBtnState = 1;
                break;
                
            case 1:
                // pushed, not yet debounced - if the debounce time has
                // passed, start the hold period
                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() > 2050)
                {
                    // enter calibration mode
                    calBtnState = 3;
                    calBtnTimer.reset();
                    cfg.resetPlunger();
                }
                break;
                
            case 3:
                // 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;
            }
        }
        else
        {
            // Button released.  If we're in calibration mode, and
            // the calibration time has elapsed, end the calibration
            // and save the results to flash.
            //
            // 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)
            {
                // exit calibration mode
                calBtnState = 0;
                
                // save the updated configuration
                cfg.d.plungerCal = 1;
                cfg.save(iap, flash_addr);
                
                // the flash state is now valid
                flash_valid = true;
            }
            else if (calBtnState != 3)
            {
                // didn't make it to calibration mode - cancel the operation
                calBtnState = 0;
            }
        }       
        
        // light/flash the calibration button light, if applicable
        int newCalBtnLit = calBtnLit;
        switch (calBtnState)
        {
        case 2:
            // in the hold period - flash the light
            newCalBtnLit = ((calBtnTimer.read_ms()/250) & 1);
            break;
            
        case 3:
            // calibration mode - show steady on
            newCalBtnLit = true;
            break;
            
        default:
            // not calibrating/holding - show steady off
            newCalBtnLit = false;
            break;
        }
        
        // light or flash the external calibration button LED, and 
        // do the same with the on-board blue LED
        if (calBtnLit != newCalBtnLit)
        {
            calBtnLit = newCalBtnLit;
            if (calBtnLit) {
                if (calBtnLed != 0)
                    calBtnLed->write(1);
                ledR = 1;
                ledG = 1;
                ledB = 0;
            }
            else {
                if (calBtnLed != 0)
                    calBtnLed->write(0);
                ledR = 1;
                ledG = 1;
                ledB = 1;
            }
        }
        
        // If the plunger is enabled, 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 && cfg.d.plungerEnabled && z >= JOYMAX/6)
        {
            // monitor the plunger until it's time for our next report
            while (reportTimer.read_ms() < 15)
            {
                // do a fast low-res scan; if it's at or past the zero point,
                // start a firing event
                if (plungerSensor.lowResScan() <= cfg.d.plungerZero)
                    firing = 1;
            }
        }

        // read the plunger sensor, if it's enabled
        if (cfg.d.plungerEnabled)
        {
            // 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 got 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.d.plungerMin)
                        cfg.d.plungerMin = pos;
                    if (pos < cfg.d.plungerZero)
                        cfg.d.plungerZero = pos;
                    if (pos > cfg.d.plungerMax)
                        cfg.d.plungerMax = pos;
                        
                    // normalize to the full physical range while calibrating
                    znew = int(round(float(pos)/npix * 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 smmall 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.d.plungerMax)
                        pos = cfg.d.plungerMax;
                    znew = int(round(float(pos - cfg.d.plungerZero)
                        / (cfg.d.plungerMax - cfg.d.plungerZero + 1) * 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.
                int pos1 = plungerSensor.lowResScan();
                Timer tw;
                tw.start();
                while (tw.read_ms() < 6)
                {
                    // if we've crossed the rest position, it's a firing event
                    if (pos1 < cfg.d.plungerZero)
                    {
                        firing = 1;
                        break;
                    }
                    
                    // read the new position
                    int pos2 = plungerSensor.lowResScan();
                    
                    // if it's stable, stop looping
                    if (abs(pos2 - pos1) < int(npix/(3.2*8)))
                        break;
                        
                    // the new reading is now the prior reading
                    pos1 = pos2;
                }
            }
            
            // Check for a simulated Launch Ball button press, if enabled
            if (ZBLaunchBallPort != 0)
            {
                const int cockThreshold = JOYMAX/3;
                const int pushThreshold = int(-JOYMAX/3 * LaunchBallPushDistance);
                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 "launch" state.
                    if (znew >= cockThreshold)
                        newState = 1;
                    else if (znew <= pushThreshold)
                        newState = 3;
                    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;
                }
                
                // change states if desired
                const uint32_t lbButtonBit = (1 << (LaunchBallButton - 1));
                if (newState != lbState)
                {
                    // if we're entering Launch state, and the ZB Launch Ball
                    // LedWiz signal is turned on, simulate a Launch Ball button
                    // press
                    if (newState == 3 && lbState != 4 && wizOn[ZBLaunchBallPort-1])
                    {
                        lbBtnTimer.reset();
                        lbBtnTimer.start();
                        simButtons |= lbButtonBit;
                    }
                        
                    // if we're switching to state 0, release the button
                    if (newState == 0)
                        simButtons &= ~(1 << (LaunchBallButton - 1));
                    
                    // switch to the new state
                    lbState = newState;
                    
                    // start timing in the new state
                    lbTimer.reset();
                }

                // if the simulated Launch Ball button press is in effect,
                // and either it's been in effect too long or the ZB Launch
                // Ball signal is no longer active, turn off the button
                if ((simButtons & lbButtonBit) != 0
                    && (!wizOn[ZBLaunchBallPort-1] || lbBtnTimer.read_ms() > 250))
                {
                    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;
        }

        // update the buttons
        uint32_t buttons = readButtons();

        // If it's been long enough since our last USB status report,
        // send the new report.  We throttle the report rate because
        // it can overwhelm the PC side if we report too frequently.
        // VP only wants to sync with the real world in 10ms intervals,
        // so reporting more frequently only creates i/o overhead
        // without doing anything to improve the simulation.
        if (reportTimer.read_ms() > 15)
        {
            // read the accelerometer
            int xa, ya;
            accel.get(xa, ya);
            
            // confine the results to our joystick axis range
            if (xa < -JOYMAX) xa = -JOYMAX;
            if (xa > JOYMAX) xa = JOYMAX;
            if (ya < -JOYMAX) ya = -JOYMAX;
            if (ya > JOYMAX) ya = JOYMAX;
            
            // store the updated accelerometer coordinates
            x = xa;
            y = ya;
            
            // Send the status report.  Note that the nominal x and y axes
            // are reversed - this makes it more intuitive to set up in VP.
            // If we mount the Freesale card flat on the floor of the cabinet
            // with the USB connectors facing the front of the cabinet, this
            // 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, buttons | simButtons, statusFlags);
            
            // we've just started a new report interval, so reset the timer
            reportTimer.reset();
        }
        
        // If we're in pixel dump mode, report all pixel exposure values
        if (reportPix)
        {
            // send the report            
            plungerSensor.sendExposureReport(js);

            // we have satisfied this request
            reportPix = false;
        }
        
#ifdef DEBUG_PRINTF
        if (x != 0 || y != 0)
            printf("%d,%d\r\n", x, y);
#endif

        // provide a visual status indication on the on-board LED
        if (calBtnState < 2 && hbTimer.read_ms() > 1000) 
        {
            if (js.isSuspended() || !js.isConnected())
            {
                // suspended - turn off the LED
                ledR = 1;
                ledG = 1;
                ledB = 1;

                // show a status flash every so often                
                if (hbcnt % 3 == 0)
                {
                    // disconnected = red/red flash; suspended = red
                    for (int n = js.isConnected() ? 1 : 2 ; n > 0 ; --n)
                    {
                        ledR = 0;
                        wait(0.05);
                        ledR = 1;
                        wait(0.25);
                    }
                }
            }
            else if (needReset)
            {
                // connected, need to reset due to changes in config parameters -
                // flash red/green
                hb = !hb;
                ledR = (hb ? 0 : 1);
                ledG = (hb ? 1 : 0);
                ledB = 0;
            }
            else if (cfg.d.plungerEnabled && !cfg.d.plungerCal)
            {
                // connected, plunger calibration needed - flash yellow/green
                hb = !hb;
                ledR = (hb ? 0 : 1);
                ledG = 0;
                ledB = 1;
            }
            else
            {
                // connected - flash blue/green
                hb = !hb;
                ledR = 1;
                ledG = (hb ? 0 : 1);
                ledB = (hb ? 1 : 0);
            }
            
            // reset the heartbeat timer
            hbTimer.reset();
            ++hbcnt;
        }
    }
}