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

Revision:
17:ab3cec0c8bf4
Parent:
16:c35f905c3311
Child:
18:5e890ebd0023
--- a/main.cpp	Mon Dec 29 19:27:52 2014 +0000
+++ b/main.cpp	Fri Feb 27 04:14:04 2015 +0000
@@ -19,96 +19,81 @@
 //
 // Pinscape Controller
 //
-// "Pinscape" is the name of my custom-built virtual pinball cabinet.  I wrote this
-// software to perform a number of 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 host PC via USB and can interface with numerous types of external
-// hardware.
+// "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 Pinscape, but 
-// it uses standard interfaces in Windows and Visual Pinball, so it should be
-// readily usable in anyone else's VP-based cabinet.  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.
+// 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 device appears to the host computer as a USB joystick.  This works with the
-// standard Windows joystick device drivers, so there's no need to install any
-// software on the PC - Windows should recognize it as a joystick when you plug
-// it in and shouldn't ask you to install anything.  If you bring up the 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 device calibrates itself automatically for the
-// accelerometer data, and has its own special calibration procedure for the
-// plunger (see below).
-//
-// The controller provides the following functions.  It should be possible to use
-// any subet of the features without using all of them.  External hardware for any
-// particular function can simply be omitted if that feature isn't needed.
+// 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).
 //
-//  - Nudge sensing via the KL25Z's on-board accelerometer.  Nudge accelerations are
-//    processed into a physics model of a rolling ball, and changes to the ball's
-//    motion are sent to the host computer via the joystick interface.  This is designed
-//    especially to work with Visuall Pinball's nudge handling to produce realistic 
-//    on-screen results in VP.  By doing some physics modeling right on the device, 
-//    rather than sending raw accelerometer data to VP, we can produce better results
-//    using our awareness of the real physical parameters of a pinball cabinet.
-//    VP's nudge handling has to be more generic, so it can't make the same sorts
-//    of assumptions that we can about the dynamics of a real cabinet.
+// 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.
 //
-//    The nudge data reports are compatible with the built-in Windows USB joystick 
-//    drivers and with VP's own joystick input scheme, so the nudge sensing is almost 
-//    plug-and-play.  There are no Windiows drivers to install, and the only VP work 
-//    needed is to customize a few global preference settings.
+//  - 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.  
-//    The sensor must be wired to a particular set of I/O ports on the KL25Z, and must 
-//    be positioned adjacent to the plunger with proper lighting.  The physical and
-//    electronic installation details are desribed in the project documentation.  We read 
-//    the CCD to determine how far back the plunger is pulled, and report this to Visual 
-//    Pinball via the joystick interface.  As with the nudge data, this is all nearly
-//    plug-and-play, in that it works with the default Windows USB drivers and works 
-//    with the existing VP handling for analog plunger input.  A few VP settings are
-//    needed to tell VP to allow the plunger.
+//    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 might change slightly 
-//    when you put everything back together.)  To calibrate, you have to attach a
-//    momentary switch (e.g., a push-button switch) between one of the KL25Z ground
-//    pins (e.g., jumper J9 pin 12) and PTE29 (J10 pin 9).  Press and hold the
-//    button for about two seconds - the LED on the KL25Z wlil flash blue while
-//    you hold the button, and will turn solid blue when you've held it down long
-//    enough to enter calibration mode.  This mode will last about 15 seconds.
-//    Simply pull the plunger all the way back, hold it for a few moments, and
-//    gradually return it to the starting position.  *Don't* release it - we want
-//    to measure the maximum retracted position and the rest position, but NOT
-//    the maximum forward position when the outer barrel spring is compressed.
-//    After about 15 seconds, the device will save the new calibration settings
-//    to its flash memory, and the LED will return to the regular "heartbeat" 
-//    flashes.  If this is the first time you calibrated, you should observe the
-//    color of the flashes change from yellow/green to blue/green to indicate
-//    that the plunger has been calibrated.
+//    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.
 //
