Mirror with some correction

Dependencies:   mbed FastIO FastPWM USBDevice

Revision:
53:9b2611964afc
Parent:
52:8298b2a73eb2
Child:
54:fd77a6b2f76c
--- a/main.cpp	Sat Mar 05 00:16:52 2016 +0000
+++ b/main.cpp	Fri Apr 22 17:58:35 2016 +0000
@@ -82,57 +82,53 @@
 //    to the PC as a joystick button or as a keyboard key (you can select which key
 //    is used for each 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 hardware is limited to 10 PWM ports.  Ports beyond the
-//    10 PWM ports are simple digital on/off ports.  Intensity level settings on 
-//    digital ports is ignored, so such ports can only be used for devices such as 
-//    contactors and solenoids that don't need differeing intensities.
+//  - LedWiz emulation.  The KL25Z can pretend to be an LedWiz device.  This lets
+//    you connect feedback devices (lights, solenoids, motors) to GPIO ports on the 
+//    KL25Z, and lets PC software (such as Visual Pinball) control them during game 
+//    play to create a more immersive playing experience.  The Pinscape software
+//    presents itself to the host as an LedWiz device and accepts the full LedWiz
+//    command set, so software on the PC designed for real LedWiz'es can control
+//    attached devices without any modifications.
 //
-//    Note that the KL25Z can only supply or sink 4mA on its output ports, so external 
-//    amplifier hardware is required to use 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.  For example, you can use it to drive a motor at
-//    different speeds via the PWM intensity.
-//
-//    The Controller device can report any desired LedWiz unit number to the host, 
-//    which makes it possible for one or more Pinscape Controller units to coexist
-//    with one more more real LedWiz units in the same machine.  The LedWiz design 
-//    allows for up to 16 units to be installed in one machine.  Each device needs
-//    to have a distinct LedWiz Unit Number, which allows software on the PC to
-//    address each device independently.
-//
-//    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.
+//    Even though the software provides a very thorough LedWiz emulation, the KL25Z
+//    GPIO hardware design imposes some serious limitations.  The big one is that
+//    the KL25Z only has 10 PWM channels, meaning that only 10 ports can have
+//    varying-intensity outputs (e.g., for controlling the brightness level of an
+//    LED or the speed or a motor).  You can control more than 10 output ports, but
+//    only 10 can have PWM control; the rest are simple "digital" ports that can only
+//    be switched fully on or fully off.  The second limitation is that the KL25Z
+//    just doesn't have that many GPIO ports overall.  There are enough to populate
+//    all 32 button inputs OR all 32 LedWiz outputs, but not both.  The default is
+//    to assign 24 buttons and 22 LedWiz ports; you can change this balance to trade
+//    off more outputs for fewer inputs, or vice versa.  The third limitation is that
+//    the KL25Z GPIO pins have *very* tiny amperage limits - just 4mA, which isn't
+//    even enough to control a small LED.  So in order to connect any kind of feedback
+//    device to an output, you *must* build some external circuitry to boost the
+//    current handing.  The Build Guide has a reference circuit design for this
+//    purpose that's simple and inexpensive to build.
 //
 //  - Enhanced LedWiz emulation with TLC5940 PWM controller chips.  You can attach
 //    external PWM controller chips for controlling device outputs, instead of using
-//    the limited LedWiz emulation through the on-board GPIO ports as described above. 
-//    The software can control a set of daisy-chained TLC5940 chips, which provide
-//    16 PWM outputs per chip.  Two of these chips give you the full complement
-//    of 32 output ports of an actual LedWiz, and four give you 64 ports, which
-//    should be plenty for nearly any virtual pinball project.  A private, extended
-//    version of the LedWiz protocol lets the host control the extra outputs, up to
-//    128 outputs per KL25Z (8 TLC5940s).  To take advantage of the extra outputs
-//    on the PC side, you need software that knows about the protocol extensions,
-//    which means you need the latest version of DirectOutput Framework (DOF).  VP
-//    uses DOF for its output, so VP will be able to use the added ports without any
-//    extra work on your part.  Older software (e.g., Future Pinball) that doesn't
-//    use DOF will still be able to use the LedWiz-compatible protocol, so it'll be
-//    able to control your first 32 ports (numbered 1-32 in the LedWiz scheme), but
-//    older software won't be able to address higher-numbered ports.  That shouldn't
-//    be a problem because older software wouldn't know what to do with the extra
-//    devices anyway - FP, for example, is limited to a pre-defined set of outputs.
-//    As long as you put the most common devices on the first 32 outputs, and use
-//    higher numbered ports for the less common devices that older software can't
-//    use anyway, you'll get maximum functionality out of software new and old.
+//    the on-board GPIO ports as described above.  The software can control a set of 
+//    daisy-chained TLC5940 chips.  Each chip provides 16 PWM outputs, so you just
+//    need two of them to get the full complement of 32 output ports of a real LedWiz.
+//    You can hook up even more, though.  Four chips gives you 64 ports, which should
+//    be plenty for nearly any virtual pinball project.  To accommodate the larger
+//    supply of ports possible with the PWM chips, the controller software provides
+//    a custom, extended version of the LedWiz protocol that can handle up to 128
+//    ports.  PC software designed only for the real LedWiz obviously won't know
+//    about the extended protocol and won't be able to take advantage of its extra
+//    capabilities, but the latest version of DOF (DirectOutput Framework) *does* 
+//    know the new language and can take full advantage.  Older software will still
+//    work, though - the new extensions are all backward compatible, so old software
+//    that only knows about the original LedWiz protocol will still work, with the
+//    obvious limitation that it can only access the first 32 ports.
+//
+//    The Pinscape Expansion Board project (which appeared in early 2016) provides
+//    a reference hardware design, with EAGLE circuit board layouts, that takes full
+//    advantage of the TLC5940 capability.  It lets you create a customized set of
+//    outputs with full PWM control and power handling for high-current devices
+//    built in to the boards.  
 //
 //  - Night Mode control for output devices.  You can connect a switch or button
 //    to the controller to activate "Night Mode", which disables feedback devices
@@ -213,6 +209,71 @@
 #define DECL_EXTERNS
 #include "config.h"
 
+
+// --------------------------------------------------------------------------
+// 
+// OpenSDA module identifier.  This is for the benefit of the Windows
+// configuration tool.  When the config tool installs a .bin file onto
+// the KL25Z, it will first find the sentinel string within the .bin file,
+// and patch the "\0" bytes that follow the sentinel string with the 
+// OpenSDA module ID data.  This allows us to report the OpenSDA 
+// identifiers back to the host system via USB, which in turn allows the 
+// config tool to figure out which OpenSDA MSD (mass storage device - a 
+// virtual disk drive) correlates to which Pinscape controller USB 
+// interface.  
+// 
+// This is only important if multiple Pinscape devices are attached to 
+// the same host.  There doesn't seem to be any other way to figure out 
+// which OpenSDA MSD corresponds to which KL25Z USB interface; the OpenSDA 
+// MSD doesn't report the KL25Z CPU ID anywhere, and the KL25Z doesn't
+// have any way to learn about the OpenSDA module it's connected to.  The
+// only way to pass this information to the KL25Z side that I can come up 
+// with is to have the Windows host embed it in the .bin file before 
+// downloading it to the OpenSDA MSD.
+//
+// We initialize the const data buffer (the part after the sentinel string)
+// with all "\0" bytes, so that's what will be in the executable image that
+// comes out of the mbed compiler.  If you manually install the resulting
+// .bin file onto the KL25Z (via the Windows desktop, say), the "\0" bytes
+// will stay this way and read as all 0's at run-time.  Since a real TUID
+// would never be all 0's, that tells us that we were never patched and
+// thus don't have any information on the OpenSDA module.
+//
+const char *getOpenSDAID()
+{
+    #define OPENSDA_PREFIX "///Pinscape.OpenSDA.TUID///"
+    static const char OpenSDA[] = OPENSDA_PREFIX "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0///";
+    const size_t OpenSDA_prefix_length = sizeof(OPENSDA_PREFIX) - 1;
+    
+    return OpenSDA + OpenSDA_prefix_length;
+}
+
+// --------------------------------------------------------------------------
+//
+// Build ID.  We use the date and time of compiling the program as a build
+// identifier.  It would be a little nicer to use a simple serial number
+// instead, but the mbed platform doesn't have a way to automate that.  The
+// timestamp is a pretty good proxy for a serial number in that it will
+// naturally increase on each new build, which is the primary property we
+// want from this.
+//
+// As with the embedded OpenSDA ID, we store the build timestamp with a
+// sentinel string prefix, to allow automated tools to find the static data
+// in the .bin file by searching for the sentinel string.  In contrast to 
+// the OpenSDA ID, the value we store here is for tools to extract rather 
+// than store, since we automatically populate it via the preprocessor 
+// macros.
+//
+const char *getBuildID()
+{
+    #define BUILDID_PREFIX "///Pinscape.Build.ID///"
+    static const char BuildID[] = BUILDID_PREFIX __DATE__ " " __TIME__ "///";
+    const size_t BuildID_prefix_length = sizeof(BUILDID_PREFIX) - 1;
+    
+    return BuildID + BuildID_prefix_length;
+}
+
+
 // --------------------------------------------------------------------------
 //
 // Custom memory allocator.  We use our own version of malloc() to provide
