An I/O controller for virtual pinball machines: accelerometer nudge sensing, analog plunger input, button input encoding, LedWiz compatible output controls, and more.

Dependencies:   mbed FastIO FastPWM USBDevice

Fork of Pinscape_Controller by Mike R

/media/uploads/mjr/pinscape_no_background_small_L7Miwr6.jpg

This is Version 2 of the Pinscape Controller, an I/O controller for virtual pinball machines. (You can find the old version 1 software here.) Pinscape is software for the KL25Z that turns the board into a full-featured I/O controller for virtual pinball, with support for accelerometer-based nudging, a mechanical plunger, button inputs, and feedback device control.

In case you haven't heard of the idea before, a "virtual pinball machine" is basically a video pinball simulator that's built into a real pinball machine body. A TV monitor goes in place of the pinball playfield, and a second TV goes in the backbox to show the backglass artwork. Some cabs also include a third monitor to simulate the DMD (Dot Matrix Display) used for scoring on 1990s machines, or even an original plasma DMD. A computer (usually a Windows PC) is hidden inside the cabinet, running pinball emulation software that displays a life-sized playfield on the main TV. The cabinet has all of the usual buttons, too, so it not only looks like the real thing, but plays like it too. That's a picture of my own machine to the right. On the outside, it's built exactly like a real arcade pinball machine, with the same overall dimensions and all of the standard pinball cabinet trim hardware.

It's possible to buy a pre-built virtual pinball machine, but it also makes a great DIY project. If you have some basic wood-working skills and know your way around PCs, you can build one from scratch. The computer part is just an ordinary Windows PC, and all of the pinball emulation can be built out of free, open-source software. In that spirit, the Pinscape Controller is an open-source software/hardware project that offers a no-compromises, all-in-one control center for all of the unique input/output needs of a virtual pinball cabinet. If you've been thinking about building one of these, but you're not sure how to connect a plunger, flipper buttons, lights, nudge sensor, and whatever else you can think of, this project might be just what you're looking for.

You can find much more information about DIY Pin Cab building in general in the Virtual Cabinet Forum on vpforums.org. Also visit my Pinscape Resources page for more about this project and other virtual pinball projects I'm working on.

Downloads

  • Pinscape Release Builds: This page has download links for all of the Pinscape software. To get started, install and run the Pinscape Config Tool on your Windows computer. It will lead you through the steps for installing the Pinscape firmware on the KL25Z.
  • Config Tool Source Code. The complete C# source code for the config tool. You don't need this to run the tool, but it's available if you want to customize anything or see how it works inside.

Documentation

The new Version 2 Build Guide is now complete! This new version aims to be a complete guide to building a virtual pinball machine, including not only the Pinscape elements but all of the basics, from sourcing parts to building all of the hardware.

You can also refer to the original Hardware Build Guide (PDF), but that's out of date now, since it refers to the old version 1 software, which was rather different (especially when it comes to configuration).

System Requirements

The new Config Tool requires a fairly up-to-date Microsoft .NET installation. If you use Windows Update to keep your system current, you should be fine. A modern version of Internet Explorer (IE) is required, even if you don't use it as your main browser, because the Config Tool uses some system components that Microsoft packages into the IE install set. I test with IE11, so that's known to work. IE8 doesn't work. IE9 and 10 are unknown at this point.

The Windows requirements are only for the config tool. The firmware doesn't care about anything on the Windows side, so if you can make do without the config tool, you can use almost any Windows setup.

Main Features

Plunger: The Pinscape Controller started out as a "mechanical plunger" controller: a device for attaching a real pinball plunger to the video game software so that you could launch the ball the natural way. This is still, of course, a central feature of the project. The software supports several types of sensors: a high-resolution optical sensor (which works by essentially taking pictures of the plunger as it moves); a slide potentiometer (which determines the position via the changing electrical resistance in the pot); a quadrature sensor (which counts bars printed on a special guide rail that it moves along); and an IR distance sensor (which determines the position by sending pulses of light at the plunger and measuring the round-trip travel time). The Build Guide explains how to set up each type of sensor.

Nudging: The KL25Z (the little microcontroller that the software runs on) has a built-in accelerometer. The Pinscape software uses it to sense when you nudge the cabinet, and feeds the acceleration data to the pinball software on the PC. This turns physical nudges into virtual English on the ball. The accelerometer is quite sensitive and accurate, so we can measure the difference between little bumps and hard shoves, and everything in between. The result is natural and immersive.

Buttons: You can wire real pinball buttons to the KL25Z, and the software will translate the buttons into PC input. You have the option to map each button to a keyboard key or joystick button. You can wire up your flipper buttons, Magna Save buttons, Start button, coin slots, operator buttons, and whatever else you need.

Feedback devices: You can also attach "feedback devices" to the KL25Z. Feedback devices are things that create tactile, sound, and lighting effects in sync with the game action. The most popular PC pinball emulators know how to address a wide variety of these devices, and know how to match them to on-screen action in each virtual table. You just need an I/O controller that translates commands from the PC into electrical signals that turn the devices on and off. The Pinscape Controller can do that for you.

Expansion Boards

There are two main ways to run the Pinscape Controller: standalone, or using the "expansion boards".

In the basic standalone setup, you just need the KL25Z, plus whatever buttons, sensors, and feedback devices you want to attach to it. This mode lets you take advantage of everything the software can do, but for some features, you'll have to build some ad hoc external circuitry to interface external devices with the KL25Z. The Build Guide has detailed plans for exactly what you need to build.

The other option is the Pinscape Expansion Boards. The expansion boards are a companion project, which is also totally free and open-source, that provides Printed Circuit Board (PCB) layouts that are designed specifically to work with the Pinscape software. The PCB designs are in the widely used EAGLE format, which many PCB manufacturers can turn directly into physical boards for you. The expansion boards organize all of the external connections more neatly than on the standalone KL25Z, and they add all of the interface circuitry needed for all of the advanced software functions. The big thing they bring to the table is lots of high-power outputs. The boards provide a modular system that lets you add boards to add more outputs. If you opt for the basic core setup, you'll have enough outputs for all of the toys in a really well-equipped cabinet. If your ambitions go beyond merely well-equipped and run to the ridiculously extravagant, just add an extra board or two. The modular design also means that you can add to the system over time.

Expansion Board project page

Update notes

If you have a Pinscape V1 setup already installed, you should be able to switch to the new version pretty seamlessly. There are just a couple of things to be aware of.

First, the "configuration" procedure is completely different in the new version. Way better and way easier, but it's not what you're used to from V1. In V1, you had to edit the project source code and compile your own custom version of the program. No more! With V2, you simply install the standard, pre-compiled .bin file, and select options using the Pinscape Config Tool on Windows.

Second, if you're using the TSL1410R optical sensor for your plunger, there's a chance you'll need to boost your light source's brightness a little bit. The "shutter speed" is faster in this version, which means that it doesn't spend as much time collecting light per frame as before. The software actually does "auto exposure" adaptation on every frame, so the increased shutter speed really shouldn't bother it, but it does require a certain minimum level of contrast, which requires a certain minimal level of lighting. Check the plunger viewer in the setup tool if you have any problems; if the image looks totally dark, try increasing the light level to see if that helps.

New Features

V2 has numerous new features. Here are some of the highlights...

Dynamic configuration: as explained above, configuration is now handled through the Config Tool on Windows. It's no longer necessary to edit the source code or compile your own modified binary.

Improved plunger sensing: the software now reads the TSL1410R optical sensor about 15x faster than it did before. This allows reading the sensor at full resolution (400dpi), about 400 times per second. The faster frame rate makes a big difference in how accurately we can read the plunger position during the fast motion of a release, which allows for more precise position sensing and faster response. The differences aren't dramatic, since the sensing was already pretty good even with the slower V1 scan rate, but you might notice a little better precision in tricky skill shots.

Keyboard keys: button inputs can now be mapped to keyboard keys. The joystick button option is still available as well, of course. Keyboard keys have the advantage of being closer to universal for PC pinball software: some pinball software can be set up to take joystick input, but nearly all PC pinball emulators can take keyboard input, and nearly all of them use the same key mappings.

Local shift button: one physical button can be designed as the local shift button. This works like a Shift button on a keyboard, but with cabinet buttons. It allows each physical button on the cabinet to have two PC keys assigned, one normal and one shifted. Hold down the local shift button, then press another key, and the other key's shifted key mapping is sent to the PC. The shift button can have a regular key mapping of its own as well, so it can do double duty. The shift feature lets you access more functions without cluttering your cabinet with extra buttons. It's especially nice for less frequently used functions like adjusting the volume or activating night mode.

Night mode: the output controller has a new "night mode" option, which lets you turn off all of your noisy devices with a single button, switch, or PC command. You can designate individual ports as noisy or not. Night mode only disables the noisemakers, so you still get the benefit of your flashers, button lights, and other quiet devices. This lets you play late into the night without disturbing your housemates or neighbors.

Gamma correction: you can designate individual output ports for gamma correction. This adjusts the intensity level of an output to make it match the way the human eye perceives brightness, so that fades and color mixes look more natural in lighting devices. You can apply this to individual ports, so that it only affects ports that actually have lights of some kind attached.

IR Remote Control: the controller software can transmit and/or receive IR remote control commands if you attach appropriate parts (an IR LED to send, an IR sensor chip to receive). This can be used to turn on your TV(s) when the system powers on, if they don't turn on automatically, and for any other functions you can think of requiring IR send/receive capabilities. You can assign IR commands to cabinet buttons, so that pressing a button on your cabinet sends a remote control command from the attached IR LED, and you can have the controller generate virtual key presses on your PC in response to received IR commands. If you have the IR sensor attached, the system can use it to learn commands from your existing remotes.

Yet more USB fixes: I've been gradually finding and fixing USB bugs in the mbed library for months now. This version has all of the fixes of the last couple of releases, of course, plus some new ones. It also has a new "last resort" feature, since there always seems to be "just one more" USB bug. The last resort is that you can tell the device to automatically reboot itself if it loses the USB connection and can't restore it within a given time limit.

More Downloads

  • Custom VP builds: I created modified versions of Visual Pinball 9.9 and Physmod5 that you might want to use in combination with this controller. The modified versions have special handling for plunger calibration specific to the Pinscape Controller, as well as some enhancements to the nudge physics. If you're not using the plunger, you might still want it for the nudge improvements. The modified version also works with any other input controller, so you can get the enhanced nudging effects even if you're using a different plunger/nudge kit. The big change in the modified versions is a "filter" for accelerometer input that's designed to make the response to cabinet nudges more realistic. It also makes the response more subdued than in the standard VP, so it's not to everyone's taste. The downloads include both the updated executables and the source code changes, in case you want to merge the changes into your own custom version(s).

    Note! These features are now standard in the official VP releases, so you don't need my custom builds if you're using 9.9.1 or later and/or VP 10. I don't think there's any reason to use my versions instead of the latest official ones, and in fact I'd encourage you to use the official releases since they're more up to date, but I'm leaving my builds available just in case. In the official versions, look for the checkbox "Enable Nudge Filter" in the Keys preferences dialog. My custom versions don't include that checkbox; they just enable the filter unconditionally.
  • Output circuit shopping list: This is a saved shopping cart at mouser.com with the parts needed to build one copy of the high-power output circuit for the LedWiz emulator feature, for use with the standalone KL25Z (that is, without the expansion boards). The quantities in the cart are for one output channel, so if you want N outputs, simply multiply the quantities by the N, with one exception: you only need one ULN2803 transistor array chip for each eight output circuits. If you're using the expansion boards, you won't need any of this, since the boards provide their own high-power outputs.
  • Cary Owens' optical sensor housing: A 3D-printable design for a housing/mounting bracket for the optical plunger sensor, designed by Cary Owens. This makes it easy to mount the sensor.
  • Lemming77's potentiometer mounting bracket and shooter rod connecter: Sketchup designs for 3D-printable parts for mounting a slide potentiometer as the plunger sensor. These were designed for a particular slide potentiometer that used to be available from an Aliexpress.com seller but is no longer listed. You can probably use this design as a starting point for other similar devices; just check the dimensions before committing the design to plastic.

Copyright and License

The Pinscape firmware is copyright 2014, 2021 by Michael J Roberts. It's released under an MIT open-source license. See License.

Warning to VirtuaPin Kit Owners

This software isn't designed as a replacement for the VirtuaPin plunger kit's firmware. If you bought the VirtuaPin kit, I recommend that you don't install this software. The KL25Z can only run one firmware program at a time, so if you install the Pinscape firmware on your KL25Z, it will replace and erase your existing VirtuaPin proprietary firmware. If you do this, the only way to restore your VirtuaPin firmware is to physically ship the KL25Z back to VirtuaPin and ask them to re-flash it. They don't allow you to do this at home, and they don't even allow you to back up your firmware, since they want to protect their proprietary software from copying. For all of these reasons, if you want to run the Pinscape software, I strongly recommend that you buy a "blank" retail KL25Z to use with Pinscape. They only cost about $15 and are available at several online retailers, including Amazon, Mouser, and eBay. The blank retail boards don't come with any proprietary firmware pre-installed, so installing Pinscape won't delete anything that you paid extra for.

With those warnings in mind, if you're absolutely sure that you don't mind permanently erasing your VirtuaPin firmware, it is at least possible to use Pinscape as a replacement for the VirtuaPin firmware. Pinscape uses the same button wiring conventions as the VirtuaPin setup, so you can keep your buttons (although you'll have to update the GPIO pin mappings in the Config Tool to match your physical wiring). As of the June, 2021 firmware, the Vishay VCNL4010 plunger sensor that comes with the VirtuaPin v3 plunger kit is supported, so you can also keep your plunger, if you have that chip. (You should check to be sure that's the sensor chip you have before committing to this route, if keeping the plunger sensor is important to you. The older VirtuaPin plunger kits came with different IR sensors that the Pinscape software doesn't handle.)

Files at this revision

API Documentation at this revision

Comitter:
mjr
Date:
Sat Dec 19 06:37:19 2015 +0000
Parent:
34:6b981a2afab7
Child:
36:b9747461331e
Commit message:
Keyboard/Media Control interface working, but the extra interface confuses the DOF connector.

Changed in this revision

FastAnalogIn.lib Show annotated file Show diff for this revision Revisions of this file
TSL1410R/tsl1410r.h Show annotated file Show diff for this revision Revisions of this file
TSL1410R/tsl410r.cpp Show annotated file Show diff for this revision Revisions of this file
USBDevice.lib Show annotated file Show diff for this revision Revisions of this file
USBJoystick/USBJoystick.cpp Show annotated file Show diff for this revision Revisions of this file
USBJoystick/USBJoystick.h Show annotated file Show diff for this revision Revisions of this file
USBProtocol.h Show annotated file Show diff for this revision Revisions of this file
Updates.h Show annotated file Show diff for this revision Revisions of this file
ccdSensor.h Show annotated file Show diff for this revision Revisions of this file
config.h Show annotated file Show diff for this revision Revisions of this file
main.cpp Show annotated file Show diff for this revision Revisions of this file
nullSensor.h Show annotated file Show diff for this revision Revisions of this file
nvm.h Show annotated file Show diff for this revision Revisions of this file
plunger.h Show annotated file Show diff for this revision Revisions of this file
potSensor.h Show annotated file Show diff for this revision Revisions of this file
--- a/FastAnalogIn.lib	Thu Dec 03 07:34:57 2015 +0000
+++ b/FastAnalogIn.lib	Sat Dec 19 06:37:19 2015 +0000
@@ -1,1 +1,1 @@
-http://mbed.org/users/Sissors/code/FastAnalogIn/#afc3b84dbbd6
+http://mbed.org/users/Sissors/code/FastAnalogIn/#234c5cd2b8de
--- a/TSL1410R/tsl1410r.h	Thu Dec 03 07:34:57 2015 +0000
+++ b/TSL1410R/tsl1410r.h	Sat Dec 19 06:37:19 2015 +0000
@@ -4,25 +4,51 @@
  *  This provides a high-level interface for the Taos TSL1410R linear CCD array sensor.
  */
  
- #include "mbed.h"
- #include "config.h"
- #include "FastIO.h"
- #include "FastAnalogIn.h"
+#include "mbed.h"
+#include "config.h"
+#include "FastAnalogIn.h"
  
- #ifndef TSL1410R_H
- #define TSL1410R_H
+#ifndef TSL1410R_H
+#define TSL1410R_H
+
+// For faster GPIO on the clock pin, we write the IOPORT registers directly.
+// PORT_BASE gives us the memory mapped location of the IOPORT register set
+// for a pin; PINMASK gives us the bit pattern to write to the registers.
+//
+// - To turn a pin ON:  PORT_BASE(pin)->PSOR |= PINMASK(pin)
+// - To turn a pin OFF: PORT_BASE(pin)->PCOR |= PINMASK(pin)
+// - To toggle a pin:   PORT_BASE(pin)->PTOR |= PINMASK(pin)
+//
+// When used in a loop where the port address and pin mask are cached in
+// local variables, this runs at the same speed as the FastIO library - about 
+// 78ns per pin write on the KL25Z.  Not surprising since it's doing the same
+// thing, and the compiler should be able to reduce a pin write to a single ARM
+// instruction when the port address and mask are in local register variables.
+// The advantage over the FastIO library is that this approach allows for pins
+// to be assigned dynamically at run-time, which we prefer because it allows for
+// configuration changes to be made on the fly rather than having to recompile
+// the program.
+#define GPIO_PORT_BASE(pin)   ((FGPIO_Type *)(FPTA_BASE + ((unsigned int)pin >> PORT_SHIFT) * 0x40))
+#define GPIO_PINMASK(pin)     (1 << ((pin & 0x7F) >> 2))
  
-template <PinName siPin, PinName clockPin> class TSL1410R
+class TSL1410R
 {
 public:
-    // set up the analog in port for reading the currently selected 
-    // pixel value
-    TSL1410R(PinName aoPin) : ao(aoPin)
+    TSL1410R(int nPix, PinName siPin, PinName clockPin, PinName ao1Pin, PinName ao2Pin) 
+        : nPix(nPix), si(siPin), clock(clockPin), ao1(ao1Pin), ao2(ao2Pin)
     {
+        // we're in parallel mode if ao2 is a valid pin
+        parallel = (ao2Pin != NC);
+        
+        // remember the clock pin port base and pin mask for fast access
+        clockPort = GPIO_PORT_BASE(clockPin);
+        clockMask = GPIO_PINMASK(clockPin);
+        
         // disable continuous conversion mode in FastAnalogIn - since we're
         // reading discrete pixel values, we want to control when the samples
         // are taken rather than continuously averaging over time
-        ao.disable();
+        ao1.disable();
+        if (parallel) ao2.disable();
 
         // clear out power-on noise by clocking through all pixels twice
         clear();
@@ -66,38 +92,84 @@
     // the current pixels and start a fresh integration cycle.
     void read(uint16_t *pix, int n)
     {
+        // get the clock pin pointers into local variables for fast access
+        register FGPIO_Type *clockPort = this->clockPort;
+        register uint32_t clockMask = this->clockMask;
+        
         // start the next integration cycle by pulsing SI and one clock
         si = 1;
-        clock = 1;
+        clockPort->PSOR |= clockMask;       // turn the clock pin on (clock = 1)
         si = 0;
-        clock = 0;
+        clockPort->PCOR |= clockMask;       // turn the clock pin off (clock = 0)
         
         // figure how many pixels to skip on each read
         int skip = nPix/n - 1;
         
         // read all of the pixels
-        for (int src = 0, dst = 0 ; src < nPix ; ++src)
+        if (parallel)
         {
-            // clock in and read the next pixel
-            clock = 1;
-            ao.enable();
-            wait_us(1);
-            clock = 0;
-            wait_us(11);
-            pix[dst++] = ao.read_u16();
-            ao.disable();
-            
-            // clock skipped pixels
-            for (int i = 0 ; i < skip ; ++i, ++src) 
+            // parallel mode - read pixels from each half sensor concurrently
+            int nPixHalf = nPix/2;
+            for (int src = 0, dst = 0 ; src < nPixHalf ; ++src)
             {
-                clock = 1;
-                clock = 0;
+                // pulse the clock and start the ADC sampling
+                clockPort->PSOR |= clockMask;
+                ao1.enable();
+                ao2.enable();
+                wait_us(1);
+                clockPort->PCOR |= clockMask;
+                
+                // wait for the ADCs to stabilize
+                wait_us(11);
+                
+                // read the pixels
+                pix[dst] = ao1.read_u16();
+                pix[dst+n/2] = ao2.read_u16();
+                
+                // turn off the ADC until the next pixel is ready
+                ao1.disable();
+                ao2.disable();
+                
+                // clock skipped pixels
+                for (int i = 0 ; i < skip ; ++i, ++src) 
+                {
+                    clockPort->PSOR |= clockMask;
+                    clockPort->PCOR |= clockMask;
+                }
+            }
+        }
+        else
+        {
+            // serial mode - read all pixels in a single file
+            for (int src = 0, dst = 0 ; src < nPix ; ++src)
+            {
+                // pulse the clock and start the ADC sampling
+                clockPort->PSOR |= clockMask;
+                ao1.enable();
+                wait_us(1);
+                clockPort->PCOR |= clockMask;
+                
+                // wait for the ADC sample to stabilize
+                wait_us(11);
+                
+                // read the ADC sample
+                pix[dst++] = ao1.read_u16();
+                
+                // turn off the ADC until the next pixel is ready
+                ao1.disable();
+                
+                // clock skipped pixels
+                for (int i = 0 ; i < skip ; ++i, ++src) 
+                {
+                    clockPort->PSOR |= clockMask;
+                    clockPort->PCOR |= clockMask;
+                }
             }
         }
         
         // clock out one extra pixel to leave A1 in the high-Z state
-        clock = 1;
-        clock = 0;
+        clockPort->PSOR |= clockMask;
+        clockPort->PCOR |= clockMask;
     }
 
     // Clock through all pixels to clear the array.  Pulses SI at the
@@ -106,27 +178,32 @@
     // integrated while the clear() was taking place.
     void clear()
     {
+        // get the clock pin pointers into local variables for fast access
+        register FGPIO_Type *clockPort = this->clockPort;
+        register uint32_t clockMask = this->clockMask;
+
         // clock in an SI pulse
         si = 1;
-        clock = 1;
-        clock = 0;
+        clockPort->PSOR |= clockMask;
         si = 0;
+        clockPort->PCOR |= clockMask;
         
         // clock out all pixels
         for (int i = 0 ; i < nPix + 1 ; ++i) {
-            clock = 1;
-            clock = 0;
+            clockPort->PSOR |= clockMask;
+            clockPort->PCOR |= clockMask;
         }
     }
 
-    // number of pixels in the array
-    static const int nPix = CCD_NPIXELS;
-    
-    
 private:
-    FastOut<siPin> si;
-    FastOut<clockPin> clock;
-    FastAnalogIn ao;
+    int nPix;                 // number of pixels in physical sensor array
+    DigitalOut si;
+    DigitalOut clock;
+    FGPIO_Type *clockPort;    // IOPORT base address for clock pin, for fast writes
+    uint32_t clockMask;       // IOPORT register bit mask for clock pin
+    FastAnalogIn ao1; 
+    FastAnalogIn ao2;         // valid iff running in parallel mode
+    bool parallel;            // true -> running in parallel mode
 };
  
 #endif /* TSL1410R_H */
--- a/TSL1410R/tsl410r.cpp	Thu Dec 03 07:34:57 2015 +0000
+++ b/TSL1410R/tsl410r.cpp	Sat Dec 19 06:37:19 2015 +0000
@@ -1,85 +1,3 @@
-#if 0
 // this file is no longer used - the method bodies are no in the header,
 // which was necessary because of the change to a template class, which
 // itself was necessary because of the use of the FastIO library
-
-#include "mbed.h"
-#include "tsl1410r.h"
-
-template <PinName siPin, PinName clockPin> TSL1410R<siPin, clockPin>::
-    TSL1410R<siPin, clockPin>(PinName aoPort) : ao(aoPort)
-{
-    // clear out power-on noise by clocking through all pixels twice
-    clear();
-    clear();
-}
-
-template <PinName siPin, PinName clockPin> void TSL1410R<siPin, clockPin>::clear()
-{
-    // clock in an SI pulse
-    si = 1;
-    clock = 1;
-    clock = 0;
-    si = 0;
-    
-    // clock out all pixels
-    for (int i = 0 ; i < nPix + 1 ; ++i) {
-        clock = 1;
-        clock = 0;
-    }
-}
-
-template <PinName siPin, PinName clockPin> void TSL1410R<siPin, clockPin>::
-    read(uint16_t *pix, int n, void (*cb)(void *ctx), void *cbctx, int cbcnt)
-{
-    // start the next integration cycle by pulsing SI and one clock
-    si = 1;
-    clock = 1;
-    clock = 0;
-    si = 0;
-        
-    // figure how many pixels to skip on each read
-    int skip = nPix/n - 1;
-    
-    // figure the callback interval
-    int cbInterval = nPix;
-    if (cb != 0)
-        cbInterval = nPix/(cbcnt+1);
-
-    // read all of the pixels
-    for (int src = 0, dst = 0 ; src < nPix ; )
-    {
-        // figure the end of this callback interval
-        int srcEnd = src + cbInterval;
-        if (srcEnd > nPix)
-            srcEnd = nPix;
-        
-        // read one callback chunk of pixels
-        for ( ; src < srcEnd ; ++src)
-        {
-            // read this pixel
-            pix[dst++] = ao.read_u16();
-        
-            // clock in the next pixel
-            clock = 1;
-            clock = 0;
-            
-            // clock skipped pixels
-            for (int i = 0 ; i < skip ; ++i, ++src) 
-            {
-                clock = 1;
-                clock = 0;
-            }
-        }
-        
-        // call the callback, if we're not at the last pixel
-        if (cb != 0 && src < nPix)
-            (*cb)(cbctx);
-    }
-    
-    // clock out one extra pixel to leave A1 in the high-Z state
-    clock = 1;
-    clock = 0;
-}
-
-#endif /* 0 */
--- a/USBDevice.lib	Thu Dec 03 07:34:57 2015 +0000
+++ b/USBDevice.lib	Sat Dec 19 06:37:19 2015 +0000
@@ -1,1 +1,1 @@
-http://mbed.org/users/mjr/code/USBDevice/#a8eb758f4074
+http://mbed.org/users/mjr/code/USBDevice/#b0a3f6b27b07
--- a/USBJoystick/USBJoystick.cpp	Thu Dec 03 07:34:57 2015 +0000
+++ b/USBJoystick/USBJoystick.cpp	Sat Dec 19 06:37:19 2015 +0000
@@ -22,11 +22,12 @@
 
 #include "config.h"  // Pinscape configuration
 
+
+
 // Length of our joystick reports.  Important: This must be kept in sync 
 // with the actual joystick report format sent in update().
 const int reportLen = 14;
 
-#ifdef ENABLE_JOYSTICK
 bool USBJoystick::update(int16_t x, int16_t y, int16_t z, uint32_t buttons, uint16_t status) 
 {
    _x = x;
@@ -39,7 +40,7 @@
    // send the report
    return update();
 }
- 
+
 bool USBJoystick::update() 
 {
    HID_REPORT report;
@@ -62,6 +63,30 @@
    return sendTO(&report, 100);
 }
 
+bool USBJoystick::kbUpdate(uint8_t data[8])
+{
+    // set up the report
+    HID_REPORT report;
+    report.data[0] = REPORT_ID_KB;      // report ID = keyboard
+    memcpy(&report.data[1], data, 8);   // copy the kb report data
+    report.length = 9;                  // length = ID prefix + kb report length
+    
+    // send it to endpoint 4 (the keyboard interface endpoint)
+    return writeTO(EP4IN, report.data, report.length, MAX_PACKET_SIZE_EPINT, 100);
+}
+
+bool USBJoystick::mediaUpdate(uint8_t data)
+{
+    // set up the report
+    HID_REPORT report;
+    report.data[0] = REPORT_ID_MEDIA;   // report ID = media
+    report.data[1] = data;              // key pressed bits
+    report.length = 2;
+    
+    // send it
+    return writeTO(EP4IN, report.data, report.length, MAX_PACKET_SIZE_EPINT, 100);
+}
+ 
 bool USBJoystick::updateExposure(int &idx, int npix, const uint16_t *pix)
 {
     HID_REPORT report;
@@ -91,10 +116,10 @@
     }
     
     // send the report
-    return send(&report);
+    return sendTO(&report, 100);
 }
 
-bool USBJoystick::reportConfig(int numOutputs, int unitNo)
+bool USBJoystick::reportConfig(int numOutputs, int unitNo, int plungerZero, int plungerMax)
 {
     HID_REPORT report;
 
@@ -111,9 +136,13 @@
     // write the unit number
     put(4, unitNo);
     
+    // write the plunger zero and max values
+    put(6, plungerZero);
+    put(8, plungerMax);
+    
     // send the report
     report.length = reportLen;
-    return send(&report);
+    return sendTO(&report, 100);
 }
 
 bool USBJoystick::move(int16_t x, int16_t y) 
@@ -136,8 +165,6 @@
      return update();
 }
 
-#else /* ENABLE_JOYSTICK */
-
 bool USBJoystick::updateStatus(uint32_t status)
 {
    HID_REPORT report;
@@ -152,9 +179,6 @@
    return sendTO(&report, 100);
 }
 
-#endif /* ENABLE_JOYSTICK */
- 
- 
 void USBJoystick::_init() {
  
    _x = 0;                       
@@ -166,93 +190,183 @@
 }
  
  