-//    Note that while Visual Pinball itself has good native support for analog 
-//    plungers, most of the VP tables in circulation don't implement the necessary
-//    scripting features to make this work properly.  Therefore, you'll have to do
-//    a little scripting work for each table you download to add the required code
-//    to that individual table.  The work has to be customized for each table, so
-//    I haven't been able to automate this process, but I have tried to reduce it
-//    to a relatively simple recipe that I've documented separately.
-//
-//  - In addition to the CCD sensor, a button should be attached (also described in 
-//    the project documentation) to activate calibration mode for the plunger.  When 
-//    calibration mode is activated, the software reads the plunger position for about 
-//    10 seconds when to note the limits of travel, and uses these limits to ensure
-//    accurate reports to VP that properly report the actual position of the physical
-//    plunger.  The calibration is stored in non-volatile memory on the KL25Z, so it's
-//    only necessary to calibrate once - the calibration will survive power cycling
-//    and reboots of the PC.  It's only necessary to recalibrate if the CCD sensor or
-//    the plunger are removed and reinstalled, since the relative alignment of the
-//    parts could cahnge slightly when reinstalling.
+//    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
@@ -229,13 +214,20 @@
 #include "FreescaleIAP.h"
 #include "crc32.h"
 
+// our local configuration file
+#include "config.h"
+
 
 // ---------------------------------------------------------------------------
-//
-// Configuration details
+// utilities
+
+// number of elements in an array
+#define countof(x) (sizeof(x)/sizeof((x)[0]))
+
+
+// ---------------------------------------------------------------------------
+// USB device vendor ID, product ID, and version.  
 //
-
-// Our 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
@@ -249,13 +241,6 @@
 // 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.
 //
-// We use unit #7 by default.  There doesn't seem to be a requirement that
-// unit numbers be contiguous (DirectOutput Framework and other software
-// seem happy to have units 0 and 7 installed, without 1-6 existing).
-// Marking this unit as #7 should work for almost everybody out of the box;
-// the most common case seems to be to have a single LedWiz installed, and
-// it's probably extremely rare to more than two.
-//
 // 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
@@ -266,235 +251,29 @@
 const uint16_t USB_VENDOR_ID = 0xFAFA;
 const uint16_t USB_PRODUCT_ID = 0x00F0;
 const uint16_t USB_VERSION_NO = 0x0006;
-const uint8_t DEFAULT_LEDWIZ_UNIT_NUMBER = 0x07;
 