@@ -276,6 +337,14 @@
     bool running;
 };
 
+
+// --------------------------------------------------------------------------
+//
+// Reboot timer.  When we have a deferred reboot operation pending, we
+// set the target time and start the timer.
+Timer2 rebootTimer;
+long rebootTime_us;
+
 // --------------------------------------------------------------------------
 // 
 // USB product version number
@@ -332,29 +401,36 @@
     return (int32_t)wireUI32(b);
 }
 
-static const PinName pinNameMap[] =  {
-    NC,    PTA1,  PTA2,  PTA4,  PTA5,  PTA12, PTA13, PTA16, PTA17, PTB0,    // 0-9
-    PTB1,  PTB2,  PTB3,  PTB8,  PTB9,  PTB10, PTB11, PTB18, PTB19, PTC0,    // 10-19
-    PTC1,  PTC2,  PTC3,  PTC4,  PTC5,  PTC6,  PTC7,  PTC8,  PTC9,  PTC10,   // 20-29
-    PTC11, PTC12, PTC13, PTC16, PTC17, PTD0,  PTD1,  PTD2,  PTD3,  PTD4,    // 30-39
-    PTD5,  PTD6,  PTD7,  PTE0,  PTE1,  PTE2,  PTE3,  PTE4,  PTE5,  PTE20,   // 40-49
-    PTE21, PTE22, PTE23, PTE29, PTE30, PTE31                                // 50-55
-};
-inline PinName wirePinName(int c)
+// Convert "wire" (USB) pin codes to/from PinName values.
+// 
+// The internal mbed PinName format is 
+//
+//   ((port) << PORT_SHIFT) | (pin << 2)    // MBED FORMAT
+//
+// where 'port' is 0-4 for Port A to Port E, and 'pin' is
+// 0 to 31.  E.g., E31 is (4 << PORT_SHIFT) | (31<<2).
+//
+// We remap this to our more compact wire format where each
+// pin name fits in 8 bits:
+//
+//   ((port) << 5) | pin)                   // WIRE FORMAT
+//
+// E.g., E31 is (4 << 5) | 31.
+//
+// Wire code FF corresponds to PinName NC (not connected).
+//
+inline PinName wirePinName(uint8_t c)
 {
-    return (c < countof(pinNameMap) ? pinNameMap[c] : NC);
+    if (c == 0xFF)
+        return NC;                                  // 0xFF -> NC
+    else 
+        return PinName(
+            (int(c & 0xE0) << (PORT_SHIFT - 5))      // top three bits are the port
+            | (int(c & 0x1F) << 2));                // bottom five bits are pin
 }
 inline void pinNameWire(uint8_t *b, PinName n)
 {
-    b[0] = 0; // presume invalid -> NC
-    for (int i = 0 ; i < countof(pinNameMap) ; ++i)
-    {
-        if (pinNameMap[i] == n)
-        {
-            b[0] = i;
-            return;
-        }
-    }
+    *b = PINNAME_TO_WIRE(n);
 }
 
 
@@ -420,10 +496,6 @@
     for (int i = 0 ; i < MAX_OUT_PORTS && cfg.outPort[i].typ != PortTypeDisabled ; ++i)
         l.check(cfg.outPort[i]);
     
-    // check the special ports
-    for (int i = 0 ; i < countof(cfg.specialPort) ; ++i)
-        l.check(cfg.specialPort[i]);
-    
     // We now know which segments are taken for LedWiz use and which
     // are free.  Create diagnostic ports for the ones not claimed for
     // LedWiz use.
@@ -495,9 +567,34 @@
     virtual void set(uint8_t val) { out->set(255 - val); }
     
 private:
+    // underlying physical output
     LwOut *out;
 };
 
+// Global ZB Launch Ball state
+bool zbLaunchOn = false;
+
+// ZB Launch Ball output.  This is layered on a port (physical or virtual)
+// to track the ZB Launch Ball signal.
+class LwZbLaunchOut: public LwOut
+{
+public:
+    LwZbLaunchOut(LwOut *o) : out(o) { }
+    virtual void set(uint8_t val)
+    {
+        // update the global ZB Launch Ball state
+        zbLaunchOn = (val != 0);
+        
+        // pass it along to the underlying port, in case it's a physical output
+        out->set(val);
+    }
+        
+private:
+    // underlying physical or virtual output
+    LwOut *out;
+};
+
+
 // Gamma correction table for 8-bit input values
 static const uint8_t gamma[] = {
       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0, 
@@ -531,6 +628,9 @@
     LwOut *out;
 };
 
+// global night mode flag
+static bool nightMode = false;
+
 // Noisy output.  This is a filter object that we layer on top of
 // a physical pin output.  This filter disables the port when night
 // mode is engaged.
@@ -540,15 +640,28 @@
     LwNoisyOut(LwOut *o) : out(o) { }
     virtual void set(uint8_t val) { out->set(nightMode ? 0 : val); }
     
-    static bool nightMode;
+private:
+    LwOut *out;
+};
+
+// Night Mode indicator output.  This is a filter object that we
+// layer on top of a physical pin output.  This filter ignores the
+// host value and simply shows the night mode status.
+class LwNightModeIndicatorOut: public LwOut
+{
+public:
+    LwNightModeIndicatorOut(LwOut *o) : out(o) { }
+    virtual void set(uint8_t) 
+    {
+        // ignore the host value and simply show the current 
+        // night mode setting
+        out->set(nightMode ? 255 : 0);
+    }
 
 private:
     LwOut *out;
 };
 
-// global night mode flag
-bool LwNoisyOut::nightMode = false;
-
 
 //
 // The TLC5940 interface object.  We'll set this up with the port 
@@ -559,8 +672,13 @@
 {
     if (cfg.tlc5940.nchips != 0)
     {
-        tlc5940 = new TLC5940(cfg.tlc5940.sclk, cfg.tlc5940.sin, cfg.tlc5940.gsclk,
-            cfg.tlc5940.blank, cfg.tlc5940.xlat, cfg.tlc5940.nchips);
+        tlc5940 = new TLC5940(
+            wirePinName(cfg.tlc5940.sclk), 
+            wirePinName(cfg.tlc5940.sin),
+            wirePinName(cfg.tlc5940.gsclk),
+            wirePinName(cfg.tlc5940.blank), 
+            wirePinName(cfg.tlc5940.xlat), 
+            cfg.tlc5940.nchips);
     }
 }
 