-uint8_t * USBJoystick::reportDesc() 
-{    
-#ifdef ENABLE_JOYSTICK
-    // Joystick reports are enabled.  Use the full joystick report
-    // format.
-    static uint8_t reportDescriptor[] = 
-    {         
-         USAGE_PAGE(1), 0x01,            // Generic desktop
-         USAGE(1), 0x04,                 // Joystick
-         COLLECTION(1), 0x01,            // Application
+// --------------------------------------------------------------------------
+//
+// USB HID Report Descriptor - Joystick
+//
+static uint8_t reportDescriptorJS[] = 
+{         
+    USAGE_PAGE(1), 0x01,            // Generic desktop
+    USAGE(1), 0x04,                 // Joystick
+    COLLECTION(1), 0x01,            // Application
+     
+        // input report (device to host)
+
+        USAGE_PAGE(1), 0x06,        // generic device controls - for config status
+        USAGE(1), 0x00,             // undefined device control
+        LOGICAL_MINIMUM(1), 0x00,   // 8-bit values
+        LOGICAL_MAXIMUM(1), 0xFF,
+        REPORT_SIZE(1), 0x08,       // 8 bits per report
+        REPORT_COUNT(1), 0x04,      // 4 reports (4 bytes)
+        INPUT(1), 0x02,             // Data, Variable, Absolute
+
+        USAGE_PAGE(1), 0x09,        // Buttons
+        USAGE_MINIMUM(1), 0x01,     // { buttons }
+        USAGE_MAXIMUM(1), 0x20,     // {  1-32   }
+        LOGICAL_MINIMUM(1), 0x00,   // 1-bit buttons - 0...
+        LOGICAL_MAXIMUM(1), 0x01,   // ...to 1
+        REPORT_SIZE(1), 0x01,       // 1 bit per report
+        REPORT_COUNT(1), 0x20,      // 32 reports
+        UNIT_EXPONENT(1), 0x00,     // Unit_Exponent (0)
+        UNIT(1), 0x00,              // Unit (None)                                           
+        INPUT(1), 0x02,             // Data, Variable, Absolute
+       
+        USAGE_PAGE(1), 0x01,        // Generic desktop
+        USAGE(1), 0x30,             // X axis
+        USAGE(1), 0x31,             // Y axis
+        USAGE(1), 0x32,             // Z axis
+        LOGICAL_MINIMUM(2), 0x00,0xF0,   // each value ranges -4096
+        LOGICAL_MAXIMUM(2), 0x00,0x10,   // ...to +4096
+        REPORT_SIZE(1), 0x10,       // 16 bits per report
+        REPORT_COUNT(1), 0x03,      // 3 reports (X, Y, Z)
+        INPUT(1), 0x02,             // Data, Variable, Absolute
          
-             // input report (device to host)
+        // output report (host to device)
+        REPORT_SIZE(1), 0x08,       // 8 bits per report
+        REPORT_COUNT(1), 0x08,      // output report count - 8-byte LedWiz format
+        0x09, 0x01,                 // usage
+        0x91, 0x01,                 // Output (array)
+
+    END_COLLECTION(0)
+};
+
+// 
+// USB HID Report Descriptor - Keyboard/Media Control
+//
+static uint8_t reportDescriptorKB[] = 
+{
+    USAGE_PAGE(1), 0x01,                    // Generic Desktop
+    USAGE(1), 0x06,                         // Keyboard
+    COLLECTION(1), 0x01,                    // Application
+        REPORT_ID(1), REPORT_ID_KB,
 
-             USAGE_PAGE(1), 0x06,        // generic device controls - for config status
-             USAGE(1), 0x00,             // undefined device control
-             LOGICAL_MINIMUM(1), 0x00,   // 8-bit values
-             LOGICAL_MAXIMUM(1), 0xFF,
-             REPORT_SIZE(1), 0x08,       // 8 bits per report
-             REPORT_COUNT(1), 0x04,      // 4 reports (4 bytes)
-             INPUT(1), 0x02,             // Data, Variable, Absolute
+        USAGE_PAGE(1), 0x07,                    // Key Codes
+        USAGE_MINIMUM(1), 0xE0,
+        USAGE_MAXIMUM(1), 0xE7,
+        LOGICAL_MINIMUM(1), 0x00,
+        LOGICAL_MAXIMUM(1), 0x01,
+        REPORT_SIZE(1), 0x01,
+        REPORT_COUNT(1), 0x08,
+        INPUT(1), 0x02,                         // Data, Variable, Absolute
+        REPORT_COUNT(1), 0x01,
+        REPORT_SIZE(1), 0x08,
+        INPUT(1), 0x01,                         // Constant
+
+        REPORT_COUNT(1), 0x05,
+        REPORT_SIZE(1), 0x01,
+        USAGE_PAGE(1), 0x08,                    // LEDs
+        USAGE_MINIMUM(1), 0x01,
+        USAGE_MAXIMUM(1), 0x05,
+        OUTPUT(1), 0x02,                        // Data, Variable, Absolute
+        REPORT_COUNT(1), 0x01,
+        REPORT_SIZE(1), 0x03,
+        OUTPUT(1), 0x01,                        // Constant
 
-             USAGE_PAGE(1), 0x09,        // Buttons
-             USAGE_MINIMUM(1), 0x01,     // { buttons }
-             USAGE_MAXIMUM(1), 0x20,     // {  1-32   }
-             LOGICAL_MINIMUM(1), 0x00,   // 1-bit buttons - 0...
-             LOGICAL_MAXIMUM(1), 0x01,   // ...to 1
-             REPORT_SIZE(1), 0x01,       // 1 bit per report
-             REPORT_COUNT(1), 0x20,      // 32 reports
-             UNIT_EXPONENT(1), 0x00,     // Unit_Exponent (0)
-             UNIT(1), 0x00,              // Unit (None)                                           
-             INPUT(1), 0x02,             // Data, Variable, Absolute
-           
-             USAGE_PAGE(1), 0x01,        // Generic desktop
-             USAGE(1), 0x30,             // X axis
-             USAGE(1), 0x31,             // Y axis
-             USAGE(1), 0x32,             // Z axis
-             LOGICAL_MINIMUM(2), 0x00,0xF0,   // each value ranges -4096
-             LOGICAL_MAXIMUM(2), 0x00,0x10,   // ...to +4096
-             REPORT_SIZE(1), 0x10,       // 16 bits per report
-             REPORT_COUNT(1), 0x03,      // 3 reports (X, Y, Z)
-             INPUT(1), 0x02,             // Data, Variable, Absolute
-             
-             // output report (host to device)
-             REPORT_SIZE(1), 0x08,       // 8 bits per report
-             REPORT_COUNT(1), 0x08,      // output report count - 8-byte LedWiz format
-             0x09, 0x01,                 // usage
-             0x91, 0x01,                 // Output (array)
+        REPORT_COUNT(1), 0x06,
+        REPORT_SIZE(1), 0x08,
+        LOGICAL_MINIMUM(1), 0x00,
+        LOGICAL_MAXIMUM(1), 0x65,
+        USAGE_PAGE(1), 0x07,                    // Key Codes
+        USAGE_MINIMUM(1), 0x00,
+        USAGE_MAXIMUM(1), 0x65,
+        INPUT(1), 0x00,                         // Data, Array
+    END_COLLECTION(0),
 
-         END_COLLECTION(0)
+    // Media Control
+    USAGE_PAGE(1), 0x0C,
+    USAGE(1), 0x01,
+    COLLECTION(1), 0x01,
+        REPORT_ID(1), REPORT_ID_MEDIA,
+        USAGE_PAGE(1), 0x0C,
+        LOGICAL_MINIMUM(1), 0x00,
+        LOGICAL_MAXIMUM(1), 0x01,
+        REPORT_SIZE(1), 0x01,
+        REPORT_COUNT(1), 0x07,
+        USAGE(1), 0xE9,             // Volume Up
+        USAGE(1), 0xEA,             // Volume Down
+        USAGE(1), 0xE2,             // Mute
+        USAGE(1), 0xB5,             // Next Track
+        USAGE(1), 0xB6,             // Previous Track
+        USAGE(1), 0xB7,             // Stop
+        USAGE(1), 0xCD,             // Play / Pause
+        INPUT(1), 0x02,             // Input (Data, Variable, Absolute)
+        REPORT_COUNT(1), 0x01,
+        INPUT(1), 0x01,
+    END_COLLECTION(0),
+};
 
-      };
-#else /* defined(ENABLE_JOYSTICK) */
+// 
+// USB HID Report Descriptor - LedWiz only, with no joystick or keyboard
+// input reporting
+//
+static uint8_t reportDescriptorLW[] = 
+{         
+    USAGE_PAGE(1), 0x01,            // Generic desktop
+    USAGE(1), 0x00,                 // Undefined
 
-    // Joystick reports are disabled.  We still want to appear
-    // as a USB device for the LedWiz output emulation, but we
-    // don't want to appear as a joystick.
+    COLLECTION(1), 0x01,            // Application
      
-    static uint8_t reportDescriptor[] = 
-    {         
-         USAGE_PAGE(1), 0x01,            // Generic desktop
-         USAGE(1), 0x00,                 // Undefined
+        // input report (device to host)
+        USAGE_PAGE(1), 0x06,        // generic device controls - for config status
+        USAGE(1), 0x00,             // undefined device control
+        LOGICAL_MINIMUM(1), 0x00,   // 8-bit values
+        LOGICAL_MAXIMUM(1), 0xFF,
+        REPORT_SIZE(1), 0x08,       // 8 bits per report
+        REPORT_COUNT(1), reportLen, // standard report length (same as if we were in joystick mode)
+        INPUT(1), 0x02,             // Data, Variable, Absolute
+
+        // output report (host to device)
+        REPORT_SIZE(1), 0x08,       // 8 bits per report
+        REPORT_COUNT(1), 0x08,      // output report count (LEDWiz messages)
+        0x09, 0x01,                 // usage
+        0x91, 0x01,                 // Output (array)
+
+    END_COLLECTION(0)
+};
+
 
-         COLLECTION(1), 0x01,            // Application
-         
-             // input report (device to host)
-             USAGE_PAGE(1), 0x06,        // generic device controls - for config status
-             USAGE(1), 0x00,             // undefined device control
-             LOGICAL_MINIMUM(1), 0x00,   // 8-bit values
-             LOGICAL_MAXIMUM(1), 0xFF,
-             REPORT_SIZE(1), 0x08,       // 8 bits per report
-             REPORT_COUNT(1), reportLen, // standard report length (same as if we were in joystick mode)
-             INPUT(1), 0x02,             // Data, Variable, Absolute
-
-             // output report (host to device)
-             REPORT_SIZE(1), 0x08,       // 8 bits per report
-             REPORT_COUNT(1), 0x08,      // output report count (LEDWiz messages)
-             0x09, 0x01,                 // usage
-             0x91, 0x01,                 // Output (array)
-
-         END_COLLECTION(0)
-      };
- 
-#endif /* defined(ENABLE_JOYSTICK) */
-
-      reportLength = sizeof(reportDescriptor);
-      return reportDescriptor;
-}
+uint8_t * USBJoystick::reportDescN(int idx) 
+{    
+    if (enableJoystick)
+    {
+        // Joystick reports are enabled.  Use the full joystick report
+        // format, or full keyboard report format, depending on which
+        // interface is being requested.
+        switch (idx)
+        {
+        case 0:
+            // joystick interface
+            reportLength = sizeof(reportDescriptorJS);
+            return reportDescriptorJS;
+            
+        case 1:
+            // keyboard interface
+            reportLength = sizeof(reportDescriptorKB);
+            return reportDescriptorKB;
+            
+        default:
+            // unknown interface
+            reportLength = 0;
+            return 0;
+        }
+    }
+    else
+    {
+        // Joystick reports are disabled.  Use the LedWiz-only format.
+        reportLength = sizeof(reportDescriptorLW);
+        return reportDescriptorLW;
+    }
+} 
  
  uint8_t * USBJoystick::stringImanufacturerDesc() {
     static uint8_t stringImanufacturerDescriptor[] = {
@@ -282,3 +396,206 @@
     };
     return stringIproductDescriptor;
 }
+
+#define DEFAULT_CONFIGURATION (1)
+
+uint8_t * USBJoystick::configurationDesc() 
+{
+    int rptlen0 = reportDescLengthN(0);
+    int rptlen1 = reportDescLengthN(1);
+    if (useKB)
+    {
+        int cfglenKB = ((1 * CONFIGURATION_DESCRIPTOR_LENGTH)
+                        + (2 * INTERFACE_DESCRIPTOR_LENGTH)
+                        + (2 * HID_DESCRIPTOR_LENGTH)
+                        + (4 * ENDPOINT_DESCRIPTOR_LENGTH));
+        static uint8_t configurationDescriptorWithKB[] = 
+        {
+            CONFIGURATION_DESCRIPTOR_LENGTH,// bLength
+            CONFIGURATION_DESCRIPTOR,       // bDescriptorType
+            LSB(cfglenKB),                  // wTotalLength (LSB)
+            MSB(cfglenKB),                  // wTotalLength (MSB)
+            0x02,                           // bNumInterfaces - TWO INTERFACES (JOYSTICK + KEYBOARD)
+            DEFAULT_CONFIGURATION,          // bConfigurationValue
+            0x00,                           // iConfiguration
+            C_RESERVED | C_SELF_POWERED,    // bmAttributes
+            C_POWER(0),                     // bMaxPowerHello World from Mbed
+        
+            // INTERFACE 0 - JOYSTICK/LEDWIZ
+            INTERFACE_DESCRIPTOR_LENGTH,    // bLength
+            INTERFACE_DESCRIPTOR,           // bDescriptorType
+            0x00,                           // bInterfaceNumber - first interface = 0
+            0x00,                           // bAlternateSetting
+            0x02,                           // bNumEndpoints
+            HID_CLASS,                      // bInterfaceClass
+            HID_SUBCLASS_NONE,              // bInterfaceSubClass
+            HID_PROTOCOL_NONE,              // bInterfaceProtocol
+            0x00,                           // iInterface
+        
+            HID_DESCRIPTOR_LENGTH,          // bLength
+            HID_DESCRIPTOR,                 // bDescriptorType
+            LSB(HID_VERSION_1_11),          // bcdHID (LSB)
+            MSB(HID_VERSION_1_11),          // bcdHID (MSB)
+            0x00,                           // bCountryCode
+            0x01,                           // bNumDescriptors
+            REPORT_DESCRIPTOR,              // bDescriptorType
+            LSB(rptlen0),                   // wDescriptorLength (LSB)
+            MSB(rptlen0),                   // wDescriptorLength (MSB)
+        
+            ENDPOINT_DESCRIPTOR_LENGTH,     // bLength
+            ENDPOINT_DESCRIPTOR,            // bDescriptorType
+            PHY_TO_DESC(EPINT_IN),          // bEndpointAddress - EPINT == EP1
+            E_INTERRUPT,                    // bmAttributes
+            LSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (LSB)
+            MSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (MSB)
+            1,                              // bInterval (milliseconds)
+        
+            ENDPOINT_DESCRIPTOR_LENGTH,     // bLength
+            ENDPOINT_DESCRIPTOR,            // bDescriptorType
+            PHY_TO_DESC(EPINT_OUT),         // bEndpointAddress - EPINT == EP1
+            E_INTERRUPT,                    // bmAttributes
+            LSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (LSB)
+            MSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (MSB)
+            1,                              // bInterval (milliseconds)
+            
+            // INTERFACE 1 - KEYBOARD
+            INTERFACE_DESCRIPTOR_LENGTH,    // bLength
+            INTERFACE_DESCRIPTOR,           // bDescriptorType
+            0x01,                           // bInterfaceNumber - second interface = 1
+            0x00,                           // bAlternateSetting
+            0x02,                           // bNumEndpoints
+            HID_CLASS,                      // bInterfaceClass
+            1,                              // bInterfaceSubClass - KEYBOARD
+            1,                              // bInterfaceProtocol - KEYBOARD
+            0x00,                           // iInterface
+        
+            HID_DESCRIPTOR_LENGTH,          // bLength
+            HID_DESCRIPTOR,                 // bDescriptorType
+            LSB(HID_VERSION_1_11),          // bcdHID (LSB)
+            MSB(HID_VERSION_1_11),          // bcdHID (MSB)
+            0x00,                           // bCountryCode
+            0x01,                           // bNumDescriptors
+            REPORT_DESCRIPTOR,              // bDescriptorType
+            LSB(rptlen1),                   // wDescriptorLength (LSB)
+            MSB(rptlen1),                   // wDescriptorLength (MSB)
+        
+            ENDPOINT_DESCRIPTOR_LENGTH,     // bLength
+            ENDPOINT_DESCRIPTOR,            // bDescriptorType
+            PHY_TO_DESC(EP4IN),             // bEndpointAddress
+            E_INTERRUPT,                    // bmAttributes
+            LSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (LSB)
+            MSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (MSB)
+            1,                              // bInterval (milliseconds)
+        
+            ENDPOINT_DESCRIPTOR_LENGTH,     // bLength
+            ENDPOINT_DESCRIPTOR,            // bDescriptorType
+            PHY_TO_DESC(EP4OUT),            // bEndpointAddress
+            E_INTERRUPT,                    // bmAttributes
+            LSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (LSB)
+            MSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (MSB)
+            1,                              // bInterval (milliseconds)
+        };
+
+        // Keyboard + joystick interfaces
+        return configurationDescriptorWithKB;
+    }
+    else
+    {
+        // No keyboard - joystick interface only
+        int cfglenNoKB = ((1 * CONFIGURATION_DESCRIPTOR_LENGTH)
+                          + (1 * INTERFACE_DESCRIPTOR_LENGTH)
+                          + (1 * HID_DESCRIPTOR_LENGTH)
+                          + (2 * ENDPOINT_DESCRIPTOR_LENGTH));
+        static uint8_t configurationDescriptorNoKB[] = 
+        {
+            CONFIGURATION_DESCRIPTOR_LENGTH,// bLength
+            CONFIGURATION_DESCRIPTOR,       // bDescriptorType
+            LSB(cfglenNoKB),                // wTotalLength (LSB)
+            MSB(cfglenNoKB),                // wTotalLength (MSB)
+            0x01,                           // bNumInterfaces
+            DEFAULT_CONFIGURATION,          // bConfigurationValue
+            0x00,                           // iConfiguration
+            C_RESERVED | C_SELF_POWERED,    // bmAttributes
+            C_POWER(0),                     // bMaxPowerHello World from Mbed
+        
+            INTERFACE_DESCRIPTOR_LENGTH,    // bLength
+            INTERFACE_DESCRIPTOR,           // bDescriptorType
+            0x00,                           // bInterfaceNumber
+            0x00,                           // bAlternateSetting
+            0x02,                           // bNumEndpoints
+            HID_CLASS,                      // bInterfaceClass
+            1,                              // bInterfaceSubClass
+            1,                              // bInterfaceProtocol (keyboard)
+            0x00,                           // iInterface
+        
+            HID_DESCRIPTOR_LENGTH,          // bLength
+            HID_DESCRIPTOR,                 // bDescriptorType
+            LSB(HID_VERSION_1_11),          // bcdHID (LSB)
+            MSB(HID_VERSION_1_11),          // bcdHID (MSB)
+            0x00,                           // bCountryCode
+            0x01,                           // bNumDescriptors
+            REPORT_DESCRIPTOR,              // bDescriptorType
+            (uint8_t)(LSB(rptlen0)),        // wDescriptorLength (LSB)
+            (uint8_t)(MSB(rptlen0)),        // wDescriptorLength (MSB)
+        
+            ENDPOINT_DESCRIPTOR_LENGTH,     // bLength
+            ENDPOINT_DESCRIPTOR,            // bDescriptorType
+            PHY_TO_DESC(EPINT_IN),          // bEndpointAddress
+            E_INTERRUPT,                    // bmAttributes
+            LSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (LSB)
+            MSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (MSB)
+            1,                              // bInterval (milliseconds)
+        
+            ENDPOINT_DESCRIPTOR_LENGTH,     // bLength
+            ENDPOINT_DESCRIPTOR,            // bDescriptorType
+            PHY_TO_DESC(EPINT_OUT),         // bEndpointAddress
+            E_INTERRUPT,                    // bmAttributes
+            LSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (LSB)
+            MSB(MAX_PACKET_SIZE_EPINT),     // wMaxPacketSize (MSB)
+            1,                              // bInterval (milliseconds)
+        };
+
+        return configurationDescriptorNoKB;
+    }
+}
+
+// Set the configuration.  We need to set up the endpoints for
+// our active interfaces.
+bool USBJoystick::USBCallback_setConfiguration(uint8_t configuration) 
+{
+    // we only have one valid configuration
+    if (configuration != DEFAULT_CONFIGURATION)
+        return false;
+        
+    // Configure endpoint 1 - we use this in all cases, for either
+    // the combined joystick/ledwiz interface or just the ledwiz interface
+    addEndpoint(EPINT_IN, MAX_PACKET_SIZE_EPINT);
+    addEndpoint(EPINT_OUT, MAX_PACKET_SIZE_EPINT);
+    readStart(EPINT_OUT, MAX_HID_REPORT_SIZE);
+    
+    // if the keyboard is enabled, configure endpoint 4 for the kb interface
+    if (useKB)
+    {
+        addEndpoint(EP4IN, MAX_PACKET_SIZE_EPINT);
+        addEndpoint(EP4OUT, MAX_PACKET_SIZE_EPINT);
+        readStart(EP4OUT, MAX_PACKET_SIZE_EPINT);
+    }
+
+    // success
+    return true;
+}
+
+// Handle messages on endpoint 4 - this is the keyboard interface.
+// The host uses this to send updates for the keyboard indicator LEDs
+// (caps lock, num lock, etc).  We don't do anything with these, but
+// we at least need to read them to keep the pipe from clogging up.
+bool USBJoystick::EP4_OUT_callback() 
+{
+    // read this message
+    uint32_t bytesRead = 0;
+    uint8_t led[65];
+    USBDevice::readEP(EP4OUT, led, &bytesRead, MAX_HID_REPORT_SIZE);
+
+    // start the next read
+    return readStart(EP4OUT, MAX_HID_REPORT_SIZE);
+}
--- a/USBJoystick/USBJoystick.h	Thu Dec 03 07:34:57 2015 +0000
+++ b/USBJoystick/USBJoystick.h	Sat Dec 19 06:37:19 2015 +0000
@@ -7,9 +7,11 @@
 #define USBJOYSTICK_H
  
 #include "USBHID.h"