-// Number of pixels we read from the sensor on each frame.  This can be
-// less than the physical pixel count if desired; we'll read every nth
-// piexl if so.  E.g., with a 1280-pixel physical sensor, if npix is 320,
-// we'll read every 4th pixel.  It takes time to read each pixel, so the
-// fewer pixels we read, the higher the refresh rate we can achieve.
-// It's therefore better not to read more pixels than we have to.
-//
-// VP seems to have an internal resolution in the 8-bit range, so there's
-// no apparent benefit to reading more than 128-256 pixels when using VP.
-// Empirically, 160 pixels seems about right.  The overall travel of a
-// standard pinball plunger is about 3", so 160 pixels gives us resolution
-// of about 1/50".  This seems to take full advantage of VP's modeling
-// ability, and is probably also more precise than a human player's
-// perception of the plunger position.
-const int npix = 160;
-
-// On-board RGB LED elements - we use these for diagnostic displays.
-DigitalOut ledR(LED1), ledG(LED2), ledB(LED3);
-
-// calibration button - switch input and LED output
-DigitalIn calBtn(PTE29);
-DigitalOut calBtnLed(PTE23);
-
-// Joystick button input pin assignments.  You can wire up to
-// 32 GPIO ports to buttons (equipped with momentary switches).
-// Connect each switch between the desired GPIO port and ground
-// (J9 pin 12 or 14).  When the button is pressed, we'll tell the
-// host PC that the corresponding joystick button is pressed.  We
-// debounce the keystrokes in software, so you can simply wire
-// directly to pushbuttons with no additional external hardware.
-//
-// Note that we assign 24 buttons by default, even though the USB
-// joystick interface can handle up to 32 buttons.  VP itself only
-// allows mapping of up to 24 buttons in the preferences dialog 
-// (although it can recognize 32 buttons internally).  If you want 
-// more buttons, you can reassign pins that are assigned by default
-// as LedWiz outputs.  To reassign a pin, find the pin you wish to
-// reassign in the LedWizPortMap array below, and change the pin name 
-// there to NC (for Not Connected).  You can then change one of the
-// "NC" entries below to the reallocated pin name.  The limit is 32
-// buttons total.
-//
-// Note: PTD1 (pin J2-12) should NOT be assigned as a button input,
-// as this pin is physically connected on the KL25Z to the on-board
-// indicator LED's blue segment.  This precludes any other use of
-// the pin.
-PinName buttonMap[] = {
-    PTC2,      // J10 pin 10, joystick button 1
-    PTB3,      // J10 pin 8,  joystick button 2
-    PTB2,      // J10 pin 6,  joystick button 3
-    PTB1,      // J10 pin 4,  joystick button 4
-    
-    PTE30,     // J10 pin 11, joystick button 5
-    PTE22,     // J10 pin 5,  joystick button 6
-    
-    PTE5,      // J9 pin 15,  joystick button 7
-    PTE4,      // J9 pin 13,  joystick button 8
-    PTE3,      // J9 pin 11,  joystick button 9
-    PTE2,      // J9 pin 9,   joystick button 10
-    PTB11,     // J9 pin 7,   joystick button 11
-    PTB10,     // J9 pin 5,   joystick button 12
-    PTB9,      // J9 pin 3,   joystick button 13
-    PTB8,      // J9 pin 1,   joystick button 14
-    
-    PTC12,     // J2 pin 1,   joystick button 15
-    PTC13,     // J2 pin 3,   joystick button 16
-    PTC16,     // J2 pin 5,   joystick button 17
-    PTC17,     // J2 pin 7,   joystick button 18
-    PTA16,     // J2 pin 9,   joystick button 19
-    PTA17,     // J2 pin 11,  joystick button 20
-    PTE31,     // J2 pin 13,  joystick button 21
-    PTD6,      // J2 pin 17,  joystick button 22
-    PTD7,      // J2 pin 19,  joystick button 23
-    
-    PTE1,      // J2 pin 20,  joystick button 24
-
-    NC,        // not used,   joystick button 25
-    NC,        // not used,   joystick button 26
-    NC,        // not used,   joystick button 27
-    NC,        // not used,   joystick button 28
-    NC,        // not used,   joystick button 29
-    NC,        // not used,   joystick button 30
-    NC,        // not used,   joystick button 31
-    NC         // not used,   joystick button 32
-};
-
-// LED-Wiz emulation output pin assignments.  
-//
-// The LED-Wiz protocol allows setting individual intensity levels
-// on all outputs, with 48 levels of intensity.  This can be used
-// to control lamp brightness and motor speeds, among other things.
-// Unfortunately, the KL25Z only has 10 PWM channels, so while we 
-// can support the full complement of 32 outputs, we can only provide 
-// PWM dimming/speed control on 10 of them.  The remaining outputs 
-// can only be switched fully on and fully off - we can't support
-// dimming on these, so they'll ignore any intensity level setting 
-// requested by the host.  Use these for devices that don't have any
-// use for intensity settings anyway, such as contactors and knockers.
-//
-// Ports with pins assigned as "NC" are not connected.  That is,
-// there's no physical pin for that LedWiz port number.  You can
-// send LedWiz commands to turn NC ports on and off, but doing so
-// will have no effect.  The reason we leave some ports unassigned
-// is that we don't have enough physical GPIO pins to fill out the
-// full LedWiz complement of 32 ports.  Many pins are already taken
-// for other purposes, such as button inputs or the plunger CCD
-// interface.
-//
-// The mapping between physical output pins on the KL25Z and the
-// assigned LED-Wiz port numbers is essentially arbitrary - you can
-// customize this by changing the entries in the array below if you
-// wish to rearrange the pins for any reason.  Be aware that some
-// of the physical outputs are already used for other purposes
-// (e.g., some of the GPIO pins on header J10 are used for the
-// CCD sensor - but you can of course reassign those as well by
-// changing the corresponding declarations elsewhere in this module).
-// The assignments we make here have two main objectives: first,
-// to group the outputs on headers J1 and J2 (to facilitate neater
-// wiring by keeping the output pins together physically), and
-// second, to make the physical pin layout match the LED-Wiz port
-// numbering order to the extent possible.  There's one big wrench
-// in the works, though, which is the limited number and discontiguous
-// placement of the KL25Z PWM-capable output pins.  This prevents
-// us from doing the most obvious sequential ordering of the pins,
-// so we end up with the outputs arranged into several blocks.
-// Hopefully this isn't too confusing; for more detailed rationale,
-// read on...
-// 
-// With the LED-Wiz, the host software configuration usually 
-// assumes that each RGB LED is hooked up to three consecutive ports
-// (for the red, green, and blue components, which need to be 
-// physically wired to separate outputs to allow each color to be 
-// controlled independently).  To facilitate this, we arrange the 
-// PWM-enabled outputs so that they're grouped together in the 
-// port numbering scheme.  Unfortunately, these outputs aren't
-// together in a single group in the physical pin layout, so to
-// group them logically in the LED-Wiz port numbering scheme, we
-// have to break up the overall numbering scheme into several blocks.
-// So our port numbering goes sequentially down each column of
-// header pins, but there are several break points where we have
-// to interrupt the obvious sequence to keep the PWM pins grouped
-// logically.
-//
-// In the list below, "pin J1-2" refers to pin 2 on header J1 on
-// the KL25Z, using the standard pin numbering in the KL25Z 
-// documentation - this is the physical pin that the port controls.
-// "LW port 1" means LED-Wiz port 1 - this is the LED-Wiz port
-// number that you use on the PC side (in the DirectOutput config
-// file, for example) to address the port.  PWM-capable ports are
-// marked as such - we group the PWM-capable ports into the first
-// 10 LED-Wiz port numbers.
-//
-// If you wish to reallocate a pin in the array below to some other
-// use, such as a button input port, simply change the pin name in
-// the entry to NC (for Not Connected).  This will disable the given
-// logical LedWiz port number and free up the physical pin.
-//
-// If you wish to reallocate a pin currently assigned to the button
-// input array, simply change the entry for the pin in the buttonMap[]
-// array above to NC (for "not connected"), and plug the pin name into
-// a slot of your choice in the array below.
-//
-// Note: PTD1 (pin J2-12) should NOT be assigned as an LedWiz output,
-// as this pin is physically connected on the KL25Z to the on-board
-// indicator LED's blue segment.  This precludes any other use of
-// the pin.
-// 
-struct {
-    PinName pin;
-    bool isPWM;
-} ledWizPortMap[32] = {
-    { PTA1, true },      // pin J1-2,  LW port 1  (PWM capable - TPM 2.0 = channel 9)
-    { PTA2, true },      // pin J1-4,  LW port 2  (PWM capable - TPM 2.1 = channel 10)
-    { PTD4, true },      // pin J1-6,  LW port 3  (PWM capable - TPM 0.4 = channel 5)
-    { PTA12, true },     // pin J1-8,  LW port 4  (PWM capable - TPM 1.0 = channel 7)
-    { PTA4, true },      // pin J1-10, LW port 5  (PWM capable - TPM 0.1 = channel 2)
-    { PTA5, true },      // pin J1-12, LW port 6  (PWM capable - TPM 0.2 = channel 3)
-    { PTA13, true },     // pin J2-2,  LW port 7  (PWM capable - TPM 1.1 = channel 13)
-    { PTD5, true },      // pin J2-4,  LW port 8  (PWM capable - TPM 0.5 = channel 6)
-    { PTD0, true },      // pin J2-6,  LW port 9  (PWM capable - TPM 0.0 = channel 1)
-    { PTD3, true },      // pin J2-10, LW port 10 (PWM capable - TPM 0.3 = channel 4)
-    { PTD2, false },     // pin J2-8,  LW port 11
-    { PTC8, false },     // pin J1-14, LW port 12
-    { PTC9, false },     // pin J1-16, LW port 13
-    { PTC7, false },     // pin J1-1,  LW port 14
-    { PTC0, false },     // pin J1-3,  LW port 15
-    { PTC3, false },     // pin J1-5,  LW port 16
-    { PTC4, false },     // pin J1-7,  LW port 17
-    { PTC5, false },     // pin J1-9,  LW port 18
-    { PTC6, false },     // pin J1-11, LW port 19
-    { PTC10, false },    // pin J1-13, LW port 20
-    { PTC11, false },    // pin J1-15, LW port 21
-    { PTE0, false },     // pin J2-18, LW port 22
-    { NC, false },       // Not used,  LW port 23
-    { NC, false },       // Not used,  LW port 24
-    { NC, false },       // Not used,  LW port 25
-    { NC, false },       // Not used,  LW port 26
-    { NC, false },       // Not used,  LW port 27
-    { NC, false },       // Not used,  LW port 28
-    { NC, false },       // Not used,  LW port 29
-    { NC, false },       // Not used,  LW port 30
-    { NC, false },       // Not used,  LW port 31
-    { NC, false }        // Not used,  LW port 32
-};
-
-
-// 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
 
 // Joystick axis report range - we report from -JOYMAX to +JOYMAX
 #define JOYMAX 4096
 
 