@@ -656,7 +774,12 @@
 {
     if (cfg.hc595.nchips != 0)
     {
-        hc595 = new HC595(cfg.hc595.nchips, cfg.hc595.sin, cfg.hc595.sclk, cfg.hc595.latch, cfg.hc595.ena);
+        hc595 = new HC595(
+            wirePinName(cfg.hc595.nchips), 
+            wirePinName(cfg.hc595.sin), 
+            wirePinName(cfg.hc595.sclk), 
+            wirePinName(cfg.hc595.latch), 
+            wirePinName(cfg.hc595.ena));
         hc595->init();
         hc595->update();
     }
@@ -763,13 +886,6 @@
 static int numOutputs;
 static LwOut **lwPin;
 
-// Special output ports:
-//
-//    [0] = Night Mode indicator light
-//
-static LwOut *specialPin[1];
-const int SPECIAL_PIN_NIGHTMODE = 0;
-
 
 // Number of LedWiz emulation outputs.  This is the number of ports
 // accessible through the standard (non-extended) LedWiz protocol
@@ -785,7 +901,7 @@
 static uint8_t *outLevel;
 
 // create a single output pin
-LwOut *createLwPin(LedWizPortCfg &pc, Config &cfg)
+LwOut *createLwPin(int portno, LedWizPortCfg &pc, Config &cfg)
 {
     // get this item's values
     int typ = pc.typ;
@@ -883,6 +999,16 @@
     // If it's gamma-corrected, layer on a gamma corrector
     if (gamma)
         lwp = new LwGammaOut(lwp);
+        
+    // If this is the ZB Launch Ball port, layer a monitor object.  Note
+    // that the nominal port numbering in the cofnig starts at 1, but we're
+    // using an array index, so test against portno+1.
+    if (portno + 1 == cfg.plunger.zbLaunchBall.port)
+        lwp = new LwZbLaunchOut(lwp);
+        
+    // If this is the Night Mode indicator port, layer a night mode object.
+    if (portno + 1 == cfg.nightMode.port)
+        lwp = new LwNightModeIndicatorOut(lwp);
 
     // turn it off initially      
     lwp->set(0);
@@ -923,11 +1049,7 @@
     
     // create the pin interface object for each port
     for (i = 0 ; i < numOutputs ; ++i)
-        lwPin[i] = createLwPin(cfg.outPort[i], cfg);
-        
-    // create the pin interface for each special port
-    for (i = 0 ; i < countof(cfg.specialPort) ; ++i)
-        specialPin[i] = createLwPin(cfg.specialPort[i], cfg);
+        lwPin[i] = createLwPin(i, cfg.outPort[i], cfg);
 }
 
 // LedWiz output states.
@@ -1157,49 +1279,57 @@
     ButtonState()
     {
         di = NULL;
-        on = 0;
-        pressed = prev = 0;
-        dbstate = 0;
-        js = 0;
-        keymod = 0;
-        keycode = 0;
-        special = 0;
+        physState = logState = prevLogState = 0;
+        virtState = 0;
+        dbState = 0;
         pulseState = 0;
-        pulseTime = 0.0f;
+        pulseTime = 0;
     }
     
-    // DigitalIn for the button
+    // "Virtually" press or un-press the button.  This can be used to
+    // control the button state via a software (virtual) source, such as
+    // the ZB Launch Ball feature.
+    //
+    // To allow sharing of one button by multiple virtual sources, each
+    // virtual source must keep track of its own state internally, and 
+    // only call this routine to CHANGE the state.  This is because calls
+    // to this routine are additive: turning the button ON twice will
+    // require turning it OFF twice before it actually turns off.
+    void virtPress(bool on)
+    {
+        // Increment or decrement the current state
+        virtState += on ? 1 : -1;
+    }
+    
+    // DigitalIn for the button, if connected to a physical input
     TinyDigitalIn *di;
     
     // current PHYSICAL on/off state, after debouncing
-    uint8_t on : 1;
+    uint8_t physState : 1;
     
     // current LOGICAL on/off state as reported to the host.
-    uint8_t pressed : 1;
+    uint8_t logState : 1;
 
     // previous logical on/off state, when keys were last processed for USB 
     // reports and local effects
-    uint8_t prev : 1;
+    uint8_t prevLogState : 1;
+    
+    // Virtual press state.  This is used to simulate pressing the button via
+    // software inputs rather than physical inputs.  To allow one button to be
+    // controlled by mulitple software sources, each source should keep track
+    // of its own virtual state for the button independently, and then INCREMENT
+    // this variable when the source's state transitions from off to on, and
+    // DECREMENT it when the source's state transitions from on to off.  That
+    // will make the button's pressed state the logical OR of all of the virtual
+    // and physical source states.
+    uint8_t virtState;
     
     // Debounce history.  On each scan, we shift in a 1 bit to the lsb if
     // the physical key is reporting ON, and shift in a 0 bit if the physical
     // key is reporting OFF.  We consider the key to have a new stable state
     // if we have N consecutive 0's or 1's in the low N bits (where N is
     // a parameter that determines how long we wait for transients to settle).
-    uint8_t dbstate;
-    
-    // joystick button mask for the button, if mapped as a joystick button
-    uint32_t js;
-    
-    // keyboard modifier bits and scan code for the button, if mapped as a keyboard key
-    uint8_t keymod;
-    uint8_t keycode;
-    
-    // media control key code
-    uint8_t mediakey;
-    
-    // special key code
-    uint8_t special;
+    uint8_t dbState;
     
     // Pulse mode: a button in pulse mode transmits a brief logical button press and
     // release each time the attached physical switch changes state.  This is useful
@@ -1220,17 +1350,17 @@
     //
     // Each state change sticks for a minimum period; when the timer expires,
     // if the underlying physical switch is in a different state, we switch
-    // to the next state and restart the timer.  pulseTime is the amount of
-    // time remaining before we can make another state transition.  The state
-    // transitions require a complete cycle, 1 -> 2 -> 3 -> 4 -> 1...; this
-    // guarantees that the parity of the pulse count always matches the 
+    // to the next state and restart the timer.  pulseTime is the time remaining
+    // remaining before we can make another state transition, in microseconds.
+    // The state transitions require a complete cycle, 1 -> 2 -> 3 -> 4 -> 1...; 
+    // this guarantees that the parity of the pulse count always matches the 
     // current physical switch state when the latter is stable, which makes
     // it impossible to "trick" the host by rapidly toggling the switch state.
     // (On my original Pinscape cabinet, I had a hardware pulse generator
     // for coin door, and that *was* possible to trick by rapid toggling.
     // This software system can't be fooled that way.)
     uint8_t pulseState;
-    float pulseTime;
+    uint32_t pulseTime;
     
 } __attribute__((packed)) buttonState[MAX_BUTTONS];
 
@@ -1267,7 +1397,8 @@
     ButtonState *bs = buttonState;
     for (int i = 0 ; i < MAX_BUTTONS ; ++i, ++bs)
     {
-        // if it's connected, check its physical state
+        // if this logical button is connected to a physical input, check 
+        // the GPIO pin state
         if (bs->di != NULL)
         {
             // Shift the new state into the debounce history.  Note that
@@ -1275,10 +1406,10 @@
             // the reading by XOR'ing the low bit with 1.  And of course we
             // only want the low bit (since the history is effectively a bit
             // vector), so mask the whole thing with 0x01 as well.
-            uint8_t db = bs->dbstate;
+            uint8_t db = bs->dbState;
             db <<= 1;
             db |= (bs->di->read() & 0x01) ^ 0x01;
-            bs->dbstate = db;
+            bs->dbState = db;
             
             // if we have all 0's or 1's in the history for the required
             // debounce period, the key state is stable - check for a change
@@ -1286,7 +1417,7 @@
             const uint8_t stable = 0x1F;   // 00011111b -> 5 stable readings
             db &= stable;
             if (db == 0 || db == stable)
-                bs->on = db;
+                bs->physState = db & 1;
         }
     }
 }
@@ -1302,6 +1433,17 @@
     // presume we'll find no keyboard keys
     kbKeys = false;
     
+    // Configure the virtual buttons.  These are buttons controlled via
+    // software triggers rather than physical GPIO inputs.  The virtual
+    // buttons have the same control structures as regular buttons, but
+    // they get their configuration data from other config variables.
+    
+    // ZB Launch Ball button
+    cfg.button[ZBL_BUTTON].set(
+        PINNAME_TO_WIRE(NC),
+        cfg.plunger.zbLaunchBall.keytype,
+        cfg.plunger.zbLaunchBall.keycode);
+    
     // create the digital inputs
     ButtonState *bs = buttonState;
     for (int i = 0 ; i < MAX_BUTTONS ; ++i, ++bs)
@@ -1316,41 +1458,29 @@
             if (cfg.button[i].flags & BtnFlagPulse)
                 bs->pulseState = 1;
             
-            // note if it's a keyboard key of some kind (including media keys)
-            uint8_t val = cfg.button[i].val;
+            // Note if it's a keyboard key of some kind.  If we find any keyboard
+            // mappings, we'll declare a keyboard interface when we send our HID 
+            // configuration to the host during USB connection setup.
             switch (cfg.button[i].typ)
             {
-            case BtnTypeJoystick:
-                // joystick button - get the button bit mask
-                bs->js = 1 << val;
-                break;
-                
             case BtnTypeKey:
-                // regular keyboard key - note the scan code
-                bs->keycode = val;
+                // note that we have at least one keyboard key
                 kbKeys = true;
                 break;
                 
-            case BtnTypeModKey:
-                // keyboard mod key - note the modifier mask
-                bs->keymod = val;
-                kbKeys = true;
-                break;
-                
-            case BtnTypeMedia:
-                // media key - note the code
-                bs->mediakey = val;
-                kbKeys = true;
-                break;
-                
-            case BtnTypeSpecial:
-                // special key
-                bs->special = val;
+            default:
+                // not a keyboard key
                 break;
             }
         }
     }
     
+    // If the ZB Launch Ball feature is enabled, and it uses a keyboard
+    // key, this requires setting up a USB keyboard interface.
+    if (cfg.plunger.zbLaunchBall.port != 0 
+        && cfg.plunger.zbLaunchBall.keytype == BtnTypeKey)
+        kbKeys = true;
+    
     // start the button scan thread
     buttonTicker.attach_us(scanButtons, 1000);
 