- 
-#define REPORT_ID_JOYSTICK  4
- 
+
+// keyboard interface report IDs 
+const uint8_t REPORT_ID_KB = 1;
+const uint8_t REPORT_ID_MEDIA = 2;
+
 /* Common usage */
 enum JOY_BUTTON {
      JOY_B0 = 0x0001,
@@ -90,15 +92,32 @@
          * @param product_id Your product_id (default: 0x0002)
          * @param product_release Your product_release (default: 0x0001)
          */
-         USBJoystick(uint16_t vendor_id = 0x1234, uint16_t product_id = 0x0100, uint16_t product_release = 0x0001, int waitForConnect = true): 
-             USBHID(16, 64, vendor_id, product_id, product_release, false)
-             { 
-                 _init();
-                 connect(waitForConnect);
-             };
+         USBJoystick(uint16_t vendor_id, uint16_t product_id, uint16_t product_release, 
+             int waitForConnect, bool enableJoystick, bool useKB)
+             : USBHID(16, 64, vendor_id, product_id, product_release, false)
+         { 
+             _init();
+             this->useKB = useKB;
+             this->enableJoystick = enableJoystick;
+             connect(waitForConnect);
+         };
          
          /**
-         * Write a state of the mouse
+          * Send a keyboard report.  The argument gives the key state, in the standard
+          * 6KRO USB keyboard report format: byte 0 is the modifier key bit mask, byte 1
+          * is reserved (must be 0), and bytes 2-6 are the currently pressed keys, as
+          * USB key codes.
+          */
+         bool kbUpdate(uint8_t data[8]);
+         
+         /**
+          * Send a media key update.  The argument gives the bit mask of media keys
+          * currently pressed.  See the HID report descriptor for the order of bits.
+          */
+         bool mediaUpdate(uint8_t data);
+         
+         /**
+         * Update the joystick status
          *
          * @param x x-axis position
          * @param y y-axis position
@@ -131,10 +150,10 @@
          * @param numOutputs the number of configured output channels
          * @param unitNo the device unit number
          */
-         bool reportConfig(int numOutputs, int unitNo);
+         bool reportConfig(int numOutputs, int unitNo, int plungerZero, int plungerMax);
  
          /**
-         * Write a state of the mouse
+         * Send a joystick report to the host
          *
          * @returns true if there is no error, false otherwise
          */
@@ -163,20 +182,25 @@
          * @returns true if there is no error, false otherwise
          */
          bool buttons(uint32_t buttons);
-         
-         /*
-         * To define the report descriptor. Warning: this method has to store the length of the report descriptor in reportLength.
-         *
-         * @returns pointer to the report descriptor
-         */
-         virtual uint8_t * reportDesc();
+
+         /* USB descriptor overrides */
+         virtual uint8_t * configurationDesc();
+         virtual uint8_t * reportDescN(int n);
  
          /* USB descriptor string overrides */
          virtual uint8_t *stringImanufacturerDesc();
          virtual uint8_t *stringIserialDesc();
          virtual uint8_t *stringIproductDesc();
+         
+         /* callback overrides */
+         virtual bool USBCallback_setConfiguration(uint8_t configuration);
+         virtual bool USBCallback_setInterface(uint16_t interface, uint8_t alternate)
+            { return interface == 0 || interface == 1; }
+         virtual bool EP4_OUT_callback();
  
      private:
+         bool enableJoystick;
+         bool useKB;
          int16_t _x;                       
          int16_t _y;     
          int16_t _z;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/USBProtocol.h	Sat Dec 19 06:37:19 2015 +0000
@@ -0,0 +1,519 @@
+// USB Message Protocol
+//
+// This file is purely for documentation, to describe our USB protocol.
+// We use the standard HID setup with one endpoint in each direction.
+// See USBJoystick.cpp/.h for our USB descriptor arrangement.
+//
+
+// ------ OUTGOING MESSAGES (DEVICE TO HOST) ------
+//
+// In most cases, our outgoing messages are HID joystick reports, using the
+// format defined in USBJoystick.cpp.  This allows us to be installed on
+// Windows as a standard USB joystick, which all versions of Windows support
+// using in-the-box drivers.  This allows a completely transparent, driverless,
+// plug-and-play installation experience on Windows.
+//
+// We subvert the joystick report format in certain cases to report other 
+// types of information, when specifically requested by the host.  This allows
+// our custom configuration UI on the Windows side to query additional 
+// information that we don't normally send via the joystick reports.  We
+// define a custom vendor-specific "status" field in the reports that we
+// use to identify these special reports, as described below.
+//
+// Normal joystick reports always have 0 in the high bit of the first byte
+// of the report.  Special non-joystick reports always have 1 in the high bit 
+// of the first byte.  (This byte is defined in the HID Report Descriptor
+// as an opaque vendor-defined value, so the joystick interface on the
+// Windows side simply ignores it.)
+//
+// Pixel dumps:  requested by custom protocol message 65 3 (see below).  
+// This sends a series of reports to the host in the following format, for 
+// as many messages as are neessary to report all pixels:
+//
+//    bytes 0:1 = 11-bit index, with high 5 bits set to 10000.  For 
+//                example, 0x04 0x80 indicates index 4.  This is the 
+//                starting pixel number in the report.  The first report 
+//                will be 0x00 0x80 to indicate pixel #0.  
+//    bytes 2:3 = 16-bit unsigned int brightness level of pixel at index
+//    bytes 4:5 = brightness of pixel at index+1
+//    etc for the rest of the packet
+//
+// Configuration query:  requested by custom protocol message 65 4 (see 
+// below).  This sends one report to the host using this format:
+//
+//    bytes 0:1 = 0x8800.  This has the bit pattern 10001 in the high
+//                5 bits, which distinguishes it from regular joystick
+//                reports and from exposure status reports.
+//    bytes 2:3 = total number of outputs, little endian
+//    bytes 4:5 = plunger calibration zero point, little endian
+//    bytes 6:7 = plunger calibration maximum point, little endian
+//    remaining bytes = reserved for future use; set to 0 in current version
+//
+//
+// WHY WE USE THIS HACKY APPROACH TO DIFFERENT REPORT TYPES
+//
+// The HID report system was specifically designed to provide a clean,
+// structured way for devices to describe the data they send to the host.
+// Our approach isn't clean or structured; it ignores the promises we
+// make about the contents of our report via the HID Report Descriptor
+// and stuffs our own different data format into the same structure.
+//
+// We use this hacky approach only because we can't use the official 
+// mechanism, due to the constraint that we want to emulate the LedWiz.
+// The right way to send different report types is to declare different
+// report types via extra HID Report Descriptors, then send each report
+// using one of the types we declared.  If it weren't for the LedWiz
+// constraint, we'd simply define the pixel dump and config query reports
+// as their own separate HID Report types, each consisting of opaque
+// blocks of bytes.  But we can't do this.  The snag is that some versions
+// of the LedWiz Windows host software parse the USB HID descriptors as part
+// of identifying a device as a valid LedWiz unit, and will only recognize
+// the device if it matches certain particulars about the descriptor
+// structure of a real LedWiz.  One of the features that's important to
+// some versions of the software is the descriptor link structure, which
+// is affected by the layout of HID Report Descriptor entries.  In order
+// to match the expected layout, we can only define a single kind of output
+// report.  Since we have to use Joystick reports for the sake of VP and
+// other pinball software, and we're only allowed the one report type, we
+// have to make that one report type the Joystick type.  That's why we
+// overload the joystick reports with other meanings.  It's a hack, but
+// at least it's a fairly reliable and isolated hack, iun that our special 
+// reports are only generated when clients specifically ask for them.
+// Plus, even if a client who doesn't ask for a special report somehow 
+// gets one, the worst that happens is that they get a momentary spurious
+// reading from the accelerometer and plunger.
+
+
+
+// ------- INCOMING MESSAGES (HOST TO DEVICE) -------
+//
+// For LedWiz compatibility, our incoming message format conforms to the
+// basic USB format used by real LedWiz units.  This is simply 8 data
+// bytes, all private vendor-specific values (meaning that the Windows HID
+// driver treats them as opaque and doesn't attempt to parse them).
+//
+// Within this basic 8-byte format, we recognize the full protocol used
+// by real LedWiz units, plus an extended protocol that we define privately.
+// The LedWiz protocol leaves a large part of the potential protocol space 
+// undefined, so we take advantage of this undefined region for our 
+// extensions.  This ensures that we can properly recognize all messages 
+// intended for a real LedWiz unit, as well as messages from custom host 
+// software that knows it's talking to a Pinscape unit.
+
+// --- REAL LED WIZ MESSAGES ---
+//
+// The real LedWiz protocol has two message types, identified by the first
+// byte of the 8-byte USB packet:
+//
+// 64              -> SBA (64 xx xx xx xx ss uu uu)
+//                    xx = on/off bit mask for 8 outputs
+//                    ss = global flash speed setting (1-7)
+//                    uu = unused
+//
+// If the first byte has value 64 (0x40), it's an SBA message.  This type of 
+// message sets all 32 outputs individually ON or OFF according to the next 
+// 32 bits (4 bytes) of the message, and sets the flash speed to the value in 
+// the sixth byte.  (The flash speed sets the global cycle rate for flashing
+// outputs - outputs with their values set to the range 128-132 - to a   
+// relative speed, scaled linearly in frequency.  1 is the slowest at about 
+// 2 Hz, 7 is the fastest at about 14 Hz.)
+//
+// 0-49 or 128-132 -> PBA (bb bb bb bb bb bb bb bb)
+//                    bb = brightness level/flash pattern for one output
+//
+// If the first byte is any valid brightness setting, it's a PBA message.
+// Valid brightness settings are:
+//
+//     0-48 = fixed brightness level, linearly from 0% to 100% intensity
+//     49   = fixed brightness level at 100% intensity (same as 48)
+//     129  = flashing pattern, fade up / fade down (sawtooth wave)
+//     130  = flashing pattern, on / off (square wave)
+//     131  = flashing pattern, on for 50% duty cycle / fade down
+//     132  = flashing pattern, fade up / on for 50% duty cycle
+//     
+// A PBA message sets 8 outputs out of 32.  Which 8 are to be set is 
+// implicit in the message sequence: the first PBA sets outputs 1-8, the 
+// second sets 9-16, and so on, rolling around after each fourth PBA.  
+// An SBA also resets the implicit "bank" for the next PBA to outputs 1-8.
+//
+// Note that there's no special first byte to indicate the PBA message
+// type, as there is in an SBA.  The first byte of a PBA is simply the
+// first output setting.  The way the LedWiz creators conceived this, the 
+// SBA distinguishable from a PBA because 64 isn't a valid output setting, 
+// hence a message that starts with a byte value of 64 isn't a valid PBA 
+// message.
+//
+// Our extended protocol uses the same principle, taking advantage of the
+// other byte value ranges that are invalid in PBA messages.  To be a valid
+// PBA message, the first byte must be in the range 0-49 or 129-132.  As
+// already mentioned, byte value 64 indicates an SBA message.  This leaves
+// these ranges available for other uses: 50-63, 65-128, and 133-255.
+
+
+// --- PRIVATE EXTENDED MESSAGES ---
+//
+// All of our extended protocol messages are identified by the first byte:
+//
+// 65  -> Miscellaneous control message.  The second byte specifies the specific
+//        operation:
+//
+//        1 -> Set device unit number and plunger status, and save the changes immediately
+//             to flash.  The device will automatically reboot after the changes are saved.
+//             The additional bytes of the message give the parameters:
+//
+//               third byte = new unit number (0-15, corresponding to nominal unit numbers 1-16)
+//               fourth byte = plunger on/off (0=disabled, 1=enabled)
+//
+//        2 -> Begin plunger calibration mode.  The device stays in this mode for about
+//             15 seconds, and sets the zero point and maximum retraction points to the
+//             observed endpoints of sensor readings while the mode is running.  After
+//             the time limit elapses, the device automatically stores the results in
+//             non-volatile flash memory and exits the mode.
+//
+//        3 -> Send pixel dump.  The plunger sensor object sends a series of the special 
+//             pixel dump reports, defined in USBJoystick.cpp; the device automatically
+//             resumes normal joystick messages after sending all pixels.  If the 
+//             plunger sensor isn't an image sensor type, no pixel messages are sent.
+//
+//        4 -> Query configuration.  The device sends a special configuration report,
+//             defined in USBJoystick.cpp, then resumes sending normal joystick reports.
+//
+//        5 -> Turn all outputs off and restore LedWiz defaults.  Sets output ports
+//             1-32 to OFF and LedWiz brightness/mode setting 48, sets outputs 33 and
+//             higher to brightness level 0, and sets the LedWiz global flash speed to 2.
+//
+//        6 -> Save configuration to flash.  This saves all variable updates sent via
+//             type 66 messages since the last reboot, then automatically reboots the
+//             device to put the changes into effect.
+//
+// 66  -> Set configuration variable.  The second byte of the message is the config
+//        variable number, and the remaining bytes give the new value for the variable.
+//        The value format is specific to each variable; see the list below for details.
+//        This message only sets the value in RAM - it doesn't write the value to flash
+//        and doesn't put the change into effect immediately.  To put updates into effect,
+//        the host must send a type 65 subtype 6 message (see above), which saves updates
+//        to flash and reboots the device.
+//
+// 200-228 -> Set extended output brightness.  This sets outputs N to N+6 to the
+//        respective brightness values in the 2nd through 8th bytes of the message
+//        (output N is set to the 2nd byte value, N+1 is set to the 3rd byte value, 
+//        etc).  Each brightness level is a linear brightness level from 0-255,
+//        where 0 is 0% brightness and 255 is 100% brightness.  N is calculated as
+//        (first byte - 200)*7 + 1:
+//
+//               200 = outputs 1-7
+//               201 = outputs 8-14
+//               202 = outputs 15-21
+//               ...
+//               228 = outputs 197-203
+//
+//        This message is the only way to address ports 33 and higher, since standard
+//        LedWiz messages are inherently limited to ports 1-32.
+//
+//        Note that these extended output messages differ from regular LedWiz settings
+//        in two ways.  First, the brightness is the ONLY attribute when an output is
+//        set using this mode - there's no separate ON/OFF setting per output as there 
+//        is with the SBA/PBA messages.  To turn an output OFF with this message, set
+//        the intensity to 0.  Setting a non-zero intensity turns it on immediately
+//        without regard to the SBA status for the port.  Second, the brightness is
+//        on a full 8-bit scale (0-255) rather than the LedWiz's approximately 5-bit
+//        scale, because there are no parts of the range reserved for flashing modes.
+//
+//        Outputs 1-32 can be controlled by EITHER the regular LedWiz SBA/PBA messages
+//        or by the extended messages.  The latest setting for a given port takes
+//        precedence.  If an SBA/PBA message was the last thing sent to a port, the
+//        normal LedWiz combination of ON/OFF and brightness/flash mode status is used
+//        to determine the port's physical output setting.  If an extended brightness
+//        message was the last thing sent to a port, the LedWiz ON/OFF status and
+//        flash modes are ignored, and the fixed brightness is set.  Outputs 33 and
+//        higher inherently can't be addressed or affected by SBA/PBA messages.
+
+
+// ------- CONFIGURATION VARIABLES -------
+//
+// Message type 66 (see above) sets one configuration variable.  The second byte
+// of the message is the variable ID, and the rest of the bytes give the new
+// value, in a variable-specific format.  16-bit values are little endian.
+//
+// 1  -> USB device ID.  Bytes 3-4 give the 16-bit USB Vendor ID; bytes
+//       5-6 give the 16-bit USB Product ID.  For LedWiz emulation, use
+//       vendor 0xFAFA and product 0x00EF + unit# (where unit# is the
+//       nominal LedWiz unit number, from 1 to 16).  If LedWiz emulation
+//       isn't desired or causes host conflicts, you can use our private
+//       ID assigned by http://pid.codes (a registry for open-source USB
+//       devices) of vendor 0x1209 and product 0xEAEA.  (You can also use
+//       any other values that don't cause a conflict on your PC, but we
+//       recommend using one of these pre-assigned values if possible.)
+//
+// 2  -> Pinscape Controller unit number for DOF.  Byte 3 is the new
+//       unit number, from 1 to 16.
+//
+// 3  -> Enable/disable joystick reports.  Byte 2 is 1 to enable, 0 to
+//       disable.  When disabled, the device registers as a generic HID 
+/        device, and only sends the private report types used by the
+//       Windows config tool.
+//
+// 4  -> Accelerometer orientation.  Byte 3 is the new setting:
+//        
+//        0 = ports at front (USB ports pointing towards front of cabinet)
+//        1 = ports at left
+//        2 = ports at right
+//        3 = ports at rear
+//
+// 5  -> Plunger sensor type.  Byte 3 is the type ID:
+//
+//         0 = none (disabled)
+//         1 = TSL1410R linear image sensor, 1280x1 pixels, serial mode
+//         2 = TSL1410R, parallel mode
+//         3 = TSL1412R linear image sensor, 1536x1 pixels, serial mode
+//         4 = TSL1412R, parallel mode
+//         5 = Potentiometer with linear taper, or any other device that
+//             represents the position reading with a single analog voltage
+//         6 = AEDR8300 optical quadrature sensor, 75lpi
+//         7 = AS5304 magnetic quadrature sensor, 160 steps per 2mm
+//
+// 6  -> Plunger pin assignments.  Bytes 3-6 give the pin assignments for
+//       pins 1, 2, 3, and 4.  These use the Pin Number Mappings listed
+//       below.  The meaning of each pin depends on the plunger type:
+//
+//         TSL1410R/1412R, serial:    SI (DigitalOut), CLK (DigitalOut), AO (AnalogIn),  NC
+//         TSL1410R/1412R, parallel:  SI (DigitalOut), CLK (DigitalOut), AO1 (AnalogIn), AO2 (AnalogIn)
+//         Potentiometer:             AO (AnalogIn),   NC,               NC,             NC
+//         AEDR8300:                  A (InterruptIn), B (InterruptIn),  NC,             NC
+//         AS5304:                    A (InterruptIn), B (InterruptIn),  NC,             NC
+//
+// 7  -> Plunger calibration button pin assignments.  Byte 3 is the DigitalIn
+//       pin for the button switch; byte 4 is the DigitalOut pin for the indicator
+//       lamp.  Either can be set to NC to disable the function.  (Use the Pin
+//       Number Mappins listed below for both bytes.)
+//
+// 8  -> ZB Launch Ball setup.  This configures the ZB Launch Ball feature.  Byte
+//       3 is the LedWiz port number (1-255) mapped to the "ZB Launch Ball" output
+//       in DOF.  Set the port to 0 to disable the feature.  Byte 4 is the button
+//       number (1-32) that we'll "press" when the feature is activated.  Bytes 5-6
+//       give the "push distance" for activating the button by pushing forward on
+//       the plunger knob, in .001 inch increments (e.g., 80 represents 0.08", which
+//       is the recommended setting).
+//
+// 9  -> TV ON relay setup.  This requires external circuitry implemented on the
+//       Expansion Board (or an equivalent circuit as described in the Build Guide).
+//       Byte 3 is the GPIO DigitalIn pin for the "power status" input, using the 
+//       Pin Number Mappings below.  Byte 4 is the DigitalOut pin for the "latch"
+//       output.  Byte 5 is the DigitalOut pin for the relay trigger.  Bytes 6-7
+//       give the delay time in 10ms increments as an unsigned 16-bit value (e.g.,
+//       550 represents 5.5 seconds).  
+//
+// 10 -> TLC5940NT setup.  This chip is an external PWM controller, with 32 outputs
+//       per chip and a serial data interface that allows the chips to be daisy-
+//       chained.  We can use these chips to add an arbitrary number of PWM output 
+//       ports for the LedWiz emulation.  Set the number of chips to 0 to disable
+//       the feature.  The bytes of the message are:
+//          byte 3 = number of chips attached (connected in daisy chain)
+//          byte 4 = SIN pin - Serial data (must connect to SPIO MOSI -> PTC6 or PTD2)
+//          byte 5 = SCLK pin - Serial clock (must connect to SPIO SCLK -> PTC5 or PTD1)
+//          byte 6 = XLAT pin - XLAT (latch) signal (any GPIO pin)
+//          byte 7 = BLANK pin - BLANK signal (any GPIO pin)
+//          byte 8 = GSCLK pin - Grayscale clock signal (must be a PWM-out capable pin)
+//
+// 11 -> 74HC595 setup.  This chip is an external shift register, with 8 outputs per
+//       chip and a serial data interface that allows daisy-chaining.  We use this
+//       chips to add extra digital outputs for the LedWiz emulation.  In particular,
+//       the Chime Board (part of the Expansion Board suite) uses these to add timer-
+//       protected outputs for coil devices (knockers, chimes, bells, etc).  Set the
+//       number of chips to 0 to disable the feature.  The message bytes are:
+//          byte 3 = number of chips attached (connected in daisy chain)
+//          byte 4 = SIN pin - Serial data (any GPIO pin)
+//          byte 5 = SCLK pin - Serial clock (any GPIO pin)
+//          byte 6 = LATCH pin - LATCH signal (any GPIO pin)
+//          byte 7 = ENA pin - ENABLE signal (any GPIO pin)
+//
+// 12 -> Input button setup.  This sets up one button; it can be repeated for each
+//       button to be configured.  There are 32 button slots, numbered 1-32.  Each
+//       key can be configured as a joystick button, a regular keyboard key, a
+//       keyboard modifier key (such as Shift, Ctrl, or Alt), or a media control
+//       key (such as volume up/down).
+//
+//       The bytes of the message are:
+//          byte 3 = Button number (1-32)
+//          byte 4 = GPIO pin to read for button input
+//          byte 5 = key type reported to PC when button is pushed:
+//                    1 = joystick button -> byte 6 is the button number, 1-32
+//                    2 = regular keyboard key -> byte 6 is the USB key code (see below)
+//                    3 = keyboard modifier key -> byte 6 is the USB modifier code (see below)
+//                    4 = media control key -> byte 6 is the USB key code (see below)
+//          byte 6 = key code, which depends on the key type in byte 5
+//          
+// 13 -> LedWiz output port setup.  This sets up one output port; it can be repeated
+//       for each port to be configured.  There are 203 possible slots for output ports, 
+//       numbered 1 to 203.  The number of ports visible to the host is determined by
+//       the first DISABLED port (type 0).  For example, if ports 1-32 are set as GPIO
+//       outputs and port 33 is disabled, the host will see 32 ports, regardless of
+//       the settings for post 34 and higher.
+//
+//       The bytes of the message are:
+//         byte 3 = LedWiz port number (1 to maximum number or ports)
+//         byte 4 = physical output type:
+//                   0 = Disabled.  This output isn't used, and isn't visible to the
+//                       LedWiz/DOF software on the host.  The FIRST disabled port
+//                       determines the number of ports visible to the host - ALL ports
+//                       after the first disabled port are also implicitly disabled.
+//                   1 = GPIO PWM output: connected to GPIO pin specified in byte 5,
+//                       operating in PWM mode.  Note that only a subset of KL25Z GPIO
+//                       ports are PWM-capable.
+//                   2 = GPIO Digital output: connected to GPIO pin specified in byte 5,
+//                       operating in digital mode.  Digital ports can only be set ON
+//                       or OFF, with no brightness/intensity control.  All pins can be
+//                       used in this mode.
+//                   3 = TLC5940 port: connected to TLC5940 output port number specified 
+//                       in byte 5.  Ports are numbered sequentially starting from port 0
+//                       for the first output (OUT0) on the first chip in the daisy chain.
+//                   4 = 74HC595 port: connected to 74HC595 output port specified in byte 5.
+//                       As with the TLC5940 outputs, ports are numbered sequentially from 0
+//                       for the first output on the first chip in the daisy chain.
+//                   5 = Virtual output: this output port exists for the purposes of the
+//                       LedWiz/DOF software on the host, but isn't physically connected
+//                       to any output device.  This can be used to create a virtual output
+//                       for the DOF ZB Launch Ball signal, for example, or simply as a
+//                       placeholder in the LedWiz port numbering.  The physical output ID 
+//                       (byte 5) is ignored for this port type.
+//         byte 5 = physical output ID, interpreted according to the value in byte 4
+//         byte 6 = flags: a combination of these bit values:
+//                   1 = active-high output (0V on output turns attached device ON)
+
+
+// --- PIN NUMBER MAPPINGS ---
+//
+// In USB messages that specify GPIO pin assignments, pins are identified with
+// our own private numbering scheme.  Our numbering scheme only includes the 
+// ports connected to external header pins on the KL25Z board, so this is only
+// a sparse subset of the full GPIO port set.  These are numbered in order of
+// pin name.  The special value 0 = NC = Not Connected can be used where
+// appropriate to indicate a disabled or unused pin.
+//
+//     0 = NC (not connected)
+//     1 = PTA1
+//     2 = PTA2
+//     3 = PTA4
+//     4 = PTA5
+//     5 = PTA12
+//     6 = PTA13
+//     7 = PTA16
+//     8 = PTA17
+//     9 = PTB0
+//    10 = PTB1
+//    11 = PTB2
+//    12 = PTB3
+//    13 = PTB8
+//    14 = PTB9
+//    15 = PTB10
+//    16 = PTB11
+//    17 = PTC0
+//    18 = PTC1
+//    19 = PTC2
+//    20 = PTC3
+//    21 = PTC4
+//    22 = PTC5
+//    23 = PTC6
+//    24 = PTC7
+//    25 = PTC8
+//    26 = PTC9
+//    27 = PTC10
+//    28 = PTC11
+//    29 = PTC12
+//    30 = PTC13
+//    31 = PTC16
+//    32 = PTC17
+//    33 = PTD0
+//    34 = PTD1
+//    35 = PTD2
+//    36 = PTD3
+//    37 = PTD4
+//    38 = PTD5
+//    39 = PTD6
+//    40 = PTD7
+//    41 = PTE0
+//    42 = PTE1
+//    43 = PTE2
+//    44 = PTE3
+//    45 = PTE4
+//    46 = PTE5
+//    47 = PTE20
+//    48 = PTE21
+//    49 = PTE22
+//    50 = PTE23
+//    51 = PTE29
+//    52 = PTE30
+//    53 = PTE31
+
+
+// --- USB KEYBOARD SCAN CODES ---
+//
+// Use the standard USB HID keyboard codes for regular keys.  See the
+// HID Usage Tables in the official USB specifications for a full list.
+// Here are the most common codes for quick references:
+//
+//    A-Z              -> 4-29
+//    top row numbers  -> 30-39
+//    Return           -> 40
+//    Escape           -> 41
+//    Backspace        -> 42
+//    Tab              -> 43
+//    Spacebar         -> 44
+//    -_               -> 45
+//    =+               -> 46
+//    [{               -> 47
+//    ]}               -> 48
+//    \|               -> 49
+//    ;:               -> 51
+//    '"               -> 52
+//    `~               -> 53
+//    ,<               -> 54
+//    .>               -> 55
+//    /?               -> 56
+//    Caps Lock        -> 57
+//    F1-F12           -> 58-69
+//    F13-F24          -> 104-115
+//    Print Screen     -> 70
+//    Scroll Lock      -> 71
+//    Pause            -> 72
+//    Insert           -> 73
+//    Home             -> 74
+//    Page Up          -> 75
+//    Del              -> 76
+//    End              -> 77
+//    Page Down        -> 78
+//    Right Arrow      -> 79
+//    Left Arrow       -> 80
+//    Down Arrow       -> 81
+//    Up Arrow         -> 82
+//    Num Lock/Clear   -> 83
+//    Keypad / * - +   -> 84 85 86 87
+//    Keypad Enter     -> 88
+//    Keypad 1-9       -> 89-97
+//    Keypad 0         -> 98
+//    Keypad .         -> 99
+//  
+
+
+// --- USB KEYBOARD MODIFIER KEY CODES ---
+//
+// Use these codes for modifier keys in the button mappings
+//
+//    0x01 = Left Control
+//    0x02 = Left Shift
+//    0x04 = Left Alt
+//    0x08 = Left GUI ("Windows" key)
+//    0x10 = Right Control
+//    0x20 = Right Shift
+//    0x40 = Right Alt
+//    0x80 = Right GUI ("Windows" key)
+
+
+// --- USB KEYBOARD MEDIA KEY CODES ---
+//
+// Use these for media control keys in the button mappings
+//
+//    0x01 = Volume Up
+//    0x02 = Volume Down
+//    0x04 = Mute on/off
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Updates.h	Sat Dec 19 06:37:19 2015 +0000
@@ -0,0 +1,32 @@
+// UPDATES
+//
+// This is a record of new features and changes in recent versions.
+//
+
+// January 2016
+//
+// Dynamic configuration:  all configuration options are now handled dynamically,
+// through the Windows config tool.  In earlier versions, most configuration options
+// were set through compile-time constants, which made it necessary for everyone
+// who wanted to customize anything to create a private branched version of the
+// source repository, edit the source code, and compile their own binary.  This
+// was cumbersome, and required way too much technical knowledge to be worth the
+// trouble to a lot of people.  The goal of the new approach is that everyone can
+// use the same standard binary build, and set options from the Windows tool.
+//
+// TSL1410R and 1412R parallel mode support:  these sensors are physically built
+// out of two separate pixel arrays, which can be read independently.  Past
+// versions only supported "serial" mode pixel transfer, where we read all of 
+// the first array's pixels before reading any of the second array's pixels.
+// In parallel mode, we can read pixels from both arrays at the same time.  The
+// limiting factor in image read speed is the amount of time it takes for the
+// ADC to transfer charge from a pixel and stabilize on a reading.  The KL25Z
+// has multiple ADC hardware channels, so we can read multiple analog values
+// concurrently - it takes the same amount of time for one ADC reading to
+// stabilize as two readings.  So by reading from the two sensor sections 
+// concurrently, we can essentially double the transfer speed.  Faster pixel
+// transfer allows for more accurate motion tracking when the plunger is
+// moving at high speed, allowing for more realistic plunger action on the
+// virtual side.
+//
+// 
--- a/ccdSensor.h	Thu Dec 03 07:34:57 2015 +0000
+++ b/ccdSensor.h	Sat Dec 19 06:37:19 2015 +0000
@@ -1,24 +1,40 @@
 // CCD plunger sensor
 //
-// This file implements our generic plunger sensor interface for the 
-// TAOS TSL1410R CCD array sensor.
+// This class implements our generic plunger sensor interface for the 
+// TAOS TSL1410R and TSL1412R linear sensor arrays.  Physically, these
+// sensors are installed with their image window running parallel to
+// the plunger rod, spanning the travel range of the plunger tip.
+// A light source is positioned on the opposite side of the rod, so
+// that the rod casts a shadow on the sensor.  We sense the position
+// by looking for the edge of the shadow.
+//
+// These sensors can take an image quickly, but it takes a significant
+// amount of time to transfer the image data from the sensor to the 
+// microcontroller, since each pixel's analog voltage level must be
+// sampled serially.  It takes about 20us to sample a pixel accurately.
+// The TSL1410R has 1280 pixels, and the 1412R has 1536.  Sampling 
+// every pixel would thus take about 25ms or 30ms respectively.
+// This is too slow for a responsive feel in the UI, and much too
+// slow to track the plunger release motion in real time.  To improve
+// on the read speed, we only sample a subset of pixels for each
+// reading - for higher speed at the expense of spatial resolution.
+// The sensor's native resolution is much higher than we need, so
+// this is a perfectly equitable trade.
 
+#include "plunger.h"
 
 
-// Number of pixels we read from the CCD on each frame.  Use the
-// sample size from config.h.
-const int npix = CCD_NPIXELS_SAMPLED;
-
 // PlungerSensor interface implementation for the CCD
-class PlungerSensor
+class PlungerSensorCCD: public PlungerSensor
 {
 public:
-    PlungerSensor() : ccd(CCD_SO_PIN)
+    PlungerSensorCCD(int nPix, PinName si, PinName clock, PinName ao1, PinName ao2) 
+        : ccd(nPix, si, clock, ao1, ao2)
     {
     }
     
     // initialize
-    void init()
+    virtual void init()
     {
         // flush any random power-on values from the CCD's integration
         // capacitors, and start the first integration cycle
@@ -26,9 +42,8 @@
     }
     
     // Perform a low-res scan of the sensor.  
-    int lowResScan()
+    virtual bool lowResScan(int &pos)
     {
-
         // read the pixels at low resolution
         const int nlpix = 32;
         uint16_t pix[nlpix];
@@ -53,17 +68,17 @@
             {
                 // got it - normalize it to normal 'npix' resolution and
                 // return the result
-                return n*npix/nlpix;
+                pos = n*npix/nlpix;
+                return true;
             }
         }
         
-        // didn't find a shadow - assume the whole array is in shadow (so
-        // the edge is at the zero pixel point)
-        return 0;
+        // didn't find a shadow - return failure
+        return false;
     }
 
     // Perform a high-res scan of the sensor.
-    bool highResScan(int &pos)
+    virtual bool highResScan(int &pos)
     {
         // read the array
         ccd.read(pix, npix);
@@ -127,7 +142,7 @@
     }
     
     // send an exposure report to the joystick interface
-    void sendExposureReport(USBJoystick &js)
+    virtual void sendExposureReport(USBJoystick &js)
     {
         // send reports for all pixels
         int idx = 0;
@@ -147,10 +162,44 @@
         ccd.read(pix, npix);
     }
     
-private:
+protected:
     // pixel buffer
-    uint16_t pix[npix];
+    uint16_t *pix;
     
     // the low-level interface to the CCD hardware
-    TSL1410R<CCD_SI_PIN, CCD_CLOCK_PIN> ccd;
+    TSL1410R ccd;
 };
+
+
+// TSL1410R sensor 
+class PlungerSensorTSL1410R: public PlungerSensorCCD
+{
+public:
+    PlungerSensorTSL1410R(PinName si, PinName clock, PinName ao1, PinName ao2) 
+        : PlungerSensorCCD(1280, si, clock, ao1, ao2)
+    {
+        // This sensor is 1x1280 pixels at 400dpi.  Sample every 8th
+        // pixel -> 160 pixels at 50dpi == 0.5mm spatial resolution.
+        npix = 160;
+        pix = pixbuf;
+    }
+    
+    uint16_t pixbuf[160];
+};
+
+// TSL1412R
+class PlungerSensorTSL1412R: public PlungerSensorCCD
+{
+public:
+    PlungerSensorTSL1412R(PinName si, PinName clock, PinName ao1, PinName ao2)
+        : PlungerSensorCCD(1536, si, clock, ao1, ao2)
+    {
+        // This sensor is 1x1536 pixels at 400dpi.  Sample every 8th
+        // pixel -> 192 pixels at 50dpi == 0.5mm spatial resolution.
+        npix = 192;
+        pix = pixbuf;
+    }
+    
+    uint16_t pixbuf[192];
+};
+
--- a/config.h	Thu Dec 03 07:34:57 2015 +0000
+++ b/config.h	Sat Dec 19 06:37:19 2015 +0000
@@ -1,916 +1,346 @@
 // Pinscape Controller Configuration
 //
-// To customize your private configuration, simply open this file in the 
-// mbed on-line IDE, make your changes, save the file, and click the Compile
-// button at the top of the window.  That will generate a customized .bin
-// file that you can download onto your KL25Z board.
+// New for 2016:  dynamic configuration!  To configure the controller, connect
+// the KL25Z to your PC, install the .bin file, and run the Windows config tool.  
+// There's no need (as there was in the past) to edit this file or to compile a 
+// custom version of the binary (.bin) to customize setup options.
+//
+// In earlier versions, configuration was largely handled with compile-time
+// constants.  To customize the setup, you had to create a private forked copy
+// of the source code, edit the constants defined in config.h, and compile a
+// custom binary.  That's no longer necessary!
+//
+// The new approach is to do everything (or as much as possible, anyway)
+// via the Windows config tool.  You shouldn't have to recompile a custom
+// version just to make a configurable change.  Of course, you're still free
+// to create a custom version if you need to add or change features in ways
+// that weren't anticipated in the original design. 
+//
+
 
 #ifndef CONFIG_H
 #define CONFIG_H
 
-// ---------------------------------------------------------------------------
-//
-// Expansion Board.  If you're using the expansion board, un-comment the
-// line below.  This will select all of the correct defaults for the board.
-//
-// The expansion board settings are mostly automatic, so you shouldn't have
-// to change much else.  However, you should still look at and adjust the
-// following as needed:
-//    - TV power on delay time
-//    - Plunger sensor settings, if you're using a plunger
-//
-//#define EXPANSION_BOARD
 