-// ---------------------------------------------------------------------------
-// utilities
+// --------------------------------------------------------------------------
+//
+// Potentiometer configuration
+//
+#ifdef POT_SENSOR_ENABLED
+#define IF_POT(x) x
+#else
+#define IF_POT(x)
+#endif
 
-// number of elements in an array
-#define countof(x) (sizeof(x)/sizeof((x)[0]))
+
+// ---------------------------------------------------------------------------
+//
+// On-board RGB LED elements - we use these for diagnostic displays.
+//
+DigitalOut ledR(LED1), ledG(LED2), ledB(LED3);
+
 
 // ---------------------------------------------------------------------------
 //
@@ -701,48 +480,79 @@
     return buttons;
 }
 
-// Read buttons with debouncing.  We keep a circular buffer
-// of recent input readings.  We'll AND together the status of
-// each button over the past 50ms.  A button that has been on
-// continuously for 50ms will be reported as ON.  All others
-// will be reported as OFF.
+// Read buttons with debouncing.  
+//
+// Debouncing is the process of filtering out transients from button
+// state changes.  When an electrical switch is closed or opened, the
+// signal can have a brief period of instability that makes the switch
+// appear to turn on and off very rapidly.  This is known as "bouncing".
+//
+// To remove the transients, we filter the signal by requiring each 
+// change to stick for at least a minimum interval (we use 50ms).  We
+// keep a short recent history of each button's state for this purpose.
+// If we see a button change state, we ignore the change if we saw the
+// same button make another change within the same interval.
 uint32_t readButtonsDebounced()
 {
     struct reading {
-        int dt;           // time since previous reading
-        uint32_t b;       // button state at this reading
+        // elapsed time between this reading and the previous reading
+        int dt;
+        
+        // Final button state for each button that changed on this
+        // report.  OR this with a new report (after applying the
+        // mask 'm') to carry forward the changes that occurred in
+        // this report to the new report.
+        uint32_t b;
+        
+        // Change mask at this report.  This is a bit mask of the buttons
+        // that *didn't* change on this report.  AND this mask with a
+        // new reading to filter buttons out of the new reading that
+        // changed on this report.
+        uint32_t m;
     };
     static reading readings[8];  // circular buffer of readings
     static int ri = 0;    // reading buffer index (next write position)
+    static int bPrv = 0;  // immediately previous report
         
     // get the write pointer
     reading *r = &readings[ri];
 
     // figure the time since the last reading, and read the raw button state
-    r->dt = buttonTimer.read_ms();
-    uint32_t b = r->b = readButtonsRaw();
+    int ms = r->dt = buttonTimer.read_ms();
+    uint32_t b = readButtonsRaw();
     
     // start timing the next interval
     buttonTimer.reset();
     
-    // AND together readings over 25ms
-    int ms = 0;
-    for (int i = 1 ; i < countof(readings) && ms < 25 ; ++i)
+    // mask out changes for any buttons that changed state within the
+    // past 50ms
+    for (int i = 1 ; i < countof(readings) && ms < 50 ; ++i)
     {
         // find the next prior reading, wrapping in the circular buffer
         int j = ri - i;
         if (j < 0) 
             j = countof(readings) - 1;
-            
         reading *rj = &readings[j];
-        
-        // AND the buttons for this reading
-        b &= rj->b;
-        
-        // count the time
+
+        // For any button that changed state in the prior reading 'rj',
+        // remove any new change and restore it to its 'rj' state.
+        b &= rj->m;
+        b |= rj->b;
+                
+        // add in the time to the next prior report
         ms += rj->dt;
     }
     
+    // figure which buttons changed on this report vs the prior report
+    uint32_t m = b ^ bPrv;
+    
+    // save the change mask and changed button vector in our history entry
+    r->m = ~m;
+    r->b = b & m;
+    
+    // save this as the prior report
+    bPrv = b;
+    
     // advance the write position for next time
     ri += 1;
     if (ri >= countof(readings)) 
@@ -754,88 +564,6 @@
 
 // ---------------------------------------------------------------------------
 //
-// 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 and max
-        int plungerMin;
-        int plungerZero;
-        int plungerMax;
-        
-        // is the CCD enabled?
-        int ccdEnabled;
-        
-        // LedWiz unit number
-        uint8_t ledWizUnitNo;
-    } d;
-};
-
-
-// ---------------------------------------------------------------------------
-//
 // Customization joystick subbclass
 //
 