@@ -1362,7 +1492,7 @@
 // media control descriptors with the current state of keys mapped to
 // those HID interfaces, and executes the local effects for any keys 
 // mapped to special device functions (e.g., Night Mode).
-void processButtons()
+void processButtons(Config &cfg)
 {
     // start with an empty list of USB key codes
     uint8_t modkeys = 0;
@@ -1376,33 +1506,36 @@
     uint8_t mediakeys = 0;
     
     // calculate the time since the last run
-    float dt = buttonTimer.read();
+    uint32_t dt = buttonTimer.read_us();
     buttonTimer.reset();
 
     // scan the button list
     ButtonState *bs = buttonState;
-    for (int i = 0 ; i < MAX_BUTTONS ; ++i, ++bs)
+    ButtonCfg *bc = cfg.button;
+    for (int i = 0 ; i < MAX_BUTTONS ; ++i, ++bs, ++bc)
     {
         // if it's a pulse-mode switch, get the virtual pressed state
         if (bs->pulseState != 0)
         {
-            // deduct the time to the next state change
-            bs->pulseTime -= dt;
-            if (bs->pulseTime < 0)
-                bs->pulseTime = 0;
-                
             // if the timer has expired, check for state changes
-            if (bs->pulseTime == 0)
+            if (bs->pulseTime > dt)
             {
-                const float pulseLength = 0.2;
+                // not expired yet - deduct the last interval
+                bs->pulseTime -= dt;
+            }
+            else
+            {
+                // pulse time expired - check for a state change
+                const uint32_t pulseLength = 200000UL;  // 200 milliseconds
                 switch (bs->pulseState)
                 {
                 case 1:
                     // off - if the physical switch is now on, start a button pulse
-                    if (bs->on) {
+                    if (bs->physState) 
+                    {
                         bs->pulseTime = pulseLength;
                         bs->pulseState = 2;
-                        bs->pressed = 1;
+                        bs->logState = 1;
                     }
                     break;
                     
@@ -1412,15 +1545,16 @@
                     // change in state in the logical button
                     bs->pulseState = 3;
                     bs->pulseTime = pulseLength;
-                    bs->pressed = 0;
+                    bs->logState = 0;
                     break;
                     
                 case 3:
                     // on - if the physical switch is now off, start a button pulse
-                    if (!bs->on) {
+                    if (!bs->physState) 
+                    {
                         bs->pulseTime = pulseLength;
                         bs->pulseState = 4;
-                        bs->pressed = 1;
+                        bs->logState = 1;
                     }
                     break;
                     
@@ -1428,7 +1562,7 @@
                     // transitioning on to off - end the pulse, and start a gap
                     bs->pulseState = 1;
                     bs->pulseTime = pulseLength;
-                    bs->pressed = 0;
+                    bs->logState = 0;
                     break;
                 }
             }
@@ -1436,44 +1570,102 @@
         else
         {
             // not a pulse switch - the logical state is the same as the physical state
-            bs->pressed = bs->on;
+            bs->logState = bs->physState;
         }
 
         // carry out any edge effects from buttons changing states
-        if (bs->pressed != bs->prev)
+        if (bs->logState != bs->prevLogState)
         {
             // check for special key transitions
-            switch (bs->special)
+            if (cfg.nightMode.btn == i + 1)
             {
-            case 1:
-                // night mode momentary switch - when the button transitions from
-                // OFF to ON, invert night mode
-                if (bs->pressed)
-                    toggleNightMode();
-                break;
-                
-            case 2:
-                // night mode toggle switch - when the button changes state, change
-                // night mode to match the new state
-                setNightMode(bs->pressed);
-                break;
+                // Check the switch type in the config flags.  If flag 0x01 is set,
+                // it's a persistent on/off switch, so the night mode state simply
+                // follows the current state of the switch.  Otherwise, it's a 
+                // momentary button, so each button push (i.e., each transition from
+                // logical state OFF to ON) toggles the current night mode state.
+                if (cfg.nightMode.flags & 0x01)
+                {
+                    // toggle switch - when the button changes state, change
+                    // night mode to match the new state
+                    setNightMode(bs->logState);
+                }
+                else
+                {
+                    // momentary switch - toggle the night mode state when the
+                    // physical button is pushed (i.e., when its logical state
+                    // transitions from OFF to ON)
+                    if (bs->logState)
+                        toggleNightMode();
+                }
             }
             
             // remember the new state for comparison on the next run
-            bs->prev = bs->pressed;
+            bs->prevLogState = bs->logState;
         }
 
-        // if it's pressed, add it to the appropriate key state list
-        if (bs->pressed)
+        // if it's pressed, physically or virtually, add it to the appropriate 
+        // key state list
+        if (bs->logState || bs->virtState)
         {
             // OR in the joystick button bit, mod key bits, and media key bits
-            newjs |= bs->js;
-            modkeys |= bs->keymod;
-            mediakeys |= bs->mediakey;
-            
-            // if it has a keyboard key, add the scan code to the active list
-            if (bs->keycode != 0 && nkeys < 7)
-                keys[nkeys++] = bs->keycode;
+            uint8_t val = bc->val;
+            switch (bc->typ)
+            {
+            case BtnTypeJoystick:
+                // joystick button
+                newjs |= (1 << (val - 1));
+                break;
+                
+            case BtnTypeKey:
+                // Keyboard key.  This could be a modifier key (shift, control,
+                // alt, GUI), a media key (mute, volume up, volume down), or a
+                // regular key.  Check which one.
+                if (val >= 0x7F && val <= 0x81)
+                {
+                    // It's a media key.  OR the key into the media key mask.
+                    // The media mask bits are mapped in the HID report descriptor
+                    // in USBJoystick.cpp.  For simplicity, we arrange the mask so
+                    // that the ones with regular keyboard equivalents that we catch
+                    // here are in the same order as the key scan codes:
+                    //
+                    //   Mute     = scan 0x7F = mask bit 0x01
+                    //   Vol Up   = scan 0x80 = mask bit 0x02
+                    //   Vol Down = scan 0x81 = mask bit 0x04
+                    //
+                    // So we can translate from scan code to bit mask with some
+                    // simple bit shifting:
+                    mediakeys |= (1 << (val - 0x7f));
+                }
+                else if (val >= 0xE0 && val <= 0xE7)
+                {
+                    // It's a modifier key.  Like the media keys, these are represented
+                    // in the USB reports with a bit mask, and like the media keys, we
+                    // arrange the mask bits in the same order as the scan codes.  This
+                    // makes figuring the mask a simple bit shift:
+                    modkeys |= (1 << (val - 0xE0));
+                }
+                else
+                {
+                    // It's a regular key.  Make sure it's not already in the list, and
+                    // that the list isn't full.  If neither of these apply, add the key.
+                    if (nkeys < 7)
+                    {
+                        bool found;
+                        for (int j = 0 ; j < nkeys ; ++j)
+                        {
+                            if (keys[j] == val)
+                            {
+                                found = true;
+                                break;
+                            }
+                        }
+                        if (!found)
+                            keys[nkeys++] = val;
+                    }
+                }
+                break;
+            }
         }
     }
 
@@ -2139,11 +2331,13 @@
 void startTVTimer(Config &cfg)
 {
     // only start the timer if the status sense circuit pins are configured
-    if (cfg.TVON.statusPin != NC && cfg.TVON.latchPin != NC && cfg.TVON.relayPin != NC)
+    if (cfg.TVON.statusPin != 0xFF 
+        && cfg.TVON.latchPin != 0xFF 
+        && cfg.TVON.relayPin != 0xFF)
     {
-        psu2_status_sense = new DigitalIn(cfg.TVON.statusPin);
-        psu2_status_set = new DigitalOut(cfg.TVON.latchPin);
-        tv_relay = new DigitalOut(cfg.TVON.relayPin);
+        psu2_status_sense = new DigitalIn(wirePinName(cfg.TVON.statusPin));
+        psu2_status_set = new DigitalOut(wirePinName(cfg.TVON.latchPin));
+        tv_relay = new DigitalOut(wirePinName(cfg.TVON.relayPin));
         tv_delay_time = cfg.TVON.delayTime/100.0;
     
         // Set up our time routine to run every 1/4 second.  
@@ -2245,10 +2439,12 @@
 static void setNightMode(bool on)
 {
     // set the new night mode flag in the noisy output class
-    LwNoisyOut::nightMode = on;
+    nightMode = on;
 
     // update the special output pin that shows the night mode state
-    specialPin[SPECIAL_PIN_NIGHTMODE]->set(on ? 255 : 0);
+    int port = int(cfg.nightMode.port) - 1;
+    if (port >= 0 && port < numOutputs)
+        lwPin[port]->set(nightMode ? 255 : 0);
 
     // update all outputs for the mode change
     updateAllOuts();
@@ -2257,7 +2453,7 @@
 // Toggle night mode
 static void toggleNightMode()
 {
-    setNightMode(!LwNoisyOut::nightMode);
+    setNightMode(!nightMode);
 }
 
 
@@ -2278,27 +2474,44 @@
     {
     case PlungerType_TSL1410RS:
         // pins are: SI, CLOCK, AO
-        plungerSensor = new PlungerSensorTSL1410R(cfg.plunger.sensorPin[0], cfg.plunger.sensorPin[1], cfg.plunger.sensorPin[2], NC);
+        plungerSensor = new PlungerSensorTSL1410R(
+            wirePinName(cfg.plunger.sensorPin[0]), 
+            wirePinName(cfg.plunger.sensorPin[1]),
+            wirePinName(cfg.plunger.sensorPin[2]),
+            NC);
         break;
         
     case PlungerType_TSL1410RP:
         // pins are: SI, CLOCK, AO1, AO2
-        plungerSensor = new PlungerSensorTSL1410R(cfg.plunger.sensorPin[0], cfg.plunger.sensorPin[1], cfg.plunger.sensorPin[2], cfg.plunger.sensorPin[3]);
+        plungerSensor = new PlungerSensorTSL1410R(
+            wirePinName(cfg.plunger.sensorPin[0]), 
+            wirePinName(cfg.plunger.sensorPin[1]),
+            wirePinName(cfg.plunger.sensorPin[2]), 
+            wirePinName(cfg.plunger.sensorPin[3]));
         break;
         
     case PlungerType_TSL1412RS:
         // pins are: SI, CLOCK, AO1, AO2
-        plungerSensor = new PlungerSensorTSL1412R(cfg.plunger.sensorPin[0], cfg.plunger.sensorPin[1], cfg.plunger.sensorPin[2], NC);
+        plungerSensor = new PlungerSensorTSL1412R(
+            wirePinName(cfg.plunger.sensorPin[0]),
+            wirePinName(cfg.plunger.sensorPin[1]), 
+            wirePinName(cfg.plunger.sensorPin[2]), 
+            NC);
         break;
     
     case PlungerType_TSL1412RP:
         // pins are: SI, CLOCK, AO1, AO2
-        plungerSensor = new PlungerSensorTSL1412R(cfg.plunger.sensorPin[0], cfg.plunger.sensorPin[1], cfg.plunger.sensorPin[2], cfg.plunger.sensorPin[3]);
+        plungerSensor = new PlungerSensorTSL1412R(
+            wirePinName(cfg.plunger.sensorPin[0]), 
+            wirePinName(cfg.plunger.sensorPin[1]), 
+            wirePinName(cfg.plunger.sensorPin[2]), 
+            wirePinName(cfg.plunger.sensorPin[3]));
         break;
     
     case PlungerType_Pot:
         // pins are: AO
-        plungerSensor = new PlungerSensorPot(cfg.plunger.sensorPin[0]);
+        plungerSensor = new PlungerSensorPot(
+            wirePinName(cfg.plunger.sensorPin[0]));
         break;
     
     case PlungerType_None:
@@ -2383,13 +2596,6 @@
         PlungerReading r;
         if (plungerSensor->read(r))
         {
-            // if in calibration mode, apply it to the calibration
-            if (plungerCalMode)
-            {
-                readForCal(r);
-                return;
-            }
-            
             // Pull the previous reading from the history
             const PlungerReading &prv = nthHist(0);
             
@@ -2403,14 +2609,68 @@
             // we're using a fast sensor like that.)
             if (uint32_t(r.t - prv.t) < 2000UL)
                 return;
-                
-            // bounds-check the calibration data
-            checkCalBounds(r.pos);
+
+            // check for calibration mode
+            if (plungerCalMode)
+            {
+                // Calibration mode.  Adjust the calibration bounds to fit
+                // the value.  If this value is beyond the current min or max,
+                // expand the envelope to include this new value.
+                if (r.pos > cfg.plunger.cal.max)
+                    cfg.plunger.cal.max = r.pos;
+                if (r.pos < cfg.plunger.cal.min)
+                    cfg.plunger.cal.min = r.pos;
 
-            // Apply the calibration and rescale to the joystick range.
-            r.pos = int(
-                (long(r.pos - cfg.plunger.cal.zero) * JOYMAX)
-                / (cfg.plunger.cal.max - cfg.plunger.cal.zero));
+                // If we're in calibration state 0, we're waiting for the
+                // plunger to come to rest at the park position so that we
+                // can take a sample of the park position.  Check to see if
+                // we've been at rest for a minimum interval.
+                if (calState == 0)
+                {
+                    if (abs(r.pos - calZeroStart.pos) < 65535/3/50)
+                    {
+                        // we're close enough - make sure we've been here long enough
+                        if (uint32_t(r.t - calZeroStart.t) > 100000UL)
+                        {
+                            // we've been at rest long enough - count it
+                            calZeroPosSum += r.pos;
+                            calZeroPosN += 1;
+                            
+                            // update the zero position from the new average
+                            cfg.plunger.cal.zero = uint16_t(calZeroPosSum / calZeroPosN);
+                            
+                            // switch to calibration state 1 - at rest
+                            calState = 1;
+                        }
+                    }
+                    else
+                    {
+                        // we're not close to the last position - start again here
+                        calZeroStart = r;
+                    }
+                }
+                
+                // Rescale to the joystick range, and adjust for the current
+                // park position, but don't calibrate.  We don't know the maximum
+                // point yet, so we can't calibrate the range.
+                r.pos = int(
+                    (long(r.pos - cfg.plunger.cal.zero) * JOYMAX)
+                    / (65535 - cfg.plunger.cal.zero));
+            }
+            else
+            {
+                // Not in calibration mode.  Apply the existing calibration and 
+                // rescale to the joystick range.
+                r.pos = int(
+                    (long(r.pos - cfg.plunger.cal.zero) * JOYMAX)
+                    / (cfg.plunger.cal.max - cfg.plunger.cal.zero));
+                    
+                // limit the result to the valid joystick range
+                if (r.pos > JOYMAX)
+                    r.pos = JOYMAX;
+                else if (r.pos < -JOYMAX)
+                    r.pos = -JOYMAX;
+            }
 
             // Calculate the velocity from the second-to-last reading
             // to here, in joystick distance units per microsecond.
@@ -2456,9 +2716,14 @@
                 // be strong enough to require the synthetic firing treatment.
                 if (v < 0 && r.pos > JOYMAX/6)
                 {
-                    // enter phase 1
+                    // enter firing phase 1
                     firingMode(1);
                     
+                    // if in calibration state 1 (at rest), switch to state 2 (not 
+                    // at rest)
+                    if (calState == 1)
+                        calState = 2;
+                    
                     // we don't have a freeze position yet, but note the start time
                     f1.pos = 0;
                     f1.t = r.t;
@@ -2470,7 +2735,7 @@
                     // linearly proportional to the starting retraction distance.  
                     // The barrel spring is about 1/6 the length of the main spring, 
                     // so figure it compresses by 1/6 the distance.  (This is overly
-                    // simplistic and inaccurate, but it seems to give perfectly good
+                    // simplistic and not very accurate, but it seems to give good 
                     // visual results, and that's all it's for.)
                     f2.pos = -r.pos/6;
                 }
@@ -2488,6 +2753,25 @@
                     
                     // note the start time for the firing phase
                     f2.t = r.t;
+                    
+                    // if in calibration mode, and we're in state 2 (moving), 
+                    // collect firing statistics for calibration purposes
+                    if (plungerCalMode && calState == 2)
+                    {
+                        // collect a new zero point for the average when we 
+                        // come to rest
+                        calState = 0;
+                        
+                        // collect average firing time statistics in millseconds, if 
+                        // it's in range (20 to 255 ms)
+                        int dt = uint32_t(r.t - f1.t)/1000UL;
+                        if (dt >= 20 && dt <= 255)
+                        {
+                            calRlsTimeSum += dt;
+                            calRlsTimeN += 1;
+                            cfg.plunger.cal.tRelease = uint8_t(calRlsTimeSum / calRlsTimeN);
+                        }
+                    }
                 }
                 else if (v < vprv2)
                 {
@@ -2519,6 +2803,7 @@
                 {
                     // We're not accelerating.  Cancel the firing event.
                     firingMode(0);
+                    calState = 1;
                 }
                 break;
                 
@@ -2578,11 +2863,14 @@
                     f3r.t = r.t;
                 }
 
-                // check if we've come to rest, or close enough
-                if (abs(r.pos - f3s.pos) < 200)
+                // Check if we're close to the last starting point.  The joystick
+                // positive axis range (0..4096) covers the retraction distance of 
+                // about 2.5", so 1" is about 1638 joystick units, hence 1/16" is
+                // about 100 units.
+                if (abs(r.pos - f3s.pos) < 100)
                 {
-                    // It's within an eighth inch of the last starting point. 
-                    // If it's been here for 30ms, consider it stable.
+                    // It's at roughly the same position as the starting point.
+                    // Consider it stable if this has been true for 300ms.
                     if (uint32_t(r.t - f3s.t) > 30000UL)
                     {
                         // we're done with the firing event
@@ -2649,7 +2937,7 @@
                 cfg.plunger.cal.zero = r.pos;
                 
                 // use it as the starting point for the settling watch
-                f1 = r;
+                calZeroStart = r;
             }
             else
             {
@@ -2657,8 +2945,21 @@
                 cfg.plunger.cal.zero = 0xffff/6;
                 
                 // we don't have a starting point for the setting watch
-                f1.pos = -65535;
-                f1.t = 0;
+                calZeroStart.pos = -65535;
+                calZeroStart.t = 0;
+            }
+        }
+        else if (!f && plungerCalMode)
+        {
+            // Leaving calibration mode.  Make sure the max is past the
+            // zero point - if it's not, we'd have a zero or negative
+            // denominator for the scaling calculation, which would be
+            // physically meaningless.
+            if (cfg.plunger.cal.max <= cfg.plunger.cal.zero)
+            {
+                // bad settings - reset to defaults
+                cfg.plunger.cal.max = 0xffff;
+                cfg.plunger.cal.zero = 0xffff/6;
             }
         }
             
@@ -2667,127 +2968,9 @@
     }
     
     // is a firing event in progress?
-    bool isFiring() { return firing > 3; }
+    bool isFiring() { return firing == 3; }
 
 private:
-    // Read the sensor in calibration mode
-    void readForCal(PlungerReading r)
-    {
-        // if it's outside of the current calibration bounds,
-        // expand the bounds
-        if (r.pos < cfg.plunger.cal.min)
-            cfg.plunger.cal.min = r.pos;
-        if (r.pos > cfg.plunger.cal.max)
-            cfg.plunger.cal.max = r.pos;
-                    
-        // While we're in calibration mode, report the raw sensor
-        // position as the joystick value, adjusted to the JOYMAX scale.
-        z = int16_t((long(r.pos) * JOYMAX)/65535);
-        
-        // for the release monitoring, take readings at least 2ms apart
-        if (uint32_t(r.t - f2.t) < 2000UL)
-            return;
-        
-        // Check our state
-        switch (calState)
-        {
-        case 0:
-            // We're waiting for the position to settle.  Check to see if
-            // we've been at the recent settling position long enough.
-            // Consider 1/50" (about 0.5mm) close enough to count as stable, 
-            // to allow for some slight sensor noise from reading to reading.
-            if (abs(r.pos - f1.pos) > 65535/3/50)
-            {
-                // too far away - set the new starting point
-                f1 = r;
-            }
-            else if (uint32_t(r.t - f1.t) > 100000)
-            {
-                // We've been stationary long enough to count as settled.
-                // Wwitch to "at rest" state.
-                calState = 1;
-                
-                // collect the new zero point for our average
-                calZeroPosSum += r.pos;
-                calZeroPosN += 1;
-                
-                // use the new average as the zero point
-                cfg.plunger.cal.zero = uint16_t(calZeroPosSum / calZeroPosN);
-                
-                // remember the current position in f1 to detect when we start
-                // moving again
-                f1 = r;
-            }
-            break;
-            
-        case 1:
-            // At rest.  We remain in this state until we see the plunger
-            // retract more than about 1/2".
-            if (r.pos - f1.pos > 65535/6)
-            {
-                // switch to state 2 - retracting
-                calState = 2;
-                
-                // use f1 as the max so far
-                f1 = r;
-            }
-            break;
-            
-        case 2:
-            // Away from rest position.  Note the maximum point so far in f1,
-            // and monitor for release motions.
-            if (r.pos >= f1.pos)
-            {
-                // moving back - note the new max point on this run
-                f1 = r;
-            }
-            else
-            {
-                // moving forward - switch to possible release mode
-                calState = 3;
-            }
-            break;
-            
-        case 3:
-            // Possible release.  We have to move forward on each new
-            // reading, relative to two readings ago, to stay in release 
-            // mode.
-            if (r.pos >= f3r.pos)
-            {
-                // not moving forward - switch back to retract mode
-                calState = 2;
-                f1 = r;
-            }
-            else if (r.pos <= cfg.plunger.cal.zero)
-            {
-                // Crossed the zero point.  Figure the release time.  If
-                // it's within a reasonable range, add it to the average.
-                // We'll ignore outliers on the assumption that they
-                // don't reflect actual release motions.  
-                int dt = uint32_t(r.t - f1.t)/1000;
-                if (dt < 250 && dt > 25)
-                {
-                    // count it in the average
-                    calRlsTimeSum += dt;
-                    calRlsTimeN += 1;
-                    
-                    // store the new average in the configuration
-                    cfg.plunger.cal.tRelease = uint8_t(calRlsTimeSum / calRlsTimeN);
-                    cfg.plunger.cal.tRelease = dt; // $$$
-                }
-                
-                // release done - switch to "waiting to settle" mode
-                calState = 0;
-                f1 = r;
-            }
-            break;
-        }
-        
-        // f2 is always the immediately previous reading in cal mode, 
-        // and f3r is the one before that
-        f3r = f2;
-        f2 = r;
-    }
 
     // Calibration state.  During calibration mode, we watch for release
     // events, to measure the time it takes to complete the release
@@ -2807,6 +2990,7 @@
     // itself doesn't come to rest at exactly the same spot every time, 
     // largely due to friction in the mechanism.  To calculate the average,
     // we keep a sum of the readings and a count of samples.
+    PlungerReading calZeroStart;
     long calZeroPosSum;
     int calZeroPosN;
     
@@ -2860,53 +3044,6 @@
         return hi;
     }
 
-    // Adjust the calibration bounds for a new reading.  This is used
-    // while NOT in calibration mode to ensure that a reading doesn't
-    // violate the calibration limits.  If it does, we'll readjust the
-    // limits to incorporate the new value.
-    void checkCalBounds(int pos)
-    {
-        // If the value is beyond the calibration maximum, increase the
-        // calibration point.  This ensures that our joystick reading
-        // is always within the valid joystick field range.
-        if (pos > cfg.plunger.cal.max)
-            cfg.plunger.cal.max = pos;
-            
-        // make sure we don't overflow in the opposite direction
-        if (pos < cfg.plunger.cal.zero
-            && cfg.plunger.cal.zero - pos > cfg.plunger.cal.max)
-        {
-            // we need to raise 'max' by this much to keep things in range
-            int adj = cfg.plunger.cal.zero - pos - cfg.plunger.cal.max;
-            
-            // we can raise 'max' at most this much before overflowing
-            int lim = 0xffff - cfg.plunger.cal.max;
-            
-            // if we have headroom to raise 'max' by 'adj', do so, otherwise
-            // raise it as much as we can and apply the excess to lowering the
-            // zero point
-            if (adj > lim)
-            {
-                cfg.plunger.cal.zero -= adj - lim;
-                adj = lim;
-            }
-            cfg.plunger.cal.max += adj;
-        }
-            
-        // If the calibration max isn't higher than the calibration
-        // zero, we have a negative or zero scale range, which isn't
-        // physically meaningful.  Fix it by forcing the max above
-        // the zero point (or the zero point below the max, if they're
-        // both pegged at the datatype maximum).
-        if (cfg.plunger.cal.max <= cfg.plunger.cal.zero)
-        {
-            if (cfg.plunger.cal.zero != 0xFFFF)
-                cfg.plunger.cal.max = cfg.plunger.cal.zero + 1;
-            else
-                cfg.plunger.cal.zero -= 1;
-        }
-    }
-    
     // velocity at previous reading, and the one before that
     float vprv, vprv2;
     
@@ -2939,16 +3076,12 @@
     //   0 - Default state.  We report the real instantaneous plunger 
     //       position to the joystick interface.
     //
-    //   1 - Possible release in progress.  We enter this state when
-    //       we see the plunger start to move forward, and stay in this
-    //       state as long as we see *accelerating* forward motion.
+    //   1 - Moving forward
     //
-    //   2 - Firing event started.  We report the "bounce" position for
-    //       a minimum time.
+    //   2 - Accelerating
     //
-    //   3 - Firing event hold.  We report the rest position for a
-    //       minimum interval, or until the real plunger comes to rest
-    //       somewhere, whichever comes first.
+    //   3 - Firing.  We report the rest position for a minimum interval,
+    //       or until the real plunger comes to rest somewhere.
     //
     int firing;
     
@@ -3026,12 +3159,7 @@
     {
         // start in the default state
         lbState = 0;
-        
-        // get the button bit for the ZB Launch Ball button
-        lbButtonBit = (1 << (cfg.plunger.zbLaunchBall.btn - 1));
-        
-        // start the state transition timer
-        lbTimer.start();
+        btnState = false;
     }
 
     // Update state.  This checks the current plunger position and
@@ -3040,152 +3168,96 @@
     // Updates the simulated button vector according to the current
     // launch ball state.  The main loop calls this before each 
     // joystick update to figure the new simulated button state.
-    void update(uint32_t &simButtons)
+    void update()
     {
-        // Check for a simulated Launch Ball button press, if enabled
-        if (cfg.plunger.zbLaunchBall.port != 0)
+        // If the ZB Launch Ball led wiz output is ON, check for a 
+        // plunger firing event
+        if (zbLaunchOn)
         {                
+            // note the new position
             int znew = plungerReader.getPosition();
-            const int cockThreshold = JOYMAX/3;
+            
+            // figure the push threshold from the configuration data
             const int pushThreshold = int(-JOYMAX/3.0 * cfg.plunger.zbLaunchBall.pushDistance/1000.0);
-            int newState = lbState;
+
+            // check the state
             switch (lbState)
             {
             case 0:
-                // Base state.  If the plunger is pulled back by an inch
-                // or more, go to "cocked" state.  If the plunger is pushed
-                // forward by 1/4" or more, go to "pressed" state.
-                if (znew >= cockThreshold)
-                    newState = 1;
+                // Default state.  If a launch event has been detected on
+                // the plunger, activate a timed pulse and switch to state 1.
+                // If the plunger is pushed forward of the threshold, push
+                // the button.
+                if (plungerReader.isFiring())
+                {
+                    // firing event - start a timed Launch button pulse
+                    lbTimer.reset();
+                    lbTimer.start();
+                    setButton(true);
+                    
+                    // switch to state 1
+                    lbState = 1;
+                }
                 else if (znew <= pushThreshold)
-                    newState = 5;
+                {
+                    // pushed forward without a firing event - hold the
+                    // button as long as we're pushed forward
+                    setButton(true);
+                }
+                else
+                {
+                    // not pushed forward - turn off the Launch button
+                    setButton(false);
+                }
                 break;
                 
             case 1:
-                // Cocked state.  If a firing event is now in progress,
-                // go to "launch" state.  Otherwise, if the plunger is less
-                // than 1" retracted, go to "uncocked" state - the player
-                // might be slowly returning the plunger to rest so as not
-                // to trigger a launch.
-                if (plungerReader.isFiring() || znew <= 0)
-                    newState = 3;
-                else if (znew < cockThreshold)
-                    newState = 2;
+                // State 1: Timed Launch button pulse in progress after a
+                // firing event.  Wait for the timer to expire.
+                if (lbTimer.read_us() > 200000UL)
+                {
+                    // timer expired - turn off the button
+                    setButton(false);
+                    
+                    // switch to state 2
+                    lbState = 2;
+                }
                 break;
                 
             case 2:
-                // Uncocked state.  If the plunger is more than an inch
-                // retracted, return to cocked state.  If we've been in
-                // the uncocked state for more than half a second, return
-                // to the base state.  This allows the user to return the
-                // plunger to rest without triggering a launch, by moving
-                // it at manual speed to the rest position rather than
-                // releasing it.
-                if (znew >= cockThreshold)
-                    newState = 1;
-                else if (lbTimer.read_us() > 500000)
-                    newState = 0;
-                break;
-                
-            case 3:
-                // Launch state.  If the plunger is no longer pushed
-                // forward, switch to launch rest state.
-                if (znew >= 0)
-                    newState = 4;
-                break;    
-                
-            case 4:
-                // Launch rest state.  If the plunger is pushed forward
-                // again, switch back to launch state.  If not, and we've
-                // been in this state for at least 200ms, return to the
-                // default state.
-                if (znew <= pushThreshold)
-                    newState = 3;
-                else if (lbTimer.read_us() > 200000)
-                    newState = 0;                    
-                break;
-                
-            case 5:
-                // Press-and-Hold state.  If the plunger is no longer pushed
-                // forward, AND it's been at least 50ms since we generated
-                // the simulated Launch Ball button press, return to the base 
-                // state.  The minimum time is to ensure that VP has a chance
-                // to see the button press and to avoid transient key bounce
-                // effects when the plunger position is right on the threshold.
-                if (znew > pushThreshold && lbTimer.read_us() > 50000)
-                    newState = 0;
+                // State 2: Timed Launch button pulse done.  Wait for the
+                // plunger launch event to end.
+                if (!plungerReader.isFiring())
+                {
+                    // firing event done - return to default state
+                    lbState = 0;
+                }
                 break;
             }
-            
-            // change states if desired
-            if (newState != lbState)
-            {
-                // If we're entering Launch state OR we're entering the
-                // Press-and-Hold state, AND the ZB Launch Ball LedWiz signal 
-                // is turned on, simulate a Launch Ball button press.
-                if (((newState == 3 && lbState != 4) || newState == 5)
-                    && wizOn[cfg.plunger.zbLaunchBall.port-1])
-                {
-                    lbBtnTimer.reset();
-                    lbBtnTimer.start();
-                    simButtons |= lbButtonBit;
-                }
-                
-                // if we're switching to state 0, release the button
-                if (newState == 0)
-                    simButtons &= ~(1 << (cfg.plunger.zbLaunchBall.btn - 1));
-                
-                // switch to the new state
-                lbState = newState;
+        }
+        else
+        {
+            // ZB Launch Ball disabled - turn off the button if it was on
+            setButton(false);
                 
-                // start timing in the new state
-                lbTimer.reset();
-            }
-            
-            // If the Launch Ball button press is in effect, but the
-            // ZB Launch Ball LedWiz signal is no longer turned on, turn
-            // off the button.
-            //
-            // If we're in one of the Launch states (state #3 or #4),
-            // and the button has been on for long enough, turn it off.
-            // The Launch mode is triggered by a pull-and-release gesture.
-            // From the user's perspective, this is just a single gesture
-            // that should trigger just one momentary press on the Launch
-            // Ball button.  Physically, though, the plunger usually
-            // bounces back and forth for 500ms or so before coming to
-            // rest after this gesture.  That's what the whole state
-            // #3-#4 business is all about - we stay in this pair of
-            // states until the plunger comes to rest.  As long as we're
-            // in these states, we won't send duplicate button presses.
-            // But we also don't want the one button press to continue 
-            // the whole time, so we'll time it out now.
-            //
-            // (This could be written as one big 'if' condition, but
-            // I'm breaking it out verbosely like this to make it easier
-            // for human readers such as myself to comprehend the logic.)
-            if ((simButtons & lbButtonBit) != 0)
-            {
-                int turnOff = false;
-                
-                // turn it off if the ZB Launch Ball signal is off
-                if (!wizOn[cfg.plunger.zbLaunchBall.port-1])
-                    turnOff = true;
-                    
-                // also turn it off if we're in state 3 or 4 ("Launch"),
-                // and the button has been on long enough
-                if ((lbState == 3 || lbState == 4) && lbBtnTimer.read_us() > 250000)
-                    turnOff = true;
-                    
-                // if we decided to turn off the button, do so
-                if (turnOff)
-                {
-                    lbBtnTimer.stop();
-                    simButtons &= ~lbButtonBit;
-                }
-            }
+            // return to the default state
+            lbState = 0;
         }
     }
-  
+    
+    // Set the button state
+    void setButton(bool on)
+    {
+        if (btnState != on)
+        {
+            // remember the new state
+            btnState = on;
+            
+            // update the virtual button state
+            buttonState[ZBL_BUTTON].virtPress(on);
+        }
+    }
+    
 private:
     // Simulated Launch Ball button state.  If a "ZB Launch Ball" port is
     // defined for our LedWiz port mapping, any time that port is turned ON,
@@ -3196,16 +3268,13 @@
     //
     // States:
     //   0 = default
-    //   1 = cocked (plunger has been pulled back about 1" from state 0)
-    //   2 = uncocked (plunger is pulled back less than 1" from state 1)
-    //   3 = launching, plunger is forward beyond park position
-    //   4 = launching, plunger is behind park position
-    //   5 = pressed and holding (plunger has been pressed forward beyond 
-    //       the park position from state 0)
-    int lbState;
+    //   1 = firing (firing event has activated a Launch button pulse)
+    //   2 = firing done (Launch button pulse ended, waiting for plunger
+    //       firing event to end)
+    uint8_t lbState;
     
-    // button bit for ZB launch ball button
-    uint32_t lbButtonBit;
+    // button state
+    bool btnState;
     
     // Time since last lbState transition.  Some of the states are time-
     // sensitive.  In the "uncocked" state, we'll return to state 0 if
@@ -3215,11 +3284,6 @@
     // the Launch Ball button after a moment, and we need to wait for 
     // the plunger to come to rest before returning to state 0.
     Timer lbTimer;
-    
-    // Launch Ball simulated push timer.  We start this when we simulate
-    // the button push, and turn off the simulated button when enough time
-    // has elapsed.
-    Timer lbBtnTimer;
 };
 
 // ---------------------------------------------------------------------------
@@ -3286,8 +3350,8 @@
 // Pixel dump mode - the host requested a dump of image sensor pixels
 // (helpful for installing and setting up the sensor and light source)
 bool reportPlungerStat = false;
-uint8_t reportStatFlags;    // pixel report flag bits (see ccdSensor.h)
-uint8_t reportStatVisMode;  // pixel report visualization mode (not currently used)
+uint8_t reportPlungerStatFlags; // plunger pixel report flag bits (see ccdSensor.h)
+uint8_t reportPlungerStatTime;  // extra exposure time for plunger pixel report
 
 
 // ---------------------------------------------------------------------------
@@ -3313,9 +3377,10 @@
 
 // Handle SET messages - write configuration variables from USB message data
 #define if_msg_valid(test)  if (test)
-#define v_byte(var, ofs)   cfg.var = data[ofs]
-#define v_ui16(var, ofs)   cfg.var = wireUI16(data+ofs)
-#define v_pin(var, ofs)    cfg.var = wirePinName(data[ofs])
+#define v_byte(var, ofs)    cfg.var = data[ofs]
+#define v_ui16(var, ofs)    cfg.var = wireUI16(data+(ofs))
+#define v_pin(var, ofs)     cfg.var = wirePinName(data[ofs])
+#define v_byte_ro(val, ofs) // ignore read-only variables on SET
 #define v_func configVarSet
 #include "cfgVarMsgMap.h"
 
@@ -3324,13 +3389,15 @@
 #undef v_byte
 #undef v_ui16
 #undef v_pin
+#undef v_byte_ro
 #undef v_func
 
 // Handle GET messages - read variable values and return in USB message daa
 #define if_msg_valid(test)
-#define v_byte(var, ofs)   data[ofs] = cfg.var
-#define v_ui16(var, ofs)   ui16Wire(data+ofs, cfg.var)
-#define v_pin(var, ofs)    pinNameWire(data+ofs, cfg.var)
+#define v_byte(var, ofs)    data[ofs] = cfg.var
+#define v_ui16(var, ofs)    ui16Wire(data+(ofs), cfg.var)
+#define v_pin(var, ofs)     pinNameWire(data+(ofs), cfg.var)
+#define v_byte_ro(val, ofs) data[ofs] = val
 #define v_func  configVarGet
 #include "cfgVarMsgMap.h"
 
@@ -3465,10 +3532,10 @@
         case 3:
             // 3 = plunger sensor status report
             //     data[2] = flag bits
-            //     data[3] = visualization mode
+            //     data[3] = extra exposure time, 100us (.1ms) increments
             reportPlungerStat = true;
-            reportStatFlags = data[2];
-            reportStatVisMode = data[3];
+            reportPlungerStatFlags = data[2];
+            reportPlungerStatTime = data[3];
             
             // show purple until we finish sending the report
             diagLED(1, 0, 1);
@@ -3493,17 +3560,16 @@
             // 6 = Save configuration to flash.
             saveConfigToFlash();
             
-            // Reboot the microcontroller.  Nearly all config changes
-            // require a reset, and a reset only takes a few seconds, 
-            // so we don't bother tracking whether or not a reboot is
-            // really needed.
-            reboot(js);
+            // before disconnecting, pause for the delay time specified in
+            // the parameter byte (in seconds)
+            rebootTime_us = data[2] * 1000000L;
+            rebootTimer.start();
             break;
             
         case 7:
             // 7 = Device ID report
-            // (No parameters)
-            js.reportID();
+            //     data[2] = ID index: 1=CPU ID, 2=OpenSDA TUID
+            js.reportID(data[2]);
             break;
             
         case 8:
@@ -3517,10 +3583,12 @@
             //     data[2] = config var ID
             //     data[3] = array index (for array vars: button assignments, output ports)
             {
-                // set up the reply buffer with the variable ID data
+                // set up the reply buffer with the variable ID data, and zero out
+                // the rest of the buffer
                 uint8_t reply[8];
                 reply[1] = data[2];
                 reply[2] = data[3];
+                memset(reply+3, 0, sizeof(reply)-3);
                 
                 // query the value
                 configVarGet(reply);
@@ -3529,6 +3597,11 @@
                 js.reportConfigVar(reply + 1);
             }
             break;
+            
+        case 10:
+            // 10 = Build ID query.
+            js.reportBuildInfo(getBuildID());
+            break;
         }
     }
     else if (data[0] == 66)
@@ -3721,8 +3794,10 @@
     statusFlags = (cfg.plunger.enabled ? 0x01 : 0x00);
 
     // initialize the calibration buttons, if present
-    DigitalIn *calBtn = (cfg.plunger.cal.btn == NC ? 0 : new DigitalIn(cfg.plunger.cal.btn));
-    DigitalOut *calBtnLed = (cfg.plunger.cal.led == NC ? 0 : new DigitalOut(cfg.plunger.cal.led));
+    DigitalIn *calBtn = (cfg.plunger.cal.btn == 0xFF ? 0 : 
+        new DigitalIn(wirePinName(cfg.plunger.cal.btn)));
+    DigitalOut *calBtnLed = (cfg.plunger.cal.led == 0xFF ? 0 :
+        new DigitalOut(wirePinName(cfg.plunger.cal.led)));
 
     // initialize the calibration button 
     calBtnTimer.start();
@@ -3745,14 +3820,6 @@
     // acceleration via the joystick x & y axes, per the VP convention)
     int x = 0, y = 0;
     
-    // 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;
-    
     // initialize the plunger sensor
     plungerSensor->init();
     
@@ -3881,12 +3948,12 @@
         // read the plunger sensor
         plungerReader.read();
         
-        // process button updates
-        processButtons();
+        // update the ZB Launch Ball status
+        zbLaunchBall.update();
         
-        // handle the ZB Launch Ball feature
-        zbLaunchBall.update(simButtons);
-
+        // process button updates
+        processButtons(cfg);
+        
         // send a keyboard report if we have new data
         if (kbState.changed)
         {
@@ -3934,10 +4001,7 @@
             // a traditional plunger, so we don't want to confuse VP with
             // regular plunger inputs.
             int z = plungerReader.getPosition();
-            int zrep = (!cfg.plunger.enabled ? 0 :
-                        cfg.plunger.zbLaunchBall.port != 0 
-                          && wizOn[cfg.plunger.zbLaunchBall.port-1] ? 0 :
-                        z);
+            int zrep = (!cfg.plunger.enabled || zbLaunchOn ? 0 : z);
             
             // rotate X and Y according to the device orientation in the cabinet
             accelRotate(x, y);
@@ -3949,7 +4013,7 @@
 #endif
 
             // send the joystick report
-            jsOK = js.update(x, y, zrep, jsButtons | simButtons, statusFlags);
+            jsOK = js.update(x, y, zrep, jsButtons, statusFlags);
             
             // we've just started a new report interval, so reset the timer
             jsReportTimer.reset();
@@ -3959,7 +4023,7 @@
         if (reportPlungerStat)
         {
             // send the report            
-            plungerSensor->sendStatusReport(js, reportStatFlags, reportStatVisMode);
+            plungerSensor->sendStatusReport(js, reportPlungerStatFlags, reportPlungerStatTime);
 
             // we have satisfied this request
             reportPlungerStat = false;
@@ -4053,6 +4117,10 @@
             }
         }
         
+        // if we have a reboot timer pending, check for completion
+        if (rebootTimer.isRunning() && rebootTimer.read_us() > rebootTime_us)
+            reboot(js);
+        
         // if we're disconnected, initiate a new connection
         if (!connected)
         {