-
-// --------------------------------------------------------------------------
-//
-// Enable/disable joystick functions.
-//
-// This controls whether or not we send joystick reports to the PC with the 
-// plunger and accelerometer readings.  By default, this is enabled.   If
-// you want to use two or more physical KL25Z Pinscape controllers in your
-// system (e.g., if you want to increase the number of output ports
-// available by using two or more KL25Z's), you should disable the joystick
-// features on the second (and third+) controller.  It's not useful to have
-// more than one board reporting the accelerometer readings to the host -
-// doing so will just add USB overhead.  This setting lets you turn off the
-// reports for the secondary controllers, turning the secondary boards into
-// output-only devices.
-//
-// Note that you can't use button inputs on a controller that has the
-// joystick features disabled, because the buttons are handled via the
-// joystick reports.  Wire all of your buttons to the primary KL25Z that
-// has the joystick features enabled.
-//
-// To disable the joystick features, just comment out the next line (add
-// two slashes at the beginning of the line).
-//
-#define ENABLE_JOYSTICK
-
+// Plunger type codes
+// NOTE!  These values are part of the external USB interface.  New
+// values can be added, but the meaning of an existing assigned number 
+// should remain fixed to keep the PC-side config tool compatible across 
+// versions.
+const int PlungerType_None      = 0;     // no plunger
+const int PlungerType_TSL1410RS = 1;     // TSL1410R linear image sensor (1280x1 pixels, 400dpi), serial mode
+const int PlungerType_TSL1410RP = 2;     // TSL1410R, parallel mode (reads the two sensor sections concurrently)
+const int PlungerType_TSL1412RS = 3;     // TSL1412R linear image sensor (1536x1 pixels, 400dpi), serial mode
+const int PlungerType_TSL1412RP = 4;     // TSL1412R, parallel mode
+const int PlungerType_Pot       = 5;     // potentionmeter
+const int PlungerType_OptQuad   = 6;     // AEDR8300 optical quadrature sensor
+const int PlungerType_MagQuad   = 7;     // AS5304 magnetic quadrature sensor
 
-// ---------------------------------------------------------------------------
-//
-// USB device vendor ID and product ID.  These values identify the device 
-// to the host software on the PC.  By default, we use the same settings as
-// a real LedWiz so that host software will recognize us as an LedWiz.
-//
-// The standard settings *should* work without conflicts, even if you have 
-// a real LedWiz.  My reference system is 64-bit Windows 7 with a real LedWiz 
-// on unit #1 and a Pinscape controller on unit #8 (the default), and the 
-// two coexist happily in my system.  The LedWiz is designed specifically 
-// to allow multiple units in one system, using the unit number value 
-// (see below) to distinguish multiple units, so there should be no conflict
-// between Pinscape and any real LedWiz devices you have.
-//
-// However, even though conflicts *shouldn't* happen, I've had one report
-// from a user who experienced a Windows USB driver conflict that they could
-// only resolve by changing the vendor ID.  The real underlying cause is 
-// still a mystery, but whatever was going on, changing the vendor ID fixed 
-// it.  If you run into a similar problem, you can try the same fix as a
-// last resort.  Before doing that, though, you should try changing the 
-// Pinscape unit number first - it's possible that your real LedWiz is using 
-// unit #8, which is our default setting.
-//
-// If you must change the vendor ID for any reason, you'll sacrifice LedWiz
-// compatibility, which means that old programs like Future Pinball that use
-// the LedWiz interface directly won't be able to access the LedWiz output
-// controller features.  However, all is not lost.  All of the other functions
-// (plunger, nudge, and key input) use the joystick interface, which will 
-// work regardless of the ID values.  In addition, DOF R3 recognizes the
-// "emergency fallback" ID below, so if you use that, *all* functions
-// including the output controller will work in any DOF R3-enabled software,
-// including Visual Pinball and PinballX.  So the only loss will be that
-// old LedWiz-only software won't be able to control the outputs.
-//
-// The "emergency fallback" ID below is officially registerd with 
-// http://pid.codes, a registry for open-source USB projects, which should 
-// all but guarantee that this alternative ID shouldn't conflict with 
-// any other devices in your system.
+// Accelerometer orientation codes
+// These values are part of the external USB interface
+const int OrientationFront     = 0;      // USB ports pointed toward front of cabinet
+const int OrientationLeft      = 1;      // ports pointed toward left side of cabinet
+const int OrientationRight     = 2;      // ports pointed toward right side of cabinet
+const int OrientationRear      = 3;      // ports pointed toward back of cabinet
 
-
-// STANDARD ID SETTINGS.  These provide full, transparent LedWiz compatibility.
-const uint16_t USB_VENDOR_ID = 0xFAFA;      // LedWiz vendor ID = FAFA
-const uint16_t USB_PRODUCT_ID = 0x00F0;     // LedWiz start of product ID range = 00F0
-
+// input button types
+const int BtnTypeJoystick      = 1;      // joystick button
+const int BtnTypeKey           = 2;      // regular keyboard key
+const int BtnTypeModKey        = 3;      // keyboard modifier key (shift, ctrl, etc)
+const int BtnTypeMedia         = 4;      // media control key (volume up/down, etc)
 
-// EMERGENCY FALLBACK ID SETTINGS.  These settings are not LedWiz-compatible,
-// so older LedWiz-only software won't be able to access the output controller
-// features.  However, DOF R3 recognizes these IDs, so DOF-aware software (Visual 
-// Pinball, PinballX) will have full access to all features.
-//
-//const uint16_t USB_VENDOR_ID = 0x1209;   // DOF R3-compatible vendor ID = 1209
-//const uint16_t USB_PRODUCT_ID = 0xEAEA;  // DOF R3-compatible product ID = EAEA
-
+// maximum number of input button mappings
+const int MAX_BUTTONS = 32;
 
-// ---------------------------------------------------------------------------
-//
-// LedWiz unit number.
-//
-// Each LedWiz device has a unit number, from 1 to 16.  This lets you install
-// more than one LedWiz in your system: as long as each one has a different
-// unit number, the software on the PC can tell them apart and route commands 
-// to the right device.
-//
-// A real LedWiz has its unit number set at the factory.  If you don't tell
-// them otherwise when placing your order, they will set it to unit #1.  Most
-// real LedWiz units therefore are set to unit #1.  There's no provision on
-// a real LedWiz for users to change the unit number after it leaves the 
-// factory.
-//
-// For our *emulated* LedWiz, we default to unit #8 if we're the primary
-// Pinscape controller in the system, or unit #9 if we're set up as the
-// secondary controller with the joystick functions turned off.
-//
-// The reason we start at unit #8 is that we want to avoid conflicting with
-// any real LedWiz devices in your system.  Most real LedWiz devices are
-// set up as unit #1, and in the rare cases where people have two of them,
-// the second one is usually unit #2.  
-//
-// Note 1:  the unit number here is the *user visible* unit number that
-// you use on the PC side.  It's the number you specify in your DOF
-// configuration and so forth.  Internally, the USB reports subtract
-// one from this number - e.g., nominal unit #1 shows up as 0 in the USB
-// reports.  If you're trying to puzzle out why all of the USB reports
-// are all off by one from the unit number you select here, that's why.
-//
-// Note 2:  the DOF Configtool (google it) knows about the Pinscape 
-// controller.  There it's referred to as simply "KL25Z" rather than 
-// Pinscape Controller, but that's what they're talking about.  The DOF 
-// tool knows that it uses #8 as its default unit number, so it names the 
-// .ini file for this controller xxx8.ini.  If you change the unit number 
-// here, remember to rename the DOF-generated .ini file to match, by 
-// changing the "8" at the end of the filename to the new number you set 
-// here.
-const uint8_t DEFAULT_LEDWIZ_UNIT_NUMBER = 
-#ifdef ENABLE_JOYSTICK
-   0x08;   // joystick enabled - assume we're the primary KL25Z, so use unit #8
-#else
-   0x09;   // joystick disabled - assume we're a secondary, output-only KL25Z, so use #9
-#endif
+// LedWiz output port type codes
+// These values are part of the external USB interface
+const int PortTypeDisabled     = 0;      // port is disabled - not visible to LedWiz/DOF host
+const int PortTypeGPIOPWM      = 1;      // GPIO port, PWM enabled
+const int PortTypeGPIODig      = 2;      // GPIO port, digital out
+const int PortTypeTLC5940      = 3;      // TLC5940 port
+const int PortType74HC595      = 4;      // 74HC595 port
+const int PortTypeVirtual      = 5;      // Virtual port - visible to host software, but not connected to a physical output
 
+// LedWiz output port flag bits
+const uint8_t PortFlagActiveLow = 0x01;  // physical output is active-low
+
+// maximum number of output ports
+const int MAX_OUT_PORTS = 203;
 
-// --------------------------------------------------------------------------
-//
-// Accelerometer orientation.  The accelerometer feature lets Visual Pinball 
-// (and other pinball software) sense nudges to the cabinet, and simulate 
-// the effect on the ball's trajectory during play.  We report the direction
-// of the accelerometer readings as well as the strength, so it's important
-// for VP and the KL25Z to agree on the physical orientation of the
-// accelerometer relative to the cabinet.  The accelerometer on the KL25Z
-// is always mounted the same way on the board, but we still have to know
-// which way you mount the board in your cabinet.  We assume as default
-// orientation where the KL25Z is mounted flat on the bottom of your
-// cabinet with the USB ports pointing forward, toward the coin door.  If
-// it's more convenient for you to mount the board in a different direction,
-// you simply need to select the matching direction here.  Comment out the
-// ORIENTATION_PORTS_AT_FRONT line and un-comment the line that matches
-// your board's orientation.
-
-#define ORIENTATION_PORTS_AT_FRONT      // USB ports pointing toward front of cabinet
-// #define ORIENTATION_PORTS_AT_LEFT    // USB ports pointing toward left side of cab
-// #define ORIENTATION_PORTS_AT_RIGHT   // USB ports pointing toward right side of cab
-// #define ORIENTATION_PORTS_AT_REAR    // USB ports pointing toward back of cabinet
-
-
-
-// --------------------------------------------------------------------------
-//
-// Plunger CCD sensor.
-//
-// If you're NOT using the CCD sensor, comment out the next line (by adding
-// two slashes at the start of the line).
-
-#define ENABLE_CCD_SENSOR
-
-// Physical pixel count for your sensor.  This software has been tested with
-// TAOS TSL1410R (1280 pixels) and TSL1412R (1536 pixels) sensors.  It might
-// work with other similar sensors as well, but you'll probably have to make
-// some changes to the software interface to the sensor if you're using any
-// sensor outside of the TAOS TSL14xxR series.
-//
-// If you're not using a CCD sensor, you can ignore this.
-const int CCD_NPIXELS = 1280;
-
-// Number of pixels from the CCD to sample on each high-res scan.  We don't
-// sample every pixel from the sensor on each scan, because (a) we don't
-// have to, and (b) we don't want to.  We don't have to sample all of the
-// pixels because these sensors have much finer resolution than we need to
-// get good results.  On a typical pinball cabinet setup with a 1920x1080
-// HD TV display, the on-screen plunger travel distance is about 165 pixels,
-// so that's all the pixels we need to sample for pixel-accurate animation.
-// Even so, we still *could* sample at higher resolution, but we don't *want*
-// to sample more pixels than we have to,  because reading each pixel takes 
-// time.  The limiting factor for read speed is the sampling time for the ADC 
-// (analog to digital  converter); it needs about 20us per sample to get an 
-// accurate voltage reading.  We want to animate the on-screen plunger in 
-// real time, with minimal lag, so it's important that we complete each scan 
-// as quickly as possible.  The fewer pixels we sample, the faster we 
-// complete each scan.
-//
-// Happily, the time needed to read the approximately 165 pixels required
-// for pixel-accurate positioning on the display is short enough that we can
-// complete a scan within the cycle time for USB reports.  Visual Pinball
-// only polls for input at about 10ms intervals, so there's no benefit
-// to going much faster than this.  The sensor timing is such that we can
-// read about 165 pixels in well under 10ms.  So that's really the sweet
-// spot for our scans.
-//
-// Note that we distribute the sampled pixels evenly across the full range
-// of the sensor's pixels.  That is, we read every nth pixel, and skip the
-// ones in between.  That means that the sample count here has to be an even
-// divisor of the physical pixel count.  Empirically, reading every 8th
-// pixel gives us good results on both the TSL1410R and TSL1412R, so you
-// shouldn't need to change this if you're using one of those sensors.  If
-// you're using a different sensor, you should be sure to adjust this so that 
-// it works out to an integer result with no remainder.
-//
-const int CCD_NPIXELS_SAMPLED = CCD_NPIXELS / 8;
+struct Config
+{
+    // set all values to factory defaults
+    void setFactoryDefaults()
+    {
+        // By default, pretend to be LedWiz unit #8.  This can be from 1 to 16.  Real
+        // LedWiz units have their unit number set at the factory, and the vast majority
+        // are set up as unit #1, since that's the default for anyone who doesn't ask
+        // for a different setting.  It seems rare for anyone to use more than one unit
+        // in a pin cab, but for the few who do, the others will probably be numbered
+        // sequentially as #2, #3, etc.  It seems safe to assume that no one out there
+        // has a unit #8, so we'll use that as our default starting number.  This can
+        // be changed from the config tool, but for the sake of convenience we want the
+        // default to be a value that most people won't have to change.
+        usbVendorID = 0xFAFA;      // LedWiz vendor code
+        usbProductID = 0x00F7;     // LedWiz product code for unit #8
+        psUnitNo = 8;
+        
+        // enable joystick reports
+        joystickEnabled = true;
+        
+        // assume standard orientation, with USB ports toward front of cabinet
+        orientation = OrientationFront;
 
-// The KL25Z pins that the CCD sensor is physically attached to:
-//
-//  CCD_SI_PIN = the SI (sensor data input) pin
-//  CCD_CLOCK_PIN = the sensor clock pin
-//  CCD_SO_PIN = the SO (sensor data output) pin
-//
-// The SI an Clock pins are DigitalOut pins, so these can be set to just
-// about any gpio pins that aren't used for something else.  The SO pin must
-// be an AnalogIn capable pin - only a few of the KL25Z gpio pins qualify, 
-// so check the pinout diagram to find suitable candidates if you need to 
-// change this.  Note that some of the gpio pins shown in the mbed pinout
-// diagrams are committed to other uses by the mbed software or by the KL25Z
-// wiring itself, so if you do change these, be sure that the new pins you
-// select are really available.
-
-const PinName CCD_SI_PIN = PTE20;
-const PinName CCD_CLOCK_PIN = PTE21;
-const PinName CCD_SO_PIN = PTB0;
-
-// --------------------------------------------------------------------------
-//
-// Plunger potentiometer sensor.
-//
-// If you're using a potentiometer as the plunger sensor, un-comment the
-// next line (by removing the two slashes at the start of the line), and 
-// also comment out the ENABLE_CCD_SENSOR line above.
-
-//#define ENABLE_POT_SENSOR
-
-// The KL25Z pin that your potentiometer is attached to.  The potentiometer
-// requires wiring three connectins:
-//
-// - Wire the fixed resistance end of the potentiometer nearest the KNOB 
-//   end of the plunger to the 3.3V output from the KL25Z
-//
-// - Wire the other fixed resistance end to KL25Z Ground
-//
-// -  Wire the potentiometer wiper (the variable output terminal) to the 
-//    KL25Z pin identified below.  
-//
-// Note that you can change the pin selection below, but if you do, the new
-// pin must be AnalogIn capable.  Only a few of the KL25Z pins qualify.  Refer
-// to the KL25Z pinout diagram to find another AnalogIn pin if you need to
-// change this for any reason.  Note that the default is to use the same analog 
-// input that the CCD sensor would use if it were enabled, which is why you 
-// have to be sure to disable the CCD support in the software if you're using 
-// a potentiometer as the sensor.
-
-const PinName POT_PIN = PTB0;
-
-// --------------------------------------------------------------------------
-//
-// Plunger calibration button and indicator light.
-//
-// These specify the pin names of the plunger calibration button connections.
-// If you're not using these, you can set these to NC.  (You can even use the
-// button but not the LED; set the LED to NC if you're only using the button.)
-//
-// If you're using the button, wire one terminal of a momentary switch or
-// pushbutton to the input pin you select, and wire the other terminal to the 
-// KL25Z ground.  Push and hold the button for a few seconds to enter plunger 
-// calibration mode.
-// 
-// If you're using the LED, you'll need to build a little transistor power
-// booster circuit to power the LED, as described in the build guide.  The
-// LED gives you visual confirmation that the you've triggered calibration
-// mode and lets you know when the mode times out.  Note that the LED on
-// board the KL25Z also changes color to indicate the same information, so
-// if the KL25Z is positioned so that you can see it while you're doing the
-// calibration, you don't really need a separate button LED.  But the
-// separate LED is spiffy, especially if it's embedded in the pushbutton.
-//
-// Note that you can skip the pushbutton altogether and trigger calibration
-// from the Windows control software.  But again, the button is spiffier.
-
-// calibration button input 
-const PinName CAL_BUTTON_PIN = PTE29;
-
-// calibration button indicator LED
-const PinName CAL_BUTTON_LED = PTE23;
-
-
-// ---------------------------------------------------------------------------
-//
-// TV Power-On Timer.  This section lets you set up a delayed relay timer
-// for turning on your TV monitor(s) shortly after you turn on power to the
-// system.  This requires some external circuitry, which is built in to the
-// expansion board, or which you can build yourself - refer to the Build
-// Guide for the circuit plan.  
-//
-// If you're using this feature, un-comment the next line, and make any
-// changes to the port assignments below.  The default port assignments are
-// suitable for the expansion board.  Note that the TV timer is enabled
-// automatically if you're using the expansion board, since it's built in.
-//#define ENABLE_TV_TIMER
-
-#if defined(ENABLE_TV_TIMER) || defined(EXPANSION_BOARD)
-# define PSU2_STATUS_SENSE  PTD2    // Digital In pin to read latch status
-# define PSU2_STATUS_SET    PTE0    // Digital Out pin to set latch
-# define TV_RELAY_PIN       PTD3    // Digital Out pin to control TV switch relay
-
-// Amount of time (in seconds) to wait after system power-up before 
-// pulsing the TV ON switch relay.  Adjust as needed for your TV(s).
-// Most monitors won't respond to any buttons for the first few seconds
-// after they're plugged in, so we need to wait long enough to make sure
-// the TVs are ready to receive input before pressing the button.
-#define TV_DELAY_TIME    7.0
-
-#endif
-
+        // assume no plunger is attached
+        plunger.enabled = false;
+        plunger.sensorType = PlungerType_None;
+        
+        // assume that there's no calibration button
+        plunger.cal.btn = NC;
+        plunger.cal.led = NC;
+        
+        // clear the plunger calibration
+        plunger.cal.reset(4096);
+        
+        // disable the ZB Launch Ball by default
+        plunger.zbLaunchBall.port = 0;
+        plunger.zbLaunchBall.btn = 0;
+        
+        // assume no TV ON switch
+        TVON.statusPin = NC;
+        TVON.latchPin = NC;
+        TVON.relayPin = NC;
+        TVON.delayTime = 0;
+        
+        // assume no TLC5940 chips
+        tlc5940.nchips = 0;
+        
+        // assume no 74HC595 chips
+        hc595.nchips = 0;
+        
+        // initially configure with no LedWiz output ports
+        outPort[0].typ = PortTypeDisabled;
+        
+        // initially configure with no input buttons
+        for (int i = 0 ; i < MAX_BUTTONS ; ++i)
+            button[i].pin = 0;   // 0 == index of NC in USB-to-PinName mapping
+            
+        button[0].pin = 6; // PTA13
+        button[0].typ = BtnTypeKey;
+        button[0].val = 4;  // A
+        button[1].pin = 38; // PTD5
+        button[1].typ = BtnTypeJoystick;
+        button[1].val = 5;  // B
+        button[2].pin = 37; // PTD4
+        button[2].typ = BtnTypeModKey;
+        button[2].val = 0x02;  // left shift
+        button[3].pin = 5;  // PTA12
+        button[3].typ = BtnTypeMedia;
+        button[3].val = 0x01;  // volume up
+        button[4].pin = 3;  // PTA4
+        button[4].typ = BtnTypeMedia;
+        button[4].val = 0x02;  // volume down
+    }        
+    
+    // --- USB DEVICE CONFIGURATION ---
+    
+    // USB device identification - vendor ID and product ID.  For LedLWiz
+    // emulation, use vendor ID 0xFAFA and product ID 0x00EF + unit#, where
+    // unit# is the nominal LedWiz unit number from 1 to 16.  Alternatively,
+    // if LedWiz emulation isn't desired or causes any driver conflicts on
+    // the host, we have a private Pinscape assignment as vendor ID 0x1209 
+    // and product ID 0xEAEA (registered with http://pid.codes, a registry
+    // for open-source USB projects).
+    uint16_t usbVendorID;
+    uint16_t usbProductID;
+    
+    // Pinscape Controller unit number.  This is the nominal unit number,
+    // from 1 to 16.  We report this in the status query; DOF uses it to
+    // distinguish multiple Pinscape units.  Note that this doesn't affect 
+    // the LedWiz unit numbering, which is implied by the USB Product ID.
+    uint8_t psUnitNo;
+            
+    // Are joystick reports enabled?  Joystick reports can be turned off, to
+    // use the device as purely an output controller.
+    char joystickEnabled;
+    
+    
+    // --- ACCELEROMETER ---
+    
+    // accelerometer orientation (ORIENTATION_xxx value)
+    char orientation;
+    
+    
+    // --- PLUNGER CONFIGURATION ---
+    struct
+    {
+        // plunger enabled/disabled
+        char enabled;
 
-// --------------------------------------------------------------------------
-//
-// Pseudo "Launch Ball" button.
-//
-// Zeb of zebsboards.com came up with a clever scheme for his plunger kit
-// that lets the plunger simulate a Launch Ball button for tables where
-// the original used a Launch button instead of a plunger (e.g., Medieval 
-// Madness, T2, or Star Trek: The Next Generation).  The scheme uses an
-// LedWiz output to tell us when such a table is loaded.  On the DOF
-// Configtool site, this is called "ZB Launch Ball".  When this LedWiz
-// output is ON, it tells us that the table will ignore the analog plunger
-// because it doesn't have a plunger object, so the analog plunger should
-// send a Launch Ball button press signal when the user releases the plunger.
-// 
-// If you wish to use this feature, you need to do two things:
-//
-// First, adjust the two lines below to set the LedWiz output and joystick
-// button you wish to use for this feature.  The defaults below should be
-// fine for most people, but if you're using the Pinscape controller for
-// your physical button wiring, you should set the launch button to match
-// where you physically wired your actual Launch Ball button.  Likewise,
-// change the LedWiz port if you're using the one below for some actual
-// hardware output.  This is a virtual port that won't control any hardware;
-// it's just for signaling the plunger that we're in "button mode".  Note
-// that the numbering for the both the LedWiz port and joystick button 
-// start at 1 to match the DOF Configtool and VP dialog numbering.
-//
-// Second, in the DOF Configtool, make sure you have a Pinscape controller
-// in your cabinet configuration, then go to your Port Assignments and set
-// the port defined below to "ZB Launch Ball".
-//
-// Third, open the Visual Pinball editor, open the Preferences | Keys
-// dialog, and find the Plunger item.  Open the drop-down list under that
-// item and select the button number defined below.
-//
-// To disable this feature, just set ZBLaunchBallPort to 0 here.
-
-const int ZBLaunchBallPort = 32;
-const int LaunchBallButton = 24;
+        // plunger sensor type
+        char sensorType;
+    
+        // Plunger sensor pins.  To accommodate a wide range of sensor types,
+        // we keep a generic list of 4 pin assignments.  The use of each pin
+        // varies by sensor.  The lists below are in order of the generic
+        // pins; NC means that the pin isn't used by the sensor.  Each pin's
+        // GPIO usage is also listed.  Certain usages limit which physical
+        // pins can be assigned (e.g., AnalogIn or PwmOut).
+        //
+        // TSL1410R/1412R, serial:    SI (DigitalOut), CLK (DigitalOut), AO (AnalogIn),  NC
+        // TSL1410R/1412R, parallel:  SI (DigitalOut), CLK (DigitalOut), AO1 (AnalogIn), AO2 (AnalogIn)
+        // Potentiometer:             AO (AnalogIn),   NC,               NC,             NC
+        // AEDR8300:                  A (InterruptIn), B (InterruptIn),  NC,             NC
+        // AS5304:                    A (InterruptIn), B (InterruptIn),  NC,             NC
+        PinName sensorPin[4];
+        
+        // Pseudo LAUNCH BALL button.  
+        //
+        // This configures the "ZB Launch Ball" feature in DOF, based on Zeb's (of 
+        // zebsboards.com) scheme for using a mechanical plunger as a Launch button.
+        // Set the port to 0 to disable the feature.
+        //
+        // The port number is an LedWiz port number that we monitor for activation.
+        // This port isn't connected to a physical device; rather, the host turns it
+        // on to indicate that the pseudo Launch button mode is in effect.  
+        //
+        // The button number gives the button that we "press" when a launch occurs.
+        // This can be connected to the physical Launch button, or can simply be
+        // an otherwise unused button.
+        //
+        // The "push distance" is the distance, in inches, for registering a push
+        // on the plunger as a button push.  If the player pushes the plunger forward
+        // of the rest position by this amount, we'll treat it as pushing the button,
+        // even if the player didn't pull back the plunger first.  This lets the
+        // player treat the plunger knob as a button for games where it's meaningful
+        // to hold down the Launch button for specific intervals (e.g., "Championship 
+        // Pub").
+        struct
+        {
+            int port;
+            int btn;
+            float pushDistance;
+        
+        } zbLaunchBall;
+           
+        // --- PLUNGER CALIBRATION ---
+        struct
+        {
+            // has the plunger been calibrated?
+            int calibrated;
+        
+            // calibration button switch pin
+            PinName btn;
+        
+            // calibration button indicator light pin
+            PinName led;
+            
+            // 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 min;
+            int zero;
+            int max;
+    
+            // reset the plunger calibration
+            void reset(int npix)
+            {
+                calibrated = 0;          // not calibrated
+                min = 0;                 // assume we can go all the way forward...
+                max = npix;              // ...and all the way back
+                zero = npix/6;           // the rest position is usually around 1/2" back = 1/6 of total travel
+            }
 
-// Distance necessary to push the plunger to activate the simulated 
-// launch ball button, in inches.  A standard pinball plunger can be 
-// pushed forward about 1/2".  However, the barrel spring is very
-// stiff, and anything more than about 1/8" requires quite a bit
-// of force.  Ideally the force required should be about the same as 
-// for any ordinary pushbutton.
-//
-// On my cabinet, empirically, a distance around 2mm (.08") seems
-// to work pretty well.  It's far enough that it doesn't trigger
-// spuriously, but short enough that it responds to a reasonably
-// light push.
-//
-// You might need to adjust this up or down to get the right feel.
-// Alternatively, if you don't like the "push" gesture at all and
-// would prefer to only make the plunger respond to a pull-and-release
-// motion, simply set this to, say, 2.0 - it's impossible to push a 
-// plunger forward that far, so that will effectively turn off the 
-// push mode.
-const float LaunchBallPushDistance = .08;
+        } cal;
 
+    } plunger;
 
-// --------------------------------------------------------------------------
-//
-// TLC5940 PWM controller chip setup - Enhanced LedWiz emulation
-//
-// By default, the Pinscape Controller software can provide limited LedWiz
-// emulation through the KL25Z's on-board GPIO ports.  This lets you hook
-// up external devices, such as LED flashers or solenoids, to the KL25Z
-// outputs (using external circuitry to boost power - KL25Z GPIO ports
-// are limited to a meager 4mA per port).  This capability is limited by
-// the number of available GPIO ports on the KL25Z, and even smaller limit
-// of 10 PWM-capable GPIO ports.
-//
-// As an alternative, the controller software lets you use external PWM
-// controller chips to control essentially unlimited channels with full
-// PWM control on all channels.  This requires building external circuitry
-// using TLC5940 chips.  Each TLC5940 chip provides 16 full PWM channels,
-// and you can daisy-chain multiple TLC5940 chips together to set up 32, 
-// 48, 64, or more channels.
-//
-// If you do add TLC5940 circuits to your controller hardware, use this
-// section to configure the connection to the KL25Z.
-//
-// Note that when using the TLC5940, you can still also use some GPIO
-// pins for outputs as normal.  See ledWizPinMap[] for 
+    
+    // --- TV ON SWITCH ---
+    //
+    // To use the TV ON switch feature, the special power sensing circuitry
+    // implemented on the Expansion Board must be attached (or an equivalent
+    // circuit, as described in the Build Guide).  The circuitry lets us
+    // detect power state changes on the secondary power supply.
+    struct 
+    {
+        // PSU2 power status sense (DigitalIn pin).  This pin goes LOW when the
+        // secondary power supply is turned off, and remains LOW until the LATCH
+        // pin is raised high AND the secondary PSU is turned on.  Once HIGH,
+        // it remains HIGH as long as the secondary PSU is on.
+        PinName statusPin;
+    
+        // PSU2 power status latch (DigitalOut pin)
+        PinName latchPin;
+        
+        // TV ON relay pin (DigitalOut pin).  This pin controls the TV switch 
+        // relay.  Raising the pin HIGH turns the relay ON (energizes the coil).
+        PinName relayPin;
+        
+        // TV ON delay time, in seconds.  This is the interval between sensing
+        // that the secondary power supply has turned on and pulsing the TV ON
+        // switch relay.  
+        float delayTime;
+    
+    } TVON;
+    
 
-// Number of TLC5940 chips you're using.  For a full LedWiz-compatible
-// setup, you need two of these chips, for 32 outputs.  The software
-// will handle up to 8.  
-// If you're using the expansion board, the main KL25Z interface board
-// has 2 chips and the MOSFET board has 2 more, for a total of 4.  If
-// you add extra daisy-chained MOSFET boards, add 2 more per board.
-#ifdef EXPANSION_BOARD
-# define TLC5940_NCHIPS  4
-#else
-# define TLC5940_NCHIPS  0     // change this if you're using TLC5940's without the expansion board
-#endif
+    // --- TLC5940NT PWM Controller Chip Setup ---
+    struct
+    {
+        // number of TLC5940NT chips connected in daisy chain
+        int nchips;
+        
+        // pin connections
+        PinName sin;        // Serial data - must connect to SPIO MOSI -> PTC6 or PTD2
+        PinName sclk;       // Serial clock - must connect to SPIO SCLK -> PTC5 or PTD1
+                            // (but don't use PTD1, since it's hard-wired to the on-board blue LED)
+        PinName xlat;       // XLAT (latch) signal - connect to any GPIO pin
+        PinName blank;      // BLANK signal - connect to any GPIO pin
+        PinName gsclk;      // Grayscale clock - must connect to a PWM-out capable pin
 
-// If you're using TLC5940s, change any of these as needed to match the
-// GPIO pins that you connected to the TLC5940 control pins.  Note that
-// SIN and SCLK *must* be connected to the KL25Z SPI0 MOSI and SCLK
-// outputs, respectively, which effectively limits them to the default
-// selections, and that the GSCLK pin must be PWM-capable.  These defaults
-// all match the expansion board wiring.
-#define TLC5940_SIN    PTC6    // Serial data - Must connect to SPI0 MOSI -> PTC6 or PTD2
-#define TLC5940_SCLK   PTC5    // Serial clock - Must connect to SPI0 SCLK -> PTC5 or PTD1,
-                               //  but don't use PTD1 because it's hard-wired to the on-board 
-                               //  blue LED
-#define TLC5940_XLAT   PTC10   // XLAT (latch) signal - Any GPIO pin can be used
-#define TLC5940_BLANK  PTC7    // BLANK signal - Any GPIO pin can be used
-#define TLC5940_GSCLK  PTA1    // Grayscale clock - Must be a PWM-capable pin
+    } tlc5940; 
+    
+
+    // --- 74HC595 Shift Register Setup ---
+    struct
+    {
+        // number of 74HC595 chips attached in daisy chain
+        int nchips;
+        
+        // pin connections
+        PinName sin;        // Serial data - use any GPIO pin
+        PinName sclk;       // Serial clock - use any GPIO pin
+        PinName latch;      // Latch - use any GPIO pin
+        PinName ena;        // Enable signal - use any GPIO pin
+    
+    } hc595;
 
 
-// --------------------------------------------------------------------------
-//
-// 74HC595 digital output setup - "Chime Board" module
-//
-// The 74HC595 is an 8-output serial-to-parallel shift register IC.  This lets
-// us add extra digital outputs (on/off only, not PWM), 8 at a time, similar
-// to the way the TLC5940 lets us add extra PWM outputs.  The 74HC595 requires
-// four control signals, so one chip gives us 8 outputs using only 4 GPIOs.
-// The chips can be daisy-chained, so by adding multiple chips, we can add 
-// any number of new outputs, still using only 4 GPIO pins for the whole chain.
-//
-// The TLC5940 is more useful for general-purpose outputs because of its PWM
-// capabilities, but digital-only outputs are better for some special cases.
-//
-// The Expansion Board "Chime" module uses these chips to add timer-protected
-// outputs.  The timer triggers are edge-sensitive, so we want simple on/off
-// signals to control them; a PWM signal wouldn't work properly because it's
-// constantly switching on and off even when nominally 100% on.
-//
-
-#define HC595_NCHIPS   0       // Number of chips == number of Chime boards connected
-#define HC595_SIN      PTA5    // Serial data - use any GPIO pin
-#define HC595_SCLK     PTA4    // Serial clock - use any GPIO pin
-#define HC595_LATCH    PTA12   // Latch signal - use any GPIO pin
-#define HC595_ENA      PTD4    // Enable signal - use any GPIO pin
-
-
-#endif // CONFIG_H - end of include-once section (code below this point can be multiply included)
-
-
-#ifdef DECL_EXTERNS  // this section defines global variables, only if this macro is set
-
-// --------------------------------------------------------------------------
-//
+    // --- Button Input Setup ---
+    struct
+    {
+        uint8_t pin;        // physical input GPIO pin - a USB-to-PinName mapping index
+        uint8_t typ;        // key type reported to PC - a BtnTypeXxx value
+        uint8_t val;        // key value reported - meaning depends on 'typ' value
+        
+    } button[MAX_BUTTONS];
+    
 
-// 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.
-//
-// (If you're using TLC5940 chips to control outputs, many of the
-// GPIO pins that are mapped to LedWiz outputs in the default
-// mapping can be reassigned as keys, since the TLC5940 outputs
-// take over for the GPIO pins.  The exceptions are the pins that
-// are reassigned to control the TLC5940 chips.)
-//
-// 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.
-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
-#ifdef EXPANSION_BOARD
-    PTC11,     // J1 pin 15,  joystick button 6
-#else
-    PTE22,     // J10 pin 5,  joystick button 6
-#endif
-    
-    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
+    // --- LedWiz Output Port Setup ---
+    struct
+    {
+        uint8_t typ;        // port type:  a PortTypeXxx value
+        uint8_t pin;        // physical output pin:  for a GPIO port, this is an index in the 
+                            // USB-to-PinName mapping list; for a TLC5940 or 74HC595 port, it's 
+                            // the output number, starting from 0 for OUT0 on the first chip in 
+                            // the daisy chain.  For inactive and virtual ports, it's unused.
+        uint8_t flags;      // flags:  a combination of PortFlagXxx values
+    } outPort[MAX_OUT_PORTS];
 };
 