@@ -904,6 +632,19 @@
 // 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
 {
@@ -1191,6 +932,113 @@
 
 // ---------------------------------------------------------------------------
 //
+// 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
@@ -1239,11 +1087,11 @@
         cfg.d.sig = cfg.SIGNATURE;
         cfg.d.vsn = cfg.VERSION;
         cfg.d.plungerCal = 0;
-        cfg.d.plungerZero = 0;
-        cfg.d.plungerMin = 0;
-        cfg.d.plungerMax = npix;
+        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.ccdEnabled = true;
+        cfg.d.plungerEnabled = true;
     }
     
     // Create the joystick USB client.  Note that we use the LedWiz unit
@@ -1252,6 +1100,15 @@
         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;
@@ -1278,32 +1135,108 @@
     // create the accelerometer object
     Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, MMA8451_INT_PIN);
     
-    // create the CCD array object
-    TSL1410R ccd(PTE20, PTE21, PTB0);
+    // 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;
     
-    // last accelerometer report, in mouse coordinates
-    int x = 0, y = 0, z = 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;
     
-    // previous two plunger readings, for "debouncing" the results (z0 is
-    // the most recent, z1 is the one before that)
-    int z0 = 0, z1 = 0, z2 = 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();
+    
+    // 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.
-    // The actual plunger spring return speed seems to be too slow for VP, 
-    // so when we detect the start of this motion, we immediately tell VP
-    // to return the plunger to rest, then we monitor the real plunger 
-    // until it atcually stops.
+    //
+    // 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
-    ccd.clear();
+    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.ccdEnabled ? 0x01 : 0x00);
+    uint16_t statusFlags = (cfg.d.plungerEnabled ? 0x01 : 0x00);
     
     // flag: send a pixel dump after the next read
     bool reportPix = false;