-// --------------------------------------------------------------------------
-//
-// LED-Wiz emulation output pin assignments
-//
-// This sets the mapping from logical LedWiz port numbers, as used
-// in the software on the PC side, to physical hardware pins on the
-// KL25Z and/or the TLC5940 controllers.
-//
-// The LedWiz protocol lets the PC software set a "brightness" level
-// for each output.  This is used to control the intensity of LEDs
-// and other lights, and can also control motor speeds.  To implement 
-// the intensity level in hardware, we use PWM, or pulse width
-// modulation, which switches the output on and off very rapidly
-// to give the effect of a reduced voltage.  Unfortunately, the KL25Z
-// hardware is limited to 10 channels of PWM control for its GPIO
-// outputs, so it's impossible to implement the LedWiz's full set
-// of 32 adjustable outputs using only GPIO ports.  However, you can
-// create 10 adjustable ports and fill out the rest with "digital"
-// GPIO pins, which are simple on/off switches.  The intensity level
-// of a digital port can't be adjusted - it's either fully on or
-// fully off - but this is fine for devices that don't have
-// different intensity settings anyway, such as replay knockers
-// and flipper solenoids.
-//
-// In the mapping list below, you can decide how to dole out the
-// PWM-capable and digital-only GPIO pins.  To make it easier to
-// remember which is which, the default mapping below groups all
-// of the PWM-capable ports together in the first 10 logical LedWiz
-// port numbers.  Unfortunately, these ports aren't *physically*
-// together on the KL25Z pin headers, so this layout may be simple
-// in terms of the LedWiz numbering, but it's a little jumbled
-// in the physical layout.t
-//
-// "NC" in the pin name slot means "not connected".  This means
-// that there's no physical output for this LedWiz port number.
-// The device will still accept commands that control the port,
-// but these will just be silently ignored, since there's no pin
-// to turn on or off for these ports.  The reason we leave some 
-// ports unconnected 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 file).
-// 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: Don't assign PTD1 (pin J2-12) as an LedWiz output.  That pin
-// is hard-wired on the KL25Z to the on-board indicator LED's blue segment,  
-// which pretty precludes other uses of the pin.
-//
-// ACTIVE-LOW PORTS:  By default, when a logical port is turned on in
-// the software, we set the physical GPIO voltage to "high" (3.3V), and
-// set it "low" (0V) when the logical port is off.  This is the right
-// scheme for the booster circuit described in the build guide.  Some
-// third-party booster circuits want the opposite voltage scheme, where
-// logical "on" is represented by 0V on the port and logical "off" is
-// represented by 3.3V.  If you're using an "active low" booster like
-// that, set the PORT_ACTIVE_LOW flag in the array below for each 
-// affected port.
-//
-// TLC5940 PORTS:  To assign an LedWiz output port number to a particular
-// output on a TLC5940, set the port type to TLC_PORT and set the 'pin'
-// value to the index of the output port in the daisy chain.  The first
-// chip in the daisy chain has ports 1-16, the second has ports 17-32, 
-// and so on.
-//
-// 74HC595 PORTS:  To assign an LedWiz output port to a 74HC595 port,
-// set the port type to HC595_PORT and set 'pin' to the index of the port
-// in the daisy chain.  The first chip has ports 1-8, the second has 
-// 9-16, etc.
-//
-
-// ledWizPortMap 'typ' values
-enum LWPortType {
-    NO_PORT    = -1,  // Not connected
-    DIG_GPIO   = 0,   // DigitalOut I/O pin (not PWM capable)
-    PWM_GPIO   = 1,   // AnalogOut I/O pin (PWM capable)
-    TLC_PORT   = 2,   // TLC5940 output port
-    HC595_PORT = 3    // 74HC595 output port
-};
-
-// flags - combine with '|'
-const int PORT_ACTIVE_LOW = 0x0001;  // use LOW voltage (0V) when port is ON
-
-struct {
-    int pin;            // Pin name/index - PinName for GPIO, pin index for TLC5940 or 74HC595
-    LWPortType typ;     // type of pin
-    int flags;          // flags - a combination of PORT_xxx flag bits (see above)
-} ledWizPortMap[] = {
-    
-#if TLC5940_NCHIPS == 0
-
-    // *** BASIC MODE - GPIO OUTPUTS ONLY ***
-    // This is the basic mapping, using entirely GPIO pins, for when you're 
-    // not using external TLC5940 chips.  We provide 22 physical outputs, 10 
-    // of which are PWM capable.
-    //
-    // Important!  Note that the "isPWM" setting isn't just something we get to
-    // choose.  It's a feature of the KL25Z hardware.  Some pins are PWM capable 
-    // and some aren't, and there's nothing we can do about that in the software.
-    // Refer to the KL25Z manual or schematics for the possible connections.  Note 
-    // that there are other PWM-capable pins besides the 10 shown below, BUT they 
-    // all share TPM channels with the pins below.  For example, TPM 2.0 can be 
-    // connected to PTA1, PTB2, PTB18, PTE22 - but only one at a time.  So if you 
-    // want to use PTB2 as a PWM out, it means you CAN'T use PTA1 as a PWM out.
-    // We commented each PWM pin with its hardware channel number to help you keep
-    // track of available channels if you do need to rearrange any of these pins.
-
-    { PTA1,  PWM_GPIO },      // pin J1-2,  LW port 1  (PWM capable - TPM 2.0 = channel 9)
-    { PTA2,  PWM_GPIO },      // pin J1-4,  LW port 2  (PWM capable - TPM 2.1 = channel 10)
-    { PTD4,  PWM_GPIO },      // pin J1-6,  LW port 3  (PWM capable - TPM 0.4 = channel 5)
-    { PTA12, PWM_GPIO },      // pin J1-8,  LW port 4  (PWM capable - TPM 1.0 = channel 7)
-    { PTA4,  PWM_GPIO },      // pin J1-10, LW port 5  (PWM capable - TPM 0.1 = channel 2)
-    { PTA5,  PWM_GPIO },      // pin J1-12, LW port 6  (PWM capable - TPM 0.2 = channel 3)
-    { PTA13, PWM_GPIO },      // pin J2-2,  LW port 7  (PWM capable - TPM 1.1 = channel 13)
-    { PTD5,  PWM_GPIO },      // pin J2-4,  LW port 8  (PWM capable - TPM 0.5 = channel 6)
-    { PTD0,  PWM_GPIO },      // pin J2-6,  LW port 9  (PWM capable - TPM 0.0 = channel 1)
-    { PTD3,  PWM_GPIO },      // pin J2-10, LW port 10 (PWM capable - TPM 0.3 = channel 4)
-    { PTD2,  DIG_GPIO },      // pin J2-8,  LW port 11
-    { PTC8,  DIG_GPIO },      // pin J1-14, LW port 12
-    { PTC9,  DIG_GPIO },      // pin J1-16, LW port 13
-    { PTC7,  DIG_GPIO },      // pin J1-1,  LW port 14
-    { PTC0,  DIG_GPIO },      // pin J1-3,  LW port 15
-    { PTC3,  DIG_GPIO },      // pin J1-5,  LW port 16
-    { PTC4,  DIG_GPIO },      // pin J1-7,  LW port 17
-    { PTC5,  DIG_GPIO },      // pin J1-9,  LW port 18
-    { PTC6,  DIG_GPIO },      // pin J1-11, LW port 19
-    { PTC10, DIG_GPIO },      // pin J1-13, LW port 20
-    { PTC11, DIG_GPIO },      // pin J1-15, LW port 21
-    { PTE0,  DIG_GPIO },      // pin J2-18, LW port 22
-    { NC,    NO_PORT  },      // Not connected,  LW port 23
-    { NC,    NO_PORT  },      // Not connected,  LW port 24
-    { NC,    NO_PORT  },      // Not connected,  LW port 25
-    { NC,    NO_PORT  },      // Not connected,  LW port 26
-    { NC,    NO_PORT  },      // Not connected,  LW port 27
-    { NC,    NO_PORT  },      // Not connected,  LW port 28
-    { NC,    NO_PORT  },      // Not connected,  LW port 29
-    { NC,    NO_PORT  },      // Not connected,  LW port 30
-    { NC,    NO_PORT  },      // Not connected,  LW port 31
-    { NC,    NO_PORT  }       // Not connected,  LW port 32
-
-#elif defined(EXPANSION_BOARD)
-
-    // *** EXPANSION BOARD MODE ***
-    // 
-    // This mapping is for the expansion board, which uses four TLC5940
-    // chips to provide 64  outputs.  The expansion board also uses
-    // one GPIO pin to provide a digital (non-PWM) output dedicated to
-    // the knocker circuit.  That's on a digital pin because it's used
-    // to trigger an external timer circuit that limits the amount of
-    // time that the knocker coil can be continuously energized, to protect
-    // it against software faults on the PC that leave the port stuck on.
-    // (The knocker coil is unique among standard virtual cabinet output
-    // devices in this respect - it's the only device in common use that
-    // can be damaged if left on for too long.  Other devices won't be
-    // damaged, so they don't require such elaborate precautions.)
-    //
-    // The specific device assignments in the last column are just 
-    // recommendations.  You can assign any port to any device with 
-    // compatible power needs.  The "General Purpose" ports are good to
-    // at least 5A, so you can use these for virtually anything; put
-    // your heavy-duty devices, such as solenoids and motors, on these
-    // outputs.  You can also put lighter loads like lamps and LEDs
-    // on these if you have ports left over after connecting all of
-    // your high-power devices.  The "Flasher" and "Button light" ports 
-    // are good to about 1.5A, so they work for medium loads like lamps, 
-    // flashers, high-power LEDs, etc.  The flipper and magnasave ports 
-    // only provide 20mA; use these only for small LEDs.
-    //
-    // The TLC5940 outputs on the expansion board are hard-wired to
-    // specific output drivers - that's what determines the power
-    // limits described above.  You can rearrange the ports in the
-    // list below to change the LedWiz port numbering to any order 
-    // you prefer, but the association between a TLC5940 port number 
-    // and the output circuit type can't be changed in the software.
-    // That's a function of how the TLC5940 port is physically wired 
-    // on the board.  Likewise, the PTC8 output is hard-wired to the 
-    // knocker time limiter.
-    //   TLC ports 1-20 and 44-47 = Darlington outputs, 1.5A max
-    //   TLC ports 21-44 = MOSFET outputs (limit depends on MOSFET chosen)
-    //   TLC ports 49-64 = direct outputs, limited to 20mA
-
-    // The first 32 ports are LedWiz-compatible, so they're universally
-    // accessible, even to older non-DOF software.  Attach the most common
-    // devices to these ports.
-    { 1, TLC_PORT },         // TLC port 1,  LW output 1  - Flasher 1 R
-    { 2, TLC_PORT },         // TLC port 2,  LW output 2  - Flasher 1 G
-    { 3, TLC_PORT },         // TLC port 3,  LW output 3  - Flasher 1 B
-    { 4, TLC_PORT },         // TLC port 4,  LW output 4  - Flasher 2 R
-    { 5, TLC_PORT },         // TLC port 5,  LW output 5  - Flasher 2 G
-    { 6, TLC_PORT },         // TLC port 6,  LW output 6  - Flasher 2 B
-    { 7, TLC_PORT },         // TLC port 7,  LW output 7  - Flasher 3 R
-    { 8, TLC_PORT },         // TLC port 8,  LW output 8  - Flasher 3 G
-    { 9, TLC_PORT },         // TLC port 9,  LW output 9  - Flasher 3 B
-    { 10, TLC_PORT },        // TLC port 10, LW output 10 - Flasher 4 R
-    { 11, TLC_PORT },        // TLC port 11, LW output 11 - Flasher 4 G
-    { 12, TLC_PORT },        // TLC port 12, LW output 12 - Flasher 4 B
-    { 13, TLC_PORT },        // TLC port 13, LW output 13 - Flasher 5 R
-    { 14, TLC_PORT },        // TLC port 14, LW output 14 - Flasher 5 G
-    { 15, TLC_PORT },        // TLC port 15, LW output 15 - Flasher 5 B
-    { 16, TLC_PORT },        // TLC port 16, LW output 16 - Strobe/Button light
-    { 17, TLC_PORT },        // TLC port 17, LW output 17 - Button light 1
-    { 18, TLC_PORT },        // TLC port 18, LW output 18 - Button light 2
-    { 19, TLC_PORT },        // TLC port 19, LW output 19 - Button light 3
-    { 20, TLC_PORT },        // TLC port 20, LW output 20 - Button light 4
-    { PTC8, DIG_GPIO },      // PTC8,        LW output 21 - Replay Knocker
-    { 21, TLC_PORT },        // TLC port 21, LW output 22 - Contactor 1/General purpose
-    { 22, TLC_PORT },        // TLC port 22, LW output 23 - Contactor 2/General purpose
-    { 23, TLC_PORT },        // TLC port 23, LW output 24 - Contactor 3/General purpose
-    { 24, TLC_PORT },        // TLC port 24, LW output 25 - Contactor 4/General purpose
-    { 25, TLC_PORT },        // TLC port 25, LW output 26 - Contactor 5/General purpose
-    { 26, TLC_PORT },        // TLC port 26, LW output 27 - Contactor 6/General purpose
-    { 27, TLC_PORT },        // TLC port 27, LW output 28 - Contactor 7/General purpose
-    { 28, TLC_PORT },        // TLC port 28, LW output 29 - Contactor 8/General purpose
-    { 29, TLC_PORT },        // TLC port 29, LW output 30 - Contactor 9/General purpose
-    { 30, TLC_PORT },        // TLC port 30, LW output 31 - Contactor 10/General purpose
-    { 31, TLC_PORT },        // TLC port 31, LW output 32 - Shaker Motor/General purpose
-    
-    // Ports 33+ are accessible only to DOF-based software.  Older LedWiz-only
-    // software on the can't access these.  Attach less common devices to these ports.
-    { 32, TLC_PORT },        // TLC port 32, LW output 33 - Gear Motor/General purpose
-    { 33, TLC_PORT },        // TLC port 33, LW output 34 - Fan/General purpose
-    { 34, TLC_PORT },        // TLC port 34, LW output 35 - Beacon/General purpose
-    { 35, TLC_PORT },        // TLC port 35, LW output 36 - Undercab RGB R/General purpose
-    { 36, TLC_PORT },        // TLC port 36, LW output 37 - Undercab RGB G/General purpose
-    { 37, TLC_PORT },        // TLC port 37, LW output 38 - Undercab RGB B/General purpose
-    { 38, TLC_PORT },        // TLC port 38, LW output 39 - Bell/General purpose
-    { 39, TLC_PORT },        // TLC port 39, LW output 40 - Chime 1/General purpose
-    { 40, TLC_PORT },        // TLC port 40, LW output 41 - Chime 2/General purpose
-    { 41, TLC_PORT },        // TLC port 41, LW output 42 - Chime 3/General purpose
-    { 42, TLC_PORT },        // TLC port 42, LW output 43 - General purpose
-    { 43, TLC_PORT },        // TLC port 43, LW output 44 - General purpose
-    { 44, TLC_PORT },        // TLC port 44, LW output 45 - Button light 5
-    { 45, TLC_PORT },        // TLC port 45, LW output 46 - Button light 6
-    { 46, TLC_PORT },        // TLC port 46, LW output 47 - Button light 7
-    { 47, TLC_PORT },        // TLC port 47, LW output 48 - Button light 8
-    { 49, TLC_PORT },        // TLC port 49, LW output 49 - Flipper button RGB left R
-    { 50, TLC_PORT },        // TLC port 50, LW output 50 - Flipper button RGB left G
-    { 51, TLC_PORT },        // TLC port 51, LW output 51 - Flipper button RGB left B
-    { 52, TLC_PORT },        // TLC port 52, LW output 52 - Flipper button RGB right R
-    { 53, TLC_PORT },        // TLC port 53, LW output 53 - Flipper button RGB right G
-    { 54, TLC_PORT },        // TLC port 54, LW output 54 - Flipper button RGB right B
-    { 55, TLC_PORT },        // TLC port 55, LW output 55 - MagnaSave button RGB left R
-    { 56, TLC_PORT },        // TLC port 56, LW output 56 - MagnaSave button RGB left G
-    { 57, TLC_PORT },        // TLC port 57, LW output 57 - MagnaSave button RGB left B
-    { 58, TLC_PORT },        // TLC port 58, LW output 58 - MagnaSave button RGB right R
-    { 59, TLC_PORT },        // TLC port 59, LW output 59 - MagnaSave button RGB right G
-    { 60, TLC_PORT },        // TLC port 60, LW output 60 - MagnaSave button RGB right B
-    { 61, TLC_PORT },        // TLC port 61, LW output 61 - Extra RGB LED R
-    { 62, TLC_PORT },        // TLC port 62, LW output 62 - Extra RGB LED G
-    { 63, TLC_PORT },        // TLC port 63, LW output 63 - Extra RGB LED B
-    { 64, TLC_PORT }         // TLC port 64, LW output 64 - Extra single LED
-    
-#else
-
-    // *** TLC5940 + GPIO OUTPUTS, Without the expansion board ***
-    //
-    // This is the mapping for the ehnanced mode, with one or more TLC5940 
-    // chips connected.  Each TLC5940 chip provides 16 PWM channels.  We
-    // can supplement the TLC5940 outputs with GPIO pins to get even more 
-    // physical outputs.  
-    //
-    // Because we've already declared the number of TLC5940 chips earlier
-    // in this file, we don't actually have to map out all of the TLC5940
-    // ports here.  The software will automatically assign all of the 
-    // TLC5940 ports that aren't explicitly mentioned here to the next
-    // available LedWiz port numbers after the end of this array, assigning
-    // them sequentially in TLC5940 port order.
-    //
-    // In contrast to the basic mode arrangement, we're putting all of the
-    // NON PWM ports first in this mapping.  The logic is that all of the 
-    // TLC5940 ports are PWM-capable, and they'll all at the end of the list
-    // here, so by putting the PWM GPIO pins last here, we'll keep all of the
-    // PWM ports grouped in the final mapping.
-    //
-    // Note that the TLC5940 control wiring takes away several GPIO pins
-    // that we used as output ports in the basic mode.  Further, because the
-    // TLC5940 makes ports so plentiful, we're intentionally omitting several 
-    // more of the pins from the basic set, to make them available for other
-    // uses.  To keep things more neatly grouped, we're only assigning J1 pins
-    // in this set.  This leaves the following ports from the basic mode output
-    // set available for other users: PTA13, PTD0, PTD2, PTD3, PTD5, PTE0.
-    
-    { PTC8,  DIG_GPIO },      // pin J1-14, LW port 1
-    { PTC9,  DIG_GPIO },      // pin J1-16, LW port 2
-    { PTC0,  DIG_GPIO },      // pin J1-3,  LW port 3
-    { PTC3,  DIG_GPIO },      // pin J1-5,  LW port 4
-    { PTC4,  DIG_GPIO },      // pin J1-7,  LW port 5
-    { PTC11, DIG_GPIO },      // pin J1-15, LW port 6
-    { PTA2,  PWM_GPIO },      // pin J1-4,  LW port 7   (PWM capable - TPM 2.1 = channel 10)
-    { PTD4,  PWM_GPIO },      // pin J1-6,  LW port 8   (PWM capable - TPM 0.4 = channel 5)
-    { PTA12, PWM_GPIO },      // pin J1-8,  LW port 9   (PWM capable - TPM 1.0 = channel 7)
-    { PTA4,  PWM_GPIO },      // pin J1-10, LW port 10  (PWM capable - TPM 0.1 = channel 2)
-    { PTA5,  PWM_GPIO }       // pin J1-12, LW port 11  (PWM capable - TPM 0.2 = channel 3)
-
-    // TLC5940 ports start here!
-    // First chip port 0 ->   LW port 12
-    // First chip port 1 ->   LW port 13
-    // ... etc, filling out all chip ports sequentially ...
-
-#endif // TLC5940_NCHIPS
-};
-
-
-#endif // DECL_EXTERNS
+#endif
--- a/main.cpp	Thu Dec 03 07:34:57 2015 +0000
+++ b/main.cpp	Sat Dec 19 06:37:19 2015 +0000
@@ -1,4 +1,4 @@
-/* Copyright 2014 M J Roberts, MIT License
+/* Copyright 2014, 2015 M J Roberts, MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 * and associated documentation files (the "Software"), to deal in the Software without
@@ -17,64 +17,43 @@
 */
 
 //
-// Pinscape Controller
-//
-// "Pinscape" is the name of my custom-built virtual pinball cabinet, so I call this
-// software the Pinscape Controller.  I wrote it to handle several tasks that I needed
-// for my cabinet.  It runs on a Freescale KL25Z microcontroller, which is a small and 
-// inexpensive device that attaches to the cabinet PC via a USB cable, and can attach
-// via custom wiring to sensors, buttons, and other devices in the cabinet.
+// The Pinscape Controller
+// A comprehensive input/output controller for virtual pinball machines
 //
-// 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.
+// This project implements an I/O controller designed for use in custom-built virtual
+// pinball cabinets.  It can handle nearly all of the functions involved in connecting 
+// pinball simulation software on a Windows PC with devices in the cabinet, including
+// input devices such as buttons and sensors, and output devices that generate visual
+// or mechanical feedback during play, like lights, solenoids, and shaker motors.
+// You can use one, some, or all of the functions, in any combination.  You can select
+// options and configure the controller using a setup tool that runs on Windows.
 //
-// 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).
+// The main functions are:
 //
-// This software provides a whole bunch of separate features.  You can use any of these 
-// features individually or all together.  If you're not using a particular feature, you
-// can simply omit the extra wiring and/or hardware for that feature.  You can use
-// the nudging feature by itself without any extra hardware attached, since the
-// accelerometer is built in to the KL25Z board.
-//
-//  - Nudge sensing via the KL25Z's on-board accelerometer.  Nudging the cabinet
+//  - 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.
+//    Visual Pinball (or other pinball emulator software) on the PC via the joystick
+//    interface, using the X and Y axes.  VP and most other PC pinball emulators have 
+//    native handling for this type of nudge input, so all you have to do is set some 
+//    preferences in VP to let it know that an accelerometer is attached.
 //
-//  - Plunger position sensing via an attached TAOS TSL 1410R CCD linear array sensor.  
-//    To use this feature, you need to buy the TAOS device (it's not built in to the
-//    KL25Z, obviously), wire it to the KL25Z (5 wire connections between the two
-//    devices are required), and mount the TAOS sensor in your cabinet so that it's
-//    positioned properly to capture images of the physical plunger shooter rod.
-//
-//    The physical mounting and wiring details are desribed in the project 
-//    documentation.  
+//  - Plunger position sensing, via a number of sensor options.  To use this feature,
+//    you need to choose a sensor and set it up, connect the sensor electrically to 
+//    the KL25Z, and configure the Pinscape software on the KL25Z to let it know how 
+//    the sensor is hooked up.  The Pinscape software monitors the sensor and sends
+//    readings to Visual Pinball via the joystick Z axis.  VP and other PC software
+//    has native support for this type of input as well; as with the nudge setup,
+//    you just have to set some options in VP to activate the plunger.
 //
-//    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.
+//    The Pinscape software supports optical sensors (the TAOS TSL1410R and TSL1412R 
+//    linear sensor arrays) as well as slide potentiometers.  The specific equipment
+//    that's supported, along with physical mounting and wiring details, can be found
+//    in the Build Guide.
+//
+//    Note that while VP has its own built-in support for plunger devices like this
+//    one, many existing VP tables will ignore it, because they use custom scripting 
+//    that's only designed for keyboard plunger input.  The Build Guide has advice on
+//    adjusting tables to add plunger support when necessary.
 //
 //    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
@@ -103,11 +82,7 @@
 //    for input - you just have to assign a VP function to each button using VP's
 //    keyboard options dialog.  To wire a button physically, connect one terminal of
 //    the button switch to the KL25Z ground, and connect the other terminal to the
-//    the GPIO port you wish to assign to the button.  See the buttonMap[] array
-//    below for the available GPIO ports and their assigned joystick button numbers.
-//    If you're not using a GPIO port, you can just leave it unconnected - the digital
-//    inputs have built-in pull-up resistors, so an unconnected port is the same as
-//    an open switch (an "off" state for the button).
+//    the GPIO port you wish to assign to the button.
 //
 //  - LedWiz emulation.  The KL25Z can appear to the PC as an LedWiz device, and will
 //    accept and process LedWiz commands from the host.  The software can turn digital
@@ -159,6 +134,8 @@
 //    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.
 //
+//
+//
 // STATUS LIGHTS:  The on-board LED on the KL25Z flashes to indicate the current 
 // device status.  The flash patterns are:
 //
@@ -169,10 +146,6 @@
 //
 //    short red flash = the host computer is in sleep/suspend mode
 //
-//    long red/green = the LedWiz unti number has been changed, so a reset
-//        is needed.  You can simply unplug the device and plug it back in,
-//        or presss and hold the reset button on the device for a few seconds.
-//
 //    long yellow/green = everything's working, but the plunger hasn't
 //        been calibrated; follow the calibration procedure described above.
 //        This flash mode won't appear if the CCD has been disabled.  Note
@@ -181,79 +154,12 @@
 //        in config.h or use the  Windows config tool to disable the CCD 
 //        software features.
 //
-//    alternating blue/green = everything's working
-//
-// Software configuration: you can some change option settings by sending special
-// USB commands from the PC.  I've provided a Windows program for this purpose;
-// refer to the documentation for details.  For reference, here's the format
-// of the USB command for option changes:
-//
-//    length of report = 8 bytes
-//    byte 0 = 65 (0x41)
-//    byte 1 = 1  (0x01)
-//    byte 2 = new LedWiz unit number, 0x01 to 0x0f
-//    byte 3 = feature enable bit mask:
-//             0x01 = enable CCD (default = on)
-//
-// Plunger calibration mode: the host can activate plunger calibration mode
-// by sending this packet.  This has the same effect as pressing and holding
-// the plunger calibration button for two seconds, to allow activating this
-// mode without attaching a physical button.
-//
-//    length = 8 bytes
-//    byte 0 = 65 (0x41)
-//    byte 1 = 2  (0x02)
-//
-// Exposure reports: the host can request a report of the full set of pixel
-// values for the next frame by sending this special packet:
-//
-//    length = 8 bytes
-//    byte 0 = 65 (0x41)
-//    byte 1 = 3  (0x03)
-//
-// We'll respond with a series of special reports giving the exposure status.
-// Each report has the following structure:
+//    alternating blue/green = everything's working, and the plunger has
+//        been calibrated
 //
-//    bytes 0:1 = 11-bit index, with high 5 bits set to 10000.  For 
-//                example, 0x04 0x80 indicates index 4.  This is the 
-//                starting pixel number in the report.  The first report 
-//                will be 0x00 0x80 to indicate pixel #0.  
-//    bytes 2:3 = 16-bit unsigned int brightness level of pixel at index
-//    bytes 4:5 = brightness of pixel at index+1
-//    etc for the rest of the packet
 //
-// This still has the form of a joystick packet at the USB level, but
-// can be differentiated by the host via the status bits.  It would have
-// been cleaner to use a different Report ID at the USB level, but this
-// would have necessitated a different container structure in the report
-// descriptor, which would have broken LedWiz compatibility.  Given that
-// constraint, we have to re-use the joystick report type, making for
-// this somewhat kludgey approach.
-//
-// Configuration query: the host can request a full report of our hardware
-// configuration with this message.
-//
-//    length = 8 bytes
-//    byte 0 = 65 (0x41)
-//    byte 1 = 4  (0x04)
-//
-// We'll response with one report containing the configuration status:
-//
-//    bytes 0:1 = 0x8800.  This has the bit pattern 10001 in the high
-//                5 bits, which distinguishes it from regular joystick
-//                reports and from exposure status reports.
-//    bytes 2:3 = number of outputs
-//    remaining bytes = reserved for future use; set to 0 in current version
-//
-// Turn off all outputs: this message tells the device to turn off all
-// outputs and restore power-up LedWiz defaults.  This sets outputs #1-32
-// to profile 48 (full brightness) and switch state Off, sets all extended
-// outputs (#33 and above) to brightness 0, and sets the LedWiz flash rate
-// to 2.
-//
-//    length = 8 bytes
-//    byte 0 = 65 (0x41)
-//    byte 1 = 5  (0x05)
+// USB PROTOCOL:  please refer to USBProtocol.h for details on the USB
+// message protocol.
 
 
 #include "mbed.h"
@@ -265,6 +171,11 @@
 #include "crc32.h"
 #include "TLC5940.h"
 #include "74HC595.h"
+#include "nvm.h"
+#include "plunger.h"
+#include "ccdSensor.h"
+#include "potSensor.h"
+#include "nullSensor.h"
 
 #define DECL_EXTERNS
 #include "config.h"
@@ -287,18 +198,7 @@
 // 
 // USB product version number
 //
-const uint16_t USB_VERSION_NO = 0x0007;
-
-
-//
-// Build the full USB product ID.  If we're using the LedWiz compatible
-// vendor ID, the full product ID is the combination of the LedWiz base
-// product ID (0x00F0) and the 0-based unit number (0-15).  If we're not
-// trying to be LedWiz compatible, we just use the exact product ID
-// specified in config.h.
-#define MAKE_USB_PRODUCT_ID(vid, pidbase, unit) \
-    ((vid) == 0xFAFA && (pidbase) == 0x00F0 ? (pidbase) | (unit) : (pidbase))
-
+const uint16_t USB_VERSION_NO = 0x0008;
 
 // --------------------------------------------------------------------------
 //
@@ -306,52 +206,6 @@
 //
 #define JOYMAX 4096
 
-// --------------------------------------------------------------------------
-//
-// Set up mappings for the joystick X and Y reports based on the mounting
-// orientation of the KL25Z in the cabinet.  Visual Pinball and other 
-// pinball software effectively use video coordinates to define the axes:
-// positive X is to the right of the table, negative Y to the left, positive
-// Y toward the front of the table, negative Y toward the back.  The KL25Z
-// accelerometer is mounted on the board with positive Y toward the USB
-// ports and positive X toward the right side of the board with the USB
-// ports pointing up.  It's a simple matter to remap the KL25Z coordinate
-// system to match VP's coordinate system for mounting orientations at
-// 90-degree increments...
-//
-#if defined(ORIENTATION_PORTS_AT_FRONT)
-# define JOY_X(x, y)   (y)
-# define JOY_Y(x, y)   (x)
-#elif defined(ORIENTATION_PORTS_AT_LEFT)
-# define JOY_X(x, y)   (-(x))
-# define JOY_Y(x, y)   (y)
-#elif defined(ORIENTATION_PORTS_AT_RIGHT)
-# define JOY_X(x, y)   (x)
-# define JOY_Y(x, y)   (-(y))
-#elif defined(ORIENTATION_PORTS_AT_REAR)
-# define JOY_X(x, y)   (-(y))
-# define JOY_Y(x, y)   (-(x))
-#else
-# error Please define one of the ORIENTATION_PORTS_AT_xxx macros to establish the accelerometer orientation in your cabinet
-#endif
-
-
-
-// --------------------------------------------------------------------------
-//
-// Define a symbol to tell us whether any sort of plunger sensor code
-// is enabled in this build.  Note that this doesn't tell us that a
-// plunger device is actually attached or *currently* enabled; it just
-// tells us whether or not the code for plunger sensing is enabled in 
-// the software build.  This lets us leave out some unnecessary code
-// on installations where no physical plunger is attached.
-//
-const int PLUNGER_CODE_ENABLED =
-#if defined(ENABLE_CCD_SENSOR) || defined(ENABLE_POT_SENSOR)
-    1;
-#else
-    0;
-#endif
 
 // ---------------------------------------------------------------------------
 //
@@ -369,6 +223,47 @@
 
 // ---------------------------------------------------------------------------
 //
+// Wire protocol value translations.  These translate byte values from
+// the USB protocol to local native format.
+//
+
+// unsigned 16-bit integer 
+inline uint16_t wireUI16(const uint8_t *b)
+{
+    return b[0] | ((uint16_t)b[1] << 8);
+}
+
+inline int16_t wireI16(const uint8_t *b)
+{
+    return (int16_t)wireUI16(b);
+}
+
+inline uint32_t wireUI32(const uint8_t *b)
+{
+    return b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24);
+}
+
+inline int32_t wireI32(const uint8_t *b)
+{
+    return (int32_t)wireUI32(b);
+}
+
+inline PinName wirePinName(int c)
+{
+    static const PinName p[] =  {
+        NC,    PTA1,  PTA2,  PTA4,  PTA5,  PTA12, PTA13, PTA16, PTA17, PTB0,   // 0-9
+        PTB1,  PTB2,  PTB3,  PTB8,  PTB9,  PTB10, PTB11, PTC0,  PTC1,  PTC2,   // 10-19
+        PTC3,  PTC4,  PTC5,  PTC6,  PTC7,  PTC8,  PTC9,  PTC10, PTC11, PTC12,  // 20-29
+        PTC13, PTC16, PTC17, PTD0,  PTD1,  PTD2,  PTD3,  PTD4,  PTD5,  PTD6,   // 30-39
+        PTD7,  PTE0,  PTE1,  PTE2,  PTE3,  PTE4,  PTE5,  PTE20, PTE21, PTE22,  // 40-49 
+        PTE23, PTE29, PTE30, PTE31                                             // 50-53
+    };
+    return (c < countof(p) ? p[c] : NC);
+}
+
+
+// ---------------------------------------------------------------------------
+//
 // LedWiz emulation, and enhanced TLC5940 output controller
 //
 // There are two modes for this feature.  The default mode uses the on-board
@@ -407,18 +302,15 @@
     virtual void set(float val) = 0;
 };
 
-// LwOut class for unmapped ports.  The LedWiz protocol is hardwired
-// for 32 ports, but we might not want to assign all 32 software ports
-// to physical output pins - the KL25Z has a limited number of GPIO
-// ports, so we might not have enough available GPIOs to fill out the
-// full LedWiz complement after assigning GPIOs for other functions.
-// This class is used to populate the LedWiz mapping array for ports
-// that aren't connected to physical outputs; it simply ignores value 
-// changes.
-class LwUnusedOut: public LwOut
+// LwOut class for virtual ports.  This type of port is visible to
+// the host software, but isn't connected to any physical output.
+// This can be used for special software-only ports like the ZB
+// Launch Ball output, or simply for placeholders in the LedWiz port
+// numbering.
+class LwVirtualOut: public LwOut
 {
 public:
-    LwUnusedOut() { }
+    LwVirtualOut() { }
     virtual void set(float val) { }
 };
 
@@ -436,13 +328,19 @@
 };
 
 
-#if TLC5940_NCHIPS
+//
+// The TLC5940 interface object.  We'll set this up with the port 
+// assignments set in config.h.
 //
-// The TLC5940 interface object.  Set this up with the port assignments
-// set in config.h.
-//
-TLC5940 tlc5940(TLC5940_SCLK, TLC5940_SIN, TLC5940_GSCLK, TLC5940_BLANK,
-    TLC5940_XLAT, TLC5940_NCHIPS);
+TLC5940 *tlc5940 = 0;
+void init_tlc5940(Config &cfg)
+{
+    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);
+    }
+}
 
 // LwOut class for TLC5940 outputs.  These are fully PWM capable.
 // The 'idx' value in the constructor is the output index in the
@@ -456,35 +354,27 @@
     virtual void set(float val)
     {
         if (val != prv)
-           tlc5940.set(idx, (int)((prv = val) * 4095));
+           tlc5940->set(idx, (int)((prv = val) * 4095));
     }
     int idx;
     float prv;
 };
 
-#else
-// No TLC5940 chips are attached, so we shouldn't encounter any ports
-// in the map marked for TLC5940 outputs.  If we do, treat them as unused.
-class Lw5940Out: public LwUnusedOut
-{
-public:
-    Lw5940Out(int idx) { }
-};
 
-// dummy tlc5940 interface
-class Dummy5940
-{
-public:
-    void start() { }
-};
-Dummy5940 tlc5940;
-
-#endif // TLC5940_NCHIPS
-
-#if HC595_NCHIPS
 // 74HC595 interface object.  Set this up with the port assignments in
 // config.h.
-HC595 hc595(HC595_NCHIPS, HC595_SIN, HC595_SCLK, HC595_LATCH, HC595_ENA);
+HC595 *hc595 = 0;
+
+// initialize the 74HC595 interface
+void init_hc595(Config &cfg)
+{
+    if (cfg.hc595.nchips != 0)
+    {
+        hc595 = new HC595(cfg.hc595.nchips, cfg.hc595.sin, cfg.hc595.sclk, cfg.hc595.latch, cfg.hc595.ena);
+        hc595->init();
+        hc595->update();
+    }
+}
 
 // LwOut class for 74HC595 outputs.  These are simple digial outs.
 // The 'idx' value in the constructor is the output index in the
@@ -498,31 +388,12 @@
     virtual void set(float val)
     {
         if (val != prv)
-           hc595.set(idx, (prv = val) == 0.0 ? 0 : 1);
+           hc595->set(idx, (prv = val) == 0.0 ? 0 : 1);
     }
     int idx;
     float prv;
 };
 
-#else // HC595_NCHIPS
-// No 74HC595 chips are attached, so we shouldn't encounter any ports
-// in the map marked for these outputs.  If we do, treat them as unused.
-class Lw595Out: public LwUnusedOut
-{
-public:
-    Lw595Out(int idx) { }
-};
-
-// dummy placeholder class
-class DummyHC595 
-{
-public:
-    void init() { }
-    void update() { }
-};
-DummyHC595 hc595;
-
-#endif // HC595_NCHIPS
 
 // 
 // Default LedWiz mode - using on-board GPIO ports.  In this mode, we
@@ -562,14 +433,23 @@
 
 // Array of output physical pin assignments.  This array is indexed
 // by LedWiz logical port number - lwPin[n] is the maping for LedWiz
-// port n (0-based).  If we're using GPIO ports to implement outputs,
-// we initialize the array at start-up to map each logical port to the 
-// physical GPIO pin for the port specified in the ledWizPortMap[] 
-// array in config.h.  If we're using TLC5940 chips for the outputs,
-// we map each logical port to the corresponding TLC5940 output.
+// port n (0-based).  
+//
+// Each pin is handled by an interface object for the physical output 
+// type for the port, as set in the configuration.  The interface 
+// objects handle the specifics of addressing the different hardware
+// types (GPIO PWM ports, GPIO digital ports, TLC5940 ports, and
+// 74HC595 ports).
 static int numOutputs;
 static LwOut **lwPin;
 
+// Number of LedWiz emulation outputs.  This is the number of ports
+// accessible through the standard (non-extended) LedWiz protocol
+// messages.  The protocol has a fixed set of 32 outputs, but we
+// might have fewer actual outputs.  This is therefore set to the
+// lower of 32 or the actual number of outputs.
+static int numLwOutputs;
+
 // Current absolute brightness level for an output.  This is a float
 // value from 0.0 for fully off to 1.0 for fully on.  This is the final
 // derived value for the port.  For outputs set by LedWiz messages, 
@@ -579,90 +459,67 @@
 static float *outLevel;
 
 // initialize the output pin array
-void initLwOut()
+void initLwOut(Config &cfg)
 {
-    // Figure out how many outputs we have.  We always have at least
-    // 32 outputs, since that's the number fixed by the original LedWiz
-    // protocol.  If we're using TLC5940 chips, each chip provides 16
-    // outputs.  Likewise, each 74HC595 provides 8 outputs.
-    
-    // start with 16 ports per TLC5940 and 8 per 74HC595
-    numOutputs = TLC5940_NCHIPS*16 + HC595_NCHIPS*8;
-    
-    // add outputs explicitly assigned to GPIO pins or not connected
+    // Count the outputs.  The first disabled output determines the
+    // total number of ports.
+    numOutputs = MAX_OUT_PORTS;
     int i;
-    for (i = 0 ; i < countof(ledWizPortMap) ; ++i)
+    for (i = 0 ; i < MAX_OUT_PORTS ; ++i)
     {
-        switch (ledWizPortMap[i].typ)
+        if (cfg.outPort[i].typ == PortTypeDisabled)
         {
-        case DIG_GPIO:
-        case PWM_GPIO:
-        case NO_PORT:
-            // count an explicitly GPIO port
-            ++numOutputs;
-            break;
-            
-        default:
-            // DON'T count TLC5940 or 74HC595 ports, as we've already
-            // counted all of these above
+            numOutputs = i;
             break;
         }
     }
     
-    // always set up at least 32 outputs, so that we don't have to
-    // check bounds on commands from the basic LedWiz protocol
-    if (numOutputs < 32)
-        numOutputs = 32;
-        
+    // the real LedWiz protocol can access at most 32 ports, or the
+    // actual number of outputs, whichever is lower
+    numLwOutputs = (numOutputs < 32 ? numOutputs : 32);
+    
     // allocate the pin array
     lwPin = new LwOut*[numOutputs];    
     
-    // allocate the current brightness array
-    outLevel = new float[numOutputs];
+    // Allocate the current brightness array.
+    outLevel = new float[numOutputs < 32 ? 32 : numOutputs];
     
-    // allocate a temporary array to keep track of which physical 
-    // TLC5940 ports we've assigned so far
-    char *tlcasi = new char[TLC5940_NCHIPS*16+1];
-    memset(tlcasi, 0, TLC5940_NCHIPS*16);
+    // create the pin interface object for each port
+    for (i = 0 ; i < numOutputs ; ++i)
+    {
+        // get this item's values
+        int typ = cfg.outPort[i].typ;
+        int pin = cfg.outPort[i].pin;
+        int flags = cfg.outPort[i].flags;
+        int activeLow = flags & PortFlagActiveLow;
 
-    // likewise for the 74HC595 ports
-    char *hcasi = new char[HC595_NCHIPS*8+1];
-    memset(hcasi, 0, HC595_NCHIPS*8);
-
-    // assign all pins from the explicit port map in config.h
-    for (i = 0 ; i < countof(ledWizPortMap) ; ++i)
-    {
-        int pin = ledWizPortMap[i].pin;
-        LWPortType typ = ledWizPortMap[i].typ;
-        int flags = ledWizPortMap[i].flags;
-        int activeLow = flags & PORT_ACTIVE_LOW;
+        // create the pin interface object according to the port type        
         switch (typ)
         {
-        case DIG_GPIO:
-            lwPin[i] = new LwDigOut((PinName)pin);
+        case PortTypeGPIOPWM:
+            // PWM GPIO port
+            lwPin[i] = new LwPwmOut(wirePinName(pin));
             break;
         
-        case PWM_GPIO:
-            // PWM GPIO port
-            lwPin[i] = new LwPwmOut((PinName)pin);
+        case PortTypeGPIODig:
+            // Digital GPIO port
+            lwPin[i] = new LwDigOut(wirePinName(pin));
             break;
         
-        case TLC_PORT:
-            // TLC5940 port (note that the nominal pin in the map is 1-based, so we
-            // have to decrement it to get the real pin index)
-            lwPin[i] = new Lw5940Out(pin-1);
-            tlcasi[pin-1] = 1;
+        case PortTypeTLC5940:
+            // TLC5940 port
+            lwPin[i] = new Lw5940Out(pin);
             break;
         
-        case HC595_PORT:
-            // 74HC595 port (the pin in the map is 1-based, so decrement it to get the 
-            // real pin index)
-            lwPin[i] = new Lw595Out(pin-1);
-            hcasi[pin-1] = 1;
+        case PortType74HC595:
+            // 74HC595 port
+            lwPin[i] = new Lw595Out(pin);
             break;
-            
+
+        case PortTypeVirtual:
         default:
-            lwPin[i] = new LwUnusedOut();
+            // virtual or unknown
+            lwPin[i] = new LwVirtualOut();
             break;
         }
         
@@ -673,41 +530,6 @@
         // turn it off initially      
         lwPin[i]->set(0);
     }
-    
-    // If we haven't assigned all of the LedWiz ports to physical pins,
-    // fill out the unassigned LedWiz ports with any unassigned TLC5940
-    // pins, then with any unassigned 74HC595 ports.
-    int tlcnxt, hcnxt;
-    for (tlcnxt = 0 ; tlcnxt < TLC5940_NCHIPS*16 && tlcasi[tlcnxt] ; ++tlcnxt) ;
-    for (hcnxt = 0 ; hcnxt < HC595_NCHIPS*8 && hcasi[hcnxt] ; ++hcnxt) ;
-    for ( ; i < numOutputs ; ++i)
-    {
-        // If we have any more unassigned TLC5940 outputs, assign this LedWiz
-        // port to the next available TLC5940 output, or the next 74HC595 output
-        // if we're out of TLC5940 outputs.  Leave it unassigned if there are
-        // no more unassigned ports of any type.
-        if (tlcnxt < TLC5940_NCHIPS*16)
-        {
-            // assign this available TLC5940 pin, and find the next unused one
-            lwPin[i] = new Lw5940Out(tlcnxt);
-            for (++tlcnxt ; tlcnxt < TLC5940_NCHIPS*16 && tlcasi[tlcnxt] ; ++tlcnxt) ;
-        }
-        else if (hcnxt < HC595_NCHIPS*8)
-        {
-            // assign this available 74HC595 pin, and find the next unused one
-            lwPin[i] = new Lw595Out(hcnxt);
-            for (++hcnxt ; hcnxt < HC595_NCHIPS*8 && hcasi[hcnxt] ; ++hcnxt) ;
-        }
-        else
-        {
-            // no more ports available - set up this port as unconnected
-            lwPin[i] = new LwUnusedOut();
-        }
-    }
-    
-    // done with the temporary TLC5940 and 74HC595 port assignment lists
-    delete [] tlcasi;
-    delete [] hcasi;
 }
 
 // LedWiz output states.
@@ -855,7 +677,7 @@
     
     // if we have any flashing lights, update them
     int ena = false;
-    for (int i = 0 ; i < 32 ; ++i)
+    for (int i = 0 ; i < numLwOutputs ; ++i)
     {
         if (wizOn[i])
         {
@@ -884,7 +706,7 @@
 {
     // update each output
     int pulse = false;
-    for (int i = 0 ; i < 32 ; ++i)
+    for (int i = 0 ; i < numLwOutputs ; ++i)
     {
         pulse |= (wizVal[i] >= 129 && wizVal[i] <= 132);
         lwPin[i]->set(wizState(i));
@@ -896,7 +718,8 @@
         wizPulseTimer.attach(wizPulse, WIZ_PULSE_TIME_BASE);
         
     // flush changes to 74HC595 chips, if attached
-    hc595.update();
+    if (hc595 != 0)
+        hc595->update();
 }
         
 // ---------------------------------------------------------------------------
@@ -904,12 +727,14 @@
 // Button input
 //
 
-// button input map array
-DigitalIn *buttonDigIn[32];
-
 // button state
 struct ButtonState
 {
+    ButtonState() : di(NULL), pressed(0), t(0), js(0), keymod(0), keycode(0) { }
+    
+    // DigitalIn for the button
+    DigitalIn *di;
+
     // current on/off state
     int pressed;
     
@@ -917,49 +742,120 @@
     // state transition occurs, we set this to a debounce
     // period.  Future state transitions will be ignored
     // until the debounce time elapses.
-    int t;
-} buttonState[32];
+    float t;
+    
+    // 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;
+    
+
+} buttonState[MAX_BUTTONS];
 
 // timer for button reports
 static Timer buttonTimer;
 
 // initialize the button inputs
-void initButtons()
+void initButtons(Config &cfg, bool &kbKeys)
 {
+    // presume we'll find no keyboard keys
+    kbKeys = false;
+    
     // create the digital inputs
-    for (int i = 0 ; i < countof(buttonDigIn) ; ++i)
+    ButtonState *bs = buttonState;
+    for (int i = 0 ; i < MAX_BUTTONS ; ++i, ++bs)
     {
-        if (i < countof(buttonMap) && buttonMap[i] != NC)
-            buttonDigIn[i] = new DigitalIn(buttonMap[i]);
-        else
-            buttonDigIn[i] = 0;
+        PinName pin = wirePinName(cfg.button[i].pin);
+        if (pin != NC)
+        {
+            // set up the GPIO input pin for this button
+            bs->di = new DigitalIn(pin);
+            
+            // note if it's a keyboard key of some kind (including media keys)
+            uint8_t val = cfg.button[i].val;
+            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;
+                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;
+            }
+        }
     }
     
     // start the button timer
+    buttonTimer.reset();
     buttonTimer.start();
 }
 
+// Button data
+uint32_t jsButtons = 0;
+
+// Keyboard state
+struct
+{
+    bool changed;       // flag: changed since last report sent
+    int nkeys;          // number of active keys in the list
+    uint8_t data[8];    // key state, in USB report format: byte 0 is the modifier key mask,
+                        // byte 1 is reserved, and bytes 2-7 are the currently pressed key codes
+} kbState = { false, 0, { 0, 0, 0, 0, 0, 0, 0, 0 } };
+
+// Media key state
+struct
+{
+    bool changed;       // flag: changed since last report sent
+    uint8_t data;       // key state byte for USB reports
+} mediaState = { false, 0 };
 
 // read the button input state
-uint32_t readButtons()
+void readButtons(Config &cfg)
 {
-    // start with all buttons off
-    uint32_t buttons = 0;
+    // start with an empty list of USB key codes
+    uint8_t modkeys = 0;
+    uint8_t keys[7] = { 0, 0, 0, 0, 0, 0, 0 };
+    int nkeys = 0;
     
+    // clear the joystick buttons
+    jsButtons = 0;
+    
+    // start with no media keys pressed
+    uint8_t mediakeys = 0;
+
     // figure the time elapsed since the last scan
-    int dt = buttonTimer.read_ms();
+    float dt = buttonTimer.read();
     
-    // reset the timef for the next scan
+    // reset the time for the next scan
     buttonTimer.reset();
     
     // scan the button list
-    uint32_t bit = 1;
-    DigitalIn **di = buttonDigIn;
     ButtonState *bs = buttonState;
-    for (int i = 0 ; i < countof(buttonDigIn) ; ++i, ++di, ++bs, bit <<= 1)
+    for (int i = 0 ; i < MAX_BUTTONS ; ++i, ++bs)
     {
         // read this button
-        if (*di != 0)
+        if (bs->di != 0)
         {
             // deduct the elapsed time since the last update
             // from the button's remaining sticky time
@@ -976,7 +872,7 @@
             if (bs->t == 0)
             {
                 // get the new physical state
-                int pressed = !(*di)->read();
+                int pressed = !bs->di->read();
                 
                 // update the button's logical state if this is a change
                 if (pressed != bs->pressed)
@@ -986,18 +882,51 @@
                     
                     // start a new sticky period for debouncing this
                     // state change
-                    bs->t = 25;
+                    bs->t = 0.005;
                 }
             }
-            
-            // if it's pressed, OR its bit into the state
+
+            // if it's pressed, add it to the appropriate key state list
             if (bs->pressed)
-                buttons |= bit;
+            {
+                // OR in the joystick button bit, mod key bits, and media key bits
+                jsButtons |= 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;
+            }
         }
     }
     
-    // return the new button list
-    return buttons;
+    // Check for changes to the keyboard keys
+    if (kbState.data[0] != modkeys
+        || kbState.nkeys != nkeys
+        || memcmp(keys, &kbState.data[2], 6) != 0)
+    {
+        // we have changes - set the change flag and store the new key data
+        kbState.changed = true;
+        kbState.data[0] = modkeys;
+        if (nkeys <= 6) {
+            // 6 or fewer simultaneous keys - report the key codes
+            kbState.nkeys = nkeys;
+            memcpy(&kbState.data[2], keys, 6);
+        }
+        else {
+            // more than 6 simultaneous keys - report rollover (all '1' key codes)
+            kbState.nkeys = 6;
+            memset(&kbState.data[2], 1, 6);
+        }
+    }        
+    
+    // Check for changes to media keys
+    if (mediaState.data != mediakeys)
+    {
+        mediaState.changed = true;
+        mediaState.data = mediakeys;
+    }
 }
 
 // ---------------------------------------------------------------------------
@@ -1008,8 +937,9 @@
 class MyUSBJoystick: public USBJoystick
 {
 public:
-    MyUSBJoystick(uint16_t vendor_id, uint16_t product_id, uint16_t product_release) 
-        : USBJoystick(vendor_id, product_id, product_release, true)
+    MyUSBJoystick(uint16_t vendor_id, uint16_t product_id, uint16_t product_release,
+        bool waitForConnect, bool enableJoystick, bool useKB) 
+        : USBJoystick(vendor_id, product_id, product_release, waitForConnect, enableJoystick, useKB)
     {
         suspended_ = false;
     }
@@ -1347,116 +1277,6 @@
  
 // ---------------------------------------------------------------------------
 //
-// 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.
-//
-
-#if defined(ENABLE_CCD_SENSOR)
-#include "ccdSensor.h"
-#elif defined(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 and version, to verify that we saved the config
-    // data to flash on a past run (as opposed to uninitialized
-    // data from a firmware update)
-    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
-        d.sig = SIGNATURE;
-        d.vsn = VERSION;
-        d.sz = sizeof(NVM);
-        checksum = CRC32(&d, sizeof(d));
-        
-        // 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;
-};
-
-// ---------------------------------------------------------------------------
-//
 // Simple binary (on/off) input debouncer.  Requires an input to be stable 
 // for a given interval before allowing an update.
 //
@@ -1527,7 +1347,7 @@
 void allOutputsOff()
 {
     // reset all LedWiz outputs to OFF/48
-    for (int i = 0 ; i < 32 ; ++i)
+    for (int i = 0 ; i < numLwOutputs ; ++i)
     {
         outLevel[i] = 0;
         wizOn[i] = 0;
@@ -1546,7 +1366,8 @@
     wizSpeed = 2;
     
     // flush changes to hc595, if applicable
-    hc595.update();
+    if (hc595 != 0)
+        hc595->update();
 }
 
 // ---------------------------------------------------------------------------
@@ -1615,7 +1436,6 @@
 //   so we don't want to push the button on a TV that's already on.
 //   
 //
-#ifdef ENABLE_TV_TIMER
 
 // Current PSU2 state:
 //   1 -> default: latch was on at last check, or we haven't checked yet
@@ -1625,12 +1445,22 @@
 //   5 -> TV relay on
 //   
 int psu2_state = 1;
-DigitalIn psu2_status_sense(PSU2_STATUS_SENSE);
-DigitalOut psu2_status_set(PSU2_STATUS_SET);
-DigitalOut tv_relay(TV_RELAY_PIN);
-Timer tv_timer;
+
+// PSU2 power sensing circuit connections
+DigitalIn *psu2_status_sense;
+DigitalOut *psu2_status_set;
+
+// TV ON switch relay control
+DigitalOut *tv_relay;
+
+// Timer interrupt
+Ticker tv_ticker;
+float tv_delay_time;
 void TVTimerInt()
 {
+    // time since last state change
+    static Timer tv_timer;
+
     // Check our internal state
     switch (psu2_state)
     {
@@ -1640,20 +1470,20 @@
         // either case, if the latch is off, switch to state 2 and
         // try pulsing the latch.  Next time we check, if the latch
         // stuck, it means that PSU2 is now on after being off.
-        if (!psu2_status_sense)
+        if (!psu2_status_sense->read())
         {
             // switch to OFF state
             psu2_state = 2;
             
             // try setting the latch
-            psu2_status_set = 1;
+            psu2_status_set->write(1);
         }
         break;
         
     case 2:
         // PSU2 was off last time we checked, and we tried setting
         // the latch.  Drop the SET signal and go to CHECK state.
-        psu2_status_set = 0;
+        psu2_status_set->write(0);
         psu2_state = 3;
         break;
         
@@ -1662,7 +1492,7 @@
         // if that stuck.  If the latch is now on, PSU2 has transitioned
         // from OFF to ON, so start the TV countdown.  If the latch is
         // off, our SET command didn't stick, so PSU2 is still off.
-        if (psu2_status_sense)
+        if (psu2_status_sense->read())
         {
             // The latch stuck, so PSU2 has transitioned from OFF
             // to ON.  Start the TV countdown timer.
@@ -1675,7 +1505,7 @@
             // The latch didn't stick, so PSU2 was still off at
             // our last check.  Try pulsing it again in case PSU2
             // was turned on since the last check.
-            psu2_status_set = 1;
+            psu2_status_set->write(1);
             psu2_state = 2;
         }
         break;
@@ -1683,10 +1513,10 @@
     case 4:
         // TV timer countdown in progress.  If we've reached the
         // delay time, pulse the relay.
-        if (tv_timer.read() >= TV_DELAY_TIME)
+        if (tv_timer.read() >= tv_delay_time)
         {
             // turn on the relay for one timer interval
-            tv_relay = 1;
+            tv_relay->write(1);
             psu2_state = 5;
         }
         break;
@@ -1694,27 +1524,631 @@
     case 5:
         // TV timer relay on.  We pulse this for one interval, so
         // it's now time to turn it off and return to the default state.
-        tv_relay = 0;
+        tv_relay->write(0);
         psu2_state = 1;
         break;
     }
 }
 
-Ticker tv_ticker;
-void startTVTimer()
+// Start the TV ON checker.  If the status sense circuit is enabled in
+// the configuration, we'll set up the pin connections and start the
+// interrupt handler that periodically checks the status.  Does nothing
+// if any of the pins are configured as NC.
+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)
+    {
+        psu2_status_sense = new DigitalIn(cfg.TVON.statusPin);
+        psu2_status_set = new DigitalOut(cfg.TVON.latchPin);
+        tv_relay = new DigitalOut(cfg.TVON.relayPin);
+        tv_delay_time = cfg.TVON.delayTime;
+    
+        // Set up our time routine to run every 1/4 second.  
+        tv_ticker.attach(&TVTimerInt, 0.25);
+    }
+}
+
+// ---------------------------------------------------------------------------
+//
+// In-memory configuration data structure.  This is the live version in RAM
+// that we use to determine how things are set up.
+//
+// When we save the configuration settings, we copy this structure to
+// non-volatile flash memory.  At startup, we check the flash location where
+// we might have saved settings on a previous run, and it's valid, we copy 
+// the flash data to this structure.  Firmware updates wipe the flash
+// memory area, so you have to use the PC config tool to send the settings
+// again each time the firmware is updated.
+//
+NVM nvm;
+
+// For convenience, a macro for the Config part of the NVM structure
+#define cfg (nvm.d.c)
+
+// flash memory controller interface
+FreescaleIAP iap;
+
+// figure the flash address as a pointer along with the number of sectors
+// required to store the structure
+NVM *configFlashAddr(int &addr, int &numSectors)
+{
+    // figure how many flash sectors we span, rounding up to whole sectors
+    numSectors = (sizeof(NVM) + SECTOR_SIZE - 1)/SECTOR_SIZE;
+
+    // figure the address - this is the highest flash address where the
+    // structure will fit with the start aligned on a sector boundary
+    addr = iap.flash_size() - (numSectors * SECTOR_SIZE);
+    
+    // return the address as a pointer
+    return (NVM *)addr;
+}
+
+// figure the flash address as a pointer
+NVM *configFlashAddr()
+{
+    int addr, numSectors;
+    return configFlashAddr(addr, numSectors);
+}
+
+// Load the config from flash
+void loadConfigFromFlash()
+{
+    // We want to use the KL25Z's on-board flash to store our configuration
+    // data persistently, so that we can restore it across power cycles.
+    // Unfortunatly, the mbed platform doesn't explicitly support this.
+    // mbed treats the on-board flash as a raw storage device for linker
+    // output, and assumes that the linker output is the only thing
+    // stored there.  There's no file system and no allowance for shared
+    // use for other purposes.  Fortunately, the linker ues the space in
+    // the obvious way, storing the entire linked program in a contiguous
+    // block starting at the lowest flash address.  This means that the
+    // rest of flash - from the end of the linked program to the highest
+    // flash address - is all unused free space.  Writing our data there
+    // won't conflict with anything else.  Since the linker doesn't give
+    // us any programmatic access to the total linker output size, it's
+    // safest to just store our config data at the very end of the flash
+    // region (i.e., the highest address).  As long as it's smaller than
+    // the free space, it won't collide with the linker area.
+    
+    // Figure how many sectors we need for our structure
+    NVM *flash = configFlashAddr();
+    
+    // if the flash is valid, load it; otherwise initialize to defaults
+    if (flash->valid()) 
+    {
+        // flash is valid - load it into the RAM copy of the structure
+        memcpy(&nvm, flash, sizeof(NVM));
+    }
+    else 
+    {
+        // flash is invalid - load factory settings nito RAM structure
+        cfg.setFactoryDefaults();
+    }
+}
+
+void saveConfigToFlash()
 {
-    // Set up our time routine to run every 1/4 second.  
-    tv_ticker.attach(&TVTimerInt, 0.25);
+    int addr, sectors;
+    configFlashAddr(addr, sectors);
+    nvm.save(iap, addr);
+}
+
+// ---------------------------------------------------------------------------
+//
+// Plunger Sensor
+//
+
+// the plunger sensor interface object
+PlungerSensor *plungerSensor = 0;
+
+// Create the plunger sensor based on the current configuration.  If 
+// there's already a sensor object, we'll delete it.
+void createPlunger()
+{
+    // delete any existing sensor object
+    if (plungerSensor != 0)
+        delete plungerSensor;
+        
+    // create the new sensor object according to the type
+    switch (cfg.plunger.sensorType)
+    {
+    case PlungerType_TSL1410RS:
+        // pins are: SI, CLOCK, AO
+        plungerSensor = new PlungerSensorTSL1410R(cfg.plunger.sensorPin[0], cfg.plunger.sensorPin[1], 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]);
+        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);
+        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]);
+        break;
+    
+    case PlungerType_Pot:
+        // pins are: AO
+        plungerSensor = new PlungerSensorPot(cfg.plunger.sensorPin[0]);
+        break;
+    
+    case PlungerType_None:
+    default:
+        plungerSensor = new PlungerSensorNull();
+        break;
+    }
 }
 