@@ -1366,13 +1299,13 @@
                         
                         // set the configuration parameters from the message
                         cfg.d.ledWizUnitNo = newUnitNo;
-                        cfg.d.ccdEnabled = data[3] & 0x01;
+                        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.ccdEnabled)
+                        if (!cfg.d.plungerEnabled)
                             z = 0;
                         
                         // save the configuration
@@ -1425,7 +1358,7 @@
         }
        
         // check for plunger calibration
-        if (!calBtn)
+        if (calBtn != 0 && !calBtn->read())
         {
             // check the state
             switch (calBtnState)
@@ -1516,283 +1449,321 @@
         {
             calBtnLit = newCalBtnLit;
             if (calBtnLit) {
-                calBtnLed = 1;
+                if (calBtnLed != 0)
+                    calBtnLed->write(1);
                 ledR = 1;
                 ledG = 1;
                 ledB = 0;
             }
             else {
-                calBtnLed = 0;
+                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
-        uint16_t pix[npix];
-        if (cfg.d.ccdEnabled)
+        if (cfg.d.plungerEnabled)
         {
             // start with the previous reading, in case we don't have a
             // clear result on this frame
             int znew = z;
-
-            // read the array
-            ccd.read(pix, npix, ccdReadCB, 0, 3);
-    
-            // get the average brightness at each end of the sensor
-            long avg1 = (long(pix[0]) + long(pix[1]) + long(pix[2]) + long(pix[3]) + long(pix[4]))/5;
-            long avg2 = (long(pix[npix-1]) + long(pix[npix-2]) + long(pix[npix-3]) + long(pix[npix-4]) + long(pix[npix-5]))/5;
-            
-            // figure the midpoint in the brightness; multiply by 3 so that we can
-            // compare sums of three pixels at a time to smooth out noise
-            long midpt = (avg1 + avg2)/2 * 3;
-            
-            // Work from the bright end to the dark end.  VP interprets the
-            // Z axis value as the amount the plunger is pulled: zero is the
-            // rest position, and the axis maximum is fully pulled.  So we 
-            // essentially want to report how much of the sensor is lit,
-            // since this increases as the plunger is pulled back.
-            int si = 1, di = 1;
-            if (avg1 < avg2)
-                si = npix - 2, di = -1;
-    
-            // If the bright end and dark end don't differ by enough, skip this
-            // reading entirely - we must have an overexposed or underexposed frame.
-            // Otherwise proceed with the scan.
-            if (labs(avg1 - avg2) > 0x1000)
+            if (plungerSensor.highResScan(pos))
             {
-                uint16_t *pixp = pix + si;           
-                for (int n = 1 ; n < npix - 1 ; ++n, pixp += di)
+                // 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)
                 {
-                    // if we've crossed the midpoint, report this position
-                    if (long(pixp[-1]) + long(pixp[0]) + long(pixp[1]) < midpt)
-                    {
-                        // note the new position
-                        int pos = n;
+                    // 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;
                         
-                        // Calibrate, or apply calibration, depending on the mode.
-                        // In either case, normalize to our range.  VP appears to
-                        // ignore negative Z axis values.
-                        if (calBtnState == 3)
-                        {
-                            // calibrating - note if we're expanding the calibration envelope
-                            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
-                        {
-                            // Running normally - normalize to the calibration range.  Note
-                            // that values below the zero point are allowed - the zero point
-                            // represents the park position, where the plunger sits when at
-                            // rest, but a mechanical plunger has a smmall amount of travel
-                            // in the "push" direction.  We represent forward travel with
-                            // negative z values.
-                            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));
-                        }
-                        
-                        // done
-                        break;
-                    }
+                    // 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));
                 }
             }
 
-            // Determine if the plunger is being fired - i.e., if the player
-            // has just released the plunger from a retracted position.
-            //
-            // We treat firing as an event.  That is, we tell VP when the
-            // plunger is fired, and then stop sending data until the firing
-            // is complete, allowing VP to carry out the firing motion using
-            // its internal model plunger rather than trying to track the
-            // intermediate positions of the mechanical plunger throughout
-            // the firing motion.  This is essential because the firing
-            // motion is too fast for us to track - in the time it takes us
-            // to read one frame, the plunger can make it all the way to the
-            // zero position and bounce back halfway.  Fortunately, the range
-            // of motions for the plunger is limited, so if we see any rapid
-            // change of position toward the rest position, it's reasonably
-            // safe to interpret it as a firing event.  
-            //
-            // This isn't foolproof.  The user can trick us by doing a 
-            // controlled rapid forward push but stopping short of the rest 
-            // position.  We'll misinterpret that as a firing event.  But 
-            // that's not a natural motion that a user would make with a
-            // plunger, so it's probably an acceptable false positive.
-            //
-            // Possible future enhancement: we could add a second physical
-            // sensor that detects when the plunger reaches the zero position
-            // and asserts an interrupt.  In the interrupt handler, set a
-            // flag indicating the zero position signal.  On each scan of
-            // the CCD, also check that flag; if it's set, enter firing
-            // event mode just as we do now.  The key here is that the
-            // secondary sensor would have to be something much faster
-            // than our CCD scan - it would have to react on, say, the
-            // millisecond time scale.  A simple mechanical switch or a
-            // proximity sensor could work.  This would let us detect
-            // with certainty when the plunger physically fires, eliminating
-            // the case where the use can fool us with motion that's fast
-            // enough to look like a release but doesn't actually reach the
-            // starting position.
+            // 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 && wizOn[ZBLaunchBallPort-1])
+            {
+                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 >= JOYMAX/3)
+                        newState = 1;
+                    else if (znew < -JOYMAX/12)
+                        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 < JOYMAX/3)
+                        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.
+                    if (znew >= JOYMAX/3)
+                        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 > -JOYMAX/24)
+                        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 < -JOYMAX/12)
+                        newState = 3;
+                    else if (lbTimer.read_ms() > 200)
+                        newState = 0;                    
+                    break;
+                }
+                
+                // change states if desired
+                if (newState != lbState)
+                {
+                    // if we're entering Launch state, press the Launch Ball button
+                    if (newState == 3 && lbState != 4)
+                        simButtons |= (1 << (LaunchBallButton - 1));
+                        
+                    // 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 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.
             //
-            // To detremine when a firing even occurs, we watch for rapid
-            // motion from a retracted position towards the rest position -
-            // that is, large position changes in the negative direction over
-            // a couple of consecutive readings.  When we see a rapid move
-            // toward zero, we set our internal 'firing' flag, immediately
-            // report to VP that the plunger has returned to the zero 
-            // position, and then suspend reports until the mechanical
-            // readings indicate that the plunger has come to rest (indicated
-            // by several readings in a row at roughly the same position).
+            // 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.
             //
-            // Tolerance for firing is 1/3 of the current pull distance, or
-            // about 1/2", whichever is greater.  Making this value too small
-            // makes for too many false positives.  Empirically, 1/4" is too
-            // twitchy, so set a floor at about 1/2".  But we can be less
-            // sensitive the further back the plunger is pulled, since even
-            // a long pull will snap back quickly.  Note that JOYMAX always
-            // corresponds to about 3", no matter how many pixels we're
-            // reading, since the physical sensor is about 3" long; so we
-            // factor out the pixel count calculate (approximate) physical
-            // distances based on the normalized axis range.
-            // 
-            // Firing pattern: when firing, don't simply report a solid 0,
-            // but instead report a series of pseudo-bouces.  This looks
-            // more realistic, beacause the real plunger is also bouncing
-            // around during this time.  To get maximum firing power in
-            // the simulation, though, our pseudo-bounces are tiny cmopared
-            // to the real thing.
-            const int restTol = JOYMAX/24;
-            int fireTol = z/3 > JOYMAX/6 ? z/3 : JOYMAX/6;
-            static const int firePattern[] = { 
-                -JOYMAX/12, -JOYMAX/12, -JOYMAX/12, 
-            };
-            if (firing != 0)
+            // 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 - we've already told VP to send its
-                // model plunger all the way back to the rest position, so
-                // send no further reports until the mechanical plunger
-                // actually comes to rest somewhere.
-                if (abs(z0 - z2) < restTol && abs(znew - z2) < restTol)
+                // Firing in progress.  Keep reporting the park position
+                // until the physical plunger position comes to rest.
+                const int restTol = JOYMAX/24;
+                if (firing == 1)
                 {
-                    // the plunger is back at rest - firing is done
-                    firing = 0;
-                    
-                    // resume normal reporting
-                    z = z2;
+                    // 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 < countof(firePattern))
+                else if (firing == 2)
                 {
-                    // firing - report the next position in the pseudo-bounce 
-                    // pattern
-                    z = firePattern[firing++];
+                    // 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
                 {
-                    // firing, out of pseudo-bounce items - just report the
-                    // rest position
+                    // until the physical plunger comes to rest, simply 
+                    // report the park position
                     z = 0;
+                    ++firing;
                 }
             }
-            else if (z0 < z2 && z1 < z2 && znew < z2
-                     && (z0 < z2 - fireTol 
-                         || z1 < z2 - fireTol
-                         || znew < z2 - fireTol))
-            {
-                // Big jumps toward rest position in last two readings - 
-                // firing has begun.  Report an immediate return to the
-                // rest position, and send no further reports until the
-                // physical plunger has come to rest.  This effectively
-                // detaches VP's model plunger from the real world for
-                // the duration of the spring return, letting VP evolve
-                // its model without trying to synchronize with the
-                // mechanical version.  The release motion is too fast
-                // for that to work well; we can't take samples quickly
-                // enough to get prcise velocity or acceleration
-                // readings.  It's better to let VP figure the speed
-                // and acceleration through modeling.  Plus, that lets
-                // each virtual table set the desired parameters for its
-                // virtual plunger, rather than imposing the actual
-                // mechanical charateristics of the physical plunger on
-                // every table.
-                firing = 1;
-                
-                // report the first firing pattern position
-                z = firePattern[0];
-            }
             else
             {
-                // everything normal; report the 3rd recent position on
-                // tape delay
-                z = z2;
+                // not in firing mode - report the true physical position
+                z = znew;
             }
-        
-            // shift in the new reading
+
+            // shift the new reading into the recent history buffer
             z2 = z1;
             z1 = z0;
             z0 = znew;
         }
-        else
-        {
-            // plunger disabled - pause 10ms to throttle updates to a
-            // reasonable pace
-            wait_ms(10);
-        }
 
-        // read the accelerometer
-        int xa, ya;
-        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;
-        
         // update the buttons
         uint32_t buttons = readButtonsDebounced();
-        
-        // 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, statusFlags);
+
+        // 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;
-            
-            // send reports for all pixels
-            int idx = 0;
-            while (idx < npix)
-                js.updateExposure(idx, npix, pix);
-                
-            // The pixel dump requires many USB reports, since each report
-            // can only send a few pixel values.  An integration cycle has
-            // been running all this time, since each read starts a new
-            // cycle.  Our timing is longer than usual on this round, so
-            // the integration won't be comparable to a normal cycle.  Throw
-            // this one away by doing a read now, and throwing it away - that 
-            // will get the timing of the *next* cycle roughly back to normal.
-            ccd.read(pix, npix);
         }
         
 #ifdef DEBUG_PRINTF
@@ -1832,7 +1803,7 @@
                 ledG = (hb ? 1 : 0);
                 ledB = 0;
             }
-            else if (cfg.d.ccdEnabled && !cfg.d.plungerCal)
+            else if (cfg.d.plungerEnabled && !cfg.d.plungerCal)
             {
                 // connected, plunger calibration needed - flash yellow/green
                 hb = !hb;