+// ---------------------------------------------------------------------------
+//
+// Reboot - resets the microcontroller
+//
+void reboot(USBJoystick &js)
+{
+    // disconnect from USB
+    js.disconnect();
+    
+    // wait a few seconds to make sure the host notices the disconnect
+    wait(5);
+    
+    // reset the device
+    NVIC_SystemReset();
+    while (true) { }
+}
+
+// ---------------------------------------------------------------------------
+//
+// Translate joystick readings from raw values to reported values, based
+// on the orientation of the controller card in the cabinet.
+//
+void accelRotate(int &x, int &y)
+{
+    int tmp;
+    switch (cfg.orientation)
+    {
+    case OrientationFront:
+        tmp = x;
+        x = y;
+        y = tmp;
+        break;
+    
+    case OrientationLeft:
+        x = -x;
+        break;
+    
+    case OrientationRight:
+        y = -y;
+        break;
+    
+    case OrientationRear:
+        tmp = -x;
+        x = -y;
+        y = tmp;
+        break;
+    }
+}
+
+// ---------------------------------------------------------------------------
+//
+// 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:
+//    0x0001  -> plunger sensor enabled
+//    0x8000  -> RESERVED - must always be zero
+//
+// Note that the high bit (0x8000) must always be 0, since we use that
+// to distinguish special request reply packets.
+uint16_t statusFlags;
+    
+// flag: send a pixel dump after the next read
+bool reportPix = false;
+
 
-#else // ENABLE_TV_TIMER
+// ---------------------------------------------------------------------------
+//
+// Calibration button state:
+//  0 = not pushed
+//  1 = pushed, not yet debounced
+//  2 = pushed, debounced, waiting for hold time
+//  3 = pushed, hold time completed - in calibration mode
+int calBtnState = 0;
+
+// calibration button debounce timer
+Timer calBtnTimer;
+
+// calibration button light state
+int calBtnLit = false;
+    
+
+// ---------------------------------------------------------------------------
+//
+// Handle a configuration variable update.  'data' is the USB message we
+// received from the host.
+//
+void configVarMsg(uint8_t *data)
+{
+    switch (data[1])
+    {
+    case 1:
+        // USB identification (Vendor ID, Product ID)
+        cfg.usbVendorID = wireUI16(data+2);
+        cfg.usbProductID = wireUI16(data+4);
+        break;
+        
+    case 2:
+        // Pinscape Controller unit number - note that data[2] contains
+        // the nominal unit number, 1-16
+        if (data[2] >= 1 && data[2] <= 16)
+            cfg.psUnitNo = data[2];
+        break;
+        
+    case 3:
+        // Enable/disable joystick
+        cfg.joystickEnabled = data[2];
+        break;
+        
+    case 4:
+        // Accelerometer orientation
+        cfg.orientation = data[2];
+        break;
+
+    case 5:
+        // Plunger sensor type
+        cfg.plunger.sensorType = data[2];
+        break;
+        
+    case 6:
+        // Set plunger pin assignments
+        cfg.plunger.sensorPin[0] = wirePinName(data[2]);
+        cfg.plunger.sensorPin[1] = wirePinName(data[3]);
+        cfg.plunger.sensorPin[2] = wirePinName(data[4]);
+        cfg.plunger.sensorPin[3] = wirePinName(data[5]);
+        break;
+        
+    case 7:
+        // Plunger calibration button and indicator light pin assignments
+        cfg.plunger.cal.btn = wirePinName(data[2]);
+        cfg.plunger.cal.led = wirePinName(data[3]);
+        break;
+        
+    case 8:
+        // ZB Launch Ball setup
+        cfg.plunger.zbLaunchBall.port = (int)(unsigned char)data[2];
+        cfg.plunger.zbLaunchBall.btn = (int)(unsigned char)data[3];
+        cfg.plunger.zbLaunchBall.pushDistance = (float)wireUI16(data+4) / 1000.0;
+        break;
+        
+    case 9:
+        // TV ON setup
+        cfg.TVON.statusPin = wirePinName(data[2]);
+        cfg.TVON.latchPin = wirePinName(data[3]);
+        cfg.TVON.relayPin = wirePinName(data[4]);
+        cfg.TVON.delayTime = (float)wireUI16(data+5) / 100.0;
+        break;
+        
+    case 10:
+        // TLC5940NT PWM controller chip setup
+        cfg.tlc5940.nchips = (int)(unsigned char)data[2];
+        cfg.tlc5940.sin = wirePinName(data[3]);
+        cfg.tlc5940.sclk = wirePinName(data[4]);
+        cfg.tlc5940.xlat = wirePinName(data[5]);
+        cfg.tlc5940.blank = wirePinName(data[6]);
+        cfg.tlc5940.gsclk = wirePinName(data[7]);
+        break;
+        
+    case 11:
+        // 74HC595 shift register chip setup
+        cfg.hc595.nchips = (int)(unsigned char)data[2];
+        cfg.hc595.sin = wirePinName(data[3]);
+        cfg.hc595.sclk = wirePinName(data[4]);
+        cfg.hc595.latch = wirePinName(data[5]);
+        cfg.hc595.ena = wirePinName(data[6]);
+        break;
+        
+    case 12:
+        // button setup
+        {
+            // get the button number
+            int idx = data[2];
+            
+            // if it's in range, set the button data
+            if (idx > 0 && idx <= MAX_BUTTONS)
+            {
+                // adjust to an array index
+                --idx;
+                
+                // set the values
+                cfg.button[idx].pin = data[3];
+                cfg.button[idx].typ = data[4];
+                cfg.button[idx].val = data[5];
+            }
+        }
+        break;
+        
+    case 13:
+        // LedWiz output port setup
+        {
+            // get the port number
+            int idx = data[2];
+            
+            // if it's in range, set the port data
+            if (idx > 0 && idx <= MAX_OUT_PORTS)
+            {
+                // adjust to an array index
+                --idx;
+                
+                // set the values
+                cfg.outPort[idx].typ = data[3];
+                cfg.outPort[idx].pin = data[4];
+                cfg.outPort[idx].flags = data[5];
+            }
+        }
+        break;
+    }
+}
+
+// ---------------------------------------------------------------------------
+//
+// Handle an input report from the USB host.  Input reports use our extended
+// LedWiz protocol.
 //
-// TV timer not used - just provide a dummy startup function
-void startTVTimer() { }
-//
-#endif // ENABLE_TV_TIMER
+void handleInputMsg(HID_REPORT &report, USBJoystick &js, int &z)
+{
+    // all Led-Wiz reports are exactly 8 bytes
+    if (report.length == 8)
+    {
+        // LedWiz commands come in two varieties:  SBA and PBA.  An
+        // SBA is marked by the first byte having value 64 (0x40).  In
+        // the real LedWiz protocol, any other value in the first byte
+        // means it's a PBA message.  However, *valid* PBA messages
+        // always have a first byte (and in fact all 8 bytes) in the
+        // range 0-49 or 129-132.  Anything else is invalid.  We take
+        // advantage of this to implement private protocol extensions.
+        // So our full protocol is as follows:
+        //
+        // first byte =
+        //   0-48     -> LWZ-PBA
+        //   64       -> LWZ SBA 
+        //   65       -> private control message; second byte specifies subtype
+        //   129-132  -> LWZ-PBA
+        //   200-228  -> extended bank brightness set for outputs N to N+6, where
+        //               N is (first byte - 200)*7
+        //   other    -> reserved for future use
+        //
+        uint8_t *data = report.data;
+        if (data[0] == 64) 
+        {
+            // LWZ-SBA - first four bytes are bit-packed on/off flags
+            // for the outputs; 5th byte is the pulse speed (1-7)
+            //printf("LWZ-SBA %02x %02x %02x %02x ; %02x\r\n",
+            //       data[1], data[2], data[3], data[4], data[5]);
+
+            // update all on/off states
+            for (int i = 0, bit = 1, ri = 1 ; i < numLwOutputs ; ++i, bit <<= 1)
+            {
+                // figure the on/off state bit for this output
+                if (bit == 0x100) {
+                    bit = 1;
+                    ++ri;
+                }
+                
+                // set the on/off state
+                wizOn[i] = ((data[ri] & bit) != 0);
+                
+                // If the wizVal setting is 255, it means that this
+                // output was last set to a brightness value with the
+                // extended protocol.  Return it to LedWiz control by
+                // rescaling the brightness setting to the LedWiz range
+                // and updating wizVal with the result.  If it's any
+                // other value, it was previously set by a PBA message,
+                // so simply retain the last setting - in the normal
+                // LedWiz protocol, the "profile" (brightness) and on/off
+                // states are independent, so an SBA just turns an output
+                // on or off but retains its last brightness level.
+                if (wizVal[i] == 255)
+                    wizVal[i] = (uint8_t)round(outLevel[i]*48);
+            }
+            
+            // set the flash speed - enforce the value range 1-7
+            wizSpeed = data[5];
+            if (wizSpeed < 1)
+                wizSpeed = 1;
+            else if (wizSpeed > 7)
+                wizSpeed = 7;
+
+            // update the physical outputs
+            updateWizOuts();
+            if (hc595 != 0)
+                hc595->update();
+            
+            // reset the PBA counter
+            pbaIdx = 0;
+        }
+        else if (data[0] == 65)
+        {
+            // Private control message.  This isn't an LedWiz message - it's
+            // an extension for this device.  65 is an invalid PBA setting,
+            // and isn't used for any other LedWiz message, so we appropriate
+            // it for our own private use.  The first byte specifies the 
+            // message type.
+            if (data[1] == 1)
+            {
+                // 1 = Old Set Configuration:
+                //     data[2] = LedWiz unit number (0x00 to 0x0f)
+                //     data[3] = feature enable bit mask:
+                //               0x01 = enable plunger sensor
+
+                // get the new LedWiz unit number - this is 0-15, whereas we
+                // we save the *nominal* unit number 1-16 in the config                
+                uint8_t newUnitNo = (data[2] & 0x0f) + 1;
 
+                // we'll need a reset if the LedWiz unit number is changing
+                bool needReset = (newUnitNo != cfg.psUnitNo);
+                
+                // set the configuration parameters from the message
+                cfg.psUnitNo = newUnitNo;
+                cfg.plunger.enabled = data[3] & 0x01;
+                
+                // update the status flags
+                statusFlags = (statusFlags & ~0x01) | (data[3] & 0x01);
+                
+                // if the plunger is no longer enabled, use 0 for z reports
+                if (!cfg.plunger.enabled)
+                    z = 0;
+                
+                // save the configuration
+                saveConfigToFlash();
+                
+                // reboot if necessary
+                if (needReset)
+                    reboot(js);
+            }
+            else if (data[1] == 2)
+            {
+                // 2 = Calibrate plunger
+                // (No parameters)
+                
+                // enter calibration mode
+                calBtnState = 3;
+                calBtnTimer.reset();
+                cfg.plunger.cal.reset(plungerSensor->npix);
+            }
+            else if (data[1] == 3)
+            {
+                // 3 = pixel dump
+                // (No parameters)
+                reportPix = true;
+                
+                // show purple until we finish sending the report
+                ledR = 0;
+                ledB = 0;
+                ledG = 1;
+            }
+            else if (data[1] == 4)
+            {
+                // 4 = hardware configuration query
+                // (No parameters)
+                wait_ms(1);
+                js.reportConfig(
+                    numOutputs, 
+                    cfg.psUnitNo - 1,   // report 0-15 range for unit number (we store 1-16 internally)
+                    cfg.plunger.cal.zero, cfg.plunger.cal.max);
+            }
+            else if (data[1] == 5)
+            {
+                // 5 = all outputs off, reset to LedWiz defaults
+                allOutputsOff();
+            }
+            else if (data[1] == 6)
+            {
+                // 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);
+            }
+        }
+        else if (data[0] == 66)
+        {
+            // Extended protocol - Set configuration variable.
+            // The second byte of the message is the ID of the variable
+            // to update, and the remaining bytes give the new value,
+            // in a variable-dependent format.
+            configVarMsg(data);
+        }
+        else if (data[0] >= 200 && data[0] <= 228)
+        {
+            // Extended protocol - Extended output port brightness update.  
+            // data[0]-200 gives us the bank of 7 outputs we're setting:
+            // 200 is outputs 0-6, 201 is outputs 7-13, 202 is 14-20, etc.
+            // The remaining bytes are brightness levels, 0-255, for the
+            // seven outputs in the selected bank.  The LedWiz flashing 
+            // modes aren't accessible in this message type; we can only 
+            // set a fixed brightness, but in exchange we get 8-bit 
+            // resolution rather than the paltry 0-48 scale that the real
+            // LedWiz uses.  There's no separate on/off status for outputs
+            // adjusted with this message type, either, as there would be
+            // for a PBA message - setting a non-zero value immediately
+            // turns the output, overriding the last SBA setting.
+            //
+            // For outputs 0-31, this overrides any previous PBA/SBA
+            // settings for the port.  Any subsequent PBA/SBA message will
+            // in turn override the setting made here.  It's simple - the
+            // most recent message of either type takes precedence.  For
+            // outputs above the LedWiz range, PBA/SBA messages can't
+            // address those ports anyway.
+            int i0 = (data[0] - 200)*7;
+            int i1 = i0 + 7 < numOutputs ? i0 + 7 : numOutputs; 
+            for (int i = i0 ; i < i1 ; ++i)
+            {
+                // set the brightness level for the output
+                float b = data[i-i0+1]/255.0;
+                outLevel[i] = b;
+                
+                // if it's in the basic LedWiz output set, set the LedWiz
+                // profile value to 255, which means "use outLevel"
+                if (i < 32) 
+                    wizVal[i] = 255;
+                    
+                // set the output
+                lwPin[i]->set(b);
+            }
+            
+            // update 74HC595 outputs, if attached
+            if (hc595 != 0)
+                hc595->update();
+        }
+        else 
+        {
+            // Everything else is LWZ-PBA.  This is a full "profile"
+            // dump from the host for one bank of 8 outputs.  Each
+            // byte sets one output in the current bank.  The current
+            // bank is implied; the bank starts at 0 and is reset to 0
+            // by any LWZ-SBA message, and is incremented to the next
+            // bank by each LWZ-PBA message.  Our variable pbaIdx keeps
+            // track of our notion of the current bank.  There's no direct
+            // way for the host to select the bank; it just has to count
+            // on us staying in sync.  In practice, the host will always
+            // send a full set of 4 PBA messages in a row to set all 32
+            // outputs.
+            //
+            // Note that a PBA implicitly overrides our extended profile
+            // messages (message prefix 200-219), because this sets the
+            // wizVal[] entry for each output, and that takes precedence
+            // over the extended protocol settings.
+            //
+            //printf("LWZ-PBA[%d] %02x %02x %02x %02x %02x %02x %02x %02x\r\n",
+            //       pbaIdx, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
+
+            // Update all output profile settings
+            for (int i = 0 ; i < 8 ; ++i)
+                wizVal[pbaIdx + i] = data[i];
+
+            // Update the physical LED state if this is the last bank.
+            // Note that hosts always send a full set of four PBA
+            // messages, so there's no need to do a physical update
+            // until we've received the last bank's PBA message.
+            if (pbaIdx == 24)
+            {
+                updateWizOuts();
+                if (hc595 != 0)
+                    hc595->update();
+                pbaIdx = 0;
+            }
+            else
+                pbaIdx += 8;
+        }
+    }
+}
 
 // ---------------------------------------------------------------------------
 //
@@ -1732,83 +2166,58 @@
     ledG = 1;
     ledB = 1;
     
+    // clear the I2C bus for the accelerometer
+    clear_i2c();
+    
+    // load the saved configuration
+    loadConfigFromFlash();
+    
     // start the TV timer, if applicable
-    startTVTimer();
+    startTVTimer(cfg);
     
     // we're not connected/awake yet
     bool connected = false;
     time_t connectChangeTime = time(0);
 
-    // initialize the LedWiz ports
-    initLwOut();
-    
-    // initialize the button input ports
-    initButtons();
+    // create the plunger sensor interface
+    createPlunger();
 
-    // start the TLC5940 clock, if present
-    tlc5940.start();
+    // set up the TLC5940 interface and start the TLC5940 clock, if applicable
+    init_tlc5940(cfg);
 
     // enable the 74HC595 chips, if present
-    hc595.init();
-    hc595.update();
-
-    // we don't need a reset yet
-    bool needReset = false;
+    init_hc595(cfg);
     
-    // clear the I2C bus for the accelerometer
-    clear_i2c();
-    
-    // set up a flash memory controller
-    FreescaleIAP iap;
-    
-    // use the last sector of flash for our non-volatile memory structure
-    int flash_addr = (iap.flash_size() - SECTOR_SIZE);
-    NVM *flash = (NVM *)flash_addr;
-    NVM cfg;
+    // initialize the LedWiz ports
+    initLwOut(cfg);
     
-    // if the flash is valid, load it; otherwise initialize to defaults
-    if (flash->valid()) {
-        memcpy(&cfg, flash, sizeof(cfg));
-        printf("Flash restored: plunger cal=%d, min=%d, zero=%d, max=%d\r\n", 
-            cfg.d.plungerCal, cfg.d.plungerMin, cfg.d.plungerZero, cfg.d.plungerMax);
-    }
-    else {
-        printf("Factory reset\r\n");
-        cfg.d.plungerCal = 0;
-        cfg.d.plungerMin = 0;        // assume we can go all the way forward...
-        cfg.d.plungerMax = npix;     // ...and all the way back
-        cfg.d.plungerZero = npix/6;  // the rest position is usually around 1/2" back
-        cfg.d.ledWizUnitNo = DEFAULT_LEDWIZ_UNIT_NUMBER - 1;  // unit numbering starts from 0 internally
-        cfg.d.plungerEnabled = PLUNGER_CODE_ENABLED;
-    }
-    
+    // start the TLC5940 clock
+    if (tlc5940 != 0)
+        tlc5940->start();
+        
+    // initialize the button input ports
+    bool kbKeys = false;
+    initButtons(cfg, kbKeys);
+
     // Create the joystick USB client.  Note that we use the LedWiz unit
     // number from the saved configuration.
-    MyUSBJoystick js(
-        USB_VENDOR_ID, 
-        MAKE_USB_PRODUCT_ID(USB_VENDOR_ID, USB_PRODUCT_ID, cfg.d.ledWizUnitNo),
-        USB_VERSION_NO);
+    MyUSBJoystick js(cfg.usbVendorID, cfg.usbProductID, USB_VERSION_NO, true, cfg.joystickEnabled, kbKeys);
         
     // 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();
+    
+    // set the initial status flags
+    statusFlags = (cfg.plunger.enabled ? 0x01 : 0x00);
 
     // 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));
+    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));
 
-    // plunger calibration button debounce timer
-    Timer calBtnTimer;
+    // initialize the calibration button 
     calBtnTimer.start();
-    int calBtnLit = false;
-    
-    // Calibration button state:
-    //  0 = not pushed
-    //  1 = pushed, not yet debounced
-    //  2 = pushed, debounced, waiting for hold time
-    //  3 = pushed, hold time completed - in calibration mode
-    int calBtnState = 0;
+    calBtnState = 0;
     
     // set up a timer for our heartbeat indicator
     Timer hbTimer;
@@ -1823,18 +2232,10 @@
     // create the accelerometer object
     Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, MMA8451_INT_PIN);
     
-#ifdef ENABLE_JOYSTICK
     // 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;
     
-    // flag: send a pixel dump after the next read
-    bool reportPix = false;
-#endif
-
-    // create our plunger sensor object
-    PlungerSensor plungerSensor;
-
     // last plunger report position, in 'npix' normalized pixel units
     int pos = 0;
     
@@ -1869,6 +2270,9 @@
     //       the park position from state 0)
     int lbState = 0;
     
+    // button bit for ZB launch ball button
+    const uint32_t lbButtonBit = (1 << (cfg.plunger.zbLaunchBall.btn - 1));
+    
     // Time since last lbState transition.  Some of the states are time-
     // sensitive.  In the "uncocked" state, we'll return to state 0 if
     // we remain in this state for more than a few milliseconds, since
@@ -1929,17 +2333,7 @@
     int firing = 0;
 
     // start the first CCD integration cycle
-    plungerSensor.init();
-    
-    // Device status.  We report this on each update so that the host config
-    // tool can detect our current settings.  This is a bit mask consisting
-    // of these bits:
-    //    0x0001  -> plunger sensor enabled
-    //    0x8000  -> RESERVED - must always be zero
-    //
-    // Note that the high bit (0x8000) must always be 0, since we use that
-    // to distinguish special request reply packets.
-    uint16_t statusFlags = (cfg.d.plungerEnabled ? 0x01 : 0x00);
+    plungerSensor->init();
     
     // we're all set up - now just loop, processing sensor reports and 
     // host requests
@@ -1954,224 +2348,7 @@
         HID_REPORT report;
         for (int rr = 0 ; rr < 4 && js.readNB(&report) ; ++rr, wait_ms(1))
         {
-            // all Led-Wiz reports are 8 bytes exactly
-            if (report.length == 8)
-            {
-                // LedWiz commands come in two varieties:  SBA and PBA.  An
-                // SBA is marked by the first byte having value 64 (0x40).  In
-                // the real LedWiz protocol, any other value in the first byte
-                // means it's a PBA message.  However, *valid* PBA messages
-                // always have a first byte (and in fact all 8 bytes) in the
-                // range 0-49 or 129-132.  Anything else is invalid.  We take
-                // advantage of this to implement private protocol extensions.
-                // So our full protocol is as follows:
-                //
-                // first byte =
-                //   0-48     -> LWZ-PBA
-                //   64       -> LWZ SBA 
-                //   65       -> private control message; second byte specifies subtype
-                //   129-132  -> LWZ-PBA
-                //   200-219  -> extended bank brightness set for outputs N to N+6, where
-                //               N is (first byte - 200)*7
-                //   other    -> reserved for future use
-                //
-                uint8_t *data = report.data;
-                if (data[0] == 64) 
-                {
-                    // LWZ-SBA - first four bytes are bit-packed on/off flags
-                    // for the outputs; 5th byte is the pulse speed (1-7)
-                    //printf("LWZ-SBA %02x %02x %02x %02x ; %02x\r\n",
-                    //       data[1], data[2], data[3], data[4], data[5]);
-    
-                    // update all on/off states
-                    for (int i = 0, bit = 1, ri = 1 ; i < 32 ; ++i, bit <<= 1)
-                    {
-                        // figure the on/off state bit for this output
-                        if (bit == 0x100) {
-                            bit = 1;
-                            ++ri;
-                        }
-                        
-                        // set the on/off state
-                        wizOn[i] = ((data[ri] & bit) != 0);
-                        
-                        // If the wizVal setting is 255, it means that this
-                        // output was last set to a brightness value with the
-                        // extended protocol.  Return it to LedWiz control by
-                        // rescaling the brightness setting to the LedWiz range
-                        // and updating wizVal with the result.  If it's any
-                        // other value, it was previously set by a PBA message,
-                        // so simply retain the last setting - in the normal
-                        // LedWiz protocol, the "profile" (brightness) and on/off
-                        // states are independent, so an SBA just turns an output
-                        // on or off but retains its last brightness level.
-                        if (wizVal[i] == 255)
-                            wizVal[i] = (uint8_t)round(outLevel[i]*48);
-                    }
-                    
-                    // set the flash speed - enforce the value range 1-7
-                    wizSpeed = data[5];
-                    if (wizSpeed < 1)
-                        wizSpeed = 1;
-                    else if (wizSpeed > 7)
-                        wizSpeed = 7;
-        
-                    // update the physical outputs
-                    updateWizOuts();
-                    hc595.update();
-                    
-                    // reset the PBA counter
-                    pbaIdx = 0;
-                }
-                else if (data[0] == 65)
-                {
-                    // Private control message.  This isn't an LedWiz message - it's
-                    // an extension for this device.  65 is an invalid PBA setting,
-                    // and isn't used for any other LedWiz message, so we appropriate
-                    // it for our own private use.  The first byte specifies the 
-                    // message type.
-                    if (data[1] == 1)
-                    {
-                        // 1 = Set Configuration:
-                        //     data[2] = LedWiz unit number (0x00 to 0x0f)
-                        //     data[3] = feature enable bit mask:
-                        //               0x01 = enable plunger sensor
-                        
-                        // we'll need a reset if the LedWiz unit number is changing
-                        uint8_t newUnitNo = data[2] & 0x0f;
-                        needReset |= (newUnitNo != cfg.d.ledWizUnitNo);
-                        
-                        // set the configuration parameters from the message
-                        cfg.d.ledWizUnitNo = newUnitNo;
-                        cfg.d.plungerEnabled = data[3] & 0x01;
-                        
-                        // update the status flags
-                        statusFlags = (statusFlags & ~0x01) | (data[3] & 0x01);
-                        
-                        // if the ccd is no longer enabled, use 0 for z reports
-                        if (!cfg.d.plungerEnabled)
-                            z = 0;
-                        
-                        // save the configuration
-                        cfg.save(iap, flash_addr);
-                    }
-#ifdef ENABLE_JOYSTICK
-                    else if (data[1] == 2)
-                    {
-                        // 2 = Calibrate plunger
-                        // (No parameters)
-                        
-                        // enter calibration mode
-                        calBtnState = 3;
-                        calBtnTimer.reset();
-                        cfg.resetPlunger();
-                    }
-                    else if (data[1] == 3)
-                    {
-                        // 3 = pixel dump
-                        // (No parameters)
-                        reportPix = true;
-                        
-                        // show purple until we finish sending the report
-                        ledR = 0;
-                        ledB = 0;
-                        ledG = 1;
-                    }
-                    else if (data[1] == 4)
-                    {
-                        // 4 = hardware configuration query
-                        // (No parameters)
-                        wait_ms(1);
-                        js.reportConfig(numOutputs, cfg.d.ledWizUnitNo);
-                    }
-                    else if (data[1] == 5)
-                    {
-                        // 5 = all outputs off, reset to LedWiz defaults
-                        allOutputsOff();
-                    }
-#endif // ENABLE_JOYSTICK
-                }
-                else if (data[0] >= 200 && data[0] < 220)
-                {
-                    // Extended protocol - banked brightness update.  
-                    // data[0]-200 gives us the bank of 7 outputs we're setting:
-                    // 200 is outputs 0-6, 201 is outputs 7-13, 202 is 14-20, etc.
-                    // The remaining bytes are brightness levels, 0-255, for the
-                    // seven outputs in the selected bank.  The LedWiz flashing 
-                    // modes aren't accessible in this message type; we can only 
-                    // set a fixed brightness, but in exchange we get 8-bit 
-                    // resolution rather than the paltry 0-48 scale that the real
-                    // LedWiz uses.  There's no separate on/off status for outputs
-                    // adjusted with this message type, either, as there would be
-                    // for a PBA message - setting a non-zero value immediately
-                    // turns the output, overriding the last SBA setting.
-                    //
-                    // For outputs 0-31, this overrides any previous PBA/SBA
-                    // settings for the port.  Any subsequent PBA/SBA message will
-                    // in turn override the setting made here.  It's simple - the
-                    // most recent message of either type takes precedence.  For
-                    // outputs above the LedWiz range, PBA/SBA messages can't
-                    // address those ports anyway.
-                    int i0 = (data[0] - 200)*7;
-                    int i1 = i0 + 7 < numOutputs ? i0 + 7 : numOutputs; 
-                    for (int i = i0 ; i < i1 ; ++i)
-                    {
-                        // set the brightness level for the output
-                        float b = data[i-i0+1]/255.0;
-                        outLevel[i] = b;
-                        
-                        // if it's in the basic LedWiz output set, set the LedWiz
-                        // profile value to 255, which means "use outLevel"
-                        if (i < 32) 
-                            wizVal[i] = 255;
-                            
-                        // set the output
-                        lwPin[i]->set(b);
-                    }
-                    
-                    // update 74HC595 outputs, if attached
-                    hc595.update();
-                }
-                else 
-                {
-                    // Everything else is LWZ-PBA.  This is a full "profile"
-                    // dump from the host for one bank of 8 outputs.  Each
-                    // byte sets one output in the current bank.  The current
-                    // bank is implied; the bank starts at 0 and is reset to 0
-                    // by any LWZ-SBA message, and is incremented to the next
-                    // bank by each LWZ-PBA message.  Our variable pbaIdx keeps
-                    // track of our notion of the current bank.  There's no direct
-                    // way for the host to select the bank; it just has to count
-                    // on us staying in sync.  In practice, the host will always
-                    // send a full set of 4 PBA messages in a row to set all 32
-                    // outputs.
-                    //
-                    // Note that a PBA implicitly overrides our extended profile
-                    // messages (message prefix 200-219), because this sets the
-                    // wizVal[] entry for each output, and that takes precedence
-                    // over the extended protocol settings.
-                    //
-                    //printf("LWZ-PBA[%d] %02x %02x %02x %02x %02x %02x %02x %02x\r\n",
-                    //       pbaIdx, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
-    
-                    // Update all output profile settings
-                    for (int i = 0 ; i < 8 ; ++i)
-                        wizVal[pbaIdx + i] = data[i];
-    
-                    // Update the physical LED state if this is the last bank.
-                    // Note that hosts always send a full set of four PBA
-                    // messages, so there's no need to do a physical update
-                    // until we've received the last bank's PBA message.
-                    if (pbaIdx == 24)
-                    {
-                        updateWizOuts();
-                        hc595.update();
-                        pbaIdx = 0;
-                    }
-                    else
-                        pbaIdx += 8;
-                }
-            }
+            handleInputMsg(report, js, z);
         }
        
         // check for plunger calibration
@@ -2201,7 +2378,9 @@
                     // enter calibration mode
                     calBtnState = 3;
                     calBtnTimer.reset();
-                    cfg.resetPlunger();
+                    
+                    // reset the plunger calibration limits
+                    cfg.plunger.cal.reset(plungerSensor->npix);
                 }
                 break;
                 
@@ -2227,8 +2406,8 @@
                 calBtnState = 0;
                 
                 // save the updated configuration
-                cfg.d.plungerCal = 1;
-                cfg.save(iap, flash_addr);
+                cfg.plunger.cal.calibrated = 1;
+                saveConfigToFlash();
             }
             else if (calBtnState != 3)
             {
@@ -2282,25 +2461,26 @@
         // 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)
+        if (!firing && cfg.plunger.enabled && 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)
+                int pos0;
+                if (plungerSensor->lowResScan(pos0) && pos0 <= cfg.plunger.cal.zero)
                     firing = 1;
             }
         }
 
         // read the plunger sensor, if it's enabled
-        if (cfg.d.plungerEnabled)
+        if (cfg.plunger.enabled)
         {
             // start with the previous reading, in case we don't have a
             // clear result on this frame
             int znew = z;
-            if (plungerSensor.highResScan(pos))
+            if (plungerSensor->highResScan(pos))
             {
                 // We got a new reading.  If we're in calibration mode, use it
                 // to figure the new calibration, otherwise adjust the new reading
@@ -2309,15 +2489,15 @@
                 {
                     // 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;
+                    if (pos < cfg.plunger.cal.min)
+                        cfg.plunger.cal.min = pos;
+                    if (pos < cfg.plunger.cal.zero)
+                        cfg.plunger.cal.zero = pos;
+                    if (pos > cfg.plunger.cal.max)
+                        cfg.plunger.cal.max = pos;
                         
                     // normalize to the full physical range while calibrating
-                    znew = int(round(float(pos)/npix * JOYMAX));
+                    znew = int(round(float(pos)/plungerSensor->npix * JOYMAX));
                 }
                 else
                 {
@@ -2329,10 +2509,10 @@
                     // plunger has a small amount of travel in the "push" direction,
                     // since the barrel spring can be compressed slightly.  Negative
                     // values represent travel in the push direction.
-                    if (pos > cfg.d.plungerMax)
-                        pos = cfg.d.plungerMax;
-                    znew = int(round(float(pos - cfg.d.plungerZero)
-                        / (cfg.d.plungerMax - cfg.d.plungerZero + 1) * JOYMAX));
+                    if (pos > cfg.plunger.cal.max)
+                        pos = cfg.plunger.cal.max;
+                    znew = int(round(float(pos - cfg.plunger.cal.zero)
+                        / (cfg.plunger.cal.max - cfg.plunger.cal.zero + 1) * JOYMAX));
                 }
             }
 
@@ -2348,54 +2528,59 @@
                 // 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 pos0 = plungerSensor.lowResScan();
-                int pos1 = pos0;
-                Timer tw;
-                tw.start();
-                while (tw.read_ms() < 6)
+                int pos0;
+                if (plungerSensor->lowResScan(pos0))
                 {
-                    // read the new position
-                    int pos2 = plungerSensor.lowResScan();
-                    
-                    // If it's stable over consecutive readings, stop looping.
-                    // (Count it as stable if the position is within about 1/8".
-                    // pos1 and pos2 are reported in pixels, so they range from
-                    // 0 to npix.  The overall travel of a standard plunger is
-                    // about 3.2", so we have (npix/3.2) pixels per inch, hence
-                    // 1/8" is (npix/3.2)*(1/8) pixels.)
-                    if (abs(pos2 - pos1) < int(npix/(3.2*8)))
-                        break;
-
-                    // If we've crossed the rest position, and we've moved by
-                    // a minimum distance from where we starting this loop, begin
-                    // a firing event.  (We require a minimum distance to prevent
-                    // spurious firing from random analog noise in the readings
-                    // when the plunger is actually just sitting still at the 
-                    // rest position.  If it's at rest, it's normal to see small
-                    // random fluctuations in the analog reading +/- 1% or so
-                    // from the 0 point, especially with a sensor like a
-                    // potentionemeter that reports the position as a single 
-                    // analog voltage.)  Note that we compare the latest reading
-                    // to the first reading of the loop - we don't require the
-                    // threshold motion over consecutive readings, but any time
-                    // over the stability wait loop.
-                    if (pos1 < cfg.d.plungerZero
-                        && abs(pos2 - pos0) > int(npix/(3.2*8)))
+                    int pos1 = pos0;
+                    Timer tw;
+                    tw.start();
+                    while (tw.read_ms() < 6)
                     {
-                        firing = 1;
-                        break;
+                        // read the new position
+                        int pos2;
+                        if (plungerSensor->lowResScan(pos2))
+                        {
+                            // If it's stable over consecutive readings, stop looping.
+                            // (Count it as stable if the position is within about 1/8".
+                            // pos1 and pos2 are reported in pixels, so they range from
+                            // 0 to npix.  The overall travel of a standard plunger is
+                            // about 3.2", so we have (npix/3.2) pixels per inch, hence
+                            // 1/8" is (npix/3.2)*(1/8) pixels.)
+                            if (abs(pos2 - pos1) < int(plungerSensor->npix/(3.2*8)))
+                                break;
+        
+                            // If we've crossed the rest position, and we've moved by
+                            // a minimum distance from where we starting this loop, begin
+                            // a firing event.  (We require a minimum distance to prevent
+                            // spurious firing from random analog noise in the readings
+                            // when the plunger is actually just sitting still at the 
+                            // rest position.  If it's at rest, it's normal to see small
+                            // random fluctuations in the analog reading +/- 1% or so
+                            // from the 0 point, especially with a sensor like a
+                            // potentionemeter that reports the position as a single 
+                            // analog voltage.)  Note that we compare the latest reading
+                            // to the first reading of the loop - we don't require the
+                            // threshold motion over consecutive readings, but any time
+                            // over the stability wait loop.
+                            if (pos1 < cfg.plunger.cal.zero
+                                && abs(pos2 - pos0) > int(plungerSensor->npix/(3.2*8)))
+                            {
+                                firing = 1;
+                                break;
+                            }
+                                                    
+                            // the new reading is now the prior reading
+                            pos1 = pos2;
+                        }
                     }
-                                            
-                    // the new reading is now the prior reading
-                    pos1 = pos2;
                 }
             }
             
             // Check for a simulated Launch Ball button press, if enabled
-            if (ZBLaunchBallPort != 0)
+            if (cfg.plunger.zbLaunchBall.port != 0)
             {
                 const int cockThreshold = JOYMAX/3;
-                const int pushThreshold = int(-JOYMAX/3 * LaunchBallPushDistance);
+                const int pushThreshold = int(-JOYMAX/3 * cfg.plunger.zbLaunchBall.pushDistance);
                 int newState = lbState;
                 switch (lbState)
                 {
@@ -2466,14 +2651,13 @@
                 }
                 
                 // change states if desired
-                const uint32_t lbButtonBit = (1 << (LaunchBallButton - 1));
                 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[ZBLaunchBallPort-1])
+                        && wizOn[cfg.plunger.zbLaunchBall.port-1])
                     {
                         lbBtnTimer.reset();
                         lbBtnTimer.start();
@@ -2482,7 +2666,7 @@
                     
                     // if we're switching to state 0, release the button
                     if (newState == 0)
-                        simButtons &= ~(1 << (LaunchBallButton - 1));
+                        simButtons &= ~(1 << (cfg.plunger.zbLaunchBall.btn - 1));
                     
                     // switch to the new state
                     lbState = newState;
@@ -2517,7 +2701,7 @@
                     int turnOff = false;
                     
                     // turn it off if the ZB Launch Ball signal is off
-                    if (!wizOn[ZBLaunchBallPort-1])
+                    if (!wizOn[cfg.plunger.zbLaunchBall.port-1])
                         turnOff = true;
                         
                     // also turn it off if we're in state 3 or 4 ("Launch"),
@@ -2618,16 +2802,15 @@
         }
 
         // update the buttons
-        uint32_t buttons = readButtons();
+        readButtons(cfg);
 
-#ifdef ENABLE_JOYSTICK
         // 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)
+        // so reporting more frequently creates I/O overhead without 
+        // doing anything to improve the simulation.
+        if (cfg.joystickEnabled && reportTimer.read_ms() > 15)
         {
             // read the accelerometer
             int xa, ya;
@@ -2648,13 +2831,33 @@
             // ZB Launch Ball turns off the plunger position because it
             // tells us that the table has a Launch Ball button instead of
             // a traditional plunger.
-            int zrep = (ZBLaunchBallPort != 0 && wizOn[ZBLaunchBallPort-1] ? 0 : z);
+            int zrep = (cfg.plunger.zbLaunchBall.port != 0 && wizOn[cfg.plunger.zbLaunchBall.port-1] ? 0 : z);
+            
+            // rotate X and Y according to the device orientation in the cabinet
+            accelRotate(x, y);
+
+            // send the joystick report
+            js.update(x, y, zrep, jsButtons | simButtons, statusFlags);
             
-            // Send the status report.  Note that we have to map the X and Y
-            // axes from the accelerometer to match the Windows joystick axes.
-            // The mapping is determined according to the mounting direction
-            // set in config.h via the ORIENTATION_xxx macros.
-            js.update(JOY_X(x,y), JOY_Y(x,y), zrep, buttons | simButtons, statusFlags);
+            // send the keyboard report(s), if applicable
+            bool waitBeforeMedia = false;
+            if (kbState.changed)
+            {
+                js.kbUpdate(kbState.data);
+                kbState.changed = false;
+                waitBeforeMedia = true;
+            }
+            if (mediaState.changed)
+            {
+                // just sent a key report - give the channel a moment to clear before 
+                // sending another report on its heels, to avoid clogging the pipe
+                if (waitBeforeMedia)
+                    wait_us(1);
+                    
+                // send the media key report
+                js.mediaUpdate(mediaState.data);
+                mediaState.changed = false;
+            }
             
             // we've just started a new report interval, so reset the timer
             reportTimer.reset();
@@ -2664,23 +2867,19 @@
         if (reportPix)
         {
             // send the report            
-            plungerSensor.sendExposureReport(js);
+            plungerSensor->sendExposureReport(js);
 
             // we have satisfied this request
             reportPix = false;
         }
         
-#else // ENABLE_JOYSTICK
-        // We're a secondary controller, with no joystick reporting.  Send
-        // a generic status report to the host periodically for the sake of
-        // the Windows config tool.
-        if (reportTimer.read_ms() > 200)
+        // If joystick reports are turned off, send a generic status report
+        // periodically for the sake of the Windows config tool.
+        if (!cfg.joystickEnabled && reportTimer.read_ms() > 200)
         {
             js.updateStatus(0);
         }
 
-#endif // ENABLE_JOYSTICK
-        
 #ifdef DEBUG_PRINTF
         if (x != 0 || y != 0)
             printf("%d,%d\r\n", x, y);
@@ -2727,16 +2926,7 @@
                     }
                 }
             }
-            else if (needReset)
-            {
-                // connected, need to reset due to changes in config parameters -
-                // flash red/green
-                hb = !hb;
-                ledR = (hb ? 0 : 1);
-                ledG = (hb ? 1 : 0);
-                ledB = 0;
-            }
-            else if (cfg.d.plungerEnabled && !cfg.d.plungerCal)
+            else if (cfg.plunger.enabled && !cfg.plunger.cal.calibrated)
             {
                 // connected, plunger calibration needed - flash yellow/green
                 hb = !hb;
--- a/nullSensor.h	Thu Dec 03 07:34:57 2015 +0000
+++ b/nullSensor.h	Sat Dec 19 06:37:19 2015 +0000
@@ -3,16 +3,19 @@
 // This file defines a class that provides the plunger sensor interface
 // that the main program expects, but with no physical sensor underneath.
 
-const int npix = JOYMAX;
+#ifndef NULLSENSOR_H
+#define NULLSENSOR_H
 
-class PlungerSensor
+#include "plunger.h"
+
+class PlungerSensorNull: public PlungerSensor
 {
 public:
-    PlungerSensor() { }
+    PlungerSensorNull() { }
     
-    void init() { }
-    int lowResScan() { return 0; }
-    bool highResScan(int &pos) { return false; }
-    void sendExposureReport(USBJoystick &) { }
+    virtual void init() { }
+    virtual bool lowResScan(int &pos) { return false; }
+    virtual bool highResScan(int &pos) { return false; }
 };
 
+#endif /* NULLSENSOR_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nvm.h	Sat Dec 19 06:37:19 2015 +0000
@@ -0,0 +1,111 @@
+// NVM - Non-Volatile Memory
+//
+// This module handles the storage of our configuration settings
+// and calibration data in flash memory, which allows us to
+// retrieve these settings after each power cycle.
+
+
+#ifndef NVM_H
+#define NVM_H
+
+#include "config.h"
+#include "FreescaleIAP.h"
+
+
+// Non-volatile memory (NVM) structure
+//
+// This structure defines the layout of our saved configuration
+// and calibration data in flash memory.
+//
+// Hack alert!
+// Our use of flash for this purpose is ad hoc and not supported
+// by the mbed platform.  mbed doesn't impose a file system on the
+// KL25Z flash; it simply treats the flash as a raw storage space
+// and assumes that the linker output is the only thing using it.
+// So if we want to use the flash, we basically have to do it on 
+// the sly, by using space that the linker happens to leave unused.
+// Fortunately, it's fairly easy to do this, because the flash
+// is mapped in the obvious way, as a single contiguous block in 
+// the CPU memory space, and the linker does the obvious thing, 
+// storing its entire output in a single contiguous block starting
+// at the lowest flash address.  This means that all flash memory
+// from (lowest flash address + length of linker output) to
+// (highest flash address) is unused and available for our sneaky
+// system.  Unfortunately, there's no reliable way for the program
+// to determine the length of the linker output, so we can't know 
+// where our available region starts.  But we do know how much flash 
+// there is overall, so we know where the flash ends.  We can 
+// therefore align our storage region at the end of memory and hope 
+// that it's small enough not to encroach on the linker space.  We
+// can actually do a little better than hope: the mbed tools tell us 
+// at the UI level how much flash the linker is using, even though it 
+// doesn't expose that information to us programmatically, so we can 
+// manually check that we have enough room.  As of this writing, the 
+// configuration structure is much much smaller than the available 
+// leftover flash space, so we should be safe indefinitely, barring 
+// a major expansion of the configuration structure or code size.
+//
+// The boot loader seems to erase the entire flash space every time
+// we load new firmware, so our configuration structure is lost
+// when we update.  Furthermore, since we explicitly choose to put
+// the config structure in space that isn't initialized by the linker,
+// we can't specify the new contents stored on these erasure events.
+// To deal with this, we use a signature and checksum to check the
+// integrity of the stored data.  The erasure leaves deterministic
+// values in memory unused by the linker, so we'll always detect
+// an uninitialized config structure after an update.
+//
+struct NVM
+{
+public:
+    // checksum - we use this to determine if the flash record
+    // has been properly initialized
+    uint32_t checksum;
+
+    // signature and version reference values
+    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
+        d.sig = SIGNATURE;
+        d.vsn = VERSION;
+        d.sz = sizeof(NVM);
+        checksum = CRC32(&d, sizeof(d));
+        
+        // figure the number of sectors required
+        int sectors = (sizeof(NVM) + SECTOR_SIZE - 1) / SECTOR_SIZE;
+        for (int i = 0 ; i < sectors ; ++i)
+            iap.erase_sector(addr + i*SECTOR_SIZE);
+
+        // save the data
+        iap.program_flash(addr, this, sizeof(*this));
+    }
+    
+    // stored data (excluding the checksum)
+    struct
+    {
+        // Signature, structure version, and structure size, as further
+        // verification that we have valid data.
+        uint32_t sig;
+        uint16_t vsn;
+        int sz;
+        
+        // configuration and calibration data
+        Config c;
+    } d;
+};
+
+#endif /* NVM_M */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plunger.h	Sat Dec 19 06:37:19 2015 +0000
@@ -0,0 +1,79 @@
+// Plunger Sensor Interface
+//
+// This module defines the abstract interface to the plunger sensors.
+// We support several different physical sensor types, so we need a
+// common interface for use in the main code.
+//
+
+#ifndef PLUNGER_H
+#define PLUNGER_H
+
+class PlungerSensor
+{
+public:
+
+    PlungerSensor() { }
+    virtual ~PlungerSensor() { }
+    
+    // Number of "pixels" in the sensor's range.  We use the term "pixels"
+    // here for historical reasons, namely that the first sensor we implemented
+    // was an imaging sensor that physically sensed the plunger position as
+    // a pixel coordinate in the image.  But it's no longer the right word,
+    // since we support sensor types that have nothing to do with imaging.
+    // Even so, the function this serves is still applicable.  Abstractly,
+    // it represents the physical resolution of the sensor, by giving the
+    // total number of quanta that the sensor can resolve over the entire 
+    // range of travel of the plunger.  For devices that inherently quantize
+    // the position reading at the physical level, such as imaging sensors 
+    // and quadrature sensors, this should be set to the total number of
+    // quanta (resolvable position steps) over the range of travel.  For
+    // devices with physically analog outputs, such as potentiometers or
+    // LVDTs, the reading still has to be digitized for us to be able to
+    // work with it, but this happens invisibly in the ADC, so the "pixel" 
+    // scale is essentially arbitrary.  Analog sensor types should thus 
+    // simply use the maximum joystick report range, since that's the
+    // final scale we have to convert to - using a different scale would
+    // have no benefit and would just introduce rounding errors.
+    //
+    // This value MUST be initialized in the constructor.
+    int npix;
+         
+    // Initialize the physical sensor device.  This is called at startup
+    // to set up the device for first use.
+    virtual void init() = 0;
+
+    // Take a high-resolution reading.  Sets pos to the current position,
+    // on a scale from 0 to npix:  0 is the maximum forward plunger position,
+    // and npix is the maximum retracted position, in terms of the sensor's
+    // extremes.  This is a raw reading in terms of the sensor range; the
+    // caller is responsible for applying calibration data and scaling the
+    // result to the the joystick report range.
+    //
+    // Returns true on success, false on failure.  Return false if it wasn't
+    // possible to take a good reading for any reason.
+    virtual bool highResScan(int &pos) = 0;
+
+    // Take a low-resolution reading.  This reports the result on the same
+    // 0..npix scale as highResScan().  Returns true on success, false on
+    // failure.
+    //
+    // The difference between the high-res and low-res scans is the amount 
+    // of time it takes to complete the reading.  The high-res scan is allowed
+    // to take about 10ms; a low-res scan take less than 1ms.  For many
+    // sensors, either of these time scales would yield identical resolution;
+    // if that's the case, simply take a reading the same way in both functions.
+    // The distinction is for the benefit of sensors that need significantly
+    // longer to read at higher resolutions, such as image sensors that have
+    // to sample pixels serially.
+    virtual bool lowResScan(int &pos) = 0;
+        
+    // Send an exposure report to the joystick interface.  This is specifically
+    // for image sensors, and should be omitted by other sensor types.  For
+    // image sensors, this takes one exposure and sends all pixels to the host
+    // through special joystick reports.  This is used for PC-side testing tools
+    // to let the user check the sensor installation by directly viewing its
+    // pixel output.
+    virtual void sendExposureReport(class USBJoystick &js) { }
+};
+
+#endif /* PLUNGER_H */
--- a/potSensor.h	Thu Dec 03 07:34:57 2015 +0000
+++ b/potSensor.h	Sat Dec 19 06:37:19 2015 +0000
@@ -5,30 +5,29 @@
 
 #include "FastAnalogIn.h"
 
-// The potentiometer doesn't have pixels, but we still need an
-// integer range for normalizing our digitized voltage level values.
-// The number here is fairly arbitrary; the higher it is, the finer
-// the digitized steps.  A 40" 1080p HDTV has about 55 pixels per inch
-// on its physical display, so if the on-screen plunger is displayed
-// at roughly the true physical size, it's about 3" on screen or about
-// 165 pixels.  So the minimum quantization size here should be about
-// the same.  For the pot sensor, this is just a scaling factor, 
-// so higher values don't cost us anything (unlike the CCD, where the
-// read time is proportional to the number of pixels we sample).
-const int npix = 4096;
-
-class PlungerSensor
+class PlungerSensorPot: public PlungerSensor
 {
 public:
-    PlungerSensor() : pot(POT_PIN)
+    PlungerSensorPot(PinName ao) : pot(ao)
     {
     }
     
-    void init() 
+    virtual void init() 
     {
+        // The potentiometer doesn't have pixels, but we still need an
+        // integer range for normalizing our digitized voltage level values.
+        // The number here is fairly arbitrary; the higher it is, the finer
+        // the digitized steps.  A 40" 1080p HDTV has about 55 pixels per inch
+        // on its physical display, so if the on-screen plunger is displayed
+        // at roughly the true physical size, it's about 3" on screen or about
+        // 165 pixels.  So the minimum quantization size here should be about
+        // the same.  For the pot sensor, this is just a scaling factor, 
+        // so higher values don't cost us anything (unlike the CCD, where the
+        // read time is proportional to the number of pixels we sample).
+        npix = 4096;
     }
     
-    bool highResScan(int &pos)
+    virtual bool highResScan(int &pos)
     {
         // Take a few readings and use the average, to reduce the effect
         // of analog voltage fluctuations.  The voltage range on the ADC
@@ -43,20 +42,17 @@
         return true;
     }
     
-    int lowResScan()
+    virtual bool lowResScan(int &pos)
     {
         // Use an average of several readings.  Note that even though this
         // is nominally a "low res" scan, we can still afford to take an
         // average.  The point of the low res interface is speed, and since
         // we only have one analog value to read, we can afford to take
         // several samples here even in the low res case.
-        return int((pot.read() + pot.read() + pot.read())/3.0 * npix);
+        pos = int((pot.read() + pot.read() + pot.read())/3.0 * npix);
+        return true;
     }
         
-    void sendExposureReport(USBJoystick &) 
-    { 
-    }
-    
 private:
     AnalogIn pot;
